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