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