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