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