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