1 /* 2 * Copyright (C) 2019 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.insets; 18 19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY; 21 import static android.view.Surface.ROTATION_0; 22 import static android.view.Surface.ROTATION_180; 23 import static android.view.Surface.ROTATION_90; 24 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 25 26 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 27 28 import static org.hamcrest.Matchers.notNullValue; 29 import static org.junit.Assert.assertEquals; 30 import static org.junit.Assert.assertFalse; 31 import static org.junit.Assert.assertTrue; 32 import static org.junit.Assume.assumeFalse; 33 import static org.junit.Assume.assumeTrue; 34 35 import android.app.Activity; 36 import android.content.ComponentName; 37 import android.content.pm.PackageManager; 38 import android.graphics.Insets; 39 import android.os.Bundle; 40 import android.platform.test.annotations.Presubmit; 41 import android.server.wm.ActivityManagerTestBase; 42 import android.server.wm.RotationSession; 43 import android.util.Log; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.view.Window; 47 import android.view.WindowInsets; 48 import android.view.WindowManager.LayoutParams; 49 50 import androidx.test.rule.ActivityTestRule; 51 52 import com.android.compatibility.common.util.WindowUtil; 53 54 import org.hamcrest.CustomTypeSafeMatcher; 55 import org.hamcrest.Matcher; 56 import org.junit.Assert; 57 import org.junit.Before; 58 import org.junit.ClassRule; 59 import org.junit.Rule; 60 import org.junit.Test; 61 import org.junit.rules.ErrorCollector; 62 63 import java.util.function.Supplier; 64 65 @Presubmit 66 @android.server.wm.annotation.Group2 67 public class WindowInsetsPolicyTest extends ActivityManagerTestBase { 68 private static final String TAG = WindowInsetsPolicyTest.class.getSimpleName(); 69 70 private ComponentName mTestActivityComponentName; 71 72 @ClassRule 73 public static DisableImmersiveModeConfirmationRule mDisableImmersiveModeConfirmationRule = 74 new DisableImmersiveModeConfirmationRule(); 75 76 @Rule 77 public final ErrorCollector mErrorCollector = new ErrorCollector(); 78 79 @Rule 80 public final ActivityTestRule<TestActivity> mTestActivity = 81 new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */, 82 false /* launchActivity */); 83 84 @Rule 85 public final ActivityTestRule<FullscreenTestActivity> mFullscreenTestActivity = 86 new ActivityTestRule<>(FullscreenTestActivity.class, false /* initialTouchMode */, 87 false /* launchActivity */); 88 89 @Rule 90 public final ActivityTestRule<FullscreenWmFlagsTestActivity> mFullscreenWmFlagsTestActivity = 91 new ActivityTestRule<>(FullscreenWmFlagsTestActivity.class, 92 false /* initialTouchMode */, false /* launchActivity */); 93 94 @Rule 95 public final ActivityTestRule<ImmersiveFullscreenTestActivity> mImmersiveTestActivity = 96 new ActivityTestRule<>(ImmersiveFullscreenTestActivity.class, 97 false /* initialTouchMode */, false /* launchActivity */); 98 99 @Before 100 @Override setUp()101 public void setUp() throws Exception { 102 super.setUp(); 103 mTestActivityComponentName = new ComponentName(mContext, TestActivity.class); 104 } 105 106 @Test testWindowInsets_dispatched()107 public void testWindowInsets_dispatched() { 108 final TestActivity activity = launchAndWait(mTestActivity); 109 110 WindowInsets insets = getOnMainSync(activity::getDispatchedInsets); 111 Assert.assertThat("test setup failed, no insets dispatched", insets, notNullValue()); 112 113 commonAsserts(insets); 114 } 115 116 @Test testWindowInsets_root()117 public void testWindowInsets_root() { 118 final TestActivity activity = launchAndWait(mTestActivity); 119 120 WindowInsets insets = getOnMainSync(activity::getRootInsets); 121 Assert.assertThat("test setup failed, no insets at root", insets, notNullValue()); 122 123 commonAsserts(insets); 124 } 125 126 /** 127 * Tests whether an activity in split screen gets the top insets force consumed if 128 * {@link View#SYSTEM_UI_FLAG_FULLSCREEN} is set, and doesn't otherwise. 129 */ 130 @Test testForcedConsumedTopInsets()131 public void testForcedConsumedTopInsets() throws Exception { 132 assumeTrue("Skipping test: no split multi-window support", 133 supportsSplitScreenMultiWindow()); 134 135 final TestActivity activity = launchAndWait(mTestActivity); 136 final int rotation = activity.getDisplay().getRotation(); 137 final boolean isPortrait = activity.getResources().getConfiguration() 138 .orientation == ORIENTATION_PORTRAIT; 139 final RotationSession rotationSession = createManagedRotationSession(); 140 if (isPortrait) { 141 // Rotate to landscape. 142 rotationSession.set(rotation == ROTATION_0 || rotation == ROTATION_180 143 ? ROTATION_90 : ROTATION_0); 144 } else { 145 // Keep in landscape. 146 rotationSession.set(rotation); 147 } 148 149 mWmState.waitForValidState(mTestActivityComponentName); 150 final int taskId = mWmState.getTaskByActivity(mTestActivityComponentName).getTaskId(); 151 launchActivityInPrimarySplit(LAUNCHING_ACTIVITY); 152 mTaskOrganizer.putTaskInSplitSecondary(taskId); 153 mWmState.waitForValidState(mTestActivityComponentName); 154 155 // Ensure that top insets are not consumed for LAYOUT_FULLSCREEN 156 WindowInsets insets = getOnMainSync(activity::getDispatchedInsets); 157 final WindowInsets rootInsets = getOnMainSync(activity::getRootInsets); 158 assertEquals("top inset must be dispatched in split screen", 159 rootInsets.getSystemWindowInsetTop(), insets.getSystemWindowInsetTop()); 160 161 // Ensure that top insets are fully consumed for FULLSCREEN 162 final TestActivity fullscreenActivity = launchAndWait(mFullscreenTestActivity); 163 insets = getOnMainSync(fullscreenActivity::getDispatchedInsets); 164 assertEquals("top insets must be consumed if FULLSCREEN is set", 165 0, insets.getSystemWindowInsetTop()); 166 167 // Ensure that top insets are fully consumed for FULLSCREEN when setting it over wm 168 // layout params 169 final TestActivity fullscreenWmFlagsActivity = 170 launchAndWait(mFullscreenWmFlagsTestActivity); 171 insets = getOnMainSync(fullscreenWmFlagsActivity::getDispatchedInsets); 172 assertEquals("top insets must be consumed if FULLSCREEN is set", 173 0, insets.getSystemWindowInsetTop()); 174 } 175 176 @Test testNonAutomotiveFullScreenNotBlockedBySystemComponents()177 public void testNonAutomotiveFullScreenNotBlockedBySystemComponents() { 178 assumeFalse("Skipping test: Automotive is allowed to partially block fullscreen " 179 + "applications with system bars.", isAutomotive()); 180 181 final TestActivity fullscreenActivity = launchAndWait(mFullscreenTestActivity); 182 View decorView = fullscreenActivity.getDecorView(); 183 View contentView = decorView.findViewById(android.R.id.content); 184 boolean hasFullWidth = decorView.getMeasuredWidth() == contentView.getMeasuredWidth(); 185 boolean hasFullHeight = decorView.getMeasuredHeight() == contentView.getMeasuredHeight(); 186 187 assertTrue(hasFullWidth && hasFullHeight); 188 } 189 190 @Test testImmersiveFullscreenHidesSystemBars()191 public void testImmersiveFullscreenHidesSystemBars() throws Throwable { 192 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 193 194 // Run the test twice, because the issue that shows system bars even in the immersive mode, 195 // happens at the 2nd try. 196 for (int i = 1; i <= 2; ++i) { 197 Log.d(TAG, "testImmersiveFullscreenHidesSystemBars: try" + i); 198 199 TestActivity immersiveActivity = launchAndWait(mImmersiveTestActivity); 200 WindowInsets insets = getOnMainSync(immersiveActivity::getDispatchedInsets); 201 202 assertFalse(insets.isVisible(WindowInsets.Type.statusBars())); 203 assertFalse(insets.isVisible(WindowInsets.Type.navigationBars())); 204 205 WindowInsets rootInsets = getOnMainSync(immersiveActivity::getRootInsets); 206 assertFalse(rootInsets.isVisible(WindowInsets.Type.statusBars())); 207 assertFalse(rootInsets.isVisible(WindowInsets.Type.navigationBars())); 208 209 View statusBarBgView = getOnMainSync(immersiveActivity::getStatusBarBackgroundView); 210 // The status bar background view can be non-existent or invisible. 211 assertTrue(statusBarBgView == null 212 || statusBarBgView.getVisibility() == android.view.View.INVISIBLE); 213 214 View navigationBarBgView = getOnMainSync( 215 immersiveActivity::getNavigationBarBackgroundView); 216 // The navigation bar background view can be non-existent or invisible. 217 assertTrue(navigationBarBgView == null 218 || navigationBarBgView.getVisibility() == android.view.View.INVISIBLE); 219 } 220 } 221 commonAsserts(WindowInsets insets)222 private void commonAsserts(WindowInsets insets) { 223 assertForAllInsets("must be non-negative", insets, insetsGreaterThanOrEqualTo(Insets.NONE)); 224 225 assertThat("system gesture insets must include mandatory system gesture insets", 226 insets.getMandatorySystemGestureInsets(), 227 insetsLessThanOrEqualTo(insets.getSystemGestureInsets())); 228 229 Insets stableAndSystem = Insets.min(insets.getSystemWindowInsets(), 230 insets.getStableInsets()); 231 assertThat("mandatory system gesture insets must include intersection between " 232 + "stable and system window insets", 233 stableAndSystem, 234 insetsLessThanOrEqualTo(insets.getMandatorySystemGestureInsets())); 235 236 assertThat("tappable insets must be at most system window insets", 237 insets.getTappableElementInsets(), 238 insetsLessThanOrEqualTo(insets.getSystemWindowInsets())); 239 } 240 assertForAllInsets(String reason, WindowInsets actual, Matcher<? super Insets> matcher)241 private void assertForAllInsets(String reason, WindowInsets actual, 242 Matcher<? super Insets> matcher) { 243 assertThat("getSystemWindowInsets" + ": " + reason, 244 actual.getSystemWindowInsets(), matcher); 245 assertThat("getStableInsets" + ": " + reason, 246 actual.getStableInsets(), matcher); 247 assertThat("getSystemGestureInsets" + ": " + reason, 248 actual.getSystemGestureInsets(), matcher); 249 assertThat("getMandatorySystemGestureInsets" + ": " + reason, 250 actual.getMandatorySystemGestureInsets(), matcher); 251 assertThat("getTappableElementInsets" + ": " + reason, 252 actual.getTappableElementInsets(), matcher); 253 } 254 assertThat(String reason, T actual, Matcher<? super T> matcher)255 private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) { 256 mErrorCollector.checkThat(reason, actual, matcher); 257 } 258 getOnMainSync(Supplier<R> f)259 private <R> R getOnMainSync(Supplier<R> f) { 260 final Object[] result = new Object[1]; 261 runOnMainSync(() -> result[0] = f.get()); 262 //noinspection unchecked 263 return (R) result[0]; 264 } 265 runOnMainSync(Runnable runnable)266 private void runOnMainSync(Runnable runnable) { 267 getInstrumentation().runOnMainSync(runnable); 268 } 269 launchAndWait(ActivityTestRule<T> rule)270 private <T extends Activity> T launchAndWait(ActivityTestRule<T> rule) { 271 final T activity = rule.launchActivity(null); 272 WindowUtil.waitForFocus(activity); 273 return activity; 274 } 275 isAutomotive()276 private boolean isAutomotive() { 277 return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 278 } 279 insetsLessThanOrEqualTo(Insets max)280 private static Matcher<Insets> insetsLessThanOrEqualTo(Insets max) { 281 return new CustomTypeSafeMatcher<Insets>("must be smaller on each side than " + max) { 282 @Override 283 protected boolean matchesSafely(Insets actual) { 284 return actual.left <= max.left && actual.top <= max.top 285 && actual.right <= max.right && actual.bottom <= max.bottom; 286 } 287 }; 288 } 289 290 private static Matcher<Insets> insetsGreaterThanOrEqualTo(Insets min) { 291 return new CustomTypeSafeMatcher<Insets>("must be greater on each side than " + min) { 292 @Override 293 protected boolean matchesSafely(Insets actual) { 294 return actual.left >= min.left && actual.top >= min.top 295 && actual.right >= min.right && actual.bottom >= min.bottom; 296 } 297 }; 298 } 299 300 public static class TestActivity extends Activity { 301 302 private WindowInsets mDispatchedInsets; 303 304 @Override 305 protected void onCreate(Bundle savedInstanceState) { 306 super.onCreate(savedInstanceState); 307 getWindow().requestFeature(Window.FEATURE_NO_TITLE); 308 View view = new View(this); 309 view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 310 getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 311 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 312 view.setOnApplyWindowInsetsListener((v, insets) -> mDispatchedInsets = insets); 313 setContentView(view); 314 } 315 316 View getDecorView() { 317 return getWindow().getDecorView(); 318 } 319 320 View getStatusBarBackgroundView() { 321 return getWindow().getStatusBarBackgroundView(); 322 } 323 324 View getNavigationBarBackgroundView() { 325 return getWindow().getNavigationBarBackgroundView(); 326 } 327 328 WindowInsets getRootInsets() { 329 return getWindow().getDecorView().getRootWindowInsets(); 330 } 331 332 WindowInsets getDispatchedInsets() { 333 return mDispatchedInsets; 334 } 335 } 336 337 public static class FullscreenTestActivity extends TestActivity { 338 339 @Override 340 protected void onCreate(Bundle savedInstanceState) { 341 super.onCreate(savedInstanceState); 342 getDecorView().setSystemUiVisibility( 343 getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_FULLSCREEN); 344 } 345 } 346 347 public static class FullscreenWmFlagsTestActivity extends TestActivity { 348 349 @Override 350 protected void onCreate(Bundle savedInstanceState) { 351 super.onCreate(savedInstanceState); 352 getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); 353 } 354 } 355 356 public static class ImmersiveFullscreenTestActivity extends TestActivity { 357 358 @Override 359 protected void onCreate(Bundle savedInstanceState) { 360 super.onCreate(savedInstanceState); 361 // See https://developer.android.com/training/system-ui/immersive#EnableFullscreen 362 getDecorView().setSystemUiVisibility( 363 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 364 // Set the content to appear under the system bars so that the 365 // content doesn't resize when the system bars hide and show. 366 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 367 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 368 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 369 // Hide the nav bar and status bar 370 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 371 | View.SYSTEM_UI_FLAG_FULLSCREEN); 372 } 373 } 374 375 public static class NaturalOrientationTestActivity extends TestActivity { 376 } 377 } 378