1 /* 2 * Copyright (C) 2018 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.view.inputmethod.cts; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.inputmethodservice.InputMethodService.FINISH_INPUT_NO_FALLBACK_CONNECTION; 21 import static android.view.Display.DEFAULT_DISPLAY; 22 import static android.view.View.VISIBLE; 23 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 24 import static android.view.WindowInsets.Type.ime; 25 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; 26 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; 27 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; 28 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; 29 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; 30 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED; 31 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; 32 import static android.view.inputmethod.InputMethodManager.CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING; 33 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible; 34 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible; 35 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync; 36 import static android.view.inputmethod.cts.util.TestUtils.runOnMainSync; 37 import static android.widget.PopupWindow.INPUT_METHOD_NOT_NEEDED; 38 39 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 40 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 41 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 42 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEventWithKeyValue; 43 import static com.android.cts.mockime.ImeEventStreamTestUtils.hideSoftInputMatcher; 44 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent; 45 import static com.android.cts.mockime.ImeEventStreamTestUtils.showSoftInputMatcher; 46 import static com.android.cts.mockime.ImeEventStreamTestUtils.waitForInputViewLayoutStable; 47 import static com.android.cts.mockime.ImeEventStreamTestUtils.withDescription; 48 49 import static org.junit.Assert.assertEquals; 50 import static org.junit.Assert.assertFalse; 51 import static org.junit.Assert.assertNotNull; 52 import static org.junit.Assert.assertNull; 53 import static org.junit.Assert.assertTrue; 54 import static org.junit.Assert.fail; 55 import static org.junit.Assume.assumeFalse; 56 import static org.junit.Assume.assumeTrue; 57 58 import android.app.AlertDialog; 59 import android.app.Instrumentation; 60 import android.app.compat.CompatChanges; 61 import android.content.Context; 62 import android.content.Intent; 63 import android.content.pm.PackageManager; 64 import android.content.res.Resources; 65 import android.graphics.Color; 66 import android.os.SystemClock; 67 import android.os.UserHandle; 68 import android.platform.test.annotations.AppModeFull; 69 import android.platform.test.annotations.AppModeInstant; 70 import android.server.wm.WindowManagerState; 71 import android.support.test.uiautomator.UiObject2; 72 import android.text.TextUtils; 73 import android.util.Log; 74 import android.util.Pair; 75 import android.view.Gravity; 76 import android.view.KeyEvent; 77 import android.view.View; 78 import android.view.ViewTreeObserver; 79 import android.view.WindowInsets; 80 import android.view.WindowInsetsController; 81 import android.view.WindowManager; 82 import android.view.inputmethod.InputMethod; 83 import android.view.inputmethod.InputMethodManager; 84 import android.view.inputmethod.cts.util.AutoCloseableWrapper; 85 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 86 import android.view.inputmethod.cts.util.MockTestActivityUtil; 87 import android.view.inputmethod.cts.util.RequireImeCompatFlagRule; 88 import android.view.inputmethod.cts.util.TestActivity; 89 import android.view.inputmethod.cts.util.TestActivity2; 90 import android.view.inputmethod.cts.util.TestUtils; 91 import android.view.inputmethod.cts.util.TestWebView; 92 import android.view.inputmethod.cts.util.UnlockScreenRule; 93 import android.widget.EditText; 94 import android.widget.LinearLayout; 95 import android.widget.PopupWindow; 96 import android.widget.TextView; 97 import android.window.OnBackInvokedDispatcher; 98 99 import androidx.annotation.ColorInt; 100 import androidx.annotation.NonNull; 101 import androidx.test.filters.MediumTest; 102 import androidx.test.platform.app.InstrumentationRegistry; 103 import androidx.test.runner.AndroidJUnit4; 104 import androidx.test.uiautomator.By; 105 import androidx.test.uiautomator.BySelector; 106 import androidx.test.uiautomator.UiDevice; 107 import androidx.test.uiautomator.Until; 108 109 import com.android.compatibility.common.util.CtsTouchUtils; 110 import com.android.compatibility.common.util.SystemUtil; 111 import com.android.cts.mockime.ImeEvent; 112 import com.android.cts.mockime.ImeEventStream; 113 import com.android.cts.mockime.ImeLayoutInfo; 114 import com.android.cts.mockime.ImeSettings; 115 import com.android.cts.mockime.MockImeSession; 116 117 import org.junit.Before; 118 import org.junit.Rule; 119 import org.junit.Test; 120 import org.junit.function.ThrowingRunnable; 121 import org.junit.runner.RunWith; 122 123 import java.io.IOException; 124 import java.util.ArrayList; 125 import java.util.List; 126 import java.util.Map; 127 import java.util.concurrent.CountDownLatch; 128 import java.util.concurrent.TimeUnit; 129 import java.util.concurrent.atomic.AtomicBoolean; 130 import java.util.concurrent.atomic.AtomicInteger; 131 import java.util.concurrent.atomic.AtomicReference; 132 import java.util.function.Predicate; 133 134 @MediumTest 135 @RunWith(AndroidJUnit4.class) 136 public class KeyboardVisibilityControlTest extends EndToEndImeTestBase { 137 private static final String TAG = KeyboardVisibilityControlTest.class.getSimpleName(); 138 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(6); 139 private static final long START_INPUT_TIMEOUT = TimeUnit.SECONDS.toMillis(10); 140 private static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(1); 141 private static final long LAYOUT_STABLE_THRESHOLD = TimeUnit.SECONDS.toMillis(3); 142 143 private static final int NEW_KEYBOARD_HEIGHT = 400; 144 private static final PreBackPressProcedure NO_OP_PRE_BACK_PRESS_PROCEDURE = 145 (instrumentation, editorRef) -> {}; 146 147 private static final String DISABLE_AUTO_ROTATE_CMD = 148 "settings put system accelerometer_rotation 0"; 149 private static final String ENABLE_AUTO_ROTATE_CMD = 150 "settings put system accelerometer_rotation 1"; 151 152 153 @Rule 154 public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule(); 155 @Rule 156 public final RequireImeCompatFlagRule mRequireImeCompatFlagRule = new RequireImeCompatFlagRule( 157 FINISH_INPUT_NO_FALLBACK_CONNECTION, true); 158 159 private static final String TEST_MARKER_PREFIX = 160 "android.view.inputmethod.cts.KeyboardVisibilityControlTest"; 161 162 private Instrumentation mInstrumentation; 163 private CtsTouchUtils mCtsTouchUtils; 164 165 @Before setup()166 public void setup() { 167 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 168 mCtsTouchUtils = new CtsTouchUtils(mInstrumentation.getTargetContext()); 169 } 170 getTestMarker()171 private static String getTestMarker() { 172 return TEST_MARKER_PREFIX + "/" + SystemClock.elapsedRealtimeNanos(); 173 } 174 onFinishInputViewMatcher(boolean expectedFinishingInput)175 private static Predicate<ImeEvent> onFinishInputViewMatcher(boolean expectedFinishingInput) { 176 Predicate<ImeEvent> matcher = event -> { 177 if (!TextUtils.equals("onFinishInputView", event.getEventName())) { 178 return false; 179 } 180 final boolean finishingInput = event.getArguments().getBoolean("finishingInput"); 181 return finishingInput == expectedFinishingInput; 182 }; 183 return withDescription("onFinishInputView(finishingInput=" + expectedFinishingInput + ")", 184 matcher); 185 } 186 launchTestActivity(@onNull String focusedMarker, @NonNull String nonFocusedMarker)187 private Pair<EditText, EditText> launchTestActivity(@NonNull String focusedMarker, 188 @NonNull String nonFocusedMarker) { 189 final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>(); 190 final AtomicReference<EditText> nonFocusedEditTextRef = new AtomicReference<>(); 191 TestActivity.startSync(activity -> { 192 final LinearLayout layout = new LinearLayout(activity); 193 layout.setOrientation(LinearLayout.VERTICAL); 194 195 final EditText focusedEditText = new EditText(activity); 196 focusedEditText.setHint("focused editText"); 197 focusedEditText.setPrivateImeOptions(focusedMarker); 198 focusedEditText.requestFocus(); 199 focusedEditTextRef.set(focusedEditText); 200 layout.addView(focusedEditText); 201 202 final EditText nonFocusedEditText = new EditText(activity); 203 nonFocusedEditText.setPrivateImeOptions(nonFocusedMarker); 204 nonFocusedEditText.setHint("target editText"); 205 nonFocusedEditTextRef.set(nonFocusedEditText); 206 layout.addView(nonFocusedEditText); 207 return layout; 208 }); 209 return new Pair<>(focusedEditTextRef.get(), nonFocusedEditTextRef.get()); 210 } 211 launchTestActivity(@onNull String marker)212 private EditText launchTestActivity(@NonNull String marker) { 213 return launchTestActivity(marker, getTestMarker()).first; 214 } 215 launchTestActivity2(@onNull String marker)216 private EditText launchTestActivity2(@NonNull String marker) { 217 final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>(); 218 new TestActivity.Starter().startSync(activity -> { 219 final LinearLayout layout = new LinearLayout(activity); 220 layout.setOrientation(LinearLayout.VERTICAL); 221 222 final EditText focusedEditText = new EditText(activity); 223 focusedEditText.setHint("focused editText"); 224 focusedEditText.setPrivateImeOptions(marker); 225 focusedEditText.requestFocus(); 226 focusedEditTextRef.set(focusedEditText); 227 layout.addView(focusedEditText); 228 return layout; 229 }, TestActivity2.class); 230 return focusedEditTextRef.get(); 231 } 232 233 @Test testBasicShowHideSoftInput()234 public void testBasicShowHideSoftInput() throws Exception { 235 final InputMethodManager imm = mInstrumentation 236 .getTargetContext().getSystemService(InputMethodManager.class); 237 238 try (MockImeSession imeSession = MockImeSession.create( 239 mInstrumentation.getContext(), 240 mInstrumentation.getUiAutomation(), 241 new ImeSettings.Builder())) { 242 final ImeEventStream stream = imeSession.openEventStream(); 243 244 final String marker = getTestMarker(); 245 final EditText editText = launchTestActivity(marker); 246 247 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 248 notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 249 expectImeInvisible(TIMEOUT); 250 251 assertTrue("isActive() must return true if the View has IME focus", 252 getOnMainSync(() -> imm.isActive(editText))); 253 254 // Test showSoftInput() flow 255 assertTrue("showSoftInput must success if the View has IME focus", 256 getOnMainSync(() -> imm.showSoftInput(editText, 0))); 257 258 expectEvent(stream, showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 259 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 260 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 261 View.VISIBLE, TIMEOUT); 262 expectImeVisible(TIMEOUT); 263 264 // Test hideSoftInputFromWindow() flow 265 assertTrue("hideSoftInputFromWindow must success if the View has IME focus", 266 getOnMainSync(() -> imm.hideSoftInputFromWindow(editText.getWindowToken(), 0))); 267 268 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 269 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 270 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 271 View.GONE, TIMEOUT); 272 expectImeInvisible(TIMEOUT); 273 } 274 } 275 276 @FunctionalInterface 277 private interface PreBackPressProcedure { run( Instrumentation instrumentation, AtomicReference<EditText> editorRef)278 void run( 279 Instrumentation instrumentation, 280 AtomicReference<EditText> editorRef) throws Exception; 281 } 282 verifyHideImeBackPressed( boolean appRequestsBackCallback, boolean imeRequestsBackCallback, @NonNull PreBackPressProcedure preBackPressProcedure)283 private void verifyHideImeBackPressed( 284 boolean appRequestsBackCallback, boolean imeRequestsBackCallback, 285 @NonNull PreBackPressProcedure preBackPressProcedure) throws Exception { 286 final Instrumentation instrumentation = mInstrumentation; 287 final Context context = instrumentation.getTargetContext(); 288 final InputMethodManager imm = context.getSystemService(InputMethodManager.class); 289 290 // Whether 'OnBackInvokedCallback' or 'onBackPressed' (legacy back) is used is defined by 291 // the 'enableOnBackInvokedCallback' flag in the Application manifest. 292 // Registering a callback is only authorized if the flag is set to true. Since the 293 // WindowOnBackDispatcher is created at the same time as the ViewRootImpl, for test purpose, 294 // we need to manually set the flag on ApplicationInfo before the window is created which 295 // happens during the MockIme creation and TestActivity creation. 296 final boolean onBackCallbackEnabled = 297 context.getApplicationInfo().isOnBackInvokedCallbackEnabled(); 298 299 try (MockImeSession imeSession = MockImeSession.create( 300 instrumentation.getContext(), 301 instrumentation.getUiAutomation(), 302 new ImeSettings.Builder() 303 .setOnBackCallbackEnabled(imeRequestsBackCallback) 304 )) { 305 final ImeEventStream stream = imeSession.openEventStream(); 306 final String marker = getTestMarker(); 307 final AtomicInteger backCallbackInvocationCount = new AtomicInteger(); 308 309 if (appRequestsBackCallback) { 310 context.getApplicationInfo().setEnableOnBackInvokedCallback(true); 311 } 312 313 final EditText editText = launchTestActivity(marker); 314 final AtomicReference<EditText> editorRef = new AtomicReference<>(); 315 editorRef.set(editText); 316 final TestActivity testActivity = (TestActivity) editText.getContext(); 317 318 if (appRequestsBackCallback) { 319 testActivity.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 320 OnBackInvokedDispatcher.PRIORITY_DEFAULT, () -> { 321 backCallbackInvocationCount.getAndIncrement(); 322 }); 323 } else { 324 testActivity.setIgnoreBackKey(true); 325 } 326 327 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 328 notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 329 expectImeInvisible(TIMEOUT); 330 331 assertTrue("isActive() must return true if the View has IME focus", 332 getOnMainSync(() -> imm.isActive(editText))); 333 334 // Test showSoftInput() flow 335 assertTrue("showSoftInput must success if the View has IME focus", 336 getOnMainSync(() -> imm.showSoftInput(editText, 0))); 337 338 expectEvent(stream, showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 339 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 340 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 341 View.VISIBLE, TIMEOUT); 342 expectImeVisible(TIMEOUT); 343 344 preBackPressProcedure.run(instrumentation, editorRef); 345 346 // Pressing back key, expect soft-keyboard will become invisible. 347 instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 348 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 349 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 350 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 351 View.GONE, TIMEOUT); 352 expectImeInvisible(TIMEOUT); 353 354 if (appRequestsBackCallback) { 355 // Verify that IME callback is removed after IME is hidden. 356 instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 357 assertEquals(1, backCallbackInvocationCount.get()); 358 } 359 } finally { 360 context.getApplicationInfo().setEnableOnBackInvokedCallback(onBackCallbackEnabled); 361 } 362 } 363 364 @Test testHideImeAfterBackPressed_legacyAppLegacyIme()365 public void testHideImeAfterBackPressed_legacyAppLegacyIme() throws Exception { 366 verifyHideImeBackPressed(false /* appRequestsBackCallback */, 367 false /* imeRequestsBackCallback */, 368 (instrumentation, editorRef) -> {} /* pre back press procedure */); 369 } 370 371 @Test testHideImeAfterBackPressed_migratedAppLegacyIme()372 public void testHideImeAfterBackPressed_migratedAppLegacyIme() throws Exception { 373 verifyHideImeBackPressed(true /* appRequestsBackCallback */, 374 false /* imeRequestsBackCallback */, 375 NO_OP_PRE_BACK_PRESS_PROCEDURE); 376 } 377 378 @Test testHideImeAfterBackPressed_migratedAppMigratedIme()379 public void testHideImeAfterBackPressed_migratedAppMigratedIme() throws Exception { 380 verifyHideImeBackPressed(true /* appRequestsBackCallback */, 381 true /* imeRequestsBackCallback */, 382 NO_OP_PRE_BACK_PRESS_PROCEDURE); 383 } 384 385 @Test testHideImeAfterBackPressed_legacyAppMigratedIme()386 public void testHideImeAfterBackPressed_legacyAppMigratedIme() throws Exception { 387 verifyHideImeBackPressed(true /* appRequestsBackCallback */, 388 true /* imeRequestsBackCallback */, 389 NO_OP_PRE_BACK_PRESS_PROCEDURE); 390 } 391 392 @AppModeFull(reason = "KeyguardManager is not accessible from instant apps") 393 @Test testHideImeAfterBackPressed_ScreenOffOn()394 public void testHideImeAfterBackPressed_ScreenOffOn() throws Exception { 395 verifyHideImeBackPressed(true /* appRequestsBackCallback */, 396 true /* imeRequestsBackCallback */, 397 (instrumentation, editorRef) -> { 398 TestUtils.turnScreenOff(); 399 TestUtils.turnScreenOn(); 400 TestUtils.unlockScreen(); 401 // Before testing the back procedure, ensure the test activity has the window 402 // focus and the IME visible after screen-on. 403 TestUtils.waitOnMainUntil(editorRef.get()::hasWindowFocus, TIMEOUT); 404 expectImeVisible(TIMEOUT); 405 } /* pre back press procedure */); 406 } 407 408 @Test testHideImeAfterBackPressed_rootViewChanges()409 public void testHideImeAfterBackPressed_rootViewChanges() throws Exception { 410 verifyHideImeBackPressed(true /* appRequestsBackCallback */, 411 true /* imeRequestsBackCallback */, 412 (instrumentation, editorRef) -> { 413 AutoCloseableWrapper<PopupWindow> popupWindowWrapper = 414 createPopupWindowWrapper(editorRef.get()); 415 instrumentation.waitForIdleSync(); 416 // Verify IME became invisible when the non-ime-focusable PopupWindow is shown. 417 expectImeInvisible(NOT_EXPECT_TIMEOUT); 418 419 runOnMainSync(() -> popupWindowWrapper.get().dismiss()); 420 // Verify IME became visible when the non-ime-focusable PopupWindow has 421 // dismissed. 422 expectImeVisible(TIMEOUT); 423 } /* pre back press procedure */); 424 } 425 426 @Test testShowHideSoftInputShouldBeIgnoredOnNonFocusedView()427 public void testShowHideSoftInputShouldBeIgnoredOnNonFocusedView() throws Exception { 428 final InputMethodManager imm = mInstrumentation 429 .getTargetContext().getSystemService(InputMethodManager.class); 430 431 try (MockImeSession imeSession = MockImeSession.create( 432 mInstrumentation.getContext(), 433 mInstrumentation.getUiAutomation(), 434 new ImeSettings.Builder())) { 435 final ImeEventStream stream = imeSession.openEventStream(); 436 437 final String focusedMarker = getTestMarker(); 438 final String nonFocusedMarker = getTestMarker(); 439 final Pair<EditText, EditText> editTextPair = 440 launchTestActivity(focusedMarker, nonFocusedMarker); 441 final EditText nonFocusedEditText = editTextPair.second; 442 443 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 444 445 expectImeInvisible(TIMEOUT); 446 assertFalse("isActive() must return false if the View does not have IME focus", 447 getOnMainSync(() -> imm.isActive(nonFocusedEditText))); 448 assertFalse("showSoftInput must fail if the View does not have IME focus", 449 getOnMainSync(() -> imm.showSoftInput(nonFocusedEditText, 0))); 450 notExpectEvent(stream, showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 451 452 assertFalse("hideSoftInputFromWindow must fail if the View does not have IME focus", 453 getOnMainSync(() -> imm.hideSoftInputFromWindow( 454 nonFocusedEditText.getWindowToken(), 0))); 455 notExpectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 456 expectImeInvisible(TIMEOUT); 457 } 458 } 459 460 @Test testToggleSoftInput()461 public void testToggleSoftInput() throws Exception { 462 final InputMethodManager imm = mInstrumentation 463 .getTargetContext().getSystemService(InputMethodManager.class); 464 465 try (MockImeSession imeSession = MockImeSession.create( 466 mInstrumentation.getContext(), 467 mInstrumentation.getUiAutomation(), 468 new ImeSettings.Builder())) { 469 final ImeEventStream stream = imeSession.openEventStream(); 470 471 final String marker = getTestMarker(); 472 final EditText editText = launchTestActivity(marker); 473 474 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 475 notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 476 expectImeInvisible(TIMEOUT); 477 478 // Test toggleSoftInputFromWindow() flow 479 runOnMainSync(() -> imm.toggleSoftInputFromWindow(editText.getWindowToken(), 0, 0)); 480 481 expectEvent(stream.copy(), showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 482 expectEvent(stream.copy(), editorMatcher("onStartInputView", marker), TIMEOUT); 483 expectImeVisible(TIMEOUT); 484 485 // Calling toggleSoftInputFromWindow() must hide the IME. 486 runOnMainSync(() -> imm.toggleSoftInputFromWindow(editText.getWindowToken(), 0, 0)); 487 488 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 489 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 490 expectImeInvisible(TIMEOUT); 491 } 492 } 493 494 @Test testShowHideKeyboardOnWebView()495 public void testShowHideKeyboardOnWebView() throws Exception { 496 final PackageManager pm = 497 mInstrumentation.getContext().getPackageManager(); 498 assumeTrue(pm.hasSystemFeature("android.software.webview")); 499 500 try (MockImeSession imeSession = MockImeSession.create( 501 mInstrumentation.getContext(), 502 mInstrumentation.getUiAutomation(), 503 new ImeSettings.Builder())) { 504 final ImeEventStream stream = imeSession.openEventStream(); 505 final String marker = getTestMarker(); 506 final UiObject2 inputTextField = TestWebView.launchTestWebViewActivity( 507 TIMEOUT, marker); 508 assertNotNull("Editor must exists on WebView", inputTextField); 509 expectImeInvisible(TIMEOUT); 510 511 inputTextField.click(); 512 expectEvent(stream.copy(), showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 513 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 514 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 515 expectImeVisible(TIMEOUT); 516 } 517 } 518 519 @Test testShowHideKeyboardWithInterval()520 public void testShowHideKeyboardWithInterval() throws Exception { 521 final InputMethodManager imm = mInstrumentation 522 .getTargetContext().getSystemService(InputMethodManager.class); 523 524 try (MockImeSession imeSession = MockImeSession.create( 525 mInstrumentation.getContext(), 526 mInstrumentation.getUiAutomation(), 527 new ImeSettings.Builder())) { 528 final ImeEventStream stream = imeSession.openEventStream(); 529 final String marker = getTestMarker(); 530 final EditText editText = launchTestActivity(marker); 531 expectImeInvisible(TIMEOUT); 532 533 runOnMainSync(() -> imm.showSoftInput(editText, 0)); 534 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 535 expectImeVisible(TIMEOUT); 536 537 // Intervals = 10, 20, 30, ..., 100, 150, 200, ... 538 final List<Integer> intervals = new ArrayList<>(); 539 for (int i = 10; i < 100; i += 10) intervals.add(i); 540 for (int i = 100; i < 500; i += 50) intervals.add(i); 541 // Regression test for b/221483132. 542 // WindowInsetsController tries to clean up IME window after IME hide animation is done. 543 // Makes sure that IMM#showSoftInput during IME hide animation cancels the cleanup. 544 for (int intervalMillis : intervals) { 545 runOnMainSync(() -> imm.hideSoftInputFromWindow(editText.getWindowToken(), 0)); 546 SystemClock.sleep(intervalMillis); 547 runOnMainSync(() -> imm.showSoftInput(editText, 0)); 548 expectImeVisible(TIMEOUT, "IME should be visible. Interval = " + intervalMillis); 549 } 550 } 551 } 552 553 @Test testShowSoftInputWithShowForcedFlagWhenAppIsLeaving()554 public void testShowSoftInputWithShowForcedFlagWhenAppIsLeaving() throws Exception { 555 final InputMethodManager imm = mInstrumentation 556 .getTargetContext().getSystemService(InputMethodManager.class); 557 558 try (MockImeSession imeSession = MockImeSession.create( 559 mInstrumentation.getContext(), 560 mInstrumentation.getUiAutomation(), 561 new ImeSettings.Builder())) { 562 final ImeEventStream stream = imeSession.openEventStream(); 563 564 // Launch a simple test activity 565 final TestActivity testActivity = TestActivity.startSync(activity -> { 566 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 567 return new LinearLayout(activity); 568 }); 569 assertTrue("test activity should be in resume state", 570 getOnMainSync(testActivity::hasWindowFocus)); 571 572 // Launch a test editor activity 573 final String marker = getTestMarker(); 574 final AtomicReference<EditText> ediTextRef = new AtomicReference<>(); 575 final TestActivity testEditorActivity = 576 new TestActivity.Starter().asNewTask().startSync(activity -> { 577 final LinearLayout layout = new LinearLayout(activity); 578 layout.setOrientation(LinearLayout.VERTICAL); 579 580 final EditText focusedEditText = new EditText(activity); 581 focusedEditText.setHint("focused editText"); 582 focusedEditText.setPrivateImeOptions(marker); 583 focusedEditText.requestFocus(); 584 layout.addView(focusedEditText); 585 ediTextRef.set(focusedEditText); 586 return layout; 587 }, TestActivity.class); 588 589 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 590 notExpectEvent(stream, editorMatcher("onStartInputView", marker), NOT_EXPECT_TIMEOUT); 591 expectImeInvisible(TIMEOUT); 592 593 assertTrue("isActive() must return true if the View has IME focus", 594 getOnMainSync(() -> imm.isActive(ediTextRef.get()))); 595 596 // Test showSoftInput() flow with adding SHOW_FORCED flag 597 assertTrue("showSoftInput must success if the View has IME focus", 598 getOnMainSync(() -> 599 imm.showSoftInput(ediTextRef.get(), InputMethodManager.SHOW_FORCED))); 600 601 expectEvent(stream, showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 602 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 603 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 604 View.VISIBLE, TIMEOUT); 605 expectImeVisible(TIMEOUT); 606 607 // Finish testEditorActivity 608 runOnMainSync(testEditorActivity::finish); 609 610 // Verify soft-keyboard will not visible when enabling the platform compat flag to 611 // clear SHOW_FOCED flag. Otherwise, keeping the legacy behavior of SHOW_FOCED that 612 // soft-keyboard remains visible if there is no explicit hiding request. 613 if (isClearShowForcedFlagEnabled(testActivity.getPackageName())) { 614 notExpectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), 615 NOT_EXPECT_TIMEOUT); 616 expectImeInvisible(TIMEOUT); 617 } else { 618 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT); 619 expectImeVisible(TIMEOUT); 620 } 621 } 622 } 623 624 @Test testFloatingImeHideKeyboardAfterBackPressed()625 public void testFloatingImeHideKeyboardAfterBackPressed() throws Exception { 626 final Instrumentation instrumentation = mInstrumentation; 627 final InputMethodManager imm = instrumentation.getTargetContext().getSystemService( 628 InputMethodManager.class); 629 630 // Initial MockIme with floating IME settings. 631 try (MockImeSession imeSession = MockImeSession.create( 632 instrumentation.getContext(), instrumentation.getUiAutomation(), 633 getFloatingImeSettings(Color.BLACK))) { 634 final ImeEventStream stream = imeSession.openEventStream(); 635 final String marker = getTestMarker(); 636 final EditText editText = launchTestActivity(marker); 637 638 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 639 notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 640 expectImeInvisible(TIMEOUT); 641 642 assertTrue("isActive() must return true if the View has IME focus", 643 getOnMainSync(() -> imm.isActive(editText))); 644 645 // Test showSoftInput() flow 646 assertTrue("showSoftInput must success if the View has IME focus", 647 getOnMainSync(() -> imm.showSoftInput(editText, 0))); 648 649 expectEvent(stream, showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT); 650 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 651 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 652 View.VISIBLE, TIMEOUT); 653 expectImeVisible(TIMEOUT); 654 655 // Pressing back key, expect soft-keyboard will become invisible. 656 instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 657 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 658 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 659 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 660 View.GONE, TIMEOUT); 661 expectImeInvisible(TIMEOUT); 662 } 663 } 664 665 @Test testImeVisibilityWhenDismissingDialogWithImeFocused()666 public void testImeVisibilityWhenDismissingDialogWithImeFocused() throws Exception { 667 final Instrumentation instrumentation = mInstrumentation; 668 try (MockImeSession imeSession = MockImeSession.create( 669 instrumentation.getContext(), 670 instrumentation.getUiAutomation(), 671 new ImeSettings.Builder())) { 672 final ImeEventStream stream = imeSession.openEventStream(); 673 674 // Launch a simple test activity 675 final TestActivity testActivity = 676 new TestActivity.Starter() 677 .withWindowingMode(WINDOWING_MODE_FULLSCREEN) 678 .startSync(LinearLayout::new, TestActivity.class); 679 680 // Launch a dialog 681 final String marker = getTestMarker(); 682 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 683 final AtomicReference<AlertDialog> dialogRef = new AtomicReference<>(); 684 TestUtils.runOnMainSync(() -> { 685 final EditText editText = new EditText(testActivity); 686 editText.setHint("focused editText"); 687 editText.setPrivateImeOptions(marker); 688 editText.requestFocus(); 689 final AlertDialog dialog = new AlertDialog.Builder(testActivity) 690 .setView(editText) 691 .create(); 692 final WindowInsetsController.OnControllableInsetsChangedListener listener = 693 new WindowInsetsController.OnControllableInsetsChangedListener() { 694 @Override 695 public void onControllableInsetsChanged( 696 @NonNull WindowInsetsController controller, int typeMask) { 697 if ((typeMask & ime()) != 0) { 698 editText.getWindowInsetsController() 699 .removeOnControllableInsetsChangedListener(this); 700 editText.getWindowInsetsController().show(ime()); 701 } 702 } 703 }; 704 dialog.show(); 705 editText.getWindowInsetsController().addOnControllableInsetsChangedListener( 706 listener); 707 editTextRef.set(editText); 708 dialogRef.set(dialog); 709 }); 710 TestUtils.waitOnMainUntil(() -> dialogRef.get().isShowing() 711 && editTextRef.get().hasFocus(), TIMEOUT); 712 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 713 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT); 714 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 715 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 716 View.VISIBLE, TIMEOUT); 717 expectImeVisible(TIMEOUT); 718 719 // Hide keyboard and dismiss dialog. 720 TestUtils.runOnMainSync(() -> { 721 editTextRef.get().getWindowInsetsController().hide(ime()); 722 dialogRef.get().dismiss(); 723 }); 724 725 // Expect onFinishInput called and keyboard should hide successfully. 726 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 727 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 728 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 729 View.GONE, TIMEOUT); 730 expectImeInvisible(TIMEOUT); 731 732 // onWindowVisibilityChanged event can be out of sequence. Creating 733 // a copy of the ImeEventStream to handle this event. 734 final ImeEventStream streamCopy = stream.copy(); 735 736 // Expect fallback input connection started and keyboard invisible after activity 737 // focused unless avoidable keyboard startup is desired, 738 // in which case, no fallback will be started. 739 if (!isPreventImeStartup()) { 740 final ImeEvent onStart = expectEvent(stream, 741 event -> "onStartInput".equals(event.getEventName()), TIMEOUT); 742 assertTrue(onStart.getEnterState().hasFallbackInputConnection()); 743 } 744 TestUtils.waitOnMainUntil(testActivity::hasWindowFocus, TIMEOUT); 745 expectEventWithKeyValue(streamCopy, "onWindowVisibilityChanged", "visible", 746 View.GONE, TIMEOUT); 747 expectImeInvisible(TIMEOUT); 748 } 749 } 750 751 @AppModeFull(reason = "KeyguardManager is not accessible from instant apps") 752 @Test testImeState_Unspecified_EditorDialogLostFocusAfterUnlocked()753 public void testImeState_Unspecified_EditorDialogLostFocusAfterUnlocked() throws Exception { 754 runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_UNSPECIFIED); 755 } 756 757 @AppModeFull(reason = "KeyguardManager is not accessible from instant apps") 758 @Test testImeState_Visible_EditorDialogLostFocusAfterUnlocked()759 public void testImeState_Visible_EditorDialogLostFocusAfterUnlocked() throws Exception { 760 runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_VISIBLE); 761 } 762 763 @AppModeFull(reason = "KeyguardManager is not accessible from instant apps") 764 @Test testImeState_AlwaysVisible_EditorDialogLostFocusAfterUnlocked()765 public void testImeState_AlwaysVisible_EditorDialogLostFocusAfterUnlocked() throws Exception { 766 runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 767 } 768 769 @AppModeFull(reason = "KeyguardManager is not accessible from instant apps") 770 @Test testImeState_Hidden_EditorDialogLostFocusAfterUnlocked()771 public void testImeState_Hidden_EditorDialogLostFocusAfterUnlocked() throws Exception { 772 runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_HIDDEN); 773 } 774 775 @AppModeFull(reason = "KeyguardManager is not accessible from instant apps") 776 @Test testImeState_AlwaysHidden_EditorDialogLostFocusAfterUnlocked()777 public void testImeState_AlwaysHidden_EditorDialogLostFocusAfterUnlocked() throws Exception { 778 runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 779 } 780 runImeDoesntReshowAfterKeyguardTest(int softInputState)781 private void runImeDoesntReshowAfterKeyguardTest(int softInputState) throws Exception { 782 try (MockImeSession imeSession = MockImeSession.create( 783 mInstrumentation.getContext(), 784 mInstrumentation.getUiAutomation(), 785 new ImeSettings.Builder())) { 786 final ImeEventStream stream = imeSession.openEventStream(); 787 // Launch a simple test activity 788 final TestActivity testActivity = 789 new TestActivity.Starter() 790 .withWindowingMode(WINDOWING_MODE_FULLSCREEN) 791 .startSync(LinearLayout::new, TestActivity.class); 792 793 // Launch a dialog and show keyboard 794 final String marker = getTestMarker(); 795 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 796 final AtomicReference<AlertDialog> dialogRef = new AtomicReference<>(); 797 TestUtils.runOnMainSync(() -> { 798 final EditText editText = new EditText(testActivity); 799 editText.setHint("focused editText"); 800 editText.setPrivateImeOptions(marker); 801 editText.requestFocus(); 802 final AlertDialog dialog = new AlertDialog.Builder(testActivity) 803 .setView(editText) 804 .create(); 805 dialog.getWindow().setSoftInputMode(softInputState); 806 // Tracking onFocusChange callback for debugging purpose. 807 editText.setOnFocusChangeListener((v, hasFocus) -> { 808 if (Log.isLoggable(TAG, Log.VERBOSE)) { 809 Log.v(TAG, "Editor " + editText + " hasFocus=" + hasFocus, new Throwable()); 810 } 811 }); 812 dialog.show(); 813 editText.getWindowInsetsController().show(ime()); 814 editTextRef.set(editText); 815 dialogRef.set(dialog); 816 }); 817 818 try (AutoCloseableWrapper<AlertDialog> dialogCloseWrapper = AutoCloseableWrapper.create( 819 dialogRef.get(), dialog -> TestUtils.runOnMainSync(dialog::dismiss))) { 820 TestUtils.waitOnMainUntil(() -> dialogRef.get().isShowing() 821 && editTextRef.get().hasFocus(), TIMEOUT); 822 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 823 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT); 824 // Copy the event stream to verify both events in case expectEvent missed the 825 // event verification if the actual event sequence has flipped. 826 expectEvent(stream.copy(), editorMatcher("onStartInputView", marker), TIMEOUT); 827 expectEventWithKeyValue(stream.copy(), "onWindowVisibilityChanged", "visible", 828 View.VISIBLE, TIMEOUT); 829 expectImeVisible(TIMEOUT); 830 831 TestUtils.turnScreenOff(); 832 // Clear editor focus after screen-off 833 TestUtils.runOnMainSync(editTextRef.get()::clearFocus); 834 835 TestUtils.waitOnMainUntil(() -> editTextRef.get().getWindowVisibility() != VISIBLE, 836 TIMEOUT); 837 expectEvent(stream, onFinishInputViewMatcher(true), TIMEOUT); 838 if (MockImeSession.isFinishInputNoFallbackConnectionEnabled()) { 839 // When IME enabled the new app compat behavior to finish input without fallback 840 // input connection when device interactive state changed, 841 // we expect onFinishInput happens without any additional fallback input 842 // connection started and no showShowSoftInput requested. 843 expectEvent(stream, event -> "onFinishInput".equals(event.getEventName()), 844 TIMEOUT); 845 notExpectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), 846 NOT_EXPECT_TIMEOUT); 847 } else { 848 // For legacy IME, the fallback input connection will started after screen-off. 849 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 850 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 851 // Expect showSoftInput comes when system notify InsetsController to apply 852 // show IME insets after IME input target updated. 853 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), 854 TIMEOUT); 855 notExpectEvent(stream, hideSoftInputMatcher(), NOT_EXPECT_TIMEOUT); 856 } 857 858 // Verify IME will invisible after device unlocked 859 TestUtils.turnScreenOn(); 860 TestUtils.unlockScreen(); 861 // Expect hideSoftInput will called by IMMS when the same window 862 // focused since the editText view focus has been cleared. 863 TestUtils.waitOnMainUntil(() -> editTextRef.get().hasWindowFocus() 864 && !editTextRef.get().hasFocus(), TIMEOUT); 865 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 866 if (!MockImeSession.isFinishInputNoFallbackConnectionEnabled()) { 867 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 868 } 869 expectImeInvisible(TIMEOUT); 870 } 871 } 872 } 873 874 @AppModeFull 875 @Test testImeVisibilityWhenImeTransitionBetweenActivities_Full()876 public void testImeVisibilityWhenImeTransitionBetweenActivities_Full() throws Exception { 877 runImeVisibilityWhenImeTransitionBetweenActivities(false /* instant */); 878 } 879 880 @AppModeInstant 881 @Test testImeVisibilityWhenImeTransitionBetweenActivities_Instant()882 public void testImeVisibilityWhenImeTransitionBetweenActivities_Instant() throws Exception { 883 runImeVisibilityWhenImeTransitionBetweenActivities(true /* instant */); 884 } 885 886 @AppModeFull 887 @Test testImeInvisibleWhenForceStopPkgProcess_Full()888 public void testImeInvisibleWhenForceStopPkgProcess_Full() throws Exception { 889 runImeVisibilityTestWhenForceStopPackage(false /* instant */); 890 } 891 892 @AppModeInstant 893 @Test testImeInvisibleWhenForceStopPkgProcess_Instant()894 public void testImeInvisibleWhenForceStopPkgProcess_Instant() throws Exception { 895 runImeVisibilityTestWhenForceStopPackage(true /* instant */); 896 } 897 898 @Test testRestoreImeVisibility()899 public void testRestoreImeVisibility() throws Exception { 900 // TODO(b/226110728): Remove after we can send ime restore signal to DisplayAreaOrganizer. 901 assumeFalse(isImeOrganized(DEFAULT_DISPLAY)); 902 runRestoreImeVisibility(TestSoftInputMode.UNCHANGED_WITH_BACKWARD_NAV, true); 903 } 904 905 @Test testRestoreImeVisibility_noRestoreForAlwaysHidden()906 public void testRestoreImeVisibility_noRestoreForAlwaysHidden() throws Exception { 907 runRestoreImeVisibility(TestSoftInputMode.ALWAYS_HIDDEN_WITH_BACKWARD_NAV, false); 908 } 909 910 @Test testRestoreImeVisibility_noRestoreForHiddenWithForwardNav()911 public void testRestoreImeVisibility_noRestoreForHiddenWithForwardNav() throws Exception { 912 runRestoreImeVisibility(TestSoftInputMode.HIDDEN_WITH_FORWARD_NAV, false); 913 } 914 915 /** 916 * Test case for Bug 225028378. 917 * 918 * <p>This test ensures that showing a non-ime-focusable {@link PopupWindow} with 919 * {@link PopupWindow#INPUT_METHOD_NOT_NEEDED} will be on top of the IME.</p> 920 */ 921 @Test testNonImeFocusablePopupWindow_onTopOfIme()922 public void testNonImeFocusablePopupWindow_onTopOfIme() throws Exception { 923 final Instrumentation instrumentation = mInstrumentation; 924 try (MockImeSession imeSession = MockImeSession.create( 925 mInstrumentation.getContext(), 926 mInstrumentation.getUiAutomation(), 927 new ImeSettings.Builder())) { 928 final ImeEventStream stream = imeSession.openEventStream(); 929 final String marker = getTestMarker(); 930 final AtomicReference<EditText> editorRef = new AtomicReference<>(); 931 new TestActivity.Starter().withWindowingMode( 932 WINDOWING_MODE_FULLSCREEN).startSync(activity -> { 933 final LinearLayout layout = new LinearLayout(activity); 934 layout.setOrientation(LinearLayout.VERTICAL); 935 layout.setGravity(Gravity.BOTTOM); 936 final EditText editText = new EditText(activity); 937 editorRef.set(editText); 938 editText.setHint("focused editText"); 939 editText.setPrivateImeOptions(marker); 940 editText.requestFocus(); 941 layout.addView(editText); 942 return layout; 943 }, TestActivity.class); 944 // Show IME. 945 runOnMainSync(() -> editorRef.get().getContext().getSystemService( 946 InputMethodManager.class).showSoftInput(editorRef.get(), 0)); 947 948 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 949 expectImeVisible(TIMEOUT); 950 951 // Create then show a non-ime-focusable PopupWindow with INPUT_METHOD_NOT_NEEDED. 952 try (AutoCloseableWrapper<PopupWindow> popupWindowWrapper = 953 createPopupWindowWrapper(editorRef.get()) 954 ) { 955 instrumentation.waitForIdleSync(); 956 // Verify IME became invisible when the non-ime-focusable PopupWindow is shown. 957 expectImeInvisible(NOT_EXPECT_TIMEOUT); 958 959 runOnMainSync(() -> popupWindowWrapper.get().dismiss()); 960 // Verify IME became visible when the non-ime-focusable PopupWindow has dismissed. 961 expectImeVisible(TIMEOUT); 962 } 963 } 964 } 965 966 /** 967 * Test case for Bug 228766370. 968 * 969 * <p>This test ensures that IME will visible on an ime-focusable overlay window when another 970 * activity behind the overlay that requests to show IME. <p/> 971 */ 972 @Test testImeVisibleOnImeFocusableOverlay()973 public void testImeVisibleOnImeFocusableOverlay() throws Exception { 974 try (MockImeSession imeSession = MockImeSession.create( 975 mInstrumentation.getContext(), 976 mInstrumentation.getUiAutomation(), 977 new ImeSettings.Builder() 978 .setInputViewHeight(NEW_KEYBOARD_HEIGHT) 979 .setDrawsBehindNavBar(true))) { 980 final ImeEventStream stream = imeSession.openEventStream(); 981 TestActivity testActivity = TestActivity.startSync(activity -> { 982 final View view = new View(activity); 983 view.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 984 return view; 985 }); 986 987 // Show an overlay with "IME Focusable" (NOT_FOCUSABLE | ALT_FOCUSABLE_IM) flags. 988 runOnMainSync(() -> SystemUtil.runWithShellPermissionIdentity(() -> 989 testActivity.showOverlayWindow(true /* imeFocusable */))); 990 mInstrumentation.waitForIdleSync(); 991 992 // Start a next activity to expect IME should visible on top of the overlay. 993 final String marker = getTestMarker(); 994 final AtomicReference<EditText> editorRef = new AtomicReference<>(); 995 new TestActivity.Starter().asNewTask().startSync(activity -> { 996 final LinearLayout layout = new LinearLayout(activity); 997 layout.setOrientation(LinearLayout.VERTICAL); 998 layout.setGravity(Gravity.BOTTOM); 999 final EditText editText = new EditText(activity); 1000 editorRef.set(editText); 1001 editText.setHint("focused editText"); 1002 editText.setPrivateImeOptions(marker); 1003 editText.requestFocus(); 1004 layout.addView(editText); 1005 return layout; 1006 }, TestActivity.class); 1007 // Show IME. 1008 runOnMainSync(() -> editorRef.get().getContext().getSystemService( 1009 InputMethodManager.class).showSoftInput(editorRef.get(), 0)); 1010 1011 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1012 expectImeVisible(TIMEOUT); 1013 } 1014 } 1015 1016 private enum TestSoftInputMode { 1017 UNCHANGED_WITH_BACKWARD_NAV, 1018 ALWAYS_HIDDEN_WITH_BACKWARD_NAV, 1019 HIDDEN_WITH_FORWARD_NAV 1020 } 1021 runRestoreImeVisibility(TestSoftInputMode mode, boolean expectImeVisible)1022 private void runRestoreImeVisibility(TestSoftInputMode mode, boolean expectImeVisible) 1023 throws Exception { 1024 final Instrumentation instrumentation = mInstrumentation; 1025 final WindowManager wm = instrumentation.getContext().getSystemService(WindowManager.class); 1026 // As restoring IME visibility behavior is only available when TaskSnapshot mechanism 1027 // enabled, skip the test when TaskSnapshot is not supported. 1028 assumeTrue("Restoring IME visibility not available when TaskSnapshot unsupported", 1029 wm.isTaskSnapshotSupported()); 1030 1031 try (MockImeSession imeSession = MockImeSession.create( 1032 instrumentation.getContext(), instrumentation.getUiAutomation(), 1033 new ImeSettings.Builder())) { 1034 final ImeEventStream stream = imeSession.openEventStream(); 1035 final String markerForActivity1 = getTestMarker(); 1036 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 1037 // Launch a test activity with focusing editText to show keyboard 1038 new TestActivity.Starter().withWindowingMode( 1039 WINDOWING_MODE_FULLSCREEN).startSync(activity -> { 1040 final LinearLayout layout = new LinearLayout(activity); 1041 final EditText editText = new EditText(activity); 1042 editTextRef.set(editText); 1043 editText.setHint("focused editText"); 1044 editText.setPrivateImeOptions(markerForActivity1); 1045 editText.requestFocus(); 1046 layout.addView(editText); 1047 activity.getWindow().getDecorView().getWindowInsetsController().show(ime()); 1048 if (mode == TestSoftInputMode.ALWAYS_HIDDEN_WITH_BACKWARD_NAV) { 1049 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 1050 } 1051 return layout; 1052 }, TestActivity.class); 1053 1054 expectEvent(stream, editorMatcher("onStartInput", markerForActivity1), TIMEOUT); 1055 expectEvent(stream, editorMatcher("onStartInputView", markerForActivity1), TIMEOUT); 1056 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1057 View.VISIBLE, TIMEOUT); 1058 expectImeVisible(TIMEOUT); 1059 1060 // Launch another app task activity to hide keyboard 1061 new TestActivity.Starter().asNewTask().withWindowingMode( 1062 WINDOWING_MODE_FULLSCREEN).startSync(activity -> { 1063 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 1064 return new LinearLayout(activity); 1065 }, TestActivity.class); 1066 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 1067 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 1068 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1069 View.GONE, TIMEOUT); 1070 expectImeInvisible(TIMEOUT); 1071 1072 if (mode == TestSoftInputMode.HIDDEN_WITH_FORWARD_NAV) { 1073 // Start new TestActivity on the same task with STATE_HIDDEN softInputMode. 1074 final String markerForActivity2 = getTestMarker(); 1075 new TestActivity.Starter().asSameTaskAndClearTop().withWindowingMode( 1076 WINDOWING_MODE_FULLSCREEN).startSync(activity -> { 1077 final LinearLayout layout = new LinearLayout(activity); 1078 final EditText editText = new EditText(activity); 1079 editText.setHint("focused editText"); 1080 editText.setPrivateImeOptions(markerForActivity2); 1081 editText.requestFocus(); 1082 layout.addView(editText); 1083 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN); 1084 return layout; 1085 }, TestActivity.class); 1086 expectEvent(stream, editorMatcher("onStartInput", markerForActivity2), TIMEOUT); 1087 } else { 1088 // Press back key to back to the first test activity 1089 instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 1090 expectEvent(stream, editorMatcher("onStartInput", markerForActivity1), TIMEOUT); 1091 } 1092 1093 // Expect the IME visibility according to expectImeVisible 1094 // The expected result could be: 1095 // 1) The system can restore the IME visibility to show IME up when navigated back to 1096 // the original app task, even the IME is hidden when switching to the next task. 1097 // 2) The system won't restore the IME visibility in some softInputMode cases. 1098 if (expectImeVisible) { 1099 expectImeVisible(TIMEOUT); 1100 } else { 1101 expectImeInvisible(TIMEOUT); 1102 } 1103 } 1104 } 1105 runImeVisibilityWhenImeTransitionBetweenActivities(boolean instant)1106 private void runImeVisibilityWhenImeTransitionBetweenActivities(boolean instant) 1107 throws Exception { 1108 try (MockImeSession imeSession = MockImeSession.create( 1109 mInstrumentation.getContext(), 1110 mInstrumentation.getUiAutomation(), 1111 new ImeSettings.Builder() 1112 .setInputViewHeight(NEW_KEYBOARD_HEIGHT) 1113 .setDrawsBehindNavBar(true))) { 1114 final ImeEventStream stream = imeSession.openEventStream(); 1115 final String marker = getTestMarker(); 1116 1117 AtomicReference<EditText> editTextRef = new AtomicReference<>(); 1118 // Launch test activity with focusing editor 1119 final TestActivity testActivity = 1120 new TestActivity.Starter().withWindowingMode( 1121 WINDOWING_MODE_FULLSCREEN).startSync(activity -> { 1122 final LinearLayout layout = new LinearLayout(activity); 1123 layout.setOrientation(LinearLayout.VERTICAL); 1124 layout.setGravity(Gravity.BOTTOM); 1125 final EditText editText = new EditText(activity); 1126 editTextRef.set(editText); 1127 editText.setHint("focused editText"); 1128 editText.setPrivateImeOptions(marker); 1129 editText.requestFocus(); 1130 layout.addView(editText); 1131 final View decorView = activity.getWindow().getDecorView(); 1132 decorView.setFitsSystemWindows(true); 1133 decorView.getWindowInsetsController().show(ime()); 1134 return layout; 1135 }, TestActivity.class); 1136 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 1137 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT); 1138 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1139 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1140 View.VISIBLE, TIMEOUT); 1141 expectImeVisible(TIMEOUT); 1142 1143 // Launch another test activity from another process with popup dialog. 1144 MockTestActivityUtil.launchSync(instant, TIMEOUT, 1145 Map.of(MockTestActivityUtil.EXTRA_KEY_SHOW_DIALOG, "true")); 1146 BySelector dialogSelector = By.clazz(AlertDialog.class).depth(0); 1147 UiDevice uiDevice = UiDevice.getInstance(mInstrumentation); 1148 assertNotNull(uiDevice.wait(Until.hasObject(dialogSelector), TIMEOUT)); 1149 1150 // Dismiss dialog and back to original test activity 1151 MockTestActivityUtil.sendBroadcastAction(MockTestActivityUtil.EXTRA_DISMISS_DIALOG); 1152 1153 final CountDownLatch imeVisibilityUpdateLatch = new CountDownLatch(1); 1154 AtomicReference<Boolean> imeInsetsVisible = new AtomicReference<>(); 1155 TestUtils.runOnMainSync( 1156 () -> testActivity.getWindow().getDecorView().setOnApplyWindowInsetsListener( 1157 (v, insets) -> { 1158 if (insets.hasInsets()) { 1159 imeInsetsVisible.set(insets.isVisible(WindowInsets.Type.ime())); 1160 imeVisibilityUpdateLatch.countDown(); 1161 } 1162 return v.onApplyWindowInsets(insets); 1163 })); 1164 // Verify keyboard visibility should aligned with IME insets visibility. 1165 TestUtils.waitOnMainUntil( 1166 () -> testActivity.getWindow().getDecorView().getVisibility() == VISIBLE 1167 && testActivity.getWindow().getDecorView().hasWindowFocus(), TIMEOUT); 1168 assertTrue("Waiting for onApplyWindowInsets timed out", 1169 imeVisibilityUpdateLatch.await(5, TimeUnit.SECONDS)); 1170 // Wait for layout being stable in case insets visibility might not align with the 1171 // input view visibility. 1172 waitForInputViewLayoutStable(stream, LAYOUT_STABLE_THRESHOLD); 1173 1174 if (imeInsetsVisible.get()) { 1175 expectImeVisible(TIMEOUT); 1176 } else { 1177 expectImeInvisible(TIMEOUT); 1178 } 1179 } 1180 } 1181 runImeVisibilityTestWhenForceStopPackage(boolean instant)1182 private void runImeVisibilityTestWhenForceStopPackage(boolean instant) throws Exception { 1183 try (MockImeSession imeSession = MockImeSession.create( 1184 mInstrumentation.getContext(), 1185 mInstrumentation.getUiAutomation(), 1186 new ImeSettings.Builder())) { 1187 final ImeEventStream stream = imeSession.openEventStream(); 1188 final String marker = getTestMarker(); 1189 1190 // Make sure that MockIme isn't shown in the initial state. 1191 final ImeLayoutInfo lastLayout = 1192 waitForInputViewLayoutStable(stream, LAYOUT_STABLE_THRESHOLD); 1193 assertNull(lastLayout); 1194 expectImeInvisible(TIMEOUT); 1195 // Flush all the events happened before launching the test Activity. 1196 stream.skipAll(); 1197 1198 // Launch test activity with focusing an editor from remote process and expect the 1199 // IME is visible. 1200 try (AutoCloseable closable = MockTestActivityUtil.launchSync( 1201 instant, TIMEOUT, 1202 Map.of(MockTestActivityUtil.EXTRA_KEY_PRIVATE_IME_OPTIONS, marker))) { 1203 expectEvent(stream, editorMatcher("onStartInput", marker), START_INPUT_TIMEOUT); 1204 expectImeInvisible(TIMEOUT); 1205 1206 // Request showSoftInput, expect the request is valid and soft-keyboard visible. 1207 MockTestActivityUtil.sendBroadcastAction( 1208 MockTestActivityUtil.EXTRA_SHOW_SOFT_INPUT); 1209 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT); 1210 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1211 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1212 View.VISIBLE, TIMEOUT); 1213 expectImeVisible(TIMEOUT); 1214 1215 // Force stop test app package, and then expect IME should be invisible after the 1216 // remote process stopped by forceStopPackage. 1217 MockTestActivityUtil.forceStopPackage(); 1218 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 1219 expectImeInvisible(TIMEOUT); 1220 } 1221 } 1222 } 1223 1224 /** 1225 * Test case for Bug 254624767. 1226 * 1227 * <p>This test ensures that when the app requests to show/hide IME according to the IME insets 1228 * visibility with {@link WindowInsets#isVisible(int)} during switching apps, verify the system 1229 * dispatches the IME insets visibility to the app correctly during that time. </p> 1230 */ 1231 @Test testImeInsetsInvisibleAfterBackingFromImeHiddenActivity()1232 public void testImeInsetsInvisibleAfterBackingFromImeHiddenActivity() throws Exception { 1233 try (MockImeSession imeSession = MockImeSession.create( 1234 mInstrumentation.getContext(), 1235 mInstrumentation.getUiAutomation(), 1236 new ImeSettings.Builder())) { 1237 final ImeEventStream stream = imeSession.openEventStream(); 1238 final String marker = getTestMarker(); 1239 1240 // Launch the first activity 1241 final EditText editText = launchTestActivity(marker); 1242 AtomicReference<CountDownLatch> imeInsetsHiddenLatchRef = new AtomicReference<>(); 1243 expectEvent(stream, editorMatcher("onStartInput", marker), START_INPUT_TIMEOUT); 1244 notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1245 expectImeInvisible(TIMEOUT); 1246 1247 TestUtils.runOnMainSync(() -> { 1248 // Show IME with WindowInsets API and register onApplyWindowInsetsListener 1249 editText.getRootView().setOnApplyWindowInsetsListener( 1250 (v, insets) -> { 1251 if (!insets.isVisible(WindowInsets.Type.ime())) { 1252 if (imeInsetsHiddenLatchRef.get() != null) { 1253 imeInsetsHiddenLatchRef.get().countDown(); 1254 } 1255 } 1256 return v.onApplyWindowInsets(insets); 1257 }); 1258 editText.getViewTreeObserver().addOnWindowFocusChangeListener(hasFocus -> { 1259 // Test scenario: emulate the issue app implements to show IME when 1260 // focusing back if the last requested IME insets visibility is true. 1261 // And hides IME with clearing the editor focus when the app focus-out. 1262 if (hasFocus) { 1263 final boolean hasImeShown = 1264 editText.getRootWindowInsets().isVisible(ime()); 1265 if (hasImeShown) { 1266 editText.getWindowInsetsController().show(ime()); 1267 } 1268 } else { 1269 editText.getWindowInsetsController().hide(ime()); 1270 editText.clearFocus(); 1271 } 1272 }); 1273 editText.getWindowInsetsController().show(ime()); 1274 } 1275 ); 1276 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1277 expectImeVisible(TIMEOUT); 1278 1279 // Launch the second test activity with expecting to hide the IME. 1280 final TestActivity secondActivity = new TestActivity.Starter() 1281 .asNewTask().startSync(activity -> { 1282 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 1283 return new LinearLayout(activity); 1284 }, TestActivity.class); 1285 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 1286 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1287 View.GONE, TIMEOUT); 1288 expectImeInvisible(TIMEOUT); 1289 1290 // Back to first activity 1291 imeInsetsHiddenLatchRef.set(new CountDownLatch(1)); 1292 runOnMainSync(secondActivity::onBackPressed); 1293 1294 // Verify the visibility of IME insets should be hidden in onApplyWindowInsets and 1295 // expect the first activity hides the IME according to the received IME insets 1296 // visibility when backing from the second activity. 1297 imeInsetsHiddenLatchRef.get().await(5, TimeUnit.SECONDS); 1298 notExpectEvent(stream, editorMatcher("onStartInputView", marker), NOT_EXPECT_TIMEOUT); 1299 expectImeInvisible(TIMEOUT); 1300 } 1301 } 1302 1303 /** 1304 * Test case for Bug 256739702. 1305 * 1306 * <p>This test ensures that when "android:screenSize|Orientation" is not set and the keyboard 1307 * is shown implicitly in fullscreen mode, it will not disappear after the user rotates screen. 1308 */ 1309 @Test testRotateScreenWithKeyboardShownImplicitly()1310 public void testRotateScreenWithKeyboardShownImplicitly() throws Exception { 1311 // Test only when both portrait and landscape mode are supported. 1312 final PackageManager pm = mInstrumentation.getTargetContext().getPackageManager(); 1313 assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)); 1314 assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE)); 1315 1316 final InputMethodManager imm = mInstrumentation 1317 .getTargetContext().getSystemService(InputMethodManager.class); 1318 // Disable auto-rotate screen and set the screen orientation to portrait mode. 1319 setAutoRotateScreen(false); 1320 final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation); 1321 uiDevice.setOrientationPortrait(); 1322 mInstrumentation.waitForIdleSync(); 1323 1324 // Set FullscreenModePolicy as OS_DEFAULT to call the original 1325 // InputMethodService#onEvaluateFullscreenMode() 1326 try (MockImeSession imeSession = MockImeSession.create( 1327 mInstrumentation.getContext(), 1328 mInstrumentation.getUiAutomation(), 1329 new ImeSettings.Builder().setFullscreenModePolicy( 1330 ImeSettings.FullscreenModePolicy.OS_DEFAULT))) { 1331 final ImeEventStream stream = imeSession.openEventStream(); 1332 1333 final String marker = getTestMarker(); 1334 // TestActivity2 is identical to TestActivity but doesn't handle any configChanges. 1335 final EditText editText = launchTestActivity2(marker); 1336 1337 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 1338 notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1339 expectImeInvisible(TIMEOUT); 1340 assertTrue("isActive() must return true if the View has IME focus", 1341 getOnMainSync(() -> imm.isActive(editText))); 1342 1343 // Call ShowSoftInput() implicitly 1344 assertTrue("showSoftInput must success if the View has IME focus", 1345 getOnMainSync(() -> imm.showSoftInput(editText, 1346 InputMethodManager.SHOW_IMPLICIT))); 1347 1348 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 1349 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1350 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1351 View.VISIBLE, TIMEOUT); 1352 expectImeVisible(TIMEOUT); 1353 1354 // Rotate screen to landscape. 1355 uiDevice.setOrientationLandscape(); 1356 mInstrumentation.waitForIdleSync(); 1357 expectImeVisible(TIMEOUT); 1358 assertTrue("IME should be in fullscreen mode", 1359 getOnMainSync(() -> imm.isFullscreenMode())); 1360 } 1361 setAutoRotateScreen(true); 1362 } 1363 1364 /** 1365 * Test case for Bug 222064495. 1366 * 1367 * <p>Test that the IME should be hidden when the IME layering target overlay with 1368 * "NOT_FOCUSABLE | ALT_FOCUSABLE_IM" flags popup during pressing the recents key to the 1369 * overview screen.</b> 1370 */ 1371 @Test testImeHiddenWhenImeLayeringTargetDelayedToShowInAppSwitch()1372 public void testImeHiddenWhenImeLayeringTargetDelayedToShowInAppSwitch() throws Exception { 1373 assumeTrue(hasRecentsScreen()); 1374 1375 try (MockImeSession imeSession = MockImeSession.create( 1376 mInstrumentation.getContext(), 1377 mInstrumentation.getUiAutomation(), 1378 new ImeSettings.Builder())) { 1379 final ImeEventStream stream = imeSession.openEventStream(); 1380 final String marker = getTestMarker(); 1381 1382 final TestActivity testActivity = TestActivity.startSync(activity -> { 1383 final LinearLayout layout = new LinearLayout(activity); 1384 layout.setOrientation(LinearLayout.VERTICAL); 1385 1386 final EditText editText = new EditText(activity); 1387 layout.addView(editText); 1388 editText.setHint("focused editText"); 1389 editText.setPrivateImeOptions(marker); 1390 editText.requestFocus(); 1391 activity.getWindow().getDecorView().getWindowInsetsController().show(ime()); 1392 return layout; 1393 }); 1394 1395 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1396 expectImeVisible(TIMEOUT); 1397 1398 // Intentionally showing an overlay with "IME Focusable" (NOT_FOCUSABLE | 1399 // ALT_FOCUSABLE_IM) flags during pressing the recents key to the overview screen. 1400 testActivity.getWindow().getDecorView().postDelayed( 1401 () -> SystemUtil.runWithShellPermissionIdentity(() -> 1402 testActivity.showOverlayWindow(true /* imeFocusable */)), 100); 1403 mInstrumentation.sendKeyDownUpSync( 1404 KeyEvent.KEYCODE_RECENT_APPS); 1405 1406 // Expect the IME should hidden by the IME not attachable on the activity when the 1407 // overlay popup. 1408 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 1409 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1410 View.GONE, TIMEOUT); 1411 expectImeInvisible(TIMEOUT); 1412 } finally { 1413 // Back to home to clean up states after the test finished. 1414 UiDevice.getInstance(mInstrumentation).pressHome(); 1415 } 1416 } 1417 1418 /** 1419 * Test the IME visibility when in split-screen mode, switching the focus to the app task with 1420 * {@link WindowManager.LayoutParams#SOFT_INPUT_STATE_HIDDEN} flag from the app showing the 1421 * IME will expect to be hidden. 1422 */ 1423 @Test testImeHiddenWhenFocusToAppWithStateHiddenFlagInMultiWindowMode()1424 public void testImeHiddenWhenFocusToAppWithStateHiddenFlagInMultiWindowMode() throws Exception { 1425 assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); 1426 1427 try (MockImeSession imeSession = MockImeSession.create( 1428 mInstrumentation.getContext(), 1429 mInstrumentation.getUiAutomation(), 1430 new ImeSettings.Builder())) { 1431 final ImeEventStream stream = imeSession.openEventStream(); 1432 final String marker = getTestMarker(); 1433 1434 // Launch an editor activity to be on the split primary task. 1435 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 1436 final TestActivity splitPrimaryActivity = TestActivity.startSync(activity -> { 1437 final LinearLayout layout = new LinearLayout(activity); 1438 layout.setOrientation(LinearLayout.VERTICAL); 1439 final EditText editText = new EditText(activity); 1440 editTextRef.set(editText); 1441 layout.addView(editText); 1442 editText.setHint("focused editText"); 1443 editText.setPrivateImeOptions(marker); 1444 editText.requestFocus(); 1445 return layout; 1446 }); 1447 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 1448 notExpectEvent(stream, editorMatcher("onStartInputView", marker), NOT_EXPECT_TIMEOUT); 1449 expectImeInvisible(TIMEOUT); 1450 1451 // Launch another activity with SOFT_INPUT_STATE_HIDDEN flag to be on the split 1452 // secondary task, expect the IME won't receive onStartInputView and invisible. 1453 final TestActivity splitSecondaryActivity = new TestActivity.Starter() 1454 .asMultipleTask() 1455 .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) 1456 .startSync(splitPrimaryActivity, activity -> { 1457 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN); 1458 return new LinearLayout(activity); 1459 }, TestActivity2.class); 1460 notExpectEvent(stream, event -> "onStartInputView".equals(event.getEventName()), 1461 NOT_EXPECT_TIMEOUT); 1462 expectImeInvisible(TIMEOUT); 1463 1464 // Double tap the editor on the split primary task to focus the window and show the IME. 1465 mCtsTouchUtils.emulateDoubleTapOnViewCenter(mInstrumentation, 1466 null, editTextRef.get()); 1467 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1468 expectImeVisible(TIMEOUT); 1469 1470 // Tap on the split secondary task to switch focus and expect the IME will be hidden. 1471 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, 1472 null, splitSecondaryActivity.getWindow().getDecorView()); 1473 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 1474 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 1475 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 1476 View.GONE, TIMEOUT); 1477 expectImeInvisible(TIMEOUT); 1478 } 1479 } 1480 1481 /** 1482 * Test case for Bug 226689544. 1483 * 1484 * Test to verify that the IME is visible, after the following actions: 1485 * 1. Open primary activity. 1486 * 2. Open second activity from the first activity in split screen. 1487 * 3. Open and show a dialog with editText in the second activity. 1488 * 4. Focus/click on first activity, then click on the editText. 1489 */ 1490 @Test testIMEVisibleInSplitScreenAfterGainingFocus()1491 public void testIMEVisibleInSplitScreenAfterGainingFocus() throws Exception { 1492 assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); 1493 1494 try (MockImeSession imeSession = MockImeSession.create( 1495 mInstrumentation.getContext(), 1496 mInstrumentation.getUiAutomation(), 1497 new ImeSettings.Builder())) { 1498 final ImeEventStream stream = imeSession.openEventStream(); 1499 final String marker = getTestMarker(); 1500 1501 final TestActivity splitPrimaryActivity = TestActivity.startSync(LinearLayout::new); 1502 1503 final AtomicReference<AlertDialog> dialogRef = new AtomicReference<>(); 1504 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 1505 try { 1506 // Launch another activity with SOFT_INPUT_STATE_UNCHANGED flag to be on the split 1507 // secondary task as well as on its dialog, so that the IME is not visible by 1508 // default on large screens, when showing the dialog. 1509 new TestActivity.Starter() 1510 .asMultipleTask() 1511 .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) 1512 .startSync(splitPrimaryActivity, activity -> { 1513 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_UNCHANGED); 1514 1515 final EditText editText = new EditText(activity); 1516 editText.setHint("focused editText"); 1517 editText.setPrivateImeOptions(marker); 1518 editText.requestFocus(); 1519 final AlertDialog dialog = new AlertDialog.Builder(activity) 1520 .setTitle("DialogWithEditText") 1521 .setCancelable(false) 1522 .setView(editText) 1523 .create(); 1524 dialog.getWindow().setSoftInputMode(SOFT_INPUT_STATE_UNCHANGED); 1525 dialog.show(); 1526 dialogRef.set(dialog); 1527 editTextRef.set(editText); 1528 return new LinearLayout(activity); 1529 }, TestActivity2.class); 1530 1531 View decor = splitPrimaryActivity.getWindow().getDecorView(); 1532 CountDownLatch latch = new CountDownLatch(1); 1533 ViewTreeObserver observer = decor.getViewTreeObserver(); 1534 observer.addOnDrawListener(() -> { 1535 if (splitPrimaryActivity.isInMultiWindowMode()) { 1536 // check activity in multi-window mode after relayoutWindow. 1537 latch.countDown(); 1538 } 1539 }); 1540 1541 latch.await(LAYOUT_STABLE_THRESHOLD, TimeUnit.MILLISECONDS); 1542 1543 // Tap on the first activity to change focus 1544 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, 1545 null, splitPrimaryActivity.getWindow().getDecorView()); 1546 1547 notExpectEvent(stream, event -> "onStartInputView".equals(event.getEventName()), 1548 NOT_EXPECT_TIMEOUT); 1549 expectImeInvisible(TIMEOUT); 1550 1551 // Tap on the edit text in the split dialog to show the IME. 1552 mCtsTouchUtils.emulateDoubleTapOnViewCenter(mInstrumentation, 1553 null, editTextRef.get()); 1554 1555 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 1556 expectImeVisible(TIMEOUT); 1557 } finally { 1558 // dismiss dialog, in case it wasn't closed properly 1559 if (dialogRef.get() != null) { 1560 dialogRef.get().dismiss(); 1561 } 1562 } 1563 } 1564 } 1565 1566 /** 1567 * A regression Test for Bug 283342812 1568 * 1569 * 1. Open primary activity. 1570 * 2. Open second activity with 2 editText views from the first activity in split screen. 1571 * 3. Focus the 1st editor and invoke {@link WindowInsetsController#show} to make IME visible. 1572 * 4. Press the back key to make IME invisible. 1573 * 5. Focus the 2nd editor and invoke {@link WindowInsetsController#show} to make IME visible. 1574 * 6. Finish the primary activity to exit split screen mode. 1575 * 7. Test step 3-5 again to ensure it passes after exiting split screen mode. 1576 */ 1577 @Test testIMEVisibleInSplitScreenWithWindowInsetsApi()1578 public void testIMEVisibleInSplitScreenWithWindowInsetsApi() throws Throwable { 1579 assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); 1580 1581 try (MockImeSession imeSession = MockImeSession.create( 1582 mInstrumentation.getContext(), 1583 mInstrumentation.getUiAutomation(), 1584 new ImeSettings.Builder())) { 1585 final ImeEventStream stream = imeSession.openEventStream(); 1586 final TestActivity splitPrimaryActivity = TestActivity.startSync(LinearLayout::new); 1587 1588 // Launch another test activity in split-screen with 2 editor views 1589 final AtomicReference<EditText> editText1Ref = new AtomicReference<>(); 1590 final AtomicReference<EditText> editText2Ref = new AtomicReference<>(); 1591 final String editText1Marker = getTestMarker(); 1592 final String editText2Marker = getTestMarker(); 1593 final TestActivity testActivity2 = new TestActivity.Starter() 1594 .asMultipleTask() 1595 .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) 1596 .startSync(splitPrimaryActivity, activity -> { 1597 LinearLayout layout = new LinearLayout(activity); 1598 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 1599 1600 final EditText editText1 = new EditText(activity); 1601 editText1.setHint("This is editText1"); 1602 editText1.setPrivateImeOptions(editText1Marker); 1603 editText1Ref.set(editText1); 1604 1605 final EditText editText2 = new EditText(activity); 1606 editText2.setHint("This is editText2"); 1607 editText2.setPrivateImeOptions(editText2Marker); 1608 editText2Ref.set(editText2); 1609 1610 layout.addView(editText1); 1611 layout.addView(editText2); 1612 return layout; 1613 }, TestActivity2.class); 1614 1615 notExpectEvent(stream, event -> "onStartInputView".equals(event.getEventName()), 1616 NOT_EXPECT_TIMEOUT); 1617 expectImeInvisible(TIMEOUT); 1618 1619 // Tap on the test activity to change focus 1620 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, 1621 null, testActivity2.getWindow().getDecorView()); 1622 1623 ThrowingRunnable testProcedureForTestActivity2 = () -> { 1624 // Focus the 1st editor and show the IME with WindowInsets API. 1625 testActivity2.runOnUiThread(() -> { 1626 editText1Ref.get().requestFocus(); 1627 editText1Ref.get() 1628 .getWindowInsetsController().show(WindowInsets.Type.ime()); 1629 }); 1630 expectEvent(stream, editorMatcher("onStartInputView", editText1Marker), TIMEOUT); 1631 expectImeVisible(TIMEOUT); 1632 1633 // Press the back key to make the IME invisible. 1634 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 1635 expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT); 1636 expectImeInvisible(TIMEOUT); 1637 1638 // Focus the 2nd editor and show the IME with WindowInsets API. 1639 testActivity2.runOnUiThread(() -> { 1640 editText2Ref.get().requestFocus(); 1641 editText2Ref.get() 1642 .getWindowInsetsController().show(WindowInsets.Type.ime()); 1643 }); 1644 expectEvent(stream, editorMatcher("onStartInputView", editText2Marker), TIMEOUT); 1645 expectImeVisible(TIMEOUT); 1646 }; 1647 testProcedureForTestActivity2.run(); 1648 1649 // Finish the primary activity to exit split-screen mode. 1650 splitPrimaryActivity.runOnUiThread(splitPrimaryActivity::finish); 1651 TestUtils.waitOnMainUntil(() -> { 1652 final View decorView = testActivity2.getWindow().getDecorView(); 1653 return decorView.hasWindowFocus() && decorView.getVisibility() == VISIBLE; 1654 }, TIMEOUT, "Activity should visible & focused when exiting split-screen mode"); 1655 1656 // Rerun the test procedure to ensure it passes after exiting split-screen mode. 1657 testProcedureForTestActivity2.run(); 1658 } 1659 } 1660 setAutoRotateScreen(boolean enable)1661 private void setAutoRotateScreen(boolean enable) { 1662 try { 1663 final Instrumentation instrumentation = mInstrumentation; 1664 SystemUtil.runShellCommand(instrumentation, enable ? ENABLE_AUTO_ROTATE_CMD : 1665 DISABLE_AUTO_ROTATE_CMD); 1666 instrumentation.waitForIdleSync(); 1667 } catch (IOException io) { 1668 fail("Couldn't enable/disable auto-rotate screen"); 1669 } 1670 } 1671 getFloatingImeSettings(@olorInt int navigationBarColor)1672 private static ImeSettings.Builder getFloatingImeSettings(@ColorInt int navigationBarColor) { 1673 final ImeSettings.Builder builder = new ImeSettings.Builder(); 1674 builder.setWindowFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 1675 // As documented, Window#setNavigationBarColor() is actually ignored when the IME window 1676 // does not have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS. We are calling setNavigationBarColor() 1677 // to ensure it. 1678 builder.setNavigationBarColor(navigationBarColor); 1679 return builder; 1680 } 1681 1682 /** 1683 * Whether enabling a compatibility flag to clear {@link InputMethodManager#SHOW_FORCED} flag 1684 * for the given {@code packageName} of the app when it's leaving. 1685 * 1686 * @return {@code true} if the compatibility flag is enabled. 1687 */ isClearShowForcedFlagEnabled(String packageName)1688 private static boolean isClearShowForcedFlagEnabled(String packageName) { 1689 AtomicBoolean result = new AtomicBoolean(); 1690 runWithShellPermissionIdentity(() -> result.set( 1691 CompatChanges.isChangeEnabled(CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING, packageName, 1692 UserHandle.CURRENT))); 1693 return result.get(); 1694 } 1695 1696 /** Whether the IME DisplayArea is organized by WM Shell. */ isImeOrganized(int displayId)1697 private static boolean isImeOrganized(int displayId) { 1698 final WindowManagerState wmState = new WindowManagerState(); 1699 wmState.computeState(); 1700 WindowManagerState.DisplayArea imeContainer = wmState.getImeContainer(displayId); 1701 assertNotNull("ImeContainer not found for display id: " + displayId, imeContainer); 1702 return imeContainer.isOrganized(); 1703 } 1704 createPopupWindowWrapper( @onNull EditText editor)1705 private static AutoCloseableWrapper<PopupWindow> createPopupWindowWrapper( 1706 @NonNull EditText editor) { 1707 return AutoCloseableWrapper.create( 1708 TestUtils.getOnMainSync(() -> { 1709 final PopupWindow popup = new PopupWindow(editor.getContext()); 1710 popup.setInputMethodMode(INPUT_METHOD_NOT_NEEDED); 1711 final TextView textView = new TextView(editor.getContext()); 1712 textView.setText("Popup"); 1713 popup.setContentView(textView); 1714 popup.setWidth(MATCH_PARENT); 1715 popup.setHeight(MATCH_PARENT); 1716 // Show the popup window. 1717 popup.showAsDropDown(textView); 1718 return popup; 1719 }), popup -> TestUtils.runOnMainSync(popup::dismiss)); 1720 } 1721 1722 /** 1723 * Whether the device has supported the recents screen. 1724 */ hasRecentsScreen()1725 private boolean hasRecentsScreen() { 1726 try { 1727 Context context = mInstrumentation.getContext(); 1728 return context.getResources().getBoolean( 1729 Resources.getSystem().getIdentifier("config_hasRecents", "bool", "android")); 1730 } catch (Resources.NotFoundException e) { 1731 return false; 1732 } 1733 } 1734 } 1735