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