Home | History | Annotate | Download | only in wm
      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