1 /* 2 * Copyright (C) 2020 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.graphics.PixelFormat.TRANSLUCENT; 20 import static android.server.wm.ActivityManagerTestBase.isTablet; 21 import static android.view.KeyEvent.ACTION_DOWN; 22 import static android.view.KeyEvent.KEYCODE_BACK; 23 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; 24 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; 25 import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE; 26 import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE; 27 import static android.view.WindowInsets.Type.ime; 28 import static android.view.WindowInsets.Type.navigationBars; 29 import static android.view.WindowInsets.Type.statusBars; 30 import static android.view.WindowInsets.Type.systemBars; 31 import static android.view.WindowInsets.Type.systemGestures; 32 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; 33 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; 34 import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT; 35 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; 36 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 37 import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; 38 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 39 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 40 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; 41 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 42 43 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 44 45 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 46 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 47 48 import static org.hamcrest.Matchers.is; 49 import static org.hamcrest.Matchers.notNullValue; 50 import static org.hamcrest.Matchers.nullValue; 51 import static org.junit.Assert.assertEquals; 52 import static org.junit.Assert.assertFalse; 53 import static org.junit.Assert.assertTrue; 54 import static org.junit.Assume.assumeFalse; 55 import static org.junit.Assume.assumeThat; 56 import static org.junit.Assume.assumeTrue; 57 58 import android.app.Activity; 59 import android.app.AlertDialog; 60 import android.app.Instrumentation; 61 import android.content.Context; 62 import android.content.pm.PackageManager; 63 import android.content.res.Resources; 64 import android.os.Bundle; 65 import android.os.SystemClock; 66 import android.platform.test.annotations.Presubmit; 67 import android.view.InputDevice; 68 import android.view.MotionEvent; 69 import android.view.View; 70 import android.view.ViewGroup; 71 import android.view.Window; 72 import android.view.WindowInsets; 73 import android.view.WindowInsetsAnimation; 74 import android.view.WindowInsetsController; 75 import android.view.WindowManager; 76 import android.widget.EditText; 77 import android.widget.LinearLayout; 78 import android.widget.TextView; 79 80 import androidx.annotation.Nullable; 81 82 import com.android.compatibility.common.util.PollingCheck; 83 import com.android.cts.mockime.ImeEventStream; 84 import com.android.cts.mockime.ImeSettings; 85 import com.android.cts.mockime.MockImeSession; 86 87 import org.junit.Rule; 88 import org.junit.Test; 89 import org.junit.rules.ErrorCollector; 90 91 import java.util.ArrayList; 92 import java.util.List; 93 import java.util.concurrent.CountDownLatch; 94 import java.util.concurrent.TimeUnit; 95 import java.util.function.Supplier; 96 97 /** 98 * Test whether WindowInsetsController controls window insets as expected. 99 * 100 * Build/Install/Run: 101 * atest CtsWindowManagerDeviceTestCases:WindowInsetsControllerTests 102 */ 103 @Presubmit 104 public class WindowInsetsControllerTests extends WindowManagerTestBase { 105 106 private final static long TIMEOUT = 1000; // milliseconds 107 private final static long TIMEOUT_UPDATING_INPUT_WINDOW = 500; // milliseconds 108 private final static long TIME_SLICE = 50; // milliseconds 109 private final static AnimationCallback ANIMATION_CALLBACK = new AnimationCallback(); 110 111 private static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOGS = 112 "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS"; 113 114 @Rule 115 public final ErrorCollector mErrorCollector = new ErrorCollector(); 116 117 @Test testHide()118 public void testHide() { 119 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 120 121 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 122 final View rootView = activity.getWindow().getDecorView(); 123 124 testHideInternal(rootView, statusBars()); 125 testHideInternal(rootView, navigationBars()); 126 } 127 testHideInternal(View rootView, int types)128 private void testHideInternal(View rootView, int types) { 129 if (rootView.getRootWindowInsets().isVisible(types)) { 130 getInstrumentation().runOnMainSync(() -> { 131 rootView.getWindowInsetsController().hide(types); 132 }); 133 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 134 } 135 } 136 137 @Test testShow()138 public void testShow() { 139 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 140 141 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 142 final View rootView = activity.getWindow().getDecorView(); 143 144 testShowInternal(rootView, statusBars()); 145 testShowInternal(rootView, navigationBars()); 146 } 147 testShowInternal(View rootView, int types)148 private void testShowInternal(View rootView, int types) { 149 if (rootView.getRootWindowInsets().isVisible(types)) { 150 getInstrumentation().runOnMainSync(() -> { 151 rootView.getWindowInsetsController().hide(types); 152 }); 153 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 154 getInstrumentation().runOnMainSync(() -> { 155 rootView.getWindowInsetsController().show(types); 156 }); 157 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)); 158 } 159 } 160 testTopAppHidesStatusBarInternal(Activity activity, View rootView, Runnable hidingStatusBar)161 private void testTopAppHidesStatusBarInternal(Activity activity, View rootView, 162 Runnable hidingStatusBar) { 163 if (rootView.getRootWindowInsets().isVisible(statusBars())) { 164 165 // The top-fullscreen-app window hides status bar. 166 getInstrumentation().runOnMainSync(hidingStatusBar); 167 PollingCheck.waitFor(TIMEOUT, 168 () -> !rootView.getRootWindowInsets().isVisible(statusBars())); 169 170 // Add a non-fullscreen window on top of the fullscreen window. 171 // The new focused window doesn't hide status bar. 172 getInstrumentation().runOnMainSync( 173 () -> activity.getWindowManager().addView( 174 new View(activity), 175 new WindowManager.LayoutParams(1 /* w */, 1 /* h */, TYPE_APPLICATION, 176 0 /* flags */, TRANSLUCENT))); 177 178 // Check if status bar stays invisible. 179 for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) { 180 assertFalse(rootView.getRootWindowInsets().isVisible(statusBars())); 181 SystemClock.sleep(TIME_SLICE); 182 } 183 } 184 } 185 186 @Test testTopAppHidesStatusBarByMethod()187 public void testTopAppHidesStatusBarByMethod() { 188 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 189 190 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 191 final View rootView = activity.getWindow().getDecorView(); 192 193 testTopAppHidesStatusBarInternal(activity, rootView, 194 () -> rootView.getWindowInsetsController().hide(statusBars())); 195 } 196 197 @Test testTopAppHidesStatusBarByWindowFlag()198 public void testTopAppHidesStatusBarByWindowFlag() { 199 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 200 201 final TestActivity activity = startActivity(TestActivity.class); 202 final View rootView = activity.getWindow().getDecorView(); 203 204 testTopAppHidesStatusBarInternal(activity, rootView, 205 () -> activity.getWindow().addFlags(FLAG_FULLSCREEN)); 206 } 207 208 @Test testTopAppHidesStatusBarBySystemUiFlag()209 public void testTopAppHidesStatusBarBySystemUiFlag() { 210 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 211 212 final TestActivity activity = startActivity(TestActivity.class); 213 final View rootView = activity.getWindow().getDecorView(); 214 215 testTopAppHidesStatusBarInternal(activity, rootView, 216 () -> rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN)); 217 } 218 219 @Test testImeShowAndHide()220 public void testImeShowAndHide() throws Exception { 221 final Instrumentation instrumentation = getInstrumentation(); 222 assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()), 223 nullValue()); 224 final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this); 225 final ImeEventStream stream = imeSession.openEventStream(); 226 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 227 expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT); 228 229 final View rootView = activity.getWindow().getDecorView(); 230 getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().show(ime())); 231 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime())); 232 getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().hide(ime())); 233 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(ime())); 234 } 235 236 @Test testImeForceShowingNavigationBar()237 public void testImeForceShowingNavigationBar() throws Exception { 238 final Instrumentation instrumentation = getInstrumentation(); 239 assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()), 240 nullValue()); 241 final Resources resources = instrumentation.getContext().getResources(); 242 final boolean isHideNavBarForKeyboardEnabled = resources.getBoolean( 243 resources.getIdentifier("config_hideNavBarForKeyboard", "bool", "android")); 244 assumeFalse("Device is configured to not show navigation bar for keyboard", 245 isHideNavBarForKeyboardEnabled); 246 final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this); 247 final ImeEventStream stream = imeSession.openEventStream(); 248 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 249 expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT); 250 251 final View rootView = activity.getWindow().getDecorView(); 252 assumeTrue(rootView.getRootWindowInsets().isVisible(navigationBars())); 253 getInstrumentation().runOnMainSync( 254 () -> rootView.getWindowInsetsController().hide(navigationBars())); 255 PollingCheck.waitFor(TIMEOUT, 256 () -> !rootView.getRootWindowInsets().isVisible(navigationBars())); 257 getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().show(ime())); 258 PollingCheck.waitFor(TIMEOUT, 259 () -> rootView.getRootWindowInsets().isVisible(ime() | navigationBars())); 260 getInstrumentation().runOnMainSync(() -> rootView.getWindowInsetsController().hide(ime())); 261 PollingCheck.waitFor(TIMEOUT, 262 () -> !rootView.getRootWindowInsets().isVisible(ime()) 263 && !rootView.getRootWindowInsets().isVisible(navigationBars())); 264 } 265 266 @Test testSetSystemBarsAppearance()267 public void testSetSystemBarsAppearance() { 268 final TestActivity activity = startActivity(TestActivity.class); 269 final View rootView = activity.getWindow().getDecorView(); 270 final WindowInsetsController controller = rootView.getWindowInsetsController(); 271 getInstrumentation().runOnMainSync(() -> { 272 // Set APPEARANCE_LIGHT_STATUS_BARS. 273 controller.setSystemBarsAppearance( 274 APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS); 275 276 // Clear APPEARANCE_LIGHT_NAVIGATION_BARS. 277 controller.setSystemBarsAppearance( 278 0 /* appearance */, APPEARANCE_LIGHT_NAVIGATION_BARS); 279 }); 280 waitForIdle(); 281 282 // We must have APPEARANCE_LIGHT_STATUS_BARS, but not APPEARANCE_LIGHT_NAVIGATION_BARS. 283 assertEquals(APPEARANCE_LIGHT_STATUS_BARS, 284 controller.getSystemBarsAppearance() 285 & (APPEARANCE_LIGHT_STATUS_BARS | APPEARANCE_LIGHT_NAVIGATION_BARS)); 286 287 final boolean[] onPreDrawCalled = { false }; 288 rootView.getViewTreeObserver().addOnPreDrawListener(() -> { 289 onPreDrawCalled[0] = true; 290 return true; 291 }); 292 293 // Clear APPEARANCE_LIGHT_NAVIGATION_BARS again. 294 getInstrumentation().runOnMainSync(() -> controller.setSystemBarsAppearance( 295 0 /* appearance */, APPEARANCE_LIGHT_NAVIGATION_BARS)); 296 waitForIdle(); 297 298 assertFalse("Setting the same appearance must not cause a new traversal", 299 onPreDrawCalled[0]); 300 } 301 302 @Test testSetSystemBarsBehavior_default()303 public void testSetSystemBarsBehavior_default() throws InterruptedException { 304 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 305 306 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 307 final View rootView = activity.getWindow().getDecorView(); 308 309 // Assume we have the bars and they can be visible. 310 final int types = statusBars(); 311 assumeTrue(rootView.getRootWindowInsets().isVisible(types)); 312 313 rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT); 314 315 hideInsets(rootView, types); 316 317 // Tapping on display cannot show bars. 318 tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f); 319 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 320 321 // Wait for status bar invisible from InputDispatcher. Otherwise, the following 322 // dragFromTopToCenter might expand notification shade. 323 SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW); 324 325 // Swiping from top of display can show bars. 326 dragFromTopToCenter(rootView); 327 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)); 328 } 329 330 @Test testSetSystemBarsBehavior_showTransientBarsBySwipe()331 public void testSetSystemBarsBehavior_showTransientBarsBySwipe() throws InterruptedException { 332 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 333 334 final TestActivity activity = startActivity(TestActivity.class); 335 final View rootView = activity.getWindow().getDecorView(); 336 337 // Assume we have the bars and they can be visible. 338 final int types = statusBars(); 339 assumeTrue(rootView.getRootWindowInsets().isVisible(types)); 340 341 rootView.getWindowInsetsController().setSystemBarsBehavior( 342 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 343 344 hideInsets(rootView, types); 345 346 // Tapping on display cannot show bars. 347 tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f); 348 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 349 350 // Wait for status bar invisible from InputDispatcher. Otherwise, the following 351 // dragFromTopToCenter might expand notification shade. 352 SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW); 353 354 // Swiping from top of display can show transient bars, but apps cannot detect that. 355 dragFromTopToCenter(rootView); 356 // Make sure status bar stays invisible. 357 for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) { 358 assertFalse(rootView.getRootWindowInsets().isVisible(types)); 359 SystemClock.sleep(TIME_SLICE); 360 } 361 } 362 363 @Test testSetSystemBarsBehavior_systemGesture_default()364 public void testSetSystemBarsBehavior_systemGesture_default() throws InterruptedException { 365 final TestActivity activity = startActivity(TestActivity.class); 366 final View rootView = activity.getWindow().getDecorView(); 367 368 // Assume the current navigation mode has the back gesture. 369 assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0); 370 assumeTrue(canTriggerBackGesture(rootView)); 371 372 rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT); 373 hideInsets(rootView, systemBars()); 374 375 // Test if the back gesture can be triggered while system bars are hidden with the behavior. 376 assertTrue(canTriggerBackGesture(rootView)); 377 } 378 379 @Test testSetSystemBarsBehavior_systemGesture_showTransientBarsBySwipe()380 public void testSetSystemBarsBehavior_systemGesture_showTransientBarsBySwipe() 381 throws InterruptedException { 382 final TestActivity activity = startActivity(TestActivity.class); 383 final View rootView = activity.getWindow().getDecorView(); 384 385 // Assume the current navigation mode has the back gesture. 386 assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0); 387 assumeTrue(canTriggerBackGesture(rootView)); 388 389 rootView.getWindowInsetsController().setSystemBarsBehavior( 390 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 391 hideInsets(rootView, systemBars()); 392 393 // Test if the back gesture can be triggered while system bars are hidden with the behavior. 394 assertFalse(canTriggerBackGesture(rootView)); 395 } 396 canTriggerBackGesture(View rootView)397 private boolean canTriggerBackGesture(View rootView) throws InterruptedException { 398 final boolean[] hasBack = { false }; 399 final CountDownLatch latch = new CountDownLatch(1); 400 rootView.findFocus().setOnKeyListener((v, keyCode, event) -> { 401 if (keyCode == KEYCODE_BACK && event.getAction() == ACTION_DOWN) { 402 hasBack[0] = true; 403 latch.countDown(); 404 return true; 405 } 406 return false; 407 }); 408 dragFromLeftToCenter(rootView); 409 latch.await(1, TimeUnit.SECONDS); 410 return hasBack[0]; 411 } 412 413 @Test testSystemUiVisibilityCallbackCausedByInsets()414 public void testSystemUiVisibilityCallbackCausedByInsets() { 415 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 416 417 final TestActivity activity = startActivity(TestActivity.class); 418 final View controlTarget = activity.getWindow().getDecorView(); 419 final int[] targetSysUiVis = new int[1]; 420 final View nonControlTarget = new View(mTargetContext); 421 final int[] nonTargetSysUiVis = new int[1]; 422 if (isTablet()) { 423 return; 424 } 425 final WindowManager.LayoutParams nonTargetAttrs = 426 new WindowManager.LayoutParams(TYPE_APPLICATION); 427 nonTargetAttrs.flags = FLAG_NOT_FOCUSABLE; 428 getInstrumentation().runOnMainSync(() -> { 429 controlTarget.setOnSystemUiVisibilityChangeListener( 430 visibility -> targetSysUiVis[0] = visibility); 431 nonControlTarget.setOnSystemUiVisibilityChangeListener( 432 visibility -> nonTargetSysUiVis[0] = visibility); 433 activity.getWindowManager().addView(nonControlTarget, nonTargetAttrs); 434 }); 435 waitForIdle(); 436 testSysUiVisCallbackCausedByInsets(statusBars(), SYSTEM_UI_FLAG_FULLSCREEN, 437 controlTarget, targetSysUiVis, nonTargetSysUiVis); 438 testSysUiVisCallbackCausedByInsets(navigationBars(), SYSTEM_UI_FLAG_HIDE_NAVIGATION, 439 controlTarget, targetSysUiVis, nonTargetSysUiVis); 440 } 441 testSysUiVisCallbackCausedByInsets(int insetsType, int sysUiFlag, View target, int[] targetSysUiVis, int[] nonTargetSysUiVis)442 private void testSysUiVisCallbackCausedByInsets(int insetsType, int sysUiFlag, View target, 443 int[] targetSysUiVis, int[] nonTargetSysUiVis) { 444 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 445 if (target.getRootWindowInsets().isVisible(insetsType)) { 446 447 // Controlled by methods 448 getInstrumentation().runOnMainSync( 449 () -> target.getWindowInsetsController().hide(insetsType)); 450 PollingCheck.waitFor(TIMEOUT, () -> 451 targetSysUiVis[0] == sysUiFlag && targetSysUiVis[0] == nonTargetSysUiVis[0]); 452 getInstrumentation().runOnMainSync( 453 () -> target.getWindowInsetsController().show(insetsType)); 454 PollingCheck.waitFor(TIMEOUT, () -> 455 targetSysUiVis[0] == 0 && targetSysUiVis[0] == nonTargetSysUiVis[0]); 456 457 // Controlled by legacy flags 458 getInstrumentation().runOnMainSync( 459 () -> target.setSystemUiVisibility(sysUiFlag)); 460 PollingCheck.waitFor(TIMEOUT, () -> 461 targetSysUiVis[0] == sysUiFlag && targetSysUiVis[0] == nonTargetSysUiVis[0]); 462 getInstrumentation().runOnMainSync( 463 () -> target.setSystemUiVisibility(0)); 464 PollingCheck.waitFor(TIMEOUT, () -> 465 targetSysUiVis[0] == 0 && targetSysUiVis[0] == nonTargetSysUiVis[0]); 466 } 467 } 468 469 @Test testSystemUiVisibilityCallbackCausedByAppearance()470 public void testSystemUiVisibilityCallbackCausedByAppearance() { 471 final TestActivity activity = startActivity(TestActivity.class); 472 final View controlTarget = activity.getWindow().getDecorView(); 473 474 // Assume we have at least one visible system bar. 475 assumeTrue(controlTarget.getRootWindowInsets().isVisible(statusBars()) || 476 controlTarget.getRootWindowInsets().isVisible(navigationBars())); 477 478 final int[] targetSysUiVis = new int[1]; 479 getInstrumentation().runOnMainSync(() -> { 480 controlTarget.setOnSystemUiVisibilityChangeListener( 481 visibility -> targetSysUiVis[0] = visibility); 482 }); 483 waitForIdle(); 484 final int sysUiFlag = SYSTEM_UI_FLAG_LOW_PROFILE; 485 getInstrumentation().runOnMainSync(() -> controlTarget.setSystemUiVisibility(sysUiFlag)); 486 PollingCheck.waitFor(TIMEOUT, () -> targetSysUiVis[0] == sysUiFlag); 487 getInstrumentation().runOnMainSync(() -> controlTarget.setSystemUiVisibility(0)); 488 PollingCheck.waitFor(TIMEOUT, () -> targetSysUiVis[0] == 0); 489 } 490 491 @Test testSetSystemUiVisibilityAfterCleared_showBarsBySwipe()492 public void testSetSystemUiVisibilityAfterCleared_showBarsBySwipe() throws Exception { 493 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 494 495 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 496 final View rootView = activity.getWindow().getDecorView(); 497 498 // Assume we have the bars and they can be visible. 499 final int types = statusBars(); 500 assumeTrue(rootView.getRootWindowInsets().isVisible(types)); 501 502 final int targetFlags = SYSTEM_UI_FLAG_IMMERSIVE | SYSTEM_UI_FLAG_FULLSCREEN; 503 504 // Use flags to hide status bar. 505 ANIMATION_CALLBACK.reset(); 506 getInstrumentation().runOnMainSync(() -> { 507 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 508 rootView.setSystemUiVisibility(targetFlags); 509 }); 510 ANIMATION_CALLBACK.waitForFinishing(); 511 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 512 513 // Wait for status bar invisible from InputDispatcher. Otherwise, the following 514 // dragFromTopToCenter might expand notification shade. 515 SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW); 516 517 // Swiping from top of display can show bars. 518 ANIMATION_CALLBACK.reset(); 519 dragFromTopToCenter(rootView); 520 ANIMATION_CALLBACK.waitForFinishing(); 521 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types) 522 && rootView.getSystemUiVisibility() != targetFlags); 523 524 // Use flags to hide status bar again. 525 ANIMATION_CALLBACK.reset(); 526 getInstrumentation().runOnMainSync(() -> { 527 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 528 rootView.setSystemUiVisibility(targetFlags); 529 }); 530 ANIMATION_CALLBACK.waitForFinishing(); 531 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 532 533 // Wait for status bar invisible from InputDispatcher. Otherwise, the following 534 // dragFromTopToCenter might expand notification shade. 535 SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW); 536 537 // Swiping from top of display can show bars. 538 ANIMATION_CALLBACK.reset(); 539 dragFromTopToCenter(rootView); 540 ANIMATION_CALLBACK.waitForFinishing(); 541 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)); 542 543 // The swipe action brings down the notification shade which causes subsequent tests to 544 // fail. 545 if (isAutomotive(mContext)) { 546 // Bring system to a known state before requesting to close system dialogs. 547 launchHomeActivity(); 548 broadcastCloseSystemDialogs(); 549 } 550 } 551 552 @Test testSetSystemUiVisibilityAfterCleared_showBarsByApp()553 public void testSetSystemUiVisibilityAfterCleared_showBarsByApp() throws Exception { 554 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 555 556 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 557 final View rootView = activity.getWindow().getDecorView(); 558 559 // Assume we have the bars and they can be visible. 560 final int types = statusBars(); 561 assumeTrue(rootView.getRootWindowInsets().isVisible(types)); 562 563 // Use the flag to hide status bar. 564 ANIMATION_CALLBACK.reset(); 565 getInstrumentation().runOnMainSync(() -> { 566 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 567 rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN); 568 }); 569 ANIMATION_CALLBACK.waitForFinishing(); 570 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 571 572 // Clearing the flag can show status bar. 573 getInstrumentation().runOnMainSync(() -> { 574 rootView.setSystemUiVisibility(0); 575 }); 576 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)); 577 578 // Use the flag to hide status bar again. 579 ANIMATION_CALLBACK.reset(); 580 getInstrumentation().runOnMainSync(() -> { 581 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 582 rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN); 583 }); 584 ANIMATION_CALLBACK.waitForFinishing(); 585 PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types)); 586 587 // Clearing the flag can show status bar. 588 getInstrumentation().runOnMainSync(() -> { 589 rootView.setSystemUiVisibility(0); 590 }); 591 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)); 592 } 593 594 @Test testHideOnCreate()595 public void testHideOnCreate() throws Exception { 596 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 597 598 final TestHideOnCreateActivity activity = 599 startActivityInWindowingModeFullScreen(TestHideOnCreateActivity.class); 600 final View rootView = activity.getWindow().getDecorView(); 601 ANIMATION_CALLBACK.waitForFinishing(); 602 PollingCheck.waitFor(TIMEOUT, 603 () -> !rootView.getRootWindowInsets().isVisible(statusBars()) 604 && !rootView.getRootWindowInsets().isVisible(navigationBars())); 605 } 606 607 @Test testShowImeOnCreate()608 public void testShowImeOnCreate() throws Exception { 609 final Instrumentation instrumentation = getInstrumentation(); 610 assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()), 611 nullValue()); 612 MockImeHelper.createManagedMockImeSession(this); 613 final TestShowOnCreateActivity activity = startActivity(TestShowOnCreateActivity.class); 614 final View rootView = activity.getWindow().getDecorView(); 615 ANIMATION_CALLBACK.waitForFinishing(); 616 PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime())); 617 } 618 619 @Test testShowImeOnCreate_doesntCauseImeToReappearWhenDialogIsShown()620 public void testShowImeOnCreate_doesntCauseImeToReappearWhenDialogIsShown() throws Exception { 621 final Instrumentation instrumentation = getInstrumentation(); 622 assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()), 623 nullValue()); 624 try (MockImeSession imeSession = MockImeSession.create(instrumentation.getContext(), 625 instrumentation.getUiAutomation(), new ImeSettings.Builder())) { 626 final TestShowOnCreateActivity activity = 627 startActivityInWindowingModeFullScreen(TestShowOnCreateActivity.class); 628 final View rootView = activity.getWindow().getDecorView(); 629 PollingCheck.waitFor(TIMEOUT, 630 () -> rootView.getRootWindowInsets().isVisible(ime())); 631 ANIMATION_CALLBACK.waitForFinishing(); 632 ANIMATION_CALLBACK.reset(); 633 getInstrumentation().runOnMainSync(() -> { 634 rootView.getWindowInsetsController().hide(ime()); 635 }); 636 PollingCheck.waitFor(TIMEOUT, 637 () -> !rootView.getRootWindowInsets().isVisible(ime())); 638 ANIMATION_CALLBACK.waitForFinishing(); 639 getInstrumentation().runOnMainSync(() -> { 640 activity.showAltImDialog(); 641 }); 642 643 for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) { 644 assertFalse("IME visible when it shouldn't be", 645 rootView.getRootWindowInsets().isVisible(ime())); 646 SystemClock.sleep(TIME_SLICE); 647 } 648 } 649 } 650 651 @Test testShowIme_immediatelyAfterDetachAndReattach()652 public void testShowIme_immediatelyAfterDetachAndReattach() throws Exception { 653 final Instrumentation instrumentation = getInstrumentation(); 654 assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()), 655 nullValue()); 656 MockImeHelper.createManagedMockImeSession(this); 657 final TestActivity activity = startActivity(TestActivity.class); 658 final View rootView = activity.getWindow().getDecorView(); 659 660 PollingCheck.waitFor(TIMEOUT, () -> getOnMainSync(rootView::hasWindowFocus)); 661 662 View editor = getOnMainSync(rootView::findFocus); 663 ViewGroup parent = (ViewGroup) getOnMainSync(editor::getParent); 664 665 getInstrumentation().runOnMainSync(() -> { 666 parent.removeView(editor); 667 }); 668 669 // Wait until checkFocus() is dispatched 670 getInstrumentation().waitForIdleSync(); 671 672 getInstrumentation().runOnMainSync(() -> { 673 parent.addView(editor); 674 editor.requestFocus(); 675 editor.getWindowInsetsController().show(ime()); 676 }); 677 678 PollingCheck.waitFor(TIMEOUT, () -> getOnMainSync( 679 () -> rootView.getRootWindowInsets().isVisible(ime())), 680 "Expected IME to become visible but didn't."); 681 } 682 683 @Test testInsetsDispatch()684 public void testInsetsDispatch() throws Exception { 685 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 686 687 // Start an activity which hides system bars in fullscreen mode, 688 // otherwise, it might not be able to hide system bars in other windowing modes. 689 final TestHideOnCreateActivity activity = startActivityInWindowingModeFullScreen( 690 TestHideOnCreateActivity.class); 691 final View rootView = activity.getWindow().getDecorView(); 692 ANIMATION_CALLBACK.waitForFinishing(); 693 PollingCheck.waitFor(TIMEOUT, 694 () -> !rootView.getRootWindowInsets().isVisible(statusBars()) 695 && !rootView.getRootWindowInsets().isVisible(navigationBars())); 696 697 // Add a dialog which hides system bars before the dialog is added to the system while the 698 // system bar was hidden previously, and collect the window insets that the dialog receives. 699 final ArrayList<WindowInsets> windowInsetsList = new ArrayList<>(); 700 getInstrumentation().runOnMainSync(() -> { 701 final AlertDialog dialog = new AlertDialog.Builder(activity).create(); 702 final Window dialogWindow = dialog.getWindow(); 703 dialogWindow.getDecorView().setOnApplyWindowInsetsListener((view, insets) -> { 704 windowInsetsList.add(insets); 705 return view.onApplyWindowInsets(insets); 706 }); 707 dialogWindow.getInsetsController().hide(statusBars() | navigationBars()); 708 dialog.show(); 709 }); 710 getInstrumentation().waitForIdleSync(); 711 712 // The dialog must never receive any of visible insets of system bars. 713 for (WindowInsets windowInsets : windowInsetsList) { 714 assertFalse(windowInsets.isVisible(statusBars())); 715 assertFalse(windowInsets.isVisible(navigationBars())); 716 } 717 } 718 719 @Test testWindowInsetsController_availableAfterAddView()720 public void testWindowInsetsController_availableAfterAddView() throws Exception { 721 assumeFalse(isCar() && remoteInsetsControllerControlsSystemBars()); 722 723 final TestHideOnCreateActivity activity = 724 startActivityInWindowingModeFullScreen(TestHideOnCreateActivity.class); 725 final View rootView = activity.getWindow().getDecorView(); 726 ANIMATION_CALLBACK.waitForFinishing(); 727 PollingCheck.waitFor(TIMEOUT, 728 () -> !rootView.getRootWindowInsets().isVisible(statusBars()) 729 && !rootView.getRootWindowInsets().isVisible(navigationBars())); 730 731 final View childWindow = new View(activity); 732 getInstrumentation().runOnMainSync(() -> { 733 activity.getWindowManager().addView(childWindow, 734 new WindowManager.LayoutParams(TYPE_APPLICATION)); 735 mErrorCollector.checkThat(childWindow.getWindowInsetsController(), is(notNullValue())); 736 }); 737 getInstrumentation().waitForIdleSync(); 738 getInstrumentation().runOnMainSync(() -> { 739 activity.getWindowManager().removeView(childWindow); 740 }); 741 742 } 743 744 @Test testDispatchApplyWindowInsetsCount_systemBars()745 public void testDispatchApplyWindowInsetsCount_systemBars() throws InterruptedException { 746 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 747 final View rootView = activity.getWindow().getDecorView(); 748 getInstrumentation().waitForIdleSync(); 749 750 // Assume we have at least one visible system bar. 751 assumeTrue(rootView.getRootWindowInsets().isVisible(statusBars()) 752 || rootView.getRootWindowInsets().isVisible(navigationBars())); 753 754 getInstrumentation().runOnMainSync(() -> { 755 // This makes the window frame stable while changing the system bar visibility. 756 final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes(); 757 attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 758 activity.getWindow().setAttributes(attrs); 759 }); 760 getInstrumentation().waitForIdleSync(); 761 762 final int[] dispatchApplyWindowInsetsCount = {0}; 763 rootView.setOnApplyWindowInsetsListener((v, insets) -> { 764 dispatchApplyWindowInsetsCount[0]++; 765 return v.onApplyWindowInsets(insets); 766 }); 767 768 // One hide-system-bar call... 769 ANIMATION_CALLBACK.reset(); 770 getInstrumentation().runOnMainSync(() -> { 771 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 772 rootView.getWindowInsetsController().hide(systemBars()); 773 }); 774 ANIMATION_CALLBACK.waitForFinishing(); 775 776 // ... should only trigger one dispatchApplyWindowInsets 777 assertEquals(1, dispatchApplyWindowInsetsCount[0]); 778 779 // One show-system-bar call... 780 dispatchApplyWindowInsetsCount[0] = 0; 781 ANIMATION_CALLBACK.reset(); 782 getInstrumentation().runOnMainSync(() -> { 783 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 784 rootView.getWindowInsetsController().show(systemBars()); 785 }); 786 ANIMATION_CALLBACK.waitForFinishing(); 787 788 // ... should only trigger one dispatchApplyWindowInsets 789 assertEquals(1, dispatchApplyWindowInsetsCount[0]); 790 } 791 792 @Test testDispatchApplyWindowInsetsCount_ime()793 public void testDispatchApplyWindowInsetsCount_ime() throws Exception { 794 assumeFalse("Automotive is to skip this test until showing and hiding certain insets " 795 + "simultaneously in a single request is supported", isAutomotive(mContext)); 796 assumeThat(MockImeSession.getUnavailabilityReason(getInstrumentation().getContext()), 797 nullValue()); 798 799 MockImeHelper.createManagedMockImeSession(this); 800 final TestActivity activity = startActivityInWindowingModeFullScreen(TestActivity.class); 801 final View rootView = activity.getWindow().getDecorView(); 802 getInstrumentation().waitForIdleSync(); 803 804 final int[] dispatchApplyWindowInsetsCount = {0}; 805 rootView.setOnApplyWindowInsetsListener((v, insets) -> { 806 dispatchApplyWindowInsetsCount[0]++; 807 return v.onApplyWindowInsets(insets); 808 }); 809 810 // One show-ime call... 811 ANIMATION_CALLBACK.reset(); 812 getInstrumentation().runOnMainSync(() -> { 813 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 814 rootView.getWindowInsetsController().show(ime()); 815 }); 816 ANIMATION_CALLBACK.waitForFinishing(); 817 818 // ... should only trigger one dispatchApplyWindowInsets 819 assertEquals(1, dispatchApplyWindowInsetsCount[0]); 820 821 // One hide-ime call... 822 dispatchApplyWindowInsetsCount[0] = 0; 823 ANIMATION_CALLBACK.reset(); 824 getInstrumentation().runOnMainSync(() -> { 825 rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 826 rootView.getWindowInsetsController().hide(ime()); 827 }); 828 ANIMATION_CALLBACK.waitForFinishing(); 829 830 // ... should only trigger one dispatchApplyWindowInsets 831 assertEquals(1, dispatchApplyWindowInsetsCount[0]); 832 } 833 broadcastCloseSystemDialogs()834 private static void broadcastCloseSystemDialogs() { 835 executeShellCommand(AM_BROADCAST_CLOSE_SYSTEM_DIALOGS); 836 } 837 isAutomotive(Context context)838 private static boolean isAutomotive(Context context) { 839 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 840 } 841 hideInsets(View view, int types)842 private static void hideInsets(View view, int types) throws InterruptedException { 843 ANIMATION_CALLBACK.reset(); 844 getInstrumentation().runOnMainSync(() -> { 845 view.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 846 view.getWindowInsetsController().hide(types); 847 }); 848 ANIMATION_CALLBACK.waitForFinishing(); 849 PollingCheck.waitFor(TIMEOUT, () -> !view.getRootWindowInsets().isVisible(types)); 850 } 851 tapOnDisplay(float x, float y)852 private void tapOnDisplay(float x, float y) { 853 dragOnDisplay(x, y, x, y); 854 } 855 dragFromTopToCenter(View view)856 private void dragFromTopToCenter(View view) { 857 dragOnDisplay(view.getWidth() / 2f, 0 /* downY */, 858 view.getWidth() / 2f, view.getHeight() / 2f); 859 } 860 dragFromLeftToCenter(View view)861 private void dragFromLeftToCenter(View view) { 862 dragOnDisplay(0 /* downX */, view.getHeight() / 2f, 863 view.getWidth() / 2f, view.getHeight() / 2f); 864 } 865 dragOnDisplay(float downX, float downY, float upX, float upY)866 private void dragOnDisplay(float downX, float downY, float upX, float upY) { 867 final long downTime = SystemClock.elapsedRealtime(); 868 869 // down event 870 MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 871 downX, downY, 0 /* metaState */); 872 sendPointerSync(event); 873 event.recycle(); 874 875 // move event 876 event = MotionEvent.obtain(downTime, downTime + 1, MotionEvent.ACTION_MOVE, 877 (downX + upX) / 2f, (downY + upY) / 2f, 0 /* metaState */); 878 sendPointerSync(event); 879 event.recycle(); 880 881 // up event 882 event = MotionEvent.obtain(downTime, downTime + 2, MotionEvent.ACTION_UP, 883 upX, upY, 0 /* metaState */); 884 sendPointerSync(event); 885 event.recycle(); 886 } 887 sendPointerSync(MotionEvent event)888 private void sendPointerSync(MotionEvent event) { 889 event.setSource(event.getSource() | InputDevice.SOURCE_CLASS_POINTER); 890 // Use UiAutomation to inject into TestActivity because it is started and owned by the 891 // Shell, which has a different uid than this instrumentation. 892 getInstrumentation().getUiAutomation().injectInputEvent(event, true); 893 } 894 895 private static class AnimationCallback extends WindowInsetsAnimation.Callback { 896 897 private static final long ANIMATION_TIMEOUT = 5000; // milliseconds 898 899 private boolean mFinished = false; 900 AnimationCallback()901 AnimationCallback() { 902 super(DISPATCH_MODE_CONTINUE_ON_SUBTREE); 903 } 904 905 @Override onProgress(WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)906 public WindowInsets onProgress(WindowInsets insets, 907 List<WindowInsetsAnimation> runningAnimations) { 908 return insets; 909 } 910 911 @Override onEnd(WindowInsetsAnimation animation)912 public void onEnd(WindowInsetsAnimation animation) { 913 synchronized (this) { 914 mFinished = true; 915 notify(); 916 } 917 } 918 waitForFinishing()919 void waitForFinishing() throws InterruptedException { 920 synchronized (this) { 921 if (!mFinished) { 922 wait(ANIMATION_TIMEOUT); 923 } 924 } 925 } 926 reset()927 void reset() { 928 synchronized (this) { 929 mFinished = false; 930 } 931 } 932 } 933 setViews(Activity activity, @Nullable String privateImeOptions)934 private static View setViews(Activity activity, @Nullable String privateImeOptions) { 935 LinearLayout layout = new LinearLayout(activity); 936 View text = new TextView(activity); 937 EditText editor = new EditText(activity); 938 editor.setPrivateImeOptions(privateImeOptions); 939 layout.addView(text); 940 layout.addView(editor); 941 activity.setContentView(layout); 942 editor.requestFocus(); 943 return layout; 944 } 945 946 public static class TestActivity extends FocusableActivity { 947 final String mEditTextMarker = 948 getClass().getName() + "/" + SystemClock.elapsedRealtimeNanos(); 949 950 @Override onCreate(Bundle savedInstanceState)951 protected void onCreate(Bundle savedInstanceState) { 952 super.onCreate(savedInstanceState); 953 setViews(this, mEditTextMarker); 954 getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN); 955 } 956 } 957 958 public static class TestHideOnCreateActivity extends FocusableActivity { 959 960 @Override onCreate(Bundle savedInstanceState)961 protected void onCreate(Bundle savedInstanceState) { 962 super.onCreate(savedInstanceState); 963 View layout = setViews(this, null /* privateImeOptions */); 964 ANIMATION_CALLBACK.reset(); 965 getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 966 getWindow().getInsetsController().hide(statusBars()); 967 layout.getWindowInsetsController().hide(navigationBars()); 968 } 969 } 970 971 public static class TestShowOnCreateActivity extends FocusableActivity { 972 @Override onCreate(Bundle savedInstanceState)973 protected void onCreate(Bundle savedInstanceState) { 974 super.onCreate(savedInstanceState); 975 setViews(this, null /* privateImeOptions */); 976 ANIMATION_CALLBACK.reset(); 977 getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK); 978 getWindow().getInsetsController().show(ime()); 979 } 980 showAltImDialog()981 void showAltImDialog() { 982 AlertDialog dialog = new AlertDialog.Builder(this) 983 .setTitle("TestDialog") 984 .create(); 985 dialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); 986 dialog.show(); 987 } 988 } 989 getOnMainSync(Supplier<R> f)990 private <R> R getOnMainSync(Supplier<R> f) { 991 final Object[] result = new Object[1]; 992 getInstrumentation().runOnMainSync(() -> result[0] = f.get()); 993 //noinspection unchecked 994 return (R) result[0]; 995 } 996 } 997