• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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;
18 
19 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
20 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
21 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
22 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
23 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
24 import static android.server.wm.DisplayCutoutTests.TestActivity.EXTRA_CUTOUT_MODE;
25 import static android.server.wm.DisplayCutoutTests.TestActivity.EXTRA_ORIENTATION;
26 import static android.server.wm.DisplayCutoutTests.TestDef.Which.DISPATCHED;
27 import static android.server.wm.DisplayCutoutTests.TestDef.Which.ROOT;
28 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
29 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
30 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
31 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
32 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
33 
34 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
35 
36 import static org.hamcrest.Matchers.equalTo;
37 import static org.hamcrest.Matchers.everyItem;
38 import static org.hamcrest.Matchers.greaterThan;
39 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
40 import static org.hamcrest.Matchers.hasItem;
41 import static org.hamcrest.Matchers.hasSize;
42 import static org.hamcrest.Matchers.is;
43 import static org.hamcrest.Matchers.lessThanOrEqualTo;
44 import static org.hamcrest.Matchers.not;
45 import static org.hamcrest.Matchers.notNullValue;
46 import static org.hamcrest.Matchers.nullValue;
47 import static org.junit.Assert.assertEquals;
48 import static org.junit.Assert.assertTrue;
49 import static org.junit.Assume.assumeTrue;
50 
51 import android.app.Activity;
52 import android.content.Intent;
53 import android.content.pm.PackageManager;
54 import android.graphics.Insets;
55 import android.graphics.Point;
56 import android.graphics.Rect;
57 import android.os.Bundle;
58 import android.platform.test.annotations.Presubmit;
59 import android.view.DisplayCutout;
60 import android.view.View;
61 import android.view.ViewGroup;
62 import android.view.Window;
63 import android.view.WindowInsets;
64 import android.view.WindowInsets.Type;
65 
66 import androidx.test.rule.ActivityTestRule;
67 
68 import com.android.compatibility.common.util.PollingCheck;
69 
70 import org.hamcrest.CustomTypeSafeMatcher;
71 import org.hamcrest.FeatureMatcher;
72 import org.hamcrest.Matcher;
73 import org.junit.Assert;
74 import org.junit.Rule;
75 import org.junit.Test;
76 import org.junit.rules.ErrorCollector;
77 import org.junit.runner.RunWith;
78 import org.junit.runners.Parameterized;
79 import org.junit.runners.Parameterized.Parameter;
80 
81 import java.util.Arrays;
82 import java.util.List;
83 import java.util.function.Predicate;
84 import java.util.function.Supplier;
85 import java.util.stream.Collectors;
86 
87 /**
88  * Build/Install/Run:
89  *     atest CtsWindowManagerDeviceTestCases:DisplayCutoutTests
90  */
91 @Presubmit
92 @android.server.wm.annotation.Group3
93 @RunWith(Parameterized.class)
94 public class DisplayCutoutTests {
95     static final String LEFT = "left";
96     static final String TOP = "top";
97     static final String RIGHT = "right";
98     static final String BOTTOM = "bottom";
99 
100     @Parameterized.Parameters(name= "{1}({0})")
data()101     public static Object[][] data() {
102         return new Object[][]{
103                 {SCREEN_ORIENTATION_PORTRAIT, "SCREEN_ORIENTATION_PORTRAIT"},
104                 {SCREEN_ORIENTATION_LANDSCAPE, "SCREEN_ORIENTATION_LANDSCAPE"},
105                 {SCREEN_ORIENTATION_REVERSE_LANDSCAPE, "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"},
106                 {SCREEN_ORIENTATION_REVERSE_PORTRAIT, "SCREEN_ORIENTATION_REVERSE_PORTRAIT"},
107         };
108     }
109 
110     @Parameter(0)
111     public int orientation;
112 
113     @Parameter(1)
114     public String orientationName;
115 
116     @Rule
117     public final ErrorCollector mErrorCollector = new ErrorCollector();
118 
119     @Rule
120     public final ActivityTestRule<TestActivity> mDisplayCutoutActivity =
121             new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
122                     false /* launchActivity */);
123 
124     @Test
testConstructor()125     public void testConstructor() {
126         final Insets safeInsets = Insets.of(1, 2, 3, 4);
127         final Rect boundLeft = new Rect(5, 6, 7, 8);
128         final Rect boundTop = new Rect(9, 0, 10, 1);
129         final Rect boundRight = new Rect(2, 3, 4, 5);
130         final Rect boundBottom = new Rect(6, 7, 8, 9);
131 
132         final DisplayCutout displayCutout =
133                 new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom);
134 
135         assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft());
136         assertEquals(safeInsets.top, displayCutout.getSafeInsetTop());
137         assertEquals(safeInsets.right, displayCutout.getSafeInsetRight());
138         assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom());
139 
140         assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft()));
141         assertTrue(boundTop.equals(displayCutout.getBoundingRectTop()));
142         assertTrue(boundRight.equals(displayCutout.getBoundingRectRight()));
143         assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom()));
144 
145         assertEquals(Insets.NONE, displayCutout.getWaterfallInsets());
146     }
147 
148     @Test
testConstructor_waterfall()149     public void testConstructor_waterfall() {
150         final Insets safeInsets = Insets.of(1, 2, 3, 4);
151         final Rect boundLeft = new Rect(5, 6, 7, 8);
152         final Rect boundTop = new Rect(9, 0, 10, 1);
153         final Rect boundRight = new Rect(2, 3, 4, 5);
154         final Rect boundBottom = new Rect(6, 7, 8, 9);
155         final Insets waterfallInsets = Insets.of(4, 8, 12, 16);
156 
157         final DisplayCutout displayCutout =
158                 new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom,
159                         waterfallInsets);
160 
161         assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft());
162         assertEquals(safeInsets.top, displayCutout.getSafeInsetTop());
163         assertEquals(safeInsets.right, displayCutout.getSafeInsetRight());
164         assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom());
165 
166         assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft()));
167         assertTrue(boundTop.equals(displayCutout.getBoundingRectTop()));
168         assertTrue(boundRight.equals(displayCutout.getBoundingRectRight()));
169         assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom()));
170 
171         assertEquals(waterfallInsets, displayCutout.getWaterfallInsets());
172     }
173 
174     @Test
testDisplayCutout_default()175     public void testDisplayCutout_default() {
176         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,
177                 (activity, insets, displayCutout, which) -> {
178             if (displayCutout == null) {
179                 return;
180             }
181             if (which == ROOT) {
182                 assertThat("cutout must be contained within system bars in default mode",
183                         safeInsets(displayCutout), insetsLessThanOrEqualTo(stableInsets(insets)));
184             } else if (which == DISPATCHED) {
185                 assertThat("must not dipatch to hierarchy in default mode",
186                         displayCutout, nullValue());
187             }
188         });
189     }
190 
191     @Test
testDisplayCutout_shortEdges()192     public void testDisplayCutout_shortEdges() {
193         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, (a, insets, cutout, which) -> {
194             if (which == ROOT) {
195                 final Rect appBounds = getAppBounds(a);
196                 final Insets displaySafeInsets = Insets.of(safeInsets(a.getDisplay().getCutout()));
197                 final Insets expected;
198                 if (appBounds.height() > appBounds.width()) {
199                     // Portrait display
200                     expected = Insets.of(0, displaySafeInsets.top, 0, displaySafeInsets.bottom);
201                 } else if (appBounds.height() < appBounds.width()) {
202                     // Landscape display
203                     expected = Insets.of(displaySafeInsets.left, 0, displaySafeInsets.right, 0);
204                 } else {
205                     expected = Insets.NONE;
206                 }
207                 assertThat("cutout must provide the display's safe insets on short edges and zero"
208                                 + " on the long edges.",
209                         Insets.of(safeInsets(cutout)),
210                         equalTo(expected));
211             }
212         });
213     }
214 
215     @Test
testDisplayCutout_never()216     public void testDisplayCutout_never() {
217         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, (a, insets, displayCutout, which) -> {
218             assertThat("must not layout in cutout area in never mode", displayCutout, nullValue());
219         });
220     }
221 
222     @Test
testDisplayCutout_always()223     public void testDisplayCutout_always() {
224         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, (a, insets, displayCutout, which) -> {
225             if (which == ROOT) {
226                 assertThat("Display.getCutout() must equal view root cutout",
227                         a.getDisplay().getCutout(), equalTo(displayCutout));
228             }
229         });
230     }
231 
runTest(int cutoutMode, TestDef test)232     private void runTest(int cutoutMode, TestDef test) {
233         runTest(cutoutMode, test, orientation);
234     }
235 
runTest(int cutoutMode, TestDef test, int orientation)236     private void runTest(int cutoutMode, TestDef test, int orientation) {
237         assumeTrue("Skipping test: orientation not supported", supportsOrientation(orientation));
238         final TestActivity activity = launchAndWait(mDisplayCutoutActivity,
239                 cutoutMode, orientation);
240 
241         WindowInsets insets = getOnMainSync(activity::getRootInsets);
242         WindowInsets dispatchedInsets = getOnMainSync(activity::getDispatchedInsets);
243         Assert.assertThat("test setup failed, no insets at root", insets, notNullValue());
244         Assert.assertThat("test setup failed, no insets dispatched",
245                 dispatchedInsets, notNullValue());
246 
247         final DisplayCutout displayCutout = insets.getDisplayCutout();
248         final DisplayCutout dispatchedDisplayCutout = dispatchedInsets.getDisplayCutout();
249         if (displayCutout != null) {
250             commonAsserts(activity, displayCutout);
251             if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
252                 shortEdgeAsserts(activity, insets, displayCutout);
253             }
254             assertCutoutsAreConsistentWithInsets(activity, displayCutout);
255             assertSafeInsetsAreConsistentWithDisplayCutoutInsets(insets);
256         }
257         test.run(activity, insets, displayCutout, ROOT);
258 
259         if (dispatchedDisplayCutout != null) {
260             commonAsserts(activity, dispatchedDisplayCutout);
261             if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
262                 shortEdgeAsserts(activity, insets, displayCutout);
263             }
264             assertCutoutsAreConsistentWithInsets(activity, dispatchedDisplayCutout);
265             if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) {
266                 assertSafeInsetsAreConsistentWithDisplayCutoutInsets(dispatchedInsets);
267             }
268         }
269         test.run(activity, dispatchedInsets, dispatchedDisplayCutout, DISPATCHED);
270     }
271 
assertSafeInsetsAreConsistentWithDisplayCutoutInsets(WindowInsets insets)272     private void assertSafeInsetsAreConsistentWithDisplayCutoutInsets(WindowInsets insets) {
273         DisplayCutout cutout = insets.getDisplayCutout();
274         Insets safeInsets = Insets.of(safeInsets(cutout));
275         assertEquals("WindowInsets.getInsets(displayCutout()) must equal"
276                         + " DisplayCutout.getSafeInsets()",
277                 safeInsets, insets.getInsets(Type.displayCutout()));
278         assertEquals("WindowInsets.getInsetsIgnoringVisibility(displayCutout()) must equal"
279                         + " DisplayCutout.getSafeInsets()",
280                 safeInsets, insets.getInsetsIgnoringVisibility(Type.displayCutout()));
281     }
282 
commonAsserts(TestActivity activity, DisplayCutout cutout)283     private void commonAsserts(TestActivity activity, DisplayCutout cutout) {
284         assertSafeInsetsValid(cutout);
285         assertCutoutsAreWithinSafeInsets(activity, cutout);
286         assertBoundsAreNonEmpty(cutout);
287         assertAtMostOneCutoutPerEdge(activity, cutout);
288     }
289 
shortEdgeAsserts( TestActivity activity, WindowInsets insets, DisplayCutout cutout)290     private void shortEdgeAsserts(
291             TestActivity activity, WindowInsets insets, DisplayCutout cutout) {
292         assertOnlyShortEdgeHasInsets(activity, cutout);
293         assertOnlyShortEdgeHasBounds(activity, cutout);
294         assertThat("systemWindowInsets (also known as content insets) must be at least as "
295                         + "large as cutout safe insets",
296                 safeInsets(cutout), insetsLessThanOrEqualTo(systemWindowInsets(insets)));
297     }
298 
assertCutoutIsConsistentWithInset(String position, DisplayCutout cutout, int safeInsetSize, Rect appBound)299     private void assertCutoutIsConsistentWithInset(String position, DisplayCutout cutout,
300             int safeInsetSize, Rect appBound) {
301         if (safeInsetSize > 0) {
302             assertThat("cutout must have a bound on the " + position,
303                     hasBound(position, cutout, appBound), is(true));
304         } else {
305             assertThat("cutout  must have no bound on the " + position,
306                     hasBound(position, cutout, appBound), is(false));
307         }
308     }
309 
assertCutoutsAreConsistentWithInsets(TestActivity activity, DisplayCutout cutout)310     public void assertCutoutsAreConsistentWithInsets(TestActivity activity, DisplayCutout cutout) {
311         final Rect appBounds = getAppBounds(activity);
312         assertCutoutIsConsistentWithInset(TOP, cutout, cutout.getSafeInsetTop(), appBounds);
313         assertCutoutIsConsistentWithInset(BOTTOM, cutout, cutout.getSafeInsetBottom(), appBounds);
314         assertCutoutIsConsistentWithInset(LEFT, cutout, cutout.getSafeInsetLeft(), appBounds);
315         assertCutoutIsConsistentWithInset(RIGHT, cutout, cutout.getSafeInsetRight(), appBounds);
316     }
317 
assertSafeInsetsValid(DisplayCutout displayCutout)318     private void assertSafeInsetsValid(DisplayCutout displayCutout) {
319         //noinspection unchecked
320         assertThat("all safe insets must be non-negative", safeInsets(displayCutout),
321                 insetValues(everyItem((Matcher)greaterThanOrEqualTo(0))));
322         assertThat("at least one safe inset must be positive,"
323                         + " otherwise WindowInsets.getDisplayCutout()) must return null",
324                 safeInsets(displayCutout), insetValues(hasItem(greaterThan(0))));
325     }
326 
assertCutoutsAreWithinSafeInsets(TestActivity a, DisplayCutout cutout)327     private void assertCutoutsAreWithinSafeInsets(TestActivity a, DisplayCutout cutout) {
328         final Rect safeRect = getSafeRect(a, cutout);
329 
330         assertThat("safe insets must not cover the entire screen", safeRect.isEmpty(), is(false));
331         for (Rect boundingRect : cutout.getBoundingRects()) {
332             assertThat("boundingRects must not extend beyond safeInsets",
333                     boundingRect, not(intersectsWith(safeRect)));
334         }
335     }
336 
assertAtMostOneCutoutPerEdge(TestActivity a, DisplayCutout cutout)337     private void assertAtMostOneCutoutPerEdge(TestActivity a, DisplayCutout cutout) {
338         final Rect safeRect = getSafeRect(a, cutout);
339 
340         assertThat("must not have more than one left cutout",
341                 boundsWith(cutout, (r) -> r.right <= safeRect.left), hasSize(lessThanOrEqualTo(1)));
342         assertThat("must not have more than one top cutout",
343                 boundsWith(cutout, (r) -> r.bottom <= safeRect.top), hasSize(lessThanOrEqualTo(1)));
344         assertThat("must not have more than one right cutout",
345                 boundsWith(cutout, (r) -> r.left >= safeRect.right), hasSize(lessThanOrEqualTo(1)));
346         assertThat("must not have more than one bottom cutout",
347                 boundsWith(cutout, (r) -> r.top >= safeRect.bottom), hasSize(lessThanOrEqualTo(1)));
348     }
349 
assertBoundsAreNonEmpty(DisplayCutout cutout)350     private void assertBoundsAreNonEmpty(DisplayCutout cutout) {
351         for (Rect boundingRect : cutout.getBoundingRects()) {
352             assertThat("rect in boundingRects must not be empty",
353                     boundingRect.isEmpty(), is(false));
354         }
355     }
356 
assertOnlyShortEdgeHasInsets(TestActivity activity, DisplayCutout displayCutout)357     private void assertOnlyShortEdgeHasInsets(TestActivity activity,
358             DisplayCutout displayCutout) {
359         final Rect appBounds = getAppBounds(activity);
360         if (appBounds.height() > appBounds.width()) {
361             // Portrait display
362             assertThat("left edge has a cutout despite being long edge",
363                     displayCutout.getSafeInsetLeft(), is(0));
364             assertThat("right edge has a cutout despite being long edge",
365                     displayCutout.getSafeInsetRight(), is(0));
366         }
367         if (appBounds.height() < appBounds.width()) {
368             // Landscape display
369             assertThat("top edge has a cutout despite being long edge",
370                     displayCutout.getSafeInsetTop(), is(0));
371             assertThat("bottom edge has a cutout despite being long edge",
372                     displayCutout.getSafeInsetBottom(), is(0));
373         }
374     }
375 
assertOnlyShortEdgeHasBounds(TestActivity activity, DisplayCutout cutout)376     private void assertOnlyShortEdgeHasBounds(TestActivity activity, DisplayCutout cutout) {
377         final Rect appBounds = getAppBounds(activity);
378         if (appBounds.height() > appBounds.width()) {
379             // Portrait display
380             assertThat("left edge has a cutout despite being long edge",
381                     hasBound(LEFT, cutout, appBounds), is(false));
382 
383             assertThat("right edge has a cutout despite being long edge",
384                     hasBound(RIGHT, cutout, appBounds), is(false));
385         }
386         if (appBounds.height() < appBounds.width()) {
387             // Landscape display
388             assertThat("top edge has a cutout despite being long edge",
389                     hasBound(TOP, cutout, appBounds), is(false));
390 
391             assertThat("bottom edge has a cutout despite being long edge",
392                     hasBound(BOTTOM, cutout, appBounds), is(false));
393         }
394     }
395 
hasBound(String position, DisplayCutout cutout, Rect appBound)396     private boolean hasBound(String position, DisplayCutout cutout, Rect appBound) {
397         final Rect cutoutRect;
398         final int waterfallSize;
399         if (LEFT.equals(position)) {
400             cutoutRect = cutout.getBoundingRectLeft();
401             waterfallSize = cutout.getWaterfallInsets().left;
402         } else if (TOP.equals(position)) {
403             cutoutRect = cutout.getBoundingRectTop();
404             waterfallSize = cutout.getWaterfallInsets().top;
405         } else if (RIGHT.equals(position)) {
406             cutoutRect = cutout.getBoundingRectRight();
407             waterfallSize = cutout.getWaterfallInsets().right;
408         } else {
409             cutoutRect = cutout.getBoundingRectBottom();
410             waterfallSize = cutout.getWaterfallInsets().bottom;
411         }
412         return Rect.intersects(cutoutRect, appBound) || waterfallSize > 0;
413     }
414 
boundsWith(DisplayCutout cutout, Predicate<Rect> predicate)415     private List<Rect> boundsWith(DisplayCutout cutout, Predicate<Rect> predicate) {
416         return cutout.getBoundingRects().stream().filter(predicate).collect(Collectors.toList());
417     }
418 
safeInsets(DisplayCutout displayCutout)419     private static Rect safeInsets(DisplayCutout displayCutout) {
420         if (displayCutout == null) {
421             return null;
422         }
423         return new Rect(displayCutout.getSafeInsetLeft(), displayCutout.getSafeInsetTop(),
424                 displayCutout.getSafeInsetRight(), displayCutout.getSafeInsetBottom());
425     }
426 
systemWindowInsets(WindowInsets insets)427     private static Rect systemWindowInsets(WindowInsets insets) {
428         return new Rect(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
429                 insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
430     }
431 
stableInsets(WindowInsets insets)432     private static Rect stableInsets(WindowInsets insets) {
433         return new Rect(insets.getStableInsetLeft(), insets.getStableInsetTop(),
434                 insets.getStableInsetRight(), insets.getStableInsetBottom());
435     }
436 
getSafeRect(TestActivity a, DisplayCutout cutout)437     private Rect getSafeRect(TestActivity a, DisplayCutout cutout) {
438         final Rect safeRect = safeInsets(cutout);
439         safeRect.bottom = getOnMainSync(() -> a.getDecorView().getHeight()) - safeRect.bottom;
440         safeRect.right = getOnMainSync(() -> a.getDecorView().getWidth()) - safeRect.right;
441         return safeRect;
442     }
443 
getAppBounds(TestActivity a)444     private Rect getAppBounds(TestActivity a) {
445         final Rect appBounds = new Rect();
446         runOnMainSync(() -> {
447             appBounds.right = a.getDecorView().getWidth();
448             appBounds.bottom = a.getDecorView().getHeight();
449         });
450         return appBounds;
451     }
452 
insetsLessThanOrEqualTo(Rect max)453     private static Matcher<Rect> insetsLessThanOrEqualTo(Rect max) {
454         return new CustomTypeSafeMatcher<Rect>("must be smaller on each side than " + max) {
455             @Override
456             protected boolean matchesSafely(Rect actual) {
457                 return actual.left <= max.left && actual.top <= max.top
458                         && actual.right <= max.right && actual.bottom <= max.bottom;
459             }
460         };
461     }
462 
463     private static Matcher<Rect> intersectsWith(Rect safeRect) {
464         return new CustomTypeSafeMatcher<Rect>("intersects with " + safeRect) {
465             @Override
466             protected boolean matchesSafely(Rect item) {
467                 return Rect.intersects(safeRect, item);
468             }
469         };
470     }
471 
472     private static Matcher<Rect> insetValues(Matcher<Iterable<? super Integer>> valuesMatcher) {
473         return new FeatureMatcher<Rect, Iterable<Integer>>(valuesMatcher, "inset values",
474                 "inset values") {
475             @Override
476             protected Iterable<Integer> featureValueOf(Rect actual) {
477                 return Arrays.asList(actual.left, actual.top, actual.right, actual.bottom);
478             }
479         };
480     }
481 
482     private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
483         mErrorCollector.checkThat(reason, actual, matcher);
484     }
485 
486     private <R> R getOnMainSync(Supplier<R> f) {
487         final Object[] result = new Object[1];
488         runOnMainSync(() -> result[0] = f.get());
489         //noinspection unchecked
490         return (R) result[0];
491     }
492 
493     private void runOnMainSync(Runnable runnable) {
494         getInstrumentation().runOnMainSync(runnable);
495     }
496 
497     private <T extends TestActivity> T launchAndWait(ActivityTestRule<T> rule, int cutoutMode,
498             int orientation) {
499         final T activity = rule.launchActivity(
500                 new Intent().putExtra(EXTRA_CUTOUT_MODE, cutoutMode)
501                         .putExtra(EXTRA_ORIENTATION, orientation));
502         PollingCheck.waitFor(activity::hasWindowFocus);
503         PollingCheck.waitFor(() -> {
504             final Rect appBounds = getAppBounds(activity);
505             final Point displaySize = new Point();
506             activity.getDisplay().getRealSize(displaySize);
507             // During app launch into a different rotation, we have temporarily have the display
508             // in a different rotation than the app itself. Wait for this to settle.
509             return (appBounds.width() > appBounds.height()) == (displaySize.x > displaySize.y);
510         });
511         return activity;
512     }
513 
514     private boolean supportsOrientation(int orientation) {
515         String systemFeature = "";
516         switch(orientation) {
517             case SCREEN_ORIENTATION_PORTRAIT:
518             case SCREEN_ORIENTATION_REVERSE_PORTRAIT:
519                 systemFeature = PackageManager.FEATURE_SCREEN_PORTRAIT;
520                 break;
521             case SCREEN_ORIENTATION_LANDSCAPE:
522             case SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
523                 systemFeature = PackageManager.FEATURE_SCREEN_LANDSCAPE;
524                 break;
525             default:
526                 throw new UnsupportedOperationException("Orientation not supported");
527         }
528 
529         return getInstrumentation().getTargetContext().getPackageManager()
530                 .hasSystemFeature(systemFeature);
531     }
532 
533     public static class TestActivity extends Activity {
534 
535         static final String EXTRA_CUTOUT_MODE = "extra.cutout_mode";
536         static final String EXTRA_ORIENTATION = "extra.orientation";
537         private WindowInsets mDispatchedInsets;
538 
539         @Override
540         protected void onCreate(Bundle savedInstanceState) {
541             super.onCreate(savedInstanceState);
542             getWindow().requestFeature(Window.FEATURE_NO_TITLE);
543             if (getIntent() != null) {
544                 getWindow().getAttributes().layoutInDisplayCutoutMode = getIntent().getIntExtra(
545                         EXTRA_CUTOUT_MODE, LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT);
546                 setRequestedOrientation(getIntent().getIntExtra(
547                         EXTRA_ORIENTATION, SCREEN_ORIENTATION_UNSPECIFIED));
548             }
549             View view = new View(this);
550             view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
551             view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets);
552             setContentView(view);
553         }
554 
555         @Override
556         public void onWindowFocusChanged(boolean hasFocus) {
557             if (hasFocus) {
558                 getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
559                         | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
560             }
561         }
562 
563         View getDecorView() {
564             return getWindow().getDecorView();
565         }
566 
567         WindowInsets getRootInsets() {
568             return getWindow().getDecorView().getRootWindowInsets();
569         }
570 
571         WindowInsets getDispatchedInsets() {
572             return mDispatchedInsets;
573         }
574     }
575 
576     interface TestDef {
577         void run(TestActivity a, WindowInsets insets, DisplayCutout cutout, Which whichInsets);
578 
579         enum Which {
580             DISPATCHED, ROOT
581         }
582     }
583 }
584