• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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