1 package com.android.launcher3.icons; 2 3 import static android.graphics.Paint.DITHER_FLAG; 4 import static android.graphics.Paint.FILTER_BITMAP_FLAG; 5 6 import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; 7 8 import android.content.Context; 9 import android.content.Intent; 10 import android.content.pm.PackageManager; 11 import android.content.res.Resources; 12 import android.graphics.Bitmap; 13 import android.graphics.Canvas; 14 import android.graphics.Color; 15 import android.graphics.Paint; 16 import android.graphics.PaintFlagsDrawFilter; 17 import android.graphics.Rect; 18 import android.graphics.RectF; 19 import android.graphics.drawable.AdaptiveIconDrawable; 20 import android.graphics.drawable.BitmapDrawable; 21 import android.graphics.drawable.ColorDrawable; 22 import android.graphics.drawable.Drawable; 23 import android.graphics.drawable.InsetDrawable; 24 import android.os.Build; 25 import android.os.Process; 26 import android.os.UserHandle; 27 28 import androidx.annotation.NonNull; 29 30 import com.android.launcher3.icons.BitmapInfo.Extender; 31 32 /** 33 * This class will be moved to androidx library. There shouldn't be any dependency outside 34 * this package. 35 */ 36 public class BaseIconFactory implements AutoCloseable { 37 38 private static final String TAG = "BaseIconFactory"; 39 private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; 40 static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; 41 static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; 42 43 private static final float ICON_BADGE_SCALE = 0.444f; 44 45 private final Rect mOldBounds = new Rect(); 46 protected final Context mContext; 47 private final Canvas mCanvas; 48 private final PackageManager mPm; 49 private final ColorExtractor mColorExtractor; 50 private boolean mDisableColorExtractor; 51 private boolean mBadgeOnLeft = false; 52 53 protected final int mFillResIconDpi; 54 protected final int mIconBitmapSize; 55 56 private IconNormalizer mNormalizer; 57 private ShadowGenerator mShadowGenerator; 58 private final boolean mShapeDetection; 59 60 private Drawable mWrapperIcon; 61 private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; 62 private Bitmap mUserBadgeBitmap; 63 64 private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 65 private static final float PLACEHOLDER_TEXT_SIZE = 20f; 66 private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(240, 240, 240); 67 BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, boolean shapeDetection)68 protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, 69 boolean shapeDetection) { 70 mContext = context.getApplicationContext(); 71 mShapeDetection = shapeDetection; 72 mFillResIconDpi = fillResIconDpi; 73 mIconBitmapSize = iconBitmapSize; 74 75 mPm = mContext.getPackageManager(); 76 mColorExtractor = new ColorExtractor(); 77 78 mCanvas = new Canvas(); 79 mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); 80 mTextPaint.setTextAlign(Paint.Align.CENTER); 81 mTextPaint.setColor(PLACEHOLDER_BACKGROUND_COLOR); 82 mTextPaint.setTextSize(context.getResources().getDisplayMetrics().density * 83 PLACEHOLDER_TEXT_SIZE); 84 clear(); 85 } 86 BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize)87 public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) { 88 this(context, fillResIconDpi, iconBitmapSize, false); 89 } 90 clear()91 protected void clear() { 92 mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; 93 mDisableColorExtractor = false; 94 mBadgeOnLeft = false; 95 } 96 getShadowGenerator()97 public ShadowGenerator getShadowGenerator() { 98 if (mShadowGenerator == null) { 99 mShadowGenerator = new ShadowGenerator(mIconBitmapSize); 100 } 101 return mShadowGenerator; 102 } 103 getNormalizer()104 public IconNormalizer getNormalizer() { 105 if (mNormalizer == null) { 106 mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection); 107 } 108 return mNormalizer; 109 } 110 111 @SuppressWarnings("deprecation") createIconBitmap(Intent.ShortcutIconResource iconRes)112 public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) { 113 try { 114 Resources resources = mPm.getResourcesForApplication(iconRes.packageName); 115 if (resources != null) { 116 final int id = resources.getIdentifier(iconRes.resourceName, null, null); 117 // do not stamp old legacy shortcuts as the app may have already forgotten about it 118 return createBadgedIconBitmap( 119 resources.getDrawableForDensity(id, mFillResIconDpi), 120 Process.myUserHandle() /* only available on primary user */, 121 false /* do not apply legacy treatment */); 122 } 123 } catch (Exception e) { 124 // Icon not found. 125 } 126 return null; 127 } 128 129 /** 130 * Create a placeholder icon using the passed in text. 131 * 132 * @param placeholder used for foreground element in the icon bitmap 133 * @param color used for the foreground text color 134 * @return 135 */ createIconBitmap(String placeholder, int color)136 public BitmapInfo createIconBitmap(String placeholder, int color) { 137 if (!ATLEAST_OREO) return null; 138 139 Bitmap placeholderBitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize, 140 Bitmap.Config.ARGB_8888); 141 mTextPaint.setColor(color); 142 Canvas canvas = new Canvas(placeholderBitmap); 143 canvas.drawText(placeholder, mIconBitmapSize / 2, mIconBitmapSize * 5 / 8, mTextPaint); 144 AdaptiveIconDrawable drawable = new AdaptiveIconDrawable( 145 new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR), 146 new BitmapDrawable(mContext.getResources(), placeholderBitmap)); 147 Bitmap icon = createIconBitmap(drawable, 1f); 148 return BitmapInfo.of(icon, extractColor(icon)); 149 } 150 createIconBitmap(Bitmap icon)151 public BitmapInfo createIconBitmap(Bitmap icon) { 152 if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) { 153 icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f); 154 } 155 156 return BitmapInfo.of(icon, extractColor(icon)); 157 } 158 159 /** 160 * Creates an icon from the bitmap cropped to the current device icon shape 161 */ createShapedIconBitmap(Bitmap icon, UserHandle user)162 public BitmapInfo createShapedIconBitmap(Bitmap icon, UserHandle user) { 163 Drawable d = new FixedSizeBitmapDrawable(icon); 164 if (ATLEAST_OREO) { 165 float inset = AdaptiveIconDrawable.getExtraInsetFraction(); 166 inset = inset / (1 + 2 * inset); 167 d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), 168 new InsetDrawable(d, inset, inset, inset, inset)); 169 } 170 return createBadgedIconBitmap(d, user, true); 171 } 172 createBadgedIconBitmap(Drawable icon, UserHandle user, boolean shrinkNonAdaptiveIcons)173 public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, 174 boolean shrinkNonAdaptiveIcons) { 175 return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null); 176 } 177 createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk)178 public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, 179 int iconAppTargetSdk) { 180 return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false); 181 } 182 createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, boolean isInstantApp)183 public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, 184 int iconAppTargetSdk, boolean isInstantApp) { 185 return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null); 186 } 187 createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, boolean isInstantApp, float[] scale)188 public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, 189 int iconAppTargetSdk, boolean isInstantApp, float[] scale) { 190 boolean shrinkNonAdaptiveIcons = ATLEAST_P || 191 (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O); 192 return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale); 193 } 194 createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk)195 public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) { 196 boolean shrinkNonAdaptiveIcons = ATLEAST_P || 197 (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O); 198 return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons); 199 } 200 201 /** 202 * Creates bitmap using the source drawable and various parameters. 203 * The bitmap is visually normalized with other icons and has enough spacing to add shadow. 204 * 205 * @param icon source of the icon 206 * @param user info can be used for a badge 207 * @param shrinkNonAdaptiveIcons {@code true} if non adaptive icons should be treated 208 * @param isInstantApp info can be used for a badge 209 * @param scale returns the scale result from normalization 210 * @return a bitmap suitable for disaplaying as an icon at various system UIs. 211 */ createBadgedIconBitmap(@onNull Drawable icon, UserHandle user, boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale)212 public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user, 213 boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) { 214 if (scale == null) { 215 scale = new float[1]; 216 } 217 icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale); 218 Bitmap bitmap = createIconBitmap(icon, scale[0]); 219 if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { 220 mCanvas.setBitmap(bitmap); 221 getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); 222 mCanvas.setBitmap(null); 223 } 224 225 if (isInstantApp) { 226 badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge)); 227 } 228 if (user != null) { 229 BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); 230 Drawable badged = mPm.getUserBadgedIcon(drawable, user); 231 if (badged instanceof BitmapDrawable) { 232 bitmap = ((BitmapDrawable) badged).getBitmap(); 233 } else { 234 bitmap = createIconBitmap(badged, 1f); 235 } 236 } 237 int color = extractColor(bitmap); 238 return icon instanceof BitmapInfo.Extender 239 ? ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0], user) 240 : BitmapInfo.of(bitmap, color); 241 } 242 getUserBadgeBitmap(UserHandle user)243 public Bitmap getUserBadgeBitmap(UserHandle user) { 244 if (mUserBadgeBitmap == null) { 245 Bitmap bitmap = Bitmap.createBitmap( 246 mIconBitmapSize, mIconBitmapSize, Bitmap.Config.ARGB_8888); 247 Drawable badgedDrawable = mPm.getUserBadgedIcon( 248 new FixedSizeBitmapDrawable(bitmap), user); 249 if (badgedDrawable instanceof BitmapDrawable) { 250 mUserBadgeBitmap = ((BitmapDrawable) badgedDrawable).getBitmap(); 251 } else { 252 badgedDrawable.setBounds(0, 0, mIconBitmapSize, mIconBitmapSize); 253 mUserBadgeBitmap = BitmapRenderer.createSoftwareBitmap( 254 mIconBitmapSize, mIconBitmapSize, badgedDrawable::draw); 255 } 256 } 257 return mUserBadgeBitmap; 258 } 259 createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons)260 public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) { 261 RectF iconBounds = new RectF(); 262 float[] scale = new float[1]; 263 icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale); 264 return createIconBitmap(icon, 265 Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds))); 266 } 267 268 /** 269 * Switches badging to left/right 270 */ setBadgeOnLeft(boolean badgeOnLeft)271 public void setBadgeOnLeft(boolean badgeOnLeft) { 272 mBadgeOnLeft = badgeOnLeft; 273 } 274 275 /** 276 * Sets the background color used for wrapped adaptive icon 277 */ setWrapperBackgroundColor(int color)278 public void setWrapperBackgroundColor(int color) { 279 mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; 280 } 281 282 /** 283 * Disables the dominant color extraction for all icons loaded. 284 */ disableColorExtraction()285 public void disableColorExtraction() { 286 mDisableColorExtractor = true; 287 } 288 normalizeAndWrapToAdaptiveIcon(@onNull Drawable icon, boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale)289 private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon, 290 boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) { 291 if (icon == null) { 292 return null; 293 } 294 float scale = 1f; 295 296 if (shrinkNonAdaptiveIcons && ATLEAST_OREO) { 297 if (mWrapperIcon == null) { 298 mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) 299 .mutate(); 300 } 301 AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; 302 dr.setBounds(0, 0, 1, 1); 303 boolean[] outShape = new boolean[1]; 304 scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); 305 if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) { 306 FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); 307 fsd.setDrawable(icon); 308 fsd.setScale(scale); 309 icon = dr; 310 scale = getNormalizer().getScale(icon, outIconBounds, null, null); 311 312 ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); 313 } 314 } else { 315 scale = getNormalizer().getScale(icon, outIconBounds, null, null); 316 } 317 318 outScale[0] = scale; 319 return icon; 320 } 321 322 /** 323 * Adds the {@param badge} on top of {@param target} using the badge dimensions. 324 */ badgeWithDrawable(Bitmap target, Drawable badge)325 public void badgeWithDrawable(Bitmap target, Drawable badge) { 326 mCanvas.setBitmap(target); 327 badgeWithDrawable(mCanvas, badge); 328 mCanvas.setBitmap(null); 329 } 330 331 /** 332 * Adds the {@param badge} on top of {@param target} using the badge dimensions. 333 */ badgeWithDrawable(Canvas target, Drawable badge)334 public void badgeWithDrawable(Canvas target, Drawable badge) { 335 int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize); 336 if (mBadgeOnLeft) { 337 badge.setBounds(0, mIconBitmapSize - badgeSize, badgeSize, mIconBitmapSize); 338 } else { 339 badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, 340 mIconBitmapSize, mIconBitmapSize); 341 } 342 badge.draw(target); 343 } 344 createIconBitmap(Drawable icon, float scale)345 private Bitmap createIconBitmap(Drawable icon, float scale) { 346 return createIconBitmap(icon, scale, mIconBitmapSize); 347 } 348 349 /** 350 * @param icon drawable that should be flattened to a bitmap 351 * @param scale the scale to apply before drawing {@param icon} on the canvas 352 */ createIconBitmap(@onNull Drawable icon, float scale, int size)353 public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) { 354 Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); 355 if (icon == null) { 356 return bitmap; 357 } 358 mCanvas.setBitmap(bitmap); 359 mOldBounds.set(icon.getBounds()); 360 361 if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { 362 int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), 363 Math.round(size * (1 - scale) / 2 )); 364 icon.setBounds(offset, offset, size - offset, size - offset); 365 if (icon instanceof BitmapInfo.Extender) { 366 ((Extender) icon).drawForPersistence(mCanvas); 367 } else { 368 icon.draw(mCanvas); 369 } 370 } else { 371 if (icon instanceof BitmapDrawable) { 372 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; 373 Bitmap b = bitmapDrawable.getBitmap(); 374 if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) { 375 bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); 376 } 377 } 378 int width = size; 379 int height = size; 380 381 int intrinsicWidth = icon.getIntrinsicWidth(); 382 int intrinsicHeight = icon.getIntrinsicHeight(); 383 if (intrinsicWidth > 0 && intrinsicHeight > 0) { 384 // Scale the icon proportionally to the icon dimensions 385 final float ratio = (float) intrinsicWidth / intrinsicHeight; 386 if (intrinsicWidth > intrinsicHeight) { 387 height = (int) (width / ratio); 388 } else if (intrinsicHeight > intrinsicWidth) { 389 width = (int) (height * ratio); 390 } 391 } 392 final int left = (size - width) / 2; 393 final int top = (size - height) / 2; 394 icon.setBounds(left, top, left + width, top + height); 395 mCanvas.save(); 396 mCanvas.scale(scale, scale, size / 2, size / 2); 397 icon.draw(mCanvas); 398 mCanvas.restore(); 399 400 } 401 icon.setBounds(mOldBounds); 402 mCanvas.setBitmap(null); 403 return bitmap; 404 } 405 406 @Override close()407 public void close() { 408 clear(); 409 } 410 makeDefaultIcon(UserHandle user)411 public BitmapInfo makeDefaultIcon(UserHandle user) { 412 return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi), 413 user, Build.VERSION.SDK_INT); 414 } 415 getFullResDefaultActivityIcon(int iconDpi)416 public static Drawable getFullResDefaultActivityIcon(int iconDpi) { 417 return Resources.getSystem().getDrawableForDensity( 418 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O 419 ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon, 420 iconDpi); 421 } 422 423 /** 424 * Badges the provided source with the badge info 425 */ badgeBitmap(Bitmap source, BitmapInfo badgeInfo)426 public BitmapInfo badgeBitmap(Bitmap source, BitmapInfo badgeInfo) { 427 Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> { 428 getShadowGenerator().recreateIcon(source, c); 429 badgeWithDrawable(c, new FixedSizeBitmapDrawable(badgeInfo.icon)); 430 }); 431 return BitmapInfo.of(icon, badgeInfo.color); 432 } 433 extractColor(Bitmap bitmap)434 private int extractColor(Bitmap bitmap) { 435 return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap); 436 } 437 438 /** 439 * Returns the correct badge size given an icon size 440 */ getBadgeSizeForIconSize(int iconSize)441 public static int getBadgeSizeForIconSize(int iconSize) { 442 return (int) (ICON_BADGE_SCALE * iconSize); 443 } 444 445 /** 446 * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. 447 * This allows the badging to be done based on the action bitmap size rather than 448 * the scaled bitmap size. 449 */ 450 private static class FixedSizeBitmapDrawable extends BitmapDrawable { 451 FixedSizeBitmapDrawable(Bitmap bitmap)452 public FixedSizeBitmapDrawable(Bitmap bitmap) { 453 super(null, bitmap); 454 } 455 456 @Override getIntrinsicHeight()457 public int getIntrinsicHeight() { 458 return getBitmap().getWidth(); 459 } 460 461 @Override getIntrinsicWidth()462 public int getIntrinsicWidth() { 463 return getBitmap().getWidth(); 464 } 465 } 466 } 467