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