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.server.wm.DisplayCutoutTests.TestActivity.EXTRA_CUTOUT_MODE; 20 import static android.server.wm.DisplayCutoutTests.TestDef.Which.DISPATCHED; 21 import static android.server.wm.DisplayCutoutTests.TestDef.Which.ROOT; 22 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; 24 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; 25 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; 26 27 import static androidx.test.InstrumentationRegistry.getInstrumentation; 28 29 import static org.hamcrest.Matchers.equalTo; 30 import static org.hamcrest.Matchers.everyItem; 31 import static org.hamcrest.Matchers.greaterThan; 32 import static org.hamcrest.Matchers.greaterThanOrEqualTo; 33 import static org.hamcrest.Matchers.hasItem; 34 import static org.hamcrest.Matchers.hasSize; 35 import static org.hamcrest.Matchers.is; 36 import static org.hamcrest.Matchers.lessThanOrEqualTo; 37 import static org.hamcrest.Matchers.not; 38 import static org.hamcrest.Matchers.notNullValue; 39 import static org.hamcrest.Matchers.nullValue; 40 import static org.junit.Assert.assertEquals; 41 import static org.junit.Assert.assertTrue; 42 43 import android.app.Activity; 44 import android.content.Intent; 45 import android.graphics.Insets; 46 import android.graphics.Point; 47 import android.graphics.Rect; 48 import android.os.Bundle; 49 import android.platform.test.annotations.Presubmit; 50 import android.view.DisplayCutout; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.view.Window; 54 import android.view.WindowInsets; 55 56 import androidx.test.rule.ActivityTestRule; 57 58 import com.android.compatibility.common.util.PollingCheck; 59 60 import org.hamcrest.CustomTypeSafeMatcher; 61 import org.hamcrest.FeatureMatcher; 62 import org.hamcrest.Matcher; 63 import org.junit.Assert; 64 import org.junit.Rule; 65 import org.junit.Test; 66 import org.junit.rules.ErrorCollector; 67 68 import java.util.Arrays; 69 import java.util.List; 70 import java.util.function.Predicate; 71 import java.util.function.Supplier; 72 import java.util.stream.Collectors; 73 74 /** 75 * Build/Install/Run: 76 * atest CtsWindowManagerDeviceTestCases:DisplayCutoutTests 77 */ 78 @Presubmit 79 public class DisplayCutoutTests { 80 static final Rect ZERO_RECT = new Rect(); 81 82 @Rule 83 public final ErrorCollector mErrorCollector = new ErrorCollector(); 84 85 @Rule 86 public final ActivityTestRule<TestActivity> mDisplayCutoutActivity = 87 new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, 88 false /* launchActivity */); 89 90 @Test 91 public void testConstructor() { 92 final Insets safeInsets = Insets.of(1, 2, 3, 4); 93 final Rect boundLeft = new Rect(5, 6, 7, 8); 94 final Rect boundTop = new Rect(9, 0, 10, 1); 95 final Rect boundRight = new Rect(2, 3, 4, 5); 96 final Rect boundBottom = new Rect(6, 7, 8, 9); 97 98 final DisplayCutout displayCutout = 99 new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom); 100 101 assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft()); 102 assertEquals(safeInsets.top, displayCutout.getSafeInsetTop()); 103 assertEquals(safeInsets.right, displayCutout.getSafeInsetRight()); 104 assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom()); 105 106 assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft())); 107 assertTrue(boundTop.equals(displayCutout.getBoundingRectTop())); 108 assertTrue(boundRight.equals(displayCutout.getBoundingRectRight())); 109 assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom())); 110 } 111 112 @Test 113 public void testDisplayCutout_default_portrait() { 114 runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, (activity, insets, displayCutout, which) -> { 115 if (displayCutout == null) { 116 return; 117 } 118 if (which == ROOT) { 119 assertThat("cutout must be contained within system bars in default mode", 120 safeInsets(displayCutout), insetsLessThanOrEqualTo(stableInsets(insets))); 121 } else if (which == DISPATCHED) { 122 assertThat("must not dipatch to hierarchy in default mode", 123 displayCutout, nullValue()); 124 } 125 }); 126 } 127 128 @Test 129 public void testDisplayCutout_landscape() { 130 // TODO add landscape variants 131 } 132 133 @Test 134 public void testDisplayCutout_shortEdges_portrait() { 135 runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, (a, insets, displayCutout, which) -> { 136 if (which == ROOT) { 137 assertThat("Display.getCutout() must equal view root cutout", 138 a.getDisplay().getCutout(), equalTo(displayCutout)); 139 } 140 }); 141 } 142 143 @Test 144 public void testDisplayCutout_never_portrait() { 145 runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, (a, insets, displayCutout, which) -> { 146 assertThat("must not layout in cutout area in never mode", displayCutout, nullValue()); 147 }); 148 } 149 150 private void runTest(int cutoutMode, TestDef test) { 151 final TestActivity activity = launchAndWait(mDisplayCutoutActivity, 152 cutoutMode); 153 154 WindowInsets insets = getOnMainSync(activity::getRootInsets); 155 WindowInsets dispatchedInsets = getOnMainSync(activity::getDispatchedInsets); 156 Assert.assertThat("test setup failed, no insets at root", insets, notNullValue()); 157 Assert.assertThat("test setup failed, no insets dispatched", 158 dispatchedInsets, notNullValue()); 159 160 final DisplayCutout displayCutout = insets.getDisplayCutout(); 161 final DisplayCutout dispatchedDisplayCutout = dispatchedInsets.getDisplayCutout(); 162 163 if (displayCutout != null) { 164 commonAsserts(activity, insets, displayCutout); 165 assertCutoutsAreConsistentWithInsets(displayCutout); 166 } 167 test.run(activity, insets, displayCutout, ROOT); 168 169 if (dispatchedDisplayCutout != null) { 170 commonAsserts(activity, dispatchedInsets, dispatchedDisplayCutout); 171 assertCutoutsAreConsistentWithInsets(dispatchedDisplayCutout); 172 } 173 test.run(activity, dispatchedInsets, dispatchedDisplayCutout, DISPATCHED); 174 } 175 176 private void commonAsserts(TestActivity activity, WindowInsets insets, DisplayCutout cutout) { 177 assertSafeInsetsValid(cutout); 178 assertThat("systemWindowInsets (also known as content insets) must be at least as large as " 179 + "cutout safe insets", 180 safeInsets(cutout), insetsLessThanOrEqualTo(systemWindowInsets(insets))); 181 assertOnlyShortEdgeHasInsets(activity, cutout); 182 assertOnlyShortEdgeHasBounds(activity, insets, cutout); 183 assertCutoutsAreWithinSafeInsets(activity, cutout); 184 assertBoundsAreNonEmpty(cutout); 185 assertAtMostOneCutoutPerEdge(activity, cutout); 186 } 187 188 private void assertCutoutIsConsistentWithInset(String position, int insetSize, Rect bound) { 189 if (insetSize > 0) { 190 assertThat("cutout must have a bound on the " + position, bound, 191 not(equalTo(ZERO_RECT))); 192 } else { 193 assertThat("cutout must have no bound on the " + position, bound, 194 equalTo(ZERO_RECT)); 195 } 196 } 197 198 public void assertCutoutsAreConsistentWithInsets(DisplayCutout cutout) { 199 final Rect safeInsets = safeInsets(cutout); 200 assertCutoutIsConsistentWithInset("top", safeInsets.top, cutout.getBoundingRectTop()); 201 assertCutoutIsConsistentWithInset("bottom", safeInsets.bottom, 202 cutout.getBoundingRectBottom()); 203 assertCutoutIsConsistentWithInset("left", safeInsets.left, cutout.getBoundingRectLeft()); 204 assertCutoutIsConsistentWithInset("right", safeInsets.right, cutout.getBoundingRectRight()); 205 } 206 207 private void assertSafeInsetsValid(DisplayCutout displayCutout) { 208 //noinspection unchecked 209 assertThat("all safe insets must be non-negative", safeInsets(displayCutout), 210 insetValues(everyItem((Matcher)greaterThanOrEqualTo(0)))); 211 assertThat("at least one safe inset must be positive," 212 + " otherwise WindowInsets.getDisplayCutout()) must return null", 213 safeInsets(displayCutout), insetValues(hasItem(greaterThan(0)))); 214 } 215 216 private void assertCutoutsAreWithinSafeInsets(TestActivity a, DisplayCutout cutout) { 217 final Rect safeRect = getSafeRect(a, cutout); 218 219 assertThat("safe insets must not cover the entire screen", safeRect.isEmpty(), is(false)); 220 for (Rect boundingRect : cutout.getBoundingRects()) { 221 assertThat("boundingRects must not extend beyond safeInsets", 222 boundingRect, not(intersectsWith(safeRect))); 223 } 224 } 225 226 private void assertAtMostOneCutoutPerEdge(TestActivity a, DisplayCutout cutout) { 227 final Rect safeRect = getSafeRect(a, cutout); 228 229 assertThat("must not have more than one left cutout", 230 boundsWith(cutout, (r) -> r.right <= safeRect.left), hasSize(lessThanOrEqualTo(1))); 231 assertThat("must not have more than one top cutout", 232 boundsWith(cutout, (r) -> r.bottom <= safeRect.top), hasSize(lessThanOrEqualTo(1))); 233 assertThat("must not have more than one right cutout", 234 boundsWith(cutout, (r) -> r.left >= safeRect.right), hasSize(lessThanOrEqualTo(1))); 235 assertThat("must not have more than one bottom cutout", 236 boundsWith(cutout, (r) -> r.top >= safeRect.bottom), hasSize(lessThanOrEqualTo(1))); 237 } 238 239 private void assertBoundsAreNonEmpty(DisplayCutout cutout) { 240 for (Rect boundingRect : cutout.getBoundingRects()) { 241 assertThat("rect in boundingRects must not be empty", 242 boundingRect.isEmpty(), is(false)); 243 } 244 } 245 246 private void assertOnlyShortEdgeHasInsets(TestActivity activity, 247 DisplayCutout displayCutout) { 248 final Point displaySize = new Point(); 249 runOnMainSync(() -> activity.getDecorView().getDisplay().getRealSize(displaySize)); 250 if (displaySize.y > displaySize.x) { 251 // Portrait display 252 assertThat("left edge has a cutout despite being long edge", 253 displayCutout.getSafeInsetLeft(), is(0)); 254 assertThat("right edge has a cutout despite being long edge", 255 displayCutout.getSafeInsetRight(), is(0)); 256 } 257 if (displaySize.y < displaySize.x) { 258 // Landscape display 259 assertThat("top edge has a cutout despite being long edge", 260 displayCutout.getSafeInsetTop(), is(0)); 261 assertThat("bottom edge has a cutout despite being long edge", 262 displayCutout.getSafeInsetBottom(), is(0)); 263 } 264 } 265 266 private void assertOnlyShortEdgeHasBounds(TestActivity activity, WindowInsets insets, 267 DisplayCutout cutout) { 268 final Point displaySize = new Point(); 269 runOnMainSync(() -> activity.getDecorView().getDisplay().getRealSize(displaySize)); 270 if (displaySize.y > displaySize.x) { 271 // Portrait display 272 assertThat("left edge has a cutout despite being long edge", 273 cutout.getBoundingRectLeft(), is(ZERO_RECT)); 274 assertThat("right edge has a cutout despite being long edge", 275 cutout.getBoundingRectRight(), is(ZERO_RECT)); 276 } 277 if (displaySize.y < displaySize.x) { 278 // Landscape display 279 assertThat("top edge has a cutout despite being long edge", 280 cutout.getBoundingRectTop(), is(ZERO_RECT)); 281 assertThat("bottom edge has a cutout despite being long edge", 282 cutout.getBoundingRectBottom(), is(ZERO_RECT)); 283 } 284 } 285 286 private List<Rect> boundsWith(DisplayCutout cutout, Predicate<Rect> predicate) { 287 return cutout.getBoundingRects().stream().filter(predicate).collect(Collectors.toList()); 288 } 289 290 291 private static Rect safeInsets(DisplayCutout displayCutout) { 292 return new Rect(displayCutout.getSafeInsetLeft(), displayCutout.getSafeInsetTop(), 293 displayCutout.getSafeInsetRight(), displayCutout.getSafeInsetBottom()); 294 } 295 296 private static Rect systemWindowInsets(WindowInsets insets) { 297 return new Rect(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), 298 insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); 299 } 300 301 private static Rect stableInsets(WindowInsets insets) { 302 return new Rect(insets.getStableInsetLeft(), insets.getStableInsetTop(), 303 insets.getStableInsetRight(), insets.getStableInsetBottom()); 304 } 305 306 private Rect getSafeRect(TestActivity a, DisplayCutout cutout) { 307 final Rect safeRect = safeInsets(cutout); 308 safeRect.bottom = getOnMainSync(() -> a.getDecorView().getHeight()) - safeRect.bottom; 309 safeRect.right = getOnMainSync(() -> a.getDecorView().getWidth()) - safeRect.right; 310 return safeRect; 311 } 312 313 private static Matcher<Rect> insetsLessThanOrEqualTo(Rect max) { 314 return new CustomTypeSafeMatcher<Rect>("must be smaller on each side than " + max) { 315 @Override 316 protected boolean matchesSafely(Rect actual) { 317 return actual.left <= max.left && actual.top <= max.top 318 && actual.right <= max.right && actual.bottom <= max.bottom; 319 } 320 }; 321 } 322 323 private static Matcher<Rect> intersectsWith(Rect safeRect) { 324 return new CustomTypeSafeMatcher<Rect>("intersects with " + safeRect) { 325 @Override 326 protected boolean matchesSafely(Rect item) { 327 return Rect.intersects(safeRect, item); 328 } 329 }; 330 } 331 332 private static Matcher<Rect> insetValues(Matcher<Iterable<? super Integer>> valuesMatcher) { 333 return new FeatureMatcher<Rect, Iterable<Integer>>(valuesMatcher, "inset values", 334 "inset values") { 335 @Override 336 protected Iterable<Integer> featureValueOf(Rect actual) { 337 return Arrays.asList(actual.left, actual.top, actual.right, actual.bottom); 338 } 339 }; 340 } 341 342 private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) { 343 mErrorCollector.checkThat(reason, actual, matcher); 344 } 345 346 private <R> R getOnMainSync(Supplier<R> f) { 347 final Object[] result = new Object[1]; 348 runOnMainSync(() -> result[0] = f.get()); 349 //noinspection unchecked 350 return (R) result[0]; 351 } 352 353 private void runOnMainSync(Runnable runnable) { 354 getInstrumentation().runOnMainSync(runnable); 355 } 356 357 private <T extends Activity> T launchAndWait(ActivityTestRule<T> rule, int cutoutMode) { 358 final T activity = rule.launchActivity( 359 new Intent().putExtra(EXTRA_CUTOUT_MODE, cutoutMode)); 360 PollingCheck.waitFor(activity::hasWindowFocus); 361 return activity; 362 } 363 364 public static class TestActivity extends Activity { 365 366 static final String EXTRA_CUTOUT_MODE = "extra.cutout_mode"; 367 private WindowInsets mDispatchedInsets; 368 369 @Override 370 protected void onCreate(Bundle savedInstanceState) { 371 super.onCreate(savedInstanceState); 372 getWindow().requestFeature(Window.FEATURE_NO_TITLE); 373 if (getIntent() != null) { 374 getWindow().getAttributes().layoutInDisplayCutoutMode = getIntent().getIntExtra( 375 EXTRA_CUTOUT_MODE, LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT); 376 } 377 View view = new View(this); 378 view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 379 view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets); 380 setContentView(view); 381 } 382 383 @Override 384 public void onWindowFocusChanged(boolean hasFocus) { 385 if (hasFocus) { 386 getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 387 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 388 } 389 } 390 391 View getDecorView() { 392 return getWindow().getDecorView(); 393 } 394 395 WindowInsets getRootInsets() { 396 return getWindow().getDecorView().getRootWindowInsets(); 397 } 398 399 WindowInsets getDispatchedInsets() { 400 return mDispatchedInsets; 401 } 402 } 403 404 interface TestDef { 405 void run(TestActivity a, WindowInsets insets, DisplayCutout cutout, Which whichInsets); 406 407 enum Which { 408 DISPATCHED, ROOT 409 } 410 } 411 } 412