change package name to uiuios

This commit is contained in:
2020-11-02 15:31:36 +08:00
parent 164c1fc8a0
commit c5bda0953d
645 changed files with 3881 additions and 3881 deletions

View File

@@ -0,0 +1,351 @@
package com.android.uiuios.icons;
import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static com.android.uiuios.icons.ShadowGenerator.BLUR_FACTOR;
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.Canvas;
import android.graphics.Color;
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.os.Build;
import android.os.Process;
import android.os.UserHandle;
/**
* 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 String TAG = "BaseIconFactory";
private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
private final Rect mOldBounds = new Rect();
protected final Context mContext;
private final Canvas mCanvas;
private final PackageManager mPm;
private final ColorExtractor mColorExtractor;
private boolean mDisableColorExtractor;
protected final int mFillResIconDpi;
protected final int mIconBitmapSize;
private IconNormalizer mNormalizer;
private ShadowGenerator mShadowGenerator;
private final boolean mShapeDetection;
private Drawable mWrapperIcon;
private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
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();
}
protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
this(context, fillResIconDpi, iconBitmapSize, false);
}
protected void clear() {
mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
mDisableColorExtractor = false;
}
public ShadowGenerator getShadowGenerator() {
if (mShadowGenerator == null) {
mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
}
return mShadowGenerator;
}
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),
Process.myUserHandle() /* only available on primary user */,
false /* do not apply legacy treatment */);
}
} catch (Exception e) {
// Icon not found.
}
return null;
}
public BitmapInfo createIconBitmap(Bitmap icon) {
if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
}
return BitmapInfo.fromBitmap(icon, mDisableColorExtractor ? null : mColorExtractor);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
boolean shrinkNonAdaptiveIcons) {
return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
int iconAppTargetSdk) {
return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
int iconAppTargetSdk, boolean isInstantApp) {
return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
(ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
}
public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
(ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
}
/**
* 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
* @param user info can be used for a badge
* @param shrinkNonAdaptiveIcons {@code true} if non adaptive icons should be treated
* @param isInstantApp info can be used for a badge
* @param scale returns the scale result from normalization
* @return a bitmap suitable for disaplaying as an icon at various system UIs.
*/
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
if (scale == null) {
scale = new float[1];
}
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
Bitmap bitmap = createIconBitmap(icon, scale[0]);
if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
mCanvas.setBitmap(bitmap);
getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
mCanvas.setBitmap(null);
}
if (isInstantApp) {
badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
}
if (user != null) {
BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
Drawable badged = mPm.getUserBadgedIcon(drawable, user);
if (badged instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) badged).getBitmap();
} else {
bitmap = createIconBitmap(badged, 1f);
}
}
return BitmapInfo.fromBitmap(bitmap, mDisableColorExtractor ? null : mColorExtractor);
}
public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
RectF iconBounds = new RectF();
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale);
return createIconBitmap(icon,
Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
}
/**
* Sets the background color used for wrapped adaptive icon
*/
public void setWrapperBackgroundColor(int color) {
mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
}
/**
* Disables the dominant color extraction for all icons loaded.
*/
public void disableColorExtraction() {
mDisableColorExtractor = true;
}
private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, boolean shrinkNonAdaptiveIcons,
RectF outIconBounds, float[] outScale) {
float scale = 1f;
if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
if (mWrapperIcon == null) {
mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
.mutate();
}
AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
fsd.setDrawable(icon);
fsd.setScale(scale);
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
}
} else {
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
}
outScale[0] = scale;
return icon;
}
/**
* Adds the {@param badge} on top of {@param target} using the badge dimensions.
*/
public void badgeWithDrawable(Bitmap target, Drawable badge) {
mCanvas.setBitmap(target);
badgeWithDrawable(mCanvas, badge);
mCanvas.setBitmap(null);
}
/**
* Adds the {@param badge} on top of {@param target} using the badge dimensions.
*/
public void badgeWithDrawable(Canvas target, Drawable badge) {
int badgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size);
badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
mIconBitmapSize, mIconBitmapSize);
badge.draw(target);
}
private Bitmap createIconBitmap(Drawable icon, float scale) {
return createIconBitmap(icon, scale, mIconBitmapSize);
}
/**
* @param icon drawable that should be flattened to a bitmap
* @param scale the scale to apply before drawing {@param icon} on the canvas
*/
public Bitmap createIconBitmap(Drawable icon, float scale, int size) {
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
if (icon == null) {
return bitmap;
}
mCanvas.setBitmap(bitmap);
mOldBounds.set(icon.getBounds());
if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
Math.round(size * (1 - scale) / 2 ));
icon.setBounds(offset, offset, size - offset, size - offset);
icon.draw(mCanvas);
} else {
if (icon instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
Bitmap b = bitmapDrawable.getBitmap();
if (bitmap != 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);
mCanvas.save();
mCanvas.scale(scale, scale, size / 2, size / 2);
icon.draw(mCanvas);
mCanvas.restore();
}
icon.setBounds(mOldBounds);
mCanvas.setBitmap(null);
return bitmap;
}
@Override
public void close() {
clear();
}
public BitmapInfo makeDefaultIcon(UserHandle user) {
return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi),
user, Build.VERSION.SDK_INT);
}
public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
return Resources.getSystem().getDrawableForDensity(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
iconDpi);
}
/**
* 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(Bitmap bitmap) {
super(null, bitmap);
}
@Override
public int getIntrinsicHeight() {
return getBitmap().getWidth();
}
@Override
public int getIntrinsicWidth() {
return getBitmap().getWidth();
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.uiuios.icons;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
public class BitmapInfo {
public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
public Bitmap icon;
public int color;
public void applyTo(BitmapInfo info) {
info.icon = icon;
info.color = color;
}
public final boolean isLowRes() {
return LOW_RES_ICON == icon;
}
public static BitmapInfo fromBitmap(Bitmap bitmap) {
return fromBitmap(bitmap, null);
}
public static BitmapInfo fromBitmap(Bitmap bitmap, ColorExtractor dominantColorExtractor) {
BitmapInfo info = new BitmapInfo();
info.icon = bitmap;
info.color = dominantColorExtractor != null
? dominantColorExtractor.findDominantColorByHue(bitmap)
: 0;
return info;
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.uiuios.icons;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Picture;
import android.os.Build;
/**
* 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) {
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);
}
Picture picture = new Picture();
renderer.draw(picture.beginRecording(width, height));
picture.endRecording();
return Bitmap.createBitmap(picture);
}
void draw(Canvas out);
}

View File

@@ -0,0 +1,127 @@
/*
* 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.uiuios.icons;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.SparseArray;
import java.util.Arrays;
/**
* Utility class for extracting colors from a bitmap.
*/
public class ColorExtractor {
private final int NUM_SAMPLES = 20;
private final float[] mTmpHsv = new float[3];
private final float[] mTmpHueScoreHistogram = new float[360];
private final int[] mTmpPixels = new int[NUM_SAMPLES];
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(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
*/
public int findDominantColorByHue(Bitmap bitmap, 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;
}
}

View File

@@ -0,0 +1,143 @@
/*
* 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.uiuios.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;
public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
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.color);
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 color;
/** 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;
}
}

View File

@@ -0,0 +1,53 @@
package com.android.uiuios.icons;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.DrawableWrapper;
import android.util.AttributeSet;
import org.xmlpull.v1.XmlPullParser;
/**
* Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
*/
public class FixedScaleDrawable extends DrawableWrapper {
// TODO b/33553066 use the constant defined in MaskableIconDrawable
private static final float LEGACY_ICON_SCALE = .7f * .6667f;
private float mScaleX, mScaleY;
public FixedScaleDrawable() {
super(new ColorDrawable());
mScaleX = LEGACY_ICON_SCALE;
mScaleY = LEGACY_ICON_SCALE;
}
@Override
public void draw(Canvas canvas) {
int saveCount = canvas.save();
canvas.scale(mScaleX, mScaleY,
getBounds().exactCenterX(), getBounds().exactCenterY());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
public void setScale(float scale) {
float h = getIntrinsicHeight();
float w = getIntrinsicWidth();
mScaleX = scale * LEGACY_ICON_SCALE;
mScaleY = scale * LEGACY_ICON_SCALE;
if (h > w && w > 0) {
mScaleX *= w / h;
} else if (w > h && h > 0) {
mScaleY *= h / w;
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.uiuios.icons;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import androidx.annotation.ColorInt;
public class GraphicsUtils {
private static final String TAG = "GraphicsUtils";
/**
* 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) {
// Try go guesstimate how much space the icon will take when serialized
// to avoid unnecessary allocations/copies during the write (4 bytes per pixel).
int size = bitmap.getWidth() * bitmap.getHeight() * 4;
ByteArrayOutputStream out = new ByteArrayOutputStream(size);
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;
}
}
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;
}
}

View File

@@ -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.uiuios.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 (BaseIconFactory.ATLEAST_OREO && 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));
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.uiuios.icons;
import static com.android.uiuios.icons.GraphicsUtils.setColorAlphaBound;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BlurMaskFilter;
import android.graphics.BlurMaskFilter.Blur;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
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 float BLUR_FACTOR = 0.5f/48;
// Percent of actual icon size
public static final float KEY_SHADOW_DISTANCE = 1f/48;
private static final int KEY_SHADOW_ALPHA = 61;
// Percent of actual icon size
private static final float HALF_DISTANCE = 0.5f;
private static final int AMBIENT_SHADOW_ALPHA = 30;
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 recreateIcon(Bitmap icon, Canvas out) {
recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
}
public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
int ambientAlpha, int keyAlpha, Canvas out) {
int[] offset = new int[2];
mBlurPaint.setMaskFilter(blurMaskFilter);
Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
// Draw ambient shadow
mDrawPaint.setAlpha(ambientAlpha);
out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
// Draw key shadow
mDrawPaint.setAlpha(keyAlpha);
out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize, mDrawPaint);
// Draw the icon
mDrawPaint.setAlpha(255);
out.drawBitmap(icon, 0, 0, mDrawPaint);
}
/**
* 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;
// 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);
}
float bottomSpace = BLUR_FACTOR + KEY_SHADOW_DISTANCE;
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) {
shadowBlur = height * 1f / 24;
keyShadowDistance = height * 1f / 16;
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;
Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888);
drawShadow(new Canvas(result));
return result;
}
public void drawShadow(Canvas c) {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
p.setColor(color);
// 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);
}
}
}
}

View File

@@ -0,0 +1,563 @@
/*
* 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.uiuios.icons.cache;
import static com.android.uiuios.icons.BaseIconFactory.getFullResDefaultActivityIcon;
import static com.android.uiuios.icons.BitmapInfo.LOW_RES_ICON;
import static com.android.uiuios.icons.GraphicsUtils.setColorAlphaBound;
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.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import com.android.uiuios.icons.BaseIconFactory;
import com.android.uiuios.icons.BitmapInfo;
import com.android.uiuios.icons.BitmapRenderer;
import com.android.uiuios.icons.GraphicsUtils;
import com.android.uiuios.util.ComponentKey;
import com.android.uiuios.util.SQLiteCacheHelper;
import java.util.AbstractMap;
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;
import androidx.annotation.NonNull;
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;
// Empty class name is used for storing package default entry.
public static final String EMPTY_CLASS_NAME = ".";
public static class CacheEntry extends BitmapInfo {
public CharSequence title = "";
public CharSequence contentDescription = "";
}
private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>();
protected final Context mContext;
protected final PackageManager mPackageManager;
private final Map<ComponentKey, CacheEntry> mCache;
protected final Handler mWorkerHandler;
protected int mIconDpi;
protected IconDB mIconDb;
protected String mSystemState = "";
private final String mDbFileName;
private final BitmapFactory.Options mDecodeOptions;
private final Looper mBgLooper;
public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
int iconDpi, int iconPixelSize, 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;
}
};
}
if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDecodeOptions = new BitmapFactory.Options();
mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE;
} else {
mDecodeOptions = null;
}
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(UserHandle user);
/**
* Return true if the given app is an instant app and should be badged appropriately.
*/
protected abstract boolean isInstantApp(ApplicationInfo info);
/**
* Opens and returns an icon factory. The factory is recycled by the caller.
*/
protected abstract BaseIconFactory getIconFactory();
public void updateIconParams(int iconDpi, int iconPixelSize) {
mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
}
private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
mIconDpi = iconDpi;
mDefaultIcons.clear();
mIconDb.clear();
mIconDb.close();
mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
mCache.clear();
}
private Drawable getFullResIcon(Resources resources, int iconId) {
if (resources != null && iconId != 0) {
try {
return resources.getDrawableForDensity(iconId, mIconDpi);
} catch (Resources.NotFoundException e) { }
}
return getFullResDefaultActivityIcon(mIconDpi);
}
public Drawable getFullResIcon(String packageName, int iconId) {
try {
return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
} catch (PackageManager.NameNotFoundException e) { }
return getFullResDefaultActivityIcon(mIconDpi);
}
public Drawable getFullResIcon(ActivityInfo info) {
try {
return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
info.getIconResource());
} catch (PackageManager.NameNotFoundException e) { }
return getFullResDefaultActivityIcon(mIconDpi);
}
private BitmapInfo makeDefaultIcon(UserHandle user) {
try (BaseIconFactory li = getIconFactory()) {
return li.makeDefaultIcon(user);
}
}
/**
* Remove any records for the supplied ComponentName.
*/
public synchronized void remove(ComponentName componentName, UserHandle user) {
mCache.remove(new ComponentKey(componentName, user));
}
/**
* Remove any records for the supplied package name from memory.
*/
private void removeFromMemCacheLocked(String packageName, 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(String packageName, 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)});
}
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 locale and system-version.
*/
private void updateSystemState() {
final String locale =
mContext.getResources().getConfiguration().getLocales().toLanguageTags();
mSystemState = locale + "," + Build.VERSION.SDK_INT;
}
protected String getIconSystemState(String packageName) {
return mSystemState;
}
/**
* 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.
* package private
*/
protected synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
PackageInfo info, long userSerial, 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.icon == null || entry.isLowRes()) {
entry = null;
}
}
if (entry == null) {
entry = new CacheEntry();
cachingLogic.loadIcon(mContext, object, entry);
}
entry.title = cachingLogic.getLabel(object);
entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
mCache.put(key, entry);
ContentValues values = newContentValues(entry, entry.title.toString(),
componentName.getPackageName());
addIconToDB(values, componentName, info, userSerial);
}
/**
* Updates {@param values} to contain versioning information and adds it to the DB.
* @param values {@link ContentValues} containing icon & title
*/
private void addIconToDB(ContentValues values, ComponentName key,
PackageInfo info, long userSerial) {
values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
values.put(IconDB.COLUMN_USER, userSerial);
values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
values.put(IconDB.COLUMN_VERSION, info.versionCode);
mIconDb.insertOrReplace(values);
}
public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
if (!mDefaultIcons.containsKey(user)) {
mDefaultIcons.put(user, makeDefaultIcon(user));
}
return mDefaultIcons.get(user);
}
public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
return getDefaultIcon(user).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.
*/
protected <T> CacheEntry cacheLocked(
@NonNull ComponentName componentName, @NonNull UserHandle user,
@NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
boolean usePackageIcon, boolean useLowResIcon) {
return cacheLocked(componentName, user, infoProvider, cachingLogic, usePackageIcon,
useLowResIcon, true);
}
protected <T> CacheEntry cacheLocked(
@NonNull ComponentName componentName, @NonNull UserHandle user,
@NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
boolean usePackageIcon, boolean useLowResIcon, boolean addToMemCache) {
assertWorkerThread();
ComponentKey cacheKey = new ComponentKey(componentName, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
entry = new CacheEntry();
if (addToMemCache) {
mCache.put(cacheKey, entry);
}
// Check the DB first.
T object = null;
boolean providerFetchedOnce = false;
if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
object = infoProvider.get();
providerFetchedOnce = true;
if (object != null) {
cachingLogic.loadIcon(mContext, object, entry);
} else {
if (usePackageIcon) {
CacheEntry packageEntry = getEntryForPackageLocked(
componentName.getPackageName(), user, false);
if (packageEntry != null) {
if (DEBUG) Log.d(TAG, "using package default icon for " +
componentName.toShortString());
packageEntry.applyTo(entry);
entry.title = packageEntry.title;
entry.contentDescription = packageEntry.contentDescription;
}
}
if (entry.icon == null) {
if (DEBUG) Log.d(TAG, "using default icon for " +
componentName.toShortString());
getDefaultIcon(user).applyTo(entry);
}
}
}
if (TextUtils.isEmpty(entry.title)) {
if (object == null && !providerFetchedOnce) {
object = infoProvider.get();
providerFetchedOnce = true;
}
if (object != null) {
entry.title = cachingLogic.getLabel(object);
entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
}
}
}
return entry;
}
public synchronized void clear() {
assertWorkerThread();
mIconDb.clear();
}
/**
* Adds a default package entry in the cache. This entry is not persisted and will be removed
* when the cache is flushed.
*/
public synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
Bitmap icon, 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();
li.createIconBitmap(icon).applyTo(entry);
li.close();
}
if (!TextUtils.isEmpty(title) && entry.icon != null) {
mCache.put(cacheKey, entry);
}
}
private static ComponentKey getPackageKey(String packageName, 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.
*/
protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
boolean useLowResIcon) {
assertWorkerThread();
ComponentKey cacheKey = getPackageKey(packageName, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
entry = new CacheEntry();
boolean entryUpdated = true;
// Check the DB first.
if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
try {
int flags = Process.myUserHandle().equals(user) ? 0 :
PackageManager.GET_UNINSTALLED_PACKAGES;
PackageInfo info = mPackageManager.getPackageInfo(packageName, 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), user, appInfo.targetSdkVersion,
isInstantApp(appInfo));
li.close();
entry.title = appInfo.loadLabel(mPackageManager);
entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon;
entry.color = 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);
addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user));
} 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;
}
private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
Cursor c = null;
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()) {
// Set the alpha to be 255, so that we never have a wrong color
entry.color = setColorAlphaBound(c.getInt(0), 255);
entry.title = c.getString(1);
if (entry.title == null) {
entry.title = "";
entry.contentDescription = "";
} else {
entry.contentDescription = mPackageManager.getUserBadgedLabel(
entry.title, cacheKey.user);
}
if (lowRes) {
entry.icon = LOW_RES_ICON;
} else {
byte[] data = c.getBlob(2);
try {
entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length,
mDecodeOptions);
} catch (Exception e) { }
}
return true;
}
} catch (SQLiteException e) {
Log.d(TAG, "Error reading icon cache", e);
} finally {
if (c != null) {
c.close();
}
}
return false;
}
static final class IconDB extends SQLiteCacheHelper {
private final static int RELEASE_VERSION = 26;
public final static String TABLE_NAME = "icons";
public final static String COLUMN_ROWID = "rowid";
public final static String COLUMN_COMPONENT = "componentName";
public final static String COLUMN_USER = "profileId";
public final static String COLUMN_LAST_UPDATED = "lastUpdated";
public final static String COLUMN_VERSION = "version";
public final static String COLUMN_ICON = "icon";
public final static String COLUMN_ICON_COLOR = "icon_color";
public final static String COLUMN_LABEL = "label";
public final static String COLUMN_SYSTEM_STATE = "system_state";
public final static String[] COLUMNS_HIGH_RES = new String[] {
IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
public final static String[] COLUMNS_LOW_RES = new String[] {
IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL };
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_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " +
COLUMN_LABEL + " TEXT, " +
COLUMN_SYSTEM_STATE + " TEXT, " +
"PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
");");
}
}
private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, String packageName) {
ContentValues values = new ContentValues();
values.put(IconDB.COLUMN_ICON,
bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon));
values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
values.put(IconDB.COLUMN_LABEL, label);
values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
return values;
}
private void assertWorkerThread() {
if (Looper.myLooper() != mBgLooper) {
throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.uiuios.icons.cache;
import android.content.ComponentName;
import android.content.Context;
import android.os.UserHandle;
import com.android.uiuios.icons.BitmapInfo;
public interface CachingLogic<T> {
ComponentName getComponent(T object);
UserHandle getUser(T object);
CharSequence getLabel(T object);
void loadIcon(Context context, T object, BitmapInfo target);
}

View File

@@ -0,0 +1,67 @@
/*
* 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.uiuios.icons.cache;
import android.os.Handler;
/**
* A runnable that can be posted to a {@link Handler} which can be canceled.
*/
public abstract class HandlerRunnable implements Runnable {
private final Handler mHandler;
private final Runnable mEndRunnable;
private boolean mEnded = false;
private boolean mCanceled = false;
public HandlerRunnable(Handler handler, Runnable endRunnable) {
mHandler = handler;
mEndRunnable = endRunnable;
}
/**
* Cancels this runnable from being run, only if it has not already run.
*/
public void cancel() {
mHandler.removeCallbacks(this);
// TODO: This can actually cause onEnd to be called twice if the handler is already running
// this runnable
// NOTE: This is currently run on whichever thread the caller is run on.
mCanceled = true;
onEnd();
}
/**
* @return whether this runnable was canceled.
*/
protected boolean isCanceled() {
return mCanceled;
}
/**
* To be called by the implemention of this runnable. The end callback is done on whichever
* thread the caller is calling from.
*/
public void onEnd() {
if (!mEnded) {
mEnded = true;
if (mEndRunnable != null) {
mEndRunnable.run();
}
}
}
}

View File

@@ -0,0 +1,303 @@
/*
* 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.uiuios.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.Log;
import android.util.SparseBooleanArray;
import com.android.uiuios.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;
private static final Object ICON_UPDATE_TOKEN = new Object();
private final HashMap<String, PackageInfo> mPkgInfoMap;
private final BaseIconCache mIconCache;
private final HashMap<UserHandle, Set<String>> mPackagesToIgnore = new HashMap<>();
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();
}
public void setPackagesToIgnore(UserHandle userHandle, Set<String> packages) {
mPackagesToIgnore.put(userHandle, packages);
}
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 == info.lastUpdateTime &&
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);
}
}

View File

@@ -0,0 +1,59 @@
package com.android.uiuios.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 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;
}
}

View File

@@ -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.uiuios.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);
}
}
}

View File

@@ -0,0 +1,125 @@
package com.android.uiuios.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);
}
}
}