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