1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.app;
18 
19 import static android.content.Context.ACTIVITY_SERVICE;
20 import static android.graphics.Paint.DITHER_FLAG;
21 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.ActivityManager;
26 import android.content.Context;
27 import android.content.pm.PackageManager;
28 import android.content.res.Resources;
29 import android.content.res.Resources.Theme;
30 import android.graphics.Bitmap;
31 import android.graphics.BlurMaskFilter;
32 import android.graphics.BlurMaskFilter.Blur;
33 import android.graphics.Canvas;
34 import android.graphics.Color;
35 import android.graphics.Paint;
36 import android.graphics.PaintFlagsDrawFilter;
37 import android.graphics.PorterDuff;
38 import android.graphics.PorterDuffXfermode;
39 import android.graphics.Rect;
40 import android.graphics.RectF;
41 import android.graphics.drawable.AdaptiveIconDrawable;
42 import android.graphics.drawable.BitmapDrawable;
43 import android.graphics.drawable.ColorDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.graphics.drawable.DrawableWrapper;
46 import android.os.UserHandle;
47 import android.util.AttributeSet;
48 import android.util.Pools.SynchronizedPool;
49 
50 import com.android.internal.R;
51 
52 import org.xmlpull.v1.XmlPullParser;
53 
54 import java.nio.ByteBuffer;
55 
56 
57 /**
58  * @deprecated Use the Launcher3 Iconloaderlib at packages/apps/Launcher3/iconloaderlib. This class
59  * is a temporary fork of Iconloader. It combines all necessary methods to render app icons that are
60  * possibly badged. It is intended to be used only by Sharesheet for the Q release with custom code.
61  */
62 @Deprecated
63 public class SimpleIconFactory {
64 
65     private static final SynchronizedPool<SimpleIconFactory> sPool =
66             new SynchronizedPool<>(Runtime.getRuntime().availableProcessors());
67 
68     private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
69     private static final float BLUR_FACTOR = 0.5f / 48;
70 
71     private Context mContext;
72     private Canvas mCanvas;
73     private PackageManager mPm;
74 
75     private int mFillResIconDpi;
76     private int mIconBitmapSize;
77     private int mBadgeBitmapSize;
78     private int mWrapperBackgroundColor;
79 
80     private Drawable mWrapperIcon;
81     private final Rect mOldBounds = new Rect();
82 
83     /**
84      * Obtain a SimpleIconFactory from a pool objects.
85      *
86      * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
87      */
88     @Deprecated
obtain(Context ctx)89     public static SimpleIconFactory obtain(Context ctx) {
90         SimpleIconFactory instance = sPool.acquire();
91         if (instance == null) {
92             final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE);
93             final int iconDpi = (am == null) ? 0 : am.getLauncherLargeIconDensity();
94 
95             final Resources r = ctx.getResources();
96             final int iconSize = r.getDimensionPixelSize(R.dimen.resolver_icon_size);
97             final int badgeSize = r.getDimensionPixelSize(R.dimen.resolver_badge_size);
98 
99             instance = new SimpleIconFactory(ctx, iconDpi, iconSize, badgeSize);
100             instance.setWrapperBackgroundColor(Color.WHITE);
101         }
102 
103         return instance;
104     }
105 
106     /**
107      * Recycles the SimpleIconFactory so others may use it.
108      *
109      * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
110      */
111     @Deprecated
recycle()112     public void recycle() {
113         // Return to default background color
114         setWrapperBackgroundColor(Color.WHITE);
115         sPool.release(this);
116     }
117 
118     /**
119      * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
120      */
121     @Deprecated
SimpleIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, int badgeBitmapSize)122     private SimpleIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
123             int badgeBitmapSize) {
124         mContext = context.getApplicationContext();
125         mPm = mContext.getPackageManager();
126         mIconBitmapSize = iconBitmapSize;
127         mBadgeBitmapSize = badgeBitmapSize;
128         mFillResIconDpi = fillResIconDpi;
129 
130         mCanvas = new Canvas();
131         mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
132 
133         // Normalizer init
134         // Use twice the icon size as maximum size to avoid scaling down twice.
135         mMaxSize = iconBitmapSize * 2;
136         mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
137         mScaleCheckCanvas = new Canvas(mBitmap);
138         mPixels = new byte[mMaxSize * mMaxSize];
139         mLeftBorder = new float[mMaxSize];
140         mRightBorder = new float[mMaxSize];
141         mBounds = new Rect();
142         mAdaptiveIconBounds = new Rect();
143         mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
144 
145         // Shadow generator init
146         mDefaultBlurMaskFilter = new BlurMaskFilter(iconBitmapSize * BLUR_FACTOR,
147                 Blur.NORMAL);
148     }
149 
150     /**
151      * Sets the background color used for wrapped adaptive icon
152      *
153      * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
154      */
155     @Deprecated
setWrapperBackgroundColor(int color)156     void setWrapperBackgroundColor(int color) {
157         mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
158     }
159 
160     /**
161      * Creates bitmap using the source drawable and various parameters.
162      * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
163      * Note: this method has been modified from iconloaderlib to remove a profile diff check.
164      *
165      * @param icon                      source of the icon associated with a user that has no badge,
166      *                                  likely user 0
167      * @param user                      info can be used for a badge
168      * @return a bitmap suitable for disaplaying as an icon at various system UIs.
169      *
170      * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
171      */
172     @Deprecated
createUserBadgedIconBitmap(@ullable Drawable icon, UserHandle user)173     Bitmap createUserBadgedIconBitmap(@Nullable Drawable icon, UserHandle user) {
174         float [] scale = new float[1];
175 
176         // If no icon is provided use the system default
177         if (icon == null) {
178             icon = getFullResDefaultActivityIcon(mFillResIconDpi);
179         }
180         icon = normalizeAndWrapToAdaptiveIcon(icon, null, scale);
181         Bitmap bitmap = createIconBitmap(icon, scale[0]);
182         if (icon instanceof AdaptiveIconDrawable) {
183             mCanvas.setBitmap(bitmap);
184             recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
185             mCanvas.setBitmap(null);
186         }
187 
188         final Bitmap result;
189         if (user != null /* if modification from iconloaderlib */) {
190             BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
191             Drawable badged = mPm.getUserBadgedIcon(drawable, user);
192             if (badged instanceof BitmapDrawable) {
193                 result = ((BitmapDrawable) badged).getBitmap();
194             } else {
195                 result = createIconBitmap(badged, 1f);
196             }
197         } else {
198             result = bitmap;
199         }
200 
201         return result;
202     }
203 
204     /**
205      * Creates bitmap using the source drawable and flattened pre-rendered app icon.
206      * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
207      * This is custom functionality added to Iconloaderlib that will need to be ported.
208      *
209      * @param icon                      source of the icon associated with a user that has no badge
210      * @param renderedAppIcon           pre-rendered app icon to use as a badge, likely the output
211      *                                  of createUserBadgedIconBitmap for user 0
212      * @return a bitmap suitable for disaplaying as an icon at various system UIs.
213      *
214      * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
215      */
216     @Deprecated
createAppBadgedIconBitmap(@ullable Drawable icon, Bitmap renderedAppIcon)217     Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) {
218         // If no icon is provided use the system default
219         if (icon == null) {
220             icon = getFullResDefaultActivityIcon(mFillResIconDpi);
221         }
222 
223         // Direct share icons cannot be adaptive, most will arrive as bitmaps. To get reliable
224         // presentation, force all DS icons to be circular. Scale DS image so it completely fills.
225         int w = icon.getIntrinsicWidth();
226         int h = icon.getIntrinsicHeight();
227         float scale = 1;
228         if (h > w && w > 0) {
229             scale = (float) h / w;
230         } else if (w > h && h > 0) {
231             scale = (float) w / h;
232         }
233         Bitmap bitmap = createIconBitmap(icon, scale);
234         bitmap = maskBitmapToCircle(bitmap);
235         icon = new BitmapDrawable(mContext.getResources(), bitmap);
236 
237         // We now have a circular masked and scaled icon, inset and apply shadow
238         scale = getScale(icon, null);
239         bitmap = createIconBitmap(icon, scale);
240 
241         mCanvas.setBitmap(bitmap);
242         recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
243 
244         if (renderedAppIcon != null) {
245             // Now scale down and apply the badge to the bottom right corner of the flattened icon
246             renderedAppIcon = Bitmap.createScaledBitmap(renderedAppIcon, mBadgeBitmapSize,
247                     mBadgeBitmapSize, false);
248 
249             // Paint the provided badge on top of the flattened icon
250             mCanvas.drawBitmap(renderedAppIcon, mIconBitmapSize - mBadgeBitmapSize,
251                     mIconBitmapSize - mBadgeBitmapSize, null);
252         }
253 
254         mCanvas.setBitmap(null);
255 
256         return bitmap;
257     }
258 
maskBitmapToCircle(Bitmap bitmap)259     private Bitmap maskBitmapToCircle(Bitmap bitmap) {
260         final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
261                 bitmap.getHeight(), Bitmap.Config.ARGB_8888);
262         final Canvas canvas = new Canvas(output);
263         final Paint paint = new Paint();
264         paint.setAntiAlias(true);
265 
266         // Draw mask
267         paint.setColor(0xffffffff);
268         canvas.drawARGB(0, 0, 0, 0);
269         canvas.drawCircle(bitmap.getWidth() / 2f,
270                 bitmap.getHeight() / 2f,
271                 bitmap.getWidth() / 2f - 1 /* -1 to avoid circles with flat sides */,
272                 paint);
273 
274         // Draw masked bitmap
275         paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
276         final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
277         canvas.drawBitmap(bitmap, rect, rect, paint);
278 
279         return output;
280     }
281 
getFullResDefaultActivityIcon(int iconDpi)282     private static Drawable getFullResDefaultActivityIcon(int iconDpi) {
283         return Resources.getSystem().getDrawableForDensity(android.R.mipmap.sym_def_app_icon,
284                 iconDpi);
285     }
286 
createIconBitmap(Drawable icon, float scale)287     private Bitmap createIconBitmap(Drawable icon, float scale) {
288         return createIconBitmap(icon, scale, mIconBitmapSize);
289     }
290 
291     /**
292      * @param icon drawable that should be flattened to a bitmap
293      * @param scale the scale to apply before drawing {@param icon} on the canvas
294      */
createIconBitmap(Drawable icon, float scale, int size)295     private Bitmap createIconBitmap(Drawable icon, float scale, int size) {
296         Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
297 
298         mCanvas.setBitmap(bitmap);
299         mOldBounds.set(icon.getBounds());
300 
301         if (icon instanceof AdaptiveIconDrawable) {
302             int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
303                     Math.round(size * (1 - scale) / 2));
304             icon.setBounds(offset, offset, size - offset, size - offset);
305             icon.draw(mCanvas);
306         } else {
307             if (icon instanceof BitmapDrawable) {
308                 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
309                 Bitmap b = bitmapDrawable.getBitmap();
310                 if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
311                     bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
312                 }
313             }
314             int width = size;
315             int height = size;
316 
317             int intrinsicWidth = icon.getIntrinsicWidth();
318             int intrinsicHeight = icon.getIntrinsicHeight();
319             if (intrinsicWidth > 0 && intrinsicHeight > 0) {
320                 // Scale the icon proportionally to the icon dimensions
321                 final float ratio = (float) intrinsicWidth / intrinsicHeight;
322                 if (intrinsicWidth > intrinsicHeight) {
323                     height = (int) (width / ratio);
324                 } else if (intrinsicHeight > intrinsicWidth) {
325                     width = (int) (height * ratio);
326                 }
327             }
328             final int left = (size - width) / 2;
329             final int top = (size - height) / 2;
330             icon.setBounds(left, top, left + width, top + height);
331             mCanvas.save();
332             mCanvas.scale(scale, scale, size / 2, size / 2);
333             icon.draw(mCanvas);
334             mCanvas.restore();
335 
336         }
337 
338         icon.setBounds(mOldBounds);
339         mCanvas.setBitmap(null);
340         return bitmap;
341     }
342 
normalizeAndWrapToAdaptiveIcon(Drawable icon, RectF outIconBounds, float[] outScale)343     private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, RectF outIconBounds,
344             float[] outScale) {
345         float scale = 1f;
346 
347         if (mWrapperIcon == null) {
348             mWrapperIcon = mContext.getDrawable(
349                     R.drawable.iconfactory_adaptive_icon_drawable_wrapper).mutate();
350         }
351 
352         AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
353         dr.setBounds(0, 0, 1, 1);
354         scale = getScale(icon, outIconBounds);
355         if (!(icon instanceof AdaptiveIconDrawable)) {
356             FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
357             fsd.setDrawable(icon);
358             fsd.setScale(scale);
359             icon = dr;
360             scale = getScale(icon, outIconBounds);
361 
362             ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
363         }
364 
365         outScale[0] = scale;
366         return icon;
367     }
368 
369 
370     /* Normalization block */
371 
372     private static final float SCALE_NOT_INITIALIZED = 0;
373     // Ratio of icon visible area to full icon size for a square shaped icon
374     private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
375     // Ratio of icon visible area to full icon size for a circular shaped icon
376     private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
377 
378     private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
379 
380     // Slope used to calculate icon visible area to full icon size for any generic shaped icon.
381     private static final float LINEAR_SCALE_SLOPE =
382             (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
383 
384     private static final int MIN_VISIBLE_ALPHA = 40;
385 
386     private float mAdaptiveIconScale;
387     private final Rect mAdaptiveIconBounds;
388     private final Rect mBounds;
389     private final int mMaxSize;
390     private final byte[] mPixels;
391     private final float[] mLeftBorder;
392     private final float[] mRightBorder;
393     private final Bitmap mBitmap;
394     private final Canvas mScaleCheckCanvas;
395 
396     /**
397      * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
398      * matches the design guidelines for a launcher icon.
399      *
400      * We first calculate the convex hull of the visible portion of the icon.
401      * This hull then compared with the bounding rectangle of the hull to find how closely it
402      * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not
403      * an ideal solution but it gives satisfactory result without affecting the performance.
404      *
405      * This closeness is used to determine the ratio of hull area to the full icon size.
406      * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
407      *
408      * @param outBounds optional rect to receive the fraction distance from each edge.
409      */
getScale(@onNull Drawable d, @Nullable RectF outBounds)410     private synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds) {
411         if (d instanceof AdaptiveIconDrawable) {
412             if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) {
413                 if (outBounds != null) {
414                     outBounds.set(mAdaptiveIconBounds);
415                 }
416                 return mAdaptiveIconScale;
417             }
418         }
419         int width = d.getIntrinsicWidth();
420         int height = d.getIntrinsicHeight();
421         if (width <= 0 || height <= 0) {
422             width = width <= 0 || width > mMaxSize ? mMaxSize : width;
423             height = height <= 0 || height > mMaxSize ? mMaxSize : height;
424         } else if (width > mMaxSize || height > mMaxSize) {
425             int max = Math.max(width, height);
426             width = mMaxSize * width / max;
427             height = mMaxSize * height / max;
428         }
429 
430         mBitmap.eraseColor(Color.TRANSPARENT);
431         d.setBounds(0, 0, width, height);
432         d.draw(mScaleCheckCanvas);
433 
434         ByteBuffer buffer = ByteBuffer.wrap(mPixels);
435         buffer.rewind();
436         mBitmap.copyPixelsToBuffer(buffer);
437 
438         // Overall bounds of the visible icon.
439         int topY = -1;
440         int bottomY = -1;
441         int leftX = mMaxSize + 1;
442         int rightX = -1;
443 
444         // Create border by going through all pixels one row at a time and for each row find
445         // the first and the last non-transparent pixel. Set those values to mLeftBorder and
446         // mRightBorder and use -1 if there are no visible pixel in the row.
447 
448         // buffer position
449         int index = 0;
450         // buffer shift after every row, width of buffer = mMaxSize
451         int rowSizeDiff = mMaxSize - width;
452         // first and last position for any row.
453         int firstX, lastX;
454 
455         for (int y = 0; y < height; y++) {
456             firstX = lastX = -1;
457             for (int x = 0; x < width; x++) {
458                 if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
459                     if (firstX == -1) {
460                         firstX = x;
461                     }
462                     lastX = x;
463                 }
464                 index++;
465             }
466             index += rowSizeDiff;
467 
468             mLeftBorder[y] = firstX;
469             mRightBorder[y] = lastX;
470 
471             // If there is at least one visible pixel, update the overall bounds.
472             if (firstX != -1) {
473                 bottomY = y;
474                 if (topY == -1) {
475                     topY = y;
476                 }
477 
478                 leftX = Math.min(leftX, firstX);
479                 rightX = Math.max(rightX, lastX);
480             }
481         }
482 
483         if (topY == -1 || rightX == -1) {
484             // No valid pixels found. Do not scale.
485             return 1;
486         }
487 
488         convertToConvexArray(mLeftBorder, 1, topY, bottomY);
489         convertToConvexArray(mRightBorder, -1, topY, bottomY);
490 
491         // Area of the convex hull
492         float area = 0;
493         for (int y = 0; y < height; y++) {
494             if (mLeftBorder[y] <= -1) {
495                 continue;
496             }
497             area += mRightBorder[y] - mLeftBorder[y] + 1;
498         }
499 
500         // Area of the rectangle required to fit the convex hull
501         float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
502         float hullByRect = area / rectArea;
503 
504         float scaleRequired;
505         if (hullByRect < CIRCLE_AREA_BY_RECT) {
506             scaleRequired = MAX_CIRCLE_AREA_FACTOR;
507         } else {
508             scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
509         }
510         mBounds.left = leftX;
511         mBounds.right = rightX;
512 
513         mBounds.top = topY;
514         mBounds.bottom = bottomY;
515 
516         if (outBounds != null) {
517             outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
518                     1 - ((float) mBounds.right) / width,
519                     1 - ((float) mBounds.bottom) / height);
520         }
521         float areaScale = area / (width * height);
522         // Use sqrt of the final ratio as the images is scaled across both width and height.
523         float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
524         if (d instanceof AdaptiveIconDrawable && mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
525             mAdaptiveIconScale = scale;
526             mAdaptiveIconBounds.set(mBounds);
527         }
528         return scale;
529     }
530 
531     /**
532      * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
533      * (except on either ends) with appropriate values.
534      * @param xCoordinates map of x coordinate per y.
535      * @param direction 1 for left border and -1 for right border.
536      * @param topY the first Y position (inclusive) with a valid value.
537      * @param bottomY the last Y position (inclusive) with a valid value.
538      */
convertToConvexArray( float[] xCoordinates, int direction, int topY, int bottomY)539     private static void convertToConvexArray(
540             float[] xCoordinates, int direction, int topY, int bottomY) {
541         int total = xCoordinates.length;
542         // The tangent at each pixel.
543         float[] angles = new float[total - 1];
544 
545         int first = topY; // First valid y coordinate
546         int last = -1;    // Last valid y coordinate which didn't have a missing value
547 
548         float lastAngle = Float.MAX_VALUE;
549 
550         for (int i = topY + 1; i <= bottomY; i++) {
551             if (xCoordinates[i] <= -1) {
552                 continue;
553             }
554             int start;
555 
556             if (lastAngle == Float.MAX_VALUE) {
557                 start = first;
558             } else {
559                 float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
560                 start = last;
561                 // If this position creates a concave angle, keep moving up until we find a
562                 // position which creates a convex angle.
563                 if ((currentAngle - lastAngle) * direction < 0) {
564                     while (start > first) {
565                         start--;
566                         currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
567                         if ((currentAngle - angles[start]) * direction >= 0) {
568                             break;
569                         }
570                     }
571                 }
572             }
573 
574             // Reset from last check
575             lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
576             // Update all the points from start.
577             for (int j = start; j < i; j++) {
578                 angles[j] = lastAngle;
579                 xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
580             }
581             last = i;
582         }
583     }
584 
585     /* Shadow generator block */
586 
587     private static final float KEY_SHADOW_DISTANCE = 1f / 48;
588     private static final int KEY_SHADOW_ALPHA = 61;
589     private static final int AMBIENT_SHADOW_ALPHA = 30;
590 
591     private Paint mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
592     private Paint mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
593     private BlurMaskFilter mDefaultBlurMaskFilter;
594 
recreateIcon(Bitmap icon, Canvas out)595     private synchronized void recreateIcon(Bitmap icon, Canvas out) {
596         recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
597     }
598 
recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter, int ambientAlpha, int keyAlpha, Canvas out)599     private synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
600             int ambientAlpha, int keyAlpha, Canvas out) {
601         int[] offset = new int[2];
602         mBlurPaint.setMaskFilter(blurMaskFilter);
603         Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
604 
605         // Draw ambient shadow
606         mDrawPaint.setAlpha(ambientAlpha);
607         out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
608 
609         // Draw key shadow
610         mDrawPaint.setAlpha(keyAlpha);
611         out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconBitmapSize,
612                 mDrawPaint);
613 
614         // Draw the icon
615         mDrawPaint.setAlpha(255); // TODO if b/128609682 not fixed by launch use .setAlpha(254)
616         out.drawBitmap(icon, 0, 0, mDrawPaint);
617     }
618 
619     /* Classes */
620 
621     /**
622      * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
623      */
624     public static class FixedScaleDrawable extends DrawableWrapper {
625 
626         private static final float LEGACY_ICON_SCALE = .7f * .6667f;
627         private float mScaleX, mScaleY;
628 
FixedScaleDrawable()629         public FixedScaleDrawable() {
630             super(new ColorDrawable());
631             mScaleX = LEGACY_ICON_SCALE;
632             mScaleY = LEGACY_ICON_SCALE;
633         }
634 
635         @Override
draw(@onNull Canvas canvas)636         public void draw(@NonNull Canvas canvas) {
637             int saveCount = canvas.save();
638             canvas.scale(mScaleX, mScaleY,
639                     getBounds().exactCenterX(), getBounds().exactCenterY());
640             super.draw(canvas);
641             canvas.restoreToCount(saveCount);
642         }
643 
644         @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs)645         public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
646 
647         @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)648         public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
649 
650         /**
651          * Sets the scale associated with this drawable
652          * @param scale
653          */
setScale(float scale)654         public void setScale(float scale) {
655             float h = getIntrinsicHeight();
656             float w = getIntrinsicWidth();
657             mScaleX = scale * LEGACY_ICON_SCALE;
658             mScaleY = scale * LEGACY_ICON_SCALE;
659             if (h > w && w > 0) {
660                 mScaleX *= w / h;
661             } else if (w > h && h > 0) {
662                 mScaleY *= h / w;
663             }
664         }
665     }
666 
667     /**
668      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
669      * This allows the badging to be done based on the action bitmap size rather than
670      * the scaled bitmap size.
671      */
672     private static class FixedSizeBitmapDrawable extends BitmapDrawable {
673 
FixedSizeBitmapDrawable(Bitmap bitmap)674         FixedSizeBitmapDrawable(Bitmap bitmap) {
675             super(null, bitmap);
676         }
677 
678         @Override
getIntrinsicHeight()679         public int getIntrinsicHeight() {
680             return getBitmap().getWidth();
681         }
682 
683         @Override
getIntrinsicWidth()684         public int getIntrinsicWidth() {
685             return getBitmap().getWidth();
686         }
687     }
688 
689 }
690