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