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