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