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