• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3.icons;
2 
3 import static android.graphics.Paint.ANTI_ALIAS_FLAG;
4 import static android.graphics.Paint.DITHER_FLAG;
5 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
6 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
7 
8 import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT;
9 import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
10 
11 import static java.lang.annotation.RetentionPolicy.SOURCE;
12 
13 import android.annotation.TargetApi;
14 import android.content.Context;
15 import android.content.Intent;
16 import android.content.pm.PackageManager;
17 import android.content.res.Resources;
18 import android.graphics.Bitmap;
19 import android.graphics.Bitmap.Config;
20 import android.graphics.Canvas;
21 import android.graphics.Color;
22 import android.graphics.Paint;
23 import android.graphics.PaintFlagsDrawFilter;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.graphics.drawable.AdaptiveIconDrawable;
27 import android.graphics.drawable.BitmapDrawable;
28 import android.graphics.drawable.ColorDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.graphics.drawable.DrawableWrapper;
31 import android.graphics.drawable.InsetDrawable;
32 import android.os.Build;
33 import android.os.UserHandle;
34 import android.util.SparseArray;
35 
36 import androidx.annotation.ColorInt;
37 import androidx.annotation.IntDef;
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 
41 import com.android.launcher3.icons.BitmapInfo.Extender;
42 import com.android.launcher3.util.FlagOp;
43 import com.android.launcher3.util.UserIconInfo;
44 
45 import java.lang.annotation.Retention;
46 import java.util.Objects;
47 
48 /**
49  * This class will be moved to androidx library. There shouldn't be any dependency outside
50  * this package.
51  */
52 public class BaseIconFactory implements AutoCloseable {
53 
54     private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
55     private static final float LEGACY_ICON_SCALE = .7f * (1f / (1 + 2 * getExtraInsetFraction()));
56 
57     public static final int MODE_DEFAULT = 0;
58     public static final int MODE_ALPHA = 1;
59     public static final int MODE_WITH_SHADOW = 2;
60     public static final int MODE_HARDWARE = 3;
61     public static final int MODE_HARDWARE_WITH_SHADOW = 4;
62 
63     @Retention(SOURCE)
64     @IntDef({MODE_DEFAULT, MODE_ALPHA, MODE_WITH_SHADOW, MODE_HARDWARE_WITH_SHADOW, MODE_HARDWARE})
65     @interface BitmapGenerationMode {
66     }
67 
68     private static final float ICON_BADGE_SCALE = 0.444f;
69 
70     @NonNull
71     private final Rect mOldBounds = new Rect();
72 
73     @NonNull
74     private final SparseArray<UserIconInfo> mCachedUserInfo = new SparseArray<>();
75 
76     @NonNull
77     protected final Context mContext;
78 
79     @NonNull
80     private final Canvas mCanvas;
81 
82     @NonNull
83     private final PackageManager mPm;
84 
85     @NonNull
86     private final ColorExtractor mColorExtractor;
87 
88     protected final int mFillResIconDpi;
89     protected final int mIconBitmapSize;
90 
91     protected boolean mMonoIconEnabled;
92 
93     @Nullable
94     private IconNormalizer mNormalizer;
95 
96     @Nullable
97     private ShadowGenerator mShadowGenerator;
98 
99     private final boolean mShapeDetection;
100 
101     // Shadow bitmap used as background for theme icons
102     private Bitmap mWhiteShadowLayer;
103 
104     private Drawable mWrapperIcon;
105     private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
106 
107     private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245);
108 
BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, boolean shapeDetection)109     protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
110             boolean shapeDetection) {
111         mContext = context.getApplicationContext();
112         mShapeDetection = shapeDetection;
113         mFillResIconDpi = fillResIconDpi;
114         mIconBitmapSize = iconBitmapSize;
115 
116         mPm = mContext.getPackageManager();
117         mColorExtractor = new ColorExtractor();
118 
119         mCanvas = new Canvas();
120         mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
121         clear();
122     }
123 
BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize)124     public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
125         this(context, fillResIconDpi, iconBitmapSize, false);
126     }
127 
clear()128     protected void clear() {
129         mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
130     }
131 
132     @NonNull
getShadowGenerator()133     public ShadowGenerator getShadowGenerator() {
134         if (mShadowGenerator == null) {
135             mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
136         }
137         return mShadowGenerator;
138     }
139 
140     @NonNull
getNormalizer()141     public IconNormalizer getNormalizer() {
142         if (mNormalizer == null) {
143             mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
144         }
145         return mNormalizer;
146     }
147 
148     @SuppressWarnings("deprecation")
createIconBitmap(Intent.ShortcutIconResource iconRes)149     public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
150         try {
151             Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
152             if (resources != null) {
153                 final int id = resources.getIdentifier(iconRes.resourceName, null, null);
154                 // do not stamp old legacy shortcuts as the app may have already forgotten about it
155                 return createBadgedIconBitmap(resources.getDrawableForDensity(id, mFillResIconDpi));
156             }
157         } catch (Exception e) {
158             // Icon not found.
159         }
160         return null;
161     }
162 
163     /**
164      * Create a placeholder icon using the passed in text.
165      *
166      * @param placeholder used for foreground element in the icon bitmap
167      * @param color       used for the foreground text color
168      */
createIconBitmap(String placeholder, int color)169     public BitmapInfo createIconBitmap(String placeholder, int color) {
170         AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
171                 new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
172                 new CenterTextDrawable(placeholder, color));
173         Bitmap icon = createIconBitmap(drawable, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
174         return BitmapInfo.of(icon, color);
175     }
176 
createIconBitmap(Bitmap icon)177     public BitmapInfo createIconBitmap(Bitmap icon) {
178         if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
179             icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
180         }
181 
182         return BitmapInfo.of(icon, mColorExtractor.findDominantColorByHue(icon));
183     }
184 
185     /**
186      * Creates an icon from the bitmap cropped to the current device icon shape
187      */
188     @NonNull
createShapedIconBitmap(Bitmap icon, IconOptions options)189     public BitmapInfo createShapedIconBitmap(Bitmap icon, IconOptions options) {
190         Drawable d = new FixedSizeBitmapDrawable(icon);
191         float inset = getExtraInsetFraction();
192         inset = inset / (1 + 2 * inset);
193         d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK),
194                 new InsetDrawable(d, inset, inset, inset, inset));
195         return createBadgedIconBitmap(d, options);
196     }
197 
198     @NonNull
createBadgedIconBitmap(@onNull Drawable icon)199     public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon) {
200         return createBadgedIconBitmap(icon, null);
201     }
202 
203     /**
204      * Creates bitmap using the source drawable and various parameters.
205      * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
206      *
207      * @param icon source of the icon
208      * @return a bitmap suitable for disaplaying as an icon at various system UIs.
209      */
210     @TargetApi(Build.VERSION_CODES.TIRAMISU)
211     @NonNull
createBadgedIconBitmap(@onNull Drawable icon, @Nullable IconOptions options)212     public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon,
213             @Nullable IconOptions options) {
214         float[] scale = new float[1];
215         AdaptiveIconDrawable adaptiveIcon = normalizeAndWrapToAdaptiveIcon(icon, null, scale);
216         Bitmap bitmap = createIconBitmap(adaptiveIcon, scale[0],
217                 options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
218 
219         int color = (options != null && options.mExtractedColor != null)
220                 ? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap);
221         BitmapInfo info = BitmapInfo.of(bitmap, color);
222 
223         if (adaptiveIcon instanceof BitmapInfo.Extender extender) {
224             info = extender.getExtendedInfo(bitmap, color, this, scale[0]);
225         } else if (IconProvider.ATLEAST_T && mMonoIconEnabled) {
226             Drawable mono = getMonochromeDrawable(adaptiveIcon);
227             if (mono != null) {
228                 info.setMonoIcon(createIconBitmap(mono, scale[0], MODE_ALPHA), this);
229             }
230         }
231         info = info.withFlags(getBitmapFlagOp(options));
232         return info;
233     }
234 
235     /**
236      * Returns a monochromatic version of the given drawable or null, if it is not supported
237      *
238      * @param base the original icon
239      */
240     @TargetApi(Build.VERSION_CODES.TIRAMISU)
getMonochromeDrawable(AdaptiveIconDrawable base)241     protected Drawable getMonochromeDrawable(AdaptiveIconDrawable base) {
242         Drawable mono = base.getMonochrome();
243         if (mono != null) {
244             return new ClippedMonoDrawable(mono);
245         }
246         return null;
247     }
248 
249     @NonNull
getBitmapFlagOp(@ullable IconOptions options)250     public FlagOp getBitmapFlagOp(@Nullable IconOptions options) {
251         FlagOp op = FlagOp.NO_OP;
252         if (options != null) {
253             if (options.mIsInstantApp) {
254                 op = op.addFlag(FLAG_INSTANT);
255             }
256 
257             UserIconInfo info = options.mUserIconInfo;
258             if (info == null && options.mUserHandle != null) {
259                 info = getUserInfo(options.mUserHandle);
260             }
261             if (info != null) {
262                 op = info.applyBitmapInfoFlags(op);
263             }
264         }
265         return op;
266     }
267 
268     @NonNull
getUserInfo(@onNull UserHandle user)269     protected UserIconInfo getUserInfo(@NonNull UserHandle user) {
270         int key = user.hashCode();
271         UserIconInfo info = mCachedUserInfo.get(key);
272         /*
273          * We do not have the ability to distinguish between different badged users here.
274          * As such all badged users will have the work profile badge applied.
275          */
276         if (info == null) {
277             // Simple check to check if the provided user is work profile or not based on badging
278             NoopDrawable d = new NoopDrawable();
279             boolean isWork = (d != mPm.getUserBadgedIcon(d, user));
280             info = new UserIconInfo(user, isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN);
281             mCachedUserInfo.put(key, info);
282         }
283         return info;
284     }
285 
286     @NonNull
getWhiteShadowLayer()287     public Bitmap getWhiteShadowLayer() {
288         if (mWhiteShadowLayer == null) {
289             mWhiteShadowLayer = createScaledBitmap(
290                     new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null),
291                     MODE_HARDWARE_WITH_SHADOW);
292         }
293         return mWhiteShadowLayer;
294     }
295 
296     @NonNull
createScaledBitmap(@onNull Drawable icon, @BitmapGenerationMode int mode)297     public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) {
298         RectF iconBounds = new RectF();
299         float[] scale = new float[1];
300         icon = normalizeAndWrapToAdaptiveIcon(icon, iconBounds, scale);
301         return createIconBitmap(icon,
302                 Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)), mode);
303     }
304 
305     /**
306      * Sets the background color used for wrapped adaptive icon
307      */
setWrapperBackgroundColor(final int color)308     public void setWrapperBackgroundColor(final int color) {
309         mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
310     }
311 
312     @Nullable
normalizeAndWrapToAdaptiveIcon(@ullable Drawable icon, @Nullable final RectF outIconBounds, @NonNull final float[] outScale)313     protected AdaptiveIconDrawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon,
314             @Nullable final RectF outIconBounds, @NonNull final float[] outScale) {
315         if (icon == null) {
316             return null;
317         }
318 
319         AdaptiveIconDrawable adaptiveIcon;
320         float scale;
321         adaptiveIcon = wrapToAdaptiveIcon(icon, outIconBounds);
322         scale = getNormalizer().getScale(adaptiveIcon, outIconBounds, null, null);
323         outScale[0] = scale;
324         return adaptiveIcon;
325     }
326 
327     /**
328      * Returns a drawable which draws the original drawable at a fixed scale
329      */
createScaledDrawable(@onNull Drawable main, float scale)330     private Drawable createScaledDrawable(@NonNull Drawable main, float scale) {
331         float h = main.getIntrinsicHeight();
332         float w = main.getIntrinsicWidth();
333         float scaleX = scale;
334         float scaleY = scale;
335         if (h > w && w > 0) {
336             scaleX *= w / h;
337         } else if (w > h && h > 0) {
338             scaleY *= h / w;
339         }
340         scaleX = (1 - scaleX) / 2;
341         scaleY = (1 - scaleY) / 2;
342         return new InsetDrawable(main, scaleX, scaleY, scaleX, scaleY);
343     }
344 
345     /**
346      * Wraps the provided icon in an adaptive icon drawable
347      */
wrapToAdaptiveIcon(@onNull Drawable icon, @Nullable final RectF outIconBounds)348     public AdaptiveIconDrawable wrapToAdaptiveIcon(@NonNull Drawable icon,
349             @Nullable final RectF outIconBounds) {
350         if (icon instanceof AdaptiveIconDrawable aid) {
351             return aid;
352         } else {
353             EmptyWrapper foreground = new EmptyWrapper();
354             AdaptiveIconDrawable dr = new AdaptiveIconDrawable(
355                     new ColorDrawable(mWrapperBackgroundColor), foreground);
356             dr.setBounds(0, 0, 1, 1);
357             boolean[] outShape = new boolean[1];
358             float scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
359             if (!outShape[0]) {
360                 foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE));
361             } else {
362                 foreground.setDrawable(createScaledDrawable(icon, 1 - getExtraInsetFraction()));
363             }
364             return dr;
365         }
366     }
367 
368     @NonNull
createIconBitmap(@ullable final Drawable icon, final float scale)369     protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) {
370         return createIconBitmap(icon, scale, MODE_DEFAULT);
371     }
372 
373     @NonNull
createIconBitmap(@ullable final Drawable icon, final float scale, @BitmapGenerationMode int bitmapGenerationMode)374     protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale,
375             @BitmapGenerationMode int bitmapGenerationMode) {
376         final int size = mIconBitmapSize;
377         final Bitmap bitmap;
378         switch (bitmapGenerationMode) {
379             case MODE_ALPHA:
380                 bitmap = Bitmap.createBitmap(size, size, Config.ALPHA_8);
381                 break;
382             case MODE_HARDWARE:
383             case MODE_HARDWARE_WITH_SHADOW: {
384                 return BitmapRenderer.createHardwareBitmap(size, size, canvas ->
385                         drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null));
386             }
387             case MODE_WITH_SHADOW:
388             default:
389                 bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888);
390                 break;
391         }
392         if (icon == null) {
393             return bitmap;
394         }
395         mCanvas.setBitmap(bitmap);
396         drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap);
397         mCanvas.setBitmap(null);
398         return bitmap;
399     }
400 
drawIconBitmap(@onNull Canvas canvas, @Nullable final Drawable icon, final float scale, @BitmapGenerationMode int bitmapGenerationMode, @Nullable Bitmap targetBitmap)401     private void drawIconBitmap(@NonNull Canvas canvas, @Nullable final Drawable icon,
402             final float scale, @BitmapGenerationMode int bitmapGenerationMode,
403             @Nullable Bitmap targetBitmap) {
404         final int size = mIconBitmapSize;
405         mOldBounds.set(icon.getBounds());
406 
407         if (icon instanceof AdaptiveIconDrawable) {
408             // We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the
409             // moment b/298203449
410             int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
411                     Math.round(size * (1 - scale) / 2));
412             // b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds
413             icon.setBounds(0, 0, size - offset - offset, size - offset - offset);
414             int count = canvas.save();
415             canvas.translate(offset, offset);
416             if (bitmapGenerationMode == MODE_WITH_SHADOW
417                     || bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) {
418                 getShadowGenerator().addPathShadow(
419                         ((AdaptiveIconDrawable) icon).getIconMask(), canvas);
420             }
421 
422             if (icon instanceof BitmapInfo.Extender) {
423                 ((Extender) icon).drawForPersistence(canvas);
424             } else {
425                 icon.draw(canvas);
426             }
427             canvas.restoreToCount(count);
428         } else {
429             if (icon instanceof BitmapDrawable) {
430                 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
431                 Bitmap b = bitmapDrawable.getBitmap();
432                 if (b != null && b.getDensity() == Bitmap.DENSITY_NONE) {
433                     bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
434                 }
435             }
436             int width = size;
437             int height = size;
438 
439             int intrinsicWidth = icon.getIntrinsicWidth();
440             int intrinsicHeight = icon.getIntrinsicHeight();
441             if (intrinsicWidth > 0 && intrinsicHeight > 0) {
442                 // Scale the icon proportionally to the icon dimensions
443                 final float ratio = (float) intrinsicWidth / intrinsicHeight;
444                 if (intrinsicWidth > intrinsicHeight) {
445                     height = (int) (width / ratio);
446                 } else if (intrinsicHeight > intrinsicWidth) {
447                     width = (int) (height * ratio);
448                 }
449             }
450             final int left = (size - width) / 2;
451             final int top = (size - height) / 2;
452             icon.setBounds(left, top, left + width, top + height);
453 
454             canvas.save();
455             canvas.scale(scale, scale, size / 2, size / 2);
456             icon.draw(canvas);
457             canvas.restore();
458 
459             if (bitmapGenerationMode == MODE_WITH_SHADOW && targetBitmap != null) {
460                 // Shadow extraction only works in software mode
461                 getShadowGenerator().drawShadow(targetBitmap, canvas);
462 
463                 // Draw the icon again on top:
464                 canvas.save();
465                 canvas.scale(scale, scale, size / 2, size / 2);
466                 icon.draw(canvas);
467                 canvas.restore();
468             }
469         }
470         icon.setBounds(mOldBounds);
471     }
472 
473     @Override
close()474     public void close() {
475         clear();
476     }
477 
478     @NonNull
makeDefaultIcon()479     public BitmapInfo makeDefaultIcon() {
480         return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi));
481     }
482 
483     @NonNull
getFullResDefaultActivityIcon(final int iconDpi)484     public static Drawable getFullResDefaultActivityIcon(final int iconDpi) {
485         return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity(
486                 android.R.drawable.sym_def_app_icon, iconDpi));
487     }
488 
489     /**
490      * Returns the correct badge size given an icon size
491      */
getBadgeSizeForIconSize(final int iconSize)492     public static int getBadgeSizeForIconSize(final int iconSize) {
493         return (int) (ICON_BADGE_SCALE * iconSize);
494     }
495 
496     public static class IconOptions {
497 
498         boolean mIsInstantApp;
499 
500         @BitmapGenerationMode
501         int mGenerationMode = MODE_WITH_SHADOW;
502 
503         @Nullable
504         UserHandle mUserHandle;
505         @Nullable
506         UserIconInfo mUserIconInfo;
507 
508         @ColorInt
509         @Nullable
510         Integer mExtractedColor;
511 
512 
513         /**
514          * User for this icon, in case of badging
515          */
516         @NonNull
setUser(@ullable final UserHandle user)517         public IconOptions setUser(@Nullable final UserHandle user) {
518             mUserHandle = user;
519             return this;
520         }
521 
522         /**
523          * User for this icon, in case of badging
524          */
525         @NonNull
setUser(@ullable final UserIconInfo user)526         public IconOptions setUser(@Nullable final UserIconInfo user) {
527             mUserIconInfo = user;
528             return this;
529         }
530 
531         /**
532          * If this icon represents an instant app
533          */
534         @NonNull
setInstantApp(final boolean instantApp)535         public IconOptions setInstantApp(final boolean instantApp) {
536             mIsInstantApp = instantApp;
537             return this;
538         }
539 
540         /**
541          * Disables auto color extraction and overrides the color to the provided value
542          */
543         @NonNull
setExtractedColor(@olorInt int color)544         public IconOptions setExtractedColor(@ColorInt int color) {
545             mExtractedColor = color;
546             return this;
547         }
548 
549         /**
550          * Sets the bitmap generation mode to use for the bitmap info. Note that some generation
551          * modes do not support color extraction, so consider setting a extracted color manually
552          * in those cases.
553          */
setBitmapGenerationMode(@itmapGenerationMode int generationMode)554         public IconOptions setBitmapGenerationMode(@BitmapGenerationMode int generationMode) {
555             mGenerationMode = generationMode;
556             return this;
557         }
558     }
559 
560     /**
561      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
562      * This allows the badging to be done based on the action bitmap size rather than
563      * the scaled bitmap size.
564      */
565     private static class FixedSizeBitmapDrawable extends BitmapDrawable {
566 
FixedSizeBitmapDrawable(@ullable final Bitmap bitmap)567         public FixedSizeBitmapDrawable(@Nullable final Bitmap bitmap) {
568             super(null, bitmap);
569         }
570 
571         @Override
getIntrinsicHeight()572         public int getIntrinsicHeight() {
573             return getBitmap().getWidth();
574         }
575 
576         @Override
getIntrinsicWidth()577         public int getIntrinsicWidth() {
578             return getBitmap().getWidth();
579         }
580     }
581 
582     private static class NoopDrawable extends ColorDrawable {
583         @Override
getIntrinsicHeight()584         public int getIntrinsicHeight() {
585             return 1;
586         }
587 
588         @Override
getIntrinsicWidth()589         public int getIntrinsicWidth() {
590             return 1;
591         }
592     }
593 
594     protected static class ClippedMonoDrawable extends InsetDrawable {
595 
596         @NonNull
597         private final AdaptiveIconDrawable mCrop;
598 
ClippedMonoDrawable(@ullable final Drawable base)599         public ClippedMonoDrawable(@Nullable final Drawable base) {
600             super(base, -getExtraInsetFraction());
601             mCrop = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
602         }
603 
604         @Override
draw(Canvas canvas)605         public void draw(Canvas canvas) {
606             mCrop.setBounds(getBounds());
607             int saveCount = canvas.save();
608             canvas.clipPath(mCrop.getIconMask());
609             super.draw(canvas);
610             canvas.restoreToCount(saveCount);
611         }
612     }
613 
614     private static class CenterTextDrawable extends ColorDrawable {
615 
616         @NonNull
617         private final Rect mTextBounds = new Rect();
618 
619         @NonNull
620         private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
621 
622         @NonNull
623         private final String mText;
624 
CenterTextDrawable(@onNull final String text, final int color)625         CenterTextDrawable(@NonNull final String text, final int color) {
626             mText = text;
627             mTextPaint.setColor(color);
628         }
629 
630         @Override
draw(Canvas canvas)631         public void draw(Canvas canvas) {
632             Rect bounds = getBounds();
633             mTextPaint.setTextSize(bounds.height() / 3f);
634             mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
635             canvas.drawText(mText,
636                     bounds.exactCenterX() - mTextBounds.exactCenterX(),
637                     bounds.exactCenterY() - mTextBounds.exactCenterY(),
638                     mTextPaint);
639         }
640     }
641 
642     private static class EmptyWrapper extends DrawableWrapper {
643 
EmptyWrapper()644         EmptyWrapper() {
645             super(new ColorDrawable());
646         }
647 
648         @Override
getConstantState()649         public ConstantState getConstantState() {
650             Drawable d = getDrawable();
651             return d == null ? null : d.getConstantState();
652         }
653     }
654 }
655