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