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 17 package android.graphics.drawable; 18 19 import android.annotation.ColorInt; 20 import android.annotation.DrawableRes; 21 import android.annotation.IdRes; 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.content.res.ColorStateList; 29 import android.content.res.Resources; 30 import android.graphics.Bitmap; 31 import android.graphics.BitmapFactory; 32 import android.graphics.PorterDuff; 33 import android.net.Uri; 34 import android.os.AsyncTask; 35 import android.os.Handler; 36 import android.os.Message; 37 import android.os.Parcel; 38 import android.os.Parcelable; 39 import android.text.TextUtils; 40 import android.util.Log; 41 42 import java.io.DataInputStream; 43 import java.io.DataOutputStream; 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.io.FileNotFoundException; 47 import java.io.IOException; 48 import java.io.InputStream; 49 import java.io.OutputStream; 50 import java.util.Arrays; 51 import java.util.Objects; 52 53 /** 54 * An umbrella container for several serializable graphics representations, including Bitmaps, 55 * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors). 56 * 57 * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a> 58 * has been spilled on the best way to load images, and many clients may have different needs when 59 * it comes to threading and fetching. This class is therefore focused on encapsulation rather than 60 * behavior. 61 */ 62 63 public final class Icon implements Parcelable { 64 private static final String TAG = "Icon"; 65 66 /** 67 * An icon that was created using {@link Icon#createWithBitmap(Bitmap)}. 68 * @see #getType 69 */ 70 public static final int TYPE_BITMAP = 1; 71 /** 72 * An icon that was created using {@link Icon#createWithResource}. 73 * @see #getType 74 */ 75 public static final int TYPE_RESOURCE = 2; 76 /** 77 * An icon that was created using {@link Icon#createWithData(byte[], int, int)}. 78 * @see #getType 79 */ 80 public static final int TYPE_DATA = 3; 81 /** 82 * An icon that was created using {@link Icon#createWithContentUri} 83 * or {@link Icon#createWithFilePath(String)}. 84 * @see #getType 85 */ 86 public static final int TYPE_URI = 4; 87 /** 88 * An icon that was created using {@link Icon#createWithAdaptiveBitmap}. 89 * @see #getType 90 */ 91 public static final int TYPE_ADAPTIVE_BITMAP = 5; 92 93 /** 94 * @hide 95 */ 96 @IntDef({TYPE_BITMAP, TYPE_RESOURCE, TYPE_DATA, TYPE_URI, TYPE_ADAPTIVE_BITMAP}) 97 public @interface IconType { 98 } 99 100 private static final int VERSION_STREAM_SERIALIZER = 1; 101 102 private final int mType; 103 104 private ColorStateList mTintList; 105 static final PorterDuff.Mode DEFAULT_TINT_MODE = Drawable.DEFAULT_TINT_MODE; // SRC_IN 106 private PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE; 107 108 // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed 109 // based on the value of mType. 110 111 // TYPE_BITMAP: Bitmap 112 // TYPE_RESOURCE: Resources 113 // TYPE_DATA: DataBytes 114 private Object mObj1; 115 116 // TYPE_RESOURCE: package name 117 // TYPE_URI: uri string 118 private String mString1; 119 120 // TYPE_RESOURCE: resId 121 // TYPE_DATA: data length 122 private int mInt1; 123 124 // TYPE_DATA: data offset 125 private int mInt2; 126 127 /** 128 * Gets the type of the icon provided. 129 * <p> 130 * Note that new types may be added later, so callers should guard against other 131 * types being returned. 132 */ 133 @IconType getType()134 public int getType() { 135 return mType; 136 } 137 138 /** 139 * @return The {@link android.graphics.Bitmap} held by this {@link #TYPE_BITMAP} Icon. 140 * @hide 141 */ getBitmap()142 public Bitmap getBitmap() { 143 if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) { 144 throw new IllegalStateException("called getBitmap() on " + this); 145 } 146 return (Bitmap) mObj1; 147 } 148 setBitmap(Bitmap b)149 private void setBitmap(Bitmap b) { 150 mObj1 = b; 151 } 152 153 /** 154 * @return The length of the compressed bitmap byte array held by this {@link #TYPE_DATA} Icon. 155 * @hide 156 */ getDataLength()157 public int getDataLength() { 158 if (mType != TYPE_DATA) { 159 throw new IllegalStateException("called getDataLength() on " + this); 160 } 161 synchronized (this) { 162 return mInt1; 163 } 164 } 165 166 /** 167 * @return The offset into the byte array held by this {@link #TYPE_DATA} Icon at which 168 * valid compressed bitmap data is found. 169 * @hide 170 */ getDataOffset()171 public int getDataOffset() { 172 if (mType != TYPE_DATA) { 173 throw new IllegalStateException("called getDataOffset() on " + this); 174 } 175 synchronized (this) { 176 return mInt2; 177 } 178 } 179 180 /** 181 * @return The byte array held by this {@link #TYPE_DATA} Icon ctonaining compressed 182 * bitmap data. 183 * @hide 184 */ getDataBytes()185 public byte[] getDataBytes() { 186 if (mType != TYPE_DATA) { 187 throw new IllegalStateException("called getDataBytes() on " + this); 188 } 189 synchronized (this) { 190 return (byte[]) mObj1; 191 } 192 } 193 194 /** 195 * @return The {@link android.content.res.Resources} for this {@link #TYPE_RESOURCE} Icon. 196 * @hide 197 */ getResources()198 public Resources getResources() { 199 if (mType != TYPE_RESOURCE) { 200 throw new IllegalStateException("called getResources() on " + this); 201 } 202 return (Resources) mObj1; 203 } 204 205 /** 206 * Gets the package used to create this icon. 207 * <p> 208 * Only valid for icons of type {@link #TYPE_RESOURCE}. 209 * Note: This package may not be available if referenced in the future, and it is 210 * up to the caller to ensure safety if this package is re-used and/or persisted. 211 */ 212 @NonNull getResPackage()213 public String getResPackage() { 214 if (mType != TYPE_RESOURCE) { 215 throw new IllegalStateException("called getResPackage() on " + this); 216 } 217 return mString1; 218 } 219 220 /** 221 * Gets the resource used to create this icon. 222 * <p> 223 * Only valid for icons of type {@link #TYPE_RESOURCE}. 224 * Note: This resource may not be available if the application changes at all, and it is 225 * up to the caller to ensure safety if this resource is re-used and/or persisted. 226 */ 227 @IdRes getResId()228 public int getResId() { 229 if (mType != TYPE_RESOURCE) { 230 throw new IllegalStateException("called getResId() on " + this); 231 } 232 return mInt1; 233 } 234 235 /** 236 * @return The URI (as a String) for this {@link #TYPE_URI} Icon. 237 * @hide 238 */ getUriString()239 public String getUriString() { 240 if (mType != TYPE_URI) { 241 throw new IllegalStateException("called getUriString() on " + this); 242 } 243 return mString1; 244 } 245 246 /** 247 * Gets the uri used to create this icon. 248 * <p> 249 * Only valid for icons of type {@link #TYPE_URI}. 250 * Note: This uri may not be available in the future, and it is 251 * up to the caller to ensure safety if this uri is re-used and/or persisted. 252 */ 253 @NonNull getUri()254 public Uri getUri() { 255 return Uri.parse(getUriString()); 256 } 257 typeToString(int x)258 private static final String typeToString(int x) { 259 switch (x) { 260 case TYPE_BITMAP: return "BITMAP"; 261 case TYPE_ADAPTIVE_BITMAP: return "BITMAP_MASKABLE"; 262 case TYPE_DATA: return "DATA"; 263 case TYPE_RESOURCE: return "RESOURCE"; 264 case TYPE_URI: return "URI"; 265 default: return "UNKNOWN"; 266 } 267 } 268 269 /** 270 * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler} 271 * and then sends <code>andThen</code> to the same Handler when finished. 272 * 273 * @param context {@link android.content.Context Context} in which to load the drawable; see 274 * {@link #loadDrawable(Context)} 275 * @param andThen {@link android.os.Message} to send to its target once the drawable 276 * is available. The {@link android.os.Message#obj obj} 277 * property is populated with the Drawable. 278 */ loadDrawableAsync(Context context, Message andThen)279 public void loadDrawableAsync(Context context, Message andThen) { 280 if (andThen.getTarget() == null) { 281 throw new IllegalArgumentException("callback message must have a target handler"); 282 } 283 new LoadDrawableTask(context, andThen).runAsync(); 284 } 285 286 /** 287 * Invokes {@link #loadDrawable(Context)} on a background thread and notifies the <code> 288 * {@link OnDrawableLoadedListener#onDrawableLoaded listener} </code> on the {@code handler} 289 * when finished. 290 * 291 * @param context {@link Context Context} in which to load the drawable; see 292 * {@link #loadDrawable(Context)} 293 * @param listener to be {@link OnDrawableLoadedListener#onDrawableLoaded notified} when 294 * {@link #loadDrawable(Context)} finished 295 * @param handler {@link Handler} on which to notify the {@code listener} 296 */ loadDrawableAsync(Context context, final OnDrawableLoadedListener listener, Handler handler)297 public void loadDrawableAsync(Context context, final OnDrawableLoadedListener listener, 298 Handler handler) { 299 new LoadDrawableTask(context, handler, listener).runAsync(); 300 } 301 302 /** 303 * Returns a Drawable that can be used to draw the image inside this Icon, constructing it 304 * if necessary. Depending on the type of image, this may not be something you want to do on 305 * the UI thread, so consider using 306 * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead. 307 * 308 * @param context {@link android.content.Context Context} in which to load the drawable; used 309 * to access {@link android.content.res.Resources Resources}, for example. 310 * @return A fresh instance of a drawable for this image, yours to keep. 311 */ loadDrawable(Context context)312 public Drawable loadDrawable(Context context) { 313 final Drawable result = loadDrawableInner(context); 314 if (result != null && (mTintList != null || mTintMode != DEFAULT_TINT_MODE)) { 315 result.mutate(); 316 result.setTintList(mTintList); 317 result.setTintMode(mTintMode); 318 } 319 return result; 320 } 321 322 /** 323 * Do the heavy lifting of loading the drawable, but stop short of applying any tint. 324 */ loadDrawableInner(Context context)325 private Drawable loadDrawableInner(Context context) { 326 switch (mType) { 327 case TYPE_BITMAP: 328 return new BitmapDrawable(context.getResources(), getBitmap()); 329 case TYPE_ADAPTIVE_BITMAP: 330 return new AdaptiveIconDrawable(null, 331 new BitmapDrawable(context.getResources(), getBitmap())); 332 case TYPE_RESOURCE: 333 if (getResources() == null) { 334 // figure out where to load resources from 335 String resPackage = getResPackage(); 336 if (TextUtils.isEmpty(resPackage)) { 337 // if none is specified, try the given context 338 resPackage = context.getPackageName(); 339 } 340 if ("android".equals(resPackage)) { 341 mObj1 = Resources.getSystem(); 342 } else { 343 final PackageManager pm = context.getPackageManager(); 344 try { 345 ApplicationInfo ai = pm.getApplicationInfo( 346 resPackage, PackageManager.MATCH_UNINSTALLED_PACKAGES); 347 if (ai != null) { 348 mObj1 = pm.getResourcesForApplication(ai); 349 } else { 350 break; 351 } 352 } catch (PackageManager.NameNotFoundException e) { 353 Log.e(TAG, String.format("Unable to find pkg=%s for icon %s", 354 resPackage, this), e); 355 break; 356 } 357 } 358 } 359 try { 360 return getResources().getDrawable(getResId(), context.getTheme()); 361 } catch (RuntimeException e) { 362 Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s", 363 getResId(), 364 getResPackage()), 365 e); 366 } 367 break; 368 case TYPE_DATA: 369 return new BitmapDrawable(context.getResources(), 370 BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) 371 ); 372 case TYPE_URI: 373 final Uri uri = getUri(); 374 final String scheme = uri.getScheme(); 375 InputStream is = null; 376 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 377 || ContentResolver.SCHEME_FILE.equals(scheme)) { 378 try { 379 is = context.getContentResolver().openInputStream(uri); 380 } catch (Exception e) { 381 Log.w(TAG, "Unable to load image from URI: " + uri, e); 382 } 383 } else { 384 try { 385 is = new FileInputStream(new File(mString1)); 386 } catch (FileNotFoundException e) { 387 Log.w(TAG, "Unable to load image from path: " + uri, e); 388 } 389 } 390 if (is != null) { 391 return new BitmapDrawable(context.getResources(), 392 BitmapFactory.decodeStream(is)); 393 } 394 break; 395 } 396 return null; 397 } 398 399 /** 400 * Load the requested resources under the given userId, if the system allows it, 401 * before actually loading the drawable. 402 * 403 * @hide 404 */ loadDrawableAsUser(Context context, int userId)405 public Drawable loadDrawableAsUser(Context context, int userId) { 406 if (mType == TYPE_RESOURCE) { 407 String resPackage = getResPackage(); 408 if (TextUtils.isEmpty(resPackage)) { 409 resPackage = context.getPackageName(); 410 } 411 if (getResources() == null && !(getResPackage().equals("android"))) { 412 final PackageManager pm = context.getPackageManager(); 413 try { 414 // assign getResources() as the correct user 415 mObj1 = pm.getResourcesForApplicationAsUser(resPackage, userId); 416 } catch (PackageManager.NameNotFoundException e) { 417 Log.e(TAG, String.format("Unable to find pkg=%s user=%d", 418 getResPackage(), 419 userId), 420 e); 421 } 422 } 423 } 424 return loadDrawable(context); 425 } 426 427 /** @hide */ 428 public static final int MIN_ASHMEM_ICON_SIZE = 128 * (1 << 10); 429 430 /** 431 * Puts the memory used by this instance into Ashmem memory, if possible. 432 * @hide 433 */ convertToAshmem()434 public void convertToAshmem() { 435 if ((mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP) && 436 getBitmap().isMutable() && 437 getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) { 438 setBitmap(getBitmap().createAshmemBitmap()); 439 } 440 } 441 442 /** 443 * Writes a serialized version of an Icon to the specified stream. 444 * 445 * @param stream The stream on which to serialize the Icon. 446 * @hide 447 */ writeToStream(OutputStream stream)448 public void writeToStream(OutputStream stream) throws IOException { 449 DataOutputStream dataStream = new DataOutputStream(stream); 450 451 dataStream.writeInt(VERSION_STREAM_SERIALIZER); 452 dataStream.writeByte(mType); 453 454 switch (mType) { 455 case TYPE_BITMAP: 456 case TYPE_ADAPTIVE_BITMAP: 457 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream); 458 break; 459 case TYPE_DATA: 460 dataStream.writeInt(getDataLength()); 461 dataStream.write(getDataBytes(), getDataOffset(), getDataLength()); 462 break; 463 case TYPE_RESOURCE: 464 dataStream.writeUTF(getResPackage()); 465 dataStream.writeInt(getResId()); 466 break; 467 case TYPE_URI: 468 dataStream.writeUTF(getUriString()); 469 break; 470 } 471 } 472 Icon(int mType)473 private Icon(int mType) { 474 this.mType = mType; 475 } 476 477 /** 478 * Create an Icon from the specified stream. 479 * 480 * @param stream The input stream from which to reconstruct the Icon. 481 * @hide 482 */ createFromStream(InputStream stream)483 public static Icon createFromStream(InputStream stream) throws IOException { 484 DataInputStream inputStream = new DataInputStream(stream); 485 486 final int version = inputStream.readInt(); 487 if (version >= VERSION_STREAM_SERIALIZER) { 488 final int type = inputStream.readByte(); 489 switch (type) { 490 case TYPE_BITMAP: 491 return createWithBitmap(BitmapFactory.decodeStream(inputStream)); 492 case TYPE_ADAPTIVE_BITMAP: 493 return createWithAdaptiveBitmap(BitmapFactory.decodeStream(inputStream)); 494 case TYPE_DATA: 495 final int length = inputStream.readInt(); 496 final byte[] data = new byte[length]; 497 inputStream.read(data, 0 /* offset */, length); 498 return createWithData(data, 0 /* offset */, length); 499 case TYPE_RESOURCE: 500 final String packageName = inputStream.readUTF(); 501 final int resId = inputStream.readInt(); 502 return createWithResource(packageName, resId); 503 case TYPE_URI: 504 final String uriOrPath = inputStream.readUTF(); 505 return createWithContentUri(uriOrPath); 506 } 507 } 508 return null; 509 } 510 511 /** 512 * Compares if this icon is constructed from the same resources as another icon. 513 * Note that this is an inexpensive operation and doesn't do deep Bitmap equality comparisons. 514 * 515 * @param otherIcon the other icon 516 * @return whether this icon is the same as the another one 517 * @hide 518 */ sameAs(Icon otherIcon)519 public boolean sameAs(Icon otherIcon) { 520 if (otherIcon == this) { 521 return true; 522 } 523 if (mType != otherIcon.getType()) { 524 return false; 525 } 526 switch (mType) { 527 case TYPE_BITMAP: 528 case TYPE_ADAPTIVE_BITMAP: 529 return getBitmap() == otherIcon.getBitmap(); 530 case TYPE_DATA: 531 return getDataLength() == otherIcon.getDataLength() 532 && getDataOffset() == otherIcon.getDataOffset() 533 && Arrays.equals(getDataBytes(), otherIcon.getDataBytes()); 534 case TYPE_RESOURCE: 535 return getResId() == otherIcon.getResId() 536 && Objects.equals(getResPackage(), otherIcon.getResPackage()); 537 case TYPE_URI: 538 return Objects.equals(getUriString(), otherIcon.getUriString()); 539 } 540 return false; 541 } 542 543 /** 544 * Create an Icon pointing to a drawable resource. 545 * @param context The context for the application whose resources should be used to resolve the 546 * given resource ID. 547 * @param resId ID of the drawable resource 548 */ createWithResource(Context context, @DrawableRes int resId)549 public static Icon createWithResource(Context context, @DrawableRes int resId) { 550 if (context == null) { 551 throw new IllegalArgumentException("Context must not be null."); 552 } 553 final Icon rep = new Icon(TYPE_RESOURCE); 554 rep.mInt1 = resId; 555 rep.mString1 = context.getPackageName(); 556 return rep; 557 } 558 559 /** 560 * Version of createWithResource that takes Resources. Do not use. 561 * @hide 562 */ createWithResource(Resources res, @DrawableRes int resId)563 public static Icon createWithResource(Resources res, @DrawableRes int resId) { 564 if (res == null) { 565 throw new IllegalArgumentException("Resource must not be null."); 566 } 567 final Icon rep = new Icon(TYPE_RESOURCE); 568 rep.mInt1 = resId; 569 rep.mString1 = res.getResourcePackageName(resId); 570 return rep; 571 } 572 573 /** 574 * Create an Icon pointing to a drawable resource. 575 * @param resPackage Name of the package containing the resource in question 576 * @param resId ID of the drawable resource 577 */ createWithResource(String resPackage, @DrawableRes int resId)578 public static Icon createWithResource(String resPackage, @DrawableRes int resId) { 579 if (resPackage == null) { 580 throw new IllegalArgumentException("Resource package name must not be null."); 581 } 582 final Icon rep = new Icon(TYPE_RESOURCE); 583 rep.mInt1 = resId; 584 rep.mString1 = resPackage; 585 return rep; 586 } 587 588 /** 589 * Create an Icon pointing to a bitmap in memory. 590 * @param bits A valid {@link android.graphics.Bitmap} object 591 */ createWithBitmap(Bitmap bits)592 public static Icon createWithBitmap(Bitmap bits) { 593 if (bits == null) { 594 throw new IllegalArgumentException("Bitmap must not be null."); 595 } 596 final Icon rep = new Icon(TYPE_BITMAP); 597 rep.setBitmap(bits); 598 return rep; 599 } 600 601 /** 602 * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined 603 * by {@link AdaptiveIconDrawable}. 604 * @param bits A valid {@link android.graphics.Bitmap} object 605 */ createWithAdaptiveBitmap(Bitmap bits)606 public static Icon createWithAdaptiveBitmap(Bitmap bits) { 607 if (bits == null) { 608 throw new IllegalArgumentException("Bitmap must not be null."); 609 } 610 final Icon rep = new Icon(TYPE_ADAPTIVE_BITMAP); 611 rep.setBitmap(bits); 612 return rep; 613 } 614 615 /** 616 * Create an Icon pointing to a compressed bitmap stored in a byte array. 617 * @param data Byte array storing compressed bitmap data of a type that 618 * {@link android.graphics.BitmapFactory} 619 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 620 * @param offset Offset into <code>data</code> at which the bitmap data starts 621 * @param length Length of the bitmap data 622 */ createWithData(byte[] data, int offset, int length)623 public static Icon createWithData(byte[] data, int offset, int length) { 624 if (data == null) { 625 throw new IllegalArgumentException("Data must not be null."); 626 } 627 final Icon rep = new Icon(TYPE_DATA); 628 rep.mObj1 = data; 629 rep.mInt1 = length; 630 rep.mInt2 = offset; 631 return rep; 632 } 633 634 /** 635 * Create an Icon pointing to an image file specified by URI. 636 * 637 * @param uri A uri referring to local content:// or file:// image data. 638 */ createWithContentUri(String uri)639 public static Icon createWithContentUri(String uri) { 640 if (uri == null) { 641 throw new IllegalArgumentException("Uri must not be null."); 642 } 643 final Icon rep = new Icon(TYPE_URI); 644 rep.mString1 = uri; 645 return rep; 646 } 647 648 /** 649 * Create an Icon pointing to an image file specified by URI. 650 * 651 * @param uri A uri referring to local content:// or file:// image data. 652 */ createWithContentUri(Uri uri)653 public static Icon createWithContentUri(Uri uri) { 654 if (uri == null) { 655 throw new IllegalArgumentException("Uri must not be null."); 656 } 657 final Icon rep = new Icon(TYPE_URI); 658 rep.mString1 = uri.toString(); 659 return rep; 660 } 661 662 /** 663 * Store a color to use whenever this Icon is drawn. 664 * 665 * @param tint a color, as in {@link Drawable#setTint(int)} 666 * @return this same object, for use in chained construction 667 */ setTint(@olorInt int tint)668 public Icon setTint(@ColorInt int tint) { 669 return setTintList(ColorStateList.valueOf(tint)); 670 } 671 672 /** 673 * Store a color to use whenever this Icon is drawn. 674 * 675 * @param tintList as in {@link Drawable#setTintList(ColorStateList)}, null to remove tint 676 * @return this same object, for use in chained construction 677 */ setTintList(ColorStateList tintList)678 public Icon setTintList(ColorStateList tintList) { 679 mTintList = tintList; 680 return this; 681 } 682 683 /** 684 * Store a blending mode to use whenever this Icon is drawn. 685 * 686 * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null 687 * @return this same object, for use in chained construction 688 */ setTintMode(PorterDuff.Mode mode)689 public Icon setTintMode(PorterDuff.Mode mode) { 690 mTintMode = mode; 691 return this; 692 } 693 694 /** @hide */ hasTint()695 public boolean hasTint() { 696 return (mTintList != null) || (mTintMode != DEFAULT_TINT_MODE); 697 } 698 699 /** 700 * Create an Icon pointing to an image file specified by path. 701 * 702 * @param path A path to a file that contains compressed bitmap data of 703 * a type that {@link android.graphics.BitmapFactory} can decode. 704 */ createWithFilePath(String path)705 public static Icon createWithFilePath(String path) { 706 if (path == null) { 707 throw new IllegalArgumentException("Path must not be null."); 708 } 709 final Icon rep = new Icon(TYPE_URI); 710 rep.mString1 = path; 711 return rep; 712 } 713 714 @Override toString()715 public String toString() { 716 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 717 switch (mType) { 718 case TYPE_BITMAP: 719 case TYPE_ADAPTIVE_BITMAP: 720 sb.append(" size=") 721 .append(getBitmap().getWidth()) 722 .append("x") 723 .append(getBitmap().getHeight()); 724 break; 725 case TYPE_RESOURCE: 726 sb.append(" pkg=") 727 .append(getResPackage()) 728 .append(" id=") 729 .append(String.format("0x%08x", getResId())); 730 break; 731 case TYPE_DATA: 732 sb.append(" len=").append(getDataLength()); 733 if (getDataOffset() != 0) { 734 sb.append(" off=").append(getDataOffset()); 735 } 736 break; 737 case TYPE_URI: 738 sb.append(" uri=").append(getUriString()); 739 break; 740 } 741 if (mTintList != null) { 742 sb.append(" tint="); 743 String sep = ""; 744 for (int c : mTintList.getColors()) { 745 sb.append(String.format("%s0x%08x", sep, c)); 746 sep = "|"; 747 } 748 } 749 if (mTintMode != DEFAULT_TINT_MODE) sb.append(" mode=").append(mTintMode); 750 sb.append(")"); 751 return sb.toString(); 752 } 753 754 /** 755 * Parcelable interface 756 */ describeContents()757 public int describeContents() { 758 return (mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP || mType == TYPE_DATA) 759 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 760 } 761 762 // ===== Parcelable interface ====== 763 Icon(Parcel in)764 private Icon(Parcel in) { 765 this(in.readInt()); 766 switch (mType) { 767 case TYPE_BITMAP: 768 case TYPE_ADAPTIVE_BITMAP: 769 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 770 mObj1 = bits; 771 break; 772 case TYPE_RESOURCE: 773 final String pkg = in.readString(); 774 final int resId = in.readInt(); 775 mString1 = pkg; 776 mInt1 = resId; 777 break; 778 case TYPE_DATA: 779 final int len = in.readInt(); 780 final byte[] a = in.readBlob(); 781 if (len != a.length) { 782 throw new RuntimeException("internal unparceling error: blob length (" 783 + a.length + ") != expected length (" + len + ")"); 784 } 785 mInt1 = len; 786 mObj1 = a; 787 break; 788 case TYPE_URI: 789 final String uri = in.readString(); 790 mString1 = uri; 791 break; 792 default: 793 throw new RuntimeException("invalid " 794 + this.getClass().getSimpleName() + " type in parcel: " + mType); 795 } 796 if (in.readInt() == 1) { 797 mTintList = ColorStateList.CREATOR.createFromParcel(in); 798 } 799 mTintMode = PorterDuff.intToMode(in.readInt()); 800 } 801 802 @Override writeToParcel(Parcel dest, int flags)803 public void writeToParcel(Parcel dest, int flags) { 804 dest.writeInt(mType); 805 switch (mType) { 806 case TYPE_BITMAP: 807 case TYPE_ADAPTIVE_BITMAP: 808 final Bitmap bits = getBitmap(); 809 getBitmap().writeToParcel(dest, flags); 810 break; 811 case TYPE_RESOURCE: 812 dest.writeString(getResPackage()); 813 dest.writeInt(getResId()); 814 break; 815 case TYPE_DATA: 816 dest.writeInt(getDataLength()); 817 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 818 break; 819 case TYPE_URI: 820 dest.writeString(getUriString()); 821 break; 822 } 823 if (mTintList == null) { 824 dest.writeInt(0); 825 } else { 826 dest.writeInt(1); 827 mTintList.writeToParcel(dest, flags); 828 } 829 dest.writeInt(PorterDuff.modeToInt(mTintMode)); 830 } 831 832 public static final Parcelable.Creator<Icon> CREATOR 833 = new Parcelable.Creator<Icon>() { 834 public Icon createFromParcel(Parcel in) { 835 return new Icon(in); 836 } 837 838 public Icon[] newArray(int size) { 839 return new Icon[size]; 840 } 841 }; 842 843 /** 844 * Scale down a bitmap to a given max width and max height. The scaling will be done in a uniform way 845 * @param bitmap the bitmap to scale down 846 * @param maxWidth the maximum width allowed 847 * @param maxHeight the maximum height allowed 848 * 849 * @return the scaled bitmap if necessary or the original bitmap if no scaling was needed 850 * @hide 851 */ scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight)852 public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) { 853 int bitmapWidth = bitmap.getWidth(); 854 int bitmapHeight = bitmap.getHeight(); 855 if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) { 856 float scale = Math.min((float) maxWidth / bitmapWidth, 857 (float) maxHeight / bitmapHeight); 858 bitmap = Bitmap.createScaledBitmap(bitmap, 859 Math.max(1, (int) (scale * bitmapWidth)), 860 Math.max(1, (int) (scale * bitmapHeight)), 861 true /* filter */); 862 } 863 return bitmap; 864 } 865 866 /** 867 * Scale down this icon to a given max width and max height. 868 * The scaling will be done in a uniform way and currently only bitmaps are supported. 869 * @param maxWidth the maximum width allowed 870 * @param maxHeight the maximum height allowed 871 * 872 * @hide 873 */ scaleDownIfNecessary(int maxWidth, int maxHeight)874 public void scaleDownIfNecessary(int maxWidth, int maxHeight) { 875 if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) { 876 return; 877 } 878 Bitmap bitmap = getBitmap(); 879 setBitmap(scaleDownIfNecessary(bitmap, maxWidth, maxHeight)); 880 } 881 882 /** 883 * Implement this interface to receive a callback when 884 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 885 * is finished and your Drawable is ready. 886 */ 887 public interface OnDrawableLoadedListener { onDrawableLoaded(Drawable d)888 void onDrawableLoaded(Drawable d); 889 } 890 891 /** 892 * Wrapper around loadDrawable that does its work on a pooled thread and then 893 * fires back the given (targeted) Message. 894 */ 895 private class LoadDrawableTask implements Runnable { 896 final Context mContext; 897 final Message mMessage; 898 LoadDrawableTask(Context context, final Handler handler, final OnDrawableLoadedListener listener)899 public LoadDrawableTask(Context context, final Handler handler, 900 final OnDrawableLoadedListener listener) { 901 mContext = context; 902 mMessage = Message.obtain(handler, new Runnable() { 903 @Override 904 public void run() { 905 listener.onDrawableLoaded((Drawable) mMessage.obj); 906 } 907 }); 908 } 909 LoadDrawableTask(Context context, Message message)910 public LoadDrawableTask(Context context, Message message) { 911 mContext = context; 912 mMessage = message; 913 } 914 915 @Override run()916 public void run() { 917 mMessage.obj = loadDrawable(mContext); 918 mMessage.sendToTarget(); 919 } 920 runAsync()921 public void runAsync() { 922 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 923 } 924 } 925 } 926