Icon.java revision 02cd9f91a6bd6d06d6d1237f8b978a0ab0b8ea32
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 context The context for the application whose resources should be used to resolve the 356 * given resource ID. 357 * @param resId ID of the drawable resource 358 */ 359 public static Icon createWithResource(Context context, @DrawableRes int resId) { 360 final Icon rep = new Icon(TYPE_RESOURCE); 361 rep.mInt1 = resId; 362 rep.mString1 = context.getPackageName(); 363 return rep; 364 } 365 366 /** 367 * Version of createWithResource that takes Resources. Do not use. 368 * @hide 369 */ 370 public static Icon createWithResource(Resources res, @DrawableRes int resId) { 371 if (res == null) { 372 throw new IllegalArgumentException("Resource must not be null."); 373 } 374 final Icon rep = new Icon(TYPE_RESOURCE); 375 rep.mInt1 = resId; 376 rep.mString1 = res.getResourcePackageName(resId); 377 return rep; 378 } 379 380 /** 381 * Create an Icon pointing to a drawable resource. 382 * @param resPackage Name of the package containing the resource in question 383 * @param resId ID of the drawable resource 384 */ 385 public static Icon createWithResource(String resPackage, @DrawableRes int resId) { 386 if (resPackage == null) { 387 throw new IllegalArgumentException("Resource package name must not be null."); 388 } 389 final Icon rep = new Icon(TYPE_RESOURCE); 390 rep.mInt1 = resId; 391 rep.mString1 = resPackage; 392 return rep; 393 } 394 395 /** 396 * Create an Icon pointing to a bitmap in memory. 397 * @param bits A valid {@link android.graphics.Bitmap} object 398 */ 399 public static Icon createWithBitmap(Bitmap bits) { 400 if (bits == null) { 401 throw new IllegalArgumentException("Bitmap must not be null."); 402 } 403 final Icon rep = new Icon(TYPE_BITMAP); 404 rep.mObj1 = bits; 405 return rep; 406 } 407 408 /** 409 * Create an Icon pointing to a compressed bitmap stored in a byte array. 410 * @param data Byte array storing compressed bitmap data of a type that 411 * {@link android.graphics.BitmapFactory} 412 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 413 * @param offset Offset into <code>data</code> at which the bitmap data starts 414 * @param length Length of the bitmap data 415 */ 416 public static Icon createWithData(byte[] data, int offset, int length) { 417 if (data == null) { 418 throw new IllegalArgumentException("Data must not be null."); 419 } 420 final Icon rep = new Icon(TYPE_DATA); 421 rep.mObj1 = data; 422 rep.mInt1 = length; 423 rep.mInt2 = offset; 424 return rep; 425 } 426 427 /** 428 * Create an Icon pointing to an image file specified by URI. 429 * 430 * @param uri A uri referring to local content:// or file:// image data. 431 */ 432 public static Icon createWithContentUri(String uri) { 433 if (uri == null) { 434 throw new IllegalArgumentException("Uri must not be null."); 435 } 436 final Icon rep = new Icon(TYPE_URI); 437 rep.mString1 = uri; 438 return rep; 439 } 440 441 /** 442 * Create an Icon pointing to an image file specified by URI. 443 * 444 * @param uri A uri referring to local content:// or file:// image data. 445 */ 446 public static Icon createWithContentUri(Uri uri) { 447 if (uri == null) { 448 throw new IllegalArgumentException("Uri must not be null."); 449 } 450 final Icon rep = new Icon(TYPE_URI); 451 rep.mString1 = uri.toString(); 452 return rep; 453 } 454 455 /** 456 * Create an Icon pointing to an image file specified by path. 457 * 458 * @param path A path to a file that contains compressed bitmap data of 459 * a type that {@link android.graphics.BitmapFactory} can decode. 460 */ 461 public static Icon createWithFilePath(String path) { 462 if (path == null) { 463 throw new IllegalArgumentException("Path must not be null."); 464 } 465 final Icon rep = new Icon(TYPE_URI); 466 rep.mString1 = path; 467 return rep; 468 } 469 470 @Override 471 public String toString() { 472 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 473 switch (mType) { 474 case TYPE_BITMAP: 475 sb.append(" size=") 476 .append(getBitmap().getWidth()) 477 .append("x") 478 .append(getBitmap().getHeight()); 479 break; 480 case TYPE_RESOURCE: 481 sb.append(" pkg=") 482 .append(getResPackage()) 483 .append(" id=") 484 .append(String.format("%08x", getResId())); 485 break; 486 case TYPE_DATA: 487 sb.append(" len=").append(getDataLength()); 488 if (getDataOffset() != 0) { 489 sb.append(" off=").append(getDataOffset()); 490 } 491 break; 492 case TYPE_URI: 493 sb.append(" uri=").append(getUriString()); 494 break; 495 } 496 sb.append(")"); 497 return sb.toString(); 498 } 499 500 /** 501 * Parcelable interface 502 */ 503 public int describeContents() { 504 return (mType == TYPE_BITMAP || mType == TYPE_DATA) 505 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 506 } 507 508 // ===== Parcelable interface ====== 509 510 private Icon(Parcel in) { 511 this(in.readInt()); 512 switch (mType) { 513 case TYPE_BITMAP: 514 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 515 mObj1 = bits; 516 break; 517 case TYPE_RESOURCE: 518 final String pkg = in.readString(); 519 final int resId = in.readInt(); 520 mString1 = pkg; 521 mInt1 = resId; 522 break; 523 case TYPE_DATA: 524 final int len = in.readInt(); 525 final byte[] a = in.readBlob(); 526 if (len != a.length) { 527 throw new RuntimeException("internal unparceling error: blob length (" 528 + a.length + ") != expected length (" + len + ")"); 529 } 530 mInt1 = len; 531 mObj1 = a; 532 break; 533 case TYPE_URI: 534 final String uri = in.readString(); 535 mString1 = uri; 536 break; 537 default: 538 throw new RuntimeException("invalid " 539 + this.getClass().getSimpleName() + " type in parcel: " + mType); 540 } 541 } 542 543 @Override 544 public void writeToParcel(Parcel dest, int flags) { 545 switch (mType) { 546 case TYPE_BITMAP: 547 final Bitmap bits = getBitmap(); 548 dest.writeInt(TYPE_BITMAP); 549 getBitmap().writeToParcel(dest, flags); 550 break; 551 case TYPE_RESOURCE: 552 dest.writeInt(TYPE_RESOURCE); 553 dest.writeString(getResPackage()); 554 dest.writeInt(getResId()); 555 break; 556 case TYPE_DATA: 557 dest.writeInt(TYPE_DATA); 558 dest.writeInt(getDataLength()); 559 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 560 break; 561 case TYPE_URI: 562 dest.writeInt(TYPE_URI); 563 dest.writeString(getUriString()); 564 break; 565 } 566 } 567 568 public static final Parcelable.Creator<Icon> CREATOR 569 = new Parcelable.Creator<Icon>() { 570 public Icon createFromParcel(Parcel in) { 571 return new Icon(in); 572 } 573 574 public Icon[] newArray(int size) { 575 return new Icon[size]; 576 } 577 }; 578 579 /** 580 * Implement this interface to receive a callback when 581 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 582 * is finished and your Drawable is ready. 583 */ 584 public interface OnDrawableLoadedListener { 585 void onDrawableLoaded(Drawable d); 586 } 587 588 /** 589 * Wrapper around loadDrawable that does its work on a pooled thread and then 590 * fires back the given (targeted) Message. 591 */ 592 private class LoadDrawableTask implements Runnable { 593 final Context mContext; 594 final Message mMessage; 595 596 public LoadDrawableTask(Context context, final Handler handler, 597 final OnDrawableLoadedListener listener) { 598 mContext = context; 599 mMessage = Message.obtain(handler, new Runnable() { 600 @Override 601 public void run() { 602 listener.onDrawableLoaded((Drawable) mMessage.obj); 603 } 604 }); 605 } 606 607 public LoadDrawableTask(Context context, Message message) { 608 mContext = context; 609 mMessage = message; 610 } 611 612 @Override 613 public void run() { 614 mMessage.obj = loadDrawable(mContext); 615 mMessage.sendToTarget(); 616 } 617 618 public void runAsync() { 619 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 620 } 621 } 622} 623