1 package com.android.launcher3.icons;
2 
3 import static android.graphics.Paint.DITHER_FLAG;
4 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
5 
6 import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
7 
8 import android.content.Context;
9 import android.content.Intent;
10 import android.content.pm.PackageManager;
11 import android.content.res.Resources;
12 import android.graphics.Bitmap;
13 import android.graphics.Canvas;
14 import android.graphics.Color;
15 import android.graphics.PaintFlagsDrawFilter;
16 import android.graphics.Rect;
17 import android.graphics.RectF;
18 import android.graphics.drawable.AdaptiveIconDrawable;
19 import android.graphics.drawable.BitmapDrawable;
20 import android.graphics.drawable.ColorDrawable;
21 import android.graphics.drawable.Drawable;
22 import android.os.Build;
23 import android.os.Process;
24 import android.os.UserHandle;
25 
26 import androidx.annotation.NonNull;
27 
28 /**
29  * This class will be moved to androidx library. There shouldn't be any dependency outside
30  * this package.
31  */
32 public class BaseIconFactory implements AutoCloseable {
33 
34     private static final String TAG = "BaseIconFactory";
35     private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
36     static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
37     static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
38 
39     private static final float ICON_BADGE_SCALE = 0.444f;
40 
41     private final Rect mOldBounds = new Rect();
42     protected final Context mContext;
43     private final Canvas mCanvas;
44     private final PackageManager mPm;
45     private final ColorExtractor mColorExtractor;
46     private boolean mDisableColorExtractor;
47 
48     protected final int mFillResIconDpi;
49     protected final int mIconBitmapSize;
50 
51     private IconNormalizer mNormalizer;
52     private ShadowGenerator mShadowGenerator;
53     private final boolean mShapeDetection;
54 
55     private Drawable mWrapperIcon;
56     private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
57 
BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, boolean shapeDetection)58     protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
59             boolean shapeDetection) {
60         mContext = context.getApplicationContext();
61         mShapeDetection = shapeDetection;
62         mFillResIconDpi = fillResIconDpi;
63         mIconBitmapSize = iconBitmapSize;
64 
65         mPm = mContext.getPackageManager();
66         mColorExtractor = new ColorExtractor();
67 
68         mCanvas = new Canvas();
69         mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
70         clear();
71     }
72 
BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize)73     protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
74         this(context, fillResIconDpi, iconBitmapSize, false);
75     }
76 
clear()77     protected void clear() {
78         mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
79         mDisableColorExtractor = false;
80     }
81 
getShadowGenerator()82     public ShadowGenerator getShadowGenerator() {
83         if (mShadowGenerator == null) {
84             mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
85         }
86         return mShadowGenerator;
87     }
88 
getNormalizer()89     public IconNormalizer getNormalizer() {
90         if (mNormalizer == null) {
91             mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
92         }
93         return mNormalizer;
94     }
95 
96     @SuppressWarnings("deprecation")
createIconBitmap(Intent.ShortcutIconResource iconRes)97     public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
98         try {
99             Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
100             if (resources != null) {
101                 final int id = resources.getIdentifier(iconRes.resourceName, null, null);
102                 // do not stamp old legacy shortcuts as the app may have already forgotten about it
103                 return createBadgedIconBitmap(
104                         resources.getDrawableForDensity(id, mFillResIconDpi),
105                         Process.myUserHandle() /* only available on primary user */,
106                         false /* do not apply legacy treatment */);
107             }
108         } catch (Exception e) {
109             // Icon not found.
110         }
111         return null;
112     }
113 
createIconBitmap(Bitmap icon)114     public BitmapInfo createIconBitmap(Bitmap icon) {
115         if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
116             icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
117         }
118 
119         return BitmapInfo.fromBitmap(icon, mDisableColorExtractor ? null : mColorExtractor);
120     }
121 
createBadgedIconBitmap(Drawable icon, UserHandle user, boolean shrinkNonAdaptiveIcons)122     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
123             boolean shrinkNonAdaptiveIcons) {
124         return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null);
125     }
126 
createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk)127     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
128             int iconAppTargetSdk) {
129         return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
130     }
131 
createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, boolean isInstantApp)132     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
133             int iconAppTargetSdk, boolean isInstantApp) {
134         return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
135     }
136 
createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, boolean isInstantApp, float[] scale)137     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
138             int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
139         boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
140                 (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
141         return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
142     }
143 
createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk)144     public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
145         boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
146                 (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
147         return  createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
148     }
149 
150     /**
151      * Creates bitmap using the source drawable and various parameters.
152      * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
153      *
154      * @param icon                      source of the icon
155      * @param user                      info can be used for a badge
156      * @param shrinkNonAdaptiveIcons    {@code true} if non adaptive icons should be treated
157      * @param isInstantApp              info can be used for a badge
158      * @param scale                     returns the scale result from normalization
159      * @return a bitmap suitable for disaplaying as an icon at various system UIs.
160      */
createBadgedIconBitmap(@onNull Drawable icon, UserHandle user, boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale)161     public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user,
162             boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
163         if (scale == null) {
164             scale = new float[1];
165         }
166         icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
167         Bitmap bitmap = createIconBitmap(icon, scale[0]);
168         if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
169             mCanvas.setBitmap(bitmap);
170             getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
171             mCanvas.setBitmap(null);
172         }
173 
174         if (isInstantApp) {
175             badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
176         }
177         if (user != null) {
178             BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
179             Drawable badged = mPm.getUserBadgedIcon(drawable, user);
180             if (badged instanceof BitmapDrawable) {
181                 bitmap = ((BitmapDrawable) badged).getBitmap();
182             } else {
183                 bitmap = createIconBitmap(badged, 1f);
184             }
185         }
186         return BitmapInfo.fromBitmap(bitmap, mDisableColorExtractor ? null : mColorExtractor);
187     }
188 
createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons)189     public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
190         RectF iconBounds = new RectF();
191         float[] scale = new float[1];
192         icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale);
193         return createIconBitmap(icon,
194                 Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
195     }
196 
197     /**
198      * Sets the background color used for wrapped adaptive icon
199      */
setWrapperBackgroundColor(int color)200     public void setWrapperBackgroundColor(int color) {
201         mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
202     }
203 
204     /**
205      * Disables the dominant color extraction for all icons loaded.
206      */
disableColorExtraction()207     public void disableColorExtraction() {
208         mDisableColorExtractor = true;
209     }
210 
normalizeAndWrapToAdaptiveIcon(@onNull Drawable icon, boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale)211     private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
212             boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
213         if (icon == null) {
214             return null;
215         }
216         float scale = 1f;
217 
218         if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
219             if (mWrapperIcon == null) {
220                 mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
221                         .mutate();
222             }
223             AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
224             dr.setBounds(0, 0, 1, 1);
225             boolean[] outShape = new boolean[1];
226             scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
227             if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
228                 FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
229                 fsd.setDrawable(icon);
230                 fsd.setScale(scale);
231                 icon = dr;
232                 scale = getNormalizer().getScale(icon, outIconBounds, null, null);
233 
234                 ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
235             }
236         } else {
237             scale = getNormalizer().getScale(icon, outIconBounds, null, null);
238         }
239 
240         outScale[0] = scale;
241         return icon;
242     }
243 
244     /**
245      * Adds the {@param badge} on top of {@param target} using the badge dimensions.
246      */
badgeWithDrawable(Bitmap target, Drawable badge)247     public void badgeWithDrawable(Bitmap target, Drawable badge) {
248         mCanvas.setBitmap(target);
249         badgeWithDrawable(mCanvas, badge);
250         mCanvas.setBitmap(null);
251     }
252 
253     /**
254      * Adds the {@param badge} on top of {@param target} using the badge dimensions.
255      */
badgeWithDrawable(Canvas target, Drawable badge)256     public void badgeWithDrawable(Canvas target, Drawable badge) {
257         int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize);
258         badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
259                 mIconBitmapSize, mIconBitmapSize);
260         badge.draw(target);
261     }
262 
createIconBitmap(Drawable icon, float scale)263     private Bitmap createIconBitmap(Drawable icon, float scale) {
264         return createIconBitmap(icon, scale, mIconBitmapSize);
265     }
266 
267     /**
268      * @param icon drawable that should be flattened to a bitmap
269      * @param scale the scale to apply before drawing {@param icon} on the canvas
270      */
createIconBitmap(@onNull Drawable icon, float scale, int size)271     public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
272         Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
273         if (icon == null) {
274             return bitmap;
275         }
276         mCanvas.setBitmap(bitmap);
277         mOldBounds.set(icon.getBounds());
278 
279         if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
280             int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
281                     Math.round(size * (1 - scale) / 2 ));
282             icon.setBounds(offset, offset, size - offset, size - offset);
283             icon.draw(mCanvas);
284         } else {
285             if (icon instanceof BitmapDrawable) {
286                 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
287                 Bitmap b = bitmapDrawable.getBitmap();
288                 if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
289                     bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
290                 }
291             }
292             int width = size;
293             int height = size;
294 
295             int intrinsicWidth = icon.getIntrinsicWidth();
296             int intrinsicHeight = icon.getIntrinsicHeight();
297             if (intrinsicWidth > 0 && intrinsicHeight > 0) {
298                 // Scale the icon proportionally to the icon dimensions
299                 final float ratio = (float) intrinsicWidth / intrinsicHeight;
300                 if (intrinsicWidth > intrinsicHeight) {
301                     height = (int) (width / ratio);
302                 } else if (intrinsicHeight > intrinsicWidth) {
303                     width = (int) (height * ratio);
304                 }
305             }
306             final int left = (size - width) / 2;
307             final int top = (size - height) / 2;
308             icon.setBounds(left, top, left + width, top + height);
309             mCanvas.save();
310             mCanvas.scale(scale, scale, size / 2, size / 2);
311             icon.draw(mCanvas);
312             mCanvas.restore();
313 
314         }
315         icon.setBounds(mOldBounds);
316         mCanvas.setBitmap(null);
317         return bitmap;
318     }
319 
320     @Override
close()321     public void close() {
322         clear();
323     }
324 
makeDefaultIcon(UserHandle user)325     public BitmapInfo makeDefaultIcon(UserHandle user) {
326         return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi),
327                 user, Build.VERSION.SDK_INT);
328     }
329 
getFullResDefaultActivityIcon(int iconDpi)330     public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
331         return Resources.getSystem().getDrawableForDensity(
332                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
333                         ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
334                 iconDpi);
335     }
336 
337     /**
338      * Returns the correct badge size given an icon size
339      */
getBadgeSizeForIconSize(int iconSize)340     public static int getBadgeSizeForIconSize(int iconSize) {
341         return (int) (ICON_BADGE_SCALE * iconSize);
342     }
343 
344     /**
345      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
346      * This allows the badging to be done based on the action bitmap size rather than
347      * the scaled bitmap size.
348      */
349     private static class FixedSizeBitmapDrawable extends BitmapDrawable {
350 
FixedSizeBitmapDrawable(Bitmap bitmap)351         public FixedSizeBitmapDrawable(Bitmap bitmap) {
352             super(null, bitmap);
353         }
354 
355         @Override
getIntrinsicHeight()356         public int getIntrinsicHeight() {
357             return getBitmap().getWidth();
358         }
359 
360         @Override
getIntrinsicWidth()361         public int getIntrinsicWidth() {
362             return getBitmap().getWidth();
363         }
364     }
365 }
366