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