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