blob: 704df6f9bb24db0697b779532e2d7b37ab834a92 [file] [log] [blame]
package com.android.launcher3.icons;
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
import static com.android.launcher3.icons.BitmapInfo.FLAG_CLONE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT;
import static com.android.launcher3.icons.BitmapInfo.FLAG_WORK;
import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.os.UserHandle;
import android.util.SparseBooleanArray;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.BitmapInfo.Extender;
import com.android.launcher3.util.FlagOp;
import java.lang.annotation.Retention;
import java.util.Objects;
/**
* This class will be moved to androidx library. There shouldn't be any dependency outside
* this package.
*/
public class BaseIconFactory implements AutoCloseable {
private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
public static final int MODE_DEFAULT = 0;
public static final int MODE_ALPHA = 1;
public static final int MODE_WITH_SHADOW = 2;
public static final int MODE_HARDWARE = 3;
public static final int MODE_HARDWARE_WITH_SHADOW = 4;
@Retention(SOURCE)
@IntDef({MODE_DEFAULT, MODE_ALPHA, MODE_WITH_SHADOW, MODE_HARDWARE_WITH_SHADOW, MODE_HARDWARE})
@interface BitmapGenerationMode {}
private static final float ICON_BADGE_SCALE = 0.444f;
@NonNull
private final Rect mOldBounds = new Rect();
@NonNull
private final SparseBooleanArray mIsUserBadged = new SparseBooleanArray();
@NonNull
protected final Context mContext;
@NonNull
private final Canvas mCanvas;
@NonNull
private final PackageManager mPm;
@NonNull
private final ColorExtractor mColorExtractor;
protected final int mFillResIconDpi;
protected final int mIconBitmapSize;
protected boolean mMonoIconEnabled;
@Nullable
private IconNormalizer mNormalizer;
@Nullable
private ShadowGenerator mShadowGenerator;
private final boolean mShapeDetection;
// Shadow bitmap used as background for theme icons
private Bitmap mWhiteShadowLayer;
private Drawable mWrapperIcon;
private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245);
protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
boolean shapeDetection) {
mContext = context.getApplicationContext();
mShapeDetection = shapeDetection;
mFillResIconDpi = fillResIconDpi;
mIconBitmapSize = iconBitmapSize;
mPm = mContext.getPackageManager();
mColorExtractor = new ColorExtractor();
mCanvas = new Canvas();
mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
clear();
}
public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
this(context, fillResIconDpi, iconBitmapSize, false);
}
protected void clear() {
mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
}
@NonNull
public ShadowGenerator getShadowGenerator() {
if (mShadowGenerator == null) {
mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
}
return mShadowGenerator;
}
@NonNull
public IconNormalizer getNormalizer() {
if (mNormalizer == null) {
mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
}
return mNormalizer;
}
@SuppressWarnings("deprecation")
public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
try {
Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
if (resources != null) {
final int id = resources.getIdentifier(iconRes.resourceName, null, null);
// do not stamp old legacy shortcuts as the app may have already forgotten about it
return createBadgedIconBitmap(resources.getDrawableForDensity(id, mFillResIconDpi));
}
} catch (Exception e) {
// Icon not found.
}
return null;
}
/**
* Create a placeholder icon using the passed in text.
*
* @param placeholder used for foreground element in the icon bitmap
* @param color used for the foreground text color
* @return
*/
public BitmapInfo createIconBitmap(String placeholder, int color) {
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
new CenterTextDrawable(placeholder, color));
Bitmap icon = createIconBitmap(drawable, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
return BitmapInfo.of(icon, color);
}
public BitmapInfo createIconBitmap(Bitmap icon) {
if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
}
return BitmapInfo.of(icon, mColorExtractor.findDominantColorByHue(icon));
}
/**
* Creates an icon from the bitmap cropped to the current device icon shape
*/
@NonNull
public BitmapInfo createShapedIconBitmap(Bitmap icon, IconOptions options) {
Drawable d = new FixedSizeBitmapDrawable(icon);
float inset = getExtraInsetFraction();
inset = inset / (1 + 2 * inset);
d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK),
new InsetDrawable(d, inset, inset, inset, inset));
return createBadgedIconBitmap(d, options);
}
@NonNull
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon) {
return createBadgedIconBitmap(icon, null);
}
/**
* Creates bitmap using the source drawable and various parameters.
* The bitmap is visually normalized with other icons and has enough spacing to add shadow.
*
* @param icon source of the icon
* @return a bitmap suitable for disaplaying as an icon at various system UIs.
*/
@TargetApi(Build.VERSION_CODES.TIRAMISU)
@NonNull
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon,
@Nullable IconOptions options) {
boolean shrinkNonAdaptiveIcons = options == null || options.mShrinkNonAdaptiveIcons;
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
Bitmap bitmap = createIconBitmap(icon, scale[0],
options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
int color = (options != null && options.mExtractedColor != null)
? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap);
BitmapInfo info = BitmapInfo.of(bitmap, color);
if (icon instanceof BitmapInfo.Extender) {
info = ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0]);
} else if (IconProvider.ATLEAST_T && mMonoIconEnabled) {
Drawable mono = getMonochromeDrawable(icon);
if (mono != null) {
info.setMonoIcon(createIconBitmap(mono, scale[0], MODE_ALPHA), this);
}
}
info = info.withFlags(getBitmapFlagOp(options));
return info;
}
/**
* Returns a monochromatic version of the given drawable or null, if it is not supported
* @param base the original icon
*/
@TargetApi(Build.VERSION_CODES.TIRAMISU)
protected Drawable getMonochromeDrawable(Drawable base) {
if (base instanceof AdaptiveIconDrawable) {
Drawable mono = ((AdaptiveIconDrawable) base).getMonochrome();
if (mono != null) {
return new ClippedMonoDrawable(mono);
}
}
return null;
}
@NonNull
public FlagOp getBitmapFlagOp(@Nullable IconOptions options) {
FlagOp op = FlagOp.NO_OP;
if (options != null) {
if (options.mIsInstantApp) {
op = op.addFlag(FLAG_INSTANT);
}
if (options.mUserHandle != null) {
int key = options.mUserHandle.hashCode();
boolean isBadged;
int index;
if ((index = mIsUserBadged.indexOfKey(key)) >= 0) {
isBadged = mIsUserBadged.valueAt(index);
} else {
// Check packageManager if the provided user needs a badge
NoopDrawable d = new NoopDrawable();
isBadged = (d != mPm.getUserBadgedIcon(d, options.mUserHandle));
mIsUserBadged.put(key, isBadged);
}
// Set the clone profile badge flag in case it is present.
op = op.setFlag(FLAG_CLONE, isBadged && options.mIsCloneProfile);
// Set the Work profile badge for all other cases.
op = op.setFlag(FLAG_WORK, isBadged && !options.mIsCloneProfile);
}
}
return op;
}
@NonNull
public Bitmap getWhiteShadowLayer() {
if (mWhiteShadowLayer == null) {
mWhiteShadowLayer = createScaledBitmap(
new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null),
MODE_HARDWARE_WITH_SHADOW);
}
return mWhiteShadowLayer;
}
@NonNull
public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) {
RectF iconBounds = new RectF();
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, true, iconBounds, scale);
return createIconBitmap(icon,
Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)), mode);
}
/**
* Sets the background color used for wrapped adaptive icon
*/
public void setWrapperBackgroundColor(final int color) {
mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
}
@Nullable
protected Drawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon,
final boolean shrinkNonAdaptiveIcons, @Nullable final RectF outIconBounds,
@NonNull final float[] outScale) {
if (icon == null) {
return null;
}
float scale = 1f;
if (shrinkNonAdaptiveIcons && !(icon instanceof AdaptiveIconDrawable)) {
if (mWrapperIcon == null) {
mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
.mutate();
}
AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
if (!outShape[0]) {
FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
fsd.setDrawable(icon);
fsd.setScale(scale);
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
}
} else {
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
}
outScale[0] = scale;
return icon;
}
@NonNull
protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) {
return createIconBitmap(icon, scale, MODE_DEFAULT);
}
@NonNull
protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale,
@BitmapGenerationMode int bitmapGenerationMode) {
final int size = mIconBitmapSize;
final Bitmap bitmap;
switch (bitmapGenerationMode) {
case MODE_ALPHA:
bitmap = Bitmap.createBitmap(size, size, Config.ALPHA_8);
break;
case MODE_HARDWARE:
case MODE_HARDWARE_WITH_SHADOW: {
return BitmapRenderer.createHardwareBitmap(size, size, canvas ->
drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null));
}
case MODE_WITH_SHADOW:
default:
bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888);
break;
}
if (icon == null) {
return bitmap;
}
mCanvas.setBitmap(bitmap);
drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap);
mCanvas.setBitmap(null);
return bitmap;
}
private void drawIconBitmap(@NonNull Canvas canvas, @Nullable final Drawable icon,
final float scale, @BitmapGenerationMode int bitmapGenerationMode,
@Nullable Bitmap targetBitmap) {
final int size = mIconBitmapSize;
mOldBounds.set(icon.getBounds());
if (icon instanceof AdaptiveIconDrawable) {
int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
Math.round(size * (1 - scale) / 2));
// b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds
icon.setBounds(0, 0, size - offset - offset, size - offset - offset);
int count = canvas.save();
canvas.translate(offset, offset);
if (bitmapGenerationMode == MODE_WITH_SHADOW
|| bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) {
getShadowGenerator().addPathShadow(
((AdaptiveIconDrawable) icon).getIconMask(), canvas);
}
if (icon instanceof BitmapInfo.Extender) {
((Extender) icon).drawForPersistence(canvas);
} else {
icon.draw(canvas);
}
canvas.restoreToCount(count);
} else {
if (icon instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
Bitmap b = bitmapDrawable.getBitmap();
if (b != null && b.getDensity() == Bitmap.DENSITY_NONE) {
bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
}
}
int width = size;
int height = size;
int intrinsicWidth = icon.getIntrinsicWidth();
int intrinsicHeight = icon.getIntrinsicHeight();
if (intrinsicWidth > 0 && intrinsicHeight > 0) {
// Scale the icon proportionally to the icon dimensions
final float ratio = (float) intrinsicWidth / intrinsicHeight;
if (intrinsicWidth > intrinsicHeight) {
height = (int) (width / ratio);
} else if (intrinsicHeight > intrinsicWidth) {
width = (int) (height * ratio);
}
}
final int left = (size - width) / 2;
final int top = (size - height) / 2;
icon.setBounds(left, top, left + width, top + height);
canvas.save();
canvas.scale(scale, scale, size / 2, size / 2);
icon.draw(canvas);
canvas.restore();
if (bitmapGenerationMode == MODE_WITH_SHADOW && targetBitmap != null) {
// Shadow extraction only works in software mode
getShadowGenerator().drawShadow(targetBitmap, canvas);
// Draw the icon again on top:
canvas.save();
canvas.scale(scale, scale, size / 2, size / 2);
icon.draw(canvas);
canvas.restore();
}
}
icon.setBounds(mOldBounds);
}
@Override
public void close() {
clear();
}
@NonNull
public BitmapInfo makeDefaultIcon() {
return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi));
}
@NonNull
public static Drawable getFullResDefaultActivityIcon(final int iconDpi) {
return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity(
android.R.drawable.sym_def_app_icon, iconDpi));
}
/**
* Returns the correct badge size given an icon size
*/
public static int getBadgeSizeForIconSize(final int iconSize) {
return (int) (ICON_BADGE_SCALE * iconSize);
}
public static class IconOptions {
boolean mShrinkNonAdaptiveIcons = true;
boolean mIsInstantApp;
boolean mIsCloneProfile;
@BitmapGenerationMode
int mGenerationMode = MODE_WITH_SHADOW;
@Nullable UserHandle mUserHandle;
@ColorInt
@Nullable Integer mExtractedColor;
/**
* Set to false if non-adaptive icons should not be treated
*/
@NonNull
public IconOptions setShrinkNonAdaptiveIcons(final boolean shrink) {
mShrinkNonAdaptiveIcons = shrink;
return this;
}
/**
* User for this icon, in case of badging
*/
@NonNull
public IconOptions setUser(@Nullable final UserHandle user) {
mUserHandle = user;
return this;
}
/**
* If this icon represents an instant app
*/
@NonNull
public IconOptions setInstantApp(final boolean instantApp) {
mIsInstantApp = instantApp;
return this;
}
/**
* Disables auto color extraction and overrides the color to the provided value
*/
@NonNull
public IconOptions setExtractedColor(@ColorInt int color) {
mExtractedColor = color;
return this;
}
/**
* Sets the bitmap generation mode to use for the bitmap info. Note that some generation
* modes do not support color extraction, so consider setting a extracted color manually
* in those cases.
*/
public IconOptions setBitmapGenerationMode(@BitmapGenerationMode int generationMode) {
mGenerationMode = generationMode;
return this;
}
/**
* Used to determine the badge type for this icon.
*/
@NonNull
public IconOptions setIsCloneProfile(boolean isCloneProfile) {
mIsCloneProfile = isCloneProfile;
return this;
}
}
/**
* An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
* This allows the badging to be done based on the action bitmap size rather than
* the scaled bitmap size.
*/
private static class FixedSizeBitmapDrawable extends BitmapDrawable {
public FixedSizeBitmapDrawable(@Nullable final Bitmap bitmap) {
super(null, bitmap);
}
@Override
public int getIntrinsicHeight() {
return getBitmap().getWidth();
}
@Override
public int getIntrinsicWidth() {
return getBitmap().getWidth();
}
}
private static class NoopDrawable extends ColorDrawable {
@Override
public int getIntrinsicHeight() {
return 1;
}
@Override
public int getIntrinsicWidth() {
return 1;
}
}
protected static class ClippedMonoDrawable extends InsetDrawable {
@NonNull
private final AdaptiveIconDrawable mCrop;
public ClippedMonoDrawable(@Nullable final Drawable base) {
super(base, -getExtraInsetFraction());
mCrop = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
}
@Override
public void draw(Canvas canvas) {
mCrop.setBounds(getBounds());
int saveCount = canvas.save();
canvas.clipPath(mCrop.getIconMask());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
private static class CenterTextDrawable extends ColorDrawable {
@NonNull
private final Rect mTextBounds = new Rect();
@NonNull
private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
@NonNull
private final String mText;
CenterTextDrawable(@NonNull final String text, final int color) {
mText = text;
mTextPaint.setColor(color);
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
mTextPaint.setTextSize(bounds.height() / 3f);
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
canvas.drawText(mText,
bounds.exactCenterX() - mTextBounds.exactCenterX(),
bounds.exactCenterY() - mTextBounds.exactCenterY(),
mTextPaint);
}
}
}