Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.graphics.drawable;
     18 
     19 import android.annotation.ColorInt;
     20 import android.annotation.DrawableRes;
     21 import android.content.res.ColorStateList;
     22 import android.content.ContentResolver;
     23 import android.content.Context;
     24 import android.content.pm.ApplicationInfo;
     25 import android.content.pm.PackageManager;
     26 import android.content.res.Resources;
     27 import android.graphics.Bitmap;
     28 import android.graphics.BitmapFactory;
     29 import android.graphics.PorterDuff;
     30 import android.net.Uri;
     31 import android.os.AsyncTask;
     32 import android.os.Handler;
     33 import android.os.Message;
     34 import android.os.Parcel;
     35 import android.os.Parcelable;
     36 import android.text.TextUtils;
     37 import android.util.Log;
     38 
     39 import java.io.DataInputStream;
     40 import java.io.DataOutputStream;
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileNotFoundException;
     44 import java.io.IOException;
     45 import java.io.InputStream;
     46 import java.io.OutputStream;
     47 import java.util.Arrays;
     48 import 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 
     60 public 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      * Scale down a bitmap to a given max width and max height. The scaling will be done in a uniform way
    809      * @param bitmap the bitmap to scale down
    810      * @param maxWidth the maximum width allowed
    811      * @param maxHeight the maximum height allowed
    812      *
    813      * @return the scaled bitmap if necessary or the original bitmap if no scaling was needed
    814      * @hide
    815      */
    816     public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) {
    817         int bitmapWidth = bitmap.getWidth();
    818         int bitmapHeight = bitmap.getHeight();
    819         if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
    820             float scale = Math.min((float) maxWidth / bitmapWidth,
    821                     (float) maxHeight / bitmapHeight);
    822             bitmap = Bitmap.createScaledBitmap(bitmap, (int) (scale * bitmapWidth),
    823                     (int) (scale * bitmapHeight), true /* filter */);
    824         }
    825         return bitmap;
    826     }
    827 
    828     /**
    829      * Scale down this icon to a given max width and max height.
    830      * The scaling will be done in a uniform way and currently only bitmaps are supported.
    831      * @param maxWidth the maximum width allowed
    832      * @param maxHeight the maximum height allowed
    833      *
    834      * @hide
    835      */
    836     public void scaleDownIfNecessary(int maxWidth, int maxHeight) {
    837         if (mType != TYPE_BITMAP && mType != TYPE_ADAPTIVE_BITMAP) {
    838             return;
    839         }
    840         Bitmap bitmap = getBitmap();
    841         setBitmap(scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
    842     }
    843 
    844     /**
    845      * Implement this interface to receive a callback when
    846      * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync}
    847      * is finished and your Drawable is ready.
    848      */
    849     public interface OnDrawableLoadedListener {
    850         void onDrawableLoaded(Drawable d);
    851     }
    852 
    853     /**
    854      * Wrapper around loadDrawable that does its work on a pooled thread and then
    855      * fires back the given (targeted) Message.
    856      */
    857     private class LoadDrawableTask implements Runnable {
    858         final Context mContext;
    859         final Message mMessage;
    860 
    861         public LoadDrawableTask(Context context, final Handler handler,
    862                 final OnDrawableLoadedListener listener) {
    863             mContext = context;
    864             mMessage = Message.obtain(handler, new Runnable() {
    865                     @Override
    866                     public void run() {
    867                         listener.onDrawableLoaded((Drawable) mMessage.obj);
    868                     }
    869                 });
    870         }
    871 
    872         public LoadDrawableTask(Context context, Message message) {
    873             mContext = context;
    874             mMessage = message;
    875         }
    876 
    877         @Override
    878         public void run() {
    879             mMessage.obj = loadDrawable(mContext);
    880             mMessage.sendToTarget();
    881         }
    882 
    883         public void runAsync() {
    884             AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
    885         }
    886     }
    887 }
    888