| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package android.server.wm; |
| |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; |
| import static android.provider.Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS; |
| import static android.server.wm.DisplayCutoutTests.TestActivity.EXTRA_CUTOUT_MODE; |
| import static android.server.wm.DisplayCutoutTests.TestActivity.EXTRA_ORIENTATION; |
| import static android.server.wm.DisplayCutoutTests.TestDef.Which.DISPATCHED; |
| import static android.server.wm.DisplayCutoutTests.TestDef.Which.ROOT; |
| import static android.util.DisplayMetrics.DENSITY_DEFAULT; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; |
| import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; |
| import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; |
| import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; |
| import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; |
| |
| import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; |
| |
| import static org.hamcrest.Matchers.equalTo; |
| import static org.hamcrest.Matchers.everyItem; |
| import static org.hamcrest.Matchers.greaterThan; |
| import static org.hamcrest.Matchers.greaterThanOrEqualTo; |
| import static org.hamcrest.Matchers.hasItem; |
| import static org.hamcrest.Matchers.hasSize; |
| import static org.hamcrest.Matchers.is; |
| import static org.hamcrest.Matchers.lessThanOrEqualTo; |
| import static org.hamcrest.Matchers.not; |
| import static org.hamcrest.Matchers.notNullValue; |
| import static org.hamcrest.Matchers.nullValue; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.graphics.Insets; |
| import android.graphics.Path; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.platform.test.annotations.Presubmit; |
| import android.provider.Settings; |
| import android.server.wm.settings.SettingsSession; |
| import android.view.DisplayCutout; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowInsets; |
| import android.view.WindowInsets.Type; |
| |
| import androidx.test.rule.ActivityTestRule; |
| |
| import com.android.compatibility.common.util.WindowUtil; |
| |
| import org.hamcrest.CustomTypeSafeMatcher; |
| import org.hamcrest.FeatureMatcher; |
| import org.hamcrest.Matcher; |
| import org.junit.AfterClass; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.ErrorCollector; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameter; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.function.Predicate; |
| import java.util.function.Supplier; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Build/Install/Run: |
| * atest CtsWindowManagerDeviceTestCases:DisplayCutoutTests |
| */ |
| @Presubmit |
| @android.server.wm.annotation.Group3 |
| @RunWith(Parameterized.class) |
| public class DisplayCutoutTests { |
| private static SettingsSession<String> sImmersiveModeConfirmationSetting; |
| |
| static final String LEFT = "left"; |
| static final String TOP = "top"; |
| static final String RIGHT = "right"; |
| static final String BOTTOM = "bottom"; |
| |
| /** |
| * @see LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT |
| * @see LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES |
| */ |
| private static final int MAXIMUM_SIZE_FOR_NO_LETTERBOX_IF_DEFAULT_OR_SHORT_EDGE_DP = 16; |
| |
| @Parameterized.Parameters(name= "{1}({0})") |
| public static Object[][] data() { |
| return new Object[][]{ |
| {SCREEN_ORIENTATION_PORTRAIT, "SCREEN_ORIENTATION_PORTRAIT"}, |
| {SCREEN_ORIENTATION_LANDSCAPE, "SCREEN_ORIENTATION_LANDSCAPE"}, |
| {SCREEN_ORIENTATION_REVERSE_LANDSCAPE, "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"}, |
| {SCREEN_ORIENTATION_REVERSE_PORTRAIT, "SCREEN_ORIENTATION_REVERSE_PORTRAIT"}, |
| }; |
| } |
| |
| @Parameter(0) |
| public int orientation; |
| |
| @Parameter(1) |
| public String orientationName; |
| |
| @Rule |
| public final ErrorCollector mErrorCollector = new ErrorCollector(); |
| |
| @Rule |
| public final ActivityTestRule<TestActivity> mDisplayCutoutActivity = |
| new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| // OEMs can have an option not to letterbox, if the cutout overlaps at most |
| // 16 dp with app windows/contents for the apps using DEFAULT and SHORT_EDGES. |
| private int mMaximumSizeForNoLetterbox; |
| |
| @BeforeClass |
| public static void setUpClass() { |
| sImmersiveModeConfirmationSetting = new SettingsSession<>( |
| Settings.Secure.getUriFor(IMMERSIVE_MODE_CONFIRMATIONS), |
| Settings.Secure::getString, Settings.Secure::putString); |
| sImmersiveModeConfirmationSetting.set("confirmed"); |
| } |
| |
| @AfterClass |
| public static void tearDownClass() { |
| if (sImmersiveModeConfirmationSetting != null) { |
| sImmersiveModeConfirmationSetting.close(); |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| final Context context = getInstrumentation().getContext(); |
| mMaximumSizeForNoLetterbox = |
| (context.getResources().getConfiguration().densityDpi / DENSITY_DEFAULT) |
| * MAXIMUM_SIZE_FOR_NO_LETTERBOX_IF_DEFAULT_OR_SHORT_EDGE_DP; |
| } |
| |
| @Test |
| public void testConstructor() { |
| final Insets safeInsets = Insets.of(1, 2, 3, 4); |
| final Rect boundLeft = new Rect(5, 6, 7, 8); |
| final Rect boundTop = new Rect(9, 0, 10, 1); |
| final Rect boundRight = new Rect(2, 3, 4, 5); |
| final Rect boundBottom = new Rect(6, 7, 8, 9); |
| |
| final DisplayCutout displayCutout = |
| new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom); |
| |
| assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft()); |
| assertEquals(safeInsets.top, displayCutout.getSafeInsetTop()); |
| assertEquals(safeInsets.right, displayCutout.getSafeInsetRight()); |
| assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom()); |
| |
| assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft())); |
| assertTrue(boundTop.equals(displayCutout.getBoundingRectTop())); |
| assertTrue(boundRight.equals(displayCutout.getBoundingRectRight())); |
| assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom())); |
| |
| assertEquals(Insets.NONE, displayCutout.getWaterfallInsets()); |
| } |
| |
| @Test |
| public void testConstructor_waterfall() { |
| final Insets safeInsets = Insets.of(1, 2, 3, 4); |
| final Rect boundLeft = new Rect(5, 6, 7, 8); |
| final Rect boundTop = new Rect(9, 0, 10, 1); |
| final Rect boundRight = new Rect(2, 3, 4, 5); |
| final Rect boundBottom = new Rect(6, 7, 8, 9); |
| final Insets waterfallInsets = Insets.of(4, 8, 12, 16); |
| |
| final DisplayCutout displayCutout = |
| new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom, |
| waterfallInsets); |
| |
| assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft()); |
| assertEquals(safeInsets.top, displayCutout.getSafeInsetTop()); |
| assertEquals(safeInsets.right, displayCutout.getSafeInsetRight()); |
| assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom()); |
| |
| assertTrue(boundLeft.equals(displayCutout.getBoundingRectLeft())); |
| assertTrue(boundTop.equals(displayCutout.getBoundingRectTop())); |
| assertTrue(boundRight.equals(displayCutout.getBoundingRectRight())); |
| assertTrue(boundBottom.equals(displayCutout.getBoundingRectBottom())); |
| |
| assertEquals(waterfallInsets, displayCutout.getWaterfallInsets()); |
| } |
| |
| @Test |
| public void testBuilder() { |
| final Insets safeInsets = Insets.of(1, 2, 1, 0); |
| final Insets waterfallInsets = Insets.of(1, 0, 1, 0); |
| final Rect boundingLeft = new Rect(5, 6, 7, 8); |
| final Rect boundingRectTop = new Rect(9, 0, 10, 1); |
| final Rect boundingRight = new Rect(2, 3, 4, 5); |
| final Rect boundingBottom = new Rect(6, 7, 8, 9); |
| final Path cutoutPath = new Path(); |
| |
| final DisplayCutout displayCutout = new DisplayCutout.Builder() |
| .setSafeInsets(safeInsets) |
| .setWaterfallInsets(waterfallInsets) |
| .setBoundingRectLeft(boundingLeft) |
| .setBoundingRectTop(boundingRectTop) |
| .setBoundingRectRight(boundingRight) |
| .setBoundingRectBottom(boundingBottom) |
| .setCutoutPath(cutoutPath) |
| .build(); |
| |
| assertEquals(safeInsets.left, displayCutout.getSafeInsetLeft()); |
| assertEquals(safeInsets.top, displayCutout.getSafeInsetTop()); |
| assertEquals(safeInsets.right, displayCutout.getSafeInsetRight()); |
| assertEquals(safeInsets.bottom, displayCutout.getSafeInsetBottom()); |
| assertEquals(waterfallInsets, displayCutout.getWaterfallInsets()); |
| assertEquals(boundingLeft, displayCutout.getBoundingRectLeft()); |
| assertEquals(boundingRectTop, displayCutout.getBoundingRectTop()); |
| assertEquals(boundingRight, displayCutout.getBoundingRectRight()); |
| assertEquals(boundingBottom, displayCutout.getBoundingRectBottom()); |
| assertEquals(cutoutPath, displayCutout.getCutoutPath()); |
| } |
| |
| @Test |
| public void testDisplayCutout_default() { |
| runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, |
| (activity, insets, displayCutout, which) -> { |
| if (displayCutout == null) { |
| return; |
| } |
| if (which == ROOT) { |
| assertThat("cutout must be contained within system bars in default mode", |
| safeInsets(displayCutout, true /* canIgnoreSmallCutout */), |
| insetsLessThanOrEqualTo(stableInsets(insets))); |
| } else if (which == DISPATCHED) { |
| assertThat("must not dispatch to hierarchy in default mode", |
| displayCutout, nullValue()); |
| } |
| }); |
| } |
| |
| @Test |
| public void testDisplayCutout_shortEdges() { |
| runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, (a, insets, cutout, which) -> { |
| if (which == ROOT) { |
| final Rect appBounds = getAppBounds(a); |
| final Insets displaySafeInsets = Insets.of( |
| safeInsets(a.getDisplay().getCutout(), true /* canIgnoreSmallCutout */)); |
| final Insets expected; |
| if (appBounds.height() > appBounds.width()) { |
| // Portrait display |
| expected = Insets.of(0, displaySafeInsets.top, 0, displaySafeInsets.bottom); |
| } else if (appBounds.height() < appBounds.width()) { |
| // Landscape display |
| expected = Insets.of(displaySafeInsets.left, 0, displaySafeInsets.right, 0); |
| } else { |
| expected = Insets.NONE; |
| } |
| assertThat("cutout must provide the display's safe insets on short edges and zero" |
| + " on the long edges.", |
| Insets.of(safeInsets(cutout, true /* canIgnoreSmallCutout */)), |
| equalTo(expected)); |
| } |
| }); |
| } |
| |
| @Test |
| public void testDisplayCutout_never() { |
| runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, (a, insets, displayCutout, which) -> { |
| assertThat("must not layout in cutout area in never mode", displayCutout, nullValue()); |
| }); |
| } |
| |
| @Test |
| public void testDisplayCutout_always() { |
| runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, (a, insets, displayCutout, which) -> { |
| if (which == ROOT) { |
| assertThat("Display.getCutout() must equal view root cutout", |
| a.getDisplay().getCutout(), equalTo(displayCutout)); |
| } |
| }); |
| } |
| |
| @Test |
| public void testDisplayCutout_CutoutPaths() { |
| runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, (a, insets, displayCutout, which) -> { |
| if (displayCutout == null) { |
| return; |
| } |
| final Path cutoutPath = displayCutout.getCutoutPath(); |
| assertCutoutPath(LEFT, displayCutout.getBoundingRectLeft(), cutoutPath); |
| assertCutoutPath(TOP, displayCutout.getBoundingRectTop(), cutoutPath); |
| assertCutoutPath(RIGHT, displayCutout.getBoundingRectRight(), cutoutPath); |
| assertCutoutPath(BOTTOM, displayCutout.getBoundingRectBottom(), cutoutPath); |
| }); |
| } |
| |
| private void assertCutoutPath(String position, Rect cutoutRect, Path cutoutPath) { |
| if (cutoutRect.isEmpty()) { |
| return; |
| } |
| final Path intersected = new Path(); |
| intersected.addRect(cutoutRect.left, cutoutRect.top, cutoutRect.right, cutoutRect.bottom, |
| Path.Direction.CCW); |
| intersected.op(cutoutPath, Path.Op.INTERSECT); |
| assertFalse("Must have cutout path on " + position, intersected.isEmpty()); |
| } |
| |
| private void runTest(int cutoutMode, TestDef test) { |
| runTest(cutoutMode, test, orientation); |
| } |
| |
| private void runTest(int cutoutMode, TestDef test, int orientation) { |
| assumeTrue("Skipping test: orientation not supported", supportsOrientation(orientation)); |
| final TestActivity activity = launchAndWait(mDisplayCutoutActivity, |
| cutoutMode, orientation); |
| |
| WindowInsets insets = getOnMainSync(activity::getRootInsets); |
| WindowInsets dispatchedInsets = getOnMainSync(activity::getDispatchedInsets); |
| Assert.assertThat("test setup failed, no insets at root", insets, notNullValue()); |
| Assert.assertThat("test setup failed, no insets dispatched", |
| dispatchedInsets, notNullValue()); |
| |
| final DisplayCutout displayCutout = insets.getDisplayCutout(); |
| final DisplayCutout dispatchedDisplayCutout = dispatchedInsets.getDisplayCutout(); |
| if (displayCutout != null) { |
| commonAsserts(activity, displayCutout); |
| if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) { |
| shortEdgeAsserts(activity, insets, displayCutout, |
| canLayoutInDisplayCutoutWithoutLetterbox(cutoutMode)); |
| } |
| assertCutoutsAreConsistentWithInsets(activity, displayCutout); |
| assertSafeInsetsAreConsistentWithDisplayCutoutInsets(insets); |
| } |
| test.run(activity, insets, displayCutout, ROOT); |
| |
| if (dispatchedDisplayCutout != null) { |
| commonAsserts(activity, dispatchedDisplayCutout); |
| if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) { |
| shortEdgeAsserts(activity, insets, dispatchedDisplayCutout, |
| canLayoutInDisplayCutoutWithoutLetterbox(cutoutMode)); |
| } |
| assertCutoutsAreConsistentWithInsets(activity, dispatchedDisplayCutout); |
| if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) { |
| assertSafeInsetsAreConsistentWithDisplayCutoutInsets(dispatchedInsets); |
| } |
| } |
| test.run(activity, dispatchedInsets, dispatchedDisplayCutout, DISPATCHED); |
| } |
| |
| private void assertSafeInsetsAreConsistentWithDisplayCutoutInsets(WindowInsets insets) { |
| DisplayCutout cutout = insets.getDisplayCutout(); |
| Insets safeInsets = Insets.of(safeInsets(cutout)); |
| assertEquals("WindowInsets.getInsets(displayCutout()) must equal" |
| + " DisplayCutout.getSafeInsets()", |
| safeInsets, insets.getInsets(Type.displayCutout())); |
| assertEquals("WindowInsets.getInsetsIgnoringVisibility(displayCutout()) must equal" |
| + " DisplayCutout.getSafeInsets()", |
| safeInsets, insets.getInsetsIgnoringVisibility(Type.displayCutout())); |
| } |
| |
| private void commonAsserts(TestActivity activity, DisplayCutout cutout) { |
| assertSafeInsetsValid(cutout); |
| assertCutoutsAreWithinSafeInsets(activity, cutout); |
| assertBoundsAreNonEmpty(cutout); |
| assertAtMostOneCutoutPerEdge(activity, cutout); |
| } |
| |
| private void shortEdgeAsserts( |
| TestActivity activity, WindowInsets insets, DisplayCutout cutout, |
| boolean canIgnoreSmallCutout) { |
| final Rect safeInsets = safeInsets(cutout, canIgnoreSmallCutout); |
| assertOnlyShortEdgeHasInsets(activity, safeInsets); |
| assertOnlyShortEdgeHasBounds(activity, cutout, canIgnoreSmallCutout); |
| assertThat("systemWindowInsets (also known as content insets) must be at least as " |
| + "large as cutout safe insets", |
| safeInsets, insetsLessThanOrEqualTo(systemWindowInsets(insets))); |
| } |
| |
| private void assertCutoutIsConsistentWithInset(String position, DisplayCutout cutout, |
| int safeInsetSize, Rect appBound) { |
| if (safeInsetSize > 0) { |
| assertThat("cutout must have a bound on the " + position, |
| hasBound(position, cutout, appBound), is(true)); |
| } else { |
| assertThat("cutout must have no bound on the " + position, |
| hasBound(position, cutout, appBound), is(false)); |
| } |
| } |
| |
| public void assertCutoutsAreConsistentWithInsets(TestActivity activity, DisplayCutout cutout) { |
| final Rect appBounds = getAppBounds(activity); |
| assertCutoutIsConsistentWithInset(TOP, cutout, cutout.getSafeInsetTop(), appBounds); |
| assertCutoutIsConsistentWithInset(BOTTOM, cutout, cutout.getSafeInsetBottom(), appBounds); |
| assertCutoutIsConsistentWithInset(LEFT, cutout, cutout.getSafeInsetLeft(), appBounds); |
| assertCutoutIsConsistentWithInset(RIGHT, cutout, cutout.getSafeInsetRight(), appBounds); |
| } |
| |
| private void assertSafeInsetsValid(DisplayCutout displayCutout) { |
| //noinspection unchecked |
| assertThat("all safe insets must be non-negative", safeInsets(displayCutout), |
| insetValues(everyItem((Matcher)greaterThanOrEqualTo(0)))); |
| assertThat("at least one safe inset must be positive," |
| + " otherwise WindowInsets.getDisplayCutout()) must return null", |
| safeInsets(displayCutout), insetValues(hasItem(greaterThan(0)))); |
| } |
| |
| private void assertCutoutsAreWithinSafeInsets(TestActivity a, DisplayCutout cutout) { |
| final Rect safeRect = getSafeRect(a, cutout); |
| |
| assertThat("safe insets must not cover the entire screen", safeRect.isEmpty(), is(false)); |
| for (Rect boundingRect : cutout.getBoundingRects()) { |
| assertThat("boundingRects must not extend beyond safeInsets", |
| boundingRect, not(intersectsWith(safeRect))); |
| } |
| } |
| |
| private void assertAtMostOneCutoutPerEdge(TestActivity a, DisplayCutout cutout) { |
| final Rect safeRect = getSafeRect(a, cutout); |
| |
| assertThat("must not have more than one left cutout", |
| boundsWith(cutout, (r) -> r.right <= safeRect.left), hasSize(lessThanOrEqualTo(1))); |
| assertThat("must not have more than one top cutout", |
| boundsWith(cutout, (r) -> r.bottom <= safeRect.top), hasSize(lessThanOrEqualTo(1))); |
| assertThat("must not have more than one right cutout", |
| boundsWith(cutout, (r) -> r.left >= safeRect.right), hasSize(lessThanOrEqualTo(1))); |
| assertThat("must not have more than one bottom cutout", |
| boundsWith(cutout, (r) -> r.top >= safeRect.bottom), hasSize(lessThanOrEqualTo(1))); |
| } |
| |
| private void assertBoundsAreNonEmpty(DisplayCutout cutout) { |
| for (Rect boundingRect : cutout.getBoundingRects()) { |
| assertThat("rect in boundingRects must not be empty", |
| boundingRect.isEmpty(), is(false)); |
| } |
| } |
| |
| private void assertOnlyShortEdgeHasInsets(TestActivity activity, Rect insets) { |
| final Rect appBounds = getAppBounds(activity); |
| if (appBounds.height() > appBounds.width()) { |
| // Portrait display |
| assertThat("left edge has a cutout despite being long edge", |
| insets.left, is(0)); |
| assertThat("right edge has a cutout despite being long edge", |
| insets.right, is(0)); |
| } |
| if (appBounds.height() < appBounds.width()) { |
| // Landscape display |
| assertThat("top edge has a cutout despite being long edge", |
| insets.top, is(0)); |
| assertThat("bottom edge has a cutout despite being long edge", |
| insets.bottom, is(0)); |
| } |
| } |
| |
| private void assertOnlyShortEdgeHasBounds( |
| TestActivity activity, DisplayCutout cutout, boolean canIgnoreSmallCutout) { |
| final Rect appBounds = getAppBounds(activity); |
| if (appBounds.height() > appBounds.width()) { |
| // Portrait display |
| if (!canIgnoreSmallCutout |
| || cutout.getBoundingRectLeft().width() > mMaximumSizeForNoLetterbox) { |
| assertThat("left edge has a cutout despite being long edge", |
| hasBound(LEFT, cutout, appBounds), is(false)); |
| } |
| |
| if (!canIgnoreSmallCutout |
| || cutout.getBoundingRectRight().width() > mMaximumSizeForNoLetterbox) { |
| assertThat("right edge has a cutout despite being long edge", |
| hasBound(RIGHT, cutout, appBounds), is(false)); |
| } |
| } |
| if (appBounds.height() < appBounds.width()) { |
| // Landscape display |
| if (!canIgnoreSmallCutout |
| || cutout.getBoundingRectTop().height() > mMaximumSizeForNoLetterbox) { |
| assertThat("top edge has a cutout despite being long edge", |
| hasBound(TOP, cutout, appBounds), is(false)); |
| } |
| |
| if (!canIgnoreSmallCutout |
| || cutout.getBoundingRectBottom().height() > mMaximumSizeForNoLetterbox) { |
| assertThat("bottom edge has a cutout despite being long edge", |
| hasBound(BOTTOM, cutout, appBounds), is(false)); |
| } |
| } |
| } |
| |
| private boolean canLayoutInDisplayCutoutWithoutLetterbox(int cutoutMode) { |
| return cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT |
| || cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; |
| } |
| |
| private boolean hasBound(String position, DisplayCutout cutout, Rect appBound) { |
| final Rect cutoutRect; |
| final int waterfallSize; |
| if (LEFT.equals(position)) { |
| cutoutRect = cutout.getBoundingRectLeft(); |
| waterfallSize = cutout.getWaterfallInsets().left; |
| } else if (TOP.equals(position)) { |
| cutoutRect = cutout.getBoundingRectTop(); |
| waterfallSize = cutout.getWaterfallInsets().top; |
| } else if (RIGHT.equals(position)) { |
| cutoutRect = cutout.getBoundingRectRight(); |
| waterfallSize = cutout.getWaterfallInsets().right; |
| } else { |
| cutoutRect = cutout.getBoundingRectBottom(); |
| waterfallSize = cutout.getWaterfallInsets().bottom; |
| } |
| return Rect.intersects(cutoutRect, appBound) || waterfallSize > 0; |
| } |
| |
| private List<Rect> boundsWith(DisplayCutout cutout, Predicate<Rect> predicate) { |
| return cutout.getBoundingRects().stream().filter(predicate).collect(Collectors.toList()); |
| } |
| |
| private Rect safeInsets(DisplayCutout displayCutout) { |
| return safeInsets(displayCutout, false); |
| } |
| |
| private Rect safeInsets(DisplayCutout displayCutout, boolean canIgnoreSmallCutout) { |
| if (displayCutout == null) { |
| return null; |
| } |
| return new Rect( |
| safeInset(displayCutout.getSafeInsetLeft(), canIgnoreSmallCutout), |
| safeInset(displayCutout.getSafeInsetTop(), canIgnoreSmallCutout), |
| safeInset(displayCutout.getSafeInsetRight(), canIgnoreSmallCutout), |
| safeInset(displayCutout.getSafeInsetBottom(), canIgnoreSmallCutout)); |
| } |
| |
| private int safeInset(int inset, boolean canIgnoreSmallCutout) { |
| return !canIgnoreSmallCutout || inset > mMaximumSizeForNoLetterbox ? inset : 0; |
| } |
| |
| private static Rect systemWindowInsets(WindowInsets insets) { |
| return new Rect(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), |
| insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); |
| } |
| |
| private static Rect stableInsets(WindowInsets insets) { |
| return new Rect(insets.getStableInsetLeft(), insets.getStableInsetTop(), |
| insets.getStableInsetRight(), insets.getStableInsetBottom()); |
| } |
| |
| private Rect getSafeRect(TestActivity a, DisplayCutout cutout) { |
| final Rect safeRect = safeInsets(cutout); |
| safeRect.bottom = getOnMainSync(() -> a.getDecorView().getHeight()) - safeRect.bottom; |
| safeRect.right = getOnMainSync(() -> a.getDecorView().getWidth()) - safeRect.right; |
| return safeRect; |
| } |
| |
| private Rect getAppBounds(TestActivity a) { |
| final Rect appBounds = new Rect(); |
| runOnMainSync(() -> { |
| appBounds.right = a.getDecorView().getWidth(); |
| appBounds.bottom = a.getDecorView().getHeight(); |
| }); |
| return appBounds; |
| } |
| |
| private static Matcher<Rect> insetsLessThanOrEqualTo(Rect max) { |
| return new CustomTypeSafeMatcher<Rect>("must be smaller on each side than " + max) { |
| @Override |
| protected boolean matchesSafely(Rect actual) { |
| return actual.left <= max.left && actual.top <= max.top |
| && actual.right <= max.right && actual.bottom <= max.bottom; |
| } |
| }; |
| } |
| |
| private static Matcher<Rect> intersectsWith(Rect safeRect) { |
| return new CustomTypeSafeMatcher<Rect>("intersects with " + safeRect) { |
| @Override |
| protected boolean matchesSafely(Rect item) { |
| return Rect.intersects(safeRect, item); |
| } |
| }; |
| } |
| |
| private static Matcher<Rect> insetValues(Matcher<Iterable<? super Integer>> valuesMatcher) { |
| return new FeatureMatcher<Rect, Iterable<Integer>>(valuesMatcher, "inset values", |
| "inset values") { |
| @Override |
| protected Iterable<Integer> featureValueOf(Rect actual) { |
| return Arrays.asList(actual.left, actual.top, actual.right, actual.bottom); |
| } |
| }; |
| } |
| |
| private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) { |
| mErrorCollector.checkThat(reason, actual, matcher); |
| } |
| |
| private <R> R getOnMainSync(Supplier<R> f) { |
| final Object[] result = new Object[1]; |
| runOnMainSync(() -> result[0] = f.get()); |
| //noinspection unchecked |
| return (R) result[0]; |
| } |
| |
| private void runOnMainSync(Runnable runnable) { |
| getInstrumentation().runOnMainSync(runnable); |
| } |
| |
| private <T extends TestActivity> T launchAndWait(ActivityTestRule<T> rule, int cutoutMode, |
| int orientation) { |
| final T activity = rule.launchActivity( |
| new Intent().putExtra(EXTRA_CUTOUT_MODE, cutoutMode) |
| .putExtra(EXTRA_ORIENTATION, orientation)); |
| WindowUtil.waitForFocus(activity); |
| final WindowManagerStateHelper wmState = new WindowManagerStateHelper(); |
| wmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY); |
| wmState.waitForDisplayUnfrozen(); |
| return activity; |
| } |
| |
| private boolean supportsOrientation(int orientation) { |
| String systemFeature = ""; |
| switch(orientation) { |
| case SCREEN_ORIENTATION_PORTRAIT: |
| case SCREEN_ORIENTATION_REVERSE_PORTRAIT: |
| systemFeature = PackageManager.FEATURE_SCREEN_PORTRAIT; |
| break; |
| case SCREEN_ORIENTATION_LANDSCAPE: |
| case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: |
| systemFeature = PackageManager.FEATURE_SCREEN_LANDSCAPE; |
| break; |
| default: |
| throw new UnsupportedOperationException("Orientation not supported"); |
| } |
| |
| return getInstrumentation().getTargetContext().getPackageManager() |
| .hasSystemFeature(systemFeature); |
| } |
| |
| public static class TestActivity extends Activity { |
| |
| static final String EXTRA_CUTOUT_MODE = "extra.cutout_mode"; |
| static final String EXTRA_ORIENTATION = "extra.orientation"; |
| private WindowInsets mDispatchedInsets; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| getWindow().requestFeature(Window.FEATURE_NO_TITLE); |
| if (getIntent() != null) { |
| getWindow().getAttributes().layoutInDisplayCutoutMode = getIntent().getIntExtra( |
| EXTRA_CUTOUT_MODE, LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT); |
| setRequestedOrientation(getIntent().getIntExtra( |
| EXTRA_ORIENTATION, SCREEN_ORIENTATION_UNSPECIFIED)); |
| } |
| View view = new View(this); |
| view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); |
| view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets); |
| setContentView(view); |
| } |
| |
| @Override |
| public void onWindowFocusChanged(boolean hasFocus) { |
| if (hasFocus) { |
| getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); |
| } |
| } |
| |
| View getDecorView() { |
| return getWindow().getDecorView(); |
| } |
| |
| WindowInsets getRootInsets() { |
| return getWindow().getDecorView().getRootWindowInsets(); |
| } |
| |
| WindowInsets getDispatchedInsets() { |
| return mDispatchedInsets; |
| } |
| } |
| |
| interface TestDef { |
| void run(TestActivity a, WindowInsets insets, DisplayCutout cutout, Which whichInsets); |
| |
| enum Which { |
| DISPATCHED, ROOT |
| } |
| } |
| } |