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