Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 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; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 24 | import static android.server.wm.DisplayCutoutTests.TestActivity.EXTRA_CUTOUT_MODE; |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 25 | import static android.server.wm.DisplayCutoutTests.TestActivity.EXTRA_ORIENTATION; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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; |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 29 | import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | |
Riddle Hsu | 420a61e | 2019-11-07 02:01:16 +0800 | [diff] [blame] | 34 | import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; |
Brett Chabot | 2ca72e9 | 2019-02-15 11:21:51 -0800 | [diff] [blame] | 35 | |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 36 | import static org.hamcrest.Matchers.equalTo; |
Tadashi G. Takaoka | f280a9b | 2019-02-01 15:28:04 +0900 | [diff] [blame] | 37 | import static org.hamcrest.Matchers.everyItem; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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; |
Louis Chang | 948a5b4 | 2019-04-17 16:22:21 +0800 | [diff] [blame] | 47 | import static org.junit.Assert.assertEquals; |
| 48 | import static org.junit.Assert.assertTrue; |
JianYang Liu | be952de | 2020-05-15 16:14:58 -0700 | [diff] [blame] | 49 | import static org.junit.Assume.assumeTrue; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 50 | |
| 51 | import android.app.Activity; |
| 52 | import android.content.Intent; |
JianYang Liu | be952de | 2020-05-15 16:14:58 -0700 | [diff] [blame] | 53 | import android.content.pm.PackageManager; |
Louis Chang | 948a5b4 | 2019-04-17 16:22:21 +0800 | [diff] [blame] | 54 | import android.graphics.Insets; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 55 | import android.graphics.Point; |
| 56 | import android.graphics.Rect; |
| 57 | import android.os.Bundle; |
| 58 | import android.platform.test.annotations.Presubmit; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 59 | import android.view.DisplayCutout; |
| 60 | import android.view.View; |
| 61 | import android.view.ViewGroup; |
Adrian Roos | cf34888 | 2018-05-14 15:10:37 +0200 | [diff] [blame] | 62 | import android.view.Window; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 63 | import android.view.WindowInsets; |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 64 | import android.view.WindowInsets.Type; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 65 | |
Brett Chabot | 2ca72e9 | 2019-02-15 11:21:51 -0800 | [diff] [blame] | 66 | import androidx.test.rule.ActivityTestRule; |
| 67 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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; |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 77 | import org.junit.runner.RunWith; |
| 78 | import org.junit.runners.Parameterized; |
| 79 | import org.junit.runners.Parameterized.Parameter; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | */ |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 91 | @Presubmit |
Riddle Hsu | 7903052 | 2019-11-04 16:19:53 +0800 | [diff] [blame] | 92 | @android.server.wm.annotation.Group3 |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 93 | @RunWith(Parameterized.class) |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 94 | public class DisplayCutoutTests { |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 95 | static final String LEFT = "left"; |
| 96 | static final String TOP = "top"; |
| 97 | static final String RIGHT = "right"; |
| 98 | static final String BOTTOM = "bottom"; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 99 | |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 100 | @Parameterized.Parameters(name= "{1}({0})") |
| 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 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 |
Louis Chang | 948a5b4 | 2019-04-17 16:22:21 +0800 | [diff] [blame] | 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())); |
shawnlin | a8dc087 | 2020-01-17 15:11:19 +0800 | [diff] [blame] | 144 | |
| 145 | assertEquals(Insets.NONE, displayCutout.getWaterfallInsets()); |
| 146 | } |
| 147 | |
| 148 | @Test |
| 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()); |
Louis Chang | 948a5b4 | 2019-04-17 16:22:21 +0800 | [diff] [blame] | 172 | } |
| 173 | |
| 174 | @Test |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 175 | public void testDisplayCutout_default() { |
| 176 | runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, |
| 177 | (activity, insets, displayCutout, which) -> { |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 192 | public void testDisplayCutout_shortEdges() { |
| 193 | runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, (a, insets, cutout, which) -> { |
Adrian Roos | 1fd97b11 | 2019-01-28 16:31:04 +0100 | [diff] [blame] | 194 | if (which == ROOT) { |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 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)); |
Adrian Roos | 1fd97b11 | 2019-01-28 16:31:04 +0100 | [diff] [blame] | 211 | } |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 212 | }); |
| 213 | } |
| 214 | |
| 215 | @Test |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 216 | public void testDisplayCutout_never() { |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 222 | @Test |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 223 | public void testDisplayCutout_always() { |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 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 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 232 | private void runTest(int cutoutMode, TestDef test) { |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 233 | runTest(cutoutMode, test, orientation); |
| 234 | } |
| 235 | |
| 236 | private void runTest(int cutoutMode, TestDef test, int orientation) { |
JianYang Liu | be952de | 2020-05-15 16:14:58 -0700 | [diff] [blame] | 237 | assumeTrue("Skipping test: orientation not supported", supportsOrientation(orientation)); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 238 | final TestActivity activity = launchAndWait(mDisplayCutoutActivity, |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 239 | cutoutMode, orientation); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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(); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 249 | if (displayCutout != null) { |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 250 | commonAsserts(activity, displayCutout); |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 251 | if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) { |
| 252 | shortEdgeAsserts(activity, insets, displayCutout); |
| 253 | } |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 254 | assertCutoutsAreConsistentWithInsets(activity, displayCutout); |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 255 | assertSafeInsetsAreConsistentWithDisplayCutoutInsets(insets); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 256 | } |
| 257 | test.run(activity, insets, displayCutout, ROOT); |
| 258 | |
| 259 | if (dispatchedDisplayCutout != null) { |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 260 | commonAsserts(activity, dispatchedDisplayCutout); |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 261 | if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) { |
| 262 | shortEdgeAsserts(activity, insets, displayCutout); |
| 263 | } |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 264 | assertCutoutsAreConsistentWithInsets(activity, dispatchedDisplayCutout); |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 265 | if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) { |
| 266 | assertSafeInsetsAreConsistentWithDisplayCutoutInsets(dispatchedInsets); |
| 267 | } |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 268 | } |
| 269 | test.run(activity, dispatchedInsets, dispatchedDisplayCutout, DISPATCHED); |
| 270 | } |
| 271 | |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 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 | |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 283 | private void commonAsserts(TestActivity activity, DisplayCutout cutout) { |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 284 | assertSafeInsetsValid(cutout); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 285 | assertCutoutsAreWithinSafeInsets(activity, cutout); |
| 286 | assertBoundsAreNonEmpty(cutout); |
| 287 | assertAtMostOneCutoutPerEdge(activity, cutout); |
| 288 | } |
| 289 | |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 290 | private void shortEdgeAsserts( |
| 291 | TestActivity activity, WindowInsets insets, DisplayCutout cutout) { |
| 292 | assertOnlyShortEdgeHasInsets(activity, cutout); |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 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))); |
shawnlin | fe746d8 | 2020-01-17 17:55:58 +0800 | [diff] [blame] | 297 | } |
| 298 | |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 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)); |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 304 | } else { |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 305 | assertThat("cutout must have no bound on the " + position, |
| 306 | hasBound(position, cutout, appBound), is(false)); |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 307 | } |
| 308 | } |
| 309 | |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 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); |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 316 | } |
| 317 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | |
| 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 | |
| 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 | |
| 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 | |
| 357 | private void assertOnlyShortEdgeHasInsets(TestActivity activity, |
| 358 | DisplayCutout displayCutout) { |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 359 | final Rect appBounds = getAppBounds(activity); |
| 360 | if (appBounds.height() > appBounds.width()) { |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 361 | // Portrait display |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | } |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 367 | if (appBounds.height() < appBounds.width()) { |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 368 | // Landscape display |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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 | |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 376 | private void assertOnlyShortEdgeHasBounds(TestActivity activity, DisplayCutout cutout) { |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 377 | final Rect appBounds = getAppBounds(activity); |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 378 | if (appBounds.height() > appBounds.width()) { |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 379 | // Portrait display |
| 380 | assertThat("left edge has a cutout despite being long edge", |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 381 | hasBound(LEFT, cutout, appBounds), is(false)); |
| 382 | |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 383 | assertThat("right edge has a cutout despite being long edge", |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 384 | hasBound(RIGHT, cutout, appBounds), is(false)); |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 385 | } |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 386 | if (appBounds.height() < appBounds.width()) { |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 387 | // Landscape display |
| 388 | assertThat("top edge has a cutout despite being long edge", |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 389 | hasBound(TOP, cutout, appBounds), is(false)); |
| 390 | |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 391 | assertThat("bottom edge has a cutout despite being long edge", |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 392 | hasBound(BOTTOM, cutout, appBounds), is(false)); |
Issei Suzuki | 770b5e6 | 2018-10-03 14:02:01 +0900 | [diff] [blame] | 393 | } |
| 394 | } |
| 395 | |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 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 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 415 | private List<Rect> boundsWith(DisplayCutout cutout, Predicate<Rect> predicate) { |
| 416 | return cutout.getBoundingRects().stream().filter(predicate).collect(Collectors.toList()); |
| 417 | } |
| 418 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 419 | private static Rect safeInsets(DisplayCutout displayCutout) { |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 420 | if (displayCutout == null) { |
| 421 | return null; |
| 422 | } |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 423 | return new Rect(displayCutout.getSafeInsetLeft(), displayCutout.getSafeInsetTop(), |
| 424 | displayCutout.getSafeInsetRight(), displayCutout.getSafeInsetBottom()); |
| 425 | } |
| 426 | |
| 427 | private static Rect systemWindowInsets(WindowInsets insets) { |
| 428 | return new Rect(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), |
| 429 | insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); |
| 430 | } |
| 431 | |
| 432 | private static Rect stableInsets(WindowInsets insets) { |
| 433 | return new Rect(insets.getStableInsetLeft(), insets.getStableInsetTop(), |
| 434 | insets.getStableInsetRight(), insets.getStableInsetBottom()); |
| 435 | } |
| 436 | |
| 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 | |
shawnlin | 6ffa191 | 2020-02-22 15:13:32 +0800 | [diff] [blame] | 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 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 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) { |
Tadashi G. Takaoka | f280a9b | 2019-02-01 15:28:04 +0900 | [diff] [blame] | 494 | getInstrumentation().runOnMainSync(runnable); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 495 | } |
| 496 | |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 497 | private <T extends TestActivity> T launchAndWait(ActivityTestRule<T> rule, int cutoutMode, |
| 498 | int orientation) { |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 499 | final T activity = rule.launchActivity( |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 500 | new Intent().putExtra(EXTRA_CUTOUT_MODE, cutoutMode) |
| 501 | .putExtra(EXTRA_ORIENTATION, orientation)); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 502 | PollingCheck.waitFor(activity::hasWindowFocus); |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 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 | }); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 511 | return activity; |
| 512 | } |
| 513 | |
JianYang Liu | be952de | 2020-05-15 16:14:58 -0700 | [diff] [blame] | 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 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 533 | public static class TestActivity extends Activity { |
| 534 | |
| 535 | static final String EXTRA_CUTOUT_MODE = "extra.cutout_mode"; |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 536 | static final String EXTRA_ORIENTATION = "extra.orientation"; |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 537 | private WindowInsets mDispatchedInsets; |
| 538 | |
| 539 | @Override |
| 540 | protected void onCreate(Bundle savedInstanceState) { |
| 541 | super.onCreate(savedInstanceState); |
Adrian Roos | cf34888 | 2018-05-14 15:10:37 +0200 | [diff] [blame] | 542 | getWindow().requestFeature(Window.FEATURE_NO_TITLE); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 543 | if (getIntent() != null) { |
| 544 | getWindow().getAttributes().layoutInDisplayCutoutMode = getIntent().getIntExtra( |
| 545 | EXTRA_CUTOUT_MODE, LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT); |
Adrian Roos | 39bcb6e | 2020-04-14 11:53:48 +0200 | [diff] [blame] | 546 | setRequestedOrientation(getIntent().getIntExtra( |
| 547 | EXTRA_ORIENTATION, SCREEN_ORIENTATION_UNSPECIFIED)); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 548 | } |
| 549 | View view = new View(this); |
| 550 | view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 551 | view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets); |
| 552 | setContentView(view); |
| 553 | } |
| 554 | |
Jeff Chang | 844bd81 | 2018-12-05 14:13:08 +0800 | [diff] [blame] | 555 | @Override |
| 556 | public void onWindowFocusChanged(boolean hasFocus) { |
| 557 | if (hasFocus) { |
| 558 | getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
shawnlin | 648195c | 2020-10-14 15:06:42 +0800 | [diff] [blame] | 559 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| 560 | | View.SYSTEM_UI_FLAG_FULLSCREEN |
| 561 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); |
Jeff Chang | 844bd81 | 2018-12-05 14:13:08 +0800 | [diff] [blame] | 562 | } |
| 563 | } |
| 564 | |
Adrian Roos | 26860fa | 2018-03-15 19:07:21 +0100 | [diff] [blame] | 565 | View getDecorView() { |
| 566 | return getWindow().getDecorView(); |
| 567 | } |
| 568 | |
| 569 | WindowInsets getRootInsets() { |
| 570 | return getWindow().getDecorView().getRootWindowInsets(); |
| 571 | } |
| 572 | |
| 573 | WindowInsets getDispatchedInsets() { |
| 574 | return mDispatchedInsets; |
| 575 | } |
| 576 | } |
| 577 | |
| 578 | interface TestDef { |
| 579 | void run(TestActivity a, WindowInsets insets, DisplayCutout cutout, Which whichInsets); |
| 580 | |
| 581 | enum Which { |
| 582 | DISPATCHED, ROOT |
| 583 | } |
| 584 | } |
| 585 | } |