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