1 /* 2 * Copyright (C) 2023 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 android.server.wm.animations; 18 19 import static android.server.wm.ComponentNameUtils.getWindowName; 20 import static android.view.Display.DEFAULT_DISPLAY; 21 import static android.view.WindowInsets.Type.systemBars; 22 23 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 24 25 import static com.google.common.truth.Truth.assertThat; 26 27 import static org.junit.Assert.assertTrue; 28 import static org.junit.Assert.fail; 29 import static org.junit.Assume.assumeTrue; 30 import static org.mockito.Mockito.spy; 31 import static org.mockito.Mockito.timeout; 32 33 import android.app.Activity; 34 import android.content.ComponentName; 35 import android.graphics.Bitmap; 36 import android.graphics.Color; 37 import android.graphics.Insets; 38 import android.graphics.Rect; 39 import android.os.Bundle; 40 import android.platform.test.annotations.Presubmit; 41 import android.provider.Settings; 42 import android.server.wm.DumpOnFailure; 43 import android.server.wm.WindowManagerState; 44 import android.server.wm.WindowManagerTestBase; 45 import android.server.wm.cts.R; 46 import android.server.wm.settings.SettingsSession; 47 import android.util.TypedValue; 48 import android.view.RoundedCorner; 49 import android.view.View; 50 import android.view.WindowInsets; 51 import android.view.WindowManager; 52 53 import androidx.annotation.ColorInt; 54 import androidx.annotation.Nullable; 55 import androidx.core.view.WindowCompat; 56 import androidx.test.filters.FlakyTest; 57 import androidx.test.rule.ActivityTestRule; 58 import androidx.test.uiautomator.UiDevice; 59 60 import com.android.compatibility.common.util.ApiTest; 61 import com.android.compatibility.common.util.ColorUtils; 62 import com.android.compatibility.common.util.PollingCheck; 63 64 import org.junit.Before; 65 import org.junit.Rule; 66 import org.junit.Test; 67 import org.junit.rules.RuleChain; 68 import org.junit.rules.TestRule; 69 import org.mockito.Mockito; 70 71 import java.util.function.Consumer; 72 73 @Presubmit 74 public class BlurTests extends WindowManagerTestBase { 75 private static final int BACKGROUND_BLUR_PX = 80; 76 private static final int BLUR_BEHIND_PX = 40; 77 private static final int NO_BLUR_BACKGROUND_COLOR = 0xFF550055; 78 private static final int BROADCAST_WAIT_TIMEOUT = 300; 79 80 private Rect mBackgroundActivityBounds; 81 private Rect mPixelTestBounds; 82 83 private final DumpOnFailure mDumpOnFailure = new DumpOnFailure(); 84 85 private final TestRule mEnableBlurRule = SettingsSession.overrideForTest( 86 Settings.Global.getUriFor(Settings.Global.DISABLE_WINDOW_BLURS), 87 Settings.Global::getInt, 88 Settings.Global::putInt, 89 0); 90 private final TestRule mDisableTransitionAnimationRule = SettingsSession.overrideForTest( 91 Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), 92 Settings.Global::getFloat, 93 Settings.Global::putFloat, 94 0f); 95 96 private final ActivityTestRule<BackgroundActivity> mBackgroundActivity = 97 new ActivityTestRule<>(BackgroundActivity.class); 98 99 @Rule 100 public final TestRule methodRules = RuleChain.outerRule(mDumpOnFailure) 101 .around(mEnableBlurRule) 102 .around(mDisableTransitionAnimationRule) 103 .around(mBackgroundActivity); 104 105 @Before setUp()106 public void setUp() { 107 assumeTrue(supportsBlur()); 108 ComponentName cn = mBackgroundActivity.getActivity().getComponentName(); 109 waitAndAssertResumedActivity(cn, cn + " must be resumed"); 110 mBackgroundActivity.getActivity().waitAndAssertWindowFocusState(true); 111 112 // Use the background activity's bounds when taking the device screenshot. 113 // This is needed for multi-screen devices (foldables) where 114 // the launched activity covers just one screen 115 WindowManagerState.WindowState windowState = mWmState.getWindowState(cn); 116 WindowManagerState.Activity act = mWmState.getActivity(cn); 117 mBackgroundActivityBounds = act.getBounds(); 118 119 // Wait for the first frame *after* the splash screen is removed to take screenshots. 120 // Currently there isn't a definite event / callback for this. 121 mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY); 122 waitForActivityIdle(mBackgroundActivity.getActivity()); 123 124 insetGivenFrame(windowState, 125 insetsSource -> (insetsSource.is(WindowInsets.Type.captionBar())), 126 mBackgroundActivityBounds); 127 128 // Exclude rounded corners from screenshot comparisons. 129 mPixelTestBounds = new Rect(mBackgroundActivityBounds); 130 mPixelTestBounds.inset(mBackgroundActivity.getActivity().getInsetsToBeIgnored()); 131 132 // Basic checks common to all tests 133 verifyOnlyBackgroundImageVisible(); 134 assertTrue(mContext.getSystemService(WindowManager.class).isCrossWindowBlurEnabled()); 135 } 136 137 @Test 138 @ApiTest(apis = {"android.view.Window#setBackgroundBlurRadius(int)"}) testBackgroundBlurSimple()139 public void testBackgroundBlurSimple() { 140 final BlurActivity blurActivity = startTestActivity(BlurActivity.class); 141 getInstrumentation().runOnMainSync(() -> { 142 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 143 }); 144 145 waitForActivityIdle(blurActivity); 146 147 final Rect windowFrame = getFloatingWindowFrame(blurActivity); 148 assertOnScreenshot(screenshot -> { 149 assertBackgroundBlur(screenshot, windowFrame); 150 }); 151 } 152 153 @Test 154 @ApiTest(apis = {"android.view.WindowManager.LayoutParams#setBlurBehindRadius", 155 "android.R.styleable#Window_windowBlurBehindEnabled"}) testBlurBehindSimple()156 public void testBlurBehindSimple() throws Exception { 157 final BlurActivity blurActivity = startTestActivity(BlurActivity.class); 158 getInstrumentation().runOnMainSync(() -> { 159 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 160 }); 161 waitForActivityIdle(blurActivity); 162 final Rect windowFrame = getFloatingWindowFrame(blurActivity); 163 164 assertOnScreenshot(screenshot -> { 165 assertBlurBehind(screenshot, windowFrame); 166 assertNoBackgroundBlur(screenshot, windowFrame); 167 }); 168 169 getInstrumentation().runOnMainSync(() -> { 170 blurActivity.setBlurBehindRadius(0); 171 }); 172 waitForActivityIdle(blurActivity); 173 174 assertOnScreenshot(screenshot -> { 175 assertNoBlurBehind(screenshot, windowFrame); 176 assertNoBackgroundBlur(screenshot, windowFrame); 177 }); 178 } 179 180 @Test 181 @ApiTest(apis = {"android.view.Window#setBackgroundBlurRadius"}) testNoBackgroundBlurForNonTranslucentWindow()182 public void testNoBackgroundBlurForNonTranslucentWindow() { 183 final BlurActivity blurActivity = startTestActivity(BadBlurActivity.class); 184 getInstrumentation().runOnMainSync(() -> { 185 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 186 blurActivity.setBackgroundColor(Color.TRANSPARENT); 187 }); 188 waitForActivityIdle(blurActivity); 189 190 verifyOnlyBackgroundImageVisible(); 191 } 192 193 @Test 194 @ApiTest(apis = {"android.view.WindowManager.LayoutParams#setBlurBehindRadius", 195 "android.R.styleable#Window_windowBlurBehindEnabled"}) testNoBlurBehindWhenFlagNotSet()196 public void testNoBlurBehindWhenFlagNotSet() { 197 final BlurActivity blurActivity = startTestActivity(BadBlurActivity.class); 198 getInstrumentation().runOnMainSync(() -> { 199 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 200 blurActivity.setBackgroundColor(Color.TRANSPARENT); 201 }); 202 waitForActivityIdle(blurActivity); 203 204 verifyOnlyBackgroundImageVisible(); 205 } 206 207 @Test 208 @ApiTest(apis = {"android.view.WindowManager.LayoutParams#setBlurBehindRadius", 209 "android.R.styleable#Window_windowBlurBehindEnabled"}) testBlurBehindDisabledDynamically()210 public void testBlurBehindDisabledDynamically() { 211 final BlurActivity blurActivity = startTestActivity(BlurActivity.class); 212 getInstrumentation().runOnMainSync(() -> { 213 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 214 }); 215 waitForActivityIdle(blurActivity); 216 final Rect windowFrame = getFloatingWindowFrame(blurActivity); 217 218 assertOnScreenshot(screenshot -> { 219 assertBlurBehind(screenshot, windowFrame); 220 assertNoBackgroundBlur(screenshot, windowFrame); 221 }); 222 223 getInstrumentation().runOnMainSync(() -> { 224 blurActivity.setBlurBehindRadius(0); 225 }); 226 waitForActivityIdle(blurActivity); 227 228 assertOnScreenshot(screenshot -> { 229 assertNoBackgroundBlur(screenshot, windowFrame); 230 assertNoBlurBehind(screenshot, windowFrame); 231 }); 232 233 getInstrumentation().runOnMainSync(() -> { 234 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 235 }); 236 waitForActivityIdle(blurActivity); 237 238 assertOnScreenshot(screenshot -> { 239 assertBlurBehind(screenshot, windowFrame); 240 assertNoBackgroundBlur(screenshot, windowFrame); 241 }); 242 } 243 244 @Test 245 @ApiTest(apis = {"android.view.WindowManager.LayoutParams#setBlurBehindRadius", 246 "android.R.styleable#Window_windowBlurBehindEnabled", 247 "android.view.Window#setBackgroundBlurRadius"}) testBlurBehindAndBackgroundBlur()248 public void testBlurBehindAndBackgroundBlur() { 249 final BlurActivity blurActivity = startTestActivity(BlurActivity.class); 250 getInstrumentation().runOnMainSync(() -> { 251 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 252 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 253 }); 254 waitForActivityIdle(blurActivity); 255 final Rect windowFrame = getFloatingWindowFrame(blurActivity); 256 257 assertOnScreenshot(screenshot -> { 258 assertBlurBehind(screenshot, windowFrame); 259 assertBackgroundBlurOverBlurBehind(screenshot, windowFrame); 260 }); 261 262 getInstrumentation().runOnMainSync(() -> { 263 blurActivity.setBlurBehindRadius(0); 264 blurActivity.setBackgroundBlurRadius(0); 265 }); 266 waitForActivityIdle(blurActivity); 267 268 assertOnScreenshot(screenshot -> { 269 assertNoBackgroundBlur(screenshot, windowFrame); 270 assertNoBlurBehind(screenshot, windowFrame); 271 }); 272 273 getInstrumentation().runOnMainSync(() -> { 274 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 275 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 276 }); 277 waitForActivityIdle(blurActivity); 278 279 assertOnScreenshot(screenshot -> { 280 assertBlurBehind(screenshot, windowFrame); 281 assertBackgroundBlurOverBlurBehind(screenshot, windowFrame); 282 }); 283 } 284 285 @Test 286 @ApiTest(apis = {"android.R.styleable#Window_windowBackgroundBlurRadius", 287 "android.R.styleable#Window_windowBlurBehindRadius", 288 "android.R.styleable#Window_windowBlurBehindEnabled"}) testBlurBehindAndBackgroundBlurSetWithAttributes()289 public void testBlurBehindAndBackgroundBlurSetWithAttributes() { 290 final Activity blurAttrActivity = startTestActivity(BlurAttributesActivity.class); 291 final Rect windowFrame = getFloatingWindowFrame(blurAttrActivity); 292 293 assertOnScreenshot(screenshot -> { 294 assertBlurBehind(screenshot, windowFrame); 295 assertBackgroundBlurOverBlurBehind(screenshot, windowFrame); 296 }); 297 } 298 299 @FlakyTest(bugId = 382609163) 300 @Test 301 @ApiTest( 302 apis = { 303 "android.view.WindowManager.LayoutParams#setBlurBehindRadius", 304 "android.R.styleable#Window_windowBlurBehindEnabled", 305 "android.view.Window#setBackgroundBlurRadius" 306 }) testAllBlurRemovedAndRestoredWhenToggleBlurDisabled()307 public void testAllBlurRemovedAndRestoredWhenToggleBlurDisabled() { 308 final BlurActivity blurActivity = startTestActivity(BlurActivity.class); 309 getInstrumentation().runOnMainSync(() -> { 310 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 311 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 312 }); 313 waitForActivityIdle(blurActivity); 314 final Rect windowFrame = getFloatingWindowFrame(blurActivity); 315 316 assertOnScreenshot(screenshot -> { 317 assertBlurBehind(screenshot, windowFrame); 318 assertBackgroundBlurOverBlurBehind(screenshot, windowFrame); 319 }); 320 321 setAndAssertForceBlurDisabled(true, blurActivity.mBlurEnabledListener); 322 getInstrumentation().runOnMainSync(() -> { 323 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX * 2); 324 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX * 2); 325 }); 326 waitForActivityIdle(blurActivity); 327 328 assertOnScreenshot(screenshot -> { 329 assertNoBackgroundBlur(screenshot, windowFrame); 330 assertNoBlurBehind(screenshot, windowFrame); 331 }); 332 333 getInstrumentation().runOnMainSync(() -> { 334 blurActivity.setBackgroundColor(Color.TRANSPARENT); 335 }); 336 waitForActivityIdle(blurActivity); 337 verifyOnlyBackgroundImageVisible(); 338 339 setAndAssertForceBlurDisabled(false, blurActivity.mBlurEnabledListener); 340 waitForActivityIdle(blurActivity); 341 getInstrumentation().runOnMainSync(() -> { 342 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 343 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 344 }); 345 waitForActivityIdle(blurActivity); 346 347 assertOnScreenshot(screenshot -> { 348 assertBlurBehind(screenshot, windowFrame); 349 assertBackgroundBlurOverBlurBehind(screenshot, windowFrame); 350 }); 351 } 352 353 @Test 354 @ApiTest(apis = {"android.view.WindowManager.LayoutParams#setBlurBehindRadius", 355 "android.R.styleable#Window_windowBlurBehindEnabled", 356 "android.view.Window#setBackgroundBlurRadius"}) testBlurDestroyedAfterActivityFinished()357 public void testBlurDestroyedAfterActivityFinished() { 358 final BlurActivity blurActivity = startTestActivity(BlurActivity.class); 359 getInstrumentation().runOnMainSync(() -> { 360 blurActivity.setBlurBehindRadius(BLUR_BEHIND_PX); 361 blurActivity.setBackgroundBlurRadius(BACKGROUND_BLUR_PX); 362 }); 363 waitForActivityIdle(blurActivity); 364 365 final Rect windowFrame = getFloatingWindowFrame(blurActivity); 366 367 assertOnScreenshot(screenshot -> { 368 assertBlurBehind(screenshot, windowFrame); 369 assertBackgroundBlurOverBlurBehind(screenshot, windowFrame); 370 }); 371 372 blurActivity.finish(); 373 mWmState.waitAndAssertActivityRemoved(blurActivity.getComponentName()); 374 waitForActivityIdle(blurActivity); 375 376 verifyOnlyBackgroundImageVisible(); 377 } 378 379 380 @Test 381 @ApiTest(apis = {"android.view.WindowManager#addCrossWindowBlurEnabledListener", 382 "android.view.WindowManager#removeCrossWindowBlurEnabledListener"}) testBlurListener()383 public void testBlurListener() { 384 final BlurActivity activity = startTestActivity(BlurActivity.class); 385 Mockito.verify(activity.mBlurEnabledListener).accept(true); 386 387 setAndAssertForceBlurDisabled(true, activity.mBlurEnabledListener); 388 setAndAssertForceBlurDisabled(false, activity.mBlurEnabledListener); 389 390 activity.finishAndRemoveTask(); 391 mWmState.waitAndAssertActivityRemoved(activity.getComponentName()); 392 393 Mockito.clearInvocations(activity.mBlurEnabledListener); 394 setAndAssertForceBlurDisabled(true); 395 Mockito.verifyNoMoreInteractions(activity.mBlurEnabledListener); 396 } 397 398 public static class BackgroundActivity extends FocusableActivity { 399 private Insets mInsetsToBeIgnored = Insets.of(0, 0, 0, 0); 400 401 @Override onCreate(Bundle savedInstanceState)402 protected void onCreate(Bundle savedInstanceState) { 403 super.onCreate(savedInstanceState); 404 getSplashScreen().setOnExitAnimationListener(view -> view.remove()); 405 406 setContentView(R.layout.background_image); 407 WindowCompat.setDecorFitsSystemWindows(getWindow(), false); 408 409 View rootView = findViewById(android.R.id.content); 410 rootView.setOnApplyWindowInsetsListener((v, insets) -> { 411 Insets systemBarInsets = insets.getInsets(systemBars()); 412 413 int bottomLeft = getCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT); 414 int bottomRight = getCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT); 415 int topLeft = getCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT); 416 int topRight = getCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT); 417 418 // For each corner, inset into the apex at 45° so that the corners are excluded 419 // from the screenshot region while preserving some amount on circular screens. 420 final Insets roundedCornerInsets = Insets.of( 421 /* left= */ (int) (0.5 * Math.max(bottomLeft, topLeft)), 422 /* top= */ (int) (0.5 * Math.max(topLeft, topRight)), 423 /* right= */ (int) (0.5 * Math.max(topRight, bottomRight)), 424 /* bottom= */ (int) (0.5 * Math.max(bottomLeft, bottomRight)) 425 ); 426 427 mInsetsToBeIgnored = Insets.add(systemBarInsets, roundedCornerInsets); 428 429 // Make the insets to be ignored symmetrical horizontally so that the later 430 // computation can assume the widths of the blue area and the red area are the same. 431 mInsetsToBeIgnored = Insets.of( 432 Math.max(mInsetsToBeIgnored.left, mInsetsToBeIgnored.right), 433 mInsetsToBeIgnored.top, 434 Math.max(mInsetsToBeIgnored.left, mInsetsToBeIgnored.right), 435 mInsetsToBeIgnored.bottom); 436 return insets; 437 }); 438 } 439 getInsetsToBeIgnored()440 Insets getInsetsToBeIgnored() { 441 return mInsetsToBeIgnored; 442 } 443 getCornerRadius(WindowInsets insets, int position)444 private int getCornerRadius(WindowInsets insets, int position) { 445 final RoundedCorner corner = insets.getRoundedCorner(position); 446 447 // TODO (b/389045836): Make sure that Taskbar's "soft" rounded corners are reported 448 // in insets.getRoundedCorner so that this adjustment won't be necessary any more. 449 return Math.max((corner != null ? corner.getRadius() : 0), 450 getResources().getDimensionPixelSize(R.dimen.taskbar_corner_radius)); 451 } 452 } 453 454 public static class BlurActivity extends FocusableActivity { 455 public final Consumer<Boolean> mBlurEnabledListener = spy(new BlurListener()); 456 457 private int mBackgroundBlurRadius = 0; 458 private int mBlurBehindRadius = 0; 459 460 @Override onCreate(Bundle savedInstanceState)461 protected void onCreate(Bundle savedInstanceState) { 462 super.onCreate(savedInstanceState); 463 setContentView(R.layout.blur_activity); 464 getWindow().setDecorFitsSystemWindows(false); 465 getWindow().getAttributes().setFitInsetsSides(0); 466 } 467 468 @Override onAttachedToWindow()469 public void onAttachedToWindow() { 470 super.onAttachedToWindow(); 471 getWindowManager().addCrossWindowBlurEnabledListener(getMainExecutor(), 472 mBlurEnabledListener); 473 } 474 475 @Override onDetachedFromWindow()476 public void onDetachedFromWindow() { 477 super.onDetachedFromWindow(); 478 getWindowManager().removeCrossWindowBlurEnabledListener(mBlurEnabledListener); 479 } 480 setBackgroundBlurRadius(int backgroundBlurRadius)481 void setBackgroundBlurRadius(int backgroundBlurRadius) { 482 mBackgroundBlurRadius = backgroundBlurRadius; 483 getWindow().setBackgroundBlurRadius(mBackgroundBlurRadius); 484 setBackgroundColor( 485 mBackgroundBlurRadius > 0 && getWindowManager().isCrossWindowBlurEnabled() 486 ? Color.TRANSPARENT : NO_BLUR_BACKGROUND_COLOR); 487 } 488 setBlurBehindRadius(int blurBehindRadius)489 void setBlurBehindRadius(int blurBehindRadius) { 490 mBlurBehindRadius = blurBehindRadius; 491 getWindow().getAttributes().setBlurBehindRadius(mBlurBehindRadius); 492 getWindow().setAttributes(getWindow().getAttributes()); 493 getWindowManager().updateViewLayout(getWindow().getDecorView(), 494 getWindow().getAttributes()); 495 } 496 setBackgroundColor(int color)497 void setBackgroundColor(int color) { 498 getWindow().getDecorView().setBackgroundColor(color); 499 getWindowManager().updateViewLayout(getWindow().getDecorView(), 500 getWindow().getAttributes()); 501 } 502 503 public class BlurListener implements Consumer<Boolean> { 504 @Override accept(Boolean enabled)505 public void accept(Boolean enabled) { 506 setBackgroundBlurRadius(mBackgroundBlurRadius); 507 setBlurBehindRadius(mBlurBehindRadius); 508 } 509 } 510 } 511 512 /** 513 * This activity is used to test 2 things: 514 * 1. Blur behind does not work if WindowManager.LayoutParams.FLAG_BLUR_BEHIND is not set, 515 * respectively if windowBlurBehindEnabled is not set. 516 * 2. Background blur does not work for opaque activities (where windowIsTranslucent is false) 517 * 518 * In the style of this activity windowBlurBehindEnabled is false and windowIsTranslucent is 519 * false. As a result, we expect that neither blur behind, nor background blur is rendered, 520 * even though they are requested with setBlurBehindRadius and setBackgroundBlurRadius. 521 */ 522 public static class BadBlurActivity extends BlurActivity { 523 } 524 525 public static class BlurAttributesActivity extends FocusableActivity { 526 @Override onCreate(Bundle savedInstanceState)527 protected void onCreate(Bundle savedInstanceState) { 528 super.onCreate(savedInstanceState); 529 setContentView(R.layout.blur_activity); 530 getWindow().setDecorFitsSystemWindows(false); 531 getWindow().getAttributes().setFitInsetsSides(0); 532 } 533 } 534 startTestActivity(Class<T> activityClass)535 private <T extends FocusableActivity> T startTestActivity(Class<T> activityClass) { 536 T activity = startActivity(activityClass); 537 ComponentName activityName = activity.getComponentName(); 538 waitAndAssertResumedActivity(activityName, activityName + " must be resumed"); 539 waitForActivityIdle(activity); 540 return activity; 541 } 542 getFloatingWindowFrame(Activity activity)543 private Rect getFloatingWindowFrame(Activity activity) { 544 mWmState.computeState(activity.getComponentName()); 545 String windowName = getWindowName(activity.getComponentName()); 546 return new Rect(mWmState.getMatchingVisibleWindowState(windowName).get(0).getFrame()); 547 } 548 waitForActivityIdle(@ullable Activity activity)549 private void waitForActivityIdle(@Nullable Activity activity) { 550 // This helps with the test flakiness 551 getInstrumentation().runOnMainSync(() -> {}); 552 UiDevice.getInstance(getInstrumentation()).waitForIdle(); 553 getInstrumentation().getUiAutomation().syncInputTransactions(); 554 if (activity != null) { 555 mWmState.computeState(activity.getComponentName()); 556 } 557 } 558 setAndAssertForceBlurDisabled(boolean disable)559 private void setAndAssertForceBlurDisabled(boolean disable) { 560 setAndAssertForceBlurDisabled(disable, null); 561 } 562 setAndAssertForceBlurDisabled(boolean disable, Consumer<Boolean> blurEnabledListener)563 private void setAndAssertForceBlurDisabled(boolean disable, 564 Consumer<Boolean> blurEnabledListener) { 565 if (blurEnabledListener != null) { 566 Mockito.clearInvocations(blurEnabledListener); 567 } 568 Settings.Global.putInt(mContext.getContentResolver(), 569 Settings.Global.DISABLE_WINDOW_BLURS, disable ? 1 : 0); 570 if (blurEnabledListener != null) { 571 Mockito.verify(blurEnabledListener, timeout(BROADCAST_WAIT_TIMEOUT)) 572 .accept(!disable); 573 } 574 PollingCheck.waitFor(BROADCAST_WAIT_TIMEOUT, () -> { 575 return disable != mContext.getSystemService(WindowManager.class) 576 .isCrossWindowBlurEnabled(); 577 }); 578 assertTrue(!disable == mContext.getSystemService(WindowManager.class) 579 .isCrossWindowBlurEnabled()); 580 } 581 assertOnScreenshot(Consumer<Bitmap> assertion)582 private void assertOnScreenshot(Consumer<Bitmap> assertion) { 583 final Bitmap screenshot = takeScreenshot(); 584 try { 585 assertion.accept(screenshot); 586 } catch (AssertionError failedAssertion) { 587 mDumpOnFailure.dumpOnFailure("preAssertion", screenshot); 588 waitForActivityIdle(null); 589 mDumpOnFailure.dumpOnFailure("postAssertion", takeScreenshot()); 590 throw failedAssertion; 591 } 592 } 593 assertBlurBehind(Bitmap screenshot, Rect windowFrame)594 private void assertBlurBehind(Bitmap screenshot, Rect windowFrame) { 595 // From top of screenshot (accounting for extent on the edge) to the top of the centered 596 // window. 597 assertBlur(screenshot, BLUR_BEHIND_PX, 598 new Rect( 599 mPixelTestBounds.left + BLUR_BEHIND_PX, 600 mPixelTestBounds.top + BLUR_BEHIND_PX, 601 mPixelTestBounds.right - BLUR_BEHIND_PX, 602 windowFrame.top)); 603 // From bottom of the centered window to bottom of screenshot accounting for extent on the 604 // edge. 605 assertBlur(screenshot, BLUR_BEHIND_PX, 606 new Rect( 607 mPixelTestBounds.left + BLUR_BEHIND_PX, 608 windowFrame.bottom + 1, 609 mPixelTestBounds.right - BLUR_BEHIND_PX, 610 mPixelTestBounds.bottom - BLUR_BEHIND_PX)); 611 } 612 assertBackgroundBlur(Bitmap screenshot, Rect windowFrame)613 private void assertBackgroundBlur(Bitmap screenshot, Rect windowFrame) { 614 assertBlur(screenshot, BACKGROUND_BLUR_PX, windowFrame); 615 } 616 assertBackgroundBlurOverBlurBehind(Bitmap screenshot, Rect windowFrame)617 private void assertBackgroundBlurOverBlurBehind(Bitmap screenshot, Rect windowFrame) { 618 assertBlur(screenshot, (int) Math.hypot(BACKGROUND_BLUR_PX, BLUR_BEHIND_PX), windowFrame); 619 } 620 verifyOnlyBackgroundImageVisible()621 private void verifyOnlyBackgroundImageVisible() { 622 assertOnScreenshot(screenshot -> { 623 assertNoBlurBehind(screenshot, new Rect()); 624 }); 625 } 626 assertNoBlurBehind(Bitmap screenshot, Rect excludeFrame)627 private void assertNoBlurBehind(Bitmap screenshot, Rect excludeFrame) { 628 final int solidColorWidth = mBackgroundActivityBounds.width() / 2; 629 630 forEachPixelInRect(screenshot, mPixelTestBounds, (x, y, actual) -> { 631 if (!excludeFrame.contains(x, y)) { 632 if ((x - mBackgroundActivityBounds.left) < solidColorWidth) { 633 return assertPixel(x, y, Color.BLUE, actual); 634 } else if ((mBackgroundActivityBounds.right - x) <= solidColorWidth) { 635 return assertPixel(x, y, Color.RED, actual); 636 } 637 } 638 return false; 639 }); 640 } 641 assertNoBackgroundBlur(Bitmap screenshot, Rect windowFrame)642 private void assertNoBackgroundBlur(Bitmap screenshot, Rect windowFrame) { 643 forEachPixelInRect(screenshot, windowFrame, 644 (x, y, actual) -> assertPixel(x, y, NO_BLUR_BACKGROUND_COLOR, actual)); 645 } 646 assertBlur(Bitmap screenshot, int blurRadius, Rect blurRegion)647 private void assertBlur(Bitmap screenshot, int blurRadius, Rect blurRegion) { 648 final double midX = (blurRegion.left + blurRegion.right - 1) / 2.0; 649 650 // At 2 * radius there should be no visible blur effects. 651 final int unaffectedBluePixelX = (int) Math.floor(midX) - blurRadius * 2 - 1; 652 final int unaffectedRedPixelX = (int) Math.ceil(midX) + blurRadius * 2 + 1; 653 654 // Check a smaller part of the blurred area than strictly necessary, in order to accept 655 // various blur algorithm approximations used in RenderEngine 656 final int blurAreaStartX = 657 Math.max(blurRegion.left, (int) Math.floor(midX - blurRadius * 0.75f)); 658 final int blurAreaEndX = 659 Math.min(blurRegion.right - 1, (int) Math.ceil(midX + blurRadius * 0.75f)); 660 661 // Check only a limited number of samples per row, instead of every pixel, in order to 662 // tolerate approximations other than a full numerically-stable Gaussian kernel. 663 final int samples = 6; 664 665 for (int y = blurRegion.top; y < blurRegion.bottom; y++) { 666 Color previousColor = null; 667 int previousX = -1; 668 for (int i = 0; i <= samples; i++) { 669 final int x = blurAreaStartX + (i * (blurAreaEndX - blurAreaStartX)) / samples; 670 final Color currentColor = screenshot.getColor(x, y); 671 672 if (previousColor != null 673 && !(previousColor.blue() > currentColor.blue() 674 && previousColor.red() < currentColor.red())) { 675 fail(String.format( 676 "Expected a gradient between pixels (%d, %d) and (%d, %d): %s -> %s" 677 + " should have LESS blue and MORE red", 678 previousX, y, x, y, previousColor, currentColor)); 679 } 680 previousColor = currentColor; 681 previousX = x; 682 } 683 } 684 685 for (int y = blurRegion.top; y < blurRegion.bottom; y++) { 686 final int unaffectedBluePixel = screenshot.getPixel(unaffectedBluePixelX, y); 687 if (unaffectedBluePixel != Color.BLUE) { 688 ColorUtils.verifyColor( 689 "Expected BLUE for pixel (x, y) = (" + unaffectedBluePixelX + ", " + y + ")", 690 Color.BLUE, unaffectedBluePixel, 1); 691 } 692 final int unaffectedRedPixel = screenshot.getPixel(unaffectedRedPixelX, y); 693 if (unaffectedRedPixel != Color.RED) { 694 ColorUtils.verifyColor( 695 "Expected RED for pixel (x, y) = (" + unaffectedRedPixelX + ", " + y + ")", 696 Color.RED, unaffectedRedPixel, 1); 697 } 698 } 699 } 700 701 @FunctionalInterface 702 private interface PixelTester { 703 /** 704 * @return true if this pixel was checked, or false if it was ignored, for the purpose 705 * of making sure that a reasonable number of pixels were checked. 706 */ test(int x, int y, int color)707 boolean test(int x, int y, int color); 708 } 709 forEachPixelInRect(Bitmap screenshot, Rect bounds, PixelTester tester)710 private static void forEachPixelInRect(Bitmap screenshot, Rect bounds, PixelTester tester) { 711 @ColorInt int[] pixels = new int[bounds.height() * bounds.width()]; 712 screenshot.getPixels(pixels, 0, bounds.width(), 713 bounds.left, bounds.top, bounds.width(), bounds.height()); 714 715 // We should be making an assertion on a reasonable minimum number of pixels. Count how 716 // many pixels were actually checked so that we can fail if we didn't check enough. 717 int checkedPixels = 0; 718 719 int i = 0; 720 for (int y = bounds.top; y < bounds.bottom; y++) { 721 for (int x = bounds.left; x < bounds.right; x++, i++) { 722 if (tester.test(x, y, pixels[i])) { 723 checkedPixels++; 724 } 725 } 726 } 727 728 assertThat(checkedPixels).isGreaterThan(15); 729 } 730 731 /** 732 * Wrapper around verifyColor to speed up the test by avoiding constructing an error string 733 * unless it will be used. 734 */ assertPixel(int x, int y, @ColorInt int expected, @ColorInt int actual)735 private static boolean assertPixel(int x, int y, @ColorInt int expected, @ColorInt int actual) { 736 if (actual != expected) { 737 ColorUtils.verifyColor( 738 "failed for pixel (x, y) = (" + x + ", " + y + ")", expected, actual, 1); 739 } 740 return true; 741 } 742 } 743