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