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