1/*
2 * Copyright (C) 2017 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.support.v4.graphics.drawable;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.graphics.BitmapShader;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.Shader;
30import android.graphics.drawable.Icon;
31import android.net.Uri;
32import android.os.Build;
33import android.support.annotation.DrawableRes;
34import android.support.annotation.RequiresApi;
35import android.support.annotation.RestrictTo;
36import android.support.annotation.VisibleForTesting;
37
38/**
39 * Helper for accessing features in {@link android.graphics.drawable.Icon}.
40 */
41public class IconCompat {
42
43    // Ratio of expected size to actual icon size
44    private static final float ADAPTIVE_ICON_INSET_FACTOR = 1 / 4f;
45    private static final float DEFAULT_VIEW_PORT_SCALE = 1 / (1 + 2 * ADAPTIVE_ICON_INSET_FACTOR);
46    private static final float ICON_DIAMETER_FACTOR = 176f / 192;
47    private static final float BLUR_FACTOR = 0.5f / 48;
48    private static final float KEY_SHADOW_OFFSET_FACTOR = 1f / 48;
49
50    private static final int KEY_SHADOW_ALPHA = 61;
51    private static final int AMBIENT_SHADOW_ALPHA = 30;
52
53    private static final int TYPE_BITMAP   = 1;
54    private static final int TYPE_RESOURCE = 2;
55    private static final int TYPE_DATA     = 3;
56    private static final int TYPE_URI      = 4;
57    private static final int TYPE_ADAPTIVE_BITMAP = 5;
58
59    private final int mType;
60
61    // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed
62    // based on the value of mType.
63
64    // TYPE_BITMAP: Bitmap
65    // TYPE_ADAPTIVE_BITMAP: Bitmap
66    // TYPE_RESOURCE: Context
67    // TYPE_URI: String
68    // TYPE_DATA: DataBytes
69    private Object          mObj1;
70
71    // TYPE_RESOURCE: resId
72    // TYPE_DATA: data offset
73    private int             mInt1;
74
75    // TYPE_DATA: data length
76    private int             mInt2;
77
78    /**
79     * Create an Icon pointing to a drawable resource.
80     * @param context The context for the application whose resources should be used to resolve the
81     *                given resource ID.
82     * @param resId ID of the drawable resource
83     * @see android.graphics.drawable.Icon#createWithResource(Context, int)
84     */
85    public static IconCompat createWithResource(Context context, @DrawableRes int resId) {
86        if (context == null) {
87            throw new IllegalArgumentException("Context must not be null.");
88        }
89        final IconCompat rep = new IconCompat(TYPE_RESOURCE);
90        rep.mInt1 = resId;
91        rep.mObj1 = context;
92        return rep;
93    }
94
95    /**
96     * Create an Icon pointing to a bitmap in memory.
97     * @param bits A valid {@link android.graphics.Bitmap} object
98     * @see android.graphics.drawable.Icon#createWithBitmap(Bitmap)
99     */
100    public static IconCompat createWithBitmap(Bitmap bits) {
101        if (bits == null) {
102            throw new IllegalArgumentException("Bitmap must not be null.");
103        }
104        final IconCompat rep = new IconCompat(TYPE_BITMAP);
105        rep.mObj1 = bits;
106        return rep;
107    }
108
109    /**
110     * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined
111     * by {@link android.graphics.drawable.AdaptiveIconDrawable}.
112     * @param bits A valid {@link android.graphics.Bitmap} object
113     * @see android.graphics.drawable.Icon#createWithAdaptiveBitmap(Bitmap)
114     */
115    public static IconCompat createWithAdaptiveBitmap(Bitmap bits) {
116        if (bits == null) {
117            throw new IllegalArgumentException("Bitmap must not be null.");
118        }
119        final IconCompat rep = new IconCompat(TYPE_ADAPTIVE_BITMAP);
120        rep.mObj1 = bits;
121        return rep;
122    }
123
124    /**
125     * Create an Icon pointing to a compressed bitmap stored in a byte array.
126     * @param data Byte array storing compressed bitmap data of a type that
127     *             {@link android.graphics.BitmapFactory}
128     *             can decode (see {@link android.graphics.Bitmap.CompressFormat}).
129     * @param offset Offset into <code>data</code> at which the bitmap data starts
130     * @param length Length of the bitmap data
131     * @see android.graphics.drawable.Icon#createWithData(byte[], int, int)
132     */
133    public static IconCompat createWithData(byte[] data, int offset, int length) {
134        if (data == null) {
135            throw new IllegalArgumentException("Data must not be null.");
136        }
137        final IconCompat rep = new IconCompat(TYPE_DATA);
138        rep.mObj1 = data;
139        rep.mInt1 = offset;
140        rep.mInt2 = length;
141        return rep;
142    }
143
144    /**
145     * Create an Icon pointing to an image file specified by URI.
146     *
147     * @param uri A uri referring to local content:// or file:// image data.
148     * @see android.graphics.drawable.Icon#createWithContentUri(String)
149     */
150    public static IconCompat createWithContentUri(String uri) {
151        if (uri == null) {
152            throw new IllegalArgumentException("Uri must not be null.");
153        }
154        final IconCompat rep = new IconCompat(TYPE_URI);
155        rep.mObj1 = uri;
156        return rep;
157    }
158
159    /**
160     * Create an Icon pointing to an image file specified by URI.
161     *
162     * @param uri A uri referring to local content:// or file:// image data.
163     * @see android.graphics.drawable.Icon#createWithContentUri(String)
164     */
165    public static IconCompat createWithContentUri(Uri uri) {
166        if (uri == null) {
167            throw new IllegalArgumentException("Uri must not be null.");
168        }
169        return createWithContentUri(uri.toString());
170    }
171
172    private IconCompat(int mType) {
173        this.mType = mType;
174    }
175
176    /**
177     * Convert this compat object to {@link Icon} object.
178     *
179     * @return {@link Icon} object
180     */
181    @RequiresApi(23)
182    public Icon toIcon() {
183        switch (mType) {
184            case TYPE_BITMAP:
185                return Icon.createWithBitmap((Bitmap) mObj1);
186            case TYPE_ADAPTIVE_BITMAP:
187                if (Build.VERSION.SDK_INT >= 26) {
188                    return Icon.createWithAdaptiveBitmap((Bitmap) mObj1);
189                } else {
190                    return Icon.createWithBitmap(createLegacyIconFromAdaptiveIcon((Bitmap) mObj1));
191                }
192            case TYPE_RESOURCE:
193                return Icon.createWithResource((Context) mObj1, mInt1);
194            case TYPE_DATA:
195                return Icon.createWithData((byte[]) mObj1, mInt1, mInt2);
196            case TYPE_URI:
197                return Icon.createWithContentUri((String) mObj1);
198            default:
199                throw new IllegalArgumentException("Unknown type");
200        }
201    }
202
203    /**
204     * @hide
205     */
206    @RestrictTo(LIBRARY_GROUP)
207    public void addToShortcutIntent(Intent outIntent) {
208        switch (mType) {
209            case TYPE_BITMAP:
210                outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, (Bitmap) mObj1);
211                break;
212            case TYPE_ADAPTIVE_BITMAP:
213                outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
214                        createLegacyIconFromAdaptiveIcon((Bitmap) mObj1));
215                break;
216            case TYPE_RESOURCE:
217                outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
218                        Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1));
219                break;
220            default:
221                throw new IllegalArgumentException("Icon type not supported for intent shortcuts");
222        }
223    }
224
225    /**
226     * Converts a bitmap following the adaptive icon guide lines, into a bitmap following the
227     * shortcut icon guide lines.
228     * The returned bitmap will always have same width and height and clipped to a circle.
229     */
230    @VisibleForTesting
231    static Bitmap createLegacyIconFromAdaptiveIcon(Bitmap adaptiveIconBitmap) {
232        int size = (int) (DEFAULT_VIEW_PORT_SCALE * Math.min(adaptiveIconBitmap.getWidth(),
233                adaptiveIconBitmap.getHeight()));
234
235        Bitmap icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
236        Canvas canvas = new Canvas(icon);
237        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
238
239        float center = size * 0.5f;
240        float radius = center * ICON_DIAMETER_FACTOR;
241
242        // Draw key shadow
243        float blur = BLUR_FACTOR * size;
244        paint.setColor(Color.TRANSPARENT);
245        paint.setShadowLayer(blur, 0, KEY_SHADOW_OFFSET_FACTOR * size, KEY_SHADOW_ALPHA << 24);
246        canvas.drawCircle(center, center, radius, paint);
247
248        // Draw ambient shadow
249        paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24);
250        canvas.drawCircle(center, center, radius, paint);
251        paint.clearShadowLayer();
252
253        // Draw the clipped icon
254        paint.setColor(Color.BLACK);
255        BitmapShader shader = new BitmapShader(adaptiveIconBitmap, Shader.TileMode.CLAMP,
256                Shader.TileMode.CLAMP);
257        Matrix shift = new Matrix();
258        shift.setTranslate(-(adaptiveIconBitmap.getWidth() - size) / 2,
259                -(adaptiveIconBitmap.getHeight() - size) / 2);
260        shader.setLocalMatrix(shift);
261        paint.setShader(shader);
262        canvas.drawCircle(center, center, radius, paint);
263
264        canvas.setBitmap(null);
265        return icon;
266    }
267}
268