| /* |
| * 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.content.Context.CONTEXT_RESTRICTED; |
| import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; |
| import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; |
| import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_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.ActivityManager; |
| 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.pm.PackageManager; |
| import android.content.res.Configuration; |
| 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.hardware.display.DisplayManager; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.SystemClock; |
| import android.os.Trace; |
| import android.os.UserHandle; |
| import android.util.ArrayMap; |
| import android.util.DisplayMetrics; |
| import android.util.Slog; |
| import android.view.ContextThemeWrapper; |
| import android.view.Display; |
| import android.view.SurfaceControl; |
| import android.view.WindowManager; |
| import android.window.SplashScreenView; |
| import android.window.StartingWindowInfo; |
| 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.internal.protolog.common.ProtoLog; |
| import com.android.launcher3.icons.BaseIconFactory; |
| import com.android.launcher3.icons.IconProvider; |
| import com.android.wm.shell.common.TransactionPool; |
| import com.android.wm.shell.protolog.ShellProtoLogGroup; |
| |
| 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 = StartingWindowController.TAG; |
| |
| /** |
| * The minimum duration during which the splash screen is shown when the splash screen icon is |
| * animated. |
| */ |
| static final long MINIMAL_ANIMATION_DURATION = 400L; |
| |
| /** |
| * Allow the icon style splash screen to be displayed for longer to give time for the animation |
| * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly |
| * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration. |
| */ |
| static final long TIME_WINDOW_DURATION = 100L; |
| |
| /** |
| * The maximum duration during which the splash screen will be shown if the application is ready |
| * to show before the icon animation finishes. |
| */ |
| static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION; |
| |
| // 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 HighResIconProvider mHighResIconProvider; |
| |
| 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, IconProvider iconProvider, TransactionPool pool) { |
| mContext = context; |
| mHighResIconProvider = new HighResIconProvider(mContext, iconProvider); |
| 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); |
| } |
| |
| /** |
| * Help method to create a layout parameters for a window. |
| */ |
| static Context createContext(Context initContext, StartingWindowInfo windowInfo, |
| int theme, @StartingWindowInfo.StartingWindowType int suggestType, |
| DisplayManager displayManager) { |
| final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; |
| final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null |
| ? windowInfo.targetActivityInfo |
| : taskInfo.topActivityInfo; |
| if (activityInfo == null || activityInfo.packageName == null) { |
| return null; |
| } |
| |
| final int displayId = taskInfo.displayId; |
| final int taskId = taskInfo.taskId; |
| |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d", |
| activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType); |
| final Display display = displayManager.getDisplay(displayId); |
| if (display == null) { |
| // Can't show splash screen on requested display, so skip showing at all. |
| return null; |
| } |
| Context context = displayId == DEFAULT_DISPLAY |
| ? initContext : initContext.createDisplayContext(display); |
| if (context == null) { |
| return null; |
| } |
| if (theme != context.getThemeResId()) { |
| try { |
| context = context.createPackageContextAsUser(activityInfo.packageName, |
| CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId)); |
| context.setTheme(theme); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.w(TAG, "Failed creating package context with package name " |
| + activityInfo.packageName + " for user " + taskInfo.userId, e); |
| return null; |
| } |
| } |
| |
| final Configuration taskConfig = taskInfo.getConfiguration(); |
| if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "addSplashScreen: creating context based on task Configuration %s", |
| taskConfig); |
| final Context overrideContext = context.createConfigurationContext(taskConfig); |
| overrideContext.setTheme(theme); |
| final TypedArray typedArray = overrideContext.obtainStyledAttributes( |
| com.android.internal.R.styleable.Window); |
| final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); |
| try { |
| if (resId != 0 && overrideContext.getDrawable(resId) != null) { |
| // We want to use the windowBackground for the override context if it is |
| // available, otherwise we use the default one to make sure a themed starting |
| // window is displayed for the app. |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "addSplashScreen: apply overrideConfig %s", |
| taskConfig); |
| context = overrideContext; |
| } |
| } catch (Resources.NotFoundException e) { |
| Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: " |
| + taskId, e); |
| return null; |
| } |
| typedArray.recycle(); |
| } |
| return context; |
| } |
| |
| /** |
| * Creates the window layout parameters for splashscreen window. |
| */ |
| static WindowManager.LayoutParams createLayoutParameters(Context context, |
| StartingWindowInfo windowInfo, |
| @StartingWindowInfo.StartingWindowType int suggestType, |
| CharSequence title, int pixelFormat, IBinder appToken) { |
| final WindowManager.LayoutParams params = new WindowManager.LayoutParams( |
| WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); |
| params.setFitInsetsSides(0); |
| params.setFitInsetsTypes(0); |
| params.format = pixelFormat; |
| int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
| | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
| | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; |
| final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); |
| if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { |
| windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; |
| } |
| if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { |
| if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) { |
| windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| } |
| } else { |
| windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| } |
| params.layoutInDisplayCutoutMode = a.getInt( |
| R.styleable.Window_windowLayoutInDisplayCutoutMode, |
| params.layoutInDisplayCutoutMode); |
| params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); |
| a.recycle(); |
| |
| final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; |
| final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null |
| ? windowInfo.targetActivityInfo |
| : taskInfo.topActivityInfo; |
| final int displayId = taskInfo.displayId; |
| // Assumes it's safe to show starting windows of launched apps while |
| // the keyguard is being hidden. This is okay because starting windows never show |
| // secret information. |
| // TODO(b/113840485): Occluded may not only happen on default display |
| if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) { |
| windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; |
| } |
| |
| // Force the window flags: this is a fake window, so it is not really |
| // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM |
| // flag because we do know that the next window will take input |
| // focus, so we want to get the IME window up on top of us right away. |
| // Touches will only pass through to the host activity window and will be blocked from |
| // passing to any other windows. |
| windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
| | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
| | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; |
| params.flags = windowFlags; |
| params.token = appToken; |
| params.packageName = activityInfo.packageName; |
| params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; |
| |
| if (!context.getResources().getCompatibilityInfo().supportsScreen()) { |
| params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; |
| } |
| |
| params.setTitle("Splash Screen " + title); |
| return params; |
| } |
| /** |
| * 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, |
| StartingWindowInfo info, Consumer<SplashScreenView> splashScreenViewConsumer, |
| Consumer<Runnable> uiThreadInitConsumer) { |
| mSplashscreenWorkerHandler.post(() -> { |
| SplashScreenView contentView; |
| try { |
| Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView"); |
| contentView = makeSplashScreenContentView(context, info, suggestType, |
| uiThreadInitConsumer); |
| Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); |
| } catch (RuntimeException e) { |
| Slog.w(TAG, "failed creating starting window content at taskId: " |
| + info.taskInfo.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.TRANSLUCENT_FILTER /* filterType */); |
| if (themeBGTester.passFilterRatio() != 1) { |
| // the window background is translucent, unable to draw |
| Slog.w(TAG, "Window background is translucent, 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; |
| } |
| |
| /** |
| * Creates a SplashScreenView without read animatable icon and branding image. |
| */ |
| SplashScreenView makeSimpleSplashScreenContentView(Context context, |
| StartingWindowInfo info, int themeBGColor) { |
| updateDensity(); |
| mTmpAttrs.reset(); |
| final ActivityInfo ai = info.targetActivityInfo != null |
| ? info.targetActivityInfo |
| : info.taskInfo.topActivityInfo; |
| |
| final SplashViewBuilder builder = new SplashViewBuilder(context, ai); |
| final SplashScreenView view = builder |
| .setWindowBGColor(themeBGColor) |
| .chooseStyle(STARTING_WINDOW_TYPE_SPLASH_SCREEN) |
| .build(); |
| view.setNotCopyable(); |
| return view; |
| } |
| |
| private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info, |
| @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) { |
| updateDensity(); |
| |
| getWindowAttrs(context, mTmpAttrs); |
| mLastPackageContextConfigHash = context.getResources().getConfiguration().hashCode(); |
| |
| final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN |
| ? peekLegacySplashscreenContent(context, mTmpAttrs) : null; |
| final ActivityInfo ai = info.targetActivityInfo != null |
| ? info.targetActivityInfo |
| : info.taskInfo.topActivityInfo; |
| final int themeBGColor = legacyDrawable != null |
| ? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable)) |
| : getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs)); |
| |
| return new SplashViewBuilder(context, ai) |
| .setWindowBGColor(themeBGColor) |
| .overlayDrawable(legacyDrawable) |
| .chooseStyle(suggestType) |
| .setUiThreadInitConsumer(uiThreadInitConsumer) |
| .setAllowHandleSolidColor(info.allowHandleSolidColorSplashScreen()) |
| .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.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(); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "getWindowAttrs: window attributes color: %s, replace icon: %b", |
| Integer.toHexString(attrs.mWindowBgColor), attrs.mSplashScreenIcon != null); |
| } |
| |
| /** Creates the wrapper with system theme to avoid unexpected styles from app. */ |
| ContextThemeWrapper createViewContextWrapper(Context appContext) { |
| return new ContextThemeWrapper(appContext, mContext.getTheme()); |
| } |
| |
| /** 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; |
| |
| void reset() { |
| mWindowBgResId = 0; |
| mWindowBgColor = Color.TRANSPARENT; |
| mSplashScreenIcon = null; |
| mBrandingImage = null; |
| mIconBgColor = Color.TRANSPARENT; |
| } |
| } |
| |
| /** |
| * Get an optimal animation duration to keep the splash screen from showing. |
| * |
| * @param animationDuration The animation duration defined from app. |
| * @param appReadyDuration The real duration from the starting the app to the first app window |
| * drawn. |
| */ |
| @VisibleForTesting |
| static long getShowingDuration(long animationDuration, long appReadyDuration) { |
| if (animationDuration <= appReadyDuration) { |
| // app window ready took longer time than animation, it can be removed ASAP. |
| return appReadyDuration; |
| } |
| if (appReadyDuration < MAX_ANIMATION_DURATION) { |
| if (animationDuration > MAX_ANIMATION_DURATION |
| || appReadyDuration < MINIMAL_ANIMATION_DURATION) { |
| // animation is too long or too short, cut off with minimal duration |
| return MINIMAL_ANIMATION_DURATION; |
| } |
| // animation is longer than dOpt but shorter than max, allow it to play till finish |
| return MAX_ANIMATION_DURATION; |
| } |
| // the shortest duration is longer than dMax, cut off no matter how long the animation |
| // will be. |
| return appReadyDuration; |
| } |
| |
| private class SplashViewBuilder { |
| private final Context mContext; |
| private final ActivityInfo mActivityInfo; |
| |
| private Drawable mOverlayDrawable; |
| private int mSuggestType; |
| private int mThemeColor; |
| private Drawable[] mFinalIconDrawables; |
| private int mFinalIconSize = mIconSize; |
| private Consumer<Runnable> mUiThreadInitTask; |
| /** @see #setAllowHandleSolidColor(boolean) **/ |
| private boolean mAllowHandleSolidColor; |
| |
| SplashViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { |
| mContext = context; |
| mActivityInfo = aInfo; |
| } |
| |
| SplashViewBuilder setWindowBGColor(@ColorInt int background) { |
| mThemeColor = background; |
| return this; |
| } |
| |
| SplashViewBuilder overlayDrawable(Drawable overlay) { |
| mOverlayDrawable = overlay; |
| return this; |
| } |
| |
| SplashViewBuilder chooseStyle(int suggestType) { |
| mSuggestType = suggestType; |
| return this; |
| } |
| |
| // Set up the UI thread for the View. |
| SplashViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) { |
| mUiThreadInitTask = uiThreadInitTask; |
| return this; |
| } |
| |
| /** |
| * If true, the application will receive a the |
| * {@link |
| * android.window.SplashScreen.OnExitAnimationListener#onSplashScreenExit(SplashScreenView)} |
| * callback, effectively copying the {@link SplashScreenView} into the client process. |
| */ |
| SplashViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) { |
| mAllowHandleSolidColor = allowHandleSolidColor; |
| return this; |
| } |
| |
| SplashScreenView build() { |
| Drawable iconDrawable; |
| if (mSuggestType == STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN |
| || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { |
| // empty or legacy splash screen case |
| mFinalIconSize = 0; |
| } else if (mTmpAttrs.mSplashScreenIcon != null) { |
| // Using the windowSplashScreenAnimatedIcon attribute |
| iconDrawable = mTmpAttrs.mSplashScreenIcon; |
| |
| // 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 /* legacy */, false /* loadInDetail */); |
| } else { |
| final float iconScale = (float) mIconSize / (float) mDefaultIconSize; |
| final int densityDpi = mContext.getResources().getConfiguration().densityDpi; |
| final int scaledIconDpi = |
| (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE); |
| Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon"); |
| iconDrawable = mHighResIconProvider.getIcon( |
| mActivityInfo, densityDpi, scaledIconDpi); |
| Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); |
| if (!processAdaptiveIcon(iconDrawable)) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "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.createScaledBitmap(iconDrawable, |
| BaseIconFactory.MODE_DEFAULT); |
| Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); |
| createIconDrawable(new BitmapDrawable(bitmap), true, |
| mHighResIconProvider.mLoadInDetail); |
| } |
| } |
| |
| return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask); |
| } |
| |
| 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, |
| boolean loadInDetail) { |
| if (legacy) { |
| mFinalIconDrawables = SplashscreenIconDrawableFactory.makeLegacyIconDrawable( |
| iconDrawable, mDefaultIconSize, mFinalIconSize, loadInDetail, |
| mSplashscreenWorkerHandler); |
| } else { |
| mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable( |
| mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize, |
| mFinalIconSize, loadInDetail, 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())); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "processAdaptiveIcon: FgMainColor=%s, BgMainColor=%s, " |
| + "IsBgComplex=%b, FromCache=%b, ThemeColor=%s", |
| Integer.toHexString(iconColor.mFgColor), |
| Integer.toHexString(iconColor.mBgColor), |
| iconColor.mIsBgComplex, |
| iconColor.mReuseCount > 0, |
| 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)))) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "processAdaptiveIcon: 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, mHighResIconProvider.mLoadInDetail); |
| } else { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "processAdaptiveIcon: draw whole icon"); |
| createIconDrawable(iconDrawable, false, mHighResIconProvider.mLoadInDetail); |
| } |
| Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); |
| return true; |
| } |
| |
| private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable, |
| Consumer<Runnable> uiThreadInitTask) { |
| 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 ContextThemeWrapper wrapper = createViewContextWrapper(mContext); |
| final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper) |
| .setBackgroundColor(mThemeColor) |
| .setOverlayDrawable(mOverlayDrawable) |
| .setIconSize(iconSize) |
| .setIconBackground(background) |
| .setCenterViewDrawable(foreground) |
| .setUiThreadInitConsumer(uiThreadInitTask) |
| .setAllowHandleSolidColor(mAllowHandleSolidColor); |
| |
| if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN |
| && mTmpAttrs.mBrandingImage != null) { |
| builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth, |
| mBrandingImageHeight); |
| } |
| final SplashScreenView splashScreenView = builder.build(); |
| 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); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "isRgbSimilarInHsv a:%s, b:%s, contrast ratio:%f", |
| Integer.toHexString(a), Integer.toHexString(b), 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); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "isRgbSimilarInHsv hsvDiff: %d, ah: %f, bh: %f, as: %f, bs: %f, av: %f, bv: %f, " |
| + "sqH: %f, sqS: %f, sqV: %f, rsm: %f", |
| minAngle, aHsv[0], bHsv[0], aHsv[1], bHsv[1], aHsv[2], bHsv[2], |
| squareH, squareS, squareV, 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) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "DrawableColorTester: 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 alpha / 255.0f; |
| } |
| |
| @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) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "DrawableTester quantize: 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, long createTime, float roundedCornerRadius) { |
| final Runnable playAnimation = () -> { |
| final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, |
| view, leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback, |
| roundedCornerRadius); |
| animation.startAnimations(); |
| }; |
| if (view.getIconView() == null) { |
| playAnimation.run(); |
| return; |
| } |
| final long appReadyDuration = SystemClock.uptimeMillis() - createTime; |
| final long animDuration = view.getIconAnimationDuration() != null |
| ? view.getIconAnimationDuration().toMillis() : 0; |
| final long minimumShowingDuration = getShowingDuration(animDuration, appReadyDuration); |
| final long delayed = minimumShowingDuration - appReadyDuration; |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "applyExitAnimation delayed: %s", delayed); |
| if (delayed > 0) { |
| view.postDelayed(playAnimation, delayed); |
| } else { |
| playAnimation.run(); |
| } |
| } |
| |
| /** |
| * When loading a BitmapDrawable object with specific density, there will decode the image based |
| * on the density from display metrics, so even when load with higher override density, the |
| * final intrinsic size of a BitmapDrawable can still not big enough to draw on expect size. |
| * |
| * So here we use a standalone IconProvider object to load the Drawable object for higher |
| * density, and the resources object won't affect the entire system. |
| * |
| */ |
| private static class HighResIconProvider { |
| private final Context mSharedContext; |
| private final IconProvider mSharedIconProvider; |
| private boolean mLoadInDetail; |
| |
| // only create standalone icon provider when the density dpi is low. |
| private Context mStandaloneContext; |
| private IconProvider mStandaloneIconProvider; |
| |
| HighResIconProvider(Context context, IconProvider sharedIconProvider) { |
| mSharedContext = context; |
| mSharedIconProvider = sharedIconProvider; |
| } |
| |
| Drawable getIcon(ActivityInfo activityInfo, int currentDpi, int iconDpi) { |
| mLoadInDetail = false; |
| Drawable drawable; |
| if (currentDpi < iconDpi && currentDpi < DisplayMetrics.DENSITY_XHIGH) { |
| drawable = loadFromStandalone(activityInfo, currentDpi, iconDpi); |
| } else { |
| drawable = mSharedIconProvider.getIcon(activityInfo, iconDpi); |
| } |
| |
| if (drawable == null) { |
| drawable = mSharedContext.getPackageManager().getDefaultActivityIcon(); |
| } |
| return drawable; |
| } |
| |
| private Drawable loadFromStandalone(ActivityInfo activityInfo, int currentDpi, |
| int iconDpi) { |
| if (mStandaloneContext == null) { |
| final Configuration defConfig = mSharedContext.getResources().getConfiguration(); |
| mStandaloneContext = mSharedContext.createConfigurationContext(defConfig); |
| mStandaloneIconProvider = new IconProvider(mStandaloneContext); |
| } |
| Resources resources; |
| try { |
| resources = mStandaloneContext.getPackageManager() |
| .getResourcesForApplication(activityInfo.applicationInfo); |
| } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { |
| resources = null; |
| } |
| if (resources != null) { |
| updateResourcesDpi(resources, iconDpi); |
| } |
| final Drawable drawable = mStandaloneIconProvider.getIcon(activityInfo, iconDpi); |
| mLoadInDetail = true; |
| // reset density dpi |
| if (resources != null) { |
| updateResourcesDpi(resources, currentDpi); |
| } |
| return drawable; |
| } |
| |
| private void updateResourcesDpi(Resources resources, int densityDpi) { |
| final Configuration config = resources.getConfiguration(); |
| final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); |
| config.densityDpi = densityDpi; |
| displayMetrics.densityDpi = densityDpi; |
| resources.updateConfiguration(config, displayMetrics); |
| } |
| } |
| } |