fix: 首次提交
This commit is contained in:
@@ -0,0 +1,679 @@
|
||||
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_PRIVATE;
|
||||
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.DrawableWrapper;
|
||||
import android.graphics.drawable.InsetDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.UserHandle;
|
||||
import android.util.SparseArray;
|
||||
|
||||
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 com.android.launcher3.util.UserIconInfo;
|
||||
|
||||
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;
|
||||
private static final float LEGACY_ICON_SCALE = .7f * (1f / (1 + 2 * getExtraInsetFraction()));
|
||||
|
||||
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 SparseArray<UserIconInfo> mCachedUserInfo = new SparseArray<>();
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
UserIconInfo info = options.mUserIconInfo;
|
||||
if (info == null && options.mUserHandle != null) {
|
||||
info = getUserInfo(options.mUserHandle);
|
||||
}
|
||||
if (info != null) {
|
||||
op = info.applyBitmapInfoFlags(op);
|
||||
}
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected UserIconInfo getUserInfo(@NonNull UserHandle user) {
|
||||
int key = user.hashCode();
|
||||
UserIconInfo info = mCachedUserInfo.get(key);
|
||||
/*
|
||||
* We do not have the ability to distinguish between different badged users here.
|
||||
* As such all badged users will have the work profile badge applied.
|
||||
*/
|
||||
if (info == null) {
|
||||
// Simple check to check if the provided user is work profile or not based on badging
|
||||
NoopDrawable d = new NoopDrawable();
|
||||
boolean isWork = (d != mPm.getUserBadgedIcon(d, user));
|
||||
info = new UserIconInfo(user, isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN);
|
||||
mCachedUserInfo.put(key, info);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
@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;
|
||||
if (shrinkNonAdaptiveIcons && !(icon instanceof AdaptiveIconDrawable)) {
|
||||
EmptyWrapper foreground = new EmptyWrapper();
|
||||
AdaptiveIconDrawable dr = new AdaptiveIconDrawable(
|
||||
new ColorDrawable(mWrapperBackgroundColor), foreground);
|
||||
dr.setBounds(0, 0, 1, 1);
|
||||
boolean[] outShape = new boolean[1];
|
||||
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
|
||||
if (!outShape[0]) {
|
||||
foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE));
|
||||
icon = dr;
|
||||
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
|
||||
}
|
||||
} else {
|
||||
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
|
||||
}
|
||||
|
||||
outScale[0] = scale;
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a drawable which draws the original drawable at a fixed scale
|
||||
*/
|
||||
private Drawable createScaledDrawable(@NonNull Drawable main, float scale) {
|
||||
float h = main.getIntrinsicHeight();
|
||||
float w = main.getIntrinsicWidth();
|
||||
float scaleX = scale;
|
||||
float scaleY = scale;
|
||||
if (h > w && w > 0) {
|
||||
scaleX *= w / h;
|
||||
} else if (w > h && h > 0) {
|
||||
scaleY *= h / w;
|
||||
}
|
||||
scaleX = (1 - scaleX) / 2;
|
||||
scaleY = (1 - scaleY) / 2;
|
||||
return new InsetDrawable(main, scaleX, scaleY, scaleX, scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the provided icon in an adaptive icon drawable
|
||||
*/
|
||||
public AdaptiveIconDrawable wrapToAdaptiveIcon(@NonNull Drawable icon) {
|
||||
if (icon instanceof AdaptiveIconDrawable aid) {
|
||||
return aid;
|
||||
} else {
|
||||
EmptyWrapper foreground = new EmptyWrapper();
|
||||
AdaptiveIconDrawable dr = new AdaptiveIconDrawable(
|
||||
new ColorDrawable(mWrapperBackgroundColor), foreground);
|
||||
dr.setBounds(0, 0, 1, 1);
|
||||
boolean[] outShape = new boolean[1];
|
||||
float scale = getNormalizer().getScale(icon, null, dr.getIconMask(), outShape);
|
||||
if (!outShape[0]) {
|
||||
foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE));
|
||||
} else {
|
||||
foreground.setDrawable(createScaledDrawable(icon, 1 - getExtraInsetFraction()));
|
||||
}
|
||||
return dr;
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
// We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the moment b/298203449
|
||||
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;
|
||||
|
||||
@BitmapGenerationMode
|
||||
int mGenerationMode = MODE_WITH_SHADOW;
|
||||
|
||||
@Nullable UserHandle mUserHandle;
|
||||
@Nullable
|
||||
UserIconInfo mUserIconInfo;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* User for this icon, in case of badging
|
||||
*/
|
||||
@NonNull
|
||||
public IconOptions setUser(@Nullable final UserIconInfo user) {
|
||||
mUserIconInfo = 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EmptyWrapper extends DrawableWrapper {
|
||||
|
||||
EmptyWrapper() {
|
||||
super(new ColorDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConstantState getConstantState() {
|
||||
Drawable d = getDrawable();
|
||||
return d == null ? null : d.getConstantState();
|
||||
}
|
||||
}
|
||||
}
|
||||
226
iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
Normal file
226
iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.launcher3.util.FlagOp;
|
||||
|
||||
public class BitmapInfo {
|
||||
|
||||
public static final int FLAG_WORK = 1 << 0;
|
||||
public static final int FLAG_INSTANT = 1 << 1;
|
||||
public static final int FLAG_CLONE = 1 << 2;
|
||||
public static final int FLAG_PRIVATE = 1 << 3;
|
||||
@IntDef(flag = true, value = {
|
||||
FLAG_WORK,
|
||||
FLAG_INSTANT,
|
||||
FLAG_CLONE,
|
||||
FLAG_PRIVATE
|
||||
})
|
||||
@interface BitmapInfoFlags {}
|
||||
|
||||
public static final int FLAG_THEMED = 1 << 0;
|
||||
public static final int FLAG_NO_BADGE = 1 << 1;
|
||||
public static final int FLAG_SKIP_USER_BADGE = 1 << 2;
|
||||
@IntDef(flag = true, value = {
|
||||
FLAG_THEMED,
|
||||
FLAG_NO_BADGE,
|
||||
FLAG_SKIP_USER_BADGE,
|
||||
})
|
||||
public @interface DrawableCreationFlags {}
|
||||
|
||||
public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
|
||||
public static final BitmapInfo LOW_RES_INFO = fromBitmap(LOW_RES_ICON);
|
||||
|
||||
public static final String TAG = "BitmapInfo";
|
||||
|
||||
public final Bitmap icon;
|
||||
public final int color;
|
||||
|
||||
@Nullable
|
||||
protected Bitmap mMono;
|
||||
protected Bitmap mWhiteShadowLayer;
|
||||
|
||||
public @BitmapInfoFlags int flags;
|
||||
private BitmapInfo badgeInfo;
|
||||
|
||||
public BitmapInfo(Bitmap icon, int color) {
|
||||
this.icon = icon;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public BitmapInfo withBadgeInfo(BitmapInfo badgeInfo) {
|
||||
BitmapInfo result = clone();
|
||||
result.badgeInfo = badgeInfo;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bitmapInfo with the flagOP applied
|
||||
*/
|
||||
public BitmapInfo withFlags(@NonNull FlagOp op) {
|
||||
if (op == FlagOp.NO_OP) {
|
||||
return this;
|
||||
}
|
||||
BitmapInfo result = clone();
|
||||
result.flags = op.apply(result.flags);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected BitmapInfo copyInternalsTo(BitmapInfo target) {
|
||||
target.mMono = mMono;
|
||||
target.mWhiteShadowLayer = mWhiteShadowLayer;
|
||||
target.flags = flags;
|
||||
target.badgeInfo = badgeInfo;
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmapInfo clone() {
|
||||
return copyInternalsTo(new BitmapInfo(icon, color));
|
||||
}
|
||||
|
||||
public void setMonoIcon(Bitmap mono, BaseIconFactory iconFactory) {
|
||||
mMono = mono;
|
||||
mWhiteShadowLayer = iconFactory.getWhiteShadowLayer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ideally icon should not be null, except in cases when generating hardware bitmap failed
|
||||
*/
|
||||
public final boolean isNullOrLowRes() {
|
||||
return icon == null || icon == LOW_RES_ICON;
|
||||
}
|
||||
|
||||
public final boolean isLowRes() {
|
||||
return LOW_RES_ICON == icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* BitmapInfo can be stored on disk or other persistent storage
|
||||
*/
|
||||
public boolean canPersist() {
|
||||
return !isNullOrLowRes();
|
||||
}
|
||||
|
||||
public Bitmap getMono() {
|
||||
return mMono;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drawable for the provided BitmapInfo
|
||||
*/
|
||||
public FastBitmapDrawable newIcon(Context context) {
|
||||
return newIcon(context, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drawable for the provided BitmapInfo
|
||||
*/
|
||||
public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
|
||||
FastBitmapDrawable drawable;
|
||||
if (isLowRes()) {
|
||||
drawable = new PlaceHolderIconDrawable(this, context);
|
||||
} else if ((creationFlags & FLAG_THEMED) != 0 && mMono != null) {
|
||||
drawable = ThemedIconDrawable.newDrawable(this, context);
|
||||
} else {
|
||||
drawable = new FastBitmapDrawable(this);
|
||||
}
|
||||
applyFlags(context, drawable, creationFlags);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
protected void applyFlags(Context context, FastBitmapDrawable drawable,
|
||||
@DrawableCreationFlags int creationFlags) {
|
||||
drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
|
||||
drawable.mCreationFlags = creationFlags;
|
||||
if ((creationFlags & FLAG_NO_BADGE) == 0) {
|
||||
Drawable badge = getBadgeDrawable(context, (creationFlags & FLAG_THEMED) != 0,
|
||||
(creationFlags & FLAG_SKIP_USER_BADGE) != 0);
|
||||
if (badge != null) {
|
||||
drawable.setBadge(badge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable getBadgeDrawable(Context context, boolean isThemed) {
|
||||
return getBadgeDrawable(context, isThemed, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a drawable representing the badge for this info
|
||||
*/
|
||||
@Nullable
|
||||
private Drawable getBadgeDrawable(Context context, boolean isThemed, boolean skipUserBadge) {
|
||||
if (badgeInfo != null) {
|
||||
int creationFlag = isThemed ? FLAG_THEMED : 0;
|
||||
if (skipUserBadge) {
|
||||
creationFlag |= FLAG_SKIP_USER_BADGE;
|
||||
}
|
||||
return badgeInfo.newIcon(context, creationFlag);
|
||||
}
|
||||
if (skipUserBadge) {
|
||||
return null;
|
||||
} else if ((flags & FLAG_INSTANT) != 0) {
|
||||
return new UserBadgeDrawable(context, R.drawable.ic_instant_app_badge,
|
||||
R.color.badge_tint_instant, isThemed);
|
||||
} else if ((flags & FLAG_WORK) != 0) {
|
||||
return new UserBadgeDrawable(context, R.drawable.ic_work_app_badge,
|
||||
R.color.badge_tint_work, isThemed);
|
||||
} else if ((flags & FLAG_CLONE) != 0) {
|
||||
return new UserBadgeDrawable(context, R.drawable.ic_clone_app_badge,
|
||||
R.color.badge_tint_clone, isThemed);
|
||||
} else if ((flags & FLAG_PRIVATE) != 0) {
|
||||
return new UserBadgeDrawable(context, R.drawable.ic_private_profile_app_badge,
|
||||
R.color.badge_tint_private, isThemed);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
|
||||
return of(bitmap, 0);
|
||||
}
|
||||
|
||||
public static BitmapInfo of(@NonNull Bitmap bitmap, int color) {
|
||||
return new BitmapInfo(bitmap, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to be implemented by drawables to provide a custom BitmapInfo
|
||||
*/
|
||||
public interface Extender {
|
||||
|
||||
/**
|
||||
* Called for creating a custom BitmapInfo
|
||||
*/
|
||||
BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
|
||||
BaseIconFactory iconFactory, float normalizationScale);
|
||||
|
||||
/**
|
||||
* Called to draw the UI independent of any runtime configurations like time or theme
|
||||
*/
|
||||
void drawForPersistence(Canvas canvas);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Picture;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
|
||||
/**
|
||||
* Interface representing a bitmap draw operation.
|
||||
*/
|
||||
public interface BitmapRenderer {
|
||||
|
||||
boolean USE_HARDWARE_BITMAP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
|
||||
|
||||
static Bitmap createSoftwareBitmap(int width, int height, BitmapRenderer renderer) {
|
||||
GraphicsUtils.noteNewBitmapCreated();
|
||||
Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
renderer.draw(new Canvas(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.P)
|
||||
static Bitmap createHardwareBitmap(int width, int height, BitmapRenderer renderer) {
|
||||
if (!USE_HARDWARE_BITMAP) {
|
||||
return createSoftwareBitmap(width, height, renderer);
|
||||
}
|
||||
|
||||
GraphicsUtils.noteNewBitmapCreated();
|
||||
Picture picture = new Picture();
|
||||
renderer.draw(picture.beginRecording(width, height));
|
||||
picture.endRecording();
|
||||
return Bitmap.createBitmap(picture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bitmap from subset of the source bitmap. The new bitmap may be the
|
||||
* same object as source, or a copy may have been made.
|
||||
*/
|
||||
static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) {
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.O && source.getConfig() == Config.HARDWARE) {
|
||||
return createHardwareBitmap(width, height, c -> c.drawBitmap(source,
|
||||
new Rect(x, y, x + width, y + height), new RectF(0, 0, width, height), null));
|
||||
} else {
|
||||
GraphicsUtils.noteNewBitmapCreated();
|
||||
return Bitmap.createBitmap(source, x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void draw(Canvas out);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.LauncherApps;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.AdaptiveIconDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Factory for creating normalized bubble icons and app badges.
|
||||
*/
|
||||
public class BubbleIconFactory extends BaseIconFactory {
|
||||
|
||||
private final int mRingColor;
|
||||
private final int mRingWidth;
|
||||
|
||||
private final BaseIconFactory mBadgeFactory;
|
||||
|
||||
/**
|
||||
* Creates a bubble icon factory.
|
||||
*
|
||||
* @param context the context for the factory.
|
||||
* @param iconSize the size of the bubble icon (i.e. the large icon for the bubble).
|
||||
* @param badgeSize the size of the badge (i.e. smaller icon shown on top of the large icon).
|
||||
* @param ringColor the color of the ring optionally shown around the badge.
|
||||
* @param ringWidth the width of the ring optionally shown around the badge.
|
||||
*/
|
||||
public BubbleIconFactory(Context context, int iconSize, int badgeSize, int ringColor,
|
||||
int ringWidth) {
|
||||
super(context, context.getResources().getConfiguration().densityDpi, iconSize);
|
||||
mRingColor = ringColor;
|
||||
mRingWidth = ringWidth;
|
||||
|
||||
mBadgeFactory = new BaseIconFactory(context,
|
||||
context.getResources().getConfiguration().densityDpi,
|
||||
badgeSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the drawable that the developer has provided to display in the bubble.
|
||||
*/
|
||||
public Drawable getBubbleDrawable(@NonNull final Context context,
|
||||
@Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) {
|
||||
if (shortcutInfo != null) {
|
||||
LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
|
||||
int density = context.getResources().getConfiguration().densityDpi;
|
||||
return launcherApps.getShortcutIconDrawable(shortcutInfo, density);
|
||||
} else {
|
||||
if (ic != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
if (ic.getType() == Icon.TYPE_URI
|
||||
|| ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
|
||||
context.grantUriPermission(context.getPackageName(),
|
||||
ic.getUri(),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
return ic.loadDrawable(context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the bitmap for the provided drawable and returns the scale used for
|
||||
* drawing the actual drawable. This is used for the larger icon shown for the bubble.
|
||||
*/
|
||||
public Bitmap getBubbleBitmap(@NonNull Drawable icon, float[] outScale) {
|
||||
if (outScale == null) {
|
||||
outScale = new float[1];
|
||||
}
|
||||
icon = normalizeAndWrapToAdaptiveIcon(icon,
|
||||
true /* shrinkNonAdaptiveIcons */,
|
||||
null /* outscale */,
|
||||
outScale);
|
||||
return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This
|
||||
* will include the workprofile indicator on the badge if appropriate.
|
||||
*/
|
||||
public BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) {
|
||||
if (userBadgedAppIcon instanceof AdaptiveIconDrawable) {
|
||||
AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon;
|
||||
userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(),
|
||||
ad.getForeground());
|
||||
}
|
||||
if (isImportantConversation) {
|
||||
userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
|
||||
}
|
||||
Bitmap userBadgedBitmap = mBadgeFactory.createIconBitmap(
|
||||
userBadgedAppIcon, 1, MODE_WITH_SHADOW);
|
||||
return mBadgeFactory.createIconBitmap(userBadgedBitmap);
|
||||
}
|
||||
|
||||
private class CircularRingDrawable extends CircularAdaptiveIcon {
|
||||
final Rect mInnerBounds = new Rect();
|
||||
|
||||
final Drawable mDr;
|
||||
|
||||
CircularRingDrawable(Drawable dr) {
|
||||
super(null, null);
|
||||
mDr = dr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
int save = canvas.save();
|
||||
canvas.clipPath(getIconMask());
|
||||
canvas.drawColor(mRingColor);
|
||||
mInnerBounds.set(getBounds());
|
||||
mInnerBounds.inset(mRingWidth, mRingWidth);
|
||||
canvas.translate(mInnerBounds.left, mInnerBounds.top);
|
||||
mDr.setBounds(0, 0, mInnerBounds.width(), mInnerBounds.height());
|
||||
mDr.draw(canvas);
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CircularAdaptiveIcon extends AdaptiveIconDrawable {
|
||||
|
||||
final Path mPath = new Path();
|
||||
|
||||
CircularAdaptiveIcon(Drawable bg, Drawable fg) {
|
||||
super(bg, fg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getIconMask() {
|
||||
mPath.reset();
|
||||
Rect bounds = getBounds();
|
||||
mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
|
||||
return mPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
int save = canvas.save();
|
||||
canvas.clipPath(getIconMask());
|
||||
|
||||
Drawable d;
|
||||
if ((d = getBackground()) != null) {
|
||||
d.draw(canvas);
|
||||
}
|
||||
if ((d = getForeground()) != null) {
|
||||
d.draw(canvas);
|
||||
}
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import static com.android.launcher3.icons.IconProvider.ATLEAST_T;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BlendMode;
|
||||
import android.graphics.BlendModeColorFilter;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.AdaptiveIconDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.launcher3.icons.IconProvider.ThemeData;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
/**
|
||||
* Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
|
||||
* clock icons
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
|
||||
|
||||
public static boolean sRunningInTest = false;
|
||||
|
||||
private static final String TAG = "ClockDrawableWrapper";
|
||||
|
||||
private static final boolean DISABLE_SECONDS = true;
|
||||
private static final int NO_COLOR = -1;
|
||||
|
||||
// Time after which the clock icon should check for an update. The actual invalidate
|
||||
// will only happen in case of any change.
|
||||
public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
|
||||
|
||||
private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
|
||||
private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
|
||||
+ ".LEVEL_PER_TICK_ICON_ROUND";
|
||||
private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
|
||||
private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
|
||||
+ ".MINUTE_LAYER_INDEX";
|
||||
private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
|
||||
+ ".SECOND_LAYER_INDEX";
|
||||
private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
|
||||
+ ".DEFAULT_HOUR";
|
||||
private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
|
||||
+ ".DEFAULT_MINUTE";
|
||||
private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
|
||||
+ ".DEFAULT_SECOND";
|
||||
|
||||
/* Number of levels to jump per second for the second hand */
|
||||
private static final int LEVELS_PER_SECOND = 10;
|
||||
|
||||
public static final int INVALID_VALUE = -1;
|
||||
|
||||
private final AnimationInfo mAnimationInfo = new AnimationInfo();
|
||||
private AnimationInfo mThemeInfo = null;
|
||||
|
||||
private ClockDrawableWrapper(AdaptiveIconDrawable base) {
|
||||
super(base.getBackground(), base.getForeground());
|
||||
}
|
||||
|
||||
private void applyThemeData(ThemeData themeData) {
|
||||
if (!IconProvider.ATLEAST_T || mThemeInfo != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID);
|
||||
int count = ta.length();
|
||||
Bundle extras = new Bundle();
|
||||
for (int i = 0; i < count; i += 2) {
|
||||
TypedValue v = ta.peekValue(i + 1);
|
||||
extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT
|
||||
&& v.type <= TypedValue.TYPE_LAST_INT
|
||||
? v.data : v.resourceId);
|
||||
}
|
||||
ta.recycle();
|
||||
ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras(extras, resId -> {
|
||||
Drawable bg = new ColorDrawable(Color.WHITE);
|
||||
Drawable fg = themeData.mResources.getDrawable(resId).mutate();
|
||||
return new AdaptiveIconDrawable(bg, fg);
|
||||
});
|
||||
if (drawable != null) {
|
||||
mThemeInfo = drawable.mAnimationInfo;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error loading themed clock", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getMonochrome() {
|
||||
if (mThemeInfo == null) {
|
||||
return null;
|
||||
}
|
||||
Drawable d = mThemeInfo.baseDrawableState.newDrawable().mutate();
|
||||
if (d instanceof AdaptiveIconDrawable) {
|
||||
Drawable mono = ((AdaptiveIconDrawable) d).getForeground();
|
||||
mThemeInfo.applyTime(Calendar.getInstance(), (LayerDrawable) mono);
|
||||
return mono;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns the wrapper from the provided package, or returns null
|
||||
* if it is unable to load.
|
||||
*/
|
||||
public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi,
|
||||
@Nullable ThemeData themeData) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
ApplicationInfo appInfo = pm.getApplicationInfo(pkg,
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
|
||||
Resources res = pm.getResourcesForApplication(appInfo);
|
||||
ClockDrawableWrapper wrapper = forExtras(appInfo.metaData,
|
||||
resId -> res.getDrawableForDensity(resId, iconDpi));
|
||||
if (wrapper != null && themeData != null) {
|
||||
wrapper.applyThemeData(themeData);
|
||||
}
|
||||
return wrapper;
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Unable to load clock drawable info", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private static ClockDrawableWrapper forExtras(
|
||||
Bundle metadata, IntFunction<Drawable> drawableProvider) {
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
|
||||
if (drawableId == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Drawable drawable = drawableProvider.apply(drawableId).mutate();
|
||||
if (!(drawable instanceof AdaptiveIconDrawable)) {
|
||||
return null;
|
||||
}
|
||||
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable;
|
||||
|
||||
ClockDrawableWrapper wrapper = new ClockDrawableWrapper(aid);
|
||||
AnimationInfo info = wrapper.mAnimationInfo;
|
||||
|
||||
info.baseDrawableState = drawable.getConstantState();
|
||||
info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
|
||||
info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
|
||||
info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
|
||||
|
||||
info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
|
||||
info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
|
||||
info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
|
||||
|
||||
LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
|
||||
int layerCount = foreground.getNumberOfLayers();
|
||||
if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
|
||||
info.hourLayerIndex = INVALID_VALUE;
|
||||
}
|
||||
if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
|
||||
info.minuteLayerIndex = INVALID_VALUE;
|
||||
}
|
||||
if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
|
||||
info.secondLayerIndex = INVALID_VALUE;
|
||||
} else if (DISABLE_SECONDS) {
|
||||
foreground.setDrawable(info.secondLayerIndex, null);
|
||||
info.secondLayerIndex = INVALID_VALUE;
|
||||
}
|
||||
|
||||
if (ATLEAST_T && aid.getMonochrome() instanceof LayerDrawable) {
|
||||
wrapper.mThemeInfo = info.copyForIcon(new AdaptiveIconDrawable(
|
||||
new ColorDrawable(Color.WHITE), aid.getMonochrome().mutate()));
|
||||
}
|
||||
info.applyTime(Calendar.getInstance(), foreground);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color,
|
||||
BaseIconFactory iconFactory, float normalizationScale) {
|
||||
AdaptiveIconDrawable background = new AdaptiveIconDrawable(
|
||||
getBackground().getConstantState().newDrawable(), null);
|
||||
Bitmap flattenBG = iconFactory.createScaledBitmap(background,
|
||||
BaseIconFactory.MODE_HARDWARE_WITH_SHADOW);
|
||||
|
||||
// Only pass theme info if mono-icon is enabled
|
||||
AnimationInfo themeInfo = iconFactory.mMonoIconEnabled ? mThemeInfo : null;
|
||||
Bitmap themeBG = themeInfo == null ? null : iconFactory.getWhiteShadowLayer();
|
||||
return new ClockBitmapInfo(bitmap, color, normalizationScale,
|
||||
mAnimationInfo, flattenBG, themeInfo, themeBG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawForPersistence(Canvas canvas) {
|
||||
LayerDrawable foreground = (LayerDrawable) getForeground();
|
||||
resetLevel(foreground, mAnimationInfo.hourLayerIndex);
|
||||
resetLevel(foreground, mAnimationInfo.minuteLayerIndex);
|
||||
resetLevel(foreground, mAnimationInfo.secondLayerIndex);
|
||||
draw(canvas);
|
||||
mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
|
||||
}
|
||||
|
||||
private void resetLevel(LayerDrawable drawable, int index) {
|
||||
if (index != INVALID_VALUE) {
|
||||
drawable.getDrawable(index).setLevel(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AnimationInfo {
|
||||
|
||||
public ConstantState baseDrawableState;
|
||||
|
||||
public int hourLayerIndex;
|
||||
public int minuteLayerIndex;
|
||||
public int secondLayerIndex;
|
||||
public int defaultHour;
|
||||
public int defaultMinute;
|
||||
public int defaultSecond;
|
||||
|
||||
public AnimationInfo copyForIcon(Drawable icon) {
|
||||
AnimationInfo result = new AnimationInfo();
|
||||
result.baseDrawableState = icon.getConstantState();
|
||||
result.defaultHour = defaultHour;
|
||||
result.defaultMinute = defaultMinute;
|
||||
result.defaultSecond = defaultSecond;
|
||||
result.hourLayerIndex = hourLayerIndex;
|
||||
result.minuteLayerIndex = minuteLayerIndex;
|
||||
result.secondLayerIndex = secondLayerIndex;
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
|
||||
time.setTimeInMillis(System.currentTimeMillis());
|
||||
|
||||
// We need to rotate by the difference from the default time if one is specified.
|
||||
int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
|
||||
int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
|
||||
int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
|
||||
|
||||
boolean invalidate = false;
|
||||
if (hourLayerIndex != INVALID_VALUE) {
|
||||
final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
|
||||
if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
|
||||
invalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (minuteLayerIndex != INVALID_VALUE) {
|
||||
final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
|
||||
if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
|
||||
invalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (secondLayerIndex != INVALID_VALUE) {
|
||||
final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
|
||||
if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
|
||||
invalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
return invalidate;
|
||||
}
|
||||
}
|
||||
|
||||
static class ClockBitmapInfo extends BitmapInfo {
|
||||
|
||||
public final float boundsOffset;
|
||||
|
||||
public final AnimationInfo animInfo;
|
||||
public final Bitmap mFlattenedBackground;
|
||||
|
||||
public final AnimationInfo themeData;
|
||||
public final Bitmap themeBackground;
|
||||
|
||||
ClockBitmapInfo(Bitmap icon, int color, float scale,
|
||||
AnimationInfo animInfo, Bitmap background,
|
||||
AnimationInfo themeInfo, Bitmap themeBackground) {
|
||||
super(icon, color);
|
||||
this.boundsOffset = Math.max(ShadowGenerator.BLUR_FACTOR, (1 - scale) / 2);
|
||||
this.animInfo = animInfo;
|
||||
this.mFlattenedBackground = background;
|
||||
this.themeData = themeInfo;
|
||||
this.themeBackground = themeBackground;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||
public FastBitmapDrawable newIcon(Context context,
|
||||
@DrawableCreationFlags int creationFlags) {
|
||||
AnimationInfo info;
|
||||
Bitmap bg;
|
||||
int themedFgColor;
|
||||
ColorFilter bgFilter;
|
||||
if ((creationFlags & FLAG_THEMED) != 0 && themeData != null) {
|
||||
int[] colors = ThemedIconDrawable.getColors(context);
|
||||
Drawable tintedDrawable = themeData.baseDrawableState.newDrawable().mutate();
|
||||
themedFgColor = colors[1];
|
||||
tintedDrawable.setTint(colors[1]);
|
||||
info = themeData.copyForIcon(tintedDrawable);
|
||||
bg = themeBackground;
|
||||
bgFilter = new BlendModeColorFilter(colors[0], BlendMode.SRC_IN);
|
||||
} else {
|
||||
info = animInfo;
|
||||
themedFgColor = NO_COLOR;
|
||||
bg = mFlattenedBackground;
|
||||
bgFilter = null;
|
||||
}
|
||||
if (info == null) {
|
||||
return super.newIcon(context, creationFlags);
|
||||
}
|
||||
ClockIconDrawable.ClockConstantState cs = new ClockIconDrawable.ClockConstantState(
|
||||
icon, color, themedFgColor, boundsOffset, info, bg, bgFilter);
|
||||
FastBitmapDrawable d = cs.newDrawable();
|
||||
applyFlags(context, d, creationFlags);
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPersist() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmapInfo clone() {
|
||||
return copyInternalsTo(new ClockBitmapInfo(icon, color, 1 - 2 * boundsOffset, animInfo,
|
||||
mFlattenedBackground, themeData, themeBackground));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
|
||||
|
||||
private final Calendar mTime = Calendar.getInstance();
|
||||
|
||||
private final float mBoundsOffset;
|
||||
private final AnimationInfo mAnimInfo;
|
||||
|
||||
private final Bitmap mBG;
|
||||
private final Paint mBgPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
||||
private final ColorFilter mBgFilter;
|
||||
private final int mThemedFgColor;
|
||||
|
||||
private final AdaptiveIconDrawable mFullDrawable;
|
||||
private final LayerDrawable mFG;
|
||||
private final float mCanvasScale;
|
||||
|
||||
ClockIconDrawable(ClockConstantState cs) {
|
||||
super(cs.mBitmap, cs.mIconColor);
|
||||
mBoundsOffset = cs.mBoundsOffset;
|
||||
mAnimInfo = cs.mAnimInfo;
|
||||
|
||||
mBG = cs.mBG;
|
||||
mBgFilter = cs.mBgFilter;
|
||||
mBgPaint.setColorFilter(cs.mBgFilter);
|
||||
mThemedFgColor = cs.mThemedFgColor;
|
||||
|
||||
mFullDrawable =
|
||||
(AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable().mutate();
|
||||
mFG = (LayerDrawable) mFullDrawable.getForeground();
|
||||
|
||||
// Time needs to be applied here since drawInternal is NOT guaranteed to be called
|
||||
// before this foreground drawable is shown on the screen.
|
||||
mAnimInfo.applyTime(mTime, mFG);
|
||||
mCanvasScale = 1 - 2 * mBoundsOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
super.setAlpha(alpha);
|
||||
mBgPaint.setAlpha(alpha);
|
||||
mFG.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
|
||||
// b/211896569 AdaptiveIcon does not work properly when bounds
|
||||
// are not aligned to top/left corner
|
||||
mFullDrawable.setBounds(0, 0, bounds.width(), bounds.height());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawInternal(Canvas canvas, Rect bounds) {
|
||||
if (mAnimInfo == null) {
|
||||
super.drawInternal(canvas, bounds);
|
||||
return;
|
||||
}
|
||||
canvas.drawBitmap(mBG, null, bounds, mBgPaint);
|
||||
|
||||
// prepare and draw the foreground
|
||||
mAnimInfo.applyTime(mTime, mFG);
|
||||
int saveCount = canvas.save();
|
||||
canvas.translate(bounds.left, bounds.top);
|
||||
canvas.scale(mCanvasScale, mCanvasScale, bounds.width() / 2, bounds.height() / 2);
|
||||
canvas.clipPath(mFullDrawable.getIconMask());
|
||||
mFG.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
|
||||
reschedule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isThemed() {
|
||||
return mBgPaint.getColorFilter() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateFilter() {
|
||||
super.updateFilter();
|
||||
int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
|
||||
setAlpha(alpha);
|
||||
mBgPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mBgFilter);
|
||||
mFG.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconColor() {
|
||||
return isThemed() ? mThemedFgColor : super.getIconColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (sRunningInTest) {
|
||||
Log.d("b/319168409", "running this: " + this);
|
||||
}
|
||||
if (mAnimInfo.applyTime(mTime, mFG)) {
|
||||
invalidateSelf();
|
||||
} else {
|
||||
reschedule();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
boolean result = super.setVisible(visible, restart);
|
||||
if (visible) {
|
||||
reschedule();
|
||||
} else {
|
||||
if (sRunningInTest) {
|
||||
Log.d("b/319168409", "unScheduling self invisible this: " + this);
|
||||
}
|
||||
unscheduleSelf(this);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void reschedule() {
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
if (sRunningInTest) {
|
||||
Log.d("b/319168409", "unScheduling self this: " + this);
|
||||
}
|
||||
unscheduleSelf(this);
|
||||
final long upTime = SystemClock.uptimeMillis();
|
||||
final long step = TICK_MS; /* tick every 200 ms */
|
||||
if (sRunningInTest) {
|
||||
Log.d("b/319168409", "scheduling self this: " + this, new Throwable());
|
||||
}
|
||||
scheduleSelf(this, upTime - ((upTime % step)) + step);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FastBitmapConstantState newConstantState() {
|
||||
return new ClockConstantState(mBitmap, mIconColor, mThemedFgColor, mBoundsOffset,
|
||||
mAnimInfo, mBG, mBgPaint.getColorFilter());
|
||||
}
|
||||
|
||||
private static class ClockConstantState extends FastBitmapConstantState {
|
||||
|
||||
private final float mBoundsOffset;
|
||||
private final AnimationInfo mAnimInfo;
|
||||
private final Bitmap mBG;
|
||||
private final ColorFilter mBgFilter;
|
||||
private final int mThemedFgColor;
|
||||
|
||||
ClockConstantState(Bitmap bitmap, int color, int themedFgColor,
|
||||
float boundsOffset, AnimationInfo animInfo, Bitmap bg, ColorFilter bgFilter) {
|
||||
super(bitmap, color);
|
||||
mBoundsOffset = boundsOffset;
|
||||
mAnimInfo = animInfo;
|
||||
mBG = bg;
|
||||
mBgFilter = bgFilter;
|
||||
mThemedFgColor = themedFgColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FastBitmapDrawable createDrawable() {
|
||||
return new ClockIconDrawable(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Utility class for extracting colors from a bitmap.
|
||||
*/
|
||||
public class ColorExtractor {
|
||||
|
||||
private final int NUM_SAMPLES = 20;
|
||||
|
||||
@NonNull
|
||||
private final float[] mTmpHsv = new float[3];
|
||||
|
||||
@NonNull
|
||||
private final float[] mTmpHueScoreHistogram = new float[360];
|
||||
|
||||
@NonNull
|
||||
private final int[] mTmpPixels = new int[NUM_SAMPLES];
|
||||
|
||||
@NonNull
|
||||
private final SparseArray<Float> mTmpRgbScores = new SparseArray<>();
|
||||
|
||||
/**
|
||||
* This picks a dominant color, looking for high-saturation, high-value, repeated hues.
|
||||
* @param bitmap The bitmap to scan
|
||||
*/
|
||||
public int findDominantColorByHue(@NonNull final Bitmap bitmap) {
|
||||
return findDominantColorByHue(bitmap, NUM_SAMPLES);
|
||||
}
|
||||
|
||||
/**
|
||||
* This picks a dominant color, looking for high-saturation, high-value, repeated hues.
|
||||
* @param bitmap The bitmap to scan
|
||||
*/
|
||||
protected int findDominantColorByHue(@NonNull final Bitmap bitmap, final int samples) {
|
||||
final int height = bitmap.getHeight();
|
||||
final int width = bitmap.getWidth();
|
||||
int sampleStride = (int) Math.sqrt((height * width) / samples);
|
||||
if (sampleStride < 1) {
|
||||
sampleStride = 1;
|
||||
}
|
||||
|
||||
// This is an out-param, for getting the hsv values for an rgb
|
||||
float[] hsv = mTmpHsv;
|
||||
Arrays.fill(hsv, 0);
|
||||
|
||||
// First get the best hue, by creating a histogram over 360 hue buckets,
|
||||
// where each pixel contributes a score weighted by saturation, value, and alpha.
|
||||
float[] hueScoreHistogram = mTmpHueScoreHistogram;
|
||||
Arrays.fill(hueScoreHistogram, 0);
|
||||
float highScore = -1;
|
||||
int bestHue = -1;
|
||||
|
||||
int[] pixels = mTmpPixels;
|
||||
Arrays.fill(pixels, 0);
|
||||
int pixelCount = 0;
|
||||
|
||||
for (int y = 0; y < height; y += sampleStride) {
|
||||
for (int x = 0; x < width; x += sampleStride) {
|
||||
int argb = bitmap.getPixel(x, y);
|
||||
int alpha = 0xFF & (argb >> 24);
|
||||
if (alpha < 0x80) {
|
||||
// Drop mostly-transparent pixels.
|
||||
continue;
|
||||
}
|
||||
// Remove the alpha channel.
|
||||
int rgb = argb | 0xFF000000;
|
||||
Color.colorToHSV(rgb, hsv);
|
||||
// Bucket colors by the 360 integer hues.
|
||||
int hue = (int) hsv[0];
|
||||
if (hue < 0 || hue >= hueScoreHistogram.length) {
|
||||
// Defensively avoid array bounds violations.
|
||||
continue;
|
||||
}
|
||||
if (pixelCount < samples) {
|
||||
pixels[pixelCount++] = rgb;
|
||||
}
|
||||
float score = hsv[1] * hsv[2];
|
||||
hueScoreHistogram[hue] += score;
|
||||
if (hueScoreHistogram[hue] > highScore) {
|
||||
highScore = hueScoreHistogram[hue];
|
||||
bestHue = hue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SparseArray<Float> rgbScores = mTmpRgbScores;
|
||||
rgbScores.clear();
|
||||
int bestColor = 0xff000000;
|
||||
highScore = -1;
|
||||
// Go back over the RGB colors that match the winning hue,
|
||||
// creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
|
||||
// The highest-scoring RGB color wins.
|
||||
for (int i = 0; i < pixelCount; i++) {
|
||||
int rgb = pixels[i];
|
||||
Color.colorToHSV(rgb, hsv);
|
||||
int hue = (int) hsv[0];
|
||||
if (hue == bestHue) {
|
||||
float s = hsv[1];
|
||||
float v = hsv[2];
|
||||
int bucket = (int) (s * 100) + (int) (v * 10000);
|
||||
// Score by cumulative saturation * value.
|
||||
float score = s * v;
|
||||
Float oldTotal = rgbScores.get(bucket);
|
||||
float newTotal = oldTotal == null ? score : oldTotal + score;
|
||||
rgbScores.put(bucket, newTotal);
|
||||
if (newTotal > highScore) {
|
||||
highScore = newTotal;
|
||||
// All the colors in the winning bucket are very similar. Last in wins.
|
||||
bestColor = rgb;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestColor;
|
||||
}
|
||||
}
|
||||
150
iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
Normal file
150
iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
|
||||
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PathMeasure;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.util.Log;
|
||||
import android.view.ViewDebug;
|
||||
|
||||
/**
|
||||
* Used to draw a notification dot on top of an icon.
|
||||
*/
|
||||
public class DotRenderer {
|
||||
|
||||
private static final String TAG = "DotRenderer";
|
||||
|
||||
// The dot size is defined as a percentage of the app icon size.
|
||||
private static final float SIZE_PERCENTAGE = 0.228f;
|
||||
|
||||
private final float mCircleRadius;
|
||||
private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
|
||||
|
||||
private final Bitmap mBackgroundWithShadow;
|
||||
private final float mBitmapOffset;
|
||||
|
||||
// Stores the center x and y position as a percentage (0 to 1) of the icon size
|
||||
private final float[] mRightDotPosition;
|
||||
private final float[] mLeftDotPosition;
|
||||
|
||||
private static final int MIN_DOT_SIZE = 1;
|
||||
public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
|
||||
int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
|
||||
if (size <= 0) {
|
||||
size = MIN_DOT_SIZE;
|
||||
}
|
||||
ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT);
|
||||
builder.ambientShadowAlpha = 88;
|
||||
mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);
|
||||
mCircleRadius = builder.radius;
|
||||
|
||||
mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width.
|
||||
|
||||
// Find the points on the path that are closest to the top left and right corners.
|
||||
mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1);
|
||||
mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1);
|
||||
}
|
||||
|
||||
private static float[] getPathPoint(Path path, float size, float direction) {
|
||||
float halfSize = size / 2;
|
||||
// Small delta so that we don't get a zero size triangle
|
||||
float delta = 1;
|
||||
|
||||
float x = halfSize + direction * halfSize;
|
||||
Path trianglePath = new Path();
|
||||
trianglePath.moveTo(halfSize, halfSize);
|
||||
trianglePath.lineTo(x + delta * direction, 0);
|
||||
trianglePath.lineTo(x, -delta);
|
||||
trianglePath.close();
|
||||
|
||||
trianglePath.op(path, Path.Op.INTERSECT);
|
||||
float[] pos = new float[2];
|
||||
new PathMeasure(trianglePath, false).getPosTan(0, pos, null);
|
||||
|
||||
pos[0] = pos[0] / size;
|
||||
pos[1] = pos[1] / size;
|
||||
return pos;
|
||||
}
|
||||
|
||||
public float[] getLeftDotPosition() {
|
||||
return mLeftDotPosition;
|
||||
}
|
||||
|
||||
public float[] getRightDotPosition() {
|
||||
return mRightDotPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a circle on top of the canvas according to the given params.
|
||||
*/
|
||||
public void draw(Canvas canvas, DrawParams params) {
|
||||
if (params == null) {
|
||||
Log.e(TAG, "Invalid null argument(s) passed in call to draw.");
|
||||
return;
|
||||
}
|
||||
canvas.save();
|
||||
|
||||
Rect iconBounds = params.iconBounds;
|
||||
float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition;
|
||||
float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0];
|
||||
float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1];
|
||||
|
||||
// Ensure dot fits entirely in canvas clip bounds.
|
||||
Rect canvasBounds = canvas.getClipBounds();
|
||||
float offsetX = params.leftAlign
|
||||
? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset))
|
||||
: Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset));
|
||||
float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset));
|
||||
|
||||
// We draw the dot relative to its center.
|
||||
canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY);
|
||||
canvas.scale(params.scale, params.scale);
|
||||
|
||||
mCirclePaint.setColor(Color.BLACK);
|
||||
canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint);
|
||||
mCirclePaint.setColor(params.dotColor);
|
||||
canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
public static class DrawParams {
|
||||
/** The color (possibly based on the icon) to use for the dot. */
|
||||
@ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true)
|
||||
public int dotColor;
|
||||
/** The color (possibly based on the icon) to use for a predicted app. */
|
||||
@ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true)
|
||||
public int appColor;
|
||||
/** The bounds of the icon that the dot is drawn on top of. */
|
||||
@ViewDebug.ExportedProperty(category = "notification dot")
|
||||
public Rect iconBounds = new Rect();
|
||||
/** The progress of the animation, from 0 to 1. */
|
||||
@ViewDebug.ExportedProperty(category = "notification dot")
|
||||
public float scale;
|
||||
/** Whether the dot should align to the top left of the icon rather than the top right. */
|
||||
@ViewDebug.ExportedProperty(category = "notification dot")
|
||||
public boolean leftAlign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import static com.android.launcher3.icons.BaseIconFactory.getBadgeSizeForIconSize;
|
||||
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
|
||||
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
|
||||
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.FloatProperty;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.PathInterpolator;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
|
||||
import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
|
||||
|
||||
public class FastBitmapDrawable extends Drawable implements Drawable.Callback {
|
||||
|
||||
private static final Interpolator ACCEL = new AccelerateInterpolator();
|
||||
private static final Interpolator DEACCEL = new DecelerateInterpolator();
|
||||
private static final Interpolator HOVER_EMPHASIZED_DECELERATE_INTERPOLATOR =
|
||||
new PathInterpolator(0.05f, 0.7f, 0.1f, 1.0f);
|
||||
|
||||
@VisibleForTesting protected static final float PRESSED_SCALE = 1.1f;
|
||||
@VisibleForTesting protected static final float HOVERED_SCALE = 1.1f;
|
||||
public static final int WHITE_SCRIM_ALPHA = 138;
|
||||
|
||||
private static final float DISABLED_DESATURATION = 1f;
|
||||
private static final float DISABLED_BRIGHTNESS = 0.5f;
|
||||
protected static final int FULLY_OPAQUE = 255;
|
||||
|
||||
public static final int CLICK_FEEDBACK_DURATION = 200;
|
||||
public static final int HOVER_FEEDBACK_DURATION = 300;
|
||||
|
||||
private static boolean sFlagHoverEnabled = false;
|
||||
|
||||
protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
||||
protected final Bitmap mBitmap;
|
||||
protected final int mIconColor;
|
||||
|
||||
@Nullable private ColorFilter mColorFilter;
|
||||
|
||||
@VisibleForTesting protected boolean mIsPressed;
|
||||
@VisibleForTesting protected boolean mIsHovered;
|
||||
protected boolean mIsDisabled;
|
||||
float mDisabledAlpha = 1f;
|
||||
|
||||
@DrawableCreationFlags int mCreationFlags = 0;
|
||||
|
||||
// Animator and properties for the fast bitmap drawable's scale
|
||||
@VisibleForTesting protected static final FloatProperty<FastBitmapDrawable> SCALE
|
||||
= new FloatProperty<FastBitmapDrawable>("scale") {
|
||||
@Override
|
||||
public Float get(FastBitmapDrawable fastBitmapDrawable) {
|
||||
return fastBitmapDrawable.mScale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(FastBitmapDrawable fastBitmapDrawable, float value) {
|
||||
fastBitmapDrawable.mScale = value;
|
||||
fastBitmapDrawable.invalidateSelf();
|
||||
}
|
||||
};
|
||||
@VisibleForTesting protected ObjectAnimator mScaleAnimation;
|
||||
private float mScale = 1;
|
||||
private int mAlpha = 255;
|
||||
|
||||
private Drawable mBadge;
|
||||
|
||||
public FastBitmapDrawable(Bitmap b) {
|
||||
this(b, Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
public FastBitmapDrawable(BitmapInfo info) {
|
||||
this(info.icon, info.color);
|
||||
}
|
||||
|
||||
protected FastBitmapDrawable(Bitmap b, int iconColor) {
|
||||
mBitmap = b;
|
||||
mIconColor = iconColor;
|
||||
setFilterBitmap(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
updateBadgeBounds(bounds);
|
||||
}
|
||||
|
||||
private void updateBadgeBounds(Rect bounds) {
|
||||
if (mBadge != null) {
|
||||
setBadgeBounds(mBadge, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void draw(Canvas canvas) {
|
||||
if (mScale != 1f) {
|
||||
int count = canvas.save();
|
||||
Rect bounds = getBounds();
|
||||
canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
|
||||
drawInternal(canvas, bounds);
|
||||
if (mBadge != null) {
|
||||
mBadge.draw(canvas);
|
||||
}
|
||||
canvas.restoreToCount(count);
|
||||
} else {
|
||||
drawInternal(canvas, getBounds());
|
||||
if (mBadge != null) {
|
||||
mBadge.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawInternal(Canvas canvas, Rect bounds) {
|
||||
canvas.drawBitmap(mBitmap, null, bounds, mPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary icon color, slightly tinted white
|
||||
*/
|
||||
public int getIconColor() {
|
||||
int whiteScrim = setColorAlphaBound(Color.WHITE, WHITE_SCRIM_ALPHA);
|
||||
return ColorUtils.compositeColors(whiteScrim, mIconColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this represents a themed icon
|
||||
*/
|
||||
public boolean isThemed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the drawable was created with theme, even if it doesn't
|
||||
* support theming itself.
|
||||
*/
|
||||
public boolean isCreatedForTheme() {
|
||||
return isThemed() || (mCreationFlags & FLAG_THEMED) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
mColorFilter = cf;
|
||||
updateFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
if (mAlpha != alpha) {
|
||||
mAlpha = alpha;
|
||||
mPaint.setAlpha(alpha);
|
||||
invalidateSelf();
|
||||
if (mBadge != null) {
|
||||
mBadge.setAlpha(alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilterBitmap(boolean filterBitmap) {
|
||||
mPaint.setFilterBitmap(filterBitmap);
|
||||
mPaint.setAntiAlias(filterBitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlpha() {
|
||||
return mAlpha;
|
||||
}
|
||||
|
||||
public void resetScale() {
|
||||
if (mScaleAnimation != null) {
|
||||
mScaleAnimation.cancel();
|
||||
mScaleAnimation = null;
|
||||
}
|
||||
mScale = 1;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public float getAnimatedScale() {
|
||||
return mScaleAnimation == null ? 1 : mScale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mBitmap.getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mBitmap.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimumWidth() {
|
||||
return getBounds().width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimumHeight() {
|
||||
return getBounds().height();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateful() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColorFilter getColorFilter() {
|
||||
return mPaint.getColorFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] state) {
|
||||
boolean isPressed = false;
|
||||
boolean isHovered = false;
|
||||
for (int s : state) {
|
||||
if (s == android.R.attr.state_pressed) {
|
||||
isPressed = true;
|
||||
break;
|
||||
} else if (sFlagHoverEnabled && s == android.R.attr.state_hovered) {
|
||||
isHovered = true;
|
||||
// Do not break on hovered state, as pressed state should take precedence.
|
||||
}
|
||||
}
|
||||
if (mIsPressed != isPressed || mIsHovered != isHovered) {
|
||||
if (mScaleAnimation != null) {
|
||||
mScaleAnimation.cancel();
|
||||
}
|
||||
|
||||
float endScale = isPressed ? PRESSED_SCALE : (isHovered ? HOVERED_SCALE : 1f);
|
||||
if (mScale != endScale) {
|
||||
if (isVisible()) {
|
||||
Interpolator interpolator =
|
||||
isPressed != mIsPressed ? (isPressed ? ACCEL : DEACCEL)
|
||||
: HOVER_EMPHASIZED_DECELERATE_INTERPOLATOR;
|
||||
int duration =
|
||||
isPressed != mIsPressed ? CLICK_FEEDBACK_DURATION
|
||||
: HOVER_FEEDBACK_DURATION;
|
||||
mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, endScale);
|
||||
mScaleAnimation.setDuration(duration);
|
||||
mScaleAnimation.setInterpolator(interpolator);
|
||||
mScaleAnimation.start();
|
||||
} else {
|
||||
mScale = endScale;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
mIsPressed = isPressed;
|
||||
mIsHovered = isHovered;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setIsDisabled(boolean isDisabled) {
|
||||
if (mIsDisabled != isDisabled) {
|
||||
mIsDisabled = isDisabled;
|
||||
updateFilter();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isDisabled() {
|
||||
return mIsDisabled;
|
||||
}
|
||||
|
||||
public void setBadge(Drawable badge) {
|
||||
if (mBadge != null) {
|
||||
mBadge.setCallback(null);
|
||||
}
|
||||
mBadge = badge;
|
||||
if (mBadge != null) {
|
||||
mBadge.setCallback(this);
|
||||
}
|
||||
updateBadgeBounds(getBounds());
|
||||
updateFilter();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Drawable getBadge() {
|
||||
return mBadge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the paint to reflect the current brightness and saturation.
|
||||
*/
|
||||
protected void updateFilter() {
|
||||
mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter(mDisabledAlpha) : mColorFilter);
|
||||
if (mBadge != null) {
|
||||
mBadge.setColorFilter(getColorFilter());
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
protected FastBitmapConstantState newConstantState() {
|
||||
return new FastBitmapConstantState(mBitmap, mIconColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ConstantState getConstantState() {
|
||||
FastBitmapConstantState cs = newConstantState();
|
||||
cs.mIsDisabled = mIsDisabled;
|
||||
if (mBadge != null) {
|
||||
cs.mBadgeConstantState = mBadge.getConstantState();
|
||||
}
|
||||
cs.mCreationFlags = mCreationFlags;
|
||||
return cs;
|
||||
}
|
||||
|
||||
public static ColorFilter getDisabledColorFilter() {
|
||||
return getDisabledColorFilter(1);
|
||||
}
|
||||
|
||||
// Returns if the FastBitmapDrawable contains a badge.
|
||||
public boolean hasBadge() {
|
||||
return (mCreationFlags & FLAG_NO_BADGE) == 0;
|
||||
}
|
||||
|
||||
private static ColorFilter getDisabledColorFilter(float disabledAlpha) {
|
||||
ColorMatrix tempBrightnessMatrix = new ColorMatrix();
|
||||
ColorMatrix tempFilterMatrix = new ColorMatrix();
|
||||
|
||||
tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
|
||||
float scale = 1 - DISABLED_BRIGHTNESS;
|
||||
int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
|
||||
float[] mat = tempBrightnessMatrix.getArray();
|
||||
mat[0] = scale;
|
||||
mat[6] = scale;
|
||||
mat[12] = scale;
|
||||
mat[4] = brightnessI;
|
||||
mat[9] = brightnessI;
|
||||
mat[14] = brightnessI;
|
||||
mat[18] = disabledAlpha;
|
||||
tempFilterMatrix.preConcat(tempBrightnessMatrix);
|
||||
return new ColorMatrixColorFilter(tempFilterMatrix);
|
||||
}
|
||||
|
||||
protected static final int getDisabledColor(int color) {
|
||||
int component = (Color.red(color) + Color.green(color) + Color.blue(color)) / 3;
|
||||
float scale = 1 - DISABLED_BRIGHTNESS;
|
||||
int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
|
||||
component = Math.min(Math.round(scale * component + brightnessI), FULLY_OPAQUE);
|
||||
return Color.rgb(component, component, component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bounds for the badge drawable based on the main icon bounds
|
||||
*/
|
||||
public static void setBadgeBounds(Drawable badge, Rect iconBounds) {
|
||||
int size = getBadgeSizeForIconSize(iconBounds.width());
|
||||
badge.setBounds(iconBounds.right - size, iconBounds.bottom - size,
|
||||
iconBounds.right, iconBounds.bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(Drawable who) {
|
||||
if (who == mBadge) {
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleDrawable(Drawable who, Runnable what, long when) {
|
||||
if (who == mBadge) {
|
||||
scheduleSelf(what, when);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unscheduleDrawable(Drawable who, Runnable what) {
|
||||
unscheduleSelf(what);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether hover state functionality is enabled.
|
||||
*/
|
||||
public static void setFlagHoverEnabled(boolean isFlagHoverEnabled) {
|
||||
sFlagHoverEnabled = isFlagHoverEnabled;
|
||||
}
|
||||
|
||||
protected static class FastBitmapConstantState extends ConstantState {
|
||||
protected final Bitmap mBitmap;
|
||||
protected final int mIconColor;
|
||||
|
||||
// These are initialized later so that subclasses don't need to
|
||||
// pass everything in constructor
|
||||
protected boolean mIsDisabled;
|
||||
private ConstantState mBadgeConstantState;
|
||||
|
||||
@DrawableCreationFlags int mCreationFlags = 0;
|
||||
|
||||
public FastBitmapConstantState(Bitmap bitmap, int color) {
|
||||
mBitmap = bitmap;
|
||||
mIconColor = color;
|
||||
}
|
||||
|
||||
protected FastBitmapDrawable createDrawable() {
|
||||
return new FastBitmapDrawable(mBitmap, mIconColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final FastBitmapDrawable newDrawable() {
|
||||
FastBitmapDrawable drawable = createDrawable();
|
||||
drawable.setIsDisabled(mIsDisabled);
|
||||
if (mBadgeConstantState != null) {
|
||||
drawable.setBadge(mBadgeConstantState.newDrawable());
|
||||
}
|
||||
drawable.mCreationFlags = mCreationFlags;
|
||||
return drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangingConfigurations() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
145
iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
Normal file
145
iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.RegionIterator;
|
||||
import android.graphics.drawable.AdaptiveIconDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.PathParser;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class GraphicsUtils {
|
||||
|
||||
private static final String TAG = "GraphicsUtils";
|
||||
private static final float MASK_SIZE = 100f;
|
||||
|
||||
public static Runnable sOnNewBitmapRunnable = () -> { };
|
||||
|
||||
/**
|
||||
* Set the alpha component of {@code color} to be {@code alpha}. Unlike the support lib version,
|
||||
* it bounds the alpha in valid range instead of throwing an exception to allow for safer
|
||||
* interpolation of color animations
|
||||
*/
|
||||
@ColorInt
|
||||
public static int setColorAlphaBound(int color, int alpha) {
|
||||
if (alpha < 0) {
|
||||
alpha = 0;
|
||||
} else if (alpha > 255) {
|
||||
alpha = 255;
|
||||
}
|
||||
return (color & 0x00ffffff) | (alpha << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses the bitmap to a byte array for serialization.
|
||||
*/
|
||||
public static byte[] flattenBitmap(Bitmap bitmap) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(getExpectedBitmapSize(bitmap));
|
||||
try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||
out.flush();
|
||||
out.close();
|
||||
return out.toByteArray();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not write bitmap");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try go guesstimate how much space the icon will take when serialized to avoid unnecessary
|
||||
* allocations/copies during the write (4 bytes per pixel).
|
||||
*/
|
||||
static int getExpectedBitmapSize(Bitmap bitmap) {
|
||||
return bitmap.getWidth() * bitmap.getHeight() * 4;
|
||||
}
|
||||
|
||||
public static int getArea(Region r) {
|
||||
RegionIterator itr = new RegionIterator(r);
|
||||
int area = 0;
|
||||
Rect tempRect = new Rect();
|
||||
while (itr.next(tempRect)) {
|
||||
area += tempRect.width() * tempRect.height();
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to track new bitmap creation
|
||||
*/
|
||||
public static void noteNewBitmapCreated() {
|
||||
sOnNewBitmapRunnable.run();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the default path to be used by an icon
|
||||
*/
|
||||
public static Path getShapePath(@NonNull Context context, int size) {
|
||||
if (IconProvider.CONFIG_ICON_MASK_RES_ID != Resources.ID_NULL) {
|
||||
Path path = PathParser.createPathFromPathData(
|
||||
context.getString(IconProvider.CONFIG_ICON_MASK_RES_ID));
|
||||
if (path != null) {
|
||||
if (size != MASK_SIZE) {
|
||||
Matrix m = new Matrix();
|
||||
float scale = ((float) size) / MASK_SIZE;
|
||||
m.setScale(scale, scale);
|
||||
path.transform(m);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
|
||||
new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
|
||||
drawable.setBounds(0, 0, size, size);
|
||||
return new Path(drawable.getIconMask());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the color associated with the attribute
|
||||
*/
|
||||
public static int getAttrColor(Context context, int attr) {
|
||||
TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
|
||||
int colorAccent = ta.getColor(0, 0);
|
||||
ta.recycle();
|
||||
return colorAccent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alpha corresponding to the theme attribute {@param attr}
|
||||
*/
|
||||
public static float getFloat(Context context, int attr, float defValue) {
|
||||
TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
|
||||
float value = ta.getFloat(0, defValue);
|
||||
ta.recycle();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.AdaptiveIconDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class IconNormalizer {
|
||||
|
||||
private static final String TAG = "IconNormalizer";
|
||||
private static final boolean DEBUG = false;
|
||||
// Ratio of icon visible area to full icon size for a square shaped icon
|
||||
private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
|
||||
// Ratio of icon visible area to full icon size for a circular shaped icon
|
||||
private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
|
||||
|
||||
private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
|
||||
|
||||
// Slope used to calculate icon visible area to full icon size for any generic shaped icon.
|
||||
private static final float LINEAR_SCALE_SLOPE =
|
||||
(MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
|
||||
|
||||
private static final int MIN_VISIBLE_ALPHA = 40;
|
||||
|
||||
// Shape detection related constants
|
||||
private static final float BOUND_RATIO_MARGIN = .05f;
|
||||
private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f;
|
||||
private static final float SCALE_NOT_INITIALIZED = 0;
|
||||
|
||||
// Ratio of the diameter of an normalized circular icon to the actual icon size.
|
||||
public static final float ICON_VISIBLE_AREA_FACTOR = 0.92f;
|
||||
|
||||
private final int mMaxSize;
|
||||
private final Bitmap mBitmap;
|
||||
private final Canvas mCanvas;
|
||||
private final Paint mPaintMaskShape;
|
||||
private final Paint mPaintMaskShapeOutline;
|
||||
private final byte[] mPixels;
|
||||
|
||||
private final RectF mAdaptiveIconBounds;
|
||||
private float mAdaptiveIconScale;
|
||||
|
||||
private boolean mEnableShapeDetection;
|
||||
|
||||
// for each y, stores the position of the leftmost x and the rightmost x
|
||||
private final float[] mLeftBorder;
|
||||
private final float[] mRightBorder;
|
||||
private final Rect mBounds;
|
||||
private final Path mShapePath;
|
||||
private final Matrix mMatrix;
|
||||
|
||||
/** package private **/
|
||||
IconNormalizer(Context context, int iconBitmapSize, boolean shapeDetection) {
|
||||
// Use twice the icon size as maximum size to avoid scaling down twice.
|
||||
mMaxSize = iconBitmapSize * 2;
|
||||
mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
|
||||
mCanvas = new Canvas(mBitmap);
|
||||
mPixels = new byte[mMaxSize * mMaxSize];
|
||||
mLeftBorder = new float[mMaxSize];
|
||||
mRightBorder = new float[mMaxSize];
|
||||
mBounds = new Rect();
|
||||
mAdaptiveIconBounds = new RectF();
|
||||
|
||||
mPaintMaskShape = new Paint();
|
||||
mPaintMaskShape.setColor(Color.RED);
|
||||
mPaintMaskShape.setStyle(Paint.Style.FILL);
|
||||
mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
|
||||
|
||||
mPaintMaskShapeOutline = new Paint();
|
||||
mPaintMaskShapeOutline.setStrokeWidth(
|
||||
2 * context.getResources().getDisplayMetrics().density);
|
||||
mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
|
||||
mPaintMaskShapeOutline.setColor(Color.BLACK);
|
||||
mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
mShapePath = new Path();
|
||||
mMatrix = new Matrix();
|
||||
mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
|
||||
mEnableShapeDetection = shapeDetection;
|
||||
}
|
||||
|
||||
private static float getScale(float hullArea, float boundingArea, float fullArea) {
|
||||
float hullByRect = hullArea / boundingArea;
|
||||
float scaleRequired;
|
||||
if (hullByRect < CIRCLE_AREA_BY_RECT) {
|
||||
scaleRequired = MAX_CIRCLE_AREA_FACTOR;
|
||||
} else {
|
||||
scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
|
||||
}
|
||||
|
||||
float areaScale = hullArea / fullArea;
|
||||
// Use sqrt of the final ratio as the images is scaled across both width and height.
|
||||
return areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param d Should be AdaptiveIconDrawable
|
||||
* @param size Canvas size to use
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public static float normalizeAdaptiveIcon(Drawable d, int size, @Nullable RectF outBounds) {
|
||||
Rect tmpBounds = new Rect(d.getBounds());
|
||||
d.setBounds(0, 0, size, size);
|
||||
|
||||
Path path = ((AdaptiveIconDrawable) d).getIconMask();
|
||||
Region region = new Region();
|
||||
region.setPath(path, new Region(0, 0, size, size));
|
||||
|
||||
Rect hullBounds = region.getBounds();
|
||||
int hullArea = GraphicsUtils.getArea(region);
|
||||
|
||||
if (outBounds != null) {
|
||||
float sizeF = size;
|
||||
outBounds.set(
|
||||
hullBounds.left / sizeF,
|
||||
hullBounds.top / sizeF,
|
||||
1 - (hullBounds.right / sizeF),
|
||||
1 - (hullBounds.bottom / sizeF));
|
||||
}
|
||||
d.setBounds(tmpBounds);
|
||||
return getScale(hullArea, hullArea, size * size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the shape of the icon is same as the path.
|
||||
* For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds.
|
||||
*/
|
||||
private boolean isShape(Path maskPath) {
|
||||
// Condition1:
|
||||
// If width and height of the path not close to a square, then the icon shape is
|
||||
// not same as the mask shape.
|
||||
float iconRatio = ((float) mBounds.width()) / mBounds.height();
|
||||
if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Condition 2:
|
||||
// Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
|
||||
// should generate transparent image, if the actual icon is equivalent to the shape.
|
||||
|
||||
// Fit the shape within the icon's bounding box
|
||||
mMatrix.reset();
|
||||
mMatrix.setScale(mBounds.width(), mBounds.height());
|
||||
mMatrix.postTranslate(mBounds.left, mBounds.top);
|
||||
maskPath.transform(mMatrix, mShapePath);
|
||||
|
||||
// XOR operation
|
||||
mCanvas.drawPath(mShapePath, mPaintMaskShape);
|
||||
|
||||
// DST_OUT operation around the mask path outline
|
||||
mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
|
||||
|
||||
// Check if the result is almost transparent
|
||||
return isTransparentBitmap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine if certain the bitmap is transparent.
|
||||
*/
|
||||
private boolean isTransparentBitmap() {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(mPixels);
|
||||
buffer.rewind();
|
||||
mBitmap.copyPixelsToBuffer(buffer);
|
||||
|
||||
int y = mBounds.top;
|
||||
// buffer position
|
||||
int index = y * mMaxSize;
|
||||
// buffer shift after every row, width of buffer = mMaxSize
|
||||
int rowSizeDiff = mMaxSize - mBounds.right;
|
||||
|
||||
int sum = 0;
|
||||
for (; y < mBounds.bottom; y++) {
|
||||
index += mBounds.left;
|
||||
for (int x = mBounds.left; x < mBounds.right; x++) {
|
||||
if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
|
||||
sum++;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
index += rowSizeDiff;
|
||||
}
|
||||
|
||||
float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
|
||||
return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
|
||||
* matches the design guidelines for a launcher icon.
|
||||
*
|
||||
* We first calculate the convex hull of the visible portion of the icon.
|
||||
* This hull then compared with the bounding rectangle of the hull to find how closely it
|
||||
* resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an
|
||||
* ideal solution but it gives satisfactory result without affecting the performance.
|
||||
*
|
||||
* This closeness is used to determine the ratio of hull area to the full icon size.
|
||||
* Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
|
||||
*
|
||||
* @param outBounds optional rect to receive the fraction distance from each edge.
|
||||
*/
|
||||
public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
|
||||
@Nullable Path path, @Nullable boolean[] outMaskShape) {
|
||||
if (d instanceof AdaptiveIconDrawable) {
|
||||
if (mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
|
||||
mAdaptiveIconScale = normalizeAdaptiveIcon(d, mMaxSize, mAdaptiveIconBounds);
|
||||
}
|
||||
if (outBounds != null) {
|
||||
outBounds.set(mAdaptiveIconBounds);
|
||||
}
|
||||
return mAdaptiveIconScale;
|
||||
}
|
||||
int width = d.getIntrinsicWidth();
|
||||
int height = d.getIntrinsicHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
width = width <= 0 || width > mMaxSize ? mMaxSize : width;
|
||||
height = height <= 0 || height > mMaxSize ? mMaxSize : height;
|
||||
} else if (width > mMaxSize || height > mMaxSize) {
|
||||
int max = Math.max(width, height);
|
||||
width = mMaxSize * width / max;
|
||||
height = mMaxSize * height / max;
|
||||
}
|
||||
|
||||
mBitmap.eraseColor(Color.TRANSPARENT);
|
||||
d.setBounds(0, 0, width, height);
|
||||
d.draw(mCanvas);
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(mPixels);
|
||||
buffer.rewind();
|
||||
mBitmap.copyPixelsToBuffer(buffer);
|
||||
|
||||
// Overall bounds of the visible icon.
|
||||
int topY = -1;
|
||||
int bottomY = -1;
|
||||
int leftX = mMaxSize + 1;
|
||||
int rightX = -1;
|
||||
|
||||
// Create border by going through all pixels one row at a time and for each row find
|
||||
// the first and the last non-transparent pixel. Set those values to mLeftBorder and
|
||||
// mRightBorder and use -1 if there are no visible pixel in the row.
|
||||
|
||||
// buffer position
|
||||
int index = 0;
|
||||
// buffer shift after every row, width of buffer = mMaxSize
|
||||
int rowSizeDiff = mMaxSize - width;
|
||||
// first and last position for any row.
|
||||
int firstX, lastX;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
firstX = lastX = -1;
|
||||
for (int x = 0; x < width; x++) {
|
||||
if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
|
||||
if (firstX == -1) {
|
||||
firstX = x;
|
||||
}
|
||||
lastX = x;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
index += rowSizeDiff;
|
||||
|
||||
mLeftBorder[y] = firstX;
|
||||
mRightBorder[y] = lastX;
|
||||
|
||||
// If there is at least one visible pixel, update the overall bounds.
|
||||
if (firstX != -1) {
|
||||
bottomY = y;
|
||||
if (topY == -1) {
|
||||
topY = y;
|
||||
}
|
||||
|
||||
leftX = Math.min(leftX, firstX);
|
||||
rightX = Math.max(rightX, lastX);
|
||||
}
|
||||
}
|
||||
|
||||
if (topY == -1 || rightX == -1) {
|
||||
// No valid pixels found. Do not scale.
|
||||
return 1;
|
||||
}
|
||||
|
||||
convertToConvexArray(mLeftBorder, 1, topY, bottomY);
|
||||
convertToConvexArray(mRightBorder, -1, topY, bottomY);
|
||||
|
||||
// Area of the convex hull
|
||||
float area = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
if (mLeftBorder[y] <= -1) {
|
||||
continue;
|
||||
}
|
||||
area += mRightBorder[y] - mLeftBorder[y] + 1;
|
||||
}
|
||||
|
||||
mBounds.left = leftX;
|
||||
mBounds.right = rightX;
|
||||
|
||||
mBounds.top = topY;
|
||||
mBounds.bottom = bottomY;
|
||||
|
||||
if (outBounds != null) {
|
||||
outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
|
||||
1 - ((float) mBounds.right) / width,
|
||||
1 - ((float) mBounds.bottom) / height);
|
||||
}
|
||||
if (outMaskShape != null && mEnableShapeDetection && outMaskShape.length > 0) {
|
||||
outMaskShape[0] = isShape(path);
|
||||
}
|
||||
// Area of the rectangle required to fit the convex hull
|
||||
float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
|
||||
return getScale(area, rectArea, width * height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
|
||||
* (except on either ends) with appropriate values.
|
||||
* @param xCoordinates map of x coordinate per y.
|
||||
* @param direction 1 for left border and -1 for right border.
|
||||
* @param topY the first Y position (inclusive) with a valid value.
|
||||
* @param bottomY the last Y position (inclusive) with a valid value.
|
||||
*/
|
||||
private static void convertToConvexArray(
|
||||
float[] xCoordinates, int direction, int topY, int bottomY) {
|
||||
int total = xCoordinates.length;
|
||||
// The tangent at each pixel.
|
||||
float[] angles = new float[total - 1];
|
||||
|
||||
int first = topY; // First valid y coordinate
|
||||
int last = -1; // Last valid y coordinate which didn't have a missing value
|
||||
|
||||
float lastAngle = Float.MAX_VALUE;
|
||||
|
||||
for (int i = topY + 1; i <= bottomY; i++) {
|
||||
if (xCoordinates[i] <= -1) {
|
||||
continue;
|
||||
}
|
||||
int start;
|
||||
|
||||
if (lastAngle == Float.MAX_VALUE) {
|
||||
start = first;
|
||||
} else {
|
||||
float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
|
||||
start = last;
|
||||
// If this position creates a concave angle, keep moving up until we find a
|
||||
// position which creates a convex angle.
|
||||
if ((currentAngle - lastAngle) * direction < 0) {
|
||||
while (start > first) {
|
||||
start --;
|
||||
currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
|
||||
if ((currentAngle - angles[start]) * direction >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset from last check
|
||||
lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
|
||||
// Update all the points from start.
|
||||
for (int j = start; j < i; j++) {
|
||||
angles[j] = lastAngle;
|
||||
xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The diameter of the normalized circle that fits inside of the square (size x size).
|
||||
*/
|
||||
public static int getNormalizedCircleSize(int size) {
|
||||
float area = size * size * MAX_CIRCLE_AREA_FACTOR;
|
||||
return (int) Math.round(Math.sqrt((4 * area) / Math.PI));
|
||||
}
|
||||
}
|
||||
351
iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
Normal file
351
iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
Normal file
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import static android.content.Intent.ACTION_DATE_CHANGED;
|
||||
import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
|
||||
import static android.content.Intent.ACTION_TIME_CHANGED;
|
||||
import static android.content.res.Resources.ID_NULL;
|
||||
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.LauncherActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.AdaptiveIconDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.InsetDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.PatternMatcher;
|
||||
import android.os.Process;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.os.BuildCompat;
|
||||
|
||||
import com.android.launcher3.util.SafeCloseable;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Class to handle icon loading from different packages
|
||||
*/
|
||||
public class IconProvider {
|
||||
|
||||
private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
|
||||
static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
|
||||
"config_icon_mask", "string", "android");
|
||||
|
||||
private static final String TAG = "IconProvider";
|
||||
private static final boolean DEBUG = false;
|
||||
public static final boolean ATLEAST_T = BuildCompat.isAtLeastT();
|
||||
|
||||
private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
|
||||
|
||||
private static final String SYSTEM_STATE_SEPARATOR = " ";
|
||||
|
||||
protected final Context mContext;
|
||||
private final ComponentName mCalendar;
|
||||
private final ComponentName mClock;
|
||||
|
||||
public IconProvider(Context context) {
|
||||
mContext = context;
|
||||
mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
|
||||
mClock = parseComponentOrNull(context, R.string.clock_component_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds any modification to the provided systemState for dynamic icons. This system state
|
||||
* is used by caches to check for icon invalidation.
|
||||
*/
|
||||
public String getSystemStateForPackage(String systemState, String packageName) {
|
||||
if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
|
||||
return systemState + SYSTEM_STATE_SEPARATOR + getDay();
|
||||
} else {
|
||||
return systemState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the icon for the provided LauncherActivityInfo
|
||||
*/
|
||||
public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
|
||||
return getIconWithOverrides(info.getApplicationInfo().packageName, iconDpi,
|
||||
() -> info.getIcon(iconDpi));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the icon for the provided activity info
|
||||
*/
|
||||
public Drawable getIcon(ActivityInfo info) {
|
||||
return getIcon(info, mContext.getResources().getConfiguration().densityDpi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the icon for the provided activity info
|
||||
*/
|
||||
public Drawable getIcon(ActivityInfo info, int iconDpi) {
|
||||
return getIconWithOverrides(info.applicationInfo.packageName, iconDpi,
|
||||
() -> loadActivityInfoIcon(info, iconDpi));
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private Drawable getIconWithOverrides(String packageName, int iconDpi,
|
||||
Supplier<Drawable> fallback) {
|
||||
ThemeData td = getThemeDataForPackage(packageName);
|
||||
|
||||
Drawable icon = null;
|
||||
if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
|
||||
icon = loadCalendarDrawable(iconDpi, td);
|
||||
} else if (mClock != null && mClock.getPackageName().equals(packageName)) {
|
||||
icon = ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi, td);
|
||||
}
|
||||
if (icon == null) {
|
||||
icon = fallback.get();
|
||||
if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && td != null) {
|
||||
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon;
|
||||
if (aid.getMonochrome() == null) {
|
||||
icon = new AdaptiveIconDrawable(aid.getBackground(),
|
||||
aid.getForeground(), td.loadPaddedDrawable());
|
||||
}
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
protected ThemeData getThemeDataForPackage(String packageName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
|
||||
final int iconRes = ai.getIconResource();
|
||||
Drawable icon = null;
|
||||
// Get the preferred density icon from the app's resources
|
||||
if (density != 0 && iconRes != 0) {
|
||||
try {
|
||||
final Resources resources = mContext.getPackageManager()
|
||||
.getResourcesForApplication(ai.applicationInfo);
|
||||
icon = resources.getDrawableForDensity(iconRes, density);
|
||||
} catch (NameNotFoundException | Resources.NotFoundException exc) { }
|
||||
}
|
||||
// Get the default density icon
|
||||
if (icon == null) {
|
||||
icon = ai.loadIcon(mContext.getPackageManager());
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private Drawable loadCalendarDrawable(int iconDpi, @Nullable ThemeData td) {
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
try {
|
||||
final Bundle metadata = pm.getActivityInfo(
|
||||
mCalendar,
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
|
||||
.metaData;
|
||||
final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
|
||||
final int id = getDynamicIconId(metadata, resources);
|
||||
if (id != ID_NULL) {
|
||||
if (DEBUG) Log.d(TAG, "Got icon #" + id);
|
||||
Drawable drawable = resources.getDrawableForDensity(id, iconDpi, null /* theme */);
|
||||
if (ATLEAST_T && drawable instanceof AdaptiveIconDrawable && td != null) {
|
||||
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable;
|
||||
if (aid.getMonochrome() != null) {
|
||||
return drawable;
|
||||
}
|
||||
if ("array".equals(td.mResources.getResourceTypeName(td.mResID))) {
|
||||
TypedArray ta = td.mResources.obtainTypedArray(td.mResID);
|
||||
int monoId = ta.getResourceId(IconProvider.getDay(), ID_NULL);
|
||||
ta.recycle();
|
||||
return monoId == ID_NULL ? drawable
|
||||
: new AdaptiveIconDrawable(aid.getBackground(), aid.getForeground(),
|
||||
new ThemeData(td.mResources, monoId).loadPaddedDrawable());
|
||||
}
|
||||
}
|
||||
return drawable;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Could not get activityinfo or resources for package: "
|
||||
+ mCalendar.getPackageName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param metadata metadata of the default activity of Calendar
|
||||
* @param resources from the Calendar package
|
||||
* @return the resource id for today's Calendar icon; 0 if resources cannot be found.
|
||||
*/
|
||||
private int getDynamicIconId(Bundle metadata, Resources resources) {
|
||||
if (metadata == null) {
|
||||
return ID_NULL;
|
||||
}
|
||||
String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
|
||||
final int arrayId = metadata.getInt(key, ID_NULL);
|
||||
if (arrayId == ID_NULL) {
|
||||
return ID_NULL;
|
||||
}
|
||||
try {
|
||||
return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
|
||||
}
|
||||
return ID_NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Today's day of the month, zero-indexed.
|
||||
*/
|
||||
private static int getDay() {
|
||||
return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
|
||||
}
|
||||
|
||||
private static ComponentName parseComponentOrNull(Context context, int resId) {
|
||||
String cn = context.getString(resId);
|
||||
return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the current system icon state
|
||||
*/
|
||||
public String getSystemIconState() {
|
||||
return (CONFIG_ICON_MASK_RES_ID == ID_NULL
|
||||
? "" : mContext.getResources().getString(CONFIG_ICON_MASK_RES_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to listen for various system dependent icon changes.
|
||||
*/
|
||||
public SafeCloseable registerIconChangeListener(IconChangeListener listener, Handler handler) {
|
||||
return new IconChangeReceiver(listener, handler);
|
||||
}
|
||||
|
||||
public static class ThemeData {
|
||||
|
||||
final Resources mResources;
|
||||
final int mResID;
|
||||
|
||||
public ThemeData(Resources resources, int resID) {
|
||||
mResources = resources;
|
||||
mResID = resID;
|
||||
}
|
||||
|
||||
Drawable loadPaddedDrawable() {
|
||||
if (!"drawable".equals(mResources.getResourceTypeName(mResID))) {
|
||||
return null;
|
||||
}
|
||||
Drawable d = mResources.getDrawable(mResID).mutate();
|
||||
d = new InsetDrawable(d, .2f);
|
||||
float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction());
|
||||
Drawable fg = new InsetDrawable(d, inset);
|
||||
return fg;
|
||||
}
|
||||
}
|
||||
|
||||
private class IconChangeReceiver extends BroadcastReceiver implements SafeCloseable {
|
||||
|
||||
private final IconChangeListener mCallback;
|
||||
private String mIconState;
|
||||
|
||||
IconChangeReceiver(IconChangeListener callback, Handler handler) {
|
||||
mCallback = callback;
|
||||
mIconState = getSystemIconState();
|
||||
|
||||
|
||||
IntentFilter packageFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
|
||||
packageFilter.addDataScheme("package");
|
||||
packageFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
|
||||
mContext.registerReceiver(this, packageFilter, null, handler);
|
||||
|
||||
if (mCalendar != null || mClock != null) {
|
||||
final IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED);
|
||||
if (mCalendar != null) {
|
||||
filter.addAction(Intent.ACTION_TIME_CHANGED);
|
||||
filter.addAction(ACTION_DATE_CHANGED);
|
||||
}
|
||||
mContext.registerReceiver(this, filter, null, handler);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_TIMEZONE_CHANGED:
|
||||
if (mClock != null) {
|
||||
mCallback.onAppIconChanged(mClock.getPackageName(), Process.myUserHandle());
|
||||
}
|
||||
// follow through
|
||||
case ACTION_DATE_CHANGED:
|
||||
case ACTION_TIME_CHANGED:
|
||||
if (mCalendar != null) {
|
||||
for (UserHandle user
|
||||
: context.getSystemService(UserManager.class).getUserProfiles()) {
|
||||
mCallback.onAppIconChanged(mCalendar.getPackageName(), user);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ACTION_OVERLAY_CHANGED: {
|
||||
String newState = getSystemIconState();
|
||||
if (!mIconState.equals(newState)) {
|
||||
mIconState = newState;
|
||||
mCallback.onSystemIconStateChanged(mIconState);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mContext.unregisterReceiver(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for receiving icon changes
|
||||
*/
|
||||
public interface IconChangeListener {
|
||||
|
||||
/**
|
||||
* Called when the icon for a particular app changes
|
||||
*/
|
||||
void onAppIconChanged(String packageName, UserHandle user);
|
||||
|
||||
/**
|
||||
* Called when the global icon state changed, which can typically affect all icons
|
||||
*/
|
||||
void onSystemIconStateChanged(String iconState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
|
||||
/**
|
||||
* Subclass which draws a placeholder icon when the actual icon is not yet loaded
|
||||
*/
|
||||
public class PlaceHolderIconDrawable extends FastBitmapDrawable {
|
||||
|
||||
// Path in [0, 100] bounds.
|
||||
private final Path mProgressPath;
|
||||
|
||||
public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
|
||||
super(info);
|
||||
|
||||
mProgressPath = GraphicsUtils.getShapePath(context, 100);
|
||||
mPaint.setColor(ColorUtils.compositeColors(
|
||||
GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Canvas canvas, Rect bounds) {
|
||||
int saveCount = canvas.save();
|
||||
canvas.translate(bounds.left, bounds.top);
|
||||
canvas.scale(bounds.width() / 100f, bounds.height() / 100f);
|
||||
canvas.drawPath(mProgressPath, mPaint);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
/** Updates this placeholder to {@code newIcon} with animation. */
|
||||
public void animateIconUpdate(Drawable newIcon) {
|
||||
int placeholderColor = mPaint.getColor();
|
||||
int originalAlpha = Color.alpha(placeholderColor);
|
||||
|
||||
ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
|
||||
iconUpdateAnimation.setDuration(375);
|
||||
iconUpdateAnimation.addUpdateListener(valueAnimator -> {
|
||||
int newAlpha = (int) valueAnimator.getAnimatedValue();
|
||||
int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
|
||||
|
||||
newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP));
|
||||
});
|
||||
iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
newIcon.setColorFilter(null);
|
||||
}
|
||||
});
|
||||
iconUpdateAnimation.start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.DrawableWrapper;
|
||||
|
||||
/**
|
||||
* A drawable which clips rounded corner around a child drawable
|
||||
*/
|
||||
public class RoundDrawableWrapper extends DrawableWrapper {
|
||||
|
||||
private final RectF mTempRect = new RectF();
|
||||
private final Path mClipPath = new Path();
|
||||
private final float mRoundedCornersRadius;
|
||||
|
||||
public RoundDrawableWrapper(Drawable dr, float radius) {
|
||||
super(dr);
|
||||
mRoundedCornersRadius = radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
mTempRect.set(getBounds());
|
||||
mClipPath.reset();
|
||||
mClipPath.addRoundRect(mTempRect, mRoundedCornersRadius,
|
||||
mRoundedCornersRadius, Path.Direction.CCW);
|
||||
super.onBoundsChange(bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void draw(Canvas canvas) {
|
||||
int saveCount = canvas.save();
|
||||
canvas.clipPath(mClipPath);
|
||||
super.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BlurMaskFilter;
|
||||
import android.graphics.BlurMaskFilter.Blur;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* Utility class to add shadows to bitmaps.
|
||||
*/
|
||||
public class ShadowGenerator {
|
||||
|
||||
public static final boolean ENABLE_SHADOWS = true;
|
||||
|
||||
public static final float BLUR_FACTOR = 1.68f/48;
|
||||
|
||||
// Percent of actual icon size
|
||||
public static final float KEY_SHADOW_DISTANCE = 1f/48;
|
||||
private static final int KEY_SHADOW_ALPHA = 7;
|
||||
// Percent of actual icon size
|
||||
private static final float HALF_DISTANCE = 0.5f;
|
||||
private static final int AMBIENT_SHADOW_ALPHA = 25;
|
||||
|
||||
private final int mIconSize;
|
||||
|
||||
private final Paint mBlurPaint;
|
||||
private final Paint mDrawPaint;
|
||||
private final BlurMaskFilter mDefaultBlurMaskFilter;
|
||||
|
||||
public ShadowGenerator(int iconSize) {
|
||||
mIconSize = iconSize;
|
||||
mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL);
|
||||
}
|
||||
|
||||
public synchronized void drawShadow(Bitmap icon, Canvas out) {
|
||||
if (ENABLE_SHADOWS) {
|
||||
int[] offset = new int[2];
|
||||
mBlurPaint.setMaskFilter(mDefaultBlurMaskFilter);
|
||||
Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
|
||||
|
||||
// Draw ambient shadow
|
||||
mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
|
||||
out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
|
||||
|
||||
// Draw key shadow
|
||||
mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
|
||||
out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize,
|
||||
mDrawPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/** package private **/
|
||||
void addPathShadow(Path path, Canvas out) {
|
||||
if (ENABLE_SHADOWS) {
|
||||
mDrawPaint.setMaskFilter(mDefaultBlurMaskFilter);
|
||||
|
||||
// Draw ambient shadow
|
||||
mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
|
||||
out.drawPath(path, mDrawPaint);
|
||||
|
||||
// Draw key shadow
|
||||
int save = out.save();
|
||||
mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
|
||||
out.translate(0, KEY_SHADOW_DISTANCE * mIconSize);
|
||||
out.drawPath(path, mDrawPaint);
|
||||
out.restoreToCount(save);
|
||||
|
||||
mDrawPaint.setMaskFilter(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum amount by which an icon with {@param bounds} should be scaled
|
||||
* so that the shadows do not get clipped.
|
||||
*/
|
||||
public static float getScaleForBounds(RectF bounds) {
|
||||
float scale = 1;
|
||||
|
||||
if (ENABLE_SHADOWS) {
|
||||
// For top, left & right, we need same space.
|
||||
float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top);
|
||||
if (minSide < BLUR_FACTOR) {
|
||||
scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide);
|
||||
}
|
||||
|
||||
// We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the moment b/298203449
|
||||
float bottomSpace = BLUR_FACTOR;
|
||||
if (bounds.bottom < bottomSpace) {
|
||||
scale = Math.min(scale,
|
||||
(HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom));
|
||||
}
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
public final RectF bounds = new RectF();
|
||||
public final int color;
|
||||
|
||||
public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA;
|
||||
|
||||
public float shadowBlur;
|
||||
|
||||
public float keyShadowDistance;
|
||||
public int keyShadowAlpha = KEY_SHADOW_ALPHA;
|
||||
public float radius;
|
||||
|
||||
public Builder(int color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public Builder setupBlurForSize(int height) {
|
||||
if (ENABLE_SHADOWS) {
|
||||
shadowBlur = height * 1f / 24;
|
||||
keyShadowDistance = height * 1f / 16;
|
||||
} else {
|
||||
shadowBlur = 0;
|
||||
keyShadowDistance = 0;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bitmap createPill(int width, int height) {
|
||||
return createPill(width, height, height / 2f);
|
||||
}
|
||||
|
||||
public Bitmap createPill(int width, int height, float r) {
|
||||
radius = r;
|
||||
|
||||
int centerX = Math.round(width / 2f + shadowBlur);
|
||||
int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
|
||||
int center = Math.max(centerX, centerY);
|
||||
bounds.set(0, 0, width, height);
|
||||
bounds.offsetTo(center - width / 2f, center - height / 2f);
|
||||
|
||||
int size = center * 2;
|
||||
return BitmapRenderer.createHardwareBitmap(size, size, this::drawShadow);
|
||||
}
|
||||
|
||||
public void drawShadow(Canvas c) {
|
||||
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
p.setColor(color);
|
||||
|
||||
if (ENABLE_SHADOWS) {
|
||||
// Key shadow
|
||||
p.setShadowLayer(shadowBlur, 0, keyShadowDistance,
|
||||
setColorAlphaBound(Color.BLACK, keyShadowAlpha));
|
||||
c.drawRoundRect(bounds, radius, radius, p);
|
||||
|
||||
// Ambient shadow
|
||||
p.setShadowLayer(shadowBlur, 0, 0,
|
||||
setColorAlphaBound(Color.BLACK, ambientShadowAlpha));
|
||||
c.drawRoundRect(bounds, radius, radius, p);
|
||||
}
|
||||
|
||||
if (Color.alpha(color) < 255) {
|
||||
// Clear any content inside the pill-rect for translucent fill.
|
||||
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
p.clearShadowLayer();
|
||||
p.setColor(Color.BLACK);
|
||||
c.drawRoundRect(bounds, radius, radius, p);
|
||||
|
||||
p.setXfermode(null);
|
||||
p.setColor(color);
|
||||
c.drawRoundRect(bounds, radius, radius, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BlendMode;
|
||||
import android.graphics.BlendModeColorFilter;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
|
||||
/**
|
||||
* Class to handle monochrome themed app icons
|
||||
*/
|
||||
@SuppressWarnings("NewApi")
|
||||
public class ThemedIconDrawable extends FastBitmapDrawable {
|
||||
|
||||
public static final String TAG = "ThemedIconDrawable";
|
||||
|
||||
final BitmapInfo bitmapInfo;
|
||||
final int colorFg, colorBg;
|
||||
|
||||
// The foreground/monochrome icon for the app
|
||||
private final Bitmap mMonoIcon;
|
||||
private final Paint mMonoPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
|
||||
private final Bitmap mBgBitmap;
|
||||
private final Paint mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
|
||||
private final ColorFilter mBgFilter, mMonoFilter;
|
||||
|
||||
protected ThemedIconDrawable(ThemedConstantState constantState) {
|
||||
super(constantState.mBitmap, constantState.colorFg);
|
||||
bitmapInfo = constantState.bitmapInfo;
|
||||
colorBg = constantState.colorBg;
|
||||
colorFg = constantState.colorFg;
|
||||
|
||||
mMonoIcon = bitmapInfo.mMono;
|
||||
mMonoFilter = new BlendModeColorFilter(colorFg, BlendMode.SRC_IN);
|
||||
mMonoPaint.setColorFilter(mMonoFilter);
|
||||
|
||||
mBgBitmap = bitmapInfo.mWhiteShadowLayer;
|
||||
mBgFilter = new BlendModeColorFilter(colorBg, BlendMode.SRC_IN);
|
||||
mBgPaint.setColorFilter(mBgFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Canvas canvas, Rect bounds) {
|
||||
canvas.drawBitmap(mBgBitmap, null, bounds, mBgPaint);
|
||||
canvas.drawBitmap(mMonoIcon, null, bounds, mMonoPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateFilter() {
|
||||
super.updateFilter();
|
||||
int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
|
||||
mBgPaint.setAlpha(alpha);
|
||||
mBgPaint.setColorFilter(mIsDisabled ? new BlendModeColorFilter(
|
||||
getDisabledColor(colorBg), BlendMode.SRC_IN) : mBgFilter);
|
||||
|
||||
mMonoPaint.setAlpha(alpha);
|
||||
mMonoPaint.setColorFilter(mIsDisabled ? new BlendModeColorFilter(
|
||||
getDisabledColor(colorFg), BlendMode.SRC_IN) : mMonoFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isThemed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FastBitmapConstantState newConstantState() {
|
||||
return new ThemedConstantState(bitmapInfo, colorBg, colorFg);
|
||||
}
|
||||
|
||||
static class ThemedConstantState extends FastBitmapConstantState {
|
||||
|
||||
final BitmapInfo bitmapInfo;
|
||||
final int colorFg, colorBg;
|
||||
|
||||
public ThemedConstantState(BitmapInfo bitmapInfo, int colorBg, int colorFg) {
|
||||
super(bitmapInfo.icon, bitmapInfo.color);
|
||||
this.bitmapInfo = bitmapInfo;
|
||||
this.colorBg = colorBg;
|
||||
this.colorFg = colorFg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FastBitmapDrawable createDrawable() {
|
||||
return new ThemedIconDrawable(this);
|
||||
}
|
||||
}
|
||||
|
||||
public static FastBitmapDrawable newDrawable(BitmapInfo info, Context context) {
|
||||
int[] colors = getColors(context);
|
||||
return new ThemedConstantState(info, colors[0], colors[1]).newDrawable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an int array representing background and foreground colors for themed icons
|
||||
*/
|
||||
public static int[] getColors(Context context) {
|
||||
Resources res = context.getResources();
|
||||
int[] colors = new int[2];
|
||||
colors[0] = res.getColor(R.color.themed_icon_background_color);
|
||||
colors[1] = res.getColor(R.color.themed_icon_color);
|
||||
return colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconColor() {
|
||||
return colorFg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.icons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.DrawableWrapper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A drawable used for drawing user badge. It draws a circle around the actual badge,
|
||||
* and has support for theming.
|
||||
*/
|
||||
public class UserBadgeDrawable extends DrawableWrapper {
|
||||
|
||||
private static final float VIEWPORT_SIZE = 24;
|
||||
private static final float CENTER = VIEWPORT_SIZE / 2;
|
||||
|
||||
private static final float BG_RADIUS = 11;
|
||||
private static final float SHADOW_RADIUS = 11.5f;
|
||||
private static final float SHADOW_OFFSET_Y = 0.25f;
|
||||
|
||||
private static final int SHADOW_COLOR = 0x11000000;
|
||||
|
||||
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
private final int mBaseColor;
|
||||
private final int mBgColor;
|
||||
private boolean mShouldDrawBackground = true;
|
||||
|
||||
@VisibleForTesting
|
||||
public final boolean mIsThemed;
|
||||
|
||||
public UserBadgeDrawable(Context context, int badgeRes, int colorRes, boolean isThemed) {
|
||||
super(context.getDrawable(badgeRes));
|
||||
|
||||
mIsThemed = isThemed;
|
||||
if (isThemed) {
|
||||
mutate();
|
||||
mBaseColor = context.getColor(R.color.themed_badge_icon_color);
|
||||
mBgColor = context.getColor(R.color.themed_badge_icon_background_color);
|
||||
} else {
|
||||
mBaseColor = context.getColor(colorRes);
|
||||
mBgColor = Color.WHITE;
|
||||
}
|
||||
setTint(mBaseColor);
|
||||
}
|
||||
|
||||
private UserBadgeDrawable(Drawable base, int bgColor, int baseColor,
|
||||
boolean shouldDrawBackground) {
|
||||
super(base);
|
||||
mIsThemed = false;
|
||||
mBgColor = bgColor;
|
||||
mBaseColor = baseColor;
|
||||
mShouldDrawBackground = shouldDrawBackground;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (mShouldDrawBackground) {
|
||||
Rect b = getBounds();
|
||||
int saveCount = canvas.save();
|
||||
canvas.translate(b.left, b.top);
|
||||
canvas.scale(b.width() / VIEWPORT_SIZE, b.height() / VIEWPORT_SIZE);
|
||||
|
||||
mPaint.setColor(SHADOW_COLOR);
|
||||
canvas.drawCircle(CENTER, CENTER + SHADOW_OFFSET_Y, SHADOW_RADIUS, mPaint);
|
||||
|
||||
mPaint.setColor(mBgColor);
|
||||
canvas.drawCircle(CENTER, CENTER, BG_RADIUS, mPaint);
|
||||
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
super.draw(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter filter) {
|
||||
if (filter == null) {
|
||||
super.setTint(mBaseColor);
|
||||
} else if (filter instanceof ColorMatrixColorFilter cf) {
|
||||
ColorMatrix cm = new ColorMatrix();
|
||||
cf.getColorMatrix(cm);
|
||||
|
||||
ColorMatrix cm2 = new ColorMatrix();
|
||||
float[] base = cm2.getArray();
|
||||
base[0] = Color.red(mBaseColor) / 255f;
|
||||
base[6] = Color.green(mBaseColor) / 255f;
|
||||
base[12] = Color.blue(mBaseColor) / 255f;
|
||||
base[18] = Color.alpha(mBaseColor) / 255f;
|
||||
cm2.postConcat(cm);
|
||||
|
||||
super.setColorFilter(new ColorMatrixColorFilter(cm2));
|
||||
} else {
|
||||
// fail safe
|
||||
Paint p = new Paint();
|
||||
p.setColorFilter(filter);
|
||||
Bitmap b = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||
new Canvas(b).drawPaint(p);
|
||||
super.setTint(b.getPixel(0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public void setShouldDrawBackground(boolean shouldDrawBackground) {
|
||||
mutate();
|
||||
mShouldDrawBackground = shouldDrawBackground;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConstantState getConstantState() {
|
||||
return new MyConstantState(
|
||||
getDrawable().getConstantState(), mBgColor, mBaseColor, mShouldDrawBackground);
|
||||
}
|
||||
|
||||
private static class MyConstantState extends ConstantState {
|
||||
|
||||
private final ConstantState mBase;
|
||||
private final int mBgColor;
|
||||
private final int mBaseColor;
|
||||
private final boolean mShouldDrawBackground;
|
||||
|
||||
MyConstantState(ConstantState base, int bgColor, int baseColor,
|
||||
boolean shouldDrawBackground) {
|
||||
mBase = base;
|
||||
mBgColor = bgColor;
|
||||
mBaseColor = baseColor;
|
||||
mShouldDrawBackground = shouldDrawBackground;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangingConfigurations() {
|
||||
return mBase.getChangingConfigurations();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Drawable newDrawable() {
|
||||
return new UserBadgeDrawable(
|
||||
mBase.newDrawable(), mBgColor, mBaseColor, mShouldDrawBackground);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Drawable newDrawable(Resources res) {
|
||||
return new UserBadgeDrawable(
|
||||
mBase.newDrawable(res), mBgColor, mBaseColor, mShouldDrawBackground);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Drawable newDrawable(Resources res, Theme theme) {
|
||||
return new UserBadgeDrawable(
|
||||
mBase.newDrawable(res, theme), mBgColor, mBaseColor, mShouldDrawBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
797
iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
vendored
Normal file
797
iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
vendored
Normal file
@@ -0,0 +1,797 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons.cache;
|
||||
|
||||
import static android.graphics.BitmapFactory.decodeByteArray;
|
||||
|
||||
import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
|
||||
import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
|
||||
import static com.android.launcher3.icons.GraphicsUtils.flattenBitmap;
|
||||
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
|
||||
import static com.android.launcher3.icons.cache.IconCacheUpdateHandler.ICON_UPDATE_TOKEN;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.LocaleList;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Trace;
|
||||
import android.os.UserHandle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.launcher3.icons.BaseIconFactory;
|
||||
import com.android.launcher3.icons.BaseIconFactory.IconOptions;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.FlagOp;
|
||||
import com.android.launcher3.util.SQLiteCacheHelper;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public abstract class BaseIconCache {
|
||||
|
||||
private static final String TAG = "BaseIconCache";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
|
||||
// A format string which returns the original string as is.
|
||||
private static final String IDENTITY_FORMAT_STRING = "%1$s";
|
||||
|
||||
// Empty class name is used for storing package default entry.
|
||||
public static final String EMPTY_CLASS_NAME = ".";
|
||||
|
||||
public static class CacheEntry {
|
||||
|
||||
@NonNull
|
||||
public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
|
||||
@NonNull
|
||||
public CharSequence title = "";
|
||||
@NonNull
|
||||
public CharSequence contentDescription = "";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected final Context mContext;
|
||||
|
||||
@NonNull
|
||||
protected final PackageManager mPackageManager;
|
||||
|
||||
@NonNull
|
||||
private final Map<ComponentKey, CacheEntry> mCache;
|
||||
|
||||
@NonNull
|
||||
protected final Handler mWorkerHandler;
|
||||
|
||||
protected int mIconDpi;
|
||||
|
||||
@NonNull
|
||||
protected IconDB mIconDb;
|
||||
|
||||
@NonNull
|
||||
protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
|
||||
|
||||
@NonNull
|
||||
protected String mSystemState = "";
|
||||
|
||||
@Nullable
|
||||
private BitmapInfo mDefaultIcon;
|
||||
|
||||
@NonNull
|
||||
private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>();
|
||||
|
||||
private final SparseArray<String> mUserFormatString = new SparseArray<>();
|
||||
|
||||
@Nullable
|
||||
private final String mDbFileName;
|
||||
|
||||
@NonNull
|
||||
private final Looper mBgLooper;
|
||||
|
||||
public BaseIconCache(@NonNull final Context context, @Nullable final String dbFileName,
|
||||
@NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize,
|
||||
final boolean inMemoryCache) {
|
||||
mContext = context;
|
||||
mDbFileName = dbFileName;
|
||||
mPackageManager = context.getPackageManager();
|
||||
mBgLooper = bgLooper;
|
||||
mWorkerHandler = new Handler(mBgLooper);
|
||||
|
||||
if (inMemoryCache) {
|
||||
mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
|
||||
} else {
|
||||
// Use a dummy cache
|
||||
mCache = new AbstractMap<ComponentKey, CacheEntry>() {
|
||||
@Override
|
||||
public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheEntry put(ComponentKey key, CacheEntry value) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateSystemState();
|
||||
mIconDpi = iconDpi;
|
||||
mIconDb = new IconDB(context, dbFileName, iconPixelSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the persistable serial number for {@param user}. Subclass should implement proper
|
||||
* caching strategy to avoid making binder call every time.
|
||||
*/
|
||||
protected abstract long getSerialNumberForUser(@NonNull final UserHandle user);
|
||||
|
||||
/**
|
||||
* Return true if the given app is an instant app and should be badged appropriately.
|
||||
*/
|
||||
protected abstract boolean isInstantApp(@NonNull final ApplicationInfo info);
|
||||
|
||||
/**
|
||||
* Opens and returns an icon factory. The factory is recycled by the caller.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract BaseIconFactory getIconFactory();
|
||||
|
||||
public void updateIconParams(final int iconDpi, final int iconPixelSize) {
|
||||
mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
|
||||
}
|
||||
|
||||
private synchronized void updateIconParamsBg(final int iconDpi, final int iconPixelSize) {
|
||||
mIconDpi = iconDpi;
|
||||
mDefaultIcon = null;
|
||||
mUserFlagOpMap.clear();
|
||||
mIconDb.clear();
|
||||
mIconDb.close();
|
||||
mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
|
||||
mCache.clear();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Drawable getFullResIcon(@Nullable final Resources resources, final int iconId) {
|
||||
if (resources != null && iconId != 0) {
|
||||
try {
|
||||
return resources.getDrawableForDensity(iconId, mIconDpi);
|
||||
} catch (Resources.NotFoundException e) { }
|
||||
}
|
||||
return getFullResDefaultActivityIcon(mIconDpi);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Drawable getFullResIcon(@NonNull final String packageName, final int iconId) {
|
||||
try {
|
||||
return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
|
||||
} catch (PackageManager.NameNotFoundException e) { }
|
||||
return getFullResDefaultActivityIcon(mIconDpi);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Drawable getFullResIcon(@NonNull final ActivityInfo info) {
|
||||
try {
|
||||
return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
|
||||
info.getIconResource());
|
||||
} catch (PackageManager.NameNotFoundException e) { }
|
||||
return getFullResDefaultActivityIcon(mIconDpi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any records for the supplied ComponentName.
|
||||
*/
|
||||
public synchronized void remove(@NonNull final ComponentName componentName,
|
||||
@NonNull final UserHandle user) {
|
||||
mCache.remove(new ComponentKey(componentName, user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any records for the supplied package name from memory.
|
||||
*/
|
||||
private void removeFromMemCacheLocked(@Nullable final String packageName,
|
||||
@Nullable final UserHandle user) {
|
||||
HashSet<ComponentKey> forDeletion = new HashSet<>();
|
||||
for (ComponentKey key: mCache.keySet()) {
|
||||
if (key.componentName.getPackageName().equals(packageName)
|
||||
&& key.user.equals(user)) {
|
||||
forDeletion.add(key);
|
||||
}
|
||||
}
|
||||
for (ComponentKey condemned: forDeletion) {
|
||||
mCache.remove(condemned);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the entries related to the given package in memory and persistent DB.
|
||||
*/
|
||||
public synchronized void removeIconsForPkg(@NonNull final String packageName,
|
||||
@NonNull final UserHandle user) {
|
||||
removeFromMemCacheLocked(packageName, user);
|
||||
long userSerial = getSerialNumberForUser(user);
|
||||
mIconDb.delete(
|
||||
IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
|
||||
new String[]{packageName + "/%", Long.toString(userSerial)});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public IconCacheUpdateHandler getUpdateHandler() {
|
||||
updateSystemState();
|
||||
return new IconCacheUpdateHandler(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the system state definition used to check the validity of the cache. It
|
||||
* incorporates all the properties that can affect the cache like the list of enabled locale
|
||||
* and system-version.
|
||||
*/
|
||||
private void updateSystemState() {
|
||||
mLocaleList = mContext.getResources().getConfiguration().getLocales();
|
||||
mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
|
||||
mUserFormatString.clear();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected String getIconSystemState(@Nullable final String packageName) {
|
||||
return mSystemState;
|
||||
}
|
||||
|
||||
public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
|
||||
int key = user.hashCode();
|
||||
int index = mUserFormatString.indexOfKey(key);
|
||||
String format;
|
||||
if (index < 0) {
|
||||
format = mPackageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString();
|
||||
if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) {
|
||||
format = null;
|
||||
}
|
||||
mUserFormatString.put(key, format);
|
||||
} else {
|
||||
format = mUserFormatString.valueAt(index);
|
||||
}
|
||||
return format == null ? label : String.format(format, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entry into the DB and the in-memory cache.
|
||||
* @param replaceExisting if true, it will recreate the bitmap even if it already exists in
|
||||
* the memory. This is useful then the previous bitmap was created using
|
||||
* old data.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public synchronized <T> void addIconToDBAndMemCache(@NonNull final T object,
|
||||
@NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info,
|
||||
final long userSerial, final boolean replaceExisting) {
|
||||
UserHandle user = cachingLogic.getUser(object);
|
||||
ComponentName componentName = cachingLogic.getComponent(object);
|
||||
|
||||
final ComponentKey key = new ComponentKey(componentName, user);
|
||||
CacheEntry entry = null;
|
||||
if (!replaceExisting) {
|
||||
entry = mCache.get(key);
|
||||
// We can't reuse the entry if the high-res icon is not present.
|
||||
if (entry == null || entry.bitmap.isNullOrLowRes()) {
|
||||
entry = null;
|
||||
}
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new CacheEntry();
|
||||
entry.bitmap = cachingLogic.loadIcon(mContext, object);
|
||||
}
|
||||
// Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
|
||||
// (e.g. fallback icon, default icon). So we drop here since there's no point in caching
|
||||
// an empty entry.
|
||||
if (entry.bitmap.isNullOrLowRes()) return;
|
||||
|
||||
CharSequence entryTitle = cachingLogic.getLabel(object);
|
||||
if (TextUtils.isEmpty(entryTitle)) {
|
||||
if (entryTitle == null) {
|
||||
Log.wtf(TAG, "No label returned from caching logic instance: " + cachingLogic);
|
||||
}
|
||||
entryTitle = componentName.getPackageName();;
|
||||
}
|
||||
entry.title = entryTitle;
|
||||
|
||||
entry.contentDescription = getUserBadgedLabel(entry.title, user);
|
||||
if (cachingLogic.addToMemCache()) mCache.put(key, entry);
|
||||
|
||||
ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
|
||||
componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
|
||||
addIconToDB(values, componentName, info, userSerial,
|
||||
cachingLogic.getLastUpdatedTime(object, info));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@param values} to contain versioning information and adds it to the DB.
|
||||
* @param values {@link ContentValues} containing icon & title
|
||||
*/
|
||||
private void addIconToDB(@NonNull final ContentValues values, @NonNull final ComponentName key,
|
||||
@NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime) {
|
||||
values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
|
||||
values.put(IconDB.COLUMN_USER, userSerial);
|
||||
values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime);
|
||||
values.put(IconDB.COLUMN_VERSION, info.versionCode);
|
||||
mIconDb.insertOrReplace(values);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized BitmapInfo getDefaultIcon(@NonNull final UserHandle user) {
|
||||
if (mDefaultIcon == null) {
|
||||
try (BaseIconFactory li = getIconFactory()) {
|
||||
mDefaultIcon = li.makeDefaultIcon();
|
||||
}
|
||||
}
|
||||
return mDefaultIcon.withFlags(getUserFlagOpLocked(user));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected FlagOp getUserFlagOpLocked(@NonNull final UserHandle user) {
|
||||
int key = user.hashCode();
|
||||
int index;
|
||||
if ((index = mUserFlagOpMap.indexOfKey(key)) >= 0) {
|
||||
return mUserFlagOpMap.valueAt(index);
|
||||
} else {
|
||||
try (BaseIconFactory li = getIconFactory()) {
|
||||
FlagOp op = li.getBitmapFlagOp(new IconOptions().setUser(user));
|
||||
mUserFlagOpMap.put(key, op);
|
||||
return op;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDefaultIcon(@NonNull final BitmapInfo icon, @NonNull final UserHandle user) {
|
||||
return getDefaultIcon(user).icon == icon.icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
|
||||
* This method is not thread safe, it must be called from a synchronized method.
|
||||
*/
|
||||
@NonNull
|
||||
protected <T> CacheEntry cacheLocked(
|
||||
@NonNull final ComponentName componentName, @NonNull final UserHandle user,
|
||||
@NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
|
||||
final boolean usePackageIcon, final boolean useLowResIcon) {
|
||||
return cacheLocked(
|
||||
componentName,
|
||||
user,
|
||||
infoProvider,
|
||||
cachingLogic,
|
||||
null,
|
||||
usePackageIcon,
|
||||
useLowResIcon);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected <T> CacheEntry cacheLocked(
|
||||
@NonNull final ComponentName componentName, @NonNull final UserHandle user,
|
||||
@NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
|
||||
@Nullable final Cursor cursor, final boolean usePackageIcon,
|
||||
final boolean useLowResIcon) {
|
||||
assertWorkerThread();
|
||||
ComponentKey cacheKey = new ComponentKey(componentName, user);
|
||||
CacheEntry entry = mCache.get(cacheKey);
|
||||
if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
|
||||
entry = new CacheEntry();
|
||||
if (cachingLogic.addToMemCache()) {
|
||||
mCache.put(cacheKey, entry);
|
||||
}
|
||||
|
||||
// Check the DB first.
|
||||
T object = null;
|
||||
boolean providerFetchedOnce = false;
|
||||
boolean cacheEntryUpdated = cursor == null
|
||||
? getEntryFromDBLocked(cacheKey, entry, useLowResIcon)
|
||||
: updateTitleAndIconLocked(cacheKey, entry, cursor, useLowResIcon);
|
||||
if (!cacheEntryUpdated) {
|
||||
object = infoProvider.get();
|
||||
providerFetchedOnce = true;
|
||||
|
||||
loadFallbackIcon(
|
||||
object,
|
||||
entry,
|
||||
cachingLogic,
|
||||
usePackageIcon,
|
||||
/* usePackageTitle= */ true,
|
||||
componentName,
|
||||
user);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(entry.title)) {
|
||||
if (object == null && !providerFetchedOnce) {
|
||||
object = infoProvider.get();
|
||||
providerFetchedOnce = true;
|
||||
}
|
||||
if (object != null) {
|
||||
loadFallbackTitle(object, entry, cachingLogic, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method for loading an icon bitmap.
|
||||
*/
|
||||
protected <T> void loadFallbackIcon(@Nullable final T object, @NonNull final CacheEntry entry,
|
||||
@NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon,
|
||||
final boolean usePackageTitle, @NonNull final ComponentName componentName,
|
||||
@NonNull final UserHandle user) {
|
||||
if (object != null) {
|
||||
entry.bitmap = cachingLogic.loadIcon(mContext, object);
|
||||
} else {
|
||||
if (usePackageIcon) {
|
||||
CacheEntry packageEntry = getEntryForPackageLocked(
|
||||
componentName.getPackageName(), user, false);
|
||||
if (DEBUG) Log.d(TAG, "using package default icon for " +
|
||||
componentName.toShortString());
|
||||
entry.bitmap = packageEntry.bitmap;
|
||||
entry.contentDescription = packageEntry.contentDescription;
|
||||
|
||||
if (usePackageTitle) {
|
||||
entry.title = packageEntry.title;
|
||||
}
|
||||
}
|
||||
if (entry.bitmap == null) {
|
||||
// TODO: entry.bitmap can never be null, so this should not happen at all.
|
||||
Log.wtf(TAG, "using default icon for " + componentName.toShortString());
|
||||
entry.bitmap = getDefaultIcon(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method for loading an app title.
|
||||
*/
|
||||
protected <T> void loadFallbackTitle(
|
||||
@NonNull final T object, @NonNull final CacheEntry entry,
|
||||
@NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user) {
|
||||
entry.title = cachingLogic.getLabel(object);
|
||||
if (TextUtils.isEmpty(entry.title)) {
|
||||
entry.title = cachingLogic.getComponent(object).getPackageName();
|
||||
}
|
||||
entry.contentDescription = getUserBadgedLabel(
|
||||
cachingLogic.getDescription(object, entry.title), user);
|
||||
}
|
||||
|
||||
public synchronized void clearMemoryCache() {
|
||||
assertWorkerThread();
|
||||
mCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an icon update is in progress
|
||||
*/
|
||||
public boolean isIconUpdateInProgress() {
|
||||
return mWorkerHandler.hasMessages(0, ICON_UPDATE_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a default package entry in the cache. This entry is not persisted and will be removed
|
||||
* when the cache is flushed.
|
||||
*/
|
||||
protected synchronized void cachePackageInstallInfo(@NonNull final String packageName,
|
||||
@NonNull final UserHandle user, @Nullable final Bitmap icon,
|
||||
@Nullable final CharSequence title) {
|
||||
removeFromMemCacheLocked(packageName, user);
|
||||
|
||||
ComponentKey cacheKey = getPackageKey(packageName, user);
|
||||
CacheEntry entry = mCache.get(cacheKey);
|
||||
|
||||
// For icon caching, do not go through DB. Just update the in-memory entry.
|
||||
if (entry == null) {
|
||||
entry = new CacheEntry();
|
||||
}
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
entry.title = title;
|
||||
}
|
||||
if (icon != null) {
|
||||
BaseIconFactory li = getIconFactory();
|
||||
entry.bitmap = li.createShapedIconBitmap(icon, new IconOptions().setUser(user));
|
||||
li.close();
|
||||
}
|
||||
if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
|
||||
mCache.put(cacheKey, entry);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static ComponentKey getPackageKey(@NonNull final String packageName,
|
||||
@NonNull final UserHandle user) {
|
||||
ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
|
||||
return new ComponentKey(cn, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an entry for the package, which can be used as a fallback entry for various components.
|
||||
* This method is not thread safe, it must be called from a synchronized method.
|
||||
*/
|
||||
@WorkerThread
|
||||
@NonNull
|
||||
@SuppressWarnings("NewApi")
|
||||
protected CacheEntry getEntryForPackageLocked(@NonNull final String packageName,
|
||||
@NonNull final UserHandle user, final boolean useLowResIcon) {
|
||||
assertWorkerThread();
|
||||
ComponentKey cacheKey = getPackageKey(packageName, user);
|
||||
CacheEntry entry = mCache.get(cacheKey);
|
||||
|
||||
if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
|
||||
entry = new CacheEntry();
|
||||
boolean entryUpdated = true;
|
||||
|
||||
// Check the DB first.
|
||||
if (!getEntryFromDBLocked(cacheKey, entry, useLowResIcon)) {
|
||||
try {
|
||||
long flags = Process.myUserHandle().equals(user) ? 0 :
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES;
|
||||
flags |= PackageManager.MATCH_ARCHIVED_PACKAGES;
|
||||
PackageInfo info = mPackageManager.getPackageInfo(packageName,
|
||||
PackageManager.PackageInfoFlags.of(flags));
|
||||
ApplicationInfo appInfo = info.applicationInfo;
|
||||
if (appInfo == null) {
|
||||
throw new NameNotFoundException("ApplicationInfo is null");
|
||||
}
|
||||
|
||||
BaseIconFactory li = getIconFactory();
|
||||
// Load the full res icon for the application, but if useLowResIcon is set, then
|
||||
// only keep the low resolution icon instead of the larger full-sized icon
|
||||
BitmapInfo iconInfo = li.createBadgedIconBitmap(
|
||||
appInfo.loadIcon(mPackageManager),
|
||||
new IconOptions().setUser(user).setInstantApp(isInstantApp(appInfo)));
|
||||
li.close();
|
||||
|
||||
entry.title = appInfo.loadLabel(mPackageManager);
|
||||
entry.contentDescription = getUserBadgedLabel(entry.title, user);
|
||||
entry.bitmap = BitmapInfo.of(
|
||||
useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
|
||||
|
||||
// Add the icon in the DB here, since these do not get written during
|
||||
// package updates.
|
||||
ContentValues values = newContentValues(
|
||||
iconInfo, entry.title.toString(), packageName, null);
|
||||
addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user),
|
||||
info.lastUpdateTime);
|
||||
|
||||
} catch (NameNotFoundException e) {
|
||||
if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
|
||||
entryUpdated = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add a filled-out entry to the cache
|
||||
if (entryUpdated) {
|
||||
mCache.put(cacheKey, entry);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
protected boolean getEntryFromDBLocked(@NonNull final ComponentKey cacheKey,
|
||||
@NonNull final CacheEntry entry, final boolean lowRes) {
|
||||
Cursor c = null;
|
||||
Trace.beginSection("loadIconIndividually");
|
||||
try {
|
||||
c = mIconDb.query(
|
||||
lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
|
||||
IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
|
||||
new String[]{
|
||||
cacheKey.componentName.flattenToString(),
|
||||
Long.toString(getSerialNumberForUser(cacheKey.user))});
|
||||
if (c.moveToNext()) {
|
||||
return updateTitleAndIconLocked(cacheKey, entry, c, lowRes);
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
Log.d(TAG, "Error reading icon cache", e);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
Trace.endSection();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean updateTitleAndIconLocked(
|
||||
@NonNull final ComponentKey cacheKey, @NonNull final CacheEntry entry,
|
||||
@NonNull final Cursor c, final boolean lowRes) {
|
||||
// Set the alpha to be 255, so that we never have a wrong color
|
||||
entry.bitmap = BitmapInfo.of(LOW_RES_ICON,
|
||||
setColorAlphaBound(c.getInt(IconDB.INDEX_COLOR), 255));
|
||||
entry.title = c.getString(IconDB.INDEX_TITLE);
|
||||
if (entry.title == null) {
|
||||
entry.title = "";
|
||||
entry.contentDescription = "";
|
||||
} else {
|
||||
entry.contentDescription = getUserBadgedLabel(entry.title, cacheKey.user);
|
||||
}
|
||||
|
||||
if (!lowRes) {
|
||||
byte[] data = c.getBlob(IconDB.INDEX_ICON);
|
||||
if (data == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
|
||||
decodeOptions.inPreferredConfig = Config.HARDWARE;
|
||||
entry.bitmap = BitmapInfo.of(
|
||||
requireNonNull(decodeByteArray(data, 0, data.length, decodeOptions)),
|
||||
entry.bitmap.color);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode mono bitmap
|
||||
data = c.getBlob(IconDB.INDEX_MONO_ICON);
|
||||
Bitmap icon = entry.bitmap.icon;
|
||||
if (data != null && data.length == icon.getHeight() * icon.getWidth()) {
|
||||
Bitmap monoBitmap = Bitmap.createBitmap(
|
||||
icon.getWidth(), icon.getHeight(), Config.ALPHA_8);
|
||||
monoBitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data));
|
||||
Bitmap hwMonoBitmap = monoBitmap.copy(Config.HARDWARE, false /*isMutable*/);
|
||||
if (hwMonoBitmap != null) {
|
||||
monoBitmap.recycle();
|
||||
monoBitmap = hwMonoBitmap;
|
||||
}
|
||||
try (BaseIconFactory factory = getIconFactory()) {
|
||||
entry.bitmap.setMonoIcon(monoBitmap, factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.bitmap.flags = c.getInt(IconDB.INDEX_FLAGS);
|
||||
entry.bitmap = entry.bitmap.withFlags(getUserFlagOpLocked(cacheKey.user));
|
||||
return entry.bitmap != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cursor for an arbitrary query to the cache db
|
||||
*/
|
||||
public synchronized Cursor queryCacheDb(String[] columns, String selection,
|
||||
String[] selectionArgs) {
|
||||
return mIconDb.query(columns, selection, selectionArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache class to store the actual entries on disk
|
||||
*/
|
||||
public static final class IconDB extends SQLiteCacheHelper {
|
||||
private static final int RELEASE_VERSION = 34;
|
||||
|
||||
public static final String TABLE_NAME = "icons";
|
||||
public static final String COLUMN_ROWID = "rowid";
|
||||
public static final String COLUMN_COMPONENT = "componentName";
|
||||
public static final String COLUMN_USER = "profileId";
|
||||
public static final String COLUMN_LAST_UPDATED = "lastUpdated";
|
||||
public static final String COLUMN_VERSION = "version";
|
||||
public static final String COLUMN_ICON = "icon";
|
||||
public static final String COLUMN_ICON_COLOR = "icon_color";
|
||||
public static final String COLUMN_MONO_ICON = "mono_icon";
|
||||
public static final String COLUMN_FLAGS = "flags";
|
||||
public static final String COLUMN_LABEL = "label";
|
||||
public static final String COLUMN_SYSTEM_STATE = "system_state";
|
||||
public static final String COLUMN_KEYWORDS = "keywords";
|
||||
|
||||
public static final String[] COLUMNS_LOW_RES = new String[] {
|
||||
COLUMN_COMPONENT,
|
||||
COLUMN_LABEL,
|
||||
COLUMN_ICON_COLOR,
|
||||
COLUMN_FLAGS};
|
||||
public static final String[] COLUMNS_HIGH_RES = Arrays.copyOf(COLUMNS_LOW_RES,
|
||||
COLUMNS_LOW_RES.length + 2, String[].class);
|
||||
static {
|
||||
COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length] = COLUMN_ICON;
|
||||
COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length + 1] = COLUMN_MONO_ICON;
|
||||
}
|
||||
private static final int INDEX_TITLE = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_LABEL);
|
||||
private static final int INDEX_COLOR = Arrays.asList(COLUMNS_LOW_RES)
|
||||
.indexOf(COLUMN_ICON_COLOR);
|
||||
private static final int INDEX_FLAGS = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_FLAGS);
|
||||
private static final int INDEX_ICON = COLUMNS_LOW_RES.length;
|
||||
private static final int INDEX_MONO_ICON = INDEX_ICON + 1;
|
||||
|
||||
public IconDB(Context context, String dbFileName, int iconPixelSize) {
|
||||
super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreateTable(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
|
||||
+ COLUMN_COMPONENT + " TEXT NOT NULL, "
|
||||
+ COLUMN_USER + " INTEGER NOT NULL, "
|
||||
+ COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, "
|
||||
+ COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, "
|
||||
+ COLUMN_ICON + " BLOB, "
|
||||
+ COLUMN_MONO_ICON + " BLOB, "
|
||||
+ COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, "
|
||||
+ COLUMN_FLAGS + " INTEGER NOT NULL DEFAULT 0, "
|
||||
+ COLUMN_LABEL + " TEXT, "
|
||||
+ COLUMN_SYSTEM_STATE + " TEXT, "
|
||||
+ COLUMN_KEYWORDS + " TEXT, "
|
||||
+ "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") "
|
||||
+ ");");
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ContentValues newContentValues(@NonNull final BitmapInfo bitmapInfo,
|
||||
@NonNull final String label, @NonNull final String packageName,
|
||||
@Nullable final String keywords) {
|
||||
ContentValues values = new ContentValues();
|
||||
if (bitmapInfo.canPersist()) {
|
||||
values.put(IconDB.COLUMN_ICON, flattenBitmap(bitmapInfo.icon));
|
||||
|
||||
// Persist mono bitmap as alpha channel
|
||||
Bitmap mono = bitmapInfo.getMono();
|
||||
if (mono != null && mono.getHeight() == bitmapInfo.icon.getHeight()
|
||||
&& mono.getWidth() == bitmapInfo.icon.getWidth()
|
||||
&& mono.getConfig() == Config.ALPHA_8) {
|
||||
byte[] pixels = new byte[mono.getWidth() * mono.getHeight()];
|
||||
mono.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
|
||||
values.put(IconDB.COLUMN_MONO_ICON, pixels);
|
||||
} else {
|
||||
values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null);
|
||||
}
|
||||
} else {
|
||||
values.put(IconDB.COLUMN_ICON, (byte[]) null);
|
||||
values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null);
|
||||
}
|
||||
values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
|
||||
values.put(IconDB.COLUMN_FLAGS, bitmapInfo.flags);
|
||||
|
||||
values.put(IconDB.COLUMN_LABEL, label);
|
||||
values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
|
||||
values.put(IconDB.COLUMN_KEYWORDS, keywords);
|
||||
return values;
|
||||
}
|
||||
|
||||
private void assertWorkerThread() {
|
||||
if (Looper.myLooper() != mBgLooper) {
|
||||
throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
|
||||
}
|
||||
}
|
||||
}
|
||||
70
iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
vendored
Normal file
70
iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons.cache;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.LocaleList;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
|
||||
public interface CachingLogic<T> {
|
||||
|
||||
@NonNull
|
||||
ComponentName getComponent(@NonNull final T object);
|
||||
|
||||
@NonNull
|
||||
UserHandle getUser(@NonNull final T object);
|
||||
|
||||
@NonNull
|
||||
CharSequence getLabel(@NonNull final T object);
|
||||
|
||||
@NonNull
|
||||
default CharSequence getDescription(@NonNull final T object,
|
||||
@NonNull final CharSequence fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
BitmapInfo loadIcon(@NonNull final Context context, @NonNull final T object);
|
||||
|
||||
/**
|
||||
* Provides a option list of keywords to associate with this object
|
||||
*/
|
||||
@Nullable
|
||||
default String getKeywords(@NonNull final T object, @NonNull final LocaleList localeList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp the entry was last updated in cache.
|
||||
*/
|
||||
default long getLastUpdatedTime(@Nullable final T object, @NonNull final PackageInfo info) {
|
||||
return info.lastUpdateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true the object should be added to mem cache; otherwise returns false.
|
||||
*/
|
||||
default boolean addToMemCache() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
314
iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
vendored
Normal file
314
iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.icons.cache;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.os.SystemClock;
|
||||
import android.os.UserHandle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
|
||||
import com.android.launcher3.icons.cache.BaseIconCache.IconDB;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Utility class to handle updating the Icon cache
|
||||
*/
|
||||
public class IconCacheUpdateHandler {
|
||||
|
||||
private static final String TAG = "IconCacheUpdateHandler";
|
||||
|
||||
/**
|
||||
* In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}.
|
||||
* This mode is used for the first run.
|
||||
*/
|
||||
private static final boolean MODE_SET_INVALID_ITEMS = true;
|
||||
|
||||
/**
|
||||
* In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all
|
||||
* subsequent runs, which essentially acts as set-union of all valid items.
|
||||
*/
|
||||
private static final boolean MODE_CLEAR_VALID_ITEMS = false;
|
||||
|
||||
static final Object ICON_UPDATE_TOKEN = new Object();
|
||||
|
||||
private final HashMap<String, PackageInfo> mPkgInfoMap;
|
||||
private final BaseIconCache mIconCache;
|
||||
|
||||
private final ArrayMap<UserHandle, Set<String>> mPackagesToIgnore = new ArrayMap<>();
|
||||
|
||||
private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray();
|
||||
private boolean mFilterMode = MODE_SET_INVALID_ITEMS;
|
||||
|
||||
IconCacheUpdateHandler(BaseIconCache cache) {
|
||||
mIconCache = cache;
|
||||
|
||||
mPkgInfoMap = new HashMap<>();
|
||||
|
||||
// Remove all active icon update tasks.
|
||||
mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
|
||||
|
||||
createPackageInfoMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a package to ignore for processing
|
||||
*/
|
||||
public void addPackagesToIgnore(UserHandle userHandle, String packageName) {
|
||||
Set<String> packages = mPackagesToIgnore.get(userHandle);
|
||||
if (packages == null) {
|
||||
packages = new HashSet<>();
|
||||
mPackagesToIgnore.put(userHandle, packages);
|
||||
}
|
||||
packages.add(packageName);
|
||||
}
|
||||
|
||||
private void createPackageInfoMap() {
|
||||
PackageManager pm = mIconCache.mPackageManager;
|
||||
for (PackageInfo info :
|
||||
pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) {
|
||||
mPkgInfoMap.put(info.packageName, info);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
|
||||
* the DB and are updated.
|
||||
* @return The set of packages for which icons have updated.
|
||||
*/
|
||||
public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic,
|
||||
OnUpdateCallback onUpdateCallback) {
|
||||
// Filter the list per user
|
||||
HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>();
|
||||
int count = apps.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
T app = apps.get(i);
|
||||
UserHandle userHandle = cachingLogic.getUser(app);
|
||||
HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);
|
||||
if (componentMap == null) {
|
||||
componentMap = new HashMap<>();
|
||||
userComponentMap.put(userHandle, componentMap);
|
||||
}
|
||||
componentMap.put(cachingLogic.getComponent(app), app);
|
||||
}
|
||||
|
||||
for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) {
|
||||
updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
|
||||
}
|
||||
|
||||
// From now on, clear every valid item from the global valid map.
|
||||
mFilterMode = MODE_CLEAR_VALID_ITEMS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
|
||||
* the DB and are updated.
|
||||
* @return The set of packages for which icons have updated.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap,
|
||||
CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) {
|
||||
Set<String> ignorePackages = mPackagesToIgnore.get(user);
|
||||
if (ignorePackages == null) {
|
||||
ignorePackages = Collections.emptySet();
|
||||
}
|
||||
long userSerial = mIconCache.getSerialNumberForUser(user);
|
||||
|
||||
Stack<T> appsToUpdate = new Stack<>();
|
||||
|
||||
try (Cursor c = mIconCache.mIconDb.query(
|
||||
new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
|
||||
IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
|
||||
IconDB.COLUMN_SYSTEM_STATE},
|
||||
IconDB.COLUMN_USER + " = ? ",
|
||||
new String[]{Long.toString(userSerial)})) {
|
||||
|
||||
final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
|
||||
final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
|
||||
final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
|
||||
final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
|
||||
final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
|
||||
|
||||
while (c.moveToNext()) {
|
||||
String cn = c.getString(indexComponent);
|
||||
ComponentName component = ComponentName.unflattenFromString(cn);
|
||||
PackageInfo info = mPkgInfoMap.get(component.getPackageName());
|
||||
|
||||
int rowId = c.getInt(rowIndex);
|
||||
if (info == null) {
|
||||
if (!ignorePackages.contains(component.getPackageName())) {
|
||||
|
||||
if (mFilterMode == MODE_SET_INVALID_ITEMS) {
|
||||
mIconCache.remove(component, user);
|
||||
mItemsToDelete.put(rowId, true);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
|
||||
// Application is not present
|
||||
continue;
|
||||
}
|
||||
|
||||
long updateTime = c.getLong(indexLastUpdate);
|
||||
int version = c.getInt(indexVersion);
|
||||
T app = componentMap.remove(component);
|
||||
if (version == info.versionCode
|
||||
&& updateTime == cachingLogic.getLastUpdatedTime(app, info)
|
||||
&& TextUtils.equals(c.getString(systemStateIndex),
|
||||
mIconCache.getIconSystemState(info.packageName))) {
|
||||
|
||||
if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
|
||||
mItemsToDelete.put(rowId, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (app == null) {
|
||||
if (mFilterMode == MODE_SET_INVALID_ITEMS) {
|
||||
mIconCache.remove(component, user);
|
||||
mItemsToDelete.put(rowId, true);
|
||||
}
|
||||
} else {
|
||||
appsToUpdate.add(app);
|
||||
}
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
Log.d(TAG, "Error reading icon cache", e);
|
||||
// Continue updating whatever we have read so far
|
||||
}
|
||||
|
||||
// Insert remaining apps.
|
||||
if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
|
||||
Stack<T> appsToAdd = new Stack<>();
|
||||
appsToAdd.addAll(componentMap.values());
|
||||
new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
|
||||
onUpdateCallback).scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits all updates as part of the update handler to disk. Not more calls should be made
|
||||
* to this class after this.
|
||||
*/
|
||||
public void finish() {
|
||||
// Commit all deletes
|
||||
int deleteCount = 0;
|
||||
StringBuilder queryBuilder = new StringBuilder()
|
||||
.append(IconDB.COLUMN_ROWID)
|
||||
.append(" IN (");
|
||||
|
||||
int count = mItemsToDelete.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (mItemsToDelete.valueAt(i)) {
|
||||
if (deleteCount > 0) {
|
||||
queryBuilder.append(", ");
|
||||
}
|
||||
queryBuilder.append(mItemsToDelete.keyAt(i));
|
||||
deleteCount++;
|
||||
}
|
||||
}
|
||||
queryBuilder.append(')');
|
||||
|
||||
if (deleteCount > 0) {
|
||||
mIconCache.mIconDb.delete(queryBuilder.toString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A runnable that updates invalid icons and adds missing icons in the DB for the provided
|
||||
* LauncherActivityInfo list. Items are updated/added one at a time, so that the
|
||||
* worker thread doesn't get blocked.
|
||||
*/
|
||||
private class SerializedIconUpdateTask<T> implements Runnable {
|
||||
private final long mUserSerial;
|
||||
private final UserHandle mUserHandle;
|
||||
private final Stack<T> mAppsToAdd;
|
||||
private final Stack<T> mAppsToUpdate;
|
||||
private final CachingLogic<T> mCachingLogic;
|
||||
private final HashSet<String> mUpdatedPackages = new HashSet<>();
|
||||
private final OnUpdateCallback mOnUpdateCallback;
|
||||
|
||||
SerializedIconUpdateTask(long userSerial, UserHandle userHandle,
|
||||
Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic,
|
||||
OnUpdateCallback onUpdateCallback) {
|
||||
mUserHandle = userHandle;
|
||||
mUserSerial = userSerial;
|
||||
mAppsToAdd = appsToAdd;
|
||||
mAppsToUpdate = appsToUpdate;
|
||||
mCachingLogic = cachingLogic;
|
||||
mOnUpdateCallback = onUpdateCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!mAppsToUpdate.isEmpty()) {
|
||||
T app = mAppsToUpdate.pop();
|
||||
String pkg = mCachingLogic.getComponent(app).getPackageName();
|
||||
PackageInfo info = mPkgInfoMap.get(pkg);
|
||||
|
||||
mIconCache.addIconToDBAndMemCache(
|
||||
app, mCachingLogic, info, mUserSerial, true /*replace existing*/);
|
||||
mUpdatedPackages.add(pkg);
|
||||
|
||||
if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
|
||||
// No more app to update. Notify callback.
|
||||
mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle);
|
||||
}
|
||||
|
||||
// Let it run one more time.
|
||||
scheduleNext();
|
||||
} else if (!mAppsToAdd.isEmpty()) {
|
||||
T app = mAppsToAdd.pop();
|
||||
PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
|
||||
// We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
|
||||
// app should have package info, this is not guaranteed by the api
|
||||
if (info != null) {
|
||||
mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
|
||||
mUserSerial, false /*replace existing*/);
|
||||
}
|
||||
|
||||
if (!mAppsToAdd.isEmpty()) {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void scheduleNext() {
|
||||
mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
|
||||
SystemClock.uptimeMillis() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnUpdateCallback {
|
||||
|
||||
void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.android.launcher3.util;
|
||||
|
||||
/**
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ComponentKey {
|
||||
|
||||
public final ComponentName componentName;
|
||||
public final UserHandle user;
|
||||
|
||||
private final int mHashCode;
|
||||
|
||||
public ComponentKey(ComponentName componentName, UserHandle user) {
|
||||
if (componentName == null || user == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.componentName = componentName;
|
||||
this.user = user;
|
||||
mHashCode = Arrays.hashCode(new Object[] {componentName, user});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
ComponentKey other = (ComponentKey) o;
|
||||
return other.componentName.equals(componentName) && other.user.equals(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a component key as a string of the form [flattenedComponentString#userId].
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return componentName.flattenToString() + "#" + user.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and returns ComponentKey objected from string representation
|
||||
* Returns null if string is not properly formatted
|
||||
*/
|
||||
@Nullable
|
||||
public static ComponentKey fromString(@NonNull String str) {
|
||||
int sep = str.indexOf('#');
|
||||
if (sep < 0 || (sep + 1) >= str.length()) {
|
||||
return null;
|
||||
}
|
||||
ComponentName componentName = ComponentName.unflattenFromString(str.substring(0, sep));
|
||||
if (componentName == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new ComponentKey(componentName,
|
||||
UserHandle.getUserHandleForUid(Integer.parseInt(str.substring(sep + 1))));
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
iconloaderlib/src/com/android/launcher3/util/FlagOp.java
Normal file
48
iconloaderlib/src/com/android/launcher3/util/FlagOp.java
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.util;
|
||||
|
||||
/**
|
||||
* Utility interface for representing flag operations
|
||||
*/
|
||||
public interface FlagOp {
|
||||
|
||||
FlagOp NO_OP = i -> i;
|
||||
|
||||
int apply(int flags);
|
||||
|
||||
/**
|
||||
* Returns a new OP which adds the provided flag after applying all previous operations
|
||||
*/
|
||||
default FlagOp addFlag(int flag) {
|
||||
return i -> apply(i) | flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new OP which removes the provided flag after applying all previous operations
|
||||
*/
|
||||
default FlagOp removeFlag(int flag) {
|
||||
return i -> apply(i) & ~flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new OP which adds or removed the provided flag based on {@code enable} after
|
||||
* applying all previous operations
|
||||
*/
|
||||
default FlagOp setFlag(int flag, boolean enable) {
|
||||
return enable ? addFlag(flag) : removeFlag(flag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.util;
|
||||
|
||||
import static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.database.DatabaseErrorHandler;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteDatabase.CursorFactory;
|
||||
import android.database.sqlite.SQLiteDatabase.OpenParams;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Build;
|
||||
|
||||
/**
|
||||
* Extension of {@link SQLiteOpenHelper} which avoids creating default locale table by
|
||||
* A context wrapper which creates databases without support for localized collators.
|
||||
*/
|
||||
public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final boolean ATLEAST_P =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
|
||||
|
||||
public NoLocaleSQLiteHelper(Context context, String name, int version) {
|
||||
super(ATLEAST_P ? context : new NoLocalContext(context), name, null, version);
|
||||
if (ATLEAST_P) {
|
||||
setOpenParams(new OpenParams.Builder().addOpenFlags(NO_LOCALIZED_COLLATORS).build());
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoLocalContext extends ContextWrapper {
|
||||
public NoLocalContext(Context base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLiteDatabase openOrCreateDatabase(
|
||||
String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
|
||||
return super.openOrCreateDatabase(
|
||||
name, mode | Context.MODE_NO_LOCALIZED_COLLATORS, factory, errorHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.android.launcher3.util;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteFullException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB.
|
||||
* Any exception during write operations are ignored, and any version change causes a DB reset.
|
||||
*/
|
||||
public abstract class SQLiteCacheHelper {
|
||||
private static final String TAG = "SQLiteCacheHelper";
|
||||
|
||||
private static final boolean IN_MEMORY_CACHE = false;
|
||||
|
||||
private final String mTableName;
|
||||
private final MySQLiteOpenHelper mOpenHelper;
|
||||
|
||||
private boolean mIgnoreWrites;
|
||||
|
||||
public SQLiteCacheHelper(Context context, String name, int version, String tableName) {
|
||||
if (IN_MEMORY_CACHE) {
|
||||
name = null;
|
||||
}
|
||||
mTableName = tableName;
|
||||
mOpenHelper = new MySQLiteOpenHelper(context, name, version);
|
||||
|
||||
mIgnoreWrites = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SQLiteDatabase#delete(String, String, String[])
|
||||
*/
|
||||
public void delete(String whereClause, String[] whereArgs) {
|
||||
if (mIgnoreWrites) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mOpenHelper.getWritableDatabase().delete(mTableName, whereClause, whereArgs);
|
||||
} catch (SQLiteFullException e) {
|
||||
onDiskFull(e);
|
||||
} catch (SQLiteException e) {
|
||||
Log.d(TAG, "Ignoring sqlite exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SQLiteDatabase#insertWithOnConflict(String, String, ContentValues, int)
|
||||
*/
|
||||
public void insertOrReplace(ContentValues values) {
|
||||
if (mIgnoreWrites) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mOpenHelper.getWritableDatabase().insertWithOnConflict(
|
||||
mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
} catch (SQLiteFullException e) {
|
||||
onDiskFull(e);
|
||||
} catch (SQLiteException e) {
|
||||
Log.d(TAG, "Ignoring sqlite exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDiskFull(SQLiteFullException e) {
|
||||
Log.e(TAG, "Disk full, all write operations will be ignored", e);
|
||||
mIgnoreWrites = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SQLiteDatabase#query(String, String[], String, String[], String, String, String)
|
||||
*/
|
||||
public Cursor query(String[] columns, String selection, String[] selectionArgs) {
|
||||
return mOpenHelper.getReadableDatabase().query(
|
||||
mTableName, columns, selection, selectionArgs, null, null, null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
|
||||
}
|
||||
|
||||
public void close() {
|
||||
mOpenHelper.close();
|
||||
}
|
||||
|
||||
protected abstract void onCreateTable(SQLiteDatabase db);
|
||||
|
||||
/**
|
||||
* A private inner class to prevent direct DB access.
|
||||
*/
|
||||
private class MySQLiteOpenHelper extends NoLocaleSQLiteHelper {
|
||||
|
||||
public MySQLiteOpenHelper(Context context, String name, int version) {
|
||||
super(context, name, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onCreateTable(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion != newVersion) {
|
||||
clearDB(db);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion != newVersion) {
|
||||
clearDB(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDB(SQLiteDatabase db) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + mTableName);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.util;
|
||||
|
||||
/**
|
||||
* Extension of closeable which does not throw an exception
|
||||
*/
|
||||
public interface SafeCloseable extends AutoCloseable {
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.launcher3.util;
|
||||
|
||||
import static com.android.launcher3.icons.BitmapInfo.FLAG_CLONE;
|
||||
import static com.android.launcher3.icons.BitmapInfo.FLAG_PRIVATE;
|
||||
import static com.android.launcher3.icons.BitmapInfo.FLAG_WORK;
|
||||
|
||||
import android.os.UserHandle;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Data class which stores various properties of a {@link android.os.UserHandle}
|
||||
* which affects rendering
|
||||
*/
|
||||
public class UserIconInfo {
|
||||
|
||||
public static final int TYPE_MAIN = 0;
|
||||
public static final int TYPE_WORK = 1;
|
||||
public static final int TYPE_CLONED = 2;
|
||||
|
||||
public static final int TYPE_PRIVATE = 3;
|
||||
|
||||
@IntDef({TYPE_MAIN, TYPE_WORK, TYPE_CLONED, TYPE_PRIVATE})
|
||||
public @interface UserType { }
|
||||
|
||||
public final UserHandle user;
|
||||
@UserType
|
||||
public final int type;
|
||||
|
||||
public final long userSerial;
|
||||
|
||||
public UserIconInfo(UserHandle user, @UserType int type) {
|
||||
this(user, type, 0);
|
||||
}
|
||||
|
||||
public UserIconInfo(UserHandle user, @UserType int type, long userSerial) {
|
||||
this.user = user;
|
||||
this.type = type;
|
||||
this.userSerial = userSerial;
|
||||
}
|
||||
|
||||
public boolean isMain() {
|
||||
return type == TYPE_MAIN;
|
||||
}
|
||||
|
||||
public boolean isWork() {
|
||||
return type == TYPE_WORK;
|
||||
}
|
||||
|
||||
public boolean isCloned() {
|
||||
return type == TYPE_CLONED;
|
||||
}
|
||||
|
||||
public boolean isPrivate() {
|
||||
return type == TYPE_PRIVATE;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public FlagOp applyBitmapInfoFlags(@NonNull FlagOp op) {
|
||||
return op.setFlag(FLAG_WORK, isWork())
|
||||
.setFlag(FLAG_CLONE, isCloned())
|
||||
.setFlag(FLAG_PRIVATE, isPrivate());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user