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