blob: 29326ec90e3188507895c92efbb0b2f646471861 [file] [log] [blame]
/*
* Copyright (C) 2020 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.wm.shell.startingsurface;
import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Trace;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.View;
import android.window.SplashScreenView;
import android.window.StartingWindowInfo.StartingWindowType;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.Quantizer;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.common.TransactionPool;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.IntPredicate;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
/**
* Util class to create the view for a splash screen content.
* Everything execute in this class should be post to mSplashscreenWorkerHandler.
* @hide
*/
public class SplashscreenContentDrawer {
private static final String TAG = StartingSurfaceDrawer.TAG;
private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN;
// The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
// icon which it's non-transparent foreground area is similar to it's background area, then
// do not enlarge the foreground drawable.
// For example, an icon with the foreground 108*108 opaque pixels and it's background
// also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
/**
* If the developer doesn't specify a background for the icon, we slightly scale it up.
*
* The background is either manually specified in the theme or the Adaptive Icon
* background is used if it's different from the window background.
*/
private static final float NO_BACKGROUND_SCALE = 192f / 160;
private final Context mContext;
private final IconProvider mIconProvider;
private int mIconSize;
private int mDefaultIconSize;
private int mBrandingImageWidth;
private int mBrandingImageHeight;
private int mMainWindowShiftLength;
private int mLastPackageContextConfigHash;
private final TransactionPool mTransactionPool;
private final SplashScreenWindowAttrs mTmpAttrs = new SplashScreenWindowAttrs();
private final Handler mSplashscreenWorkerHandler;
@VisibleForTesting
final ColorCache mColorCache;
SplashscreenContentDrawer(Context context, TransactionPool pool) {
mContext = context;
mIconProvider = new IconProvider(context);
mTransactionPool = pool;
// Initialize Splashscreen worker thread
// TODO(b/185288910) move it into WMShellConcurrencyModule and provide an executor to make
// it easier to test stuff that happens on that thread later.
final HandlerThread shellSplashscreenWorkerThread =
new HandlerThread("wmshell.splashworker", THREAD_PRIORITY_TOP_APP_BOOST);
shellSplashscreenWorkerThread.start();
mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler();
mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler);
}
/**
* Create a SplashScreenView object.
*
* In order to speed up the splash screen view to show on first frame, preparing the
* view on background thread so the view and the drawable can be create and pre-draw in
* parallel.
*
* @param suggestType Suggest type to create the splash screen view.
* @param splashScreenViewConsumer Receiving the SplashScreenView object, which will also be
* executed on splash screen thread. Note that the view can be
* null if failed.
*/
void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info,
int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) {
mSplashscreenWorkerHandler.post(() -> {
SplashScreenView contentView;
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView");
contentView = makeSplashScreenContentView(context, info, suggestType);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
} catch (RuntimeException e) {
Slog.w(TAG, "failed creating starting window content at taskId: "
+ taskId, e);
contentView = null;
}
splashScreenViewConsumer.accept(contentView);
});
}
private void updateDensity() {
mIconSize = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.starting_surface_icon_size);
mDefaultIconSize = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.starting_surface_default_icon_size);
mBrandingImageWidth = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.starting_surface_brand_image_width);
mBrandingImageHeight = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.starting_surface_brand_image_height);
mMainWindowShiftLength = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.starting_surface_exit_animation_window_shift_length);
}
/**
* @return Current system background color.
*/
public static int getSystemBGColor() {
final Context systemContext = ActivityThread.currentApplication();
if (systemContext == null) {
Slog.e(TAG, "System context does not exist!");
return Color.BLACK;
}
final Resources res = systemContext.getResources();
return res.getColor(com.android.wm.shell.R.color.splash_window_background_default);
}
/**
* Estimate the background color of the app splash screen, this may take a while so use it only
* if there is no starting window exists for that context.
**/
int estimateTaskBackgroundColor(Context context) {
final SplashScreenWindowAttrs windowAttrs = new SplashScreenWindowAttrs();
getWindowAttrs(context, windowAttrs);
return peekWindowBGColor(context, windowAttrs);
}
private static Drawable createDefaultBackgroundDrawable() {
return new ColorDrawable(getSystemBGColor());
}
/** Extract the window background color from {@code attrs}. */
private static int peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "peekWindowBGColor");
final Drawable themeBGDrawable;
if (attrs.mWindowBgColor != 0) {
themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor);
} else if (attrs.mWindowBgResId != 0) {
themeBGDrawable = context.getDrawable(attrs.mWindowBgResId);
} else {
themeBGDrawable = createDefaultBackgroundDrawable();
Slog.w(TAG, "Window background does not exist, using " + themeBGDrawable);
}
final int estimatedWindowBGColor = estimateWindowBGColor(themeBGDrawable);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
return estimatedWindowBGColor;
}
private static int estimateWindowBGColor(Drawable themeBGDrawable) {
final DrawableColorTester themeBGTester = new DrawableColorTester(
themeBGDrawable, DrawableColorTester.TRANSPARENT_FILTER /* filterType */);
if (themeBGTester.passFilterRatio() == 0) {
// the window background is transparent, unable to draw
Slog.w(TAG, "Window background is transparent, fill background with black color");
return getSystemBGColor();
} else {
return themeBGTester.getDominateColor();
}
}
private static Drawable peekLegacySplashscreenContent(Context context,
SplashScreenWindowAttrs attrs) {
final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
final int resId = safeReturnAttrDefault((def) ->
a.getResourceId(R.styleable.Window_windowSplashscreenContent, def), 0);
a.recycle();
if (resId != 0) {
return context.getDrawable(resId);
}
if (attrs.mWindowBgResId != 0) {
return context.getDrawable(attrs.mWindowBgResId);
}
return null;
}
private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai,
@StartingWindowType int suggestType) {
updateDensity();
getWindowAttrs(context, mTmpAttrs);
mLastPackageContextConfigHash = context.getResources().getConfiguration().hashCode();
final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
? peekLegacySplashscreenContent(context, mTmpAttrs) : null;
final int themeBGColor = legacyDrawable != null
? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable))
: getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs));
return new StartingWindowViewBuilder(context, ai)
.setWindowBGColor(themeBGColor)
.overlayDrawable(legacyDrawable)
.chooseStyle(suggestType)
.build();
}
private int getBGColorFromCache(ActivityInfo ai, IntSupplier windowBgColorSupplier) {
return mColorCache.getWindowColor(ai.packageName, mLastPackageContextConfigHash,
mTmpAttrs.mWindowBgColor, mTmpAttrs.mWindowBgResId, windowBgColorSupplier).mBgColor;
}
private static <T> T safeReturnAttrDefault(UnaryOperator<T> getMethod, T def) {
try {
return getMethod.apply(def);
} catch (RuntimeException e) {
Slog.w(TAG, "Get attribute fail, return default: " + e.getMessage());
return def;
}
}
/**
* Get the {@link SplashScreenWindowAttrs} from {@code context} and fill them into
* {@code attrs}.
*/
private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
final TypedArray typedArray = context.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
attrs.mWindowBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
R.styleable.Window_windowSplashScreenBackground, def),
Color.TRANSPARENT);
attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
R.styleable.Window_windowSplashScreenAnimatedIcon), null);
attrs.mAnimationDuration = safeReturnAttrDefault((def) -> typedArray.getInt(
R.styleable.Window_windowSplashScreenAnimationDuration, def), 0);
attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable(
R.styleable.Window_windowSplashScreenBrandingImage), null);
attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
R.styleable.Window_windowSplashScreenIconBackgroundColor, def),
Color.TRANSPARENT);
typedArray.recycle();
if (DEBUG) {
Slog.d(TAG, "window attributes color: "
+ Integer.toHexString(attrs.mWindowBgColor)
+ " icon " + attrs.mSplashScreenIcon + " duration " + attrs.mAnimationDuration
+ " brandImage " + attrs.mBrandingImage);
}
}
/** The configuration of the splash screen window. */
public static class SplashScreenWindowAttrs {
private int mWindowBgResId = 0;
private int mWindowBgColor = Color.TRANSPARENT;
private Drawable mSplashScreenIcon = null;
private Drawable mBrandingImage = null;
private int mIconBgColor = Color.TRANSPARENT;
private int mAnimationDuration = 0;
}
private class StartingWindowViewBuilder {
private final Context mContext;
private final ActivityInfo mActivityInfo;
private Drawable mOverlayDrawable;
private int mSuggestType;
private int mThemeColor;
private Drawable[] mFinalIconDrawables;
private int mFinalIconSize = mIconSize;
StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) {
mContext = context;
mActivityInfo = aInfo;
}
StartingWindowViewBuilder setWindowBGColor(@ColorInt int background) {
mThemeColor = background;
return this;
}
StartingWindowViewBuilder overlayDrawable(Drawable overlay) {
mOverlayDrawable = overlay;
return this;
}
StartingWindowViewBuilder chooseStyle(int suggestType) {
mSuggestType = suggestType;
return this;
}
SplashScreenView build() {
Drawable iconDrawable;
final int animationDuration;
if (mSuggestType == STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
|| mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
// empty or legacy splash screen case
animationDuration = 0;
mFinalIconSize = 0;
} else if (mTmpAttrs.mSplashScreenIcon != null) {
// Using the windowSplashScreenAnimatedIcon attribute
iconDrawable = mTmpAttrs.mSplashScreenIcon;
animationDuration = mTmpAttrs.mAnimationDuration;
// There is no background below the icon, so scale the icon up
if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT
|| mTmpAttrs.mIconBgColor == mThemeColor) {
mFinalIconSize *= NO_BACKGROUND_SCALE;
}
createIconDrawable(iconDrawable, false);
} else {
final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
final int densityDpi = mContext.getResources().getDisplayMetrics().densityDpi;
final int scaledIconDpi =
(int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
iconDrawable = mIconProvider.getIcon(mActivityInfo, scaledIconDpi);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (iconDrawable == null) {
iconDrawable = mContext.getPackageManager().getDefaultActivityIcon();
}
if (!processAdaptiveIcon(iconDrawable)) {
if (DEBUG) {
Slog.d(TAG, "The icon is not an AdaptiveIconDrawable");
}
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "legacy_icon_factory");
final ShapeIconFactory factory = new ShapeIconFactory(
SplashscreenContentDrawer.this.mContext,
scaledIconDpi, mFinalIconSize);
final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(
iconDrawable, true /* shrinkNonAdaptiveIcons */);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
createIconDrawable(new BitmapDrawable(bitmap), true);
}
animationDuration = 0;
}
return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration);
}
private class ShapeIconFactory extends BaseIconFactory {
protected ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
super(context, fillResIconDpi, iconBitmapSize, true /* shapeDetection */);
}
}
private void createIconDrawable(Drawable iconDrawable, boolean legacy) {
if (legacy) {
mFinalIconDrawables = SplashscreenIconDrawableFactory.makeLegacyIconDrawable(
iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler);
} else {
mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable(
mTmpAttrs.mIconBgColor, mThemeColor,
iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler);
}
}
private boolean processAdaptiveIcon(Drawable iconDrawable) {
if (!(iconDrawable instanceof AdaptiveIconDrawable)) {
return false;
}
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "processAdaptiveIcon");
final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) iconDrawable;
final Drawable iconForeground = adaptiveIconDrawable.getForeground();
final ColorCache.IconColor iconColor = mColorCache.getIconColor(
mActivityInfo.packageName, mActivityInfo.getIconResource(),
mLastPackageContextConfigHash,
() -> new DrawableColorTester(iconForeground,
DrawableColorTester.TRANSLUCENT_FILTER /* filterType */),
() -> new DrawableColorTester(adaptiveIconDrawable.getBackground()));
if (DEBUG) {
Slog.d(TAG, "FgMainColor=" + Integer.toHexString(iconColor.mFgColor)
+ " BgMainColor=" + Integer.toHexString(iconColor.mBgColor)
+ " IsBgComplex=" + iconColor.mIsBgComplex
+ " FromCache=" + (iconColor.mReuseCount > 0)
+ " ThemeColor=" + Integer.toHexString(mThemeColor));
}
// Only draw the foreground of AdaptiveIcon to the splash screen if below condition
// meet:
// A. The background of the adaptive icon is not complicated. If it is complicated,
// it may contain some information, and
// B. The background of the adaptive icon is similar to the theme color, or
// C. The background of the adaptive icon is grayscale, and the foreground of the
// adaptive icon forms a certain contrast with the theme color.
// D. Didn't specify icon background color.
if (!iconColor.mIsBgComplex && mTmpAttrs.mIconBgColor == Color.TRANSPARENT
&& (isRgbSimilarInHsv(mThemeColor, iconColor.mBgColor)
|| (iconColor.mIsBgGrayscale
&& !isRgbSimilarInHsv(mThemeColor, iconColor.mFgColor)))) {
if (DEBUG) {
Slog.d(TAG, "makeSplashScreenContentView: choose fg icon");
}
// Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
// scale by 192/160 if we only draw adaptiveIcon's foreground.
final float noBgScale =
iconColor.mFgNonTranslucentRatio < ENLARGE_FOREGROUND_ICON_THRESHOLD
? NO_BACKGROUND_SCALE : 1f;
// Using AdaptiveIconDrawable here can help keep the shape consistent with the
// current settings.
mFinalIconSize = (int) (0.5f + mIconSize * noBgScale);
createIconDrawable(iconForeground, false);
} else {
if (DEBUG) {
Slog.d(TAG, "makeSplashScreenContentView: draw whole icon");
}
createIconDrawable(iconDrawable, false);
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
return true;
}
private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
int animationDuration) {
Drawable foreground = null;
Drawable background = null;
if (iconDrawable != null) {
foreground = iconDrawable.length > 0 ? iconDrawable[0] : null;
background = iconDrawable.length > 1 ? iconDrawable[1] : null;
}
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon");
final SplashScreenView.Builder builder = new SplashScreenView.Builder(mContext)
.setBackgroundColor(mThemeColor)
.setOverlayDrawable(mOverlayDrawable)
.setIconSize(iconSize)
.setIconBackground(background)
.setCenterViewDrawable(foreground)
.setAnimationDurationMillis(animationDuration);
if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
&& mTmpAttrs.mBrandingImage != null) {
builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,
mBrandingImageHeight);
}
final SplashScreenView splashScreenView = builder.build();
if (DEBUG) {
Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView);
}
if (mSuggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
splashScreenView.addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
SplashScreenView.applySystemBarsContrastColor(
v.getWindowInsetsController(),
splashScreenView.getInitBackgroundColor());
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
return splashScreenView;
}
}
private static boolean isRgbSimilarInHsv(int a, int b) {
if (a == b) {
return true;
}
final float lumA = Color.luminance(a);
final float lumB = Color.luminance(b);
final float contrastRatio = lumA > lumB
? (lumA + 0.05f) / (lumB + 0.05f) : (lumB + 0.05f) / (lumA + 0.05f);
if (DEBUG) {
Slog.d(TAG, "isRgbSimilarInHsv a: " + Integer.toHexString(a)
+ " b " + Integer.toHexString(b) + " contrast ratio: " + contrastRatio);
}
if (contrastRatio < 2) {
return true;
}
final float[] aHsv = new float[3];
final float[] bHsv = new float[3];
Color.colorToHSV(a, aHsv);
Color.colorToHSV(b, bHsv);
// Minimum degree of the hue between two colors, the result range is 0-180.
int minAngle = (int) Math.abs(aHsv[0] - bHsv[0]);
minAngle = (minAngle + 180) % 360 - 180;
// Calculate the difference between two colors based on the HSV dimensions.
final float normalizeH = minAngle / 180f;
final double squareH = Math.pow(normalizeH, 2);
final double squareS = Math.pow(aHsv[1] - bHsv[1], 2);
final double squareV = Math.pow(aHsv[2] - bHsv[2], 2);
final double square = squareH + squareS + squareV;
final double mean = square / 3;
final double root = Math.sqrt(mean);
if (DEBUG) {
Slog.d(TAG, "hsvDiff " + minAngle
+ " ah " + aHsv[0] + " bh " + bHsv[0]
+ " as " + aHsv[1] + " bs " + bHsv[1]
+ " av " + aHsv[2] + " bv " + bHsv[2]
+ " sqH " + squareH + " sqS " + squareS + " sqV " + squareV
+ " root " + root);
}
return root < 0.1;
}
private static class DrawableColorTester {
private static final int NO_ALPHA_FILTER = 0;
// filter out completely invisible pixels
private static final int TRANSPARENT_FILTER = 1;
// filter out translucent and invisible pixels
private static final int TRANSLUCENT_FILTER = 2;
@IntDef(flag = true, value = {
NO_ALPHA_FILTER,
TRANSPARENT_FILTER,
TRANSLUCENT_FILTER
})
private @interface QuantizerFilterType {}
private final ColorTester mColorChecker;
DrawableColorTester(Drawable drawable) {
this(drawable, NO_ALPHA_FILTER /* filterType */);
}
DrawableColorTester(Drawable drawable, @QuantizerFilterType int filterType) {
// Some applications use LayerDrawable for their windowBackground. To ensure that we
// only get the real background, so that the color is not affected by the alpha of the
// upper layer, try to get the lower layer here. This can also speed up the calculation.
if (drawable instanceof LayerDrawable) {
LayerDrawable layerDrawable = (LayerDrawable) drawable;
if (layerDrawable.getNumberOfLayers() > 0) {
if (DEBUG) {
Slog.d(TAG, "replace drawable with bottom layer drawable");
}
drawable = layerDrawable.getDrawable(0);
}
}
if (drawable == null) {
mColorChecker = new SingleColorTester(
(ColorDrawable) createDefaultBackgroundDrawable());
} else {
mColorChecker = drawable instanceof ColorDrawable
? new SingleColorTester((ColorDrawable) drawable)
: new ComplexDrawableTester(drawable, filterType);
}
}
public float passFilterRatio() {
return mColorChecker.passFilterRatio();
}
public boolean isComplexColor() {
return mColorChecker.isComplexColor();
}
public int getDominateColor() {
return mColorChecker.getDominantColor();
}
public boolean isGrayscale() {
return mColorChecker.isGrayscale();
}
/**
* A help class to check the color information from a Drawable.
*/
private interface ColorTester {
float passFilterRatio();
boolean isComplexColor();
int getDominantColor();
boolean isGrayscale();
}
private static boolean isGrayscaleColor(int color) {
final int red = Color.red(color);
final int green = Color.green(color);
final int blue = Color.blue(color);
return red == green && green == blue;
}
/**
* For ColorDrawable only. There will be only one color so don't spend too much resource for
* it.
*/
private static class SingleColorTester implements ColorTester {
private final ColorDrawable mColorDrawable;
SingleColorTester(@NonNull ColorDrawable drawable) {
mColorDrawable = drawable;
}
@Override
public float passFilterRatio() {
final int alpha = mColorDrawable.getAlpha();
return (float) (alpha / 255);
}
@Override
public boolean isComplexColor() {
return false;
}
@Override
public int getDominantColor() {
return mColorDrawable.getColor();
}
@Override
public boolean isGrayscale() {
return isGrayscaleColor(mColorDrawable.getColor());
}
}
/**
* For any other Drawable except ColorDrawable. This will use the Palette API to check the
* color information and use a quantizer to filter out transparent colors when needed.
*/
private static class ComplexDrawableTester implements ColorTester {
private static final int MAX_BITMAP_SIZE = 40;
private final Palette mPalette;
private final boolean mFilterTransparent;
private static final AlphaFilterQuantizer ALPHA_FILTER_QUANTIZER =
new AlphaFilterQuantizer();
/**
* @param drawable The test target.
* @param filterType Targeting to filter out transparent or translucent pixels,
* this would be needed if want to check
* {@link #passFilterRatio()}, also affecting the estimated result
* of the dominant color.
*/
ComplexDrawableTester(Drawable drawable, @QuantizerFilterType int filterType) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ComplexDrawableTester");
final Rect initialBounds = drawable.copyBounds();
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
// Some drawables do not have intrinsic dimensions
if (width <= 0 || height <= 0) {
width = MAX_BITMAP_SIZE;
height = MAX_BITMAP_SIZE;
} else {
width = Math.min(width, MAX_BITMAP_SIZE);
height = Math.min(height, MAX_BITMAP_SIZE);
}
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas bmpCanvas = new Canvas(bitmap);
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
drawable.draw(bmpCanvas);
// restore to original bounds
drawable.setBounds(initialBounds);
final Palette.Builder builder;
// The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
// sometimes we will need this information to know if this Drawable object is
// transparent.
mFilterTransparent = filterType != NO_ALPHA_FILTER;
if (mFilterTransparent) {
ALPHA_FILTER_QUANTIZER.setFilter(filterType);
builder = new Palette.Builder(bitmap, ALPHA_FILTER_QUANTIZER)
.maximumColorCount(5);
} else {
builder = new Palette.Builder(bitmap, null)
.maximumColorCount(5);
}
mPalette = builder.generate();
bitmap.recycle();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
@Override
public float passFilterRatio() {
return mFilterTransparent ? ALPHA_FILTER_QUANTIZER.mPassFilterRatio : 1;
}
@Override
public boolean isComplexColor() {
return mPalette.getSwatches().size() > 1;
}
@Override
public int getDominantColor() {
final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
if (mainSwatch != null) {
return mainSwatch.getInt();
}
return Color.BLACK;
}
@Override
public boolean isGrayscale() {
final List<Palette.Swatch> swatches = mPalette.getSwatches();
if (swatches != null) {
for (int i = swatches.size() - 1; i >= 0; i--) {
Palette.Swatch swatch = swatches.get(i);
if (!isGrayscaleColor(swatch.getInt())) {
return false;
}
}
}
return true;
}
private static class AlphaFilterQuantizer implements Quantizer {
private static final int NON_TRANSPARENT = 0xFF000000;
private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
private final IntPredicate mTransparentFilter = i -> (i & NON_TRANSPARENT) != 0;
private final IntPredicate mTranslucentFilter = i ->
(i & NON_TRANSPARENT) == NON_TRANSPARENT;
private IntPredicate mFilter = mTransparentFilter;
private float mPassFilterRatio;
void setFilter(@QuantizerFilterType int filterType) {
switch (filterType) {
case TRANSLUCENT_FILTER:
mFilter = mTranslucentFilter;
break;
case TRANSPARENT_FILTER:
default:
mFilter = mTransparentFilter;
break;
}
}
@Override
public void quantize(final int[] pixels, final int maxColors) {
mPassFilterRatio = 0;
int realSize = 0;
for (int i = pixels.length - 1; i > 0; i--) {
if (mFilter.test(pixels[i])) {
realSize++;
}
}
if (realSize == 0) {
if (DEBUG) {
Slog.d(TAG, "quantize: this is pure transparent image");
}
mInnerQuantizer.quantize(pixels, maxColors);
return;
}
mPassFilterRatio = (float) realSize / pixels.length;
final int[] samplePixels = new int[realSize];
int rowIndex = 0;
for (int i = pixels.length - 1; i > 0; i--) {
if (mFilter.test(pixels[i])) {
samplePixels[rowIndex] = pixels[i];
rowIndex++;
}
}
mInnerQuantizer.quantize(samplePixels, maxColors);
}
@Override
public List<Palette.Swatch> getQuantizedColors() {
return mInnerQuantizer.getQuantizedColors();
}
}
}
}
/** Cache the result of {@link DrawableColorTester} to reduce expensive calculation. */
@VisibleForTesting
static class ColorCache extends BroadcastReceiver {
/**
* The color may be different according to resource id and configuration (e.g. night mode),
* so this allows to cache more than one color per package.
*/
private static final int CACHE_SIZE = 2;
/** The computed colors of packages. */
private final ArrayMap<String, Colors> mColorMap = new ArrayMap<>();
private static class Colors {
final WindowColor[] mWindowColors = new WindowColor[CACHE_SIZE];
final IconColor[] mIconColors = new IconColor[CACHE_SIZE];
}
private static class Cache {
/** The hash used to check whether this cache is hit. */
final int mHash;
/** The number of times this cache has been reused. */
int mReuseCount;
Cache(int hash) {
mHash = hash;
}
}
static class WindowColor extends Cache {
final int mBgColor;
WindowColor(int hash, int bgColor) {
super(hash);
mBgColor = bgColor;
}
}
static class IconColor extends Cache {
final int mFgColor;
final int mBgColor;
final boolean mIsBgComplex;
final boolean mIsBgGrayscale;
final float mFgNonTranslucentRatio;
IconColor(int hash, int fgColor, int bgColor, boolean isBgComplex,
boolean isBgGrayscale, float fgNonTranslucnetRatio) {
super(hash);
mFgColor = fgColor;
mBgColor = bgColor;
mIsBgComplex = isBgComplex;
mIsBgGrayscale = isBgGrayscale;
mFgNonTranslucentRatio = fgNonTranslucnetRatio;
}
}
ColorCache(Context context, Handler handler) {
// This includes reinstall and uninstall.
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
context.registerReceiverAsUser(this, UserHandle.ALL, filter,
null /* broadcastPermission */, handler);
}
@Override
public void onReceive(Context context, Intent intent) {
final Uri packageUri = intent.getData();
if (packageUri != null) {
mColorMap.remove(packageUri.getEncodedSchemeSpecificPart());
}
}
/**
* Gets the existing cache if the hash matches. If null is returned, the caller can use
* outLeastUsedIndex to put the new cache.
*/
private static <T extends Cache> T getCache(T[] caches, int hash, int[] outLeastUsedIndex) {
int minReuseCount = Integer.MAX_VALUE;
for (int i = 0; i < CACHE_SIZE; i++) {
final T cache = caches[i];
if (cache == null) {
// Empty slot has the highest priority to put new cache.
minReuseCount = -1;
outLeastUsedIndex[0] = i;
continue;
}
if (cache.mHash == hash) {
cache.mReuseCount++;
return cache;
}
if (cache.mReuseCount < minReuseCount) {
minReuseCount = cache.mReuseCount;
outLeastUsedIndex[0] = i;
}
}
return null;
}
@NonNull WindowColor getWindowColor(String packageName, int configHash, int windowBgColor,
int windowBgResId, IntSupplier windowBgColorSupplier) {
Colors colors = mColorMap.get(packageName);
int hash = 31 * configHash + windowBgColor;
hash = 31 * hash + windowBgResId;
final int[] leastUsedIndex = { 0 };
if (colors != null) {
final WindowColor windowColor = getCache(colors.mWindowColors, hash,
leastUsedIndex);
if (windowColor != null) {
return windowColor;
}
} else {
colors = new Colors();
mColorMap.put(packageName, colors);
}
final WindowColor windowColor = new WindowColor(hash, windowBgColorSupplier.getAsInt());
colors.mWindowColors[leastUsedIndex[0]] = windowColor;
return windowColor;
}
@NonNull IconColor getIconColor(String packageName, int configHash, int iconResId,
Supplier<DrawableColorTester> fgColorTesterSupplier,
Supplier<DrawableColorTester> bgColorTesterSupplier) {
Colors colors = mColorMap.get(packageName);
final int hash = configHash * 31 + iconResId;
final int[] leastUsedIndex = { 0 };
if (colors != null) {
final IconColor iconColor = getCache(colors.mIconColors, hash, leastUsedIndex);
if (iconColor != null) {
return iconColor;
}
} else {
colors = new Colors();
mColorMap.put(packageName, colors);
}
final DrawableColorTester fgTester = fgColorTesterSupplier.get();
final DrawableColorTester bgTester = bgColorTesterSupplier.get();
final IconColor iconColor = new IconColor(hash, fgTester.getDominateColor(),
bgTester.getDominateColor(), bgTester.isComplexColor(), bgTester.isGrayscale(),
fgTester.passFilterRatio());
colors.mIconColors[leastUsedIndex[0]] = iconColor;
return iconColor;
}
}
/**
* Create and play the default exit animation for splash screen view.
*/
void applyExitAnimation(SplashScreenView view, SurfaceControl leash,
Rect frame, Runnable finishCallback) {
final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, view,
leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback);
animation.startAnimations();
}
}