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