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