xref: /aosp_15_r20/cts/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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