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