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