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