• 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.util.DisplayMetrics.DENSITY_DEFAULT;
29 import static android.view.Display.DEFAULT_DISPLAY;
30 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
31 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
32 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
33 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
34 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
35 
36 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
37 
38 import static org.hamcrest.Matchers.equalTo;
39 import static org.hamcrest.Matchers.everyItem;
40 import static org.hamcrest.Matchers.greaterThan;
41 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
42 import static org.hamcrest.Matchers.hasItem;
43 import static org.hamcrest.Matchers.hasSize;
44 import static org.hamcrest.Matchers.is;
45 import static org.hamcrest.Matchers.lessThanOrEqualTo;
46 import static org.hamcrest.Matchers.not;
47 import static org.hamcrest.Matchers.notNullValue;
48 import static org.hamcrest.Matchers.nullValue;
49 import static org.junit.Assert.assertEquals;
50 import static org.junit.Assert.assertFalse;
51 import static org.junit.Assert.assertTrue;
52 import static org.junit.Assume.assumeTrue;
53 
54 import android.app.Activity;
55 import android.content.Context;
56 import android.content.Intent;
57 import android.content.pm.PackageManager;
58 import android.content.res.Configuration;
59 import android.graphics.Insets;
60 import android.graphics.Path;
61 import android.graphics.Rect;
62 import android.os.Bundle;
63 import android.platform.test.annotations.Presubmit;
64 import android.util.Size;
65 import android.view.DisplayCutout;
66 import android.view.View;
67 import android.view.ViewGroup;
68 import android.view.Window;
69 import android.view.WindowInsets;
70 import android.view.WindowInsets.Type;
71 
72 import androidx.test.rule.ActivityTestRule;
73 
74 import com.android.compatibility.common.util.CddTest;
75 import com.android.compatibility.common.util.WindowUtil;
76 
77 import org.hamcrest.CustomTypeSafeMatcher;
78 import org.hamcrest.FeatureMatcher;
79 import org.hamcrest.Matcher;
80 import org.junit.AfterClass;
81 import org.junit.Assert;
82 import org.junit.Before;
83 import org.junit.BeforeClass;
84 import org.junit.ClassRule;
85 import org.junit.Rule;
86 import org.junit.Test;
87 import org.junit.rules.ErrorCollector;
88 import org.junit.runner.RunWith;
89 import org.junit.runners.Parameterized;
90 import org.junit.runners.Parameterized.Parameter;
91 
92 import java.util.Arrays;
93 import java.util.List;
94 import java.util.function.Predicate;
95 import java.util.function.Supplier;
96 import java.util.stream.Collectors;
97 
98 /**
99  * Build/Install/Run:
100  *     atest CtsWindowManagerDeviceTestCases:DisplayCutoutTests
101  */
102 @Presubmit
103 @android.server.wm.annotation.Group3
104 @RunWith(Parameterized.class)
105 public class DisplayCutoutTests {
106     static final String LEFT = "left";
107     static final String TOP = "top";
108     static final String RIGHT = "right";
109     static final String BOTTOM = "bottom";
110 
111     /**
112      * @see LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
113      * @see LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
114      */
115     private static final int MAXIMUM_SIZE_FOR_NO_LETTERBOX_IF_DEFAULT_OR_SHORT_EDGE_DP = 16;
116 
117     @Parameterized.Parameters(name= "{1}({0})")
data()118     public static Object[][] data() {
119         return new Object[][]{
120                 {SCREEN_ORIENTATION_PORTRAIT, "SCREEN_ORIENTATION_PORTRAIT"},
121                 {SCREEN_ORIENTATION_LANDSCAPE, "SCREEN_ORIENTATION_LANDSCAPE"},
122                 {SCREEN_ORIENTATION_REVERSE_LANDSCAPE, "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"},
123                 {SCREEN_ORIENTATION_REVERSE_PORTRAIT, "SCREEN_ORIENTATION_REVERSE_PORTRAIT"},
124         };
125     }
126 
127     @Parameter(0)
128     public int orientation;
129 
130     @Parameter(1)
131     public String orientationName;
132 
133     @ClassRule
134     public static ActivityManagerTestBase.DisableImmersiveModeConfirmationRule
135             sDisableImmersiveModeConfirmationRule =
136             new ActivityManagerTestBase.DisableImmersiveModeConfirmationRule();
137 
138     @ClassRule
139     public static SetRequestedOrientationRule sSetRequestedOrientationRule =
140             new SetRequestedOrientationRule();
141 
142     @Rule
143     public final ErrorCollector mErrorCollector = new ErrorCollector();
144 
145     @Rule
146     public final ActivityTestRule<TestActivity> mDisplayCutoutActivity =
147             new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
148                     false /* launchActivity */);
149 
150     // OEMs can have an option not to letterbox, if the cutout overlaps at most
151     // 16 dp with app windows/contents for the apps using DEFAULT and SHORT_EDGES.
152     private int mMaximumSizeForNoLetterbox;
153 
154     private static DisplayMetricsSession sDisplayMetricsSession;
155 
156     @BeforeClass
setUpClass()157     public static void setUpClass() {
158         if (!ActivityManagerTestBase.isCloseToSquareDisplay(getInstrumentation().getContext())) {
159             return;
160         }
161         // If the display size is close to square, the activity bounds may be shrunk to match its
162         // requested orientation (see ActivityRecord#orientationRespectedWithInsets). Then its
163         // insets may not contain the cutout path, so resize the display to avoid the case.
164         sDisplayMetricsSession = new DisplayMetricsSession(DEFAULT_DISPLAY);
165         final Size displaySize = sDisplayMetricsSession.getDisplayMetrics().getSize();
166         final int orientation = displaySize.getHeight() <= displaySize.getWidth()
167                 ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
168         sDisplayMetricsSession.changeAspectRatio(1.77 /* 16:9 */, orientation);
169         getInstrumentation().getUiAutomation().syncInputTransactions();
170     }
171 
172     @AfterClass
tearDownClass()173     public static void tearDownClass() {
174         if (sDisplayMetricsSession != null) {
175             sDisplayMetricsSession.close();
176             sDisplayMetricsSession = null;
177         }
178     }
179 
180     @Before
setUp()181     public void setUp() throws Exception {
182         final Context context = getInstrumentation().getContext();
183         mMaximumSizeForNoLetterbox =
184                 (int) ((context.getResources().getConfiguration().densityDpi
185                         / (float) DENSITY_DEFAULT)
186                         * MAXIMUM_SIZE_FOR_NO_LETTERBOX_IF_DEFAULT_OR_SHORT_EDGE_DP);
187     }
188 
189     @Test
testConstructor()190     public void testConstructor() {
191         final Insets safeInsets = Insets.of(1, 2, 3, 4);
192         final Rect boundLeft = new Rect(5, 6, 7, 8);
193         final Rect boundTop = new Rect(9, 0, 10, 1);
194         final Rect boundRight = new Rect(2, 3, 4, 5);
195         final Rect boundBottom = new Rect(6, 7, 8, 9);
196 
197         final DisplayCutout displayCutout =
198                 new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom);
199 
200         assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft());
201         assertEquals(safeInsets.top, displayCutout.getSafeInsetTop());
202         assertEquals(safeInsets.right, displayCutout.getSafeInsetRight());
203         assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom());
204 
205         assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft()));
206         assertTrue(boundTop.equals(displayCutout.getBoundingRectTop()));
207         assertTrue(boundRight.equals(displayCutout.getBoundingRectRight()));
208         assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom()));
209 
210         assertEquals(Insets.NONE, displayCutout.getWaterfallInsets());
211     }
212 
213     @Test
testConstructor_waterfall()214     public void testConstructor_waterfall() {
215         final Insets safeInsets = Insets.of(1, 2, 3, 4);
216         final Rect boundLeft = new Rect(5, 6, 7, 8);
217         final Rect boundTop = new Rect(9, 0, 10, 1);
218         final Rect boundRight = new Rect(2, 3, 4, 5);
219         final Rect boundBottom = new Rect(6, 7, 8, 9);
220         final Insets waterfallInsets = Insets.of(4, 8, 12, 16);
221 
222         final DisplayCutout displayCutout =
223                 new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom,
224                         waterfallInsets);
225 
226         assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft());
227         assertEquals(safeInsets.top, displayCutout.getSafeInsetTop());
228         assertEquals(safeInsets.right, displayCutout.getSafeInsetRight());
229         assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom());
230 
231         assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft()));
232         assertTrue(boundTop.equals(displayCutout.getBoundingRectTop()));
233         assertTrue(boundRight.equals(displayCutout.getBoundingRectRight()));
234         assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom()));
235 
236         assertEquals(waterfallInsets, displayCutout.getWaterfallInsets());
237     }
238 
239     @Test
testBuilder()240     public void testBuilder() {
241         final Insets safeInsets = Insets.of(1, 2, 1, 0);
242         final Insets waterfallInsets = Insets.of(1, 0, 1, 0);
243         final Rect boundingLeft = new Rect(5, 6, 7, 8);
244         final Rect boundingRectTop = new Rect(9, 0, 10, 1);
245         final Rect boundingRight = new Rect(2, 3, 4, 5);
246         final Rect boundingBottom = new Rect(6, 7, 8, 9);
247         final Path cutoutPath = new Path();
248 
249         final DisplayCutout displayCutout = new DisplayCutout.Builder()
250                 .setSafeInsets(safeInsets)
251                 .setWaterfallInsets(waterfallInsets)
252                 .setBoundingRectLeft(boundingLeft)
253                 .setBoundingRectTop(boundingRectTop)
254                 .setBoundingRectRight(boundingRight)
255                 .setBoundingRectBottom(boundingBottom)
256                 .setCutoutPath(cutoutPath)
257                 .build();
258 
259         assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft());
260         assertEquals(safeInsets.top, displayCutout.getSafeInsetTop());
261         assertEquals(safeInsets.right, displayCutout.getSafeInsetRight());
262         assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom());
263         assertEquals(waterfallInsets, displayCutout.getWaterfallInsets());
264         assertEquals(boundingLeft, displayCutout.getBoundingRectLeft());
265         assertEquals(boundingRectTop, displayCutout.getBoundingRectTop());
266         assertEquals(boundingRight, displayCutout.getBoundingRectRight());
267         assertEquals(boundingBottom, displayCutout.getBoundingRectBottom());
268         assertEquals(cutoutPath, displayCutout.getCutoutPath());
269     }
270 
271     @Test
272     @CddTest(requirements = {"3.8.15/C-1-2,C-1-3,C-1-4"})
testDisplayCutout_default()273     public void testDisplayCutout_default() {
274         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,
275                 (activity, insets, displayCutout, which) -> {
276             if (displayCutout == null) {
277                 return;
278             }
279             if (which == ROOT) {
280                 assertThat("cutout must be contained within system bars in default mode",
281                         safeInsets(displayCutout, true /* canIgnoreSmallCutout */),
282                         insetsLessThanOrEqualTo(stableInsets(insets)));
283             } else if (which == DISPATCHED) {
284                 assertThat("must not dispatch to hierarchy in default mode",
285                         displayCutout, nullValue());
286             }
287         });
288     }
289 
290     @Test
291     @CddTest(requirements = {"3.8.15/C-1-2,C-1-3,C-1-4"})
testDisplayCutout_shortEdges()292     public void testDisplayCutout_shortEdges() {
293         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, (a, insets, cutout, which) -> {
294             if (which == ROOT) {
295                 final Rect appBounds = getAppBounds(a);
296                 final Insets displaySafeInsets = Insets.of(
297                         safeInsets(a.getDisplay().getCutout(), true /* canIgnoreSmallCutout */));
298                 final Insets expected;
299                 if (appBounds.height() > appBounds.width()) {
300                     // Portrait display
301                     expected = Insets.of(0, displaySafeInsets.top, 0, displaySafeInsets.bottom);
302                 } else if (appBounds.height() < appBounds.width()) {
303                     // Landscape display
304                     expected = Insets.of(displaySafeInsets.left, 0, displaySafeInsets.right, 0);
305                 } else {
306                     expected = Insets.NONE;
307                 }
308                 assertThat("cutout must provide the display's safe insets on short edges and zero"
309                                 + " on the long edges.",
310                         Insets.of(safeInsets(cutout, true /* canIgnoreSmallCutout */)),
311                         equalTo(expected));
312             }
313         });
314     }
315 
316     @Test
317     @CddTest(requirements = {"3.8.15/C-1-2,C-1-3,C-1-4"})
testDisplayCutout_never()318     public void testDisplayCutout_never() {
319         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, (a, insets, displayCutout, which) -> {
320             assertThat("must not layout in cutout area in never mode", displayCutout, nullValue());
321         });
322     }
323 
324     @Test
325     @CddTest(requirements = {"3.8.15/C-1-2,C-1-3,C-1-4"})
testDisplayCutout_always()326     public void testDisplayCutout_always() {
327         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, (a, insets, displayCutout, which) -> {
328             if (which == ROOT) {
329                 assertThat("Display.getCutout() must equal view root cutout",
330                         a.getDisplay().getCutout(), equalTo(displayCutout));
331             }
332         });
333     }
334 
335     @Test
testDisplayCutout_CutoutPaths()336     public void testDisplayCutout_CutoutPaths() {
337         runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, (a, insets, displayCutout, which) -> {
338             if (displayCutout == null) {
339                 return;
340             }
341             final Path cutoutPath = displayCutout.getCutoutPath();
342             assertCutoutPath(LEFT, displayCutout.getBoundingRectLeft(), cutoutPath);
343             assertCutoutPath(TOP, displayCutout.getBoundingRectTop(), cutoutPath);
344             assertCutoutPath(RIGHT, displayCutout.getBoundingRectRight(), cutoutPath);
345             assertCutoutPath(BOTTOM, displayCutout.getBoundingRectBottom(), cutoutPath);
346         });
347     }
348 
assertCutoutPath(String position, Rect cutoutRect, Path cutoutPath)349     private void assertCutoutPath(String position, Rect cutoutRect, Path cutoutPath) {
350         if (cutoutRect.isEmpty()) {
351             return;
352         }
353         final Path intersected = new Path();
354         intersected.addRect(cutoutRect.left, cutoutRect.top, cutoutRect.right, cutoutRect.bottom,
355                 Path.Direction.CCW);
356         intersected.op(cutoutPath, Path.Op.INTERSECT);
357         assertFalse("Must have cutout path on " + position, intersected.isEmpty());
358     }
359 
runTest(int cutoutMode, TestDef test)360     private void runTest(int cutoutMode, TestDef test) {
361         runTest(cutoutMode, test, orientation);
362     }
363 
runTest(int cutoutMode, TestDef test, int orientation)364     private void runTest(int cutoutMode, TestDef test, int orientation) {
365         assumeTrue("Skipping test: orientation not supported", supportsOrientation(orientation));
366         final TestActivity activity = launchAndWait(mDisplayCutoutActivity,
367                 cutoutMode, orientation);
368 
369         WindowInsets insets = getOnMainSync(activity::getRootInsets);
370         WindowInsets dispatchedInsets = getOnMainSync(activity::getDispatchedInsets);
371         Assert.assertThat("test setup failed, no insets at root", insets, notNullValue());
372         Assert.assertThat("test setup failed, no insets dispatched",
373                 dispatchedInsets, notNullValue());
374 
375         final DisplayCutout displayCutout = insets.getDisplayCutout();
376         final DisplayCutout dispatchedDisplayCutout = dispatchedInsets.getDisplayCutout();
377         if (displayCutout != null) {
378             commonAsserts(activity, displayCutout);
379             if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
380                 shortEdgeAsserts(activity, insets, displayCutout,
381                         canLayoutInDisplayCutoutWithoutLetterbox(cutoutMode));
382             }
383             assertCutoutsAreConsistentWithInsets(activity, displayCutout);
384             assertSafeInsetsAreConsistentWithDisplayCutoutInsets(insets);
385         }
386         test.run(activity, insets, displayCutout, ROOT);
387 
388         if (dispatchedDisplayCutout != null) {
389             commonAsserts(activity, dispatchedDisplayCutout);
390             if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
391                 shortEdgeAsserts(activity, insets, dispatchedDisplayCutout,
392                         canLayoutInDisplayCutoutWithoutLetterbox(cutoutMode));
393             }
394             assertCutoutsAreConsistentWithInsets(activity, dispatchedDisplayCutout);
395             if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) {
396                 assertSafeInsetsAreConsistentWithDisplayCutoutInsets(dispatchedInsets);
397             }
398         }
399         test.run(activity, dispatchedInsets, dispatchedDisplayCutout, DISPATCHED);
400     }
401 
assertSafeInsetsAreConsistentWithDisplayCutoutInsets(WindowInsets insets)402     private void assertSafeInsetsAreConsistentWithDisplayCutoutInsets(WindowInsets insets) {
403         DisplayCutout cutout = insets.getDisplayCutout();
404         Insets safeInsets = Insets.of(safeInsets(cutout));
405         assertEquals("WindowInsets.getInsets(displayCutout()) must equal"
406                         + " DisplayCutout.getSafeInsets()",
407                 safeInsets, insets.getInsets(Type.displayCutout()));
408         assertEquals("WindowInsets.getInsetsIgnoringVisibility(displayCutout()) must equal"
409                         + " DisplayCutout.getSafeInsets()",
410                 safeInsets, insets.getInsetsIgnoringVisibility(Type.displayCutout()));
411     }
412 
commonAsserts(TestActivity activity, DisplayCutout cutout)413     private void commonAsserts(TestActivity activity, DisplayCutout cutout) {
414         assertSafeInsetsValid(cutout);
415         assertCutoutsAreWithinSafeInsets(activity, cutout);
416         assertBoundsAreNonEmpty(cutout);
417         assertAtMostOneCutoutPerEdge(activity, cutout);
418     }
419 
shortEdgeAsserts( TestActivity activity, WindowInsets insets, DisplayCutout cutout, boolean canIgnoreSmallCutout)420     private void shortEdgeAsserts(
421             TestActivity activity, WindowInsets insets, DisplayCutout cutout,
422             boolean canIgnoreSmallCutout) {
423         final Rect safeInsets = safeInsets(cutout, canIgnoreSmallCutout);
424         assertOnlyShortEdgeHasInsets(activity, safeInsets);
425         assertOnlyShortEdgeHasBounds(activity, cutout, canIgnoreSmallCutout);
426         assertThat("systemWindowInsets (also known as content insets) must be at least as "
427                         + "large as cutout safe insets",
428                 safeInsets, insetsLessThanOrEqualTo(systemWindowInsets(insets)));
429     }
430 
assertCutoutIsConsistentWithInset(String position, DisplayCutout cutout, int safeInsetSize, Rect appBound)431     private void assertCutoutIsConsistentWithInset(String position, DisplayCutout cutout,
432             int safeInsetSize, Rect appBound) {
433         if (safeInsetSize > 0) {
434             assertThat("cutout must have a bound on the " + position,
435                     hasBound(position, cutout, appBound), is(true));
436         } else {
437             assertThat("cutout  must have no bound on the " + position,
438                     hasBound(position, cutout, appBound), is(false));
439         }
440     }
441 
assertCutoutsAreConsistentWithInsets(TestActivity activity, DisplayCutout cutout)442     public void assertCutoutsAreConsistentWithInsets(TestActivity activity, DisplayCutout cutout) {
443         final Rect appBounds = getAppBounds(activity);
444         assertCutoutIsConsistentWithInset(TOP, cutout, cutout.getSafeInsetTop(), appBounds);
445         assertCutoutIsConsistentWithInset(BOTTOM, cutout, cutout.getSafeInsetBottom(), appBounds);
446         assertCutoutIsConsistentWithInset(LEFT, cutout, cutout.getSafeInsetLeft(), appBounds);
447         assertCutoutIsConsistentWithInset(RIGHT, cutout, cutout.getSafeInsetRight(), appBounds);
448     }
449 
assertSafeInsetsValid(DisplayCutout displayCutout)450     private void assertSafeInsetsValid(DisplayCutout displayCutout) {
451         //noinspection unchecked
452         assertThat("all safe insets must be non-negative", safeInsets(displayCutout),
453                 insetValues(everyItem((Matcher)greaterThanOrEqualTo(0))));
454         assertThat("at least one safe inset must be positive,"
455                         + " otherwise WindowInsets.getDisplayCutout()) must return null",
456                 safeInsets(displayCutout), insetValues(hasItem(greaterThan(0))));
457     }
458 
assertCutoutsAreWithinSafeInsets(TestActivity a, DisplayCutout cutout)459     private void assertCutoutsAreWithinSafeInsets(TestActivity a, DisplayCutout cutout) {
460         final Rect safeRect = getSafeRect(a, cutout);
461 
462         assertThat("safe insets must not cover the entire screen", safeRect.isEmpty(), is(false));
463         for (Rect boundingRect : cutout.getBoundingRects()) {
464             assertThat("boundingRects must not extend beyond safeInsets",
465                     boundingRect, not(intersectsWith(safeRect)));
466         }
467     }
468 
assertAtMostOneCutoutPerEdge(TestActivity a, DisplayCutout cutout)469     private void assertAtMostOneCutoutPerEdge(TestActivity a, DisplayCutout cutout) {
470         final Rect safeRect = getSafeRect(a, cutout);
471 
472         assertThat("must not have more than one left cutout",
473                 boundsWith(cutout, (r) -> r.right <= safeRect.left), hasSize(lessThanOrEqualTo(1)));
474         assertThat("must not have more than one top cutout",
475                 boundsWith(cutout, (r) -> r.bottom <= safeRect.top), hasSize(lessThanOrEqualTo(1)));
476         assertThat("must not have more than one right cutout",
477                 boundsWith(cutout, (r) -> r.left >= safeRect.right), hasSize(lessThanOrEqualTo(1)));
478         assertThat("must not have more than one bottom cutout",
479                 boundsWith(cutout, (r) -> r.top >= safeRect.bottom), hasSize(lessThanOrEqualTo(1)));
480     }
481 
assertBoundsAreNonEmpty(DisplayCutout cutout)482     private void assertBoundsAreNonEmpty(DisplayCutout cutout) {
483         for (Rect boundingRect : cutout.getBoundingRects()) {
484             assertThat("rect in boundingRects must not be empty",
485                     boundingRect.isEmpty(), is(false));
486         }
487     }
488 
assertOnlyShortEdgeHasInsets(TestActivity activity, Rect insets)489     private void assertOnlyShortEdgeHasInsets(TestActivity activity, Rect insets) {
490         final Rect appBounds = getAppBounds(activity);
491         if (appBounds.height() > appBounds.width()) {
492             // Portrait display
493             assertThat("left edge has a cutout despite being long edge",
494                     insets.left, is(0));
495             assertThat("right edge has a cutout despite being long edge",
496                     insets.right, is(0));
497         }
498         if (appBounds.height() < appBounds.width()) {
499             // Landscape display
500             assertThat("top edge has a cutout despite being long edge",
501                     insets.top, is(0));
502             assertThat("bottom edge has a cutout despite being long edge",
503                     insets.bottom, is(0));
504         }
505     }
506 
assertOnlyShortEdgeHasBounds( TestActivity activity, DisplayCutout cutout, boolean canIgnoreSmallCutout)507     private void assertOnlyShortEdgeHasBounds(
508             TestActivity activity, DisplayCutout cutout, boolean canIgnoreSmallCutout) {
509         final Rect appBounds = getAppBounds(activity);
510         if (appBounds.height() > appBounds.width()) {
511             // Portrait display
512             if (!canIgnoreSmallCutout
513                     || cutout.getBoundingRectLeft().width() > mMaximumSizeForNoLetterbox) {
514                 assertThat("left edge has a cutout despite being long edge",
515                         hasBound(LEFT, cutout, appBounds), is(false));
516             }
517 
518             if (!canIgnoreSmallCutout
519                     || cutout.getBoundingRectRight().width() > mMaximumSizeForNoLetterbox) {
520                 assertThat("right edge has a cutout despite being long edge",
521                         hasBound(RIGHT, cutout, appBounds), is(false));
522             }
523         }
524         if (appBounds.height() < appBounds.width()) {
525             // Landscape display
526             if (!canIgnoreSmallCutout
527                     || cutout.getBoundingRectTop().height() > mMaximumSizeForNoLetterbox) {
528                 assertThat("top edge has a cutout despite being long edge",
529                         hasBound(TOP, cutout, appBounds), is(false));
530             }
531 
532             if (!canIgnoreSmallCutout
533                     || cutout.getBoundingRectBottom().height() > mMaximumSizeForNoLetterbox) {
534                 assertThat("bottom edge has a cutout despite being long edge",
535                         hasBound(BOTTOM, cutout, appBounds), is(false));
536             }
537         }
538     }
539 
canLayoutInDisplayCutoutWithoutLetterbox(int cutoutMode)540     private boolean canLayoutInDisplayCutoutWithoutLetterbox(int cutoutMode) {
541         return cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
542                 || cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
543     }
544 
hasBound(String position, DisplayCutout cutout, Rect appBound)545     private boolean hasBound(String position, DisplayCutout cutout, Rect appBound) {
546         final Rect cutoutRect;
547         final int waterfallSize;
548         if (LEFT.equals(position)) {
549             cutoutRect = cutout.getBoundingRectLeft();
550             waterfallSize = cutout.getWaterfallInsets().left;
551         } else if (TOP.equals(position)) {
552             cutoutRect = cutout.getBoundingRectTop();
553             waterfallSize = cutout.getWaterfallInsets().top;
554         } else if (RIGHT.equals(position)) {
555             cutoutRect = cutout.getBoundingRectRight();
556             waterfallSize = cutout.getWaterfallInsets().right;
557         } else {
558             cutoutRect = cutout.getBoundingRectBottom();
559             waterfallSize = cutout.getWaterfallInsets().bottom;
560         }
561         return Rect.intersects(cutoutRect, appBound) || waterfallSize > 0;
562     }
563 
boundsWith(DisplayCutout cutout, Predicate<Rect> predicate)564     private List<Rect> boundsWith(DisplayCutout cutout, Predicate<Rect> predicate) {
565         return cutout.getBoundingRects().stream().filter(predicate).collect(Collectors.toList());
566     }
567 
safeInsets(DisplayCutout displayCutout)568     private Rect safeInsets(DisplayCutout displayCutout) {
569         return safeInsets(displayCutout, false);
570     }
571 
safeInsets(DisplayCutout displayCutout, boolean canIgnoreSmallCutout)572     private Rect safeInsets(DisplayCutout displayCutout, boolean canIgnoreSmallCutout) {
573         if (displayCutout == null) {
574             return null;
575         }
576         return new Rect(
577                 safeInset(displayCutout.getSafeInsetLeft(), canIgnoreSmallCutout),
578                 safeInset(displayCutout.getSafeInsetTop(), canIgnoreSmallCutout),
579                 safeInset(displayCutout.getSafeInsetRight(), canIgnoreSmallCutout),
580                 safeInset(displayCutout.getSafeInsetBottom(), canIgnoreSmallCutout));
581     }
582 
safeInset(int inset, boolean canIgnoreSmallCutout)583     private int safeInset(int inset, boolean canIgnoreSmallCutout) {
584         return !canIgnoreSmallCutout || inset > mMaximumSizeForNoLetterbox ? inset : 0;
585     }
586 
systemWindowInsets(WindowInsets insets)587     private static Rect systemWindowInsets(WindowInsets insets) {
588         return new Rect(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
589                 insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
590     }
591 
stableInsets(WindowInsets insets)592     private static Rect stableInsets(WindowInsets insets) {
593         return new Rect(insets.getStableInsetLeft(), insets.getStableInsetTop(),
594                 insets.getStableInsetRight(), insets.getStableInsetBottom());
595     }
596 
getSafeRect(TestActivity a, DisplayCutout cutout)597     private Rect getSafeRect(TestActivity a, DisplayCutout cutout) {
598         final Rect safeRect = safeInsets(cutout);
599         safeRect.bottom = getOnMainSync(() -> a.getDecorView().getHeight()) - safeRect.bottom;
600         safeRect.right = getOnMainSync(() -> a.getDecorView().getWidth()) - safeRect.right;
601         return safeRect;
602     }
603 
getAppBounds(TestActivity a)604     private Rect getAppBounds(TestActivity a) {
605         final Rect appBounds = new Rect();
606         runOnMainSync(() -> {
607             appBounds.right = a.getDecorView().getWidth();
608             appBounds.bottom = a.getDecorView().getHeight();
609         });
610         return appBounds;
611     }
612 
insetsLessThanOrEqualTo(Rect max)613     private static Matcher<Rect> insetsLessThanOrEqualTo(Rect max) {
614         return new CustomTypeSafeMatcher<Rect>("must be smaller on each side than " + max) {
615             @Override
616             protected boolean matchesSafely(Rect actual) {
617                 return actual.left <= max.left && actual.top <= max.top
618                         && actual.right <= max.right && actual.bottom <= max.bottom;
619             }
620         };
621     }
622 
623     private static Matcher<Rect> intersectsWith(Rect safeRect) {
624         return new CustomTypeSafeMatcher<Rect>("intersects with " + safeRect) {
625             @Override
626             protected boolean matchesSafely(Rect item) {
627                 return Rect.intersects(safeRect, item);
628             }
629         };
630     }
631 
632     private static Matcher<Rect> insetValues(Matcher<Iterable<? super Integer>> valuesMatcher) {
633         return new FeatureMatcher<Rect, Iterable<Integer>>(valuesMatcher, "inset values",
634                 "inset values") {
635             @Override
636             protected Iterable<Integer> featureValueOf(Rect actual) {
637                 return Arrays.asList(actual.left, actual.top, actual.right, actual.bottom);
638             }
639         };
640     }
641 
642     private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
643         mErrorCollector.checkThat(reason, actual, matcher);
644     }
645 
646     private <R> R getOnMainSync(Supplier<R> f) {
647         final Object[] result = new Object[1];
648         runOnMainSync(() -> result[0] = f.get());
649         //noinspection unchecked
650         return (R) result[0];
651     }
652 
653     private void runOnMainSync(Runnable runnable) {
654         getInstrumentation().runOnMainSync(runnable);
655     }
656 
657     private <T extends TestActivity> T launchAndWait(ActivityTestRule<T> rule, int cutoutMode,
658             int orientation) {
659         final T activity = rule.launchActivity(
660                 new Intent().putExtra(EXTRA_CUTOUT_MODE, cutoutMode)
661                         .putExtra(EXTRA_ORIENTATION, orientation));
662         WindowUtil.waitForFocus(activity);
663         final WindowManagerStateHelper wmState = new WindowManagerStateHelper();
664         wmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
665         wmState.waitForDisplayUnfrozen();
666         return activity;
667     }
668 
669     private boolean supportsOrientation(int orientation) {
670         String systemFeature = "";
671         switch(orientation) {
672             case SCREEN_ORIENTATION_PORTRAIT:
673             case SCREEN_ORIENTATION_REVERSE_PORTRAIT:
674                 systemFeature = PackageManager.FEATURE_SCREEN_PORTRAIT;
675                 break;
676             case SCREEN_ORIENTATION_LANDSCAPE:
677             case SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
678                 systemFeature = PackageManager.FEATURE_SCREEN_LANDSCAPE;
679                 break;
680             default:
681                 throw new UnsupportedOperationException("Orientation not supported");
682         }
683 
684         return getInstrumentation().getTargetContext().getPackageManager()
685                 .hasSystemFeature(systemFeature);
686     }
687 
688     public static class TestActivity extends Activity {
689 
690         static final String EXTRA_CUTOUT_MODE = "extra.cutout_mode";
691         static final String EXTRA_ORIENTATION = "extra.orientation";
692         private WindowInsets mDispatchedInsets;
693 
694         @Override
695         protected void onCreate(Bundle savedInstanceState) {
696             super.onCreate(savedInstanceState);
697             getWindow().requestFeature(Window.FEATURE_NO_TITLE);
698             if (getIntent() != null) {
699                 setRequestedOrientation(getIntent().getIntExtra(
700                         EXTRA_ORIENTATION, SCREEN_ORIENTATION_UNSPECIFIED));
701             }
702             View view = new View(this);
703             view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
704             view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets);
705             setContentView(view);
706             // Because the PhoneWindow.java will set the CutoutMode
707             // So we have to set the CutoutMode after the setContentView method
708             if (getIntent() != null) {
709                 int mode = getIntent().getIntExtra(EXTRA_CUTOUT_MODE,
710                         LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT);
711                 getWindow().getAttributes().layoutInDisplayCutoutMode = mode;
712             }
713         }
714 
715         @Override
716         public void onWindowFocusChanged(boolean hasFocus) {
717             if (hasFocus) {
718                 getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
719                         | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
720                         | View.SYSTEM_UI_FLAG_FULLSCREEN
721                         | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
722             }
723         }
724 
725         View getDecorView() {
726             return getWindow().getDecorView();
727         }
728 
729         WindowInsets getRootInsets() {
730             return getWindow().getDecorView().getRootWindowInsets();
731         }
732 
733         WindowInsets getDispatchedInsets() {
734             return mDispatchedInsets;
735         }
736     }
737 
738     interface TestDef {
739         void run(TestActivity a, WindowInsets insets, DisplayCutout cutout, Which whichInsets);
740 
741         enum Which {
742             DISPATCHED, ROOT
743         }
744     }
745 }
746