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