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