• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.startingsurface;
18 
19 import static android.content.Context.CONTEXT_RESTRICTED;
20 import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
21 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
22 import static android.view.Display.DEFAULT_DISPLAY;
23 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
24 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
25 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
26 
27 import android.annotation.ColorInt;
28 import android.annotation.IntDef;
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.app.ActivityManager;
32 import android.app.ActivityThread;
33 import android.content.BroadcastReceiver;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.pm.ActivityInfo;
38 import android.content.pm.PackageManager;
39 import android.content.res.Configuration;
40 import android.content.res.Resources;
41 import android.content.res.TypedArray;
42 import android.graphics.Bitmap;
43 import android.graphics.Canvas;
44 import android.graphics.Color;
45 import android.graphics.Rect;
46 import android.graphics.drawable.AdaptiveIconDrawable;
47 import android.graphics.drawable.BitmapDrawable;
48 import android.graphics.drawable.ColorDrawable;
49 import android.graphics.drawable.Drawable;
50 import android.graphics.drawable.LayerDrawable;
51 import android.hardware.display.DisplayManager;
52 import android.net.Uri;
53 import android.os.Handler;
54 import android.os.HandlerThread;
55 import android.os.IBinder;
56 import android.os.SystemClock;
57 import android.os.Trace;
58 import android.os.UserHandle;
59 import android.util.ArrayMap;
60 import android.util.DisplayMetrics;
61 import android.util.Slog;
62 import android.view.ContextThemeWrapper;
63 import android.view.Display;
64 import android.view.SurfaceControl;
65 import android.view.WindowManager;
66 import android.window.SplashScreenView;
67 import android.window.StartingWindowInfo;
68 import android.window.StartingWindowInfo.StartingWindowType;
69 
70 import com.android.internal.R;
71 import com.android.internal.annotations.VisibleForTesting;
72 import com.android.internal.graphics.palette.Palette;
73 import com.android.internal.graphics.palette.Quantizer;
74 import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
75 import com.android.internal.protolog.common.ProtoLog;
76 import com.android.launcher3.icons.BaseIconFactory;
77 import com.android.launcher3.icons.IconProvider;
78 import com.android.wm.shell.common.TransactionPool;
79 import com.android.wm.shell.protolog.ShellProtoLogGroup;
80 
81 import java.util.List;
82 import java.util.function.Consumer;
83 import java.util.function.IntPredicate;
84 import java.util.function.IntSupplier;
85 import java.util.function.Supplier;
86 import java.util.function.UnaryOperator;
87 
88 /**
89  * Util class to create the view for a splash screen content.
90  * Everything execute in this class should be post to mSplashscreenWorkerHandler.
91  * @hide
92  */
93 public class SplashscreenContentDrawer {
94     private static final String TAG = StartingWindowController.TAG;
95 
96     /**
97      * The minimum duration during which the splash screen is shown when the splash screen icon is
98      * animated.
99      */
100     static final long MINIMAL_ANIMATION_DURATION = 400L;
101 
102     /**
103      * Allow the icon style splash screen to be displayed for longer to give time for the animation
104      * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly
105      * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration.
106      */
107     static final long TIME_WINDOW_DURATION = 100L;
108 
109     /**
110      * The maximum duration during which the splash screen will be shown if the application is ready
111      * to show before the icon animation finishes.
112      */
113     static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION;
114 
115     // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
116     // icon which it's non-transparent foreground area is similar to it's background area, then
117     // do not enlarge the foreground drawable.
118     // For example, an icon with the foreground 108*108 opaque pixels and it's background
119     // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
120     private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
121 
122     /**
123      * If the developer doesn't specify a background for the icon, we slightly scale it up.
124      *
125      * The background is either manually specified in the theme or the Adaptive Icon
126      * background is used if it's different from the window background.
127      */
128     private static final float NO_BACKGROUND_SCALE = 192f / 160;
129     private final Context mContext;
130     private final HighResIconProvider mHighResIconProvider;
131 
132     private int mIconSize;
133     private int mDefaultIconSize;
134     private int mBrandingImageWidth;
135     private int mBrandingImageHeight;
136     private int mMainWindowShiftLength;
137     private int mLastPackageContextConfigHash;
138     private final TransactionPool mTransactionPool;
139     private final SplashScreenWindowAttrs mTmpAttrs = new SplashScreenWindowAttrs();
140     private final Handler mSplashscreenWorkerHandler;
141     @VisibleForTesting
142     final ColorCache mColorCache;
143 
SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool)144     SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) {
145         mContext = context;
146         mHighResIconProvider = new HighResIconProvider(mContext, iconProvider);
147         mTransactionPool = pool;
148 
149         // Initialize Splashscreen worker thread
150         // TODO(b/185288910) move it into WMShellConcurrencyModule and provide an executor to make
151         //  it easier to test stuff that happens on that thread later.
152         final HandlerThread shellSplashscreenWorkerThread =
153                 new HandlerThread("wmshell.splashworker", THREAD_PRIORITY_TOP_APP_BOOST);
154         shellSplashscreenWorkerThread.start();
155         mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler();
156         mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler);
157     }
158 
159     /**
160      * Help method to create a layout parameters for a window.
161      */
createContext(Context initContext, StartingWindowInfo windowInfo, int theme, @StartingWindowInfo.StartingWindowType int suggestType, DisplayManager displayManager)162     static Context createContext(Context initContext, StartingWindowInfo windowInfo,
163             int theme, @StartingWindowInfo.StartingWindowType int suggestType,
164             DisplayManager displayManager) {
165         final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
166         final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
167                 ? windowInfo.targetActivityInfo
168                 : taskInfo.topActivityInfo;
169         if (activityInfo == null || activityInfo.packageName == null) {
170             return null;
171         }
172 
173         final int displayId = taskInfo.displayId;
174         final int taskId = taskInfo.taskId;
175 
176         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
177                 "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d",
178                 activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType);
179         final Display display = displayManager.getDisplay(displayId);
180         if (display == null) {
181             // Can't show splash screen on requested display, so skip showing at all.
182             return null;
183         }
184         Context context = displayId == DEFAULT_DISPLAY
185                 ? initContext : initContext.createDisplayContext(display);
186         if (context == null) {
187             return null;
188         }
189         if (theme != context.getThemeResId()) {
190             try {
191                 context = context.createPackageContextAsUser(activityInfo.packageName,
192                         CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId));
193                 context.setTheme(theme);
194             } catch (PackageManager.NameNotFoundException e) {
195                 Slog.w(TAG, "Failed creating package context with package name "
196                         + activityInfo.packageName + " for user " + taskInfo.userId, e);
197                 return null;
198             }
199         }
200 
201         final Configuration taskConfig = taskInfo.getConfiguration();
202         if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) {
203             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
204                     "addSplashScreen: creating context based on task Configuration %s",
205                     taskConfig);
206             final Context overrideContext = context.createConfigurationContext(taskConfig);
207             overrideContext.setTheme(theme);
208             final TypedArray typedArray = overrideContext.obtainStyledAttributes(
209                     com.android.internal.R.styleable.Window);
210             final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
211             try {
212                 if (resId != 0 && overrideContext.getDrawable(resId) != null) {
213                     // We want to use the windowBackground for the override context if it is
214                     // available, otherwise we use the default one to make sure a themed starting
215                     // window is displayed for the app.
216                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
217                             "addSplashScreen: apply overrideConfig %s",
218                             taskConfig);
219                     context = overrideContext;
220                 }
221             } catch (Resources.NotFoundException e) {
222                 Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: "
223                         + taskId, e);
224                 return null;
225             }
226             typedArray.recycle();
227         }
228         return context;
229     }
230 
231     /**
232      * Creates the window layout parameters for splashscreen window.
233      */
createLayoutParameters(Context context, StartingWindowInfo windowInfo, @StartingWindowInfo.StartingWindowType int suggestType, CharSequence title, int pixelFormat, IBinder appToken)234     static WindowManager.LayoutParams createLayoutParameters(Context context,
235             StartingWindowInfo windowInfo,
236             @StartingWindowInfo.StartingWindowType int suggestType,
237             CharSequence title, int pixelFormat, IBinder appToken) {
238         final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
239                 WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
240         params.setFitInsetsSides(0);
241         params.setFitInsetsTypes(0);
242         params.format = pixelFormat;
243         int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
244                 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
245                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
246         final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
247         if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
248             windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
249         }
250         if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
251             if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) {
252                 windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
253             }
254         } else {
255             windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
256         }
257         params.layoutInDisplayCutoutMode = a.getInt(
258                 R.styleable.Window_windowLayoutInDisplayCutoutMode,
259                 params.layoutInDisplayCutoutMode);
260         params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
261         a.recycle();
262 
263         final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
264         final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
265                 ? windowInfo.targetActivityInfo
266                 : taskInfo.topActivityInfo;
267         final int displayId = taskInfo.displayId;
268         // Assumes it's safe to show starting windows of launched apps while
269         // the keyguard is being hidden. This is okay because starting windows never show
270         // secret information.
271         // TODO(b/113840485): Occluded may not only happen on default display
272         if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) {
273             windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
274         }
275 
276         // Force the window flags: this is a fake window, so it is not really
277         // touchable or focusable by the user.  We also add in the ALT_FOCUSABLE_IM
278         // flag because we do know that the next window will take input
279         // focus, so we want to get the IME window up on top of us right away.
280         // Touches will only pass through to the host activity window and will be blocked from
281         // passing to any other windows.
282         windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
283                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
284                 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
285         params.flags = windowFlags;
286         params.token = appToken;
287         params.packageName = activityInfo.packageName;
288         params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
289 
290         if (!context.getResources().getCompatibilityInfo().supportsScreen()) {
291             params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
292         }
293 
294         params.setTitle("Splash Screen " + title);
295         return params;
296     }
297     /**
298      * Create a SplashScreenView object.
299      *
300      * In order to speed up the splash screen view to show on first frame, preparing the
301      * view on background thread so the view and the drawable can be create and pre-draw in
302      * parallel.
303      *
304      * @param suggestType Suggest type to create the splash screen view.
305      * @param splashScreenViewConsumer Receiving the SplashScreenView object, which will also be
306      *                                 executed on splash screen thread. Note that the view can be
307      *                                 null if failed.
308      */
createContentView(Context context, @StartingWindowType int suggestType, StartingWindowInfo info, Consumer<SplashScreenView> splashScreenViewConsumer, Consumer<Runnable> uiThreadInitConsumer)309     void createContentView(Context context, @StartingWindowType int suggestType,
310             StartingWindowInfo info, Consumer<SplashScreenView> splashScreenViewConsumer,
311             Consumer<Runnable> uiThreadInitConsumer) {
312         mSplashscreenWorkerHandler.post(() -> {
313             SplashScreenView contentView;
314             try {
315                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView");
316                 contentView = makeSplashScreenContentView(context, info, suggestType,
317                         uiThreadInitConsumer);
318                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
319             } catch (RuntimeException e) {
320                 Slog.w(TAG, "failed creating starting window content at taskId: "
321                         + info.taskInfo.taskId, e);
322                 contentView = null;
323             }
324             splashScreenViewConsumer.accept(contentView);
325         });
326     }
327 
updateDensity()328     private void updateDensity() {
329         mIconSize = mContext.getResources().getDimensionPixelSize(
330                 com.android.internal.R.dimen.starting_surface_icon_size);
331         mDefaultIconSize = mContext.getResources().getDimensionPixelSize(
332                 com.android.internal.R.dimen.starting_surface_default_icon_size);
333         mBrandingImageWidth = mContext.getResources().getDimensionPixelSize(
334                 com.android.wm.shell.R.dimen.starting_surface_brand_image_width);
335         mBrandingImageHeight = mContext.getResources().getDimensionPixelSize(
336                 com.android.wm.shell.R.dimen.starting_surface_brand_image_height);
337         mMainWindowShiftLength = mContext.getResources().getDimensionPixelSize(
338                 com.android.wm.shell.R.dimen.starting_surface_exit_animation_window_shift_length);
339     }
340 
341     /**
342      * @return Current system background color.
343      */
getSystemBGColor()344     public static int getSystemBGColor() {
345         final Context systemContext = ActivityThread.currentApplication();
346         if (systemContext == null) {
347             Slog.e(TAG, "System context does not exist!");
348             return Color.BLACK;
349         }
350         final Resources res = systemContext.getResources();
351         return res.getColor(com.android.wm.shell.R.color.splash_window_background_default);
352     }
353 
354     /**
355      * Estimate the background color of the app splash screen, this may take a while so use it only
356      * if there is no starting window exists for that context.
357      **/
estimateTaskBackgroundColor(Context context)358     int estimateTaskBackgroundColor(Context context) {
359         final SplashScreenWindowAttrs windowAttrs = new SplashScreenWindowAttrs();
360         getWindowAttrs(context, windowAttrs);
361         return peekWindowBGColor(context, windowAttrs);
362     }
363 
createDefaultBackgroundDrawable()364     private static Drawable createDefaultBackgroundDrawable() {
365         return new ColorDrawable(getSystemBGColor());
366     }
367 
368     /** Extract the window background color from {@code attrs}. */
peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs)369     private static int peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs) {
370         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "peekWindowBGColor");
371         final Drawable themeBGDrawable;
372         if (attrs.mWindowBgColor != 0) {
373             themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor);
374         } else if (attrs.mWindowBgResId != 0) {
375             themeBGDrawable = context.getDrawable(attrs.mWindowBgResId);
376         } else {
377             themeBGDrawable = createDefaultBackgroundDrawable();
378             Slog.w(TAG, "Window background does not exist, using " + themeBGDrawable);
379         }
380         final int estimatedWindowBGColor = estimateWindowBGColor(themeBGDrawable);
381         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
382         return estimatedWindowBGColor;
383     }
384 
estimateWindowBGColor(Drawable themeBGDrawable)385     private static int estimateWindowBGColor(Drawable themeBGDrawable) {
386         final DrawableColorTester themeBGTester = new DrawableColorTester(
387                 themeBGDrawable, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */);
388         if (themeBGTester.passFilterRatio() < 0.5f) {
389             // more than half pixels of the window background is translucent, unable to draw
390             Slog.w(TAG, "Window background is translucent, fill background with black color");
391             return getSystemBGColor();
392         } else {
393             return themeBGTester.getDominateColor();
394         }
395     }
396 
peekLegacySplashscreenContent(Context context, SplashScreenWindowAttrs attrs)397     private static Drawable peekLegacySplashscreenContent(Context context,
398             SplashScreenWindowAttrs attrs) {
399         final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
400         final int resId = safeReturnAttrDefault((def) ->
401                 a.getResourceId(R.styleable.Window_windowSplashscreenContent, def), 0);
402         a.recycle();
403         if (resId != 0) {
404             return context.getDrawable(resId);
405         }
406         if (attrs.mWindowBgResId != 0) {
407             return context.getDrawable(attrs.mWindowBgResId);
408         }
409         return null;
410     }
411 
412     /**
413      * Creates a SplashScreenView without read animatable icon and branding image.
414      */
makeSimpleSplashScreenContentView(Context context, StartingWindowInfo info, int themeBGColor)415     SplashScreenView makeSimpleSplashScreenContentView(Context context,
416             StartingWindowInfo info, int themeBGColor) {
417         updateDensity();
418         mTmpAttrs.reset();
419         final ActivityInfo ai = info.targetActivityInfo != null
420                 ? info.targetActivityInfo
421                 : info.taskInfo.topActivityInfo;
422 
423         final SplashViewBuilder builder = new SplashViewBuilder(context, ai);
424         final SplashScreenView view = builder
425                 .setWindowBGColor(themeBGColor)
426                 .chooseStyle(STARTING_WINDOW_TYPE_SPLASH_SCREEN)
427                 .build();
428         view.setNotCopyable();
429         return view;
430     }
431 
makeSplashScreenContentView(Context context, StartingWindowInfo info, @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer)432     private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info,
433             @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) {
434         updateDensity();
435 
436         getWindowAttrs(context, mTmpAttrs);
437         mLastPackageContextConfigHash = context.getResources().getConfiguration().hashCode();
438 
439         final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
440                 ? peekLegacySplashscreenContent(context, mTmpAttrs) : null;
441         final ActivityInfo ai = info.targetActivityInfo != null
442                 ? info.targetActivityInfo
443                 : info.taskInfo.topActivityInfo;
444         final int themeBGColor = legacyDrawable != null
445                 ? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable))
446                 : getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs));
447 
448         return new SplashViewBuilder(context, ai)
449                 .setWindowBGColor(themeBGColor)
450                 .overlayDrawable(legacyDrawable)
451                 .chooseStyle(suggestType)
452                 .setUiThreadInitConsumer(uiThreadInitConsumer)
453                 .setAllowHandleSolidColor(info.allowHandleSolidColorSplashScreen())
454                 .build();
455     }
456 
getBGColorFromCache(ActivityInfo ai, IntSupplier windowBgColorSupplier)457     private int getBGColorFromCache(ActivityInfo ai, IntSupplier windowBgColorSupplier) {
458         return mColorCache.getWindowColor(ai.packageName, mLastPackageContextConfigHash,
459                 mTmpAttrs.mWindowBgColor, mTmpAttrs.mWindowBgResId, windowBgColorSupplier).mBgColor;
460     }
461 
safeReturnAttrDefault(UnaryOperator<T> getMethod, T def)462     private static <T> T safeReturnAttrDefault(UnaryOperator<T> getMethod, T def) {
463         try {
464             return getMethod.apply(def);
465         } catch (RuntimeException e) {
466             Slog.w(TAG, "Get attribute fail, return default: " + e.getMessage());
467             return def;
468         }
469     }
470 
471     /**
472      * Get the {@link SplashScreenWindowAttrs} from {@code context} and fill them into
473      * {@code attrs}.
474      */
getWindowAttrs(Context context, SplashScreenWindowAttrs attrs)475     private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
476         final TypedArray typedArray = context.obtainStyledAttributes(
477                 com.android.internal.R.styleable.Window);
478         attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
479         attrs.mWindowBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
480                 R.styleable.Window_windowSplashScreenBackground, def),
481                 Color.TRANSPARENT);
482         attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
483                 R.styleable.Window_windowSplashScreenAnimatedIcon), null);
484         attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable(
485                 R.styleable.Window_windowSplashScreenBrandingImage), null);
486         attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
487                 R.styleable.Window_windowSplashScreenIconBackgroundColor, def),
488                 Color.TRANSPARENT);
489         typedArray.recycle();
490         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
491                 "getWindowAttrs: window attributes color: %s, replace icon: %b",
492                 Integer.toHexString(attrs.mWindowBgColor), attrs.mSplashScreenIcon != null);
493     }
494 
495     /** Creates the wrapper with system theme to avoid unexpected styles from app. */
createViewContextWrapper(Context appContext)496     ContextThemeWrapper createViewContextWrapper(Context appContext) {
497         return new ContextThemeWrapper(appContext, mContext.getTheme());
498     }
499 
500     /** The configuration of the splash screen window. */
501     public static class SplashScreenWindowAttrs {
502         private int mWindowBgResId = 0;
503         private int mWindowBgColor = Color.TRANSPARENT;
504         private Drawable mSplashScreenIcon = null;
505         private Drawable mBrandingImage = null;
506         private int mIconBgColor = Color.TRANSPARENT;
507 
reset()508         void reset() {
509             mWindowBgResId = 0;
510             mWindowBgColor = Color.TRANSPARENT;
511             mSplashScreenIcon = null;
512             mBrandingImage = null;
513             mIconBgColor = Color.TRANSPARENT;
514         }
515     }
516 
517     /**
518      * Get an optimal animation duration to keep the splash screen from showing.
519      *
520      * @param animationDuration The animation duration defined from app.
521      * @param appReadyDuration The real duration from the starting the app to the first app window
522      *                         drawn.
523      */
524     @VisibleForTesting
getShowingDuration(long animationDuration, long appReadyDuration)525     static long getShowingDuration(long animationDuration, long appReadyDuration) {
526         if (animationDuration <= appReadyDuration) {
527             // app window ready took longer time than animation, it can be removed ASAP.
528             return appReadyDuration;
529         }
530         if (appReadyDuration < MAX_ANIMATION_DURATION) {
531             if (animationDuration > MAX_ANIMATION_DURATION
532                     || appReadyDuration < MINIMAL_ANIMATION_DURATION) {
533                 // animation is too long or too short, cut off with minimal duration
534                 return MINIMAL_ANIMATION_DURATION;
535             }
536             // animation is longer than dOpt but shorter than max, allow it to play till finish
537             return MAX_ANIMATION_DURATION;
538         }
539         // the shortest duration is longer than dMax, cut off no matter how long the animation
540         // will be.
541         return appReadyDuration;
542     }
543 
544     private class SplashViewBuilder {
545         private final Context mContext;
546         private final ActivityInfo mActivityInfo;
547 
548         private Drawable mOverlayDrawable;
549         private int mSuggestType;
550         private int mThemeColor;
551         private Drawable[] mFinalIconDrawables;
552         private int mFinalIconSize = mIconSize;
553         private Consumer<Runnable> mUiThreadInitTask;
554         /** @see #setAllowHandleSolidColor(boolean) **/
555         private boolean mAllowHandleSolidColor;
556 
SplashViewBuilder(@onNull Context context, @NonNull ActivityInfo aInfo)557         SplashViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) {
558             mContext = context;
559             mActivityInfo = aInfo;
560         }
561 
setWindowBGColor(@olorInt int background)562         SplashViewBuilder setWindowBGColor(@ColorInt int background) {
563             mThemeColor = background;
564             return this;
565         }
566 
overlayDrawable(Drawable overlay)567         SplashViewBuilder overlayDrawable(Drawable overlay) {
568             mOverlayDrawable = overlay;
569             return this;
570         }
571 
chooseStyle(int suggestType)572         SplashViewBuilder chooseStyle(int suggestType) {
573             mSuggestType = suggestType;
574             return this;
575         }
576 
577         // Set up the UI thread for the View.
setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask)578         SplashViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) {
579             mUiThreadInitTask = uiThreadInitTask;
580             return this;
581         }
582 
583         /**
584          * If true, the application will receive a the
585          * {@link
586          * android.window.SplashScreen.OnExitAnimationListener#onSplashScreenExit(SplashScreenView)}
587          * callback, effectively copying the {@link SplashScreenView} into the client process.
588          */
setAllowHandleSolidColor(boolean allowHandleSolidColor)589         SplashViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) {
590             mAllowHandleSolidColor = allowHandleSolidColor;
591             return this;
592         }
593 
build()594         SplashScreenView build() {
595             Drawable iconDrawable;
596             if (mSuggestType == STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN
597                     || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
598                 // empty or legacy splash screen case
599                 mFinalIconSize = 0;
600             } else if (mTmpAttrs.mSplashScreenIcon != null) {
601                 // Using the windowSplashScreenAnimatedIcon attribute
602                 iconDrawable = mTmpAttrs.mSplashScreenIcon;
603 
604                 // There is no background below the icon, so scale the icon up
605                 if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT
606                         || mTmpAttrs.mIconBgColor == mThemeColor) {
607                     mFinalIconSize *= NO_BACKGROUND_SCALE;
608                 }
609                 createIconDrawable(iconDrawable, false /* legacy */, false /* loadInDetail */);
610             } else {
611                 final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
612                 final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
613                 final int scaledIconDpi =
614                         (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
615                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
616                 iconDrawable = mHighResIconProvider.getIcon(
617                         mActivityInfo, densityDpi, scaledIconDpi);
618                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
619                 if (!processAdaptiveIcon(iconDrawable)) {
620                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
621                             "The icon is not an AdaptiveIconDrawable");
622                     Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "legacy_icon_factory");
623                     final ShapeIconFactory factory = new ShapeIconFactory(
624                             SplashscreenContentDrawer.this.mContext,
625                             scaledIconDpi, mFinalIconSize);
626                     final Bitmap bitmap = factory.createScaledBitmap(iconDrawable,
627                             BaseIconFactory.MODE_DEFAULT);
628                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
629                     createIconDrawable(new BitmapDrawable(bitmap), true,
630                             mHighResIconProvider.mLoadInDetail);
631                 }
632             }
633 
634             return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask);
635         }
636 
637         private class ShapeIconFactory extends BaseIconFactory {
ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize)638             protected ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
639                 super(context, fillResIconDpi, iconBitmapSize, true /* shapeDetection */);
640             }
641         }
642 
createIconDrawable(Drawable iconDrawable, boolean legacy, boolean loadInDetail)643         private void createIconDrawable(Drawable iconDrawable, boolean legacy,
644                 boolean loadInDetail) {
645             if (legacy) {
646                 mFinalIconDrawables = SplashscreenIconDrawableFactory.makeLegacyIconDrawable(
647                         iconDrawable, mDefaultIconSize, mFinalIconSize, loadInDetail,
648                         mSplashscreenWorkerHandler);
649             } else {
650                 mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable(
651                         mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize,
652                         mFinalIconSize, loadInDetail, mSplashscreenWorkerHandler);
653             }
654         }
655 
processAdaptiveIcon(Drawable iconDrawable)656         private boolean processAdaptiveIcon(Drawable iconDrawable) {
657             if (!(iconDrawable instanceof AdaptiveIconDrawable)) {
658                 return false;
659             }
660 
661             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "processAdaptiveIcon");
662             final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) iconDrawable;
663             final Drawable iconForeground = adaptiveIconDrawable.getForeground();
664             final ColorCache.IconColor iconColor = mColorCache.getIconColor(
665                     mActivityInfo.packageName, mActivityInfo.getIconResource(),
666                     mLastPackageContextConfigHash,
667                     () -> new DrawableColorTester(iconForeground,
668                             DrawableColorTester.TRANSLUCENT_FILTER /* filterType */),
669                     () -> new DrawableColorTester(adaptiveIconDrawable.getBackground()));
670             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
671                     "processAdaptiveIcon: FgMainColor=%s, BgMainColor=%s, "
672                             + "IsBgComplex=%b, FromCache=%b, ThemeColor=%s",
673                     Integer.toHexString(iconColor.mFgColor),
674                     Integer.toHexString(iconColor.mBgColor),
675                     iconColor.mIsBgComplex,
676                     iconColor.mReuseCount > 0,
677                     Integer.toHexString(mThemeColor));
678 
679             // Only draw the foreground of AdaptiveIcon to the splash screen if below condition
680             // meet:
681             // A. The background of the adaptive icon is not complicated. If it is complicated,
682             // it may contain some information, and
683             // B. The background of the adaptive icon is similar to the theme color, or
684             // C. The background of the adaptive icon is grayscale, and the foreground of the
685             // adaptive icon forms a certain contrast with the theme color.
686             // D. Didn't specify icon background color.
687             if (!iconColor.mIsBgComplex && mTmpAttrs.mIconBgColor == Color.TRANSPARENT
688                     && (isRgbSimilarInHsv(mThemeColor, iconColor.mBgColor)
689                             || (iconColor.mIsBgGrayscale
690                                     && !isRgbSimilarInHsv(mThemeColor, iconColor.mFgColor)))) {
691                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
692                         "processAdaptiveIcon: choose fg icon");
693                 // Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
694                 // scale by 192/160 if we only draw adaptiveIcon's foreground.
695                 final float noBgScale =
696                         iconColor.mFgNonTranslucentRatio < ENLARGE_FOREGROUND_ICON_THRESHOLD
697                                 ? NO_BACKGROUND_SCALE : 1f;
698                 // Using AdaptiveIconDrawable here can help keep the shape consistent with the
699                 // current settings.
700                 mFinalIconSize = (int) (0.5f + mIconSize * noBgScale);
701                 createIconDrawable(iconForeground, false, mHighResIconProvider.mLoadInDetail);
702             } else {
703                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
704                         "processAdaptiveIcon: draw whole icon");
705                 createIconDrawable(iconDrawable, false, mHighResIconProvider.mLoadInDetail);
706             }
707             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
708             return true;
709         }
710 
711         private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
712                 Consumer<Runnable> uiThreadInitTask) {
713             Drawable foreground = null;
714             Drawable background = null;
715             if (iconDrawable != null) {
716                 foreground = iconDrawable.length > 0 ? iconDrawable[0] : null;
717                 background = iconDrawable.length > 1 ? iconDrawable[1] : null;
718             }
719 
720             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon");
721             final ContextThemeWrapper wrapper = createViewContextWrapper(mContext);
722             final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper)
723                     .setBackgroundColor(mThemeColor)
724                     .setOverlayDrawable(mOverlayDrawable)
725                     .setIconSize(iconSize)
726                     .setIconBackground(background)
727                     .setCenterViewDrawable(foreground)
728                     .setUiThreadInitConsumer(uiThreadInitTask)
729                     .setAllowHandleSolidColor(mAllowHandleSolidColor);
730 
731             if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
732                     && mTmpAttrs.mBrandingImage != null) {
733                 builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,
734                         mBrandingImageHeight);
735             }
736             final SplashScreenView splashScreenView = builder.build();
737             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
738             return splashScreenView;
739         }
740     }
741 
isRgbSimilarInHsv(int a, int b)742     private static boolean isRgbSimilarInHsv(int a, int b) {
743         if (a == b) {
744             return true;
745         }
746         final float lumA = Color.luminance(a);
747         final float lumB = Color.luminance(b);
748         final float contrastRatio = lumA > lumB
749                 ? (lumA + 0.05f) / (lumB + 0.05f) : (lumB + 0.05f) / (lumA + 0.05f);
750         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
751                 "isRgbSimilarInHsv a:%s, b:%s, contrast ratio:%f",
752                 Integer.toHexString(a), Integer.toHexString(b), contrastRatio);
753         if (contrastRatio < 2) {
754             return true;
755         }
756 
757         final float[] aHsv = new float[3];
758         final float[] bHsv = new float[3];
759         Color.colorToHSV(a, aHsv);
760         Color.colorToHSV(b, bHsv);
761         // Minimum degree of the hue between two colors, the result range is 0-180.
762         int minAngle = (int) Math.abs(aHsv[0] - bHsv[0]);
763         minAngle = (minAngle + 180) % 360 - 180;
764 
765         // Calculate the difference between two colors based on the HSV dimensions.
766         final float normalizeH = minAngle / 180f;
767         final double squareH = Math.pow(normalizeH, 2);
768         final double squareS = Math.pow(aHsv[1] - bHsv[1], 2);
769         final double squareV = Math.pow(aHsv[2] - bHsv[2], 2);
770         final double square = squareH + squareS + squareV;
771         final double mean = square / 3;
772         final double root = Math.sqrt(mean);
773         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
774                 "isRgbSimilarInHsv hsvDiff: %d, ah: %f, bh: %f, as: %f, bs: %f, av: %f, bv: %f, "
775                         + "sqH: %f, sqS: %f, sqV: %f, rsm: %f",
776                 minAngle, aHsv[0], bHsv[0], aHsv[1], bHsv[1], aHsv[2], bHsv[2],
777                 squareH, squareS, squareV, root);
778         return root < 0.1;
779     }
780 
781     private static class DrawableColorTester {
782         private static final int NO_ALPHA_FILTER = 0;
783         // filter out completely invisible pixels
784         private static final int TRANSPARENT_FILTER = 1;
785         // filter out translucent and invisible pixels
786         private static final int TRANSLUCENT_FILTER = 2;
787 
788         @IntDef(flag = true, value = {
789                 NO_ALPHA_FILTER,
790                 TRANSPARENT_FILTER,
791                 TRANSLUCENT_FILTER
792         })
793         private @interface QuantizerFilterType {}
794 
795         private final ColorTester mColorChecker;
796 
DrawableColorTester(Drawable drawable)797         DrawableColorTester(Drawable drawable) {
798             this(drawable, NO_ALPHA_FILTER /* filterType */);
799         }
800 
DrawableColorTester(Drawable drawable, @QuantizerFilterType int filterType)801         DrawableColorTester(Drawable drawable, @QuantizerFilterType int filterType) {
802             // Some applications use LayerDrawable for their windowBackground. To ensure that we
803             // only get the real background, so that the color is not affected by the alpha of the
804             // upper layer, try to get the lower layer here. This can also speed up the calculation.
805             if (drawable instanceof LayerDrawable) {
806                 LayerDrawable layerDrawable = (LayerDrawable) drawable;
807                 if (layerDrawable.getNumberOfLayers() > 0) {
808                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
809                             "DrawableColorTester: replace drawable with bottom layer drawable");
810                     drawable = layerDrawable.getDrawable(0);
811                 }
812             }
813             if (drawable == null) {
814                 mColorChecker = new SingleColorTester(
815                         (ColorDrawable) createDefaultBackgroundDrawable());
816             } else {
817                 mColorChecker = drawable instanceof ColorDrawable
818                         ? new SingleColorTester((ColorDrawable) drawable)
819                         : new ComplexDrawableTester(drawable, filterType);
820             }
821         }
822 
passFilterRatio()823         public float passFilterRatio() {
824             return mColorChecker.passFilterRatio();
825         }
826 
isComplexColor()827         public boolean isComplexColor() {
828             return mColorChecker.isComplexColor();
829         }
830 
getDominateColor()831         public int getDominateColor() {
832             return mColorChecker.getDominantColor();
833         }
834 
isGrayscale()835         public boolean isGrayscale() {
836             return mColorChecker.isGrayscale();
837         }
838 
839         /**
840          * A help class to check the color information from a Drawable.
841          */
842         private interface ColorTester {
passFilterRatio()843             float passFilterRatio();
844 
isComplexColor()845             boolean isComplexColor();
846 
getDominantColor()847             int getDominantColor();
848 
isGrayscale()849             boolean isGrayscale();
850         }
851 
isGrayscaleColor(int color)852         private static boolean isGrayscaleColor(int color) {
853             final int red = Color.red(color);
854             final int green = Color.green(color);
855             final int blue = Color.blue(color);
856             return red == green && green == blue;
857         }
858 
859         /**
860          * For ColorDrawable only. There will be only one color so don't spend too much resource for
861          * it.
862          */
863         private static class SingleColorTester implements ColorTester {
864             private final ColorDrawable mColorDrawable;
865 
SingleColorTester(@onNull ColorDrawable drawable)866             SingleColorTester(@NonNull ColorDrawable drawable) {
867                 mColorDrawable = drawable;
868             }
869 
870             @Override
passFilterRatio()871             public float passFilterRatio() {
872                 final int alpha = mColorDrawable.getAlpha();
873                 return alpha / 255.0f;
874             }
875 
876             @Override
isComplexColor()877             public boolean isComplexColor() {
878                 return false;
879             }
880 
881             @Override
getDominantColor()882             public int getDominantColor() {
883                 return mColorDrawable.getColor();
884             }
885 
886             @Override
isGrayscale()887             public boolean isGrayscale() {
888                 return isGrayscaleColor(mColorDrawable.getColor());
889             }
890         }
891 
892         /**
893          * For any other Drawable except ColorDrawable. This will use the Palette API to check the
894          * color information and use a quantizer to filter out transparent colors when needed.
895          */
896         private static class ComplexDrawableTester implements ColorTester {
897             private static final int MAX_BITMAP_SIZE = 40;
898             private final Palette mPalette;
899             private final boolean mFilterTransparent;
900             private static final AlphaFilterQuantizer ALPHA_FILTER_QUANTIZER =
901                     new AlphaFilterQuantizer();
902 
903             /**
904              * @param drawable The test target.
905              * @param filterType Targeting to filter out transparent or translucent pixels,
906              *                   this would be needed if want to check
907              *                   {@link #passFilterRatio()}, also affecting the estimated result
908              *                   of the dominant color.
909              */
ComplexDrawableTester(Drawable drawable, @QuantizerFilterType int filterType)910             ComplexDrawableTester(Drawable drawable, @QuantizerFilterType int filterType) {
911                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ComplexDrawableTester");
912                 final Rect initialBounds = drawable.copyBounds();
913                 int width = drawable.getIntrinsicWidth();
914                 int height = drawable.getIntrinsicHeight();
915                 // Some drawables do not have intrinsic dimensions
916                 if (width <= 0 || height <= 0) {
917                     width = MAX_BITMAP_SIZE;
918                     height = MAX_BITMAP_SIZE;
919                 } else {
920                     width = Math.min(width, MAX_BITMAP_SIZE);
921                     height = Math.min(height, MAX_BITMAP_SIZE);
922                 }
923 
924                 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
925                 final Canvas bmpCanvas = new Canvas(bitmap);
926                 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
927                 drawable.draw(bmpCanvas);
928                 // restore to original bounds
929                 drawable.setBounds(initialBounds);
930 
931                 final Palette.Builder builder;
932                 // The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
933                 // sometimes we will need this information to know if this Drawable object is
934                 // transparent.
935                 mFilterTransparent = filterType != NO_ALPHA_FILTER;
936                 if (mFilterTransparent) {
937                     ALPHA_FILTER_QUANTIZER.setFilter(filterType);
938                     builder = new Palette.Builder(bitmap, ALPHA_FILTER_QUANTIZER)
939                             .maximumColorCount(5);
940                 } else {
941                     builder = new Palette.Builder(bitmap, null)
942                             .maximumColorCount(5);
943                 }
944                 mPalette = builder.generate();
945                 bitmap.recycle();
946                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
947             }
948 
949             @Override
passFilterRatio()950             public float passFilterRatio() {
951                 return mFilterTransparent ? ALPHA_FILTER_QUANTIZER.mPassFilterRatio : 1;
952             }
953 
954             @Override
isComplexColor()955             public boolean isComplexColor() {
956                 return mPalette.getSwatches().size() > 1;
957             }
958 
959             @Override
getDominantColor()960             public int getDominantColor() {
961                 final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
962                 if (mainSwatch != null) {
963                     return mainSwatch.getInt();
964                 }
965                 return Color.BLACK;
966             }
967 
968             @Override
isGrayscale()969             public boolean isGrayscale() {
970                 final List<Palette.Swatch> swatches = mPalette.getSwatches();
971                 if (swatches != null) {
972                     for (int i = swatches.size() - 1; i >= 0; i--) {
973                         Palette.Swatch swatch = swatches.get(i);
974                         if (!isGrayscaleColor(swatch.getInt())) {
975                             return false;
976                         }
977                     }
978                 }
979                 return true;
980             }
981 
982             private static class AlphaFilterQuantizer implements Quantizer {
983                 private static final int NON_TRANSPARENT = 0xFF000000;
984                 private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
985                 private final IntPredicate mTransparentFilter = i -> (i & NON_TRANSPARENT) != 0;
986                 private final IntPredicate mTranslucentFilter = i ->
987                         (i & NON_TRANSPARENT) == NON_TRANSPARENT;
988 
989                 private IntPredicate mFilter = mTransparentFilter;
990                 private float mPassFilterRatio;
991 
setFilter(@uantizerFilterType int filterType)992                 void setFilter(@QuantizerFilterType int filterType) {
993                     switch (filterType) {
994                         case TRANSLUCENT_FILTER:
995                             mFilter = mTranslucentFilter;
996                             break;
997                         case TRANSPARENT_FILTER:
998                         default:
999                             mFilter = mTransparentFilter;
1000                             break;
1001                     }
1002                 }
1003 
1004                 @Override
quantize(final int[] pixels, final int maxColors)1005                 public void quantize(final int[] pixels, final int maxColors) {
1006                     mPassFilterRatio = 0;
1007                     int realSize = 0;
1008                     for (int i = pixels.length - 1; i > 0; i--) {
1009                         if (mFilter.test(pixels[i])) {
1010                             realSize++;
1011                         }
1012                     }
1013                     if (realSize == 0) {
1014                         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
1015                                 "DrawableTester quantize: pure transparent image");
1016                         mInnerQuantizer.quantize(pixels, maxColors);
1017                         return;
1018                     }
1019                     mPassFilterRatio = (float) realSize / pixels.length;
1020                     final int[] samplePixels = new int[realSize];
1021                     int rowIndex = 0;
1022                     for (int i = pixels.length - 1; i > 0; i--) {
1023                         if (mFilter.test(pixels[i])) {
1024                             samplePixels[rowIndex] = pixels[i];
1025                             rowIndex++;
1026                         }
1027                     }
1028                     mInnerQuantizer.quantize(samplePixels, maxColors);
1029                 }
1030 
1031                 @Override
getQuantizedColors()1032                 public List<Palette.Swatch> getQuantizedColors() {
1033                     return mInnerQuantizer.getQuantizedColors();
1034                 }
1035             }
1036         }
1037     }
1038 
1039     /** Cache the result of {@link DrawableColorTester} to reduce expensive calculation. */
1040     @VisibleForTesting
1041     static class ColorCache extends BroadcastReceiver {
1042         /**
1043          * The color may be different according to resource id and configuration (e.g. night mode),
1044          * so this allows to cache more than one color per package.
1045          */
1046         private static final int CACHE_SIZE = 2;
1047 
1048         /** The computed colors of packages. */
1049         private final ArrayMap<String, Colors> mColorMap = new ArrayMap<>();
1050 
1051         private static class Colors {
1052             final WindowColor[] mWindowColors = new WindowColor[CACHE_SIZE];
1053             final IconColor[] mIconColors = new IconColor[CACHE_SIZE];
1054         }
1055 
1056         private static class Cache {
1057             /** The hash used to check whether this cache is hit. */
1058             final int mHash;
1059 
1060             /** The number of times this cache has been reused. */
1061             int mReuseCount;
1062 
Cache(int hash)1063             Cache(int hash) {
1064                 mHash = hash;
1065             }
1066         }
1067 
1068         static class WindowColor extends Cache {
1069             final int mBgColor;
1070 
WindowColor(int hash, int bgColor)1071             WindowColor(int hash, int bgColor) {
1072                 super(hash);
1073                 mBgColor = bgColor;
1074             }
1075         }
1076 
1077         static class IconColor extends Cache {
1078             final int mFgColor;
1079             final int mBgColor;
1080             final boolean mIsBgComplex;
1081             final boolean mIsBgGrayscale;
1082             final float mFgNonTranslucentRatio;
1083 
IconColor(int hash, int fgColor, int bgColor, boolean isBgComplex, boolean isBgGrayscale, float fgNonTranslucnetRatio)1084             IconColor(int hash, int fgColor, int bgColor, boolean isBgComplex,
1085                     boolean isBgGrayscale, float fgNonTranslucnetRatio) {
1086                 super(hash);
1087                 mFgColor = fgColor;
1088                 mBgColor = bgColor;
1089                 mIsBgComplex = isBgComplex;
1090                 mIsBgGrayscale = isBgGrayscale;
1091                 mFgNonTranslucentRatio = fgNonTranslucnetRatio;
1092             }
1093         }
1094 
ColorCache(Context context, Handler handler)1095         ColorCache(Context context, Handler handler) {
1096             // This includes reinstall and uninstall.
1097             final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
1098             filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
1099             context.registerReceiverAsUser(this, UserHandle.ALL, filter,
1100                     null /* broadcastPermission */, handler);
1101         }
1102 
1103         @Override
onReceive(Context context, Intent intent)1104         public void onReceive(Context context, Intent intent) {
1105             final Uri packageUri = intent.getData();
1106             if (packageUri != null) {
1107                 mColorMap.remove(packageUri.getEncodedSchemeSpecificPart());
1108             }
1109         }
1110 
1111         /**
1112          * Gets the existing cache if the hash matches. If null is returned, the caller can use
1113          * outLeastUsedIndex to put the new cache.
1114          */
getCache(T[] caches, int hash, int[] outLeastUsedIndex)1115         private static <T extends Cache> T getCache(T[] caches, int hash, int[] outLeastUsedIndex) {
1116             int minReuseCount = Integer.MAX_VALUE;
1117             for (int i = 0; i < CACHE_SIZE; i++) {
1118                 final T cache = caches[i];
1119                 if (cache == null) {
1120                     // Empty slot has the highest priority to put new cache.
1121                     minReuseCount = -1;
1122                     outLeastUsedIndex[0] = i;
1123                     continue;
1124                 }
1125                 if (cache.mHash == hash) {
1126                     cache.mReuseCount++;
1127                     return cache;
1128                 }
1129                 if (cache.mReuseCount < minReuseCount) {
1130                     minReuseCount = cache.mReuseCount;
1131                     outLeastUsedIndex[0] = i;
1132                 }
1133             }
1134             return null;
1135         }
1136 
getWindowColor(String packageName, int configHash, int windowBgColor, int windowBgResId, IntSupplier windowBgColorSupplier)1137         @NonNull WindowColor getWindowColor(String packageName, int configHash, int windowBgColor,
1138                 int windowBgResId, IntSupplier windowBgColorSupplier) {
1139             Colors colors = mColorMap.get(packageName);
1140             int hash = 31 * configHash + windowBgColor;
1141             hash = 31 * hash + windowBgResId;
1142             final int[] leastUsedIndex = { 0 };
1143             if (colors != null) {
1144                 final WindowColor windowColor = getCache(colors.mWindowColors, hash,
1145                         leastUsedIndex);
1146                 if (windowColor != null) {
1147                     return windowColor;
1148                 }
1149             } else {
1150                 colors = new Colors();
1151                 mColorMap.put(packageName, colors);
1152             }
1153             final WindowColor windowColor = new WindowColor(hash, windowBgColorSupplier.getAsInt());
1154             colors.mWindowColors[leastUsedIndex[0]] = windowColor;
1155             return windowColor;
1156         }
1157 
getIconColor(String packageName, int configHash, int iconResId, Supplier<DrawableColorTester> fgColorTesterSupplier, Supplier<DrawableColorTester> bgColorTesterSupplier)1158         @NonNull IconColor getIconColor(String packageName, int configHash, int iconResId,
1159                 Supplier<DrawableColorTester> fgColorTesterSupplier,
1160                 Supplier<DrawableColorTester> bgColorTesterSupplier) {
1161             Colors colors = mColorMap.get(packageName);
1162             final int hash = configHash * 31 + iconResId;
1163             final int[] leastUsedIndex = { 0 };
1164             if (colors != null) {
1165                 final IconColor iconColor = getCache(colors.mIconColors, hash, leastUsedIndex);
1166                 if (iconColor != null) {
1167                     return iconColor;
1168                 }
1169             } else {
1170                 colors = new Colors();
1171                 mColorMap.put(packageName, colors);
1172             }
1173             final DrawableColorTester fgTester = fgColorTesterSupplier.get();
1174             final DrawableColorTester bgTester = bgColorTesterSupplier.get();
1175             final IconColor iconColor = new IconColor(hash, fgTester.getDominateColor(),
1176                     bgTester.getDominateColor(), bgTester.isComplexColor(), bgTester.isGrayscale(),
1177                     fgTester.passFilterRatio());
1178             colors.mIconColors[leastUsedIndex[0]] = iconColor;
1179             return iconColor;
1180         }
1181     }
1182 
1183     /**
1184      * Create and play the default exit animation for splash screen view.
1185      */
applyExitAnimation(SplashScreenView view, SurfaceControl leash, Rect frame, Runnable finishCallback, long createTime, float roundedCornerRadius)1186     void applyExitAnimation(SplashScreenView view, SurfaceControl leash,
1187             Rect frame, Runnable finishCallback, long createTime, float roundedCornerRadius) {
1188         final Runnable playAnimation = () -> {
1189             final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext,
1190                     view, leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback,
1191                     roundedCornerRadius);
1192             animation.startAnimations();
1193         };
1194         if (view.getIconView() == null) {
1195             playAnimation.run();
1196             return;
1197         }
1198         final long appReadyDuration = SystemClock.uptimeMillis() - createTime;
1199         final long animDuration = view.getIconAnimationDuration() != null
1200                 ? view.getIconAnimationDuration().toMillis() : 0;
1201         final long minimumShowingDuration = getShowingDuration(animDuration, appReadyDuration);
1202         final long delayed = minimumShowingDuration - appReadyDuration;
1203         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
1204                 "applyExitAnimation delayed: %s", delayed);
1205         if (delayed > 0) {
1206             view.postDelayed(playAnimation, delayed);
1207         } else {
1208             playAnimation.run();
1209         }
1210     }
1211 
1212     /**
1213      * When loading a BitmapDrawable object with specific density, there will decode the image based
1214      * on the density from display metrics, so even when load with higher override density, the
1215      * final intrinsic size of a BitmapDrawable can still not big enough to draw on expect size.
1216      *
1217      * So here we use a standalone IconProvider object to load the Drawable object for higher
1218      * density, and the resources object won't affect the entire system.
1219      *
1220      */
1221     private static class HighResIconProvider {
1222         private final Context mSharedContext;
1223         private final IconProvider mSharedIconProvider;
1224         private boolean mLoadInDetail;
1225 
1226         // only create standalone icon provider when the density dpi is low.
1227         private Context mStandaloneContext;
1228         private IconProvider mStandaloneIconProvider;
1229 
HighResIconProvider(Context context, IconProvider sharedIconProvider)1230         HighResIconProvider(Context context, IconProvider sharedIconProvider) {
1231             mSharedContext = context;
1232             mSharedIconProvider = sharedIconProvider;
1233         }
1234 
getIcon(ActivityInfo activityInfo, int currentDpi, int iconDpi)1235         Drawable getIcon(ActivityInfo activityInfo, int currentDpi, int iconDpi) {
1236             mLoadInDetail = false;
1237             Drawable drawable;
1238             if (currentDpi < iconDpi && currentDpi < DisplayMetrics.DENSITY_XHIGH) {
1239                 drawable = loadFromStandalone(activityInfo, currentDpi, iconDpi);
1240             } else {
1241                 drawable = mSharedIconProvider.getIcon(activityInfo, iconDpi);
1242             }
1243 
1244             if (drawable == null) {
1245                 drawable = mSharedContext.getPackageManager().getDefaultActivityIcon();
1246             }
1247             return drawable;
1248         }
1249 
loadFromStandalone(ActivityInfo activityInfo, int currentDpi, int iconDpi)1250         private Drawable loadFromStandalone(ActivityInfo activityInfo, int currentDpi,
1251                 int iconDpi) {
1252             if (mStandaloneContext == null) {
1253                 final Configuration defConfig = mSharedContext.getResources().getConfiguration();
1254                 mStandaloneContext = mSharedContext.createConfigurationContext(defConfig);
1255                 mStandaloneIconProvider = new IconProvider(mStandaloneContext);
1256             }
1257             Resources resources;
1258             try {
1259                 resources = mStandaloneContext.getPackageManager()
1260                         .getResourcesForApplication(activityInfo.applicationInfo);
1261             } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) {
1262                 resources = null;
1263             }
1264             if (resources != null) {
1265                 updateResourcesDpi(resources, iconDpi);
1266             }
1267             final Drawable drawable = mStandaloneIconProvider.getIcon(activityInfo, iconDpi);
1268             mLoadInDetail = true;
1269             // reset density dpi
1270             if (resources != null) {
1271                 updateResourcesDpi(resources, currentDpi);
1272             }
1273             return drawable;
1274         }
1275 
updateResourcesDpi(Resources resources, int densityDpi)1276         private void updateResourcesDpi(Resources resources, int densityDpi) {
1277             final Configuration config = resources.getConfiguration();
1278             final DisplayMetrics displayMetrics = resources.getDisplayMetrics();
1279             config.densityDpi = densityDpi;
1280             displayMetrics.densityDpi = densityDpi;
1281             resources.updateConfiguration(config, displayMetrics);
1282         }
1283     }
1284 }
1285