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