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.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
20 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
21 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN;
22 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
23 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
24 
25 import android.annotation.ColorInt;
26 import android.annotation.IntDef;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.app.ActivityThread;
30 import android.content.BroadcastReceiver;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.pm.ActivityInfo;
35 import android.content.res.Resources;
36 import android.content.res.TypedArray;
37 import android.graphics.Bitmap;
38 import android.graphics.Canvas;
39 import android.graphics.Color;
40 import android.graphics.Rect;
41 import android.graphics.drawable.AdaptiveIconDrawable;
42 import android.graphics.drawable.BitmapDrawable;
43 import android.graphics.drawable.ColorDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.graphics.drawable.LayerDrawable;
46 import android.net.Uri;
47 import android.os.Handler;
48 import android.os.HandlerThread;
49 import android.os.Trace;
50 import android.os.UserHandle;
51 import android.util.ArrayMap;
52 import android.util.Slog;
53 import android.view.ContextThemeWrapper;
54 import android.view.SurfaceControl;
55 import android.view.View;
56 import android.window.SplashScreenView;
57 import android.window.StartingWindowInfo.StartingWindowType;
58 
59 import com.android.internal.R;
60 import com.android.internal.annotations.VisibleForTesting;
61 import com.android.internal.graphics.palette.Palette;
62 import com.android.internal.graphics.palette.Quantizer;
63 import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
64 import com.android.launcher3.icons.BaseIconFactory;
65 import com.android.launcher3.icons.IconProvider;
66 import com.android.wm.shell.common.TransactionPool;
67 
68 import java.util.List;
69 import java.util.function.Consumer;
70 import java.util.function.IntPredicate;
71 import java.util.function.IntSupplier;
72 import java.util.function.Supplier;
73 import java.util.function.UnaryOperator;
74 
75 /**
76  * Util class to create the view for a splash screen content.
77  * Everything execute in this class should be post to mSplashscreenWorkerHandler.
78  * @hide
79  */
80 public class SplashscreenContentDrawer {
81     private static final String TAG = StartingSurfaceDrawer.TAG;
82     private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN;
83 
84     // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
85     // icon which it's non-transparent foreground area is similar to it's background area, then
86     // do not enlarge the foreground drawable.
87     // For example, an icon with the foreground 108*108 opaque pixels and it's background
88     // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
89     private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
90 
91     /**
92      * If the developer doesn't specify a background for the icon, we slightly scale it up.
93      *
94      * The background is either manually specified in the theme or the Adaptive Icon
95      * background is used if it's different from the window background.
96      */
97     private static final float NO_BACKGROUND_SCALE = 192f / 160;
98     private final Context mContext;
99     private final IconProvider mIconProvider;
100 
101     private int mIconSize;
102     private int mDefaultIconSize;
103     private int mBrandingImageWidth;
104     private int mBrandingImageHeight;
105     private int mMainWindowShiftLength;
106     private int mLastPackageContextConfigHash;
107     private final TransactionPool mTransactionPool;
108     private final SplashScreenWindowAttrs mTmpAttrs = new SplashScreenWindowAttrs();
109     private final Handler mSplashscreenWorkerHandler;
110     @VisibleForTesting
111     final ColorCache mColorCache;
112 
SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool)113     SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) {
114         mContext = context;
115         mIconProvider = iconProvider;
116         mTransactionPool = pool;
117 
118         // Initialize Splashscreen worker thread
119         // TODO(b/185288910) move it into WMShellConcurrencyModule and provide an executor to make
120         //  it easier to test stuff that happens on that thread later.
121         final HandlerThread shellSplashscreenWorkerThread =
122                 new HandlerThread("wmshell.splashworker", THREAD_PRIORITY_TOP_APP_BOOST);
123         shellSplashscreenWorkerThread.start();
124         mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler();
125         mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler);
126     }
127 
128     /**
129      * Create a SplashScreenView object.
130      *
131      * In order to speed up the splash screen view to show on first frame, preparing the
132      * view on background thread so the view and the drawable can be create and pre-draw in
133      * parallel.
134      *
135      * @param suggestType Suggest type to create the splash screen view.
136      * @param splashScreenViewConsumer Receiving the SplashScreenView object, which will also be
137      *                                 executed on splash screen thread. Note that the view can be
138      *                                 null if failed.
139      */
createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info, int taskId, Consumer<SplashScreenView> splashScreenViewConsumer, Consumer<Runnable> uiThreadInitConsumer)140     void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info,
141             int taskId, Consumer<SplashScreenView> splashScreenViewConsumer,
142             Consumer<Runnable> uiThreadInitConsumer) {
143         mSplashscreenWorkerHandler.post(() -> {
144             SplashScreenView contentView;
145             try {
146                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView");
147                 contentView = makeSplashScreenContentView(context, info, suggestType,
148                         uiThreadInitConsumer);
149                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
150             } catch (RuntimeException e) {
151                 Slog.w(TAG, "failed creating starting window content at taskId: "
152                         + taskId, e);
153                 contentView = null;
154             }
155             splashScreenViewConsumer.accept(contentView);
156         });
157     }
158 
updateDensity()159     private void updateDensity() {
160         mIconSize = mContext.getResources().getDimensionPixelSize(
161                 com.android.internal.R.dimen.starting_surface_icon_size);
162         mDefaultIconSize = mContext.getResources().getDimensionPixelSize(
163                 com.android.internal.R.dimen.starting_surface_default_icon_size);
164         mBrandingImageWidth = mContext.getResources().getDimensionPixelSize(
165                 com.android.wm.shell.R.dimen.starting_surface_brand_image_width);
166         mBrandingImageHeight = mContext.getResources().getDimensionPixelSize(
167                 com.android.wm.shell.R.dimen.starting_surface_brand_image_height);
168         mMainWindowShiftLength = mContext.getResources().getDimensionPixelSize(
169                 com.android.wm.shell.R.dimen.starting_surface_exit_animation_window_shift_length);
170     }
171 
172     /**
173      * @return Current system background color.
174      */
getSystemBGColor()175     public static int getSystemBGColor() {
176         final Context systemContext = ActivityThread.currentApplication();
177         if (systemContext == null) {
178             Slog.e(TAG, "System context does not exist!");
179             return Color.BLACK;
180         }
181         final Resources res = systemContext.getResources();
182         return res.getColor(com.android.wm.shell.R.color.splash_window_background_default);
183     }
184 
185     /**
186      * Estimate the background color of the app splash screen, this may take a while so use it only
187      * if there is no starting window exists for that context.
188      **/
estimateTaskBackgroundColor(Context context)189     int estimateTaskBackgroundColor(Context context) {
190         final SplashScreenWindowAttrs windowAttrs = new SplashScreenWindowAttrs();
191         getWindowAttrs(context, windowAttrs);
192         return peekWindowBGColor(context, windowAttrs);
193     }
194 
createDefaultBackgroundDrawable()195     private static Drawable createDefaultBackgroundDrawable() {
196         return new ColorDrawable(getSystemBGColor());
197     }
198 
199     /** Extract the window background color from {@code attrs}. */
peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs)200     private static int peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs) {
201         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "peekWindowBGColor");
202         final Drawable themeBGDrawable;
203         if (attrs.mWindowBgColor != 0) {
204             themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor);
205         } else if (attrs.mWindowBgResId != 0) {
206             themeBGDrawable = context.getDrawable(attrs.mWindowBgResId);
207         } else {
208             themeBGDrawable = createDefaultBackgroundDrawable();
209             Slog.w(TAG, "Window background does not exist, using " + themeBGDrawable);
210         }
211         final int estimatedWindowBGColor = estimateWindowBGColor(themeBGDrawable);
212         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
213         return estimatedWindowBGColor;
214     }
215 
estimateWindowBGColor(Drawable themeBGDrawable)216     private static int estimateWindowBGColor(Drawable themeBGDrawable) {
217         final DrawableColorTester themeBGTester = new DrawableColorTester(
218                 themeBGDrawable, DrawableColorTester.TRANSPARENT_FILTER /* filterType */);
219         if (themeBGTester.passFilterRatio() == 0) {
220             // the window background is transparent, unable to draw
221             Slog.w(TAG, "Window background is transparent, fill background with black color");
222             return getSystemBGColor();
223         } else {
224             return themeBGTester.getDominateColor();
225         }
226     }
227 
peekLegacySplashscreenContent(Context context, SplashScreenWindowAttrs attrs)228     private static Drawable peekLegacySplashscreenContent(Context context,
229             SplashScreenWindowAttrs attrs) {
230         final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
231         final int resId = safeReturnAttrDefault((def) ->
232                 a.getResourceId(R.styleable.Window_windowSplashscreenContent, def), 0);
233         a.recycle();
234         if (resId != 0) {
235             return context.getDrawable(resId);
236         }
237         if (attrs.mWindowBgResId != 0) {
238             return context.getDrawable(attrs.mWindowBgResId);
239         }
240         return null;
241     }
242 
makeSplashScreenContentView(Context context, ActivityInfo ai, @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer)243     private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai,
244             @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) {
245         updateDensity();
246 
247         getWindowAttrs(context, mTmpAttrs);
248         mLastPackageContextConfigHash = context.getResources().getConfiguration().hashCode();
249 
250         final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
251                 ? peekLegacySplashscreenContent(context, mTmpAttrs) : null;
252         final int themeBGColor = legacyDrawable != null
253                 ? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable))
254                 : getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs));
255         return new StartingWindowViewBuilder(context, ai)
256                 .setWindowBGColor(themeBGColor)
257                 .overlayDrawable(legacyDrawable)
258                 .chooseStyle(suggestType)
259                 .setUiThreadInitConsumer(uiThreadInitConsumer)
260                 .build();
261     }
262 
getBGColorFromCache(ActivityInfo ai, IntSupplier windowBgColorSupplier)263     private int getBGColorFromCache(ActivityInfo ai, IntSupplier windowBgColorSupplier) {
264         return mColorCache.getWindowColor(ai.packageName, mLastPackageContextConfigHash,
265                 mTmpAttrs.mWindowBgColor, mTmpAttrs.mWindowBgResId, windowBgColorSupplier).mBgColor;
266     }
267 
safeReturnAttrDefault(UnaryOperator<T> getMethod, T def)268     private static <T> T safeReturnAttrDefault(UnaryOperator<T> getMethod, T def) {
269         try {
270             return getMethod.apply(def);
271         } catch (RuntimeException e) {
272             Slog.w(TAG, "Get attribute fail, return default: " + e.getMessage());
273             return def;
274         }
275     }
276 
277     /**
278      * Get the {@link SplashScreenWindowAttrs} from {@code context} and fill them into
279      * {@code attrs}.
280      */
getWindowAttrs(Context context, SplashScreenWindowAttrs attrs)281     private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
282         final TypedArray typedArray = context.obtainStyledAttributes(
283                 com.android.internal.R.styleable.Window);
284         attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
285         attrs.mWindowBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
286                 R.styleable.Window_windowSplashScreenBackground, def),
287                 Color.TRANSPARENT);
288         attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
289                 R.styleable.Window_windowSplashScreenAnimatedIcon), null);
290         attrs.mAnimationDuration = safeReturnAttrDefault((def) -> typedArray.getInt(
291                 R.styleable.Window_windowSplashScreenAnimationDuration, def), 0);
292         attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable(
293                 R.styleable.Window_windowSplashScreenBrandingImage), null);
294         attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
295                 R.styleable.Window_windowSplashScreenIconBackgroundColor, def),
296                 Color.TRANSPARENT);
297         typedArray.recycle();
298         if (DEBUG) {
299             Slog.d(TAG, "window attributes color: "
300                     + Integer.toHexString(attrs.mWindowBgColor)
301                     + " icon " + attrs.mSplashScreenIcon + " duration " + attrs.mAnimationDuration
302                     + " brandImage " + attrs.mBrandingImage);
303         }
304     }
305 
306     /** Creates the wrapper with system theme to avoid unexpected styles from app. */
createViewContextWrapper(Context appContext)307     ContextThemeWrapper createViewContextWrapper(Context appContext) {
308         return new ContextThemeWrapper(appContext, mContext.getTheme());
309     }
310 
311     /** The configuration of the splash screen window. */
312     public static class SplashScreenWindowAttrs {
313         private int mWindowBgResId = 0;
314         private int mWindowBgColor = Color.TRANSPARENT;
315         private Drawable mSplashScreenIcon = null;
316         private Drawable mBrandingImage = null;
317         private int mIconBgColor = Color.TRANSPARENT;
318         private int mAnimationDuration = 0;
319     }
320 
321     private class StartingWindowViewBuilder {
322         private final Context mContext;
323         private final ActivityInfo mActivityInfo;
324 
325         private Drawable mOverlayDrawable;
326         private int mSuggestType;
327         private int mThemeColor;
328         private Drawable[] mFinalIconDrawables;
329         private int mFinalIconSize = mIconSize;
330         private Consumer<Runnable> mUiThreadInitTask;
331 
StartingWindowViewBuilder(@onNull Context context, @NonNull ActivityInfo aInfo)332         StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) {
333             mContext = context;
334             mActivityInfo = aInfo;
335         }
336 
setWindowBGColor(@olorInt int background)337         StartingWindowViewBuilder setWindowBGColor(@ColorInt int background) {
338             mThemeColor = background;
339             return this;
340         }
341 
overlayDrawable(Drawable overlay)342         StartingWindowViewBuilder overlayDrawable(Drawable overlay) {
343             mOverlayDrawable = overlay;
344             return this;
345         }
346 
chooseStyle(int suggestType)347         StartingWindowViewBuilder chooseStyle(int suggestType) {
348             mSuggestType = suggestType;
349             return this;
350         }
351 
setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask)352         StartingWindowViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) {
353             mUiThreadInitTask = uiThreadInitTask;
354             return this;
355         }
356 
build()357         SplashScreenView build() {
358             Drawable iconDrawable;
359             final int animationDuration;
360             if (mSuggestType == STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
361                     || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
362                 // empty or legacy splash screen case
363                 animationDuration = 0;
364                 mFinalIconSize = 0;
365             } else if (mTmpAttrs.mSplashScreenIcon != null) {
366                 // Using the windowSplashScreenAnimatedIcon attribute
367                 iconDrawable = mTmpAttrs.mSplashScreenIcon;
368                 animationDuration = mTmpAttrs.mAnimationDuration;
369 
370                 // There is no background below the icon, so scale the icon up
371                 if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT
372                         || mTmpAttrs.mIconBgColor == mThemeColor) {
373                     mFinalIconSize *= NO_BACKGROUND_SCALE;
374                 }
375                 createIconDrawable(iconDrawable, false);
376             } else {
377                 final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
378                 final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
379                 final int scaledIconDpi =
380                         (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
381                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
382                 iconDrawable = mIconProvider.getIcon(mActivityInfo, scaledIconDpi);
383                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
384                 if (iconDrawable == null) {
385                     iconDrawable = mContext.getPackageManager().getDefaultActivityIcon();
386                 }
387                 if (!processAdaptiveIcon(iconDrawable)) {
388                     if (DEBUG) {
389                         Slog.d(TAG, "The icon is not an AdaptiveIconDrawable");
390                     }
391                     Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "legacy_icon_factory");
392                     final ShapeIconFactory factory = new ShapeIconFactory(
393                             SplashscreenContentDrawer.this.mContext,
394                             scaledIconDpi, mFinalIconSize);
395                     final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(
396                             iconDrawable, true /* shrinkNonAdaptiveIcons */);
397                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
398                     createIconDrawable(new BitmapDrawable(bitmap), true);
399                 }
400                 animationDuration = 0;
401             }
402 
403             return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration,
404                     mUiThreadInitTask);
405         }
406 
407         private class ShapeIconFactory extends BaseIconFactory {
ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize)408             protected ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
409                 super(context, fillResIconDpi, iconBitmapSize, true /* shapeDetection */);
410             }
411         }
412 
createIconDrawable(Drawable iconDrawable, boolean legacy)413         private void createIconDrawable(Drawable iconDrawable, boolean legacy) {
414             if (legacy) {
415                 mFinalIconDrawables = SplashscreenIconDrawableFactory.makeLegacyIconDrawable(
416                         iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler);
417             } else {
418                 mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable(
419                         mTmpAttrs.mIconBgColor, mThemeColor,
420                         iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler);
421             }
422         }
423 
processAdaptiveIcon(Drawable iconDrawable)424         private boolean processAdaptiveIcon(Drawable iconDrawable) {
425             if (!(iconDrawable instanceof AdaptiveIconDrawable)) {
426                 return false;
427             }
428 
429             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "processAdaptiveIcon");
430             final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) iconDrawable;
431             final Drawable iconForeground = adaptiveIconDrawable.getForeground();
432             final ColorCache.IconColor iconColor = mColorCache.getIconColor(
433                     mActivityInfo.packageName, mActivityInfo.getIconResource(),
434                     mLastPackageContextConfigHash,
435                     () -> new DrawableColorTester(iconForeground,
436                             DrawableColorTester.TRANSLUCENT_FILTER /* filterType */),
437                     () -> new DrawableColorTester(adaptiveIconDrawable.getBackground()));
438 
439             if (DEBUG) {
440                 Slog.d(TAG, "FgMainColor=" + Integer.toHexString(iconColor.mFgColor)
441                         + " BgMainColor=" + Integer.toHexString(iconColor.mBgColor)
442                         + " IsBgComplex=" + iconColor.mIsBgComplex
443                         + " FromCache=" + (iconColor.mReuseCount > 0)
444                         + " ThemeColor=" + Integer.toHexString(mThemeColor));
445             }
446 
447             // Only draw the foreground of AdaptiveIcon to the splash screen if below condition
448             // meet:
449             // A. The background of the adaptive icon is not complicated. If it is complicated,
450             // it may contain some information, and
451             // B. The background of the adaptive icon is similar to the theme color, or
452             // C. The background of the adaptive icon is grayscale, and the foreground of the
453             // adaptive icon forms a certain contrast with the theme color.
454             // D. Didn't specify icon background color.
455             if (!iconColor.mIsBgComplex && mTmpAttrs.mIconBgColor == Color.TRANSPARENT
456                     && (isRgbSimilarInHsv(mThemeColor, iconColor.mBgColor)
457                             || (iconColor.mIsBgGrayscale
458                                     && !isRgbSimilarInHsv(mThemeColor, iconColor.mFgColor)))) {
459                 if (DEBUG) {
460                     Slog.d(TAG, "makeSplashScreenContentView: choose fg icon");
461                 }
462                 // Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
463                 // scale by 192/160 if we only draw adaptiveIcon's foreground.
464                 final float noBgScale =
465                         iconColor.mFgNonTranslucentRatio < ENLARGE_FOREGROUND_ICON_THRESHOLD
466                                 ? NO_BACKGROUND_SCALE : 1f;
467                 // Using AdaptiveIconDrawable here can help keep the shape consistent with the
468                 // current settings.
469                 mFinalIconSize = (int) (0.5f + mIconSize * noBgScale);
470                 createIconDrawable(iconForeground, false);
471             } else {
472                 if (DEBUG) {
473                     Slog.d(TAG, "makeSplashScreenContentView: draw whole icon");
474                 }
475                 createIconDrawable(iconDrawable, false);
476             }
477             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
478             return true;
479         }
480 
481         private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
482                 int animationDuration, Consumer<Runnable> uiThreadInitTask) {
483             Drawable foreground = null;
484             Drawable background = null;
485             if (iconDrawable != null) {
486                 foreground = iconDrawable.length > 0 ? iconDrawable[0] : null;
487                 background = iconDrawable.length > 1 ? iconDrawable[1] : null;
488             }
489 
490             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon");
491             final ContextThemeWrapper wrapper = createViewContextWrapper(mContext);
492             final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper)
493                     .setBackgroundColor(mThemeColor)
494                     .setOverlayDrawable(mOverlayDrawable)
495                     .setIconSize(iconSize)
496                     .setIconBackground(background)
497                     .setCenterViewDrawable(foreground)
498                     .setAnimationDurationMillis(animationDuration)
499                     .setUiThreadInitConsumer(uiThreadInitTask);
500 
501             if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
502                     && mTmpAttrs.mBrandingImage != null) {
503                 builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,
504                         mBrandingImageHeight);
505             }
506             final SplashScreenView splashScreenView = builder.build();
507             if (DEBUG) {
508                 Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView);
509             }
510             if (mSuggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
511                 splashScreenView.addOnAttachStateChangeListener(
512                         new View.OnAttachStateChangeListener() {
513                             @Override
514                             public void onViewAttachedToWindow(View v) {
515                                 SplashScreenView.applySystemBarsContrastColor(
516                                         v.getWindowInsetsController(),
517                                         splashScreenView.getInitBackgroundColor());
518                             }
519 
520                             @Override
521                             public void onViewDetachedFromWindow(View v) {
522                             }
523                         });
524             }
525 
526             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
527             return splashScreenView;
528         }
529     }
530 
isRgbSimilarInHsv(int a, int b)531     private static boolean isRgbSimilarInHsv(int a, int b) {
532         if (a == b) {
533             return true;
534         }
535         final float lumA = Color.luminance(a);
536         final float lumB = Color.luminance(b);
537         final float contrastRatio = lumA > lumB
538                 ? (lumA + 0.05f) / (lumB + 0.05f) : (lumB + 0.05f) / (lumA + 0.05f);
539         if (DEBUG) {
540             Slog.d(TAG, "isRgbSimilarInHsv a: " + Integer.toHexString(a)
541                     + " b " + Integer.toHexString(b) + " contrast ratio: " + contrastRatio);
542         }
543         if (contrastRatio < 2) {
544             return true;
545         }
546 
547         final float[] aHsv = new float[3];
548         final float[] bHsv = new float[3];
549         Color.colorToHSV(a, aHsv);
550         Color.colorToHSV(b, bHsv);
551         // Minimum degree of the hue between two colors, the result range is 0-180.
552         int minAngle = (int) Math.abs(aHsv[0] - bHsv[0]);
553         minAngle = (minAngle + 180) % 360 - 180;
554 
555         // Calculate the difference between two colors based on the HSV dimensions.
556         final float normalizeH = minAngle / 180f;
557         final double squareH = Math.pow(normalizeH, 2);
558         final double squareS = Math.pow(aHsv[1] - bHsv[1], 2);
559         final double squareV = Math.pow(aHsv[2] - bHsv[2], 2);
560         final double square = squareH + squareS + squareV;
561         final double mean = square / 3;
562         final double root = Math.sqrt(mean);
563         if (DEBUG) {
564             Slog.d(TAG, "hsvDiff " + minAngle
565                     + " ah " + aHsv[0] + " bh " + bHsv[0]
566                     + " as " + aHsv[1] + " bs " + bHsv[1]
567                     + " av " + aHsv[2] + " bv " + bHsv[2]
568                     + " sqH " + squareH + " sqS " + squareS + " sqV " + squareV
569                     + " root " + root);
570         }
571         return root < 0.1;
572     }
573 
574     private static class DrawableColorTester {
575         private static final int NO_ALPHA_FILTER = 0;
576         // filter out completely invisible pixels
577         private static final int TRANSPARENT_FILTER = 1;
578         // filter out translucent and invisible pixels
579         private static final int TRANSLUCENT_FILTER = 2;
580 
581         @IntDef(flag = true, value = {
582                 NO_ALPHA_FILTER,
583                 TRANSPARENT_FILTER,
584                 TRANSLUCENT_FILTER
585         })
586         private @interface QuantizerFilterType {}
587 
588         private final ColorTester mColorChecker;
589 
DrawableColorTester(Drawable drawable)590         DrawableColorTester(Drawable drawable) {
591             this(drawable, NO_ALPHA_FILTER /* filterType */);
592         }
593 
DrawableColorTester(Drawable drawable, @QuantizerFilterType int filterType)594         DrawableColorTester(Drawable drawable, @QuantizerFilterType int filterType) {
595             // Some applications use LayerDrawable for their windowBackground. To ensure that we
596             // only get the real background, so that the color is not affected by the alpha of the
597             // upper layer, try to get the lower layer here. This can also speed up the calculation.
598             if (drawable instanceof LayerDrawable) {
599                 LayerDrawable layerDrawable = (LayerDrawable) drawable;
600                 if (layerDrawable.getNumberOfLayers() > 0) {
601                     if (DEBUG) {
602                         Slog.d(TAG, "replace drawable with bottom layer drawable");
603                     }
604                     drawable = layerDrawable.getDrawable(0);
605                 }
606             }
607             if (drawable == null) {
608                 mColorChecker = new SingleColorTester(
609                         (ColorDrawable) createDefaultBackgroundDrawable());
610             } else {
611                 mColorChecker = drawable instanceof ColorDrawable
612                         ? new SingleColorTester((ColorDrawable) drawable)
613                         : new ComplexDrawableTester(drawable, filterType);
614             }
615         }
616 
passFilterRatio()617         public float passFilterRatio() {
618             return mColorChecker.passFilterRatio();
619         }
620 
isComplexColor()621         public boolean isComplexColor() {
622             return mColorChecker.isComplexColor();
623         }
624 
getDominateColor()625         public int getDominateColor() {
626             return mColorChecker.getDominantColor();
627         }
628 
isGrayscale()629         public boolean isGrayscale() {
630             return mColorChecker.isGrayscale();
631         }
632 
633         /**
634          * A help class to check the color information from a Drawable.
635          */
636         private interface ColorTester {
passFilterRatio()637             float passFilterRatio();
638 
isComplexColor()639             boolean isComplexColor();
640 
getDominantColor()641             int getDominantColor();
642 
isGrayscale()643             boolean isGrayscale();
644         }
645 
isGrayscaleColor(int color)646         private static boolean isGrayscaleColor(int color) {
647             final int red = Color.red(color);
648             final int green = Color.green(color);
649             final int blue = Color.blue(color);
650             return red == green && green == blue;
651         }
652 
653         /**
654          * For ColorDrawable only. There will be only one color so don't spend too much resource for
655          * it.
656          */
657         private static class SingleColorTester implements ColorTester {
658             private final ColorDrawable mColorDrawable;
659 
SingleColorTester(@onNull ColorDrawable drawable)660             SingleColorTester(@NonNull ColorDrawable drawable) {
661                 mColorDrawable = drawable;
662             }
663 
664             @Override
passFilterRatio()665             public float passFilterRatio() {
666                 final int alpha = mColorDrawable.getAlpha();
667                 return (float) (alpha / 255);
668             }
669 
670             @Override
isComplexColor()671             public boolean isComplexColor() {
672                 return false;
673             }
674 
675             @Override
getDominantColor()676             public int getDominantColor() {
677                 return mColorDrawable.getColor();
678             }
679 
680             @Override
isGrayscale()681             public boolean isGrayscale() {
682                 return isGrayscaleColor(mColorDrawable.getColor());
683             }
684         }
685 
686         /**
687          * For any other Drawable except ColorDrawable. This will use the Palette API to check the
688          * color information and use a quantizer to filter out transparent colors when needed.
689          */
690         private static class ComplexDrawableTester implements ColorTester {
691             private static final int MAX_BITMAP_SIZE = 40;
692             private final Palette mPalette;
693             private final boolean mFilterTransparent;
694             private static final AlphaFilterQuantizer ALPHA_FILTER_QUANTIZER =
695                     new AlphaFilterQuantizer();
696 
697             /**
698              * @param drawable The test target.
699              * @param filterType Targeting to filter out transparent or translucent pixels,
700              *                   this would be needed if want to check
701              *                   {@link #passFilterRatio()}, also affecting the estimated result
702              *                   of the dominant color.
703              */
ComplexDrawableTester(Drawable drawable, @QuantizerFilterType int filterType)704             ComplexDrawableTester(Drawable drawable, @QuantizerFilterType int filterType) {
705                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ComplexDrawableTester");
706                 final Rect initialBounds = drawable.copyBounds();
707                 int width = drawable.getIntrinsicWidth();
708                 int height = drawable.getIntrinsicHeight();
709                 // Some drawables do not have intrinsic dimensions
710                 if (width <= 0 || height <= 0) {
711                     width = MAX_BITMAP_SIZE;
712                     height = MAX_BITMAP_SIZE;
713                 } else {
714                     width = Math.min(width, MAX_BITMAP_SIZE);
715                     height = Math.min(height, MAX_BITMAP_SIZE);
716                 }
717 
718                 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
719                 final Canvas bmpCanvas = new Canvas(bitmap);
720                 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
721                 drawable.draw(bmpCanvas);
722                 // restore to original bounds
723                 drawable.setBounds(initialBounds);
724 
725                 final Palette.Builder builder;
726                 // The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
727                 // sometimes we will need this information to know if this Drawable object is
728                 // transparent.
729                 mFilterTransparent = filterType != NO_ALPHA_FILTER;
730                 if (mFilterTransparent) {
731                     ALPHA_FILTER_QUANTIZER.setFilter(filterType);
732                     builder = new Palette.Builder(bitmap, ALPHA_FILTER_QUANTIZER)
733                             .maximumColorCount(5);
734                 } else {
735                     builder = new Palette.Builder(bitmap, null)
736                             .maximumColorCount(5);
737                 }
738                 mPalette = builder.generate();
739                 bitmap.recycle();
740                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
741             }
742 
743             @Override
passFilterRatio()744             public float passFilterRatio() {
745                 return mFilterTransparent ? ALPHA_FILTER_QUANTIZER.mPassFilterRatio : 1;
746             }
747 
748             @Override
isComplexColor()749             public boolean isComplexColor() {
750                 return mPalette.getSwatches().size() > 1;
751             }
752 
753             @Override
getDominantColor()754             public int getDominantColor() {
755                 final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
756                 if (mainSwatch != null) {
757                     return mainSwatch.getInt();
758                 }
759                 return Color.BLACK;
760             }
761 
762             @Override
isGrayscale()763             public boolean isGrayscale() {
764                 final List<Palette.Swatch> swatches = mPalette.getSwatches();
765                 if (swatches != null) {
766                     for (int i = swatches.size() - 1; i >= 0; i--) {
767                         Palette.Swatch swatch = swatches.get(i);
768                         if (!isGrayscaleColor(swatch.getInt())) {
769                             return false;
770                         }
771                     }
772                 }
773                 return true;
774             }
775 
776             private static class AlphaFilterQuantizer implements Quantizer {
777                 private static final int NON_TRANSPARENT = 0xFF000000;
778                 private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
779                 private final IntPredicate mTransparentFilter = i -> (i & NON_TRANSPARENT) != 0;
780                 private final IntPredicate mTranslucentFilter = i ->
781                         (i & NON_TRANSPARENT) == NON_TRANSPARENT;
782 
783                 private IntPredicate mFilter = mTransparentFilter;
784                 private float mPassFilterRatio;
785 
setFilter(@uantizerFilterType int filterType)786                 void setFilter(@QuantizerFilterType int filterType) {
787                     switch (filterType) {
788                         case TRANSLUCENT_FILTER:
789                             mFilter = mTranslucentFilter;
790                             break;
791                         case TRANSPARENT_FILTER:
792                         default:
793                             mFilter = mTransparentFilter;
794                             break;
795                     }
796                 }
797 
798                 @Override
quantize(final int[] pixels, final int maxColors)799                 public void quantize(final int[] pixels, final int maxColors) {
800                     mPassFilterRatio = 0;
801                     int realSize = 0;
802                     for (int i = pixels.length - 1; i > 0; i--) {
803                         if (mFilter.test(pixels[i])) {
804                             realSize++;
805                         }
806                     }
807                     if (realSize == 0) {
808                         if (DEBUG) {
809                             Slog.d(TAG, "quantize: this is pure transparent image");
810                         }
811                         mInnerQuantizer.quantize(pixels, maxColors);
812                         return;
813                     }
814                     mPassFilterRatio = (float) realSize / pixels.length;
815                     final int[] samplePixels = new int[realSize];
816                     int rowIndex = 0;
817                     for (int i = pixels.length - 1; i > 0; i--) {
818                         if (mFilter.test(pixels[i])) {
819                             samplePixels[rowIndex] = pixels[i];
820                             rowIndex++;
821                         }
822                     }
823                     mInnerQuantizer.quantize(samplePixels, maxColors);
824                 }
825 
826                 @Override
getQuantizedColors()827                 public List<Palette.Swatch> getQuantizedColors() {
828                     return mInnerQuantizer.getQuantizedColors();
829                 }
830             }
831         }
832     }
833 
834     /** Cache the result of {@link DrawableColorTester} to reduce expensive calculation. */
835     @VisibleForTesting
836     static class ColorCache extends BroadcastReceiver {
837         /**
838          * The color may be different according to resource id and configuration (e.g. night mode),
839          * so this allows to cache more than one color per package.
840          */
841         private static final int CACHE_SIZE = 2;
842 
843         /** The computed colors of packages. */
844         private final ArrayMap<String, Colors> mColorMap = new ArrayMap<>();
845 
846         private static class Colors {
847             final WindowColor[] mWindowColors = new WindowColor[CACHE_SIZE];
848             final IconColor[] mIconColors = new IconColor[CACHE_SIZE];
849         }
850 
851         private static class Cache {
852             /** The hash used to check whether this cache is hit. */
853             final int mHash;
854 
855             /** The number of times this cache has been reused. */
856             int mReuseCount;
857 
Cache(int hash)858             Cache(int hash) {
859                 mHash = hash;
860             }
861         }
862 
863         static class WindowColor extends Cache {
864             final int mBgColor;
865 
WindowColor(int hash, int bgColor)866             WindowColor(int hash, int bgColor) {
867                 super(hash);
868                 mBgColor = bgColor;
869             }
870         }
871 
872         static class IconColor extends Cache {
873             final int mFgColor;
874             final int mBgColor;
875             final boolean mIsBgComplex;
876             final boolean mIsBgGrayscale;
877             final float mFgNonTranslucentRatio;
878 
IconColor(int hash, int fgColor, int bgColor, boolean isBgComplex, boolean isBgGrayscale, float fgNonTranslucnetRatio)879             IconColor(int hash, int fgColor, int bgColor, boolean isBgComplex,
880                     boolean isBgGrayscale, float fgNonTranslucnetRatio) {
881                 super(hash);
882                 mFgColor = fgColor;
883                 mBgColor = bgColor;
884                 mIsBgComplex = isBgComplex;
885                 mIsBgGrayscale = isBgGrayscale;
886                 mFgNonTranslucentRatio = fgNonTranslucnetRatio;
887             }
888         }
889 
ColorCache(Context context, Handler handler)890         ColorCache(Context context, Handler handler) {
891             // This includes reinstall and uninstall.
892             final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
893             filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
894             context.registerReceiverAsUser(this, UserHandle.ALL, filter,
895                     null /* broadcastPermission */, handler);
896         }
897 
898         @Override
onReceive(Context context, Intent intent)899         public void onReceive(Context context, Intent intent) {
900             final Uri packageUri = intent.getData();
901             if (packageUri != null) {
902                 mColorMap.remove(packageUri.getEncodedSchemeSpecificPart());
903             }
904         }
905 
906         /**
907          * Gets the existing cache if the hash matches. If null is returned, the caller can use
908          * outLeastUsedIndex to put the new cache.
909          */
getCache(T[] caches, int hash, int[] outLeastUsedIndex)910         private static <T extends Cache> T getCache(T[] caches, int hash, int[] outLeastUsedIndex) {
911             int minReuseCount = Integer.MAX_VALUE;
912             for (int i = 0; i < CACHE_SIZE; i++) {
913                 final T cache = caches[i];
914                 if (cache == null) {
915                     // Empty slot has the highest priority to put new cache.
916                     minReuseCount = -1;
917                     outLeastUsedIndex[0] = i;
918                     continue;
919                 }
920                 if (cache.mHash == hash) {
921                     cache.mReuseCount++;
922                     return cache;
923                 }
924                 if (cache.mReuseCount < minReuseCount) {
925                     minReuseCount = cache.mReuseCount;
926                     outLeastUsedIndex[0] = i;
927                 }
928             }
929             return null;
930         }
931 
getWindowColor(String packageName, int configHash, int windowBgColor, int windowBgResId, IntSupplier windowBgColorSupplier)932         @NonNull WindowColor getWindowColor(String packageName, int configHash, int windowBgColor,
933                 int windowBgResId, IntSupplier windowBgColorSupplier) {
934             Colors colors = mColorMap.get(packageName);
935             int hash = 31 * configHash + windowBgColor;
936             hash = 31 * hash + windowBgResId;
937             final int[] leastUsedIndex = { 0 };
938             if (colors != null) {
939                 final WindowColor windowColor = getCache(colors.mWindowColors, hash,
940                         leastUsedIndex);
941                 if (windowColor != null) {
942                     return windowColor;
943                 }
944             } else {
945                 colors = new Colors();
946                 mColorMap.put(packageName, colors);
947             }
948             final WindowColor windowColor = new WindowColor(hash, windowBgColorSupplier.getAsInt());
949             colors.mWindowColors[leastUsedIndex[0]] = windowColor;
950             return windowColor;
951         }
952 
getIconColor(String packageName, int configHash, int iconResId, Supplier<DrawableColorTester> fgColorTesterSupplier, Supplier<DrawableColorTester> bgColorTesterSupplier)953         @NonNull IconColor getIconColor(String packageName, int configHash, int iconResId,
954                 Supplier<DrawableColorTester> fgColorTesterSupplier,
955                 Supplier<DrawableColorTester> bgColorTesterSupplier) {
956             Colors colors = mColorMap.get(packageName);
957             final int hash = configHash * 31 + iconResId;
958             final int[] leastUsedIndex = { 0 };
959             if (colors != null) {
960                 final IconColor iconColor = getCache(colors.mIconColors, hash, leastUsedIndex);
961                 if (iconColor != null) {
962                     return iconColor;
963                 }
964             } else {
965                 colors = new Colors();
966                 mColorMap.put(packageName, colors);
967             }
968             final DrawableColorTester fgTester = fgColorTesterSupplier.get();
969             final DrawableColorTester bgTester = bgColorTesterSupplier.get();
970             final IconColor iconColor = new IconColor(hash, fgTester.getDominateColor(),
971                     bgTester.getDominateColor(), bgTester.isComplexColor(), bgTester.isGrayscale(),
972                     fgTester.passFilterRatio());
973             colors.mIconColors[leastUsedIndex[0]] = iconColor;
974             return iconColor;
975         }
976     }
977 
978     /**
979      * Create and play the default exit animation for splash screen view.
980      */
applyExitAnimation(SplashScreenView view, SurfaceControl leash, Rect frame, Runnable finishCallback)981     void applyExitAnimation(SplashScreenView view, SurfaceControl leash,
982             Rect frame, Runnable finishCallback) {
983         final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, view,
984                 leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback);
985         animation.startAnimations();
986     }
987 }
988