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