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