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