Icon.java revision 09e51a739f3e408567f0eefafcc5fedb01bc3401
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.graphics.drawable; 18 19import android.annotation.DrawableRes; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.pm.PackageManager; 23import android.content.res.Resources; 24import android.graphics.Bitmap; 25import android.graphics.BitmapFactory; 26import android.net.Uri; 27import android.os.AsyncTask; 28import android.os.Handler; 29import android.os.Message; 30import android.os.Parcel; 31import android.os.Parcelable; 32import android.util.Log; 33 34import java.io.DataInputStream; 35import java.io.DataOutputStream; 36import java.io.File; 37import java.io.FileInputStream; 38import java.io.FileNotFoundException; 39import java.io.IOException; 40import java.io.InputStream; 41import java.io.OutputStream; 42 43/** 44 * An umbrella container for several serializable graphics representations, including Bitmaps, 45 * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors). 46 * 47 * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a> 48 * has been spilled on the best way to load images, and many clients may have different needs when 49 * it comes to threading and fetching. This class is therefore focused on encapsulation rather than 50 * behavior. 51 */ 52 53public final class Icon implements Parcelable { 54 private static final String TAG = "Icon"; 55 56 private static final int TYPE_BITMAP = 1; 57 private static final int TYPE_RESOURCE = 2; 58 private static final int TYPE_DATA = 3; 59 private static final int TYPE_URI = 4; 60 61 private static final int VERSION_STREAM_SERIALIZER = 1; 62 63 private final int mType; 64 65 // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed 66 // based on the value of mType. 67 68 // TYPE_BITMAP: Bitmap 69 // TYPE_RESOURCE: Resources 70 // TYPE_DATA: DataBytes 71 private Object mObj1; 72 73 // TYPE_RESOURCE: package name 74 // TYPE_URI: uri string 75 private String mString1; 76 77 // TYPE_RESOURCE: resId 78 // TYPE_DATA: data length 79 private int mInt1; 80 81 // TYPE_DATA: data offset 82 private int mInt2; 83 84 // Internal accessors for different mType variants 85 private Bitmap getBitmap() { 86 if (mType != TYPE_BITMAP) { 87 throw new IllegalStateException("called getBitmap() on " + this); 88 } 89 return (Bitmap) mObj1; 90 } 91 92 private int getDataLength() { 93 if (mType != TYPE_DATA) { 94 throw new IllegalStateException("called getDataLength() on " + this); 95 } 96 synchronized (this) { 97 return mInt1; 98 } 99 } 100 101 private int getDataOffset() { 102 if (mType != TYPE_DATA) { 103 throw new IllegalStateException("called getDataOffset() on " + this); 104 } 105 synchronized (this) { 106 return mInt2; 107 } 108 } 109 110 private byte[] getDataBytes() { 111 if (mType != TYPE_DATA) { 112 throw new IllegalStateException("called getDataBytes() on " + this); 113 } 114 synchronized (this) { 115 return (byte[]) mObj1; 116 } 117 } 118 119 private Resources getResources() { 120 if (mType != TYPE_RESOURCE) { 121 throw new IllegalStateException("called getResources() on " + this); 122 } 123 return (Resources) mObj1; 124 } 125 126 private String getResPackage() { 127 if (mType != TYPE_RESOURCE) { 128 throw new IllegalStateException("called getResPackage() on " + this); 129 } 130 return mString1; 131 } 132 133 private int getResId() { 134 if (mType != TYPE_RESOURCE) { 135 throw new IllegalStateException("called getResId() on " + this); 136 } 137 return mInt1; 138 } 139 140 private String getUriString() { 141 if (mType != TYPE_URI) { 142 throw new IllegalStateException("called getUriString() on " + this); 143 } 144 return mString1; 145 } 146 147 private Uri getUri() { 148 return Uri.parse(getUriString()); 149 } 150 151 // Convert a int32 into a four-char string 152 private static final String typeToString(int x) { 153 switch (x) { 154 case TYPE_BITMAP: return "BITMAP"; 155 case TYPE_DATA: return "DATA"; 156 case TYPE_RESOURCE: return "RESOURCE"; 157 case TYPE_URI: return "URI"; 158 default: return "UNKNOWN"; 159 } 160 } 161 162 /** 163 * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler} 164 * and then sends <code>andThen</code> to the same Handler when finished. 165 * 166 * @param context {@link android.content.Context Context} in which to load the drawable; see 167 * {@link #loadDrawable(Context)} 168 * @param andThen {@link android.os.Message} to send to its target once the drawable 169 * is available. The {@link android.os.Message#obj obj} 170 * property is populated with the Drawable. 171 */ 172 public void loadDrawableAsync(Context context, Message andThen) { 173 if (andThen.getTarget() == null) { 174 throw new IllegalArgumentException("callback message must have a target handler"); 175 } 176 new LoadDrawableTask(context, andThen).runAsync(); 177 } 178 179 /** 180 * Invokes {@link #loadDrawable(Context)} on a background thread 181 * and then runs <code>andThen</code> on the UI thread when finished. 182 * 183 * @param context {@link Context Context} in which to load the drawable; see 184 * {@link #loadDrawable(Context)} 185 * @param listener a callback to run on the provided 186 * @param handler {@link Handler} on which to run <code>andThen</code>. 187 */ 188 public void loadDrawableAsync(Context context, final OnDrawableLoadedListener listener, 189 Handler handler) { 190 new LoadDrawableTask(context, handler, listener).runAsync(); 191 } 192 193 /** 194 * Returns a Drawable that can be used to draw the image inside this Icon, constructing it 195 * if necessary. Depending on the type of image, this may not be something you want to do on 196 * the UI thread, so consider using 197 * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead. 198 * 199 * @param context {@link android.content.Context Context} in which to load the drawable; used 200 * to access {@link android.content.res.Resources Resources}, for example. 201 * @return A fresh instance of a drawable for this image, yours to keep. 202 */ 203 public Drawable loadDrawable(Context context) { 204 switch (mType) { 205 case TYPE_BITMAP: 206 return new BitmapDrawable(context.getResources(), getBitmap()); 207 case TYPE_RESOURCE: 208 if (getResources() == null) { 209 if (getResPackage() == null || "android".equals(getResPackage())) { 210 mObj1 = Resources.getSystem(); 211 } else { 212 final PackageManager pm = context.getPackageManager(); 213 try { 214 mObj1 = pm.getResourcesForApplication(getResPackage()); 215 } catch (PackageManager.NameNotFoundException e) { 216 Log.e(TAG, String.format("Unable to find pkg=%s", 217 getResPackage()), 218 e); 219 break; 220 } 221 } 222 } 223 try { 224 return getResources().getDrawable(getResId(), context.getTheme()); 225 } catch (RuntimeException e) { 226 Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s", 227 getResId(), 228 getResPackage()), 229 e); 230 } 231 case TYPE_DATA: 232 return new BitmapDrawable(context.getResources(), 233 BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) 234 ); 235 case TYPE_URI: 236 final Uri uri = getUri(); 237 final String scheme = uri.getScheme(); 238 InputStream is = null; 239 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 240 || ContentResolver.SCHEME_FILE.equals(scheme)) { 241 try { 242 is = context.getContentResolver().openInputStream(uri); 243 } catch (Exception e) { 244 Log.w(TAG, "Unable to load image from URI: " + uri, e); 245 } 246 } else { 247 try { 248 is = new FileInputStream(new File(mString1)); 249 } catch (FileNotFoundException e) { 250 Log.w(TAG, "Unable to load image from path: " + uri, e); 251 } 252 } 253 if (is != null) { 254 return new BitmapDrawable(context.getResources(), 255 BitmapFactory.decodeStream(is)); 256 } 257 break; 258 } 259 return null; 260 } 261 262 /** 263 * Load the requested resources under the given userId, if the system allows it, 264 * before actually loading the drawable. 265 * 266 * @hide 267 */ 268 public Drawable loadDrawableAsUser(Context context, int userId) { 269 if (mType == TYPE_RESOURCE) { 270 if (getResources() == null 271 && getResPackage() != null 272 && !(getResPackage().equals("android"))) { 273 final PackageManager pm = context.getPackageManager(); 274 try { 275 mObj1 = pm.getResourcesForApplicationAsUser(getResPackage(), userId); 276 } catch (PackageManager.NameNotFoundException e) { 277 Log.e(TAG, String.format("Unable to find pkg=%s user=%d", 278 getResPackage(), 279 userId), 280 e); 281 } 282 } 283 } 284 return loadDrawable(context); 285 } 286 287 /** 288 * Writes a serialized version of an Icon to the specified stream. 289 * 290 * @param stream The stream on which to serialize the Icon. 291 * @hide 292 */ 293 public void writeToStream(OutputStream stream) throws IOException { 294 DataOutputStream dataStream = new DataOutputStream(stream); 295 296 dataStream.writeInt(VERSION_STREAM_SERIALIZER); 297 dataStream.writeByte(mType); 298 299 switch (mType) { 300 case TYPE_BITMAP: 301 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream); 302 break; 303 case TYPE_DATA: 304 dataStream.writeInt(getDataLength()); 305 dataStream.write(getDataBytes(), getDataOffset(), getDataLength()); 306 break; 307 case TYPE_RESOURCE: 308 dataStream.writeUTF(getResPackage()); 309 dataStream.writeInt(getResId()); 310 break; 311 case TYPE_URI: 312 dataStream.writeUTF(getUriString()); 313 break; 314 } 315 } 316 317 private Icon(int mType) { 318 this.mType = mType; 319 } 320 321 /** 322 * Create an Icon from the specified stream. 323 * 324 * @param stream The input stream from which to reconstruct the Icon. 325 * @hide 326 */ 327 public static Icon createFromStream(InputStream stream) throws IOException { 328 DataInputStream inputStream = new DataInputStream(stream); 329 330 final int version = inputStream.readInt(); 331 if (version >= VERSION_STREAM_SERIALIZER) { 332 final int type = inputStream.readByte(); 333 switch (type) { 334 case TYPE_BITMAP: 335 return createWithBitmap(BitmapFactory.decodeStream(inputStream)); 336 case TYPE_DATA: 337 final int length = inputStream.readInt(); 338 final byte[] data = new byte[length]; 339 inputStream.read(data, 0 /* offset */, length); 340 return createWithData(data, 0 /* offset */, length); 341 case TYPE_RESOURCE: 342 final String packageName = inputStream.readUTF(); 343 final int resId = inputStream.readInt(); 344 return createWithResource(packageName, resId); 345 case TYPE_URI: 346 final String uriOrPath = inputStream.readUTF(); 347 return createWithContentUri(uriOrPath); 348 } 349 } 350 return null; 351 } 352 353 /** 354 * Create an Icon pointing to a drawable resource. 355 * @param res Resources for a package containing the resource in question 356 * @param resId ID of the drawable resource 357 */ 358 public static Icon createWithResource(Resources res, @DrawableRes int resId) { 359 if (res == null) { 360 throw new IllegalArgumentException("Resource must not be null."); 361 } 362 final Icon rep = new Icon(TYPE_RESOURCE); 363 rep.mObj1 = res; 364 rep.mInt1 = resId; 365 rep.mString1 = res.getResourcePackageName(resId); 366 return rep; 367 } 368 369 /** 370 * Create an Icon pointing to a drawable resource. 371 * @param resPackage Name of the package containing the resource in question 372 * @param resId ID of the drawable resource 373 */ 374 public static Icon createWithResource(String resPackage, @DrawableRes int resId) { 375 if (resPackage == null) { 376 throw new IllegalArgumentException("Resource package name must not be null."); 377 } 378 final Icon rep = new Icon(TYPE_RESOURCE); 379 rep.mInt1 = resId; 380 rep.mString1 = resPackage; 381 return rep; 382 } 383 384 /** 385 * Create an Icon pointing to a bitmap in memory. 386 * @param bits A valid {@link android.graphics.Bitmap} object 387 */ 388 public static Icon createWithBitmap(Bitmap bits) { 389 if (bits == null) { 390 throw new IllegalArgumentException("Bitmap must not be null."); 391 } 392 final Icon rep = new Icon(TYPE_BITMAP); 393 rep.mObj1 = bits; 394 return rep; 395 } 396 397 /** 398 * Create an Icon pointing to a compressed bitmap stored in a byte array. 399 * @param data Byte array storing compressed bitmap data of a type that 400 * {@link android.graphics.BitmapFactory} 401 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 402 * @param offset Offset into <code>data</code> at which the bitmap data starts 403 * @param length Length of the bitmap data 404 */ 405 public static Icon createWithData(byte[] data, int offset, int length) { 406 if (data == null) { 407 throw new IllegalArgumentException("Data must not be null."); 408 } 409 final Icon rep = new Icon(TYPE_DATA); 410 rep.mObj1 = data; 411 rep.mInt1 = length; 412 rep.mInt2 = offset; 413 return rep; 414 } 415 416 /** 417 * Create an Icon pointing to an image file specified by URI. 418 * 419 * @param uri A uri referring to local content:// or file:// image data. 420 */ 421 public static Icon createWithContentUri(String uri) { 422 if (uri == null) { 423 throw new IllegalArgumentException("Uri must not be null."); 424 } 425 final Icon rep = new Icon(TYPE_URI); 426 rep.mString1 = uri; 427 return rep; 428 } 429 430 /** 431 * Create an Icon pointing to an image file specified by URI. 432 * 433 * @param uri A uri referring to local content:// or file:// image data. 434 */ 435 public static Icon createWithContentUri(Uri uri) { 436 if (uri == null) { 437 throw new IllegalArgumentException("Uri must not be null."); 438 } 439 final Icon rep = new Icon(TYPE_URI); 440 rep.mString1 = uri.toString(); 441 return rep; 442 } 443 444 /** 445 * Create an Icon pointing to an image file specified by path. 446 * 447 * @param path A path to a file that contains compressed bitmap data of 448 * a type that {@link android.graphics.BitmapFactory} can decode. 449 */ 450 public static Icon createWithFilePath(String path) { 451 if (path == null) { 452 throw new IllegalArgumentException("Path must not be null."); 453 } 454 final Icon rep = new Icon(TYPE_URI); 455 rep.mString1 = path; 456 return rep; 457 } 458 459 @Override 460 public String toString() { 461 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 462 switch (mType) { 463 case TYPE_BITMAP: 464 sb.append(" size=") 465 .append(getBitmap().getWidth()) 466 .append("x") 467 .append(getBitmap().getHeight()); 468 break; 469 case TYPE_RESOURCE: 470 sb.append(" pkg=") 471 .append(getResPackage()) 472 .append(" id=") 473 .append(String.format("%08x", getResId())); 474 break; 475 case TYPE_DATA: 476 sb.append(" len=").append(getDataLength()); 477 if (getDataOffset() != 0) { 478 sb.append(" off=").append(getDataOffset()); 479 } 480 break; 481 case TYPE_URI: 482 sb.append(" uri=").append(getUriString()); 483 break; 484 } 485 sb.append(")"); 486 return sb.toString(); 487 } 488 489 /** 490 * Parcelable interface 491 */ 492 public int describeContents() { 493 return (mType == TYPE_BITMAP || mType == TYPE_DATA) 494 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 495 } 496 497 // ===== Parcelable interface ====== 498 499 private Icon(Parcel in) { 500 this(in.readInt()); 501 switch (mType) { 502 case TYPE_BITMAP: 503 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 504 mObj1 = bits; 505 break; 506 case TYPE_RESOURCE: 507 final String pkg = in.readString(); 508 final int resId = in.readInt(); 509 mString1 = pkg; 510 mInt1 = resId; 511 break; 512 case TYPE_DATA: 513 final int len = in.readInt(); 514 final byte[] a = in.readBlob(); 515 if (len != a.length) { 516 throw new RuntimeException("internal unparceling error: blob length (" 517 + a.length + ") != expected length (" + len + ")"); 518 } 519 mInt1 = len; 520 mObj1 = a; 521 break; 522 case TYPE_URI: 523 final String uri = in.readString(); 524 mString1 = uri; 525 break; 526 default: 527 throw new RuntimeException("invalid " 528 + this.getClass().getSimpleName() + " type in parcel: " + mType); 529 } 530 } 531 532 @Override 533 public void writeToParcel(Parcel dest, int flags) { 534 switch (mType) { 535 case TYPE_BITMAP: 536 final Bitmap bits = getBitmap(); 537 dest.writeInt(TYPE_BITMAP); 538 getBitmap().writeToParcel(dest, flags); 539 break; 540 case TYPE_RESOURCE: 541 dest.writeInt(TYPE_RESOURCE); 542 dest.writeString(getResPackage()); 543 dest.writeInt(getResId()); 544 break; 545 case TYPE_DATA: 546 dest.writeInt(TYPE_DATA); 547 dest.writeInt(getDataLength()); 548 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 549 break; 550 case TYPE_URI: 551 dest.writeInt(TYPE_URI); 552 dest.writeString(getUriString()); 553 break; 554 } 555 } 556 557 public static final Parcelable.Creator<Icon> CREATOR 558 = new Parcelable.Creator<Icon>() { 559 public Icon createFromParcel(Parcel in) { 560 return new Icon(in); 561 } 562 563 public Icon[] newArray(int size) { 564 return new Icon[size]; 565 } 566 }; 567 568 /** 569 * Implement this interface to receive a callback when 570 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 571 * is finished and your Drawable is ready. 572 */ 573 public interface OnDrawableLoadedListener { 574 void onDrawableLoaded(Drawable d); 575 } 576 577 /** 578 * Wrapper around loadDrawable that does its work on a pooled thread and then 579 * fires back the given (targeted) Message. 580 */ 581 private class LoadDrawableTask implements Runnable { 582 final Context mContext; 583 final Message mMessage; 584 585 public LoadDrawableTask(Context context, final Handler handler, 586 final OnDrawableLoadedListener listener) { 587 mContext = context; 588 mMessage = Message.obtain(handler, new Runnable() { 589 @Override 590 public void run() { 591 listener.onDrawableLoaded((Drawable) mMessage.obj); 592 } 593 }); 594 } 595 596 public LoadDrawableTask(Context context, Message message) { 597 mContext = context; 598 mMessage = message; 599 } 600 601 @Override 602 public void run() { 603 mMessage.obj = loadDrawable(mContext); 604 mMessage.sendToTarget(); 605 } 606 607 public void runAsync() { 608 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 609 } 610 } 611} 612