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