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