Home | History | Annotate | Download | only in graphics
      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