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 try (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 } catch (IOException e) { 509 throw new IllegalStateException(e); 510 } 511 break; 512 case TYPE_URI_ADAPTIVE_BITMAP: 513 try (InputStream is = getUriInputStream(context)) { 514 if (is != null) { 515 final Bitmap bitmap = BitmapFactory.decodeStream(is); 516 if (bitmap == null) { 517 Log.w(TAG, "Unable to decode image from URI: " + getUriString()); 518 if (iconLoadDrawableReturnNullWhenUriDecodeFails()) { 519 return null; 520 } 521 } 522 return new AdaptiveIconDrawable( 523 null, new BitmapDrawable(context.getResources(), 524 fixMaxBitmapSize(bitmap))); 525 } 526 } catch (IOException e) { 527 throw new IllegalStateException(e); 528 } 529 break; 530 } 531 return null; 532 } 533 getUriInputStream(Context context)534 private @Nullable InputStream getUriInputStream(Context context) { 535 final Uri uri = getUri(); 536 final String scheme = uri.getScheme(); 537 if (ContentResolver.SCHEME_CONTENT.equals(scheme) 538 || ContentResolver.SCHEME_FILE.equals(scheme)) { 539 try { 540 return context.getContentResolver().openInputStream(uri); 541 } catch (Exception e) { 542 Log.w(TAG, "Unable to load image from URI: " + uri, e); 543 } 544 } else { 545 try { 546 return new FileInputStream(new File(mString1)); 547 } catch (FileNotFoundException e) { 548 Log.w(TAG, "Unable to load image from path: " + uri, e); 549 } 550 } 551 return null; 552 } 553 554 /** 555 * Load the requested resources under the given userId, if the system allows it, 556 * before actually loading the drawable. 557 * 558 * @hide 559 */ loadDrawableAsUser(Context context, int userId)560 public Drawable loadDrawableAsUser(Context context, int userId) { 561 if (mType == TYPE_RESOURCE) { 562 String resPackage = getResPackage(); 563 if (TextUtils.isEmpty(resPackage)) { 564 resPackage = context.getPackageName(); 565 } 566 if (getResources() == null && !(getResPackage().equals("android"))) { 567 // TODO(b/173307037): Move CONTEXT_INCLUDE_CODE to ContextImpl.createContextAsUser 568 final Context userContext; 569 if (context.getUserId() == userId) { 570 userContext = context; 571 } else { 572 final boolean sameAppWithProcess = 573 UserHandle.isSameApp(context.getApplicationInfo().uid, Process.myUid()); 574 final int flags = (sameAppWithProcess ? CONTEXT_INCLUDE_CODE : 0) 575 | CONTEXT_RESTRICTED; 576 userContext = context.createContextAsUser(UserHandle.of(userId), flags); 577 } 578 579 final PackageManager pm = userContext.getPackageManager(); 580 try { 581 // assign getResources() as the correct user 582 mObj1 = pm.getResourcesForApplication(resPackage); 583 } catch (PackageManager.NameNotFoundException e) { 584 Log.e(TAG, String.format("Unable to find pkg=%s user=%d", 585 getResPackage(), 586 userId), 587 e); 588 } 589 } 590 } 591 return loadDrawable(context); 592 } 593 594 /** 595 * Load a drawable, but in the case of URI types, it will check if the passed uid has a grant 596 * to load the resource. The check will be performed using the permissions of the passed uid, 597 * and not those of the caller. 598 * <p> 599 * This should be called for {@link Icon} objects that come from a not trusted source and may 600 * contain a URI. 601 * 602 * After the check, if passed, {@link #loadDrawable} will be called. If failed, this will 603 * return {@code null}. 604 * 605 * @see #loadDrawable 606 * 607 * @hide 608 */ 609 @Nullable 610 @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) loadDrawableCheckingUriGrant( Context context, IUriGrantsManager iugm, int callingUid, String packageName )611 public Drawable loadDrawableCheckingUriGrant( 612 Context context, 613 IUriGrantsManager iugm, 614 int callingUid, 615 String packageName 616 ) { 617 if (getType() == TYPE_URI || getType() == TYPE_URI_ADAPTIVE_BITMAP) { 618 try { 619 iugm.checkGrantUriPermission_ignoreNonSystem( 620 callingUid, 621 packageName, 622 ContentProvider.getUriWithoutUserId(getUri()), 623 Intent.FLAG_GRANT_READ_URI_PERMISSION, 624 ContentProvider.getUserIdFromUri(getUri()) 625 ); 626 } catch (SecurityException | RemoteException e) { 627 Log.e(TAG, "Failed to get URI permission for: " + getUri(), e); 628 return null; 629 } 630 } 631 return loadDrawable(context); 632 } 633 634 /** @hide */ 635 public static final int MIN_ASHMEM_ICON_SIZE = 128 * (1 << 10); 636 637 /** 638 * Puts the memory used by this instance into Ashmem memory, if possible. 639 * @hide 640 */ convertToAshmem()641 public void convertToAshmem() { 642 if ((mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP) && 643 getBitmap().isMutable() && 644 getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) { 645 setBitmap(getBitmap().asShared()); 646 } 647 mCachedAshmem = true; 648 } 649 650 /** 651 * Writes a serialized version of an Icon to the specified stream. 652 * 653 * @param stream The stream on which to serialize the Icon. 654 * @hide 655 */ writeToStream(@onNull OutputStream stream)656 public void writeToStream(@NonNull OutputStream stream) throws IOException { 657 DataOutputStream dataStream = new DataOutputStream(stream); 658 659 dataStream.writeInt(VERSION_STREAM_SERIALIZER); 660 dataStream.writeByte(mType); 661 662 switch (mType) { 663 case TYPE_BITMAP: 664 case TYPE_ADAPTIVE_BITMAP: 665 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream); 666 break; 667 case TYPE_DATA: 668 dataStream.writeInt(getDataLength()); 669 dataStream.write(getDataBytes(), getDataOffset(), getDataLength()); 670 break; 671 case TYPE_RESOURCE: 672 dataStream.writeUTF(getResPackage()); 673 dataStream.writeInt(getResId()); 674 break; 675 case TYPE_URI: 676 case TYPE_URI_ADAPTIVE_BITMAP: 677 dataStream.writeUTF(getUriString()); 678 break; 679 } 680 } 681 Icon(int mType)682 private Icon(int mType) { 683 this.mType = mType; 684 } 685 686 /** 687 * Create an Icon from the specified stream. 688 * 689 * @param stream The input stream from which to reconstruct the Icon. 690 * @hide 691 */ createFromStream(@onNull InputStream stream)692 public static @Nullable Icon createFromStream(@NonNull InputStream stream) throws IOException { 693 DataInputStream inputStream = new DataInputStream(stream); 694 695 final int version = inputStream.readInt(); 696 if (version >= VERSION_STREAM_SERIALIZER) { 697 final int type = inputStream.readByte(); 698 switch (type) { 699 case TYPE_BITMAP: 700 return createWithBitmap(BitmapFactory.decodeStream(inputStream)); 701 case TYPE_ADAPTIVE_BITMAP: 702 return createWithAdaptiveBitmap(BitmapFactory.decodeStream(inputStream)); 703 case TYPE_DATA: 704 final int length = inputStream.readInt(); 705 final byte[] data = new byte[length]; 706 inputStream.read(data, 0 /* offset */, length); 707 return createWithData(data, 0 /* offset */, length); 708 case TYPE_RESOURCE: 709 final String packageName = inputStream.readUTF(); 710 final int resId = inputStream.readInt(); 711 return createWithResource(packageName, resId); 712 case TYPE_URI: 713 final String uriOrPath = inputStream.readUTF(); 714 return createWithContentUri(uriOrPath); 715 case TYPE_URI_ADAPTIVE_BITMAP: 716 final String uri = inputStream.readUTF(); 717 return createWithAdaptiveBitmapContentUri(uri); 718 } 719 } 720 return null; 721 } 722 723 /** 724 * Compares if this icon is constructed from the same resources as another icon. 725 * Note that this is an inexpensive operation and doesn't do deep Bitmap equality comparisons. 726 * 727 * @param otherIcon the other icon 728 * @return whether this icon is the same as the another one 729 * @hide 730 */ sameAs(@onNull Icon otherIcon)731 public boolean sameAs(@NonNull Icon otherIcon) { 732 if (otherIcon == this) { 733 return true; 734 } 735 if (mType != otherIcon.getType()) { 736 return false; 737 } 738 switch (mType) { 739 case TYPE_BITMAP: 740 case TYPE_ADAPTIVE_BITMAP: 741 return getBitmap() == otherIcon.getBitmap(); 742 case TYPE_DATA: 743 return getDataLength() == otherIcon.getDataLength() 744 && getDataOffset() == otherIcon.getDataOffset() 745 && Arrays.equals(getDataBytes(), otherIcon.getDataBytes()); 746 case TYPE_RESOURCE: 747 return getResId() == otherIcon.getResId() 748 && Objects.equals(getResPackage(), otherIcon.getResPackage()) 749 && mUseMonochrome == otherIcon.mUseMonochrome 750 && mInsetScale == otherIcon.mInsetScale; 751 case TYPE_URI: 752 case TYPE_URI_ADAPTIVE_BITMAP: 753 return Objects.equals(getUriString(), otherIcon.getUriString()); 754 } 755 return false; 756 } 757 758 /** 759 * Create an Icon pointing to a drawable resource. 760 * @param context The context for the application whose resources should be used to resolve the 761 * given resource ID. 762 * @param resId ID of the drawable resource 763 */ createWithResource(Context context, @DrawableRes int resId)764 public static @NonNull Icon createWithResource(Context context, @DrawableRes int resId) { 765 if (context == null) { 766 throw new IllegalArgumentException("Context must not be null."); 767 } 768 final Icon rep = new Icon(TYPE_RESOURCE); 769 rep.mInt1 = resId; 770 rep.mString1 = context.getPackageName(); 771 return rep; 772 } 773 774 /** 775 * Version of createWithResource that takes Resources. Do not use. 776 * @hide 777 */ 778 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) createWithResource(Resources res, @DrawableRes int resId)779 public static @NonNull Icon createWithResource(Resources res, @DrawableRes int resId) { 780 if (res == null) { 781 throw new IllegalArgumentException("Resource must not be null."); 782 } 783 final Icon rep = new Icon(TYPE_RESOURCE); 784 rep.mInt1 = resId; 785 rep.mString1 = res.getResourcePackageName(resId); 786 return rep; 787 } 788 789 /** 790 * Create an Icon pointing to a drawable resource. 791 * @param resPackage Name of the package containing the resource in question 792 * @param resId ID of the drawable resource 793 */ createWithResource(String resPackage, @DrawableRes int resId)794 public static @NonNull Icon createWithResource(String resPackage, @DrawableRes int resId) { 795 if (resPackage == null) { 796 throw new IllegalArgumentException("Resource package name must not be null."); 797 } 798 final Icon rep = new Icon(TYPE_RESOURCE); 799 rep.mInt1 = resId; 800 rep.mString1 = resPackage; 801 return rep; 802 } 803 804 /** 805 * Create an Icon pointing to a drawable resource. 806 * @param resPackage Name of the package containing the resource in question 807 * @param resId ID of the drawable resource 808 * @param useMonochrome if this icon should use the monochrome res from the adaptive drawable 809 * @hide 810 */ createWithResourceAdaptiveDrawable(@onNull String resPackage, @DrawableRes int resId, boolean useMonochrome, float inset)811 public static @NonNull Icon createWithResourceAdaptiveDrawable(@NonNull String resPackage, 812 @DrawableRes int resId, boolean useMonochrome, float inset) { 813 if (resPackage == null) { 814 throw new IllegalArgumentException("Resource package name must not be null."); 815 } 816 final Icon rep = new Icon(TYPE_RESOURCE); 817 rep.mInt1 = resId; 818 rep.mUseMonochrome = useMonochrome; 819 rep.mInsetScale = inset; 820 rep.mString1 = resPackage; 821 return rep; 822 } 823 824 /** 825 * Create an Icon pointing to a bitmap in memory. 826 * @param bits A valid {@link android.graphics.Bitmap} object 827 */ createWithBitmap(Bitmap bits)828 public static @NonNull Icon createWithBitmap(Bitmap bits) { 829 if (bits == null) { 830 throw new IllegalArgumentException("Bitmap must not be null."); 831 } 832 final Icon rep = new Icon(TYPE_BITMAP); 833 rep.setBitmap(bits); 834 return rep; 835 } 836 837 /** 838 * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined 839 * by {@link AdaptiveIconDrawable}. 840 * @param bits A valid {@link android.graphics.Bitmap} object 841 */ createWithAdaptiveBitmap(Bitmap bits)842 public static @NonNull Icon createWithAdaptiveBitmap(Bitmap bits) { 843 if (bits == null) { 844 throw new IllegalArgumentException("Bitmap must not be null."); 845 } 846 final Icon rep = new Icon(TYPE_ADAPTIVE_BITMAP); 847 rep.setBitmap(bits); 848 return rep; 849 } 850 851 /** 852 * Create an Icon pointing to a compressed bitmap stored in a byte array. 853 * @param data Byte array storing compressed bitmap data of a type that 854 * {@link android.graphics.BitmapFactory} 855 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 856 * @param offset Offset into <code>data</code> at which the bitmap data starts 857 * @param length Length of the bitmap data 858 */ createWithData(byte[] data, int offset, int length)859 public static @NonNull Icon createWithData(byte[] data, int offset, int length) { 860 if (data == null) { 861 throw new IllegalArgumentException("Data must not be null."); 862 } 863 final Icon rep = new Icon(TYPE_DATA); 864 rep.mObj1 = data; 865 rep.mInt1 = length; 866 rep.mInt2 = offset; 867 return rep; 868 } 869 870 /** 871 * Create an Icon pointing to an image file specified by URI. 872 * 873 * @param uri A uri referring to local content:// or file:// image data. 874 */ createWithContentUri(String uri)875 public static @NonNull Icon createWithContentUri(String uri) { 876 if (uri == null) { 877 throw new IllegalArgumentException("Uri must not be null."); 878 } 879 final Icon rep = new Icon(TYPE_URI); 880 rep.mString1 = uri; 881 return rep; 882 } 883 884 /** 885 * Create an Icon pointing to an image file specified by URI. 886 * 887 * @param uri A uri referring to local content:// or file:// image data. 888 */ createWithContentUri(Uri uri)889 public static @NonNull Icon createWithContentUri(Uri uri) { 890 if (uri == null) { 891 throw new IllegalArgumentException("Uri must not be null."); 892 } 893 return createWithContentUri(uri.toString()); 894 } 895 896 /** 897 * Create an Icon pointing to an image file specified by URI. Image file should follow the icon 898 * design guideline defined by {@link AdaptiveIconDrawable}. 899 * 900 * @param uri A uri referring to local content:// or file:// image data. 901 */ createWithAdaptiveBitmapContentUri(@onNull String uri)902 public static @NonNull Icon createWithAdaptiveBitmapContentUri(@NonNull String uri) { 903 if (uri == null) { 904 throw new IllegalArgumentException("Uri must not be null."); 905 } 906 final Icon rep = new Icon(TYPE_URI_ADAPTIVE_BITMAP); 907 rep.mString1 = uri; 908 return rep; 909 } 910 911 /** 912 * Create an Icon pointing to an image file specified by URI. Image file should follow the icon 913 * design guideline defined by {@link AdaptiveIconDrawable}. 914 * 915 * @param uri A uri referring to local content:// or file:// image data. 916 */ 917 @NonNull createWithAdaptiveBitmapContentUri(@onNull Uri uri)918 public static Icon createWithAdaptiveBitmapContentUri(@NonNull Uri uri) { 919 if (uri == null) { 920 throw new IllegalArgumentException("Uri must not be null."); 921 } 922 return createWithAdaptiveBitmapContentUri(uri.toString()); 923 } 924 925 /** 926 * Store a color to use whenever this Icon is drawn. 927 * 928 * @param tint a color, as in {@link Drawable#setTint(int)} 929 * @return this same object, for use in chained construction 930 */ setTint(@olorInt int tint)931 public @NonNull Icon setTint(@ColorInt int tint) { 932 return setTintList(ColorStateList.valueOf(tint)); 933 } 934 935 /** 936 * Store a color to use whenever this Icon is drawn. 937 * 938 * @param tintList as in {@link Drawable#setTintList(ColorStateList)}, null to remove tint 939 * @return this same object, for use in chained construction 940 */ setTintList(ColorStateList tintList)941 public @NonNull Icon setTintList(ColorStateList tintList) { 942 mTintList = tintList; 943 return this; 944 } 945 946 /** @hide */ getTintList()947 public @Nullable ColorStateList getTintList() { 948 return mTintList; 949 } 950 951 /** 952 * Store a blending mode to use whenever this Icon is drawn. 953 * 954 * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null 955 * @return this same object, for use in chained construction 956 */ setTintMode(@onNull PorterDuff.Mode mode)957 public @NonNull Icon setTintMode(@NonNull PorterDuff.Mode mode) { 958 mBlendMode = BlendMode.fromValue(mode.nativeInt); 959 return this; 960 } 961 962 /** 963 * Store a blending mode to use whenever this Icon is drawn. 964 * 965 * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null 966 * @return this same object, for use in chained construction 967 */ setTintBlendMode(@onNull BlendMode mode)968 public @NonNull Icon setTintBlendMode(@NonNull BlendMode mode) { 969 mBlendMode = mode; 970 return this; 971 } 972 973 /** @hide */ getTintBlendMode()974 public @NonNull BlendMode getTintBlendMode() { 975 return mBlendMode; 976 } 977 978 /** @hide */ 979 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) hasTint()980 public boolean hasTint() { 981 return (mTintList != null) || (mBlendMode != DEFAULT_BLEND_MODE); 982 } 983 984 /** 985 * Create an Icon pointing to an image file specified by path. 986 * 987 * @param path A path to a file that contains compressed bitmap data of 988 * a type that {@link android.graphics.BitmapFactory} can decode. 989 */ createWithFilePath(String path)990 public static @NonNull Icon createWithFilePath(String path) { 991 if (path == null) { 992 throw new IllegalArgumentException("Path must not be null."); 993 } 994 final Icon rep = new Icon(TYPE_URI); 995 rep.mString1 = path; 996 return rep; 997 } 998 999 @Override toString()1000 public String toString() { 1001 final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); 1002 switch (mType) { 1003 case TYPE_BITMAP: 1004 case TYPE_ADAPTIVE_BITMAP: 1005 sb.append(" size=") 1006 .append(getBitmap().getWidth()) 1007 .append("x") 1008 .append(getBitmap().getHeight()); 1009 break; 1010 case TYPE_RESOURCE: 1011 sb.append(" pkg=") 1012 .append(getResPackage()) 1013 .append(" id=") 1014 .append(String.format("0x%08x", getResId())); 1015 break; 1016 case TYPE_DATA: 1017 sb.append(" len=").append(getDataLength()); 1018 if (getDataOffset() != 0) { 1019 sb.append(" off=").append(getDataOffset()); 1020 } 1021 break; 1022 case TYPE_URI: 1023 case TYPE_URI_ADAPTIVE_BITMAP: 1024 sb.append(" uri=").append(getUriString()); 1025 break; 1026 } 1027 if (mTintList != null) { 1028 sb.append(" tint="); 1029 String sep = ""; 1030 for (int c : mTintList.getColors()) { 1031 sb.append(String.format("%s0x%08x", sep, c)); 1032 sep = "|"; 1033 } 1034 } 1035 if (mBlendMode != DEFAULT_BLEND_MODE) sb.append(" mode=").append(mBlendMode); 1036 sb.append(")"); 1037 return sb.toString(); 1038 } 1039 1040 /** 1041 * Parcelable interface 1042 */ describeContents()1043 public int describeContents() { 1044 return (mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP || mType == TYPE_DATA) 1045 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 1046 } 1047 1048 // ===== Parcelable interface ====== 1049 Icon(Parcel in)1050 private Icon(Parcel in) { 1051 this(in.readInt()); 1052 switch (mType) { 1053 case TYPE_BITMAP: 1054 case TYPE_ADAPTIVE_BITMAP: 1055 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); 1056 mObj1 = bits; 1057 break; 1058 case TYPE_RESOURCE: 1059 final String pkg = in.readString(); 1060 final int resId = in.readInt(); 1061 mString1 = pkg; 1062 mInt1 = resId; 1063 mUseMonochrome = in.readBoolean(); 1064 mInsetScale = in.readFloat(); 1065 break; 1066 case TYPE_DATA: 1067 final int len = in.readInt(); 1068 final byte[] a = in.readBlob(); 1069 if (len != a.length) { 1070 throw new RuntimeException("internal unparceling error: blob length (" 1071 + a.length + ") != expected length (" + len + ")"); 1072 } 1073 mInt1 = len; 1074 mObj1 = a; 1075 break; 1076 case TYPE_URI: 1077 case TYPE_URI_ADAPTIVE_BITMAP: 1078 final String uri = in.readString(); 1079 mString1 = uri; 1080 break; 1081 default: 1082 throw new RuntimeException("invalid " 1083 + this.getClass().getSimpleName() + " type in parcel: " + mType); 1084 } 1085 if (in.readInt() == 1) { 1086 mTintList = ColorStateList.CREATOR.createFromParcel(in); 1087 } 1088 mBlendMode = BlendMode.fromValue(in.readInt()); 1089 } 1090 1091 @Override writeToParcel(Parcel dest, int flags)1092 public void writeToParcel(Parcel dest, int flags) { 1093 dest.writeInt(mType); 1094 switch (mType) { 1095 case TYPE_BITMAP: 1096 case TYPE_ADAPTIVE_BITMAP: 1097 if (!mCachedAshmem) { 1098 mObj1 = ((Bitmap) mObj1).asShared(); 1099 mCachedAshmem = true; 1100 } 1101 getBitmap().writeToParcel(dest, flags); 1102 break; 1103 case TYPE_RESOURCE: 1104 dest.writeString(getResPackage()); 1105 dest.writeInt(getResId()); 1106 dest.writeBoolean(mUseMonochrome); 1107 dest.writeFloat(mInsetScale); 1108 break; 1109 case TYPE_DATA: 1110 dest.writeInt(getDataLength()); 1111 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); 1112 break; 1113 case TYPE_URI: 1114 case TYPE_URI_ADAPTIVE_BITMAP: 1115 dest.writeString(getUriString()); 1116 break; 1117 } 1118 if (mTintList == null) { 1119 dest.writeInt(0); 1120 } else { 1121 dest.writeInt(1); 1122 mTintList.writeToParcel(dest, flags); 1123 } 1124 dest.writeInt(BlendMode.toValue(mBlendMode)); 1125 } 1126 1127 public static final @android.annotation.NonNull Parcelable.Creator<Icon> CREATOR 1128 = new Parcelable.Creator<Icon>() { 1129 public Icon createFromParcel(Parcel in) { 1130 return new Icon(in); 1131 } 1132 1133 public Icon[] newArray(int size) { 1134 return new Icon[size]; 1135 } 1136 }; 1137 1138 /** 1139 * Scale down a bitmap to a given max width and max height. The scaling will be done in a uniform way 1140 * @param bitmap the bitmap to scale down 1141 * @param maxWidth the maximum width allowed 1142 * @param maxHeight the maximum height allowed 1143 * 1144 * @return the scaled bitmap if necessary or the original bitmap if no scaling was needed 1145 * @hide 1146 */ scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight)1147 public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) { 1148 int bitmapWidth = bitmap.getWidth(); 1149 int bitmapHeight = bitmap.getHeight(); 1150 if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) { 1151 float scale = Math.min((float) maxWidth / bitmapWidth, 1152 (float) maxHeight / bitmapHeight); 1153 bitmap = Bitmap.createScaledBitmap(bitmap, 1154 Math.max(1, (int) (scale * bitmapWidth)), 1155 Math.max(1, (int) (scale * bitmapHeight)), 1156 true /* filter */); 1157 } 1158 return bitmap; 1159 } 1160 1161 /** 1162 * Scale down this icon to a given max width and max height. 1163 * The scaling will be done in a uniform way and currently only bitmaps are supported. 1164 * @param maxWidth the maximum width allowed 1165 * @param maxHeight the maximum height allowed 1166 * 1167 * @hide 1168 */ scaleDownIfNecessary(int maxWidth, int maxHeight)1169 public void scaleDownIfNecessary(int maxWidth, int maxHeight) { 1170 if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) { 1171 return; 1172 } 1173 Bitmap bitmap = getBitmap(); 1174 setBitmap(scaleDownIfNecessary(bitmap, maxWidth, maxHeight)); 1175 } 1176 1177 /** 1178 * Implement this interface to receive a callback when 1179 * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync} 1180 * is finished and your Drawable is ready. 1181 */ 1182 public interface OnDrawableLoadedListener { onDrawableLoaded(Drawable d)1183 void onDrawableLoaded(Drawable d); 1184 } 1185 1186 /** 1187 * Wrapper around loadDrawable that does its work on a pooled thread and then 1188 * fires back the given (targeted) Message. 1189 */ 1190 private class LoadDrawableTask implements Runnable { 1191 final Context mContext; 1192 final Message mMessage; 1193 LoadDrawableTask(Context context, final Handler handler, final OnDrawableLoadedListener listener)1194 public LoadDrawableTask(Context context, final Handler handler, 1195 final OnDrawableLoadedListener listener) { 1196 mContext = context; 1197 mMessage = Message.obtain(handler, new Runnable() { 1198 @Override 1199 public void run() { 1200 listener.onDrawableLoaded((Drawable) mMessage.obj); 1201 } 1202 }); 1203 } 1204 LoadDrawableTask(Context context, Message message)1205 public LoadDrawableTask(Context context, Message message) { 1206 mContext = context; 1207 mMessage = message; 1208 } 1209 1210 @Override run()1211 public void run() { 1212 mMessage.obj = loadDrawable(mContext); 1213 mMessage.sendToTarget(); 1214 } 1215 runAsync()1216 public void runAsync() { 1217 AsyncTask.THREAD_POOL_EXECUTOR.execute(this); 1218 } 1219 } 1220 } 1221