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