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