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