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