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