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