blob: 832cec5a86f629e67018289065791a848dad67b5 [file] [log] [blame]
/*
* 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.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.view.Display.DEFAULT_DISPLAY;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.util.DisplayMetrics.DENSITY_DEFAULT;
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.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.platform.test.annotations.Presubmit;
import android.view.Display;
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.PollingCheck;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.Before;
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 {
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;
@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 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));
PollingCheck.waitFor(activity::hasWindowFocus);
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
}
}
}