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