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