1 /* 2 * Copyright (C) 2016 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.launcher3.graphics; 18 19 import static android.graphics.Paint.DITHER_FLAG; 20 import static android.graphics.Paint.FILTER_BITMAP_FLAG; 21 22 import static com.android.launcher3.graphics.ShadowGenerator.BLUR_FACTOR; 23 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.Intent.ShortcutIconResource; 28 import android.content.pm.PackageManager; 29 import android.content.res.Resources; 30 import android.graphics.Bitmap; 31 import android.graphics.Canvas; 32 import android.graphics.Color; 33 import android.graphics.PaintFlagsDrawFilter; 34 import android.graphics.Rect; 35 import android.graphics.RectF; 36 import android.graphics.drawable.AdaptiveIconDrawable; 37 import android.graphics.drawable.BitmapDrawable; 38 import android.graphics.drawable.ColorDrawable; 39 import android.graphics.drawable.Drawable; 40 import android.graphics.drawable.PaintDrawable; 41 import android.os.Build; 42 import android.os.Process; 43 import android.os.UserHandle; 44 import android.support.annotation.Nullable; 45 46 import com.android.launcher3.AppInfo; 47 import com.android.launcher3.FastBitmapDrawable; 48 import com.android.launcher3.IconCache; 49 import com.android.launcher3.InvariantDeviceProfile; 50 import com.android.launcher3.ItemInfoWithIcon; 51 import com.android.launcher3.LauncherAppState; 52 import com.android.launcher3.R; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.model.PackageItemInfo; 55 import com.android.launcher3.shortcuts.DeepShortcutManager; 56 import com.android.launcher3.shortcuts.ShortcutInfoCompat; 57 import com.android.launcher3.util.Provider; 58 import com.android.launcher3.util.Themes; 59 60 /** 61 * Helper methods for generating various launcher icons 62 */ 63 public class LauncherIcons implements AutoCloseable { 64 65 private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; 66 67 public static final Object sPoolSync = new Object(); 68 private static LauncherIcons sPool; 69 70 /** 71 * Return a new Message instance from the global pool. Allows us to 72 * avoid allocating new objects in many cases. 73 */ 74 public static LauncherIcons obtain(Context context) { 75 synchronized (sPoolSync) { 76 if (sPool != null) { 77 LauncherIcons m = sPool; 78 sPool = m.next; 79 m.next = null; 80 return m; 81 } 82 } 83 return new LauncherIcons(context); 84 } 85 86 /** 87 * Recycles a LauncherIcons that may be in-use. 88 */ 89 public void recycle() { 90 synchronized (sPoolSync) { 91 // Clear any temporary state variables 92 mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; 93 94 next = sPool; 95 sPool = this; 96 } 97 } 98 99 @Override 100 public void close() { 101 recycle(); 102 } 103 104 private final Rect mOldBounds = new Rect(); 105 private final Context mContext; 106 private final Canvas mCanvas; 107 private final PackageManager mPm; 108 109 private final int mFillResIconDpi; 110 private final int mIconBitmapSize; 111 112 private IconNormalizer mNormalizer; 113 private ShadowGenerator mShadowGenerator; 114 115 private Drawable mWrapperIcon; 116 private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; 117 118 // sometimes we store linked lists of these things 119 private LauncherIcons next; 120 121 private LauncherIcons(Context context) { 122 mContext = context.getApplicationContext(); 123 mPm = mContext.getPackageManager(); 124 125 InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext); 126 mFillResIconDpi = idp.fillResIconDpi; 127 mIconBitmapSize = idp.iconBitmapSize; 128 129 mCanvas = new Canvas(); 130 mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); 131 } 132 133 public ShadowGenerator getShadowGenerator() { 134 if (mShadowGenerator == null) { 135 mShadowGenerator = new ShadowGenerator(mContext); 136 } 137 return mShadowGenerator; 138 } 139 140 public IconNormalizer getNormalizer() { 141 if (mNormalizer == null) { 142 mNormalizer = new IconNormalizer(mContext); 143 } 144 return mNormalizer; 145 } 146 147 /** 148 * Returns a bitmap suitable for the all apps view. If the package or the resource do not 149 * exist, it returns null. 150 */ 151 public BitmapInfo createIconBitmap(ShortcutIconResource iconRes) { 152 try { 153 Resources resources = mPm.getResourcesForApplication(iconRes.packageName); 154 if (resources != null) { 155 final int id = resources.getIdentifier(iconRes.resourceName, null, null); 156 // do not stamp old legacy shortcuts as the app may have already forgotten about it 157 return createBadgedIconBitmap( 158 resources.getDrawableForDensity(id, mFillResIconDpi), 159 Process.myUserHandle() /* only available on primary user */, 160 0 /* do not apply legacy treatment */); 161 } 162 } catch (Exception e) { 163 // Icon not found. 164 } 165 return null; 166 } 167 168 /** 169 * Returns a bitmap which is of the appropriate size to be displayed as an icon 170 */ 171 public BitmapInfo createIconBitmap(Bitmap icon) { 172 if (mIconBitmapSize == icon.getWidth() && mIconBitmapSize == icon.getHeight()) { 173 return BitmapInfo.fromBitmap(icon); 174 } 175 return BitmapInfo.fromBitmap( 176 createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f)); 177 } 178 179 /** 180 * Returns a bitmap suitable for displaying as an icon at various launcher UIs like all apps 181 * view or workspace. The icon is badged for {@param user}. 182 * The bitmap is also visually normalized with other icons. 183 */ 184 public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk) { 185 return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false); 186 } 187 188 /** 189 * Returns a bitmap suitable for displaying as an icon at various launcher UIs like all apps 190 * view or workspace. The icon is badged for {@param user}. 191 * The bitmap is also visually normalized with other icons. 192 */ 193 public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, 194 boolean isInstantApp) { 195 float[] scale = new float[1]; 196 icon = normalizeAndWrapToAdaptiveIcon(icon, iconAppTargetSdk, null, scale); 197 Bitmap bitmap = createIconBitmap(icon, scale[0]); 198 if (Utilities.ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { 199 mCanvas.setBitmap(bitmap); 200 getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); 201 mCanvas.setBitmap(null); 202 } 203 204 final Bitmap result; 205 if (user != null && !Process.myUserHandle().equals(user)) { 206 BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); 207 Drawable badged = mPm.getUserBadgedIcon(drawable, user); 208 if (badged instanceof BitmapDrawable) { 209 result = ((BitmapDrawable) badged).getBitmap(); 210 } else { 211 result = createIconBitmap(badged, 1f); 212 } 213 } else if (isInstantApp) { 214 badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge)); 215 result = bitmap; 216 } else { 217 result = bitmap; 218 } 219 return BitmapInfo.fromBitmap(result); 220 } 221 222 /** 223 * Creates a normalized bitmap suitable for the all apps view. The bitmap is also visually 224 * normalized with other icons and has enough spacing to add shadow. 225 */ 226 public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) { 227 RectF iconBounds = new RectF(); 228 float[] scale = new float[1]; 229 icon = normalizeAndWrapToAdaptiveIcon(icon, iconAppTargetSdk, iconBounds, scale); 230 return createIconBitmap(icon, 231 Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds))); 232 } 233 234 /** 235 * Sets the background color used for wrapped adaptive icon 236 */ 237 public void setWrapperBackgroundColor(int color) { 238 mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; 239 } 240 241 private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, int iconAppTargetSdk, 242 RectF outIconBounds, float[] outScale) { 243 float scale = 1f; 244 if (Utilities.ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O) { 245 boolean[] outShape = new boolean[1]; 246 if (mWrapperIcon == null) { 247 mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) 248 .mutate(); 249 } 250 AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; 251 dr.setBounds(0, 0, 1, 1); 252 scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); 253 if (Utilities.ATLEAST_OREO && !outShape[0] && !(icon instanceof AdaptiveIconDrawable)) { 254 FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); 255 fsd.setDrawable(icon); 256 fsd.setScale(scale); 257 icon = dr; 258 scale = getNormalizer().getScale(icon, outIconBounds, null, null); 259 260 ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); 261 } 262 } else { 263 scale = getNormalizer().getScale(icon, outIconBounds, null, null); 264 } 265 266 outScale[0] = scale; 267 return icon; 268 } 269 270 /** 271 * Adds the {@param badge} on top of {@param target} using the badge dimensions. 272 */ 273 public void badgeWithDrawable(Bitmap target, Drawable badge) { 274 mCanvas.setBitmap(target); 275 badgeWithDrawable(mCanvas, badge); 276 mCanvas.setBitmap(null); 277 } 278 279 /** 280 * Adds the {@param badge} on top of {@param target} using the badge dimensions. 281 */ 282 private void badgeWithDrawable(Canvas target, Drawable badge) { 283 int badgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size); 284 badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, 285 mIconBitmapSize, mIconBitmapSize); 286 badge.draw(target); 287 } 288 289 /** 290 * @param scale the scale to apply before drawing {@param icon} on the canvas 291 */ 292 private Bitmap createIconBitmap(Drawable icon, float scale) { 293 int width = mIconBitmapSize; 294 int height = mIconBitmapSize; 295 296 if (icon instanceof PaintDrawable) { 297 PaintDrawable painter = (PaintDrawable) icon; 298 painter.setIntrinsicWidth(width); 299 painter.setIntrinsicHeight(height); 300 } else if (icon instanceof BitmapDrawable) { 301 // Ensure the bitmap has a density. 302 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; 303 Bitmap bitmap = bitmapDrawable.getBitmap(); 304 if (bitmap != null && bitmap.getDensity() == Bitmap.DENSITY_NONE) { 305 bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); 306 } 307 } 308 309 int sourceWidth = icon.getIntrinsicWidth(); 310 int sourceHeight = icon.getIntrinsicHeight(); 311 if (sourceWidth > 0 && sourceHeight > 0) { 312 // Scale the icon proportionally to the icon dimensions 313 final float ratio = (float) sourceWidth / sourceHeight; 314 if (sourceWidth > sourceHeight) { 315 height = (int) (width / ratio); 316 } else if (sourceHeight > sourceWidth) { 317 width = (int) (height * ratio); 318 } 319 } 320 // no intrinsic size --> use default size 321 int textureWidth = mIconBitmapSize; 322 int textureHeight = mIconBitmapSize; 323 324 Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, 325 Bitmap.Config.ARGB_8888); 326 mCanvas.setBitmap(bitmap); 327 328 final int left = (textureWidth-width) / 2; 329 final int top = (textureHeight-height) / 2; 330 331 mOldBounds.set(icon.getBounds()); 332 if (Utilities.ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { 333 int offset = Math.max((int) Math.ceil(BLUR_FACTOR * textureWidth), Math.max(left, top)); 334 int size = Math.max(width, height); 335 icon.setBounds(offset, offset, offset + size, offset + size); 336 } else { 337 icon.setBounds(left, top, left+width, top+height); 338 } 339 mCanvas.save(); 340 mCanvas.scale(scale, scale, textureWidth / 2, textureHeight / 2); 341 icon.draw(mCanvas); 342 mCanvas.restore(); 343 icon.setBounds(mOldBounds); 344 mCanvas.setBitmap(null); 345 346 return bitmap; 347 } 348 349 public BitmapInfo createShortcutIcon(ShortcutInfoCompat shortcutInfo) { 350 return createShortcutIcon(shortcutInfo, true /* badged */); 351 } 352 353 public BitmapInfo createShortcutIcon(ShortcutInfoCompat shortcutInfo, boolean badged) { 354 return createShortcutIcon(shortcutInfo, badged, null); 355 } 356 357 public BitmapInfo createShortcutIcon(ShortcutInfoCompat shortcutInfo, 358 boolean badged, @Nullable Provider<Bitmap> fallbackIconProvider) { 359 Drawable unbadgedDrawable = DeepShortcutManager.getInstance(mContext) 360 .getShortcutIconDrawable(shortcutInfo, mFillResIconDpi); 361 IconCache cache = LauncherAppState.getInstance(mContext).getIconCache(); 362 363 final Bitmap unbadgedBitmap; 364 if (unbadgedDrawable != null) { 365 unbadgedBitmap = createScaledBitmapWithoutShadow(unbadgedDrawable, 0); 366 } else { 367 if (fallbackIconProvider != null) { 368 // Fallback icons are already badged and with appropriate shadow 369 Bitmap fullIcon = fallbackIconProvider.get(); 370 if (fullIcon != null) { 371 return createIconBitmap(fullIcon); 372 } 373 } 374 unbadgedBitmap = cache.getDefaultIcon(Process.myUserHandle()).icon; 375 } 376 377 BitmapInfo result = new BitmapInfo(); 378 if (!badged) { 379 result.color = Themes.getColorAccent(mContext); 380 result.icon = unbadgedBitmap; 381 return result; 382 } 383 384 final Bitmap unbadgedfinal = unbadgedBitmap; 385 final ItemInfoWithIcon badge = getShortcutInfoBadge(shortcutInfo, cache); 386 387 result.color = badge.iconColor; 388 result.icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> { 389 getShadowGenerator().recreateIcon(unbadgedfinal, c); 390 badgeWithDrawable(c, new FastBitmapDrawable(badge)); 391 }); 392 return result; 393 } 394 395 public ItemInfoWithIcon getShortcutInfoBadge(ShortcutInfoCompat shortcutInfo, IconCache cache) { 396 ComponentName cn = shortcutInfo.getActivity(); 397 String badgePkg = shortcutInfo.getBadgePackage(mContext); 398 boolean hasBadgePkgSet = !badgePkg.equals(shortcutInfo.getPackage()); 399 if (cn != null && !hasBadgePkgSet) { 400 // Get the app info for the source activity. 401 AppInfo appInfo = new AppInfo(); 402 appInfo.user = shortcutInfo.getUserHandle(); 403 appInfo.componentName = cn; 404 appInfo.intent = new Intent(Intent.ACTION_MAIN) 405 .addCategory(Intent.CATEGORY_LAUNCHER) 406 .setComponent(cn); 407 cache.getTitleAndIcon(appInfo, false); 408 return appInfo; 409 } else { 410 PackageItemInfo pkgInfo = new PackageItemInfo(badgePkg); 411 cache.getTitleAndIconForApp(pkgInfo, false); 412 return pkgInfo; 413 } 414 } 415 416 /** 417 * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. 418 * This allows the badging to be done based on the action bitmap size rather than 419 * the scaled bitmap size. 420 */ 421 private static class FixedSizeBitmapDrawable extends BitmapDrawable { 422 423 public FixedSizeBitmapDrawable(Bitmap bitmap) { 424 super(null, bitmap); 425 } 426 427 @Override 428 public int getIntrinsicHeight() { 429 return getBitmap().getWidth(); 430 } 431 432 @Override 433 public int getIntrinsicWidth() { 434 return getBitmap().getWidth(); 435 } 436 } 437 } 438