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