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