1 /* 2 * Copyright (C) 2017 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.provider.InputMethodManagerDeviceConfig.KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS; 20 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 21 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; 22 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; 23 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; 24 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; 25 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED; 26 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; 27 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible; 28 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible; 29 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync; 30 import static android.view.inputmethod.cts.util.TestUtils.runOnMainSync; 31 import static android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil; 32 import static android.widget.PopupWindow.INPUT_METHOD_NEEDED; 33 import static android.widget.PopupWindow.INPUT_METHOD_NOT_NEEDED; 34 35 import static com.android.cts.input.injectinputinprocess.InjectInputInProcessKt.clickOnViewCenter; 36 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 37 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcherRestarting; 38 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcherRestartingFalse; 39 import static com.android.cts.mockime.ImeEventStreamTestUtils.eventMatcher; 40 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput; 41 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; 42 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 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.withDescription; 47 48 import static com.google.common.truth.Truth.assertThat; 49 50 import static org.junit.Assert.assertFalse; 51 import static org.junit.Assert.assertNotSame; 52 import static org.junit.Assert.assertTrue; 53 import static org.junit.Assert.fail; 54 55 import android.Manifest; 56 import android.app.Instrumentation; 57 import android.content.ComponentName; 58 import android.content.Context; 59 import android.content.Intent; 60 import android.content.ServiceConnection; 61 import android.content.pm.PackageManager; 62 import android.os.Build; 63 import android.os.Handler; 64 import android.os.HandlerThread; 65 import android.os.IBinder; 66 import android.os.Looper; 67 import android.os.Process; 68 import android.os.SystemClock; 69 import android.os.SystemProperties; 70 import android.platform.test.annotations.AppModeFull; 71 import android.platform.test.annotations.AppModeSdkSandbox; 72 import android.provider.DeviceConfig; 73 import android.text.TextUtils; 74 import android.view.KeyEvent; 75 import android.view.View; 76 import android.view.ViewGroup; 77 import android.view.ViewTreeObserver; 78 import android.view.WindowInsets; 79 import android.view.WindowManager; 80 import android.view.inputmethod.InputMethodManager; 81 import android.view.inputmethod.cts.util.AutoCloseableWrapper; 82 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 83 import android.view.inputmethod.cts.util.TestActivity; 84 import android.view.inputmethod.cts.util.TestActivity2; 85 import android.view.inputmethod.cts.util.TestUtils; 86 import android.view.inputmethod.cts.util.UnlockScreenRule; 87 import android.view.inputmethod.cts.util.WindowFocusHandleService; 88 import android.view.inputmethod.cts.util.WindowFocusStealer; 89 import android.widget.Button; 90 import android.widget.EditText; 91 import android.widget.LinearLayout; 92 import android.widget.PopupWindow; 93 import android.widget.TextView; 94 95 import androidx.annotation.NonNull; 96 import androidx.test.filters.FlakyTest; 97 import androidx.test.filters.MediumTest; 98 import androidx.test.platform.app.InstrumentationRegistry; 99 100 import com.android.compatibility.common.util.ApiTest; 101 import com.android.compatibility.common.util.SystemUtil; 102 import com.android.cts.input.UinputTouchScreen; 103 import com.android.cts.mockime.ImeCommand; 104 import com.android.cts.mockime.ImeEvent; 105 import com.android.cts.mockime.ImeEventStream; 106 import com.android.cts.mockime.ImeEventStreamTestUtils.DescribedPredicate; 107 import com.android.cts.mockime.ImeSettings; 108 import com.android.cts.mockime.MockImeSession; 109 110 import org.jetbrains.annotations.NotNull; 111 import org.junit.Assume; 112 import org.junit.Rule; 113 import org.junit.Test; 114 import org.testng.Assert; 115 116 import java.util.Objects; 117 import java.util.concurrent.BlockingQueue; 118 import java.util.concurrent.CountDownLatch; 119 import java.util.concurrent.LinkedBlockingQueue; 120 import java.util.concurrent.TimeUnit; 121 import java.util.concurrent.TimeoutException; 122 import java.util.concurrent.atomic.AtomicBoolean; 123 import java.util.concurrent.atomic.AtomicReference; 124 125 @MediumTest 126 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 127 public final class FocusHandlingTest extends EndToEndImeTestBase { 128 static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); 129 static final long EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(2); 130 static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(1); 131 132 @Rule 133 public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule(); 134 launchTestActivity(String marker)135 public EditText launchTestActivity(String marker) { 136 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 137 TestActivity.startSync(activity-> { 138 final LinearLayout layout = new LinearLayout(activity); 139 layout.setOrientation(LinearLayout.VERTICAL); 140 141 final EditText editText = new EditText(activity); 142 editText.setPrivateImeOptions(marker); 143 editText.setHint("editText"); 144 editText.requestFocus(); 145 editTextRef.set(editText); 146 147 layout.addView(editText); 148 return layout; 149 }); 150 return editTextRef.get(); 151 } 152 launchTestActivity(String marker, @NonNull AtomicBoolean outEditHasWindowFocusRef)153 public EditText launchTestActivity(String marker, 154 @NonNull AtomicBoolean outEditHasWindowFocusRef) { 155 final EditText editText = launchTestActivity(marker); 156 editText.post(() -> { 157 final ViewTreeObserver observerForEditText = editText.getViewTreeObserver(); 158 observerForEditText.addOnWindowFocusChangeListener((hasFocus) -> 159 outEditHasWindowFocusRef.set(editText.hasWindowFocus())); 160 outEditHasWindowFocusRef.set(editText.hasWindowFocus()); 161 }); 162 return editText; 163 } 164 165 @FlakyTest(bugId = 149246840) 166 @Test testOnStartInputCalledOnceIme()167 public void testOnStartInputCalledOnceIme() throws Exception { 168 try (MockImeSession imeSession = createTestImeSession()) { 169 final ImeEventStream stream = imeSession.openEventStream(); 170 171 final String marker = getTestMarker(); 172 launchTestActivity(marker); 173 174 // Wait until the MockIme gets bound to the TestActivity. 175 expectBindInput(stream, Process.myPid(), TIMEOUT); 176 177 // Wait until "onStartInput" gets called for the EditText. 178 final ImeEvent onStart = 179 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 180 181 assertFalse(stream.dump(), onStart.getEnterState().hasFallbackInputConnection()); 182 assertFalse(stream.dump(), onStart.getArguments().getBoolean("restarting")); 183 184 // There shouldn't be onStartInput any more. 185 notExpectEvent(stream, editorMatcherRestartingFalse("onStartInput", marker), 186 NOT_EXPECT_TIMEOUT); 187 } 188 } 189 190 @Test testSwitchingBetweenEquivalentNonEditableViews()191 public void testSwitchingBetweenEquivalentNonEditableViews() throws Exception { 192 // When avoidable IME prevention is enabled, the onStartInput calls do not happen 193 // TODO(b/240260832): Refine the assumption when testing a non-preemptible IME. 194 Assume.assumeFalse(isPreventImeStartup()); 195 try (MockImeSession imeSession = createTestImeSession()) { 196 final ImeEventStream stream = imeSession.openEventStream(); 197 198 // "onStartInput" with a fallback InputConnection for StateInitializeActivity. 199 // "debug.imm.optimize_noneditable_views" doesn't prevent startInput when it has 200 // WINDOW_GAINED_FOCUS flag. 201 expectEvent(stream, startInputWithFallbackInputConnectionMatcher(), EXPECT_TIMEOUT); 202 203 final AtomicReference<TextView> viewRef1 = new AtomicReference<>(); 204 final AtomicReference<TextView> viewRef2 = new AtomicReference<>(); 205 final BlockingQueue<KeyEvent> keyEvents = new LinkedBlockingQueue<>(); 206 207 final TestActivity testActivity = TestActivity.startSync(activity -> { 208 final LinearLayout layout = new LinearLayout(activity); 209 layout.setOrientation(LinearLayout.VERTICAL); 210 211 final TextView view1 = new Button(activity); 212 view1.setText("View 1"); 213 layout.addView(view1); 214 view1.setFocusableInTouchMode(true); 215 viewRef1.set(view1); 216 217 final TextView view2 = new Button(activity) { 218 @Override 219 public boolean dispatchKeyEvent(KeyEvent event) { 220 keyEvents.add(event); 221 return super.dispatchKeyEvent(event); 222 } 223 }; 224 view2.setText("View 2"); 225 layout.addView(view2); 226 view2.setFocusableInTouchMode(true); 227 viewRef2.set(view2); 228 229 return layout; 230 }); 231 232 // "onStartInput" with a fallback InputConnection for TestActivity. 233 // "debug.imm.optimize_noneditable_views" doesn't prevent startInput when it has 234 // WINDOW_GAINED_FOCUS flag. 235 expectEvent(stream, startInputWithFallbackInputConnectionMatcher(), EXPECT_TIMEOUT); 236 237 // The focus change below still triggers "onStartInput". 238 // "debug.imm.optimize_noneditable_views" doesn't prevent startInput when 239 // StartInputReason is different. 240 testActivity.runOnUiThread(() -> viewRef1.get().requestFocus()); 241 expectEvent(stream, startInputWithFallbackInputConnectionMatcher(), EXPECT_TIMEOUT); 242 243 // If optimization is enabled, we do not expect another call to "onStartInput" after 244 // a view focus change. 245 testActivity.runOnUiThread(() -> viewRef2.get().requestFocus()); 246 if (SystemProperties.getBoolean("debug.imm.optimize_noneditable_views", true)) { 247 notExpectEvent(stream, startInputWithFallbackInputConnectionMatcher(), NOT_EXPECT_TIMEOUT); 248 } else { 249 expectEvent(stream, startInputWithFallbackInputConnectionMatcher(), EXPECT_TIMEOUT); 250 } 251 252 // Force show the IME and expect it to come up 253 testActivity.runOnUiThread(() -> 254 viewRef1.get().getWindowInsetsController().show(WindowInsets.Type.ime())); 255 expectEvent(stream, showSoftInputMatcher(0), EXPECT_TIMEOUT); 256 257 final String testInput = "Test"; 258 final ImeCommand commitText = imeSession.callCommitText(testInput, 0); 259 expectCommand(stream, commitText, EXPECT_TIMEOUT); 260 261 waitOnMainUntil(() -> !keyEvents.isEmpty(), EXPECT_TIMEOUT); 262 Assert.assertEquals(keyEvents.size(), 1, "Expecting exactly one key event!"); 263 final KeyEvent keyEvent = keyEvents.remove(); 264 Assert.assertEquals(keyEvent.getAction(), KeyEvent.ACTION_MULTIPLE); 265 Assert.assertEquals(keyEvent.getKeyCode(), KeyEvent.KEYCODE_UNKNOWN); 266 Assert.assertEquals(keyEvent.getCharacters(), testInput); 267 } 268 } 269 270 @NotNull startInputWithFallbackInputConnectionMatcher()271 private static DescribedPredicate<ImeEvent> startInputWithFallbackInputConnectionMatcher() { 272 return withDescription("onStartInput(hasFallbackInputConnection=true)", 273 event -> "onStartInput".equals(event.getEventName()) 274 && event.getEnterState().hasFallbackInputConnection()); 275 } 276 277 @Test testSoftInputStateAlwaysVisibleWithoutFocusedEditorView()278 public void testSoftInputStateAlwaysVisibleWithoutFocusedEditorView() throws Exception { 279 try (MockImeSession imeSession = createTestImeSession()) { 280 final ImeEventStream stream = imeSession.openEventStream(); 281 282 final String marker = getTestMarker(); 283 final TestActivity testActivity = TestActivity.startSync(activity -> { 284 final LinearLayout layout = new LinearLayout(activity); 285 layout.setOrientation(LinearLayout.VERTICAL); 286 287 final TextView textView = new TextView(activity) { 288 @Override 289 public boolean onCheckIsTextEditor() { 290 return false; 291 } 292 }; 293 textView.setText("textView"); 294 textView.setPrivateImeOptions(marker); 295 textView.requestFocus(); 296 297 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 298 layout.addView(textView); 299 return layout; 300 }); 301 302 if (testActivity.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) { 303 // Input shouldn't start 304 notExpectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 305 // There shouldn't be onStartInput because the focused view is not an editor. 306 notExpectEvent(stream, showSoftInputMatcher(0), 307 TIMEOUT); 308 } else { 309 // Wait until the MockIme gets bound to the TestActivity. 310 expectBindInput(stream, Process.myPid(), TIMEOUT); 311 // For apps that target pre-P devices, onStartInput() should be called. 312 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 313 } 314 } 315 } 316 317 @Test testNoEditorNoStartInput()318 public void testNoEditorNoStartInput() throws Exception { 319 Assume.assumeTrue(isPreventImeStartup()); 320 try (MockImeSession imeSession = createTestImeSession()) { 321 final ImeEventStream stream = imeSession.openEventStream(); 322 323 final String marker = getTestMarker(); 324 TestActivity.startSync(activity -> { 325 final LinearLayout layout = new LinearLayout(activity); 326 layout.setOrientation(LinearLayout.VERTICAL); 327 328 final TextView textView = new TextView(activity) { 329 @Override 330 public boolean onCheckIsTextEditor() { 331 return false; 332 } 333 }; 334 textView.setText("textView"); 335 textView.requestFocus(); 336 textView.setPrivateImeOptions(marker); 337 layout.addView(textView); 338 return layout; 339 }); 340 341 // Input shouldn't start 342 notExpectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 343 } 344 } 345 346 @Test testDelayedAddEditorStartsInput()347 public void testDelayedAddEditorStartsInput() throws Exception { 348 Assume.assumeTrue(isPreventImeStartup()); 349 try (MockImeSession imeSession = createTestImeSession()) { 350 final ImeEventStream stream = imeSession.openEventStream(); 351 352 final AtomicReference<LinearLayout> layoutRef = new AtomicReference<>(); 353 final TestActivity testActivity = TestActivity.startSync(activity -> { 354 final LinearLayout layout = new LinearLayout(activity); 355 layout.setOrientation(LinearLayout.VERTICAL); 356 layoutRef.set(layout); 357 358 return layout; 359 }); 360 361 // Activity adds EditText at a later point. 362 TestUtils.waitOnMainUntil(() -> layoutRef.get().hasWindowFocus(), TIMEOUT); 363 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 364 final String marker = getTestMarker(); 365 testActivity.runOnUiThread(() -> { 366 final EditText editText = new EditText(testActivity); 367 editText.setText("Editable"); 368 editText.setPrivateImeOptions(marker); 369 layoutRef.get().addView(editText); 370 editText.requestFocus(); 371 }); 372 373 // Input should start 374 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 375 } 376 } 377 378 @Test testEditorStartsInput()379 public void testEditorStartsInput() throws Exception { 380 try (MockImeSession imeSession = createTestImeSession()) { 381 final ImeEventStream stream = imeSession.openEventStream(); 382 383 final String marker = getTestMarker(); 384 TestActivity.startSync(activity -> { 385 final LinearLayout layout = new LinearLayout(activity); 386 layout.setOrientation(LinearLayout.VERTICAL); 387 388 final EditText editText = new EditText(activity); 389 editText.setPrivateImeOptions(marker); 390 editText.setText("Editable"); 391 editText.requestFocus(); 392 layout.addView(editText); 393 return layout; 394 }); 395 396 // Input should start 397 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 398 } 399 } 400 401 @Test testSoftInputStateAlwaysVisibleFocusedEditorView()402 public void testSoftInputStateAlwaysVisibleFocusedEditorView() throws Exception { 403 try (MockImeSession imeSession = createTestImeSession()) { 404 final ImeEventStream stream = imeSession.openEventStream(); 405 406 TestActivity.startSync(activity -> { 407 final LinearLayout layout = new LinearLayout(activity); 408 layout.setOrientation(LinearLayout.VERTICAL); 409 410 final EditText editText = new EditText(activity); 411 editText.setText("editText"); 412 editText.requestFocus(); 413 414 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 415 layout.addView(editText); 416 return layout; 417 }); 418 419 // Wait until the MockIme gets bound to the TestActivity. 420 expectBindInput(stream, Process.myPid(), TIMEOUT); 421 422 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 423 } 424 } 425 426 @ApiTest(apis = {"android.inputmethodservice.InputMethodService#showSoftInput"}) 427 @FlakyTest 428 @Test testSoftInputStateAlwaysVisibleFocusEditorAfterLaunch()429 public void testSoftInputStateAlwaysVisibleFocusEditorAfterLaunch() throws Exception { 430 Assume.assumeFalse(isPreventImeStartup()); 431 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 432 try (MockImeSession imeSession = createTestImeSession()) { 433 final ImeEventStream stream = imeSession.openEventStream(); 434 435 // Launch a test activity with STATE_ALWAYS_VISIBLE without requesting editor focus. 436 AtomicReference<EditText> editTextRef = new AtomicReference<>(); 437 TestActivity.startSync(activity -> { 438 final LinearLayout layout = new LinearLayout(activity); 439 layout.setOrientation(LinearLayout.VERTICAL); 440 441 final EditText editText = new EditText(activity); 442 editTextRef.set(editText); 443 editText.setText("editText"); 444 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 445 layout.addView(editText); 446 return layout; 447 }); 448 449 // Wait until the MockIme gets bound to the TestActivity. 450 expectBindInput(stream, Process.myPid(), TIMEOUT); 451 452 // Not expect showSoftInput called when the editor not yet focused. 453 notExpectEvent(stream, showSoftInputMatcher(0), 454 NOT_EXPECT_TIMEOUT); 455 456 // Expect showSoftInput called when the editor is focused. 457 instrumentation.runOnMainSync(editTextRef.get()::requestFocus); 458 clickOnViewCenter(editTextRef.get()); 459 assertTrue(TestUtils.getOnMainSync(() -> editTextRef.get().hasFocus() 460 && editTextRef.get().hasWindowFocus())); 461 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 462 } 463 } 464 465 /** 466 * Makes sure that an existing {@link android.view.inputmethod.InputConnection} will not be 467 * invalidated by showing a focusable {@link PopupWindow} with 468 * {@link PopupWindow#INPUT_METHOD_NOT_NEEDED}. 469 * 470 * <p>If {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} is set and 471 * {@link android.view.WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} is not set to a 472 * {@link android.view.Window}, showing that window must not invalidate an existing valid 473 * {@link android.view.inputmethod.InputConnection}.</p> 474 * 475 * @see android.view.WindowManager.LayoutParams#mayUseInputMethod(int) 476 */ 477 @Test testFocusableWindowDoesNotInvalidateExistingInputConnection()478 public void testFocusableWindowDoesNotInvalidateExistingInputConnection() throws Exception { 479 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 480 try (MockImeSession imeSession = createTestImeSession()) { 481 final ImeEventStream stream = imeSession.openEventStream(); 482 483 final String marker1 = getTestMarker(FIRST_EDIT_TEXT_TAG); 484 final EditText editText = launchTestActivity(marker1); 485 instrumentation.runOnMainSync(editText::requestFocus); 486 487 // Wait until the MockIme gets bound to the TestActivity. 488 expectBindInput(stream, Process.myPid(), TIMEOUT); 489 490 expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT); 491 492 // Make sure that InputConnection#commitText() works. 493 final ImeCommand commit1 = imeSession.callCommitText("test commit", 1); 494 expectCommand(stream, commit1, TIMEOUT); 495 TestUtils.waitOnMainUntil( 496 () -> TextUtils.equals(editText.getText(), "test commit"), TIMEOUT); 497 instrumentation.runOnMainSync(() -> editText.setText("")); 498 499 // Create then show a popup window that cannot be the IME target. 500 try (AutoCloseableWrapper<PopupWindow> popupWindowWrapper = AutoCloseableWrapper.create( 501 TestUtils.getOnMainSync(() -> { 502 final Context context = instrumentation.getTargetContext(); 503 final PopupWindow popup = new PopupWindow(context); 504 popup.setFocusable(true); 505 popup.setInputMethodMode(INPUT_METHOD_NOT_NEEDED); 506 final TextView textView = new TextView(context); 507 textView.setText("Test Text"); 508 popup.setContentView(textView); 509 popup.showAsDropDown(editText); 510 return popup; 511 }), popupWindow -> runOnMainSync(popupWindow::dismiss)) 512 ) { 513 instrumentation.waitForIdleSync(); 514 515 // Make sure that the EditText no longer has window-focus 516 TestUtils.waitOnMainUntil(() -> !editText.hasWindowFocus(), TIMEOUT); 517 518 // Make sure that InputConnection#commitText() works. 519 final ImeCommand commit2 = imeSession.callCommitText("Hello!", 1); 520 expectCommand(stream, commit2, TIMEOUT); 521 TestUtils.waitOnMainUntil( 522 () -> TextUtils.equals(editText.getText(), "Hello!"), TIMEOUT); 523 instrumentation.runOnMainSync(() -> editText.setText("")); 524 525 stream.skipAll(); 526 527 final String marker2 = getTestMarker(SECOND_EDIT_TEXT_TAG); 528 // Call InputMethodManager#restartInput() 529 instrumentation.runOnMainSync(() -> { 530 editText.setPrivateImeOptions(marker2); 531 editText.getContext() 532 .getSystemService(InputMethodManager.class) 533 .restartInput(editText); 534 }); 535 536 // Make sure that onStartInput() is called with restarting == true. 537 expectEvent(stream, editorMatcherRestarting("onStartInput", marker2, true), 538 TIMEOUT); 539 540 // Make sure that InputConnection#commitText() works. 541 final ImeCommand commit3 = imeSession.callCommitText("World!", 1); 542 expectCommand(stream, commit3, TIMEOUT); 543 TestUtils.waitOnMainUntil( 544 () -> TextUtils.equals(editText.getText(), "World!"), TIMEOUT); 545 instrumentation.runOnMainSync(() -> editText.setText("")); 546 } 547 548 instrumentation.waitForIdleSync(); 549 550 // Make sure that the EditText now has window-focus again. 551 TestUtils.waitOnMainUntil(editText::hasWindowFocus, TIMEOUT); 552 553 // Make sure that InputConnection#commitText() works. 554 final ImeCommand commit4 = imeSession.callCommitText("Done!", 1); 555 expectCommand(stream, commit4, TIMEOUT); 556 TestUtils.waitOnMainUntil( 557 () -> TextUtils.equals(editText.getText(), "Done!"), TIMEOUT); 558 instrumentation.runOnMainSync(() -> editText.setText("")); 559 } 560 } 561 562 /** 563 * Test case for Bug 152698568. 564 * 565 * <p>This test ensures that showing a non-focusable {@link PopupWindow} with 566 * {@link PopupWindow#INPUT_METHOD_NEEDED} does not affect IME visibility.</p> 567 */ 568 @Test testNonFocusablePopupWindowDoesNotAffectImeVisibility()569 public void testNonFocusablePopupWindowDoesNotAffectImeVisibility() throws Exception { 570 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 571 try (MockImeSession imeSession = createTestImeSession()) { 572 final ImeEventStream stream = imeSession.openEventStream(); 573 574 final String marker = getTestMarker(); 575 final EditText editText = launchTestActivity(marker); 576 577 // Wait until the MockIme is connected to the edit text. 578 runOnMainSync(editText::requestFocus); 579 expectBindInput(stream, Process.myPid(), TIMEOUT); 580 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 581 582 expectImeInvisible(TIMEOUT); 583 584 // Show IME. 585 runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class) 586 .showSoftInput(editText, 0)); 587 588 expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT); 589 expectImeVisible(TIMEOUT); 590 591 // Create then show a non-focusable PopupWindow with INPUT_METHOD_NEEDED. 592 try (AutoCloseableWrapper<PopupWindow> popupWindowWrapper = AutoCloseableWrapper.create( 593 TestUtils.getOnMainSync(() -> { 594 final Context context = instrumentation.getTargetContext(); 595 final PopupWindow popup = new PopupWindow(context); 596 popup.setFocusable(false); 597 popup.setInputMethodMode(INPUT_METHOD_NEEDED); 598 final TextView textView = new TextView(context); 599 textView.setText("Popup"); 600 popup.setContentView(textView); 601 // Show the popup window. 602 popup.showAsDropDown(editText); 603 return popup; 604 }), popup -> TestUtils.runOnMainSync(popup::dismiss)) 605 ) { 606 instrumentation.waitForIdleSync(); 607 608 // Make sure that the IME remains to be visible. 609 expectImeVisible(TIMEOUT); 610 611 SystemClock.sleep(NOT_EXPECT_TIMEOUT); 612 613 // Make sure that the IME remains to be visible. 614 expectImeVisible(TIMEOUT); 615 } 616 } 617 } 618 619 /** 620 * Test case for Bug 70629102. 621 * 622 * {@link InputMethodManager#restartInput(View)} can be called even when another process 623 * temporarily owns focused window. {@link InputMethodManager} should continue to work after 624 * the IME target application gains window focus again. 625 */ 626 @Test testRestartInputWhileOtherProcessHasWindowFocus()627 public void testRestartInputWhileOtherProcessHasWindowFocus() throws Exception { 628 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 629 try (MockImeSession imeSession = createTestImeSession()) { 630 final ImeEventStream stream = imeSession.openEventStream(); 631 632 final String marker = getTestMarker(); 633 final EditText editText = launchTestActivity(marker); 634 instrumentation.runOnMainSync(editText::requestFocus); 635 636 // Wait until the MockIme gets bound to the TestActivity. 637 expectBindInput(stream, Process.myPid(), TIMEOUT); 638 639 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 640 641 // Get app window token 642 final IBinder appWindowToken = TestUtils.getOnMainSync( 643 editText::getApplicationWindowToken); 644 645 try (WindowFocusStealer focusStealer = 646 WindowFocusStealer.connect(instrumentation.getTargetContext(), TIMEOUT)) { 647 648 focusStealer.stealWindowFocus(appWindowToken, TIMEOUT); 649 650 // Wait until the edit text loses window focus. 651 TestUtils.waitOnMainUntil(() -> !editText.hasWindowFocus(), TIMEOUT); 652 653 // Call InputMethodManager#restartInput() 654 instrumentation.runOnMainSync(() -> { 655 editText.getContext() 656 .getSystemService(InputMethodManager.class) 657 .restartInput(editText); 658 }); 659 } 660 661 // Wait until the edit text gains window focus again. 662 TestUtils.waitOnMainUntil(editText::hasWindowFocus, TIMEOUT); 663 664 // Make sure that InputConnection#commitText() still works. 665 final ImeCommand command = imeSession.callCommitText("test commit", 1); 666 expectCommand(stream, command, TIMEOUT); 667 668 TestUtils.waitOnMainUntil( 669 () -> TextUtils.equals(editText.getText(), "test commit"), TIMEOUT); 670 } 671 } 672 673 /** 674 * Test {@link EditText#setShowSoftInputOnFocus(boolean)}. 675 */ 676 @Test testSetShowInputOnFocus()677 public void testSetShowInputOnFocus() throws Exception { 678 try (MockImeSession imeSession = createTestImeSession()) { 679 final ImeEventStream stream = imeSession.openEventStream(); 680 681 final String marker = getTestMarker(); 682 final EditText editText = launchTestActivity(marker); 683 runOnMainSync(() -> editText.setShowSoftInputOnFocus(false)); 684 685 // Wait until "onStartInput" gets called for the EditText. 686 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 687 688 clickOnViewCenter(editText); 689 690 // "showSoftInput" must not happen when setShowSoftInputOnFocus(false) is called. 691 notExpectEvent(stream, showSoftInputMatcher(0), 692 NOT_EXPECT_TIMEOUT); 693 } 694 } 695 696 @AppModeFull(reason = "Instant apps cannot hold android.permission.SYSTEM_ALERT_WINDOW") 697 @Test testMultiWindowFocusHandleOnDifferentUiThread()698 public void testMultiWindowFocusHandleOnDifferentUiThread() throws Exception { 699 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 700 try (CloseOnce session = CloseOnce.of(new ServiceSession(instrumentation)); 701 MockImeSession imeSession = createTestImeSession()) { 702 final ImeEventStream stream = imeSession.openEventStream(); 703 final AtomicBoolean popupTextHasWindowFocus = new AtomicBoolean(false); 704 final AtomicBoolean popupTextHasViewFocus = new AtomicBoolean(false); 705 final AtomicBoolean editTextHasWindowFocus = new AtomicBoolean(false); 706 707 // Start a TestActivity and verify the edit text will receive focus and keyboard shown. 708 final String marker1 = getTestMarker(FIRST_EDIT_TEXT_TAG); 709 final EditText editText = launchTestActivity(marker1, editTextHasWindowFocus); 710 711 // Wait until the MockIme gets bound to the TestActivity. 712 expectBindInput(stream, Process.myPid(), TIMEOUT); 713 714 final var display = editText.getContext().getDisplay(); 715 /* 716 * Since this test relies on window focus with multiple windows involved, we need to 717 * use a global method of emulating touch that goes through the entire pipeline. This 718 * ensures that the window manager is aware of the tap that occurred, and provides 719 * window focus to the tapped window. 720 */ 721 try (var touch = new UinputTouchScreen(instrumentation, display)) { 722 touch.tapOnViewCenter(editText); 723 TestUtils.waitOnMainUntil(editTextHasWindowFocus::get, TIMEOUT); 724 725 expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT); 726 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 727 728 // Create a popupTextView which from Service with different UI thread. 729 final ServiceSession serviceSession = (ServiceSession) session.mAutoCloseable; 730 final EditText popupTextView = serviceSession.getService().getPopupTextView( 731 popupTextHasWindowFocus); 732 assertNotSame(popupTextView.getHandler().getLooper(), 733 serviceSession.getService().getMainLooper()); 734 735 // Verify popupTextView will also receive window focus change and soft keyboard 736 // shown after tapping the view. 737 final String marker2 = getTestMarker(SECOND_EDIT_TEXT_TAG); 738 popupTextView.post(() -> { 739 popupTextView.setPrivateImeOptions(marker2); 740 popupTextHasViewFocus.set(popupTextView.requestFocus()); 741 }); 742 TestUtils.waitOnMainUntil(popupTextHasViewFocus::get, TIMEOUT); 743 744 touch.tapOnViewCenter(popupTextView); 745 TestUtils.waitOnMainUntil(() -> popupTextHasWindowFocus.get() 746 && !editTextHasWindowFocus.get(), TIMEOUT); 747 expectEvent(stream, editorMatcher("onStartInput", marker2), TIMEOUT); 748 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 749 750 // Tap editText again, verify soft keyboard and window focus will come back. 751 touch.tapOnViewCenter(editText); 752 TestUtils.waitOnMainUntil(() -> editTextHasWindowFocus.get() 753 && !popupTextHasWindowFocus.get(), TIMEOUT); 754 expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT); 755 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 756 757 // Remove the popTextView window and back to test activity, and then verify if 758 // commitText is still workable. 759 session.close(); 760 TestUtils.waitOnMainUntil(editText::hasWindowFocus, TIMEOUT); 761 final ImeCommand commit = imeSession.callCommitText("test commit", 1); 762 expectCommand(stream, commit, TIMEOUT); 763 TestUtils.waitOnMainUntil( 764 () -> TextUtils.equals(editText.getText(), "test commit"), TIMEOUT); 765 } 766 } 767 } 768 769 @Test testKeyboardStateAfterImeFocusableFlagChanged()770 public void testKeyboardStateAfterImeFocusableFlagChanged() throws Exception { 771 try (MockImeSession imeSession = createTestImeSession()) { 772 final ImeEventStream stream = imeSession.openEventStream(); 773 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 774 final String marker = getTestMarker(); 775 final TestActivity testActivity = TestActivity.startSync(activity-> { 776 // Initially set activity window to not IME focusable. 777 activity.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); 778 779 final LinearLayout layout = new LinearLayout(activity); 780 layout.setOrientation(LinearLayout.VERTICAL); 781 782 final EditText editText = new EditText(activity); 783 editText.setPrivateImeOptions(marker); 784 editText.setHint("editText"); 785 editTextRef.set(editText); 786 editText.requestFocus(); 787 788 layout.addView(editText); 789 return layout; 790 }); 791 792 // Tap editText, expect there is no "onStartInput", and "showSoftInput" happened. 793 final EditText editText = editTextRef.get(); 794 clickOnViewCenter(editText); 795 notExpectEvent(stream, editorMatcher("onStartInput", marker), NOT_EXPECT_TIMEOUT); 796 notExpectEvent(stream, showSoftInputMatcher(0), 797 NOT_EXPECT_TIMEOUT); 798 799 // Set testActivity window to be IME focusable. 800 testActivity.getWindow().getDecorView().post(() -> { 801 testActivity.getWindow().clearFlags(FLAG_ALT_FOCUSABLE_IM); 802 editTextRef.get().requestFocus(); 803 }); 804 805 // Make sure test activity's window has changed to be IME focusable. 806 TestUtils.waitOnMainUntil(() -> WindowManager.LayoutParams.mayUseInputMethod( 807 testActivity.getWindow().getAttributes().flags), TIMEOUT); 808 809 // Tap editText for the second time. 810 clickOnViewCenter(editText); 811 assertTrue(TestUtils.getOnMainSync(() -> editText.hasFocus() 812 && editText.hasWindowFocus())); 813 814 // "onStartInput", and "showSoftInput" must happen when editText became IME focusable. 815 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 816 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 817 } 818 } 819 820 @AppModeFull(reason = "Instant apps cannot hold android.permission.SYSTEM_ALERT_WINDOW") 821 @Test testOnCheckIsTextEditorRunOnUIThread()822 public void testOnCheckIsTextEditorRunOnUIThread() throws Exception { 823 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 824 final CountDownLatch uiThreadSignal = new CountDownLatch(1); 825 try (CloseOnce session = CloseOnce.of(new ServiceSession(instrumentation))) { 826 final AtomicBoolean popupTextHasWindowFocus = new AtomicBoolean(false); 827 828 // Create a popupTextView which from Service with different UI thread and set a 829 // countDownLatch to verify onCheckIsTextEditor run on UI thread. 830 final ServiceSession serviceSession = (ServiceSession) session.mAutoCloseable; 831 serviceSession.getService().setUiThreadSignal(uiThreadSignal); 832 final EditText popupTextView = serviceSession.getService().getPopupTextView( 833 popupTextHasWindowFocus); 834 assertTrue(popupTextView.getHandler().getLooper() 835 != serviceSession.getService().getMainLooper()); 836 837 clickOnViewCenter(popupTextView); 838 839 // Wait until the UI thread countDownLatch reach to 0 or timeout 840 assertTrue(uiThreadSignal.await(EXPECT_TIMEOUT, TimeUnit.MILLISECONDS)); 841 } 842 } 843 844 /** 845 * Make sure that {@link View#isInEditMode()} will never get called on a non-UI thread even if 846 * {@link InputMethodManager#isActive()} is called on a background thread. 847 * 848 * <p>This is basically a regression test for b/286016109.</p> 849 */ 850 @Test testOnCheckIsTextEditorRunOnUIThreadWithInputMethodManagerIsActive()851 public void testOnCheckIsTextEditorRunOnUIThreadWithInputMethodManagerIsActive() 852 throws Exception { 853 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 854 try (MockImeSession imeSession = MockImeSession.create( 855 instrumentation.getContext(), 856 instrumentation.getUiAutomation(), 857 new ImeSettings.Builder())) { 858 final ImeEventStream stream = imeSession.openEventStream(); 859 final String marker1 = getTestMarker(FIRST_EDIT_TEXT_TAG); 860 final AtomicReference<LinearLayout> layoutRef = new AtomicReference<>(); 861 862 // Launch test activity 863 TestActivity.startSync(activity -> { 864 final LinearLayout layout = new LinearLayout(activity); 865 layout.setOrientation(LinearLayout.VERTICAL); 866 final EditText editText = new EditText(activity); 867 editText.setPrivateImeOptions(marker1); 868 editText.setHint("editText"); 869 layoutRef.set(layout); 870 layout.addView(editText); 871 872 editText.requestFocus(); 873 return layout; 874 }); 875 876 // "onStartInput" gets called for the EditText. 877 expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT); 878 879 final HandlerThread backgroundThread = new HandlerThread("testthread"); 880 backgroundThread.start(); 881 882 final AtomicBoolean nonUiThreadCallMade = new AtomicBoolean(false); 883 final CountDownLatch latch = new CountDownLatch(1); 884 final String marker2 = getTestMarker(SECOND_EDIT_TEXT_TAG); 885 runOnMainSync(() -> { 886 final LinearLayout layout = layoutRef.get(); 887 final EditText editText2 = new EditText(layout.getContext()) { 888 @Override 889 public boolean onCheckIsTextEditor() { 890 if (!Looper.getMainLooper().isCurrentThread()) { 891 nonUiThreadCallMade.set(true); 892 } 893 return super.onCheckIsTextEditor(); 894 } 895 }; 896 editText2.setPrivateImeOptions(marker2); 897 layout.addView(editText2); 898 editText2.requestFocus(); 899 900 final InputMethodManager imm = 901 Objects.requireNonNull( 902 layout.getContext().getSystemService(InputMethodManager.class)); 903 Handler.createAsync(backgroundThread.getLooper()).post(() -> { 904 // IMM#isActive() is known to have side effect to trigger startInput(). 905 // Do this on a background thread to emulate b/286016109 906 imm.isActive(); 907 latch.countDown(); 908 }); 909 }); 910 backgroundThread.quitSafely(); 911 assertTrue(latch.await(TIMEOUT, TimeUnit.MILLISECONDS)); 912 913 expectEvent(stream, editorMatcher("onStartInput", marker2), TIMEOUT); 914 assertFalse(nonUiThreadCallMade.get()); 915 } 916 } 917 918 @Test testRequestFocusOnWindowFocusChanged()919 public void testRequestFocusOnWindowFocusChanged() throws Exception { 920 try (MockImeSession imeSession = createTestImeSession()) { 921 final ImeEventStream stream = imeSession.openEventStream(); 922 final String marker = getTestMarker(); 923 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 924 925 // Launch test activity 926 TestActivity.startSync(activity -> { 927 final LinearLayout layout = new LinearLayout(activity); 928 layout.setOrientation(LinearLayout.VERTICAL); 929 930 final EditText editText = new EditText(activity); 931 editText.setPrivateImeOptions(marker); 932 editText.setHint("editText"); 933 934 // Request focus when onWindowFocusChanged 935 final ViewTreeObserver observer = editText.getViewTreeObserver(); 936 observer.addOnWindowFocusChangeListener( 937 new ViewTreeObserver.OnWindowFocusChangeListener() { 938 @Override 939 public void onWindowFocusChanged(boolean hasFocus) { 940 editText.requestFocus(); 941 } 942 }); 943 editTextRef.set(editText); 944 layout.addView(editText); 945 return layout; 946 }); 947 948 final EditText editText = editTextRef.get(); 949 clickOnViewCenter(editText); 950 951 // "onStartInput" and "showSoftInput" gets called for the EditText. 952 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 953 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 954 955 // No "hideSoftInput" happened 956 notExpectEvent(stream, hideSoftInputMatcher(), NOT_EXPECT_TIMEOUT); 957 } 958 } 959 960 /** 961 * Start an activity with a focused test editor and wait for the IME to become visible, 962 * then start another activity with the given {@code softInputMode} and an <b>unfocused</b> 963 * test editor. 964 * 965 * @return the event stream positioned before the second app is launched 966 */ startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( int softInputMode)967 private ImeEventStream startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 968 int softInputMode) 969 throws Exception { 970 try (MockImeSession imeSession = createTestImeSession()) { 971 final String marker = getTestMarker(); 972 973 // Launch an activity with a text edit and request focus 974 TestActivity.startSync(activity -> { 975 final LinearLayout layout = new LinearLayout(activity); 976 layout.setOrientation(LinearLayout.VERTICAL); 977 978 final EditText editText = new EditText(activity); 979 editText.setText("editText"); 980 editText.setPrivateImeOptions(marker); 981 editText.requestFocus(); 982 983 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 984 layout.addView(editText); 985 return layout; 986 }); 987 988 ImeEventStream stream = imeSession.openEventStream(); 989 990 // Wait until the MockIme gets bound and started for the TestActivity. 991 expectBindInput(stream, Process.myPid(), TIMEOUT); 992 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 993 expectImeVisible(TIMEOUT); 994 995 // Skip events relating to showStateInitializeActivity() and TestActivity1 996 stream.skipAll(); 997 998 // Launch another activity without a text edit but with the requested softInputMode set 999 TestActivity2.startSync(activity -> { 1000 activity.getWindow().setSoftInputMode(softInputMode); 1001 1002 final LinearLayout layout = new LinearLayout(activity); 1003 layout.setOrientation(LinearLayout.VERTICAL); 1004 1005 final EditText editText = new EditText(activity); 1006 // Do not request focus for the editText 1007 editText.setText("Unfocused editText"); 1008 layout.addView(editText); 1009 return layout; 1010 }); 1011 1012 return stream; 1013 } 1014 } 1015 1016 @Test testUnfocusedEditor_stateUnspecified_hidesIme()1017 public void testUnfocusedEditor_stateUnspecified_hidesIme() throws Exception { 1018 ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 1019 SOFT_INPUT_STATE_UNSPECIFIED); 1020 expectEvent(stream, hideSoftInputMatcher(), EXPECT_TIMEOUT); 1021 expectEvent(stream, eventMatcher("onFinishInput"), EXPECT_TIMEOUT); 1022 } 1023 1024 @Test testUnfocusedEditor_stateHidden_hidesIme()1025 public void testUnfocusedEditor_stateHidden_hidesIme() throws Exception { 1026 Assume.assumeFalse(isPreventImeStartup()); 1027 ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 1028 SOFT_INPUT_STATE_HIDDEN); 1029 expectEvent(stream, hideSoftInputMatcher(), EXPECT_TIMEOUT); 1030 expectEvent(stream, eventMatcher("onFinishInput"), EXPECT_TIMEOUT); 1031 } 1032 1033 @Test testUnfocusedEditor_stateAlwaysHidden_hidesIme()1034 public void testUnfocusedEditor_stateAlwaysHidden_hidesIme() throws Exception { 1035 Assume.assumeFalse(isPreventImeStartup()); 1036 ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 1037 SOFT_INPUT_STATE_ALWAYS_HIDDEN); 1038 expectEvent(stream, hideSoftInputMatcher(), EXPECT_TIMEOUT); 1039 expectEvent(stream, eventMatcher("onFinishInput"), EXPECT_TIMEOUT); 1040 } 1041 1042 @Test 1043 @ApiTest(apis = {"android.inputmethodservice.InputMethodService#onStartInput", 1044 "android.inputmethodservice.InputMethodService#showSoftInput"}) testUnfocusedEditor_stateVisible()1045 public void testUnfocusedEditor_stateVisible() throws Exception { 1046 Assume.assumeFalse(isPreventImeStartup()); 1047 ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 1048 SOFT_INPUT_STATE_VISIBLE); 1049 // The previous IME should be finished 1050 expectEvent(stream, eventMatcher("onFinishInput"), EXPECT_TIMEOUT); 1051 1052 // Input should be started 1053 expectEvent(stream, eventMatcher("onStartInput"), EXPECT_TIMEOUT); 1054 1055 final boolean willHideIme = willHideImeWhenNoEditorFocus(); 1056 if (willHideIme) { 1057 // The keyboard will not expected to show when focusing the app set STATE_VISIBLE 1058 // without an editor from the IME shown activity 1059 notExpectEvent(stream, showSoftInputMatcher(0), 1060 NOT_EXPECT_TIMEOUT); 1061 } else { 1062 expectEvent(stream, showSoftInputMatcher(0), 1063 EXPECT_TIMEOUT); 1064 } 1065 } 1066 1067 @Test 1068 @ApiTest(apis = {"android.inputmethodservice.InputMethodService#onStartInput", 1069 "android.inputmethodservice.InputMethodService#showSoftInput"}) testUnfocusedEditor_stateAlwaysVisible()1070 public void testUnfocusedEditor_stateAlwaysVisible() throws Exception { 1071 Assume.assumeFalse(isPreventImeStartup()); 1072 ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 1073 SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1074 // The previous IME should be finished 1075 expectEvent(stream, eventMatcher("onFinishInput"), EXPECT_TIMEOUT); 1076 1077 // Input should be started 1078 expectEvent(stream, eventMatcher("onStartInput"), EXPECT_TIMEOUT); 1079 1080 final boolean willHideIme = willHideImeWhenNoEditorFocus(); 1081 if (willHideIme) { 1082 // The keyboard will not expected to show when focusing the app set STATE_ALWAYS_VISIBLE 1083 // without an editor from the IME shown activity 1084 notExpectEvent(stream, showSoftInputMatcher(0), NOT_EXPECT_TIMEOUT); 1085 } else { 1086 expectEvent(stream, showSoftInputMatcher(0), EXPECT_TIMEOUT); 1087 } 1088 } 1089 1090 @Test 1091 @ApiTest(apis = {"android.inputmethodservice.InputMethodService#onStartInput", 1092 "android.inputmethodservice.InputMethodService#showSoftInput"}) testUnfocusedEditor_stateUnchanged()1093 public void testUnfocusedEditor_stateUnchanged() throws Exception { 1094 Assume.assumeFalse(isPreventImeStartup()); 1095 ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity( 1096 SOFT_INPUT_STATE_UNCHANGED); 1097 // The previous IME should be finished 1098 expectEvent(stream, eventMatcher("onFinishInput"), EXPECT_TIMEOUT); 1099 1100 // Input should be started 1101 expectEvent(stream, eventMatcher("onStartInput"), EXPECT_TIMEOUT); 1102 1103 final boolean willHideIme = willHideImeWhenNoEditorFocus(); 1104 if (willHideIme) { 1105 // The keyboard will not expected to show when focusing the app set STATE_UNCHANGED 1106 // without an editor from the IME shown activity 1107 notExpectEvent(stream, showSoftInputMatcher(0), NOT_EXPECT_TIMEOUT); 1108 } else { 1109 expectEvent(stream, showSoftInputMatcher(0), EXPECT_TIMEOUT); 1110 } 1111 } 1112 1113 @Test detachServed_withDifferentNextServed_b211105987()1114 public void detachServed_withDifferentNextServed_b211105987() throws Exception { 1115 final AtomicReference<ViewGroup> layoutRef = new AtomicReference<>(); 1116 final AtomicReference<EditText> firstEditorRef = new AtomicReference<>(); 1117 final AtomicReference<EditText> secondEditorRef = new AtomicReference<>(); 1118 final AtomicReference<InputMethodManager> imm = new AtomicReference<>(); 1119 1120 TestActivity.startSync(activity -> { 1121 final LinearLayout layout = new LinearLayout(activity); 1122 layout.setOrientation(LinearLayout.VERTICAL); 1123 layoutRef.set(layout); 1124 1125 final EditText editText = new EditText(activity); 1126 editText.requestFocus(); 1127 firstEditorRef.set(editText); 1128 layout.addView(editText); 1129 imm.set(activity.getSystemService(InputMethodManager.class)); 1130 return layout; 1131 }); 1132 1133 waitOnMainUntil(() -> imm.get().hasActiveInputConnection(firstEditorRef.get()), TIMEOUT); 1134 1135 runOnMainSync(() -> { 1136 final ViewGroup layout = layoutRef.get(); 1137 1138 final EditText editText = new EditText(layout.getContext()); 1139 secondEditorRef.set(editText); 1140 layout.addView(editText); 1141 }); 1142 1143 waitOnMainUntil(() -> secondEditorRef.get().isLaidOut(), TIMEOUT); 1144 1145 runOnMainSync(() -> { 1146 secondEditorRef.get().requestFocus(); 1147 layoutRef.get().removeView(firstEditorRef.get()); 1148 }); 1149 1150 assertTrue(getOnMainSync(() -> imm.get().hasActiveInputConnection(secondEditorRef.get()))); 1151 } 1152 1153 @AppModeFull(reason = "Instant apps cannot start TranslucentActivity from existing activity.") 1154 @Test testClearCurRootViewWhenDifferentProcessBecomesActive()1155 public void testClearCurRootViewWhenDifferentProcessBecomesActive() throws Exception { 1156 final var editorRef = new AtomicReference<EditText>(); 1157 final var imm = new AtomicReference<InputMethodManager>(); 1158 1159 final var testActivity = TestActivity.startSync(activity -> { 1160 final var layout = new LinearLayout(activity); 1161 layout.setOrientation(LinearLayout.VERTICAL); 1162 1163 final var editText = new EditText(activity); 1164 editText.requestFocus(); 1165 editorRef.set(editText); 1166 layout.addView(editText); 1167 imm.set(activity.getSystemService(InputMethodManager.class)); 1168 return layout; 1169 }); 1170 1171 waitOnMainUntil(() -> imm.get().hasActiveInputConnection(editorRef.get()), TIMEOUT); 1172 1173 // launch activity in a different package. 1174 final var intent = new Intent(Intent.ACTION_MAIN); 1175 intent.setComponent(new ComponentName( 1176 "android.view.inputmethod.ctstestapp", 1177 "android.view.inputmethod.ctstestapp.TranslucentActivity")); 1178 runOnMainSync(() -> testActivity.startActivity(intent)); 1179 1180 waitOnMainUntil(() -> !imm.get().isCurrentRootView(editorRef.get()), TIMEOUT, 1181 "Initial activity did not lose IME connection after second activity started."); 1182 } 1183 1184 /** 1185 * A regression test for Bug 260682160. 1186 * 1187 * Ensure the input connection will be started eventually when temporary add & remove 1188 * ALT_FOCUSABLE_IM flag during the editor focus-out and focus-in stage. 1189 */ 1190 @Test testInputConnectionWhenAddAndRemoveAltFocusableImFlagInFocus()1191 public void testInputConnectionWhenAddAndRemoveAltFocusableImFlagInFocus() throws Exception { 1192 try (MockImeSession imeSession = createTestImeSession()) { 1193 final ImeEventStream stream = imeSession.openEventStream(); 1194 1195 final String marker1 = getTestMarker(FIRST_EDIT_TEXT_TAG); 1196 final String marker2 = getTestMarker(SECOND_EDIT_TEXT_TAG); 1197 1198 final AtomicReference<EditText> firstEditorRef = new AtomicReference<>(); 1199 final AtomicReference<EditText> secondEditorRef = new AtomicReference<>(); 1200 1201 final TestActivity testActivity = TestActivity.startSync(activity -> { 1202 final LinearLayout layout = new LinearLayout(activity); 1203 layout.setOrientation(LinearLayout.VERTICAL); 1204 final EditText firstEditor = new EditText(activity); 1205 firstEditor.setPrivateImeOptions(marker1); 1206 firstEditor.setOnFocusChangeListener((v, hasFocus) -> { 1207 if (!hasFocus) { 1208 // Test Scenario 1: add ALT_FOCUSABLE_IM flag when the first editor 1209 // lost the focus to disable the input and focusing the second editor. 1210 activity.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); 1211 secondEditorRef.get().requestFocus(); 1212 } 1213 }); 1214 firstEditor.requestFocus(); 1215 1216 final EditText secondEditor = new EditText(activity); 1217 secondEditor.setPrivateImeOptions(marker2); 1218 firstEditorRef.set(firstEditor); 1219 secondEditorRef.set(secondEditor); 1220 layout.addView(firstEditor); 1221 layout.addView(secondEditor); 1222 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_VISIBLE); 1223 return layout; 1224 }); 1225 expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT); 1226 1227 testActivity.runOnUiThread(() -> firstEditorRef.get().clearFocus()); 1228 TestUtils.waitOnMainUntil(() -> secondEditorRef.get().hasFocus(), TIMEOUT); 1229 1230 testActivity.runOnUiThread(() -> { 1231 // Test Scenario 2: remove ALT_FOCUSABLE_IM flag & call showSoftInput after 1232 // the second editor focused. 1233 testActivity.getWindow().clearFlags(FLAG_ALT_FOCUSABLE_IM); 1234 }); 1235 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 1236 1237 final InputMethodManager im = 1238 testActivity.getSystemService(InputMethodManager.class); 1239 // After removing FLAG_ALT_FOCUSABLE_IM, lets wait until InputConnection is created on 1240 // secondEditor. 1241 TestUtils.waitOnMainUntil( 1242 () -> im.hasActiveInputConnection(secondEditorRef.get()), TIMEOUT); 1243 1244 testActivity.runOnUiThread(() -> { 1245 im.showSoftInput(secondEditorRef.get(), 0); 1246 }); 1247 1248 // Expect the input connection can started and commit the text to the second editor. 1249 expectEvent(stream, editorMatcher("onStartInput", marker2), TIMEOUT); 1250 expectImeVisible(TIMEOUT); 1251 1252 final String testInput = "Test"; 1253 final ImeCommand commitText = imeSession.callCommitText(testInput, 0); 1254 expectCommand(stream, commitText, EXPECT_TIMEOUT); 1255 assertThat(secondEditorRef.get().getText().toString()).isEqualTo(testInput); 1256 } 1257 } 1258 1259 @NonNull createTestImeSession()1260 private static MockImeSession createTestImeSession() throws Exception { 1261 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 1262 return MockImeSession.create( 1263 instrumentation.getContext(), 1264 instrumentation.getUiAutomation(), 1265 new ImeSettings.Builder()); 1266 } 1267 1268 private static class ServiceSession implements ServiceConnection, AutoCloseable { 1269 private final Context mContext; 1270 private final Instrumentation mInstrumentation; 1271 ServiceSession(Instrumentation instrumentation)1272 ServiceSession(Instrumentation instrumentation) { 1273 mContext = instrumentation.getContext(); 1274 mInstrumentation = instrumentation; 1275 mInstrumentation.getUiAutomation().adoptShellPermissionIdentity( 1276 Manifest.permission.SYSTEM_ALERT_WINDOW); 1277 if (mContext.checkSelfPermission( 1278 Manifest.permission.SYSTEM_ALERT_WINDOW) != PackageManager.PERMISSION_GRANTED) { 1279 fail("Require SYSTEM_ALERT_WINDOW permission"); 1280 } 1281 Intent service = new Intent(mContext, WindowFocusHandleService.class); 1282 mContext.bindService(service, this, Context.BIND_AUTO_CREATE); 1283 1284 // Wait for service bound. 1285 try { 1286 TestUtils.waitOnMainUntil(() -> WindowFocusHandleService.getInstance() != null, 1287 TIMEOUT, "WindowFocusHandleService should be bound"); 1288 } catch (TimeoutException e) { 1289 fail("WindowFocusHandleService should be bound"); 1290 } 1291 } 1292 1293 @Override close()1294 public void close() throws Exception { 1295 mContext.unbindService(this); 1296 mInstrumentation.getUiAutomation().dropShellPermissionIdentity(); 1297 } 1298 getService()1299 WindowFocusHandleService getService() { 1300 return WindowFocusHandleService.getInstance(); 1301 } 1302 1303 @Override onServiceConnected(ComponentName name, IBinder service)1304 public void onServiceConnected(ComponentName name, IBinder service) { 1305 } 1306 1307 @Override onServiceDisconnected(ComponentName name)1308 public void onServiceDisconnected(ComponentName name) { 1309 } 1310 } 1311 1312 private static final class CloseOnce implements AutoCloseable { 1313 final AtomicBoolean mClosed = new AtomicBoolean(false); 1314 final AutoCloseable mAutoCloseable; CloseOnce(@onNull AutoCloseable autoCloseable)1315 private CloseOnce(@NonNull AutoCloseable autoCloseable) { 1316 mAutoCloseable = autoCloseable; 1317 } 1318 @Override close()1319 public void close() throws Exception { 1320 if (!mClosed.getAndSet(true)) { 1321 mAutoCloseable.close(); 1322 } 1323 } 1324 @NonNull of(@onNull AutoCloseable autoCloseable)1325 static CloseOnce of(@NonNull AutoCloseable autoCloseable) { 1326 return new CloseOnce(autoCloseable); 1327 } 1328 } 1329 willHideImeWhenNoEditorFocus()1330 private static boolean willHideImeWhenNoEditorFocus() throws Exception { 1331 return SystemUtil.callWithShellPermissionIdentity( 1332 () -> DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_METHOD_MANAGER, 1333 KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS, true)); 1334 } 1335 } 1336