Icon.java revision 877d696c382ecb8a97972450c8819536641a963c
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.DrawableRes;
20import android.content.ContentResolver;
21import android.content.pm.PackageManager;
22import android.content.res.Resources;
23import android.content.Context;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.net.Uri;
27import android.os.AsyncTask;
28import android.os.Handler;
29import android.os.Message;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.os.UserHandle;
33import android.util.Log;
34
35import java.io.File;
36import java.io.FileInputStream;
37import java.io.FileNotFoundException;
38import java.io.InputStream;
39import java.lang.IllegalArgumentException;
40import java.lang.Override;
41
42/**
43 * An umbrella container for several serializable graphics representations, including Bitmaps,
44 * compressed bitmap images (e.g. JPG or PNG), and drawable resources (including vectors).
45 *
46 * <a href="https://developer.android.com/training/displaying-bitmaps/index.html">Much ink</a>
47 * has been spilled on the best way to load images, and many clients may have different needs when
48 * it comes to threading and fetching. This class is therefore focused on encapsulation rather than
49 * behavior.
50 */
51
52public final class Icon implements Parcelable {
53    private static final String TAG = "Icon";
54
55    private static final int TYPE_BITMAP   = 1;
56    private static final int TYPE_RESOURCE = 2;
57    private static final int TYPE_DATA     = 3;
58    private static final int TYPE_URI      = 4;
59
60    private final int mType;
61
62    // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed
63    // based on the value of mType.
64
65    // TYPE_BITMAP: Bitmap
66    // TYPE_RESOURCE: Resources
67    // TYPE_DATA: DataBytes
68    private Object          mObj1;
69
70    // TYPE_RESOURCE: package name
71    // TYPE_URI: uri string
72    private String          mString1;
73
74    // TYPE_RESOURCE: resId
75    // TYPE_DATA: data length
76    private int             mInt1;
77
78    // TYPE_DATA: data offset
79    private int             mInt2;
80
81    // Internal accessors for different mType variants
82    private Bitmap getBitmap() {
83        if (mType != TYPE_BITMAP) {
84            throw new IllegalStateException("called getBitmap() on " + this);
85        }
86        return (Bitmap) mObj1;
87    }
88
89    private int getDataLength() {
90        if (mType != TYPE_DATA) {
91            throw new IllegalStateException("called getDataLength() on " + this);
92        }
93        synchronized (this) {
94            return mInt1;
95        }
96    }
97
98    private int getDataOffset() {
99        if (mType != TYPE_DATA) {
100            throw new IllegalStateException("called getDataOffset() on " + this);
101        }
102        synchronized (this) {
103            return mInt2;
104        }
105    }
106
107    private byte[] getDataBytes() {
108        if (mType != TYPE_DATA) {
109            throw new IllegalStateException("called getDataBytes() on " + this);
110        }
111        synchronized (this) {
112            return (byte[]) mObj1;
113        }
114    }
115
116    private Resources getResources() {
117        if (mType != TYPE_RESOURCE) {
118            throw new IllegalStateException("called getResources() on " + this);
119        }
120        return (Resources) mObj1;
121    }
122
123    private String getResPackage() {
124        if (mType != TYPE_RESOURCE) {
125            throw new IllegalStateException("called getResPackage() on " + this);
126        }
127        return mString1;
128    }
129
130    private int getResId() {
131        if (mType != TYPE_RESOURCE) {
132            throw new IllegalStateException("called getResId() on " + this);
133        }
134        return mInt1;
135    }
136
137    private String getUriString() {
138        if (mType != TYPE_URI) {
139            throw new IllegalStateException("called getUriString() on " + this);
140        }
141        return mString1;
142    }
143
144    private Uri getUri() {
145        return Uri.parse(getUriString());
146    }
147
148    // Convert a int32 into a four-char string
149    private static final String typeToString(int x) {
150        switch (x) {
151            case TYPE_BITMAP: return "BITMAP";
152            case TYPE_DATA: return "DATA";
153            case TYPE_RESOURCE: return "RESOURCE";
154            case TYPE_URI: return "URI";
155            default: return "UNKNOWN";
156        }
157    }
158
159    /**
160     * Invokes {@link #loadDrawable(Context)} on the given {@link android.os.Handler Handler}
161     * and then sends <code>andThen</code> to the same Handler when finished.
162     *
163     * @param context {@link android.content.Context Context} in which to load the drawable; see
164     *                {@link #loadDrawable(Context)}
165     * @param andThen {@link android.os.Message} to send to its target once the drawable
166     *                is available. The {@link android.os.Message#obj obj}
167     *                property is populated with the Drawable.
168     */
169    public void loadDrawableAsync(Context context, Message andThen) {
170        if (andThen.getTarget() == null) {
171            throw new IllegalArgumentException("callback message must have a target handler");
172        }
173        new LoadDrawableTask(context, andThen).runAsync();
174    }
175
176    /**
177     * Invokes {@link #loadDrawable(Context)} on a background thread
178     * and then runs <code>andThen</code> on the UI thread when finished.
179     *
180     * @param context {@link Context Context} in which to load the drawable; see
181     *                {@link #loadDrawable(Context)}
182     * @param listener a callback to run on the provided
183     * @param handler {@link Handler} on which to run <code>andThen</code>.
184     */
185    public void loadDrawableAsync(Context context, final OnDrawableLoadedListener listener,
186            Handler handler) {
187        new LoadDrawableTask(context, handler, listener).runAsync();
188    }
189
190    /**
191     * Returns a Drawable that can be used to draw the image inside this Icon, constructing it
192     * if necessary. Depending on the type of image, this may not be something you want to do on
193     * the UI thread, so consider using
194     * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead.
195     *
196     * @param context {@link android.content.Context Context} in which to load the drawable; used
197     *                to access {@link android.content.res.Resources Resources}, for example.
198     * @return A fresh instance of a drawable for this image, yours to keep.
199     */
200    public Drawable loadDrawable(Context context) {
201        switch (mType) {
202            case TYPE_BITMAP:
203                return new BitmapDrawable(context.getResources(), getBitmap());
204            case TYPE_RESOURCE:
205                if (getResources() == null) {
206                    if (getResPackage() == null || "android".equals(getResPackage())) {
207                        mObj1 = Resources.getSystem();
208                    } else {
209                        final PackageManager pm = context.getPackageManager();
210                        try {
211                            mObj1 = pm.getResourcesForApplication(getResPackage());
212                        } catch (PackageManager.NameNotFoundException e) {
213                            Log.e(TAG, String.format("Unable to find pkg=%s",
214                                            getResPackage()),
215                                    e);
216                            break;
217                        }
218                    }
219                }
220                try {
221                    return getResources().getDrawable(getResId(), context.getTheme());
222                } catch (RuntimeException e) {
223                    Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s",
224                                    getResId(),
225                                    getResPackage()),
226                            e);
227                }
228            case TYPE_DATA:
229                return new BitmapDrawable(context.getResources(),
230                    BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength())
231                );
232            case TYPE_URI:
233                final Uri uri = getUri();
234                final String scheme = uri.getScheme();
235                InputStream is = null;
236                if (ContentResolver.SCHEME_CONTENT.equals(scheme)
237                        || ContentResolver.SCHEME_FILE.equals(scheme)) {
238                    try {
239                        is = context.getContentResolver().openInputStream(uri);
240                    } catch (Exception e) {
241                        Log.w(TAG, "Unable to load image from URI: " + uri, e);
242                    }
243                } else {
244                    try {
245                        is = new FileInputStream(new File(mString1));
246                    } catch (FileNotFoundException e) {
247                        Log.w(TAG, "Unable to load image from path: " + uri, e);
248                    }
249                }
250                if (is != null) {
251                    return new BitmapDrawable(context.getResources(),
252                            BitmapFactory.decodeStream(is));
253                }
254                break;
255        }
256        return null;
257    }
258
259    /**
260     * Load the requested resources under the given userId, if the system allows it,
261     * before actually loading the drawable.
262     *
263     * @hide
264     */
265    public Drawable loadDrawableAsUser(Context context, int userId) {
266        if (mType == TYPE_RESOURCE) {
267            if (getResources() == null
268                    && getResPackage() != null
269                    && !(getResPackage().equals("android"))) {
270                final PackageManager pm = context.getPackageManager();
271                try {
272                    mObj1 = pm.getResourcesForApplicationAsUser(getResPackage(), userId);
273                } catch (PackageManager.NameNotFoundException e) {
274                    Log.e(TAG, String.format("Unable to find pkg=%s user=%d",
275                                    getResPackage(),
276                                    userId),
277                            e);
278                }
279            }
280        }
281        return loadDrawable(context);
282    }
283
284    private Icon(int mType) {
285        this.mType = mType;
286    }
287
288    /**
289     * Create an Icon pointing to a drawable resource.
290     * @param res Resources for a package containing the resource in question
291     * @param resId ID of the drawable resource
292     */
293    public static Icon createWithResource(Resources res, @DrawableRes int resId) {
294        final Icon rep = new Icon(TYPE_RESOURCE);
295        rep.mObj1 = res;
296        rep.mInt1 = resId;
297        rep.mString1 = res.getResourcePackageName(resId);
298        return rep;
299    }
300
301    /**
302     * Create an Icon pointing to a drawable resource.
303     * @param resPackage Name of the package containing the resource in question
304     * @param resId ID of the drawable resource
305     */
306    public static Icon createWithResource(String resPackage, @DrawableRes int resId) {
307        final Icon rep = new Icon(TYPE_RESOURCE);
308        rep.mInt1 = resId;
309        rep.mString1 = resPackage;
310        return rep;
311    }
312
313    /**
314     * Create an Icon pointing to a bitmap in memory.
315     * @param bits A valid {@link android.graphics.Bitmap} object
316     */
317    public static Icon createWithBitmap(Bitmap bits) {
318        final Icon rep = new Icon(TYPE_BITMAP);
319        rep.mObj1 = bits;
320        return rep;
321    }
322
323    /**
324     * Create an Icon pointing to a compressed bitmap stored in a byte array.
325     * @param data Byte array storing compressed bitmap data of a type that
326     *             {@link android.graphics.BitmapFactory}
327     *             can decode (see {@link android.graphics.Bitmap.CompressFormat}).
328     * @param offset Offset into <code>data</code> at which the bitmap data starts
329     * @param length Length of the bitmap data
330     */
331    public static Icon createWithData(byte[] data, int offset, int length) {
332        final Icon rep = new Icon(TYPE_DATA);
333        rep.mObj1 = data;
334        rep.mInt1 = length;
335        rep.mInt2 = offset;
336        return rep;
337    }
338
339    /**
340     * Create an Icon pointing to an image file specified by URI.
341     *
342     * @param uri A uri referring to local content:// or file:// image data.
343     */
344    public static Icon createWithContentUri(String uri) {
345        final Icon rep = new Icon(TYPE_URI);
346        rep.mString1 = uri;
347        return rep;
348    }
349
350    /**
351     * Create an Icon pointing to an image file specified by URI.
352     *
353     * @param uri A uri referring to local content:// or file:// image data.
354     */
355    public static Icon createWithContentUri(Uri uri) {
356        final Icon rep = new Icon(TYPE_URI);
357        rep.mString1 = uri.toString();
358        return rep;
359    }
360
361    /**
362     * Create an Icon pointing to an image file specified by path.
363     *
364     * @param path A path to a file that contains compressed bitmap data of
365     *           a type that {@link android.graphics.BitmapFactory} can decode.
366     */
367    public static Icon createWithFilePath(String path) {
368        final Icon rep = new Icon(TYPE_URI);
369        rep.mString1 = path;
370        return rep;
371    }
372
373    @Override
374    public String toString() {
375        final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
376        switch (mType) {
377            case TYPE_BITMAP:
378                sb.append(" size=")
379                        .append(getBitmap().getWidth())
380                        .append("x")
381                        .append(getBitmap().getHeight());
382                break;
383            case TYPE_RESOURCE:
384                sb.append(" pkg=")
385                        .append(getResPackage())
386                        .append(" id=")
387                        .append(String.format("%08x", getResId()));
388                break;
389            case TYPE_DATA:
390                sb.append(" len=").append(getDataLength());
391                if (getDataOffset() != 0) {
392                    sb.append(" off=").append(getDataOffset());
393                }
394                break;
395            case TYPE_URI:
396                sb.append(" uri=").append(getUriString());
397                break;
398        }
399        sb.append(")");
400        return sb.toString();
401    }
402
403    /**
404     * Parcelable interface
405     */
406    public int describeContents() {
407        return (mType == TYPE_BITMAP || mType == TYPE_DATA)
408                ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
409    }
410
411    // ===== Parcelable interface ======
412
413    private Icon(Parcel in) {
414        this(in.readInt());
415        switch (mType) {
416            case TYPE_BITMAP:
417                final Bitmap bits = Bitmap.CREATOR.createFromParcel(in);
418                mObj1 = bits;
419                break;
420            case TYPE_RESOURCE:
421                final String pkg = in.readString();
422                final int resId = in.readInt();
423                mString1 = pkg;
424                mInt1 = resId;
425                break;
426            case TYPE_DATA:
427                final int len = in.readInt();
428                final byte[] a = in.readBlob();
429                if (len != a.length) {
430                    throw new RuntimeException("internal unparceling error: blob length ("
431                            + a.length + ") != expected length (" + len + ")");
432                }
433                mInt1 = len;
434                mObj1 = a;
435                break;
436            case TYPE_URI:
437                final String uri = in.readString();
438                mString1 = uri;
439                break;
440            default:
441                throw new RuntimeException("invalid "
442                        + this.getClass().getSimpleName() + " type in parcel: " + mType);
443        }
444    }
445
446    @Override
447    public void writeToParcel(Parcel dest, int flags) {
448        switch (mType) {
449            case TYPE_BITMAP:
450                final Bitmap bits = getBitmap();
451                dest.writeInt(TYPE_BITMAP);
452                getBitmap().writeToParcel(dest, flags);
453                break;
454            case TYPE_RESOURCE:
455                dest.writeInt(TYPE_RESOURCE);
456                dest.writeString(getResPackage());
457                dest.writeInt(getResId());
458                break;
459            case TYPE_DATA:
460                dest.writeInt(TYPE_DATA);
461                dest.writeInt(getDataLength());
462                dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength());
463                break;
464            case TYPE_URI:
465                dest.writeInt(TYPE_URI);
466                dest.writeString(getUriString());
467                break;
468        }
469    }
470
471    public static final Parcelable.Creator<Icon> CREATOR
472            = new Parcelable.Creator<Icon>() {
473        public Icon createFromParcel(Parcel in) {
474            return new Icon(in);
475        }
476
477        public Icon[] newArray(int size) {
478            return new Icon[size];
479        }
480    };
481
482    /**
483     * Implement this interface to receive a callback when
484     * {@link #loadDrawableAsync(Context, OnDrawableLoadedListener, Handler) loadDrawableAsync}
485     * is finished and your Drawable is ready.
486     */
487    public interface OnDrawableLoadedListener {
488        void onDrawableLoaded(Drawable d);
489    }
490
491    /**
492     * Wrapper around loadDrawable that does its work on a pooled thread and then
493     * fires back the given (targeted) Message.
494     */
495    private class LoadDrawableTask implements Runnable {
496        final Context mContext;
497        final Message mMessage;
498
499        public LoadDrawableTask(Context context, final Handler handler,
500                final OnDrawableLoadedListener listener) {
501            mContext = context;
502            mMessage = Message.obtain(handler, new Runnable() {
503                    @Override
504                    public void run() {
505                        listener.onDrawableLoaded((Drawable) mMessage.obj);
506                    }
507                });
508        }
509
510        public LoadDrawableTask(Context context, Message message) {
511            mContext = context;
512            mMessage = message;
513        }
514
515        @Override
516        public void run() {
517            mMessage.obj = loadDrawable(mContext);
518            mMessage.sendToTarget();
519        }
520
521        public void runAsync() {
522            AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
523        }
524    }
525}
526