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