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.annotation.IdRes;
     22 import android.annotation.IntDef;
     23 import android.annotation.NonNull;
     24 import android.content.ContentResolver;
     25 import android.content.Context;
     26 import android.content.pm.ApplicationInfo;
     27 import android.content.pm.PackageManager;
     28 import android.content.res.ColorStateList;
     29 import android.content.res.Resources;
     30 import android.graphics.Bitmap;
     31 import android.graphics.BitmapFactory;
     32 import android.graphics.PorterDuff;
     33 import android.net.Uri;
     34 import android.os.AsyncTask;
     35 import android.os.Handler;
     36 import android.os.Message;
     37 import android.os.Parcel;
     38 import android.os.Parcelable;
     39 import android.text.TextUtils;
     40 import android.util.Log;
     41 
     42 import java.io.DataInputStream;
     43 import java.io.DataOutputStream;
     44 import java.io.File;
     45 import java.io.FileInputStream;
     46 import java.io.FileNotFoundException;
     47 import java.io.IOException;
     48 import java.io.InputStream;
     49 import java.io.OutputStream;
     50 import java.util.Arrays;
     51 import 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 
     63 public 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