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