• 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                 try (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                 } catch (IOException e) {
509                     throw new IllegalStateException(e);
510                 }
511                 break;
512             case TYPE_URI_ADAPTIVE_BITMAP:
513                 try (InputStream is = getUriInputStream(context)) {
514                     if (is != null) {
515                         final Bitmap bitmap = BitmapFactory.decodeStream(is);
516                         if (bitmap == null) {
517                             Log.w(TAG, "Unable to decode image from URI: " + getUriString());
518                             if (iconLoadDrawableReturnNullWhenUriDecodeFails()) {
519                                 return null;
520                             }
521                         }
522                         return new AdaptiveIconDrawable(
523                                         null, new BitmapDrawable(context.getResources(),
524                                 fixMaxBitmapSize(bitmap)));
525                     }
526                 } catch (IOException e) {
527                     throw new IllegalStateException(e);
528                 }
529                 break;
530         }
531         return null;
532     }
533 
getUriInputStream(Context context)534     private @Nullable InputStream getUriInputStream(Context context) {
535         final Uri uri = getUri();
536         final String scheme = uri.getScheme();
537         if (ContentResolver.SCHEME_CONTENT.equals(scheme)
538                 || ContentResolver.SCHEME_FILE.equals(scheme)) {
539             try {
540                 return context.getContentResolver().openInputStream(uri);
541             } catch (Exception e) {
542                 Log.w(TAG, "Unable to load image from URI: " + uri, e);
543             }
544         } else {
545             try {
546                 return new FileInputStream(new File(mString1));
547             } catch (FileNotFoundException e) {
548                 Log.w(TAG, "Unable to load image from path: " + uri, e);
549             }
550         }
551         return null;
552     }
553 
554     /**
555      * Load the requested resources under the given userId, if the system allows it,
556      * before actually loading the drawable.
557      *
558      * @hide
559      */
loadDrawableAsUser(Context context, int userId)560     public Drawable loadDrawableAsUser(Context context, int userId) {
561         if (mType == TYPE_RESOURCE) {
562             String resPackage = getResPackage();
563             if (TextUtils.isEmpty(resPackage)) {
564                 resPackage = context.getPackageName();
565             }
566             if (getResources() == null && !(getResPackage().equals("android"))) {
567                 // TODO(b/173307037): Move CONTEXT_INCLUDE_CODE to ContextImpl.createContextAsUser
568                 final Context userContext;
569                 if (context.getUserId() == userId) {
570                     userContext = context;
571                 } else {
572                     final boolean sameAppWithProcess =
573                             UserHandle.isSameApp(context.getApplicationInfo().uid, Process.myUid());
574                     final int flags = (sameAppWithProcess ? CONTEXT_INCLUDE_CODE : 0)
575                             | CONTEXT_RESTRICTED;
576                     userContext = context.createContextAsUser(UserHandle.of(userId), flags);
577                 }
578 
579                 final PackageManager pm = userContext.getPackageManager();
580                 try {
581                     // assign getResources() as the correct user
582                     mObj1 = pm.getResourcesForApplication(resPackage);
583                 } catch (PackageManager.NameNotFoundException e) {
584                     Log.e(TAG, String.format("Unable to find pkg=%s user=%d",
585                                     getResPackage(),
586                                     userId),
587                             e);
588                 }
589             }
590         }
591         return loadDrawable(context);
592     }
593 
594     /**
595      * Load a drawable, but in the case of URI types, it will check if the passed uid has a grant
596      * to load the resource. The check will be performed using the permissions of the passed uid,
597      * and not those of the caller.
598      * <p>
599      * This should be called for {@link Icon} objects that come from a not trusted source and may
600      * contain a URI.
601      *
602      * After the check, if passed, {@link #loadDrawable} will be called. If failed, this will
603      * return {@code null}.
604      *
605      * @see #loadDrawable
606      *
607      * @hide
608      */
609     @Nullable
610     @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
loadDrawableCheckingUriGrant( Context context, IUriGrantsManager iugm, int callingUid, String packageName )611     public Drawable loadDrawableCheckingUriGrant(
612             Context context,
613             IUriGrantsManager iugm,
614             int callingUid,
615             String packageName
616     ) {
617         if (getType() == TYPE_URI || getType() == TYPE_URI_ADAPTIVE_BITMAP) {
618             try {
619                 iugm.checkGrantUriPermission_ignoreNonSystem(
620                         callingUid,
621                         packageName,
622                         ContentProvider.getUriWithoutUserId(getUri()),
623                         Intent.FLAG_GRANT_READ_URI_PERMISSION,
624                         ContentProvider.getUserIdFromUri(getUri())
625                 );
626             } catch (SecurityException | RemoteException e) {
627                 Log.e(TAG, "Failed to get URI permission for: " + getUri(), e);
628                 return null;
629             }
630         }
631         return loadDrawable(context);
632     }
633 
634     /** @hide */
635     public static final int MIN_ASHMEM_ICON_SIZE = 128 * (1 << 10);
636 
637     /**
638      * Puts the memory used by this instance into Ashmem memory, if possible.
639      * @hide
640      */
convertToAshmem()641     public void convertToAshmem() {
642         if ((mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP) &&
643             getBitmap().isMutable() &&
644             getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) {
645             setBitmap(getBitmap().asShared());
646         }
647         mCachedAshmem = true;
648     }
649 
650     /**
651      * Writes a serialized version of an Icon to the specified stream.
652      *
653      * @param stream The stream on which to serialize the Icon.
654      * @hide
655      */
writeToStream(@onNull OutputStream stream)656     public void writeToStream(@NonNull OutputStream stream) throws IOException {
657         DataOutputStream dataStream = new DataOutputStream(stream);
658 
659         dataStream.writeInt(VERSION_STREAM_SERIALIZER);
660         dataStream.writeByte(mType);
661 
662         switch (mType) {
663             case TYPE_BITMAP:
664             case TYPE_ADAPTIVE_BITMAP:
665                 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream);
666                 break;
667             case TYPE_DATA:
668                 dataStream.writeInt(getDataLength());
669                 dataStream.write(getDataBytes(), getDataOffset(), getDataLength());
670                 break;
671             case TYPE_RESOURCE:
672                 dataStream.writeUTF(getResPackage());
673                 dataStream.writeInt(getResId());
674                 break;
675             case TYPE_URI:
676             case TYPE_URI_ADAPTIVE_BITMAP:
677                 dataStream.writeUTF(getUriString());
678                 break;
679         }
680     }
681 
Icon(int mType)682     private Icon(int mType) {
683         this.mType = mType;
684     }
685 
686     /**
687      * Create an Icon from the specified stream.
688      *
689      * @param stream The input stream from which to reconstruct the Icon.
690      * @hide
691      */
createFromStream(@onNull InputStream stream)692     public static @Nullable Icon createFromStream(@NonNull InputStream stream) throws IOException {
693         DataInputStream inputStream = new DataInputStream(stream);
694 
695         final int version = inputStream.readInt();
696         if (version >= VERSION_STREAM_SERIALIZER) {
697             final int type = inputStream.readByte();
698             switch (type) {
699                 case TYPE_BITMAP:
700                     return createWithBitmap(BitmapFactory.decodeStream(inputStream));
701                 case TYPE_ADAPTIVE_BITMAP:
702                     return createWithAdaptiveBitmap(BitmapFactory.decodeStream(inputStream));
703                 case TYPE_DATA:
704                     final int length = inputStream.readInt();
705                     final byte[] data = new byte[length];
706                     inputStream.read(data, 0 /* offset */, length);
707                     return createWithData(data, 0 /* offset */, length);
708                 case TYPE_RESOURCE:
709                     final String packageName = inputStream.readUTF();
710                     final int resId = inputStream.readInt();
711                     return createWithResource(packageName, resId);
712                 case TYPE_URI:
713                     final String uriOrPath = inputStream.readUTF();
714                     return createWithContentUri(uriOrPath);
715                 case TYPE_URI_ADAPTIVE_BITMAP:
716                     final String uri = inputStream.readUTF();
717                     return createWithAdaptiveBitmapContentUri(uri);
718             }
719         }
720         return null;
721     }
722 
723     /**
724      * Compares if this icon is constructed from the same resources as another icon.
725      * Note that this is an inexpensive operation and doesn't do deep Bitmap equality comparisons.
726      *
727      * @param otherIcon the other icon
728      * @return whether this icon is the same as the another one
729      * @hide
730      */
sameAs(@onNull Icon otherIcon)731     public boolean sameAs(@NonNull Icon otherIcon) {
732         if (otherIcon == this) {
733             return true;
734         }
735         if (mType != otherIcon.getType()) {
736             return false;
737         }
738         switch (mType) {
739             case TYPE_BITMAP:
740             case TYPE_ADAPTIVE_BITMAP:
741                 return getBitmap() == otherIcon.getBitmap();
742             case TYPE_DATA:
743                 return getDataLength() == otherIcon.getDataLength()
744                         && getDataOffset() == otherIcon.getDataOffset()
745                         && Arrays.equals(getDataBytes(), otherIcon.getDataBytes());
746             case TYPE_RESOURCE:
747                 return getResId() == otherIcon.getResId()
748                         && Objects.equals(getResPackage(), otherIcon.getResPackage())
749                         && mUseMonochrome == otherIcon.mUseMonochrome
750                         && mInsetScale == otherIcon.mInsetScale;
751             case TYPE_URI:
752             case TYPE_URI_ADAPTIVE_BITMAP:
753                 return Objects.equals(getUriString(), otherIcon.getUriString());
754         }
755         return false;
756     }
757 
758     /**
759      * Create an Icon pointing to a drawable resource.
760      * @param context The context for the application whose resources should be used to resolve the
761      *                given resource ID.
762      * @param resId ID of the drawable resource
763      */
createWithResource(Context context, @DrawableRes int resId)764     public static @NonNull Icon createWithResource(Context context, @DrawableRes int resId) {
765         if (context == null) {
766             throw new IllegalArgumentException("Context must not be null.");
767         }
768         final Icon rep = new Icon(TYPE_RESOURCE);
769         rep.mInt1 = resId;
770         rep.mString1 = context.getPackageName();
771         return rep;
772     }
773 
774     /**
775      * Version of createWithResource that takes Resources. Do not use.
776      * @hide
777      */
778     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
createWithResource(Resources res, @DrawableRes int resId)779     public static @NonNull Icon createWithResource(Resources res, @DrawableRes int resId) {
780         if (res == null) {
781             throw new IllegalArgumentException("Resource must not be null.");
782         }
783         final Icon rep = new Icon(TYPE_RESOURCE);
784         rep.mInt1 = resId;
785         rep.mString1 = res.getResourcePackageName(resId);
786         return rep;
787     }
788 
789     /**
790      * Create an Icon pointing to a drawable resource.
791      * @param resPackage Name of the package containing the resource in question
792      * @param resId ID of the drawable resource
793      */
createWithResource(String resPackage, @DrawableRes int resId)794     public static @NonNull Icon createWithResource(String resPackage, @DrawableRes int resId) {
795         if (resPackage == null) {
796             throw new IllegalArgumentException("Resource package name must not be null.");
797         }
798         final Icon rep = new Icon(TYPE_RESOURCE);
799         rep.mInt1 = resId;
800         rep.mString1 = resPackage;
801         return rep;
802     }
803 
804     /**
805      * Create an Icon pointing to a drawable resource.
806      * @param resPackage Name of the package containing the resource in question
807      * @param resId ID of the drawable resource
808      * @param useMonochrome if this icon should use the monochrome res from the adaptive drawable
809      * @hide
810      */
createWithResourceAdaptiveDrawable(@onNull String resPackage, @DrawableRes int resId, boolean useMonochrome, float inset)811     public static @NonNull Icon createWithResourceAdaptiveDrawable(@NonNull String resPackage,
812             @DrawableRes int resId, boolean useMonochrome, float inset) {
813         if (resPackage == null) {
814             throw new IllegalArgumentException("Resource package name must not be null.");
815         }
816         final Icon rep = new Icon(TYPE_RESOURCE);
817         rep.mInt1 = resId;
818         rep.mUseMonochrome = useMonochrome;
819         rep.mInsetScale = inset;
820         rep.mString1 = resPackage;
821         return rep;
822     }
823 
824     /**
825      * Create an Icon pointing to a bitmap in memory.
826      * @param bits A valid {@link android.graphics.Bitmap} object
827      */
createWithBitmap(Bitmap bits)828     public static @NonNull Icon createWithBitmap(Bitmap bits) {
829         if (bits == null) {
830             throw new IllegalArgumentException("Bitmap must not be null.");
831         }
832         final Icon rep = new Icon(TYPE_BITMAP);
833         rep.setBitmap(bits);
834         return rep;
835     }
836 
837     /**
838      * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined
839      * by {@link AdaptiveIconDrawable}.
840      * @param bits A valid {@link android.graphics.Bitmap} object
841      */
createWithAdaptiveBitmap(Bitmap bits)842     public static @NonNull Icon createWithAdaptiveBitmap(Bitmap bits) {
843         if (bits == null) {
844             throw new IllegalArgumentException("Bitmap must not be null.");
845         }
846         final Icon rep = new Icon(TYPE_ADAPTIVE_BITMAP);
847         rep.setBitmap(bits);
848         return rep;
849     }
850 
851     /**
852      * Create an Icon pointing to a compressed bitmap stored in a byte array.
853      * @param data Byte array storing compressed bitmap data of a type that
854      *             {@link android.graphics.BitmapFactory}
855      *             can decode (see {@link android.graphics.Bitmap.CompressFormat}).
856      * @param offset Offset into <code>data</code> at which the bitmap data starts
857      * @param length Length of the bitmap data
858      */
createWithData(byte[] data, int offset, int length)859     public static @NonNull Icon createWithData(byte[] data, int offset, int length) {
860         if (data == null) {
861             throw new IllegalArgumentException("Data must not be null.");
862         }
863         final Icon rep = new Icon(TYPE_DATA);
864         rep.mObj1 = data;
865         rep.mInt1 = length;
866         rep.mInt2 = offset;
867         return rep;
868     }
869 
870     /**
871      * Create an Icon pointing to an image file specified by URI.
872      *
873      * @param uri A uri referring to local content:// or file:// image data.
874      */
createWithContentUri(String uri)875     public static @NonNull Icon createWithContentUri(String uri) {
876         if (uri == null) {
877             throw new IllegalArgumentException("Uri must not be null.");
878         }
879         final Icon rep = new Icon(TYPE_URI);
880         rep.mString1 = uri;
881         return rep;
882     }
883 
884     /**
885      * Create an Icon pointing to an image file specified by URI.
886      *
887      * @param uri A uri referring to local content:// or file:// image data.
888      */
createWithContentUri(Uri uri)889     public static @NonNull Icon createWithContentUri(Uri uri) {
890         if (uri == null) {
891             throw new IllegalArgumentException("Uri must not be null.");
892         }
893         return createWithContentUri(uri.toString());
894     }
895 
896     /**
897      * Create an Icon pointing to an image file specified by URI. Image file should follow the icon
898      * design guideline defined by {@link AdaptiveIconDrawable}.
899      *
900      * @param uri A uri referring to local content:// or file:// image data.
901      */
createWithAdaptiveBitmapContentUri(@onNull String uri)902     public static @NonNull Icon createWithAdaptiveBitmapContentUri(@NonNull String uri) {
903         if (uri == null) {
904             throw new IllegalArgumentException("Uri must not be null.");
905         }
906         final Icon rep = new Icon(TYPE_URI_ADAPTIVE_BITMAP);
907         rep.mString1 = uri;
908         return rep;
909     }
910 
911     /**
912      * Create an Icon pointing to an image file specified by URI. Image file should follow the icon
913      * design guideline defined by {@link AdaptiveIconDrawable}.
914      *
915      * @param uri A uri referring to local content:// or file:// image data.
916      */
917     @NonNull
createWithAdaptiveBitmapContentUri(@onNull Uri uri)918     public static Icon createWithAdaptiveBitmapContentUri(@NonNull Uri uri) {
919         if (uri == null) {
920             throw new IllegalArgumentException("Uri must not be null.");
921         }
922         return createWithAdaptiveBitmapContentUri(uri.toString());
923     }
924 
925     /**
926      * Store a color to use whenever this Icon is drawn.
927      *
928      * @param tint a color, as in {@link Drawable#setTint(int)}
929      * @return this same object, for use in chained construction
930      */
setTint(@olorInt int tint)931     public @NonNull Icon setTint(@ColorInt int tint) {
932         return setTintList(ColorStateList.valueOf(tint));
933     }
934 
935     /**
936      * Store a color to use whenever this Icon is drawn.
937      *
938      * @param tintList as in {@link Drawable#setTintList(ColorStateList)}, null to remove tint
939      * @return this same object, for use in chained construction
940      */
setTintList(ColorStateList tintList)941     public @NonNull Icon setTintList(ColorStateList tintList) {
942         mTintList = tintList;
943         return this;
944     }
945 
946     /** @hide */
getTintList()947     public @Nullable ColorStateList getTintList() {
948         return mTintList;
949     }
950 
951     /**
952      * Store a blending mode to use whenever this Icon is drawn.
953      *
954      * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null
955      * @return this same object, for use in chained construction
956      */
setTintMode(@onNull PorterDuff.Mode mode)957     public @NonNull Icon setTintMode(@NonNull PorterDuff.Mode mode) {
958         mBlendMode = BlendMode.fromValue(mode.nativeInt);
959         return this;
960     }
961 
962     /**
963      * Store a blending mode to use whenever this Icon is drawn.
964      *
965      * @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null
966      * @return this same object, for use in chained construction
967      */
setTintBlendMode(@onNull BlendMode mode)968     public @NonNull Icon setTintBlendMode(@NonNull BlendMode mode) {
969         mBlendMode = mode;
970         return this;
971     }
972 
973     /** @hide */
getTintBlendMode()974     public @NonNull BlendMode getTintBlendMode() {
975         return mBlendMode;
976     }
977 
978     /** @hide */
979     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
hasTint()980     public boolean hasTint() {
981         return (mTintList != null) || (mBlendMode != DEFAULT_BLEND_MODE);
982     }
983 
984     /**
985      * Create an Icon pointing to an image file specified by path.
986      *
987      * @param path A path to a file that contains compressed bitmap data of
988      *           a type that {@link android.graphics.BitmapFactory} can decode.
989      */
createWithFilePath(String path)990     public static @NonNull Icon createWithFilePath(String path) {
991         if (path == null) {
992             throw new IllegalArgumentException("Path must not be null.");
993         }
994         final Icon rep = new Icon(TYPE_URI);
995         rep.mString1 = path;
996         return rep;
997     }
998 
999     @Override
toString()1000     public String toString() {
1001         final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
1002         switch (mType) {
1003             case TYPE_BITMAP:
1004             case TYPE_ADAPTIVE_BITMAP:
1005                 sb.append(" size=")
1006                         .append(getBitmap().getWidth())
1007                         .append("x")
1008                         .append(getBitmap().getHeight());
1009                 break;
1010             case TYPE_RESOURCE:
1011                 sb.append(" pkg=")
1012                         .append(getResPackage())
1013                         .append(" id=")
1014                         .append(String.format("0x%08x", getResId()));
1015                 break;
1016             case TYPE_DATA:
1017                 sb.append(" len=").append(getDataLength());
1018                 if (getDataOffset() != 0) {
1019                     sb.append(" off=").append(getDataOffset());
1020                 }
1021                 break;
1022             case TYPE_URI:
1023             case TYPE_URI_ADAPTIVE_BITMAP:
1024                 sb.append(" uri=").append(getUriString());
1025                 break;
1026         }
1027         if (mTintList != null) {
1028             sb.append(" tint=");
1029             String sep = "";
1030             for (int c : mTintList.getColors()) {
1031                 sb.append(String.format("%s0x%08x", sep, c));
1032                 sep = "|";
1033             }
1034         }
1035         if (mBlendMode != DEFAULT_BLEND_MODE) sb.append(" mode=").append(mBlendMode);
1036         sb.append(")");
1037         return sb.toString();
1038     }
1039 
1040     /**
1041      * Parcelable interface
1042      */
describeContents()1043     public int describeContents() {
1044         return (mType == TYPE_BITMAP || mType == TYPE_ADAPTIVE_BITMAP || mType == TYPE_DATA)
1045                 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
1046     }
1047 
1048     // ===== Parcelable interface ======
1049 
Icon(Parcel in)1050     private Icon(Parcel in) {
1051         this(in.readInt());
1052         switch (mType) {
1053             case TYPE_BITMAP:
1054             case TYPE_ADAPTIVE_BITMAP:
1055                 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in);
1056                 mObj1 = bits;
1057                 break;
1058             case TYPE_RESOURCE:
1059                 final String pkg = in.readString();
1060                 final int resId = in.readInt();
1061                 mString1 = pkg;
1062                 mInt1 = resId;
1063                 mUseMonochrome = in.readBoolean();
1064                 mInsetScale = in.readFloat();
1065                 break;
1066             case TYPE_DATA:
1067                 final int len = in.readInt();
1068                 final byte[] a = in.readBlob();
1069                 if (len != a.length) {
1070                     throw new RuntimeException("internal unparceling error: blob length ("
1071                             + a.length + ") != expected length (" + len + ")");
1072                 }
1073                 mInt1 = len;
1074                 mObj1 = a;
1075                 break;
1076             case TYPE_URI:
1077             case TYPE_URI_ADAPTIVE_BITMAP:
1078                 final String uri = in.readString();
1079                 mString1 = uri;
1080                 break;
1081             default:
1082                 throw new RuntimeException("invalid "
1083                         + this.getClass().getSimpleName() + " type in parcel: " + mType);
1084         }
1085         if (in.readInt() == 1) {
1086             mTintList = ColorStateList.CREATOR.createFromParcel(in);
1087         }
1088         mBlendMode = BlendMode.fromValue(in.readInt());
1089     }
1090 
1091     @Override
writeToParcel(Parcel dest, int flags)1092     public void writeToParcel(Parcel dest, int flags) {
1093         dest.writeInt(mType);
1094         switch (mType) {
1095             case TYPE_BITMAP:
1096             case TYPE_ADAPTIVE_BITMAP:
1097                 if (!mCachedAshmem) {
1098                     mObj1 = ((Bitmap) mObj1).asShared();
1099                     mCachedAshmem = true;
1100                 }
1101                 getBitmap().writeToParcel(dest, flags);
1102                 break;
1103             case TYPE_RESOURCE:
1104                 dest.writeString(getResPackage());
1105                 dest.writeInt(getResId());
1106                 dest.writeBoolean(mUseMonochrome);
1107                 dest.writeFloat(mInsetScale);
1108                 break;
1109             case TYPE_DATA:
1110                 dest.writeInt(getDataLength());
1111                 dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength());
1112                 break;
1113             case TYPE_URI:
1114             case TYPE_URI_ADAPTIVE_BITMAP:
1115                 dest.writeString(getUriString());
1116                 break;
1117         }
1118         if (mTintList == null) {
1119             dest.writeInt(0);
1120         } else {
1121             dest.writeInt(1);
1122             mTintList.writeToParcel(dest, flags);
1123         }
1124         dest.writeInt(BlendMode.toValue(mBlendMode));
1125     }
1126 
1127     public static final @android.annotation.NonNull Parcelable.Creator<Icon> CREATOR
1128             = new Parcelable.Creator<Icon>() {
1129         public Icon createFromParcel(Parcel in) {
1130             return new Icon(in);
1131         }
1132 
1133         public Icon[] newArray(int size) {
1134             return new Icon[size];
1135         }
1136     };
1137 
1138     /**
1139      * Scale down a bitmap to a given max width and max height. The scaling will be done in a uniform way
1140      * @param bitmap the bitmap to scale down
1141      * @param maxWidth the maximum width allowed
1142      * @param maxHeight the maximum height allowed
1143      *
1144      * @return the scaled bitmap if necessary or the original bitmap if no scaling was needed
1145      * @hide
1146      */
scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight)1147     public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) {
1148         int bitmapWidth = bitmap.getWidth();
1149         int bitmapHeight = bitmap.getHeight();
1150         if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
1151             float scale = Math.min((float) maxWidth / bitmapWidth,
1152                     (float) maxHeight / bitmapHeight);
1153             bitmap = Bitmap.createScaledBitmap(bitmap,
1154                     Math.max(1, (int) (scale * bitmapWidth)),
1155                     Math.max(1, (int) (scale * bitmapHeight)),
1156                     true /* filter */);
1157         }
1158         return bitmap;
1159     }
1160 
1161     /**
1162      * Scale down this icon to a given max width and max height.
1163      * The scaling will be done in a uniform way and currently only bitmaps are supported.
1164      * @param maxWidth the maximum width allowed
1165      * @param maxHeight the maximum height allowed
1166      *
1167      * @hide
1168      */
scaleDownIfNecessary(int maxWidth, int maxHeight)1169     public void scaleDownIfNecessary(int maxWidth, int maxHeight) {
1170         if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) {
1171             return;
1172         }
1173         Bitmap bitmap = getBitmap();
1174         setBitmap(scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
1175     }
1176 
1177     /**
1178      * Implement this interface to receive a callback when
1179      * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync}
1180      * is finished and your Drawable is ready.
1181      */
1182     public interface OnDrawableLoadedListener {
onDrawableLoaded(Drawable d)1183         void onDrawableLoaded(Drawable d);
1184     }
1185 
1186     /**
1187      * Wrapper around loadDrawable that does its work on a pooled thread and then
1188      * fires back the given (targeted) Message.
1189      */
1190     private class LoadDrawableTask implements Runnable {
1191         final Context mContext;
1192         final Message mMessage;
1193 
LoadDrawableTask(Context context, final Handler handler, final OnDrawableLoadedListener listener)1194         public LoadDrawableTask(Context context, final Handler handler,
1195                 final OnDrawableLoadedListener listener) {
1196             mContext = context;
1197             mMessage = Message.obtain(handler, new Runnable() {
1198                     @Override
1199                     public void run() {
1200                         listener.onDrawableLoaded((Drawable) mMessage.obj);
1201                     }
1202                 });
1203         }
1204 
LoadDrawableTask(Context context, Message message)1205         public LoadDrawableTask(Context context, Message message) {
1206             mContext = context;
1207             mMessage = message;
1208         }
1209 
1210         @Override
run()1211         public void run() {
1212             mMessage.obj = loadDrawable(mContext);
1213             mMessage.sendToTarget();
1214         }
1215 
runAsync()1216         public void runAsync() {
1217             AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
1218         }
1219     }
1220 }
1221