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