Icon.java revision b9f7aac3488873677377b36c57338d758098f78e
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.util.Log;
33
34import java.io.ByteArrayOutputStream;
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 android.content.Context Context} in which to load the drawable; see
181     *                {@link #loadDrawable(Context)}
182     * @param handler {@link android.os.Handler} on which to run <code>andThen</code>.
183     * @param listener a callback to run on the provided
184     *                 Handler once the drawable is available.
185     */
186    public void loadDrawableAsync(Context context, Handler handler,
187            final OnDrawableLoadedListener listener) {
188        new LoadDrawableTask(context, handler, listener).runAsync();
189    }
190
191    /**
192     * Returns a Drawable that can be used to draw the image inside this Icon, constructing it
193     * if necessary. Depending on the type of image, this may not be something you want to do on
194     * the UI thread, so consider using
195     * {@link #loadDrawableAsync(Context, Message) loadDrawableAsync} instead.
196     *
197     * @param context {@link android.content.Context Context} in which to load the drawable; used
198     *                to access {@link android.content.res.Resources Resources}, for example.
199     * @return A fresh instance of a drawable for this image, yours to keep.
200     */
201    public Drawable loadDrawable(Context context) {
202        switch (mType) {
203            case TYPE_BITMAP:
204                return new BitmapDrawable(context.getResources(), getBitmap());
205            case TYPE_RESOURCE:
206                if (getResources() == null) {
207                    if (getResPackage() == null || "android".equals(getResPackage())) {
208                        mObj1 = Resources.getSystem();
209                    } else {
210                        final PackageManager pm = context.getPackageManager();
211                        try {
212                            mObj1 = pm.getResourcesForApplication(getResPackage());
213                        } catch (PackageManager.NameNotFoundException e) {
214                            Log.e(TAG,
215                                    String.format("Unable to find package '%s'", getResPackage()),
216                                    e);
217                            break;
218                        }
219                    }
220                }
221                return getResources().getDrawable(getResId(), context.getTheme());
222            case TYPE_DATA:
223                return new BitmapDrawable(context.getResources(),
224                    BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength())
225                );
226            case TYPE_URI:
227                final Uri uri = getUri();
228                final String scheme = uri.getScheme();
229                InputStream is = null;
230                if (ContentResolver.SCHEME_CONTENT.equals(scheme)
231                        || ContentResolver.SCHEME_FILE.equals(scheme)) {
232                    try {
233                        is = context.getContentResolver().openInputStream(uri);
234                    } catch (Exception e) {
235                        Log.w(TAG, "Unable to load image from URI: " + uri, e);
236                    }
237                } else {
238                    try {
239                        is = new FileInputStream(new File(mString1));
240                    } catch (FileNotFoundException e) {
241                        Log.w(TAG, "Unable to load image from path: " + uri, e);
242                    }
243                }
244                if (is != null) {
245                    return new BitmapDrawable(context.getResources(),
246                            BitmapFactory.decodeStream(is));
247                }
248                break;
249        }
250        return null;
251    }
252
253    private Icon(int mType) {
254        this.mType = mType;
255    }
256
257    /**
258     * Create a Icon pointing to a drawable resource.
259     * @param res Resources for a package containing the resource in question
260     * @param resid ID of the drawable resource
261     */
262    public static Icon createWithResource(Resources res, @DrawableRes int resid) {
263        final Icon rep = new Icon(TYPE_RESOURCE);
264        rep.mObj1 = res;
265        rep.mInt1 = resid;
266        rep.mString1 = res.getResourcePackageName(resid);
267        return rep;
268    }
269
270    /**
271     * Create a Icon pointing to a bitmap in memory.
272     * @param bits A valid {@link android.graphics.Bitmap} object
273     */
274    public static Icon createWithBitmap(Bitmap bits) {
275        final Icon rep = new Icon(TYPE_BITMAP);
276        rep.mObj1 = bits;
277        return rep;
278    }
279
280    /**
281     * Create a Icon pointing to a compressed bitmap stored in a byte array.
282     * @param data Byte array storing compressed bitmap data of a type that
283     *             {@link android.graphics.BitmapFactory}
284     *             can decode (see {@link android.graphics.Bitmap.CompressFormat}).
285     * @param offset Offset into <code>data</code> at which the bitmap data starts
286     * @param length Length of the bitmap data
287     */
288    public static Icon createWithData(byte[] data, int offset, int length) {
289        final Icon rep = new Icon(TYPE_DATA);
290        rep.mObj1 = data;
291        rep.mInt1 = length;
292        rep.mInt2 = offset;
293        return rep;
294    }
295
296    /**
297     * Create a Icon pointing to a content specified by URI.
298     *
299     * @param uri A uri referring to local content:// or file:// image data.
300     */
301    public static Icon createWithContentUri(String uri) {
302        final Icon rep = new Icon(TYPE_URI);
303        rep.mString1 = uri;
304        return rep;
305    }
306
307    /**
308     * Create a Icon pointing to a content specified by URI.
309     *
310     * @param uri A uri referring to local content:// or file:// image data.
311     */
312    public static Icon createWithContentUri(Uri uri) {
313        final Icon rep = new Icon(TYPE_URI);
314        rep.mString1 = uri.toString();
315        return rep;
316    }
317
318    /**
319     * Create a Icon pointing to
320     *
321     * @param path A path to a file that contains compressed bitmap data of
322     *           a type that {@link android.graphics.BitmapFactory} can decode.
323     */
324    public static Icon createWithFilePath(String path) {
325        final Icon rep = new Icon(TYPE_URI);
326        rep.mString1 = path;
327        return rep;
328    }
329
330    @Override
331    public String toString() {
332        final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
333        switch (mType) {
334            case TYPE_BITMAP:
335                sb.append(" size=")
336                        .append(getBitmap().getWidth())
337                        .append("x")
338                        .append(getBitmap().getHeight());
339                break;
340            case TYPE_RESOURCE:
341                sb.append(" pkg=")
342                        .append(getResPackage())
343                        .append(" id=")
344                        .append(String.format("%08x", getResId()));
345                break;
346            case TYPE_DATA:
347                sb.append(" len=").append(getDataLength());
348                if (getDataOffset() != 0) {
349                    sb.append(" off=").append(getDataOffset());
350                }
351                break;
352            case TYPE_URI:
353                sb.append(" uri=").append(getUriString());
354                break;
355        }
356        sb.append(")");
357        return sb.toString();
358    }
359
360    /**
361     * Parcelable interface
362     */
363    public int describeContents() {
364        return (mType == TYPE_BITMAP || mType == TYPE_DATA)
365                ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
366    }
367
368    // ===== Parcelable interface ======
369
370    private Icon(Parcel in) {
371        this(in.readInt());
372        switch (mType) {
373            case TYPE_BITMAP:
374                final Bitmap bits = Bitmap.CREATOR.createFromParcel(in);
375                mObj1 = bits;
376                break;
377            case TYPE_RESOURCE:
378                final String pkg = in.readString();
379                final int resId = in.readInt();
380                mString1 = pkg;
381                mInt1 = resId;
382                break;
383            case TYPE_DATA:
384                final int len = in.readInt();
385                final byte[] a = in.readBlob();
386                if (len != a.length) {
387                    throw new RuntimeException("internal unparceling error: blob length ("
388                            + a.length + ") != expected length (" + len + ")");
389                }
390                mInt1 = len;
391                mObj1 = a;
392                break;
393            case TYPE_URI:
394                final String uri = in.readString();
395                mString1 = uri;
396                break;
397            default:
398                throw new RuntimeException("invalid "
399                        + this.getClass().getSimpleName() + " type in parcel: " + mType);
400        }
401    }
402
403    @Override
404    public void writeToParcel(Parcel dest, int flags) {
405        switch (mType) {
406            case TYPE_BITMAP:
407                final Bitmap bits = getBitmap();
408                dest.writeInt(TYPE_BITMAP);
409                getBitmap().writeToParcel(dest, flags);
410                break;
411            case TYPE_RESOURCE:
412                dest.writeInt(TYPE_RESOURCE);
413                dest.writeString(getResPackage());
414                dest.writeInt(getResId());
415                break;
416            case TYPE_DATA:
417                dest.writeInt(TYPE_DATA);
418                dest.writeInt(getDataLength());
419                dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength());
420                break;
421            case TYPE_URI:
422                dest.writeInt(TYPE_URI);
423                dest.writeString(getUriString());
424                break;
425        }
426    }
427
428    public static final Parcelable.Creator<Icon> CREATOR
429            = new Parcelable.Creator<Icon>() {
430        public Icon createFromParcel(Parcel in) {
431            return new Icon(in);
432        }
433
434        public Icon[] newArray(int size) {
435            return new Icon[size];
436        }
437    };
438
439    /**
440     * Implement this interface to receive notification when
441     * {@link #loadDrawableAsync(Context, Handler, OnDrawableLoadedListener) loadDrawableAsync}
442     * is finished and your Drawable is ready.
443     */
444    public interface OnDrawableLoadedListener {
445        void onDrawableLoaded(Drawable d);
446    }
447
448    /**
449     * Wrapper around loadDrawable that does its work on a pooled thread and then
450     * fires back the given (targeted) Message.
451     */
452    private class LoadDrawableTask implements Runnable {
453        final Context mContext;
454        final Message mMessage;
455
456        public LoadDrawableTask(Context context, final Handler handler,
457                final OnDrawableLoadedListener listener) {
458            mContext = context;
459            mMessage = Message.obtain(handler, new Runnable() {
460                    @Override
461                    public void run() {
462                        listener.onDrawableLoaded((Drawable) mMessage.obj);
463                    }
464                });
465        }
466
467        public LoadDrawableTask(Context context, Message message) {
468            mContext = context;
469            mMessage = message;
470        }
471
472        @Override
473        public void run() {
474            mMessage.obj = loadDrawable(mContext);
475            mMessage.sendToTarget();
476        }
477
478        public void runAsync() {
479            AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
480        }
481    }
482}
483