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