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