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