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