1 /*
2  * Copyright (C) 2022 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 com.android.inputmethodservice;
18 
19 import static android.view.WindowInsets.Type.captionBar;
20 
21 import static com.android.compatibility.common.util.SystemUtil.eventually;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.fail;
27 import static org.junit.Assume.assumeFalse;
28 import static org.junit.Assume.assumeTrue;
29 
30 import android.app.Instrumentation;
31 import android.content.Context;
32 import android.content.res.Configuration;
33 import android.graphics.Insets;
34 import android.os.RemoteException;
35 import android.provider.Settings;
36 import android.support.test.uiautomator.By;
37 import android.support.test.uiautomator.UiDevice;
38 import android.support.test.uiautomator.UiObject2;
39 import android.support.test.uiautomator.Until;
40 import android.util.Log;
41 import android.view.WindowManagerGlobal;
42 import android.view.inputmethod.EditorInfo;
43 import android.view.inputmethod.InputMethodManager;
44 
45 import androidx.test.ext.junit.runners.AndroidJUnit4;
46 import androidx.test.filters.MediumTest;
47 import androidx.test.platform.app.InstrumentationRegistry;
48 
49 import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
50 import com.android.apps.inputmethod.simpleime.testing.TestActivity;
51 import com.android.compatibility.common.util.SystemUtil;
52 import com.android.internal.inputmethod.InputMethodNavButtonFlags;
53 
54 import org.junit.After;
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 
59 import java.io.IOException;
60 import java.util.concurrent.CountDownLatch;
61 import java.util.concurrent.TimeUnit;
62 
63 @RunWith(AndroidJUnit4.class)
64 @MediumTest
65 public class InputMethodServiceTest {
66     private static final String TAG = "SimpleIMSTest";
67     private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService";
68     private static final String EDIT_TEXT_DESC = "Input box";
69     private static final long TIMEOUT_IN_SECONDS = 3;
70     private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
71             "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
72     private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
73             "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
74 
75     private Instrumentation mInstrumentation;
76     private UiDevice mUiDevice;
77     private Context mContext;
78     private String mTargetPackageName;
79     private TestActivity mActivity;
80     private InputMethodServiceWrapper mInputMethodService;
81     private String mInputMethodId;
82     private boolean mShowImeWithHardKeyboardEnabled;
83 
84     @Before
setUp()85     public void setUp() throws Exception {
86         mInstrumentation = InstrumentationRegistry.getInstrumentation();
87         mUiDevice = UiDevice.getInstance(mInstrumentation);
88         mContext = mInstrumentation.getContext();
89         mTargetPackageName = mInstrumentation.getTargetContext().getPackageName();
90         mInputMethodId = getInputMethodId();
91         prepareIme();
92         prepareEditor();
93         mInstrumentation.waitForIdleSync();
94         mUiDevice.freezeRotation();
95         mUiDevice.setOrientationNatural();
96         // Waits for input binding ready.
97         eventually(() -> {
98             mInputMethodService =
99                     InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
100             assertThat(mInputMethodService).isNotNull();
101 
102             // The editor won't bring up keyboard by default.
103             assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
104             assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
105         });
106         // Save the original value of show_ime_with_hard_keyboard from Settings.
107         mShowImeWithHardKeyboardEnabled = Settings.Secure.getInt(
108                 mInputMethodService.getContentResolver(),
109                 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0;
110     }
111 
112     @After
tearDown()113     public void tearDown() throws Exception {
114         mUiDevice.unfreezeRotation();
115         executeShellCommand("ime disable " + mInputMethodId);
116         // Change back the original value of show_ime_with_hard_keyboard in Settings.
117         executeShellCommand(mShowImeWithHardKeyboardEnabled
118                 ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
119                 : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
120     }
121 
122     /**
123      * This checks that the IME can be shown and hidden by user actions
124      * (i.e. tapping on an EditText, tapping the Home button).
125      */
126     @Test
testShowHideKeyboard_byUserAction()127     public void testShowHideKeyboard_byUserAction() throws Exception {
128         setShowImeWithHardKeyboard(true /* enabled */);
129 
130         // Performs click on editor box to bring up the soft keyboard.
131         Log.i(TAG, "Click on EditText.");
132         verifyInputViewStatus(
133                 () -> clickOnEditorText(),
134                 true /* expected */,
135                 true /* inputViewStarted */);
136         assertThat(mInputMethodService.isInputViewShown()).isTrue();
137 
138         // Press home key to hide soft keyboard.
139         Log.i(TAG, "Press home");
140         verifyInputViewStatus(
141                 () -> assertThat(mUiDevice.pressHome()).isTrue(),
142                 true /* expected */,
143                 false /* inputViewStarted */);
144         assertThat(mInputMethodService.isInputViewShown()).isFalse();
145     }
146 
147     /**
148      * This checks that the IME can be shown and hidden using the WindowInsetsController APIs.
149      */
150     @Test
testShowHideKeyboard_byApi()151     public void testShowHideKeyboard_byApi() throws Exception {
152         setShowImeWithHardKeyboard(true /* enabled */);
153 
154         // Triggers to show IME via public API.
155         verifyInputViewStatus(
156                 () -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
157                 true /* expected */,
158                 true /* inputViewStarted */);
159         assertThat(mInputMethodService.isInputViewShown()).isTrue();
160 
161         // Triggers to hide IME via public API.
162         verifyInputViewStatusOnMainSync(
163                 () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(),
164                 true /* expected */,
165                 false /* inputViewStarted */);
166         assertThat(mInputMethodService.isInputViewShown()).isFalse();
167     }
168 
169     /**
170      * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
171      */
172     @Test
testShowHideSelf()173     public void testShowHideSelf() throws Exception {
174         setShowImeWithHardKeyboard(true /* enabled */);
175 
176         // IME request to show itself without any flags, expect shown.
177         Log.i(TAG, "Call IMS#requestShowSelf(0)");
178         verifyInputViewStatusOnMainSync(
179                 () -> mInputMethodService.requestShowSelf(0 /* flags */),
180                 true /* expected */,
181                 true /* inputViewStarted */);
182         assertThat(mInputMethodService.isInputViewShown()).isTrue();
183 
184         // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
185         Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
186         verifyInputViewStatusOnMainSync(
187                 () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
188                 false /* expected */,
189                 true /* inputViewStarted */);
190         assertThat(mInputMethodService.isInputViewShown()).isTrue();
191 
192         // IME request to hide itself without any flags, expect hidden.
193         Log.i(TAG, "Call IMS#requestHideSelf(0)");
194         verifyInputViewStatusOnMainSync(
195                 () -> mInputMethodService.requestHideSelf(0 /* flags */),
196                 true /* expected */,
197                 false /* inputViewStarted */);
198         assertThat(mInputMethodService.isInputViewShown()).isFalse();
199 
200         // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
201         Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
202         verifyInputViewStatusOnMainSync(
203                 () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
204                 true /* expected */,
205                 true /* inputViewStarted */);
206         assertThat(mInputMethodService.isInputViewShown()).isTrue();
207 
208         // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
209         Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
210         verifyInputViewStatusOnMainSync(
211                 () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
212                 true /* expected */,
213                 false /* inputViewStarted */);
214         assertThat(mInputMethodService.isInputViewShown()).isFalse();
215     }
216 
217     /**
218      * This checks the return value of IMS#onEvaluateInputViewShown,
219      * when show_ime_with_hard_keyboard is enabled.
220      */
221     @Test
testOnEvaluateInputViewShown_showImeWithHardKeyboard()222     public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() throws Exception {
223         setShowImeWithHardKeyboard(true /* enabled */);
224 
225         mInputMethodService.getResources().getConfiguration().keyboard =
226                 Configuration.KEYBOARD_QWERTY;
227         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
228                 Configuration.HARDKEYBOARDHIDDEN_NO;
229         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
230 
231         mInputMethodService.getResources().getConfiguration().keyboard =
232                 Configuration.KEYBOARD_NOKEYS;
233         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
234                 Configuration.HARDKEYBOARDHIDDEN_NO;
235         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
236 
237         mInputMethodService.getResources().getConfiguration().keyboard =
238                 Configuration.KEYBOARD_QWERTY;
239         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
240                 Configuration.HARDKEYBOARDHIDDEN_YES;
241         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
242     }
243 
244     /**
245      * This checks the return value of IMSonEvaluateInputViewShown,
246      * when show_ime_with_hard_keyboard is disabled.
247      */
248     @Test
testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard()249     public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() throws Exception {
250         setShowImeWithHardKeyboard(false /* enabled */);
251 
252         mInputMethodService.getResources().getConfiguration().keyboard =
253                 Configuration.KEYBOARD_QWERTY;
254         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
255                 Configuration.HARDKEYBOARDHIDDEN_NO;
256         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isFalse());
257 
258         mInputMethodService.getResources().getConfiguration().keyboard =
259                 Configuration.KEYBOARD_NOKEYS;
260         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
261                 Configuration.HARDKEYBOARDHIDDEN_NO;
262         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
263 
264         mInputMethodService.getResources().getConfiguration().keyboard =
265                 Configuration.KEYBOARD_QWERTY;
266         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
267                 Configuration.HARDKEYBOARDHIDDEN_YES;
268         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
269     }
270 
271     /**
272      * This checks that any (implicit or explicit) show request,
273      * when IMS#onEvaluateInputViewShown returns false, results in the IME not being shown.
274      */
275     @Test
testShowSoftInput_disableShowImeWithHardKeyboard()276     public void testShowSoftInput_disableShowImeWithHardKeyboard() throws Exception {
277         setShowImeWithHardKeyboard(false /* enabled */);
278 
279         // Simulate connecting a hard keyboard.
280         mInputMethodService.getResources().getConfiguration().keyboard =
281                 Configuration.KEYBOARD_QWERTY;
282         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
283                 Configuration.HARDKEYBOARDHIDDEN_NO;
284 
285         // When InputMethodService#onEvaluateInputViewShown() returns false, the Ime should not be
286         // shown no matter what the show flag is.
287         verifyInputViewStatusOnMainSync(() -> assertThat(
288                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
289                 false /* expected */,
290                 false /* inputViewStarted */);
291         assertThat(mInputMethodService.isInputViewShown()).isFalse();
292 
293         verifyInputViewStatusOnMainSync(
294                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
295                 false /* expected */,
296                 false /* inputViewStarted */);
297         assertThat(mInputMethodService.isInputViewShown()).isFalse();
298     }
299 
300     /**
301      * This checks that an explicit show request results in the IME being shown.
302      */
303     @Test
testShowSoftInputExplicitly()304     public void testShowSoftInputExplicitly() throws Exception {
305         setShowImeWithHardKeyboard(true /* enabled */);
306 
307         // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the
308         // Ime should be shown.
309         verifyInputViewStatusOnMainSync(
310                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
311                 true /* expected */,
312                 true /* inputViewStarted */);
313         assertThat(mInputMethodService.isInputViewShown()).isTrue();
314     }
315 
316     /**
317      * This checks that an implicit show request results in the IME being shown.
318      */
319     @Test
testShowSoftInputImplicitly()320     public void testShowSoftInputImplicitly() throws Exception {
321         setShowImeWithHardKeyboard(true /* enabled */);
322 
323         // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT,
324         // the IME should be shown.
325         verifyInputViewStatusOnMainSync(() -> assertThat(
326                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
327                 true /* expected */,
328                 true /* inputViewStarted */);
329         assertThat(mInputMethodService.isInputViewShown()).isTrue();
330     }
331 
332     /**
333      * This checks that an explicit show request when the IME is not previously shown,
334      * and it should be shown in fullscreen mode, results in the IME being shown.
335      */
336     @Test
testShowSoftInputExplicitly_fullScreenMode()337     public void testShowSoftInputExplicitly_fullScreenMode() throws Exception {
338         setShowImeWithHardKeyboard(true /* enabled */);
339 
340         // Set orientation landscape to enable fullscreen mode.
341         setOrientation(2);
342         eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
343         // Wait for the TestActivity to be recreated.
344         eventually(() ->
345                 assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
346         // Get the new TestActivity.
347         mActivity = TestActivity.getLastCreatedInstance();
348         assertThat(mActivity).isNotNull();
349         InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
350         // Wait for the new EditText to be served by InputMethodManager.
351         eventually(() -> assertThat(
352                 imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
353 
354         verifyInputViewStatusOnMainSync(() -> assertThat(
355                         mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
356                 true /* expected */,
357                 true /* inputViewStarted */);
358         assertThat(mInputMethodService.isInputViewShown()).isTrue();
359     }
360 
361     /**
362      * This checks that an implicit show request when the IME is not previously shown,
363      * and it should be shown in fullscreen mode, results in the IME not being shown.
364      */
365     @Test
testShowSoftInputImplicitly_fullScreenMode()366     public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
367         setShowImeWithHardKeyboard(true /* enabled */);
368 
369         // Set orientation landscape to enable fullscreen mode.
370         setOrientation(2);
371         eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
372         // Wait for the TestActivity to be recreated.
373         eventually(() ->
374                 assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
375         // Get the new TestActivity.
376         mActivity = TestActivity.getLastCreatedInstance();
377         assertThat(mActivity).isNotNull();
378         InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
379         // Wait for the new EditText to be served by InputMethodManager.
380         eventually(() -> assertThat(
381                 imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
382 
383         verifyInputViewStatusOnMainSync(() -> assertThat(
384                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
385                 false /* expected */,
386                 false /* inputViewStarted */);
387         assertThat(mInputMethodService.isInputViewShown()).isFalse();
388     }
389 
390     /**
391      * This checks that an explicit show request when a hard keyboard is connected,
392      * results in the IME being shown.
393      */
394     @Test
testShowSoftInputExplicitly_withHardKeyboard()395     public void testShowSoftInputExplicitly_withHardKeyboard() throws Exception {
396         setShowImeWithHardKeyboard(false /* enabled */);
397 
398         // Simulate connecting a hard keyboard.
399         mInputMethodService.getResources().getConfiguration().keyboard =
400                 Configuration.KEYBOARD_QWERTY;
401         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
402                 Configuration.HARDKEYBOARDHIDDEN_YES;
403 
404         verifyInputViewStatusOnMainSync(() -> assertThat(
405                 mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
406                 true /* expected */,
407                 true /* inputViewStarted */);
408         assertThat(mInputMethodService.isInputViewShown()).isTrue();
409     }
410 
411     /**
412      * This checks that an implicit show request when a hard keyboard is connected,
413      * results in the IME not being shown.
414      */
415     @Test
testShowSoftInputImplicitly_withHardKeyboard()416     public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception {
417         setShowImeWithHardKeyboard(false /* enabled */);
418 
419         // Simulate connecting a hard keyboard.
420         mInputMethodService.getResources().getConfiguration().keyboard =
421                 Configuration.KEYBOARD_QWERTY;
422         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
423                 Configuration.HARDKEYBOARDHIDDEN_YES;
424 
425         verifyInputViewStatusOnMainSync(() -> assertThat(
426                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
427                 false /* expected */,
428                 false /* inputViewStarted */);
429         assertThat(mInputMethodService.isInputViewShown()).isFalse();
430     }
431 
432     /**
433      * This checks that an explicit show request followed by connecting a hard keyboard
434      * and a configuration change, still results in the IME being shown.
435      */
436     @Test
testShowSoftInputExplicitly_thenConfigurationChanged()437     public void testShowSoftInputExplicitly_thenConfigurationChanged() throws Exception {
438         setShowImeWithHardKeyboard(false /* enabled */);
439 
440         // Start with no hard keyboard.
441         mInputMethodService.getResources().getConfiguration().keyboard =
442                 Configuration.KEYBOARD_NOKEYS;
443         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
444                 Configuration.HARDKEYBOARDHIDDEN_YES;
445 
446         verifyInputViewStatusOnMainSync(
447                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
448                 true /* expected */,
449                 true /* inputViewStarted */);
450         assertThat(mInputMethodService.isInputViewShown()).isTrue();
451 
452         // Simulate connecting a hard keyboard.
453         mInputMethodService.getResources().getConfiguration().keyboard =
454                 Configuration.KEYBOARD_QWERTY;
455         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
456                 Configuration.HARDKEYBOARDHIDDEN_YES;
457 
458         // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
459         mInputMethodService.getResources().getConfiguration().orientation =
460                 Configuration.ORIENTATION_LANDSCAPE;
461 
462         verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
463                 mInputMethodService.getResources().getConfiguration()),
464                 true /* expected */,
465                 true /* inputViewStarted */);
466         assertThat(mInputMethodService.isInputViewShown()).isTrue();
467     }
468 
469     /**
470      * This checks that an implicit show request followed by connecting a hard keyboard
471      * and a configuration change, does not trigger IMS#onFinishInputView,
472      * but results in the IME being hidden.
473      */
474     @Test
testShowSoftInputImplicitly_thenConfigurationChanged()475     public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception {
476         setShowImeWithHardKeyboard(false /* enabled */);
477 
478         // Start with no hard keyboard.
479         mInputMethodService.getResources().getConfiguration().keyboard =
480                 Configuration.KEYBOARD_NOKEYS;
481         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
482                 Configuration.HARDKEYBOARDHIDDEN_YES;
483 
484         verifyInputViewStatusOnMainSync(() -> assertThat(
485                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
486                 true /* expected */,
487                 true /* inputViewStarted */);
488         assertThat(mInputMethodService.isInputViewShown()).isTrue();
489 
490         // Simulate connecting a hard keyboard.
491         mInputMethodService.getResources().getConfiguration().keyboard =
492                 Configuration.KEYBOARD_QWERTY;
493         mInputMethodService.getResources().getConfiguration().keyboard =
494                 Configuration.HARDKEYBOARDHIDDEN_YES;
495 
496         // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
497         mInputMethodService.getResources().getConfiguration().orientation =
498                 Configuration.ORIENTATION_LANDSCAPE;
499 
500         // Normally, IMS#onFinishInputView will be called when finishing the input view by the user.
501         // But if IMS#hideWindow is called when receiving a new configuration change, we don't
502         // expect that it's user-driven to finish the lifecycle of input view with
503         // IMS#onFinishInputView, because the input view will be re-initialized according to the
504         // last #mShowInputRequested state. So in this case we treat the input view as still alive.
505         verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
506                 mInputMethodService.getResources().getConfiguration()),
507                 true /* expected */,
508                 true /* inputViewStarted */);
509         assertThat(mInputMethodService.isInputViewShown()).isFalse();
510     }
511 
512     /**
513      * This checks that an explicit show request directly followed by an implicit show request,
514      * while a hardware keyboard is connected, still results in the IME being shown
515      * (i.e. the implicit show request is treated as explicit).
516      */
517     @Test
testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard()518     public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard()
519             throws Exception {
520         setShowImeWithHardKeyboard(false /* enabled */);
521 
522         // Simulate connecting a hard keyboard.
523         mInputMethodService.getResources().getConfiguration().keyboard =
524                 Configuration.KEYBOARD_QWERTY;
525         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
526                 Configuration.HARDKEYBOARDHIDDEN_YES;
527 
528         // Explicit show request.
529         verifyInputViewStatusOnMainSync(() -> assertThat(
530                         mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
531                 true /* expected */,
532                 true /* inputViewStarted */);
533         assertThat(mInputMethodService.isInputViewShown()).isTrue();
534 
535         // Implicit show request.
536         verifyInputViewStatusOnMainSync(() -> assertThat(
537                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
538                 false /* expected */,
539                 true /* inputViewStarted */);
540         assertThat(mInputMethodService.isInputViewShown()).isTrue();
541 
542         // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
543         // This should now consider the implicit show request, but keep the state from the
544         // explicit show request, and thus not hide the keyboard.
545         verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
546                         mInputMethodService.getResources().getConfiguration()),
547                 true /* expected */,
548                 true /* inputViewStarted */);
549         assertThat(mInputMethodService.isInputViewShown()).isTrue();
550     }
551 
552     /**
553      * This checks that a forced show request directly followed by an explicit show request,
554      * and then a hide not always request, still results in the IME being shown
555      * (i.e. the explicit show request retains the forced state).
556      */
557     @Test
testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()558     public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()
559             throws Exception {
560         setShowImeWithHardKeyboard(true /* enabled */);
561 
562         verifyInputViewStatusOnMainSync(() -> assertThat(
563                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(),
564                 true /* expected */,
565                 true /* inputViewStarted */);
566         assertThat(mInputMethodService.isInputViewShown()).isTrue();
567 
568         verifyInputViewStatusOnMainSync(() -> assertThat(
569                         mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
570                 false /* expected */,
571                 true /* inputViewStarted */);
572         assertThat(mInputMethodService.isInputViewShown()).isTrue();
573 
574         verifyInputViewStatusOnMainSync(() ->
575                         mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS),
576                 false /* expected */,
577                 true /* inputViewStarted */);
578         assertThat(mInputMethodService.isInputViewShown()).isTrue();
579     }
580 
581     /**
582      * This checks that the IME fullscreen mode state is updated after changing orientation.
583      */
584     @Test
testFullScreenMode()585     public void testFullScreenMode() throws Exception {
586         setShowImeWithHardKeyboard(true /* enabled */);
587 
588         Log.i(TAG, "Set orientation natural");
589         verifyFullscreenMode(() -> setOrientation(0),
590                 false /* expected */,
591                 true /* orientationPortrait */);
592 
593         Log.i(TAG, "Set orientation left");
594         verifyFullscreenMode(() -> setOrientation(1),
595                 true /* expected */,
596                 false /* orientationPortrait */);
597 
598         Log.i(TAG, "Set orientation right");
599         verifyFullscreenMode(() -> setOrientation(2),
600                 false /* expected */,
601                 false /* orientationPortrait */);
602     }
603 
604     /**
605      * This checks that when the system navigation bar is not created (e.g. emulator),
606      * then the IME caption bar is also not created.
607      */
608     @Test
testNoNavigationBar_thenImeNoCaptionBar()609     public void testNoNavigationBar_thenImeNoCaptionBar() throws Exception {
610         boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
611                 .hasNavigationBar(mInputMethodService.getDisplayId());
612         assumeFalse("Must not have a navigation bar", hasNavigationBar);
613 
614         assertEquals(Insets.NONE, mInputMethodService.getWindow().getWindow().getDecorView()
615                 .getRootWindowInsets().getInsetsIgnoringVisibility(captionBar()));
616     }
617 
618     /**
619      * This checks that trying to show and hide the navigation bar takes effect
620      * when the IME does draw the IME navigation bar.
621      */
622     @Test
testShowHideImeNavigationBar_doesDrawImeNavBar()623     public void testShowHideImeNavigationBar_doesDrawImeNavBar() throws Exception {
624         boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
625                 .hasNavigationBar(mInputMethodService.getDisplayId());
626         assumeTrue("Must have a navigation bar", hasNavigationBar);
627 
628         setShowImeWithHardKeyboard(true /* enabled */);
629 
630         // Show IME
631         verifyInputViewStatusOnMainSync(
632                 () -> {
633                     mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
634                             InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
635                                     | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
636                     );
637                     assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue();
638                 },
639                 true /* expected */,
640                 true /* inputViewStarted */);
641         assertThat(mInputMethodService.isInputViewShown()).isTrue();
642         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue();
643 
644         // Try to hide IME nav bar
645         mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
646                 .getInsetsController().hide(captionBar()));
647         mInstrumentation.waitForIdleSync();
648         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
649 
650         // Try to show IME nav bar
651         mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
652                 .getInsetsController().show(captionBar()));
653         mInstrumentation.waitForIdleSync();
654         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue();
655     }
656     /**
657      * This checks that trying to show and hide the navigation bar has no effect
658      * when the IME does not draw the IME navigation bar.
659      *
660      * Note: The IME navigation bar is *never* visible in 3 button navigation mode.
661      */
662     @Test
testShowHideImeNavigationBar_doesNotDrawImeNavBar()663     public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() throws Exception {
664         boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
665                 .hasNavigationBar(mInputMethodService.getDisplayId());
666         assumeTrue("Must have a navigation bar", hasNavigationBar);
667 
668         setShowImeWithHardKeyboard(true /* enabled */);
669 
670         // Show IME
671         verifyInputViewStatusOnMainSync(() -> {
672             mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
673                     0 /* navButtonFlags */);
674             assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue();
675         },
676                 true /* expected */,
677                 true /* inputViewStarted */);
678         assertThat(mInputMethodService.isInputViewShown()).isTrue();
679         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
680 
681         // Try to hide IME nav bar
682         mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
683                 .getInsetsController().hide(captionBar()));
684         mInstrumentation.waitForIdleSync();
685         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
686 
687         // Try to show IME nav bar
688         mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
689                 .getInsetsController().show(captionBar()));
690         mInstrumentation.waitForIdleSync();
691         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
692     }
693 
verifyInputViewStatus( Runnable runnable, boolean expected, boolean inputViewStarted)694     private void verifyInputViewStatus(
695             Runnable runnable, boolean expected, boolean inputViewStarted)
696             throws InterruptedException {
697         verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
698                 false /* runOnMainSync */);
699     }
700 
verifyInputViewStatusOnMainSync( Runnable runnable, boolean expected, boolean inputViewStarted)701     private void verifyInputViewStatusOnMainSync(
702             Runnable runnable, boolean expected, boolean inputViewStarted)
703             throws InterruptedException {
704         verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
705                 true /* runOnMainSync */);
706     }
707 
708     /**
709      * Verifies the status of the Input View after executing the given runnable.
710      *
711      * @param runnable the runnable to execute for showing or hiding the IME.
712      * @param expected whether the runnable is expected to trigger the signal.
713      * @param inputViewStarted the expected state of the Input View after executing the runnable.
714      * @param runOnMainSync whether to execute the runnable on the main thread.
715      */
verifyInputViewStatusInternal( Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync)716     private void verifyInputViewStatusInternal(
717             Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync)
718             throws InterruptedException {
719         CountDownLatch signal = new CountDownLatch(1);
720         mInputMethodService.setCountDownLatchForTesting(signal);
721         // Runnable to trigger onStartInputView() / onFinishInputView() / onConfigurationChanged()
722         if (runOnMainSync) {
723             mInstrumentation.runOnMainSync(runnable);
724         } else {
725             runnable.run();
726         }
727         mInstrumentation.waitForIdleSync();
728         boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
729         if (expected && !completed) {
730             fail("Timed out waiting for"
731                     + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
732         } else if (!expected && completed) {
733             fail("Unexpected call"
734                     + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
735         }
736         // Input is not finished.
737         assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
738         assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
739     }
740 
setOrientation(int orientation)741     private void setOrientation(int orientation) {
742         // Simple wrapper for catching RemoteException.
743         try {
744             switch (orientation) {
745                 case 1:
746                     mUiDevice.setOrientationLeft();
747                     break;
748                 case 2:
749                     mUiDevice.setOrientationRight();
750                     break;
751                 default:
752                     mUiDevice.setOrientationNatural();
753             }
754         } catch (RemoteException e) {
755             throw new RuntimeException(e);
756         }
757     }
758 
759     /**
760      * Verifies the IME fullscreen mode state after executing the given runnable.
761      *
762      * @param runnable the runnable to execute for setting the orientation.
763      * @param expected whether the runnable is expected to trigger the signal.
764      * @param orientationPortrait whether the orientation is expected to be portrait.
765      */
verifyFullscreenMode( Runnable runnable, boolean expected, boolean orientationPortrait)766     private void verifyFullscreenMode(
767             Runnable runnable, boolean expected, boolean orientationPortrait)
768             throws InterruptedException {
769         CountDownLatch signal = new CountDownLatch(1);
770         mInputMethodService.setCountDownLatchForTesting(signal);
771 
772         // Runnable to trigger onConfigurationChanged()
773         try {
774             runnable.run();
775         } catch (Exception e) {
776             throw new RuntimeException(e);
777         }
778         // Waits for onConfigurationChanged() to finish.
779         mInstrumentation.waitForIdleSync();
780         boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
781         if (expected && !completed) {
782             fail("Timed out waiting for onConfigurationChanged()");
783         } else if (!expected && completed) {
784             fail("Unexpected call onConfigurationChanged()");
785         }
786 
787         clickOnEditorText();
788         eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
789 
790         assertThat(mInputMethodService.getResources().getConfiguration().orientation)
791                 .isEqualTo(
792                         orientationPortrait
793                                 ? Configuration.ORIENTATION_PORTRAIT
794                                 : Configuration.ORIENTATION_LANDSCAPE);
795         EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
796         assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0);
797         assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT)
798                 .isEqualTo(
799                         orientationPortrait ? EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT : 0);
800         assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait);
801         assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait);
802 
803         mUiDevice.pressBack();
804     }
805 
prepareIme()806     private void prepareIme() throws Exception {
807         executeShellCommand("ime enable " + mInputMethodId);
808         executeShellCommand("ime set " + mInputMethodId);
809         mInstrumentation.waitForIdleSync();
810         Log.i(TAG, "Finish preparing IME");
811     }
812 
prepareEditor()813     private void prepareEditor() {
814         mActivity = TestActivity.start(mInstrumentation);
815         Log.i(TAG, "Finish preparing activity with editor.");
816     }
817 
getInputMethodId()818     private String getInputMethodId() {
819         return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
820     }
821 
822     /**
823      * Sets the value of show_ime_with_hard_keyboard, only if it is different to the default value.
824      *
825      * @param enabled the value to be set.
826      */
setShowImeWithHardKeyboard(boolean enabled)827     private void setShowImeWithHardKeyboard(boolean enabled) throws IOException {
828         if (mShowImeWithHardKeyboardEnabled != enabled) {
829             executeShellCommand(enabled
830                     ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
831                     : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
832             mInstrumentation.waitForIdleSync();
833         }
834     }
835 
executeShellCommand(String cmd)836     private String executeShellCommand(String cmd) throws IOException {
837         Log.i(TAG, "Run command: " + cmd);
838         return SystemUtil.runShellCommandOrThrow(cmd);
839     }
840 
clickOnEditorText()841     private void clickOnEditorText() {
842         // Find the editText and click it.
843         UiObject2 editTextUiObject =
844                 mUiDevice.wait(
845                         Until.findObject(By.desc(EDIT_TEXT_DESC)),
846                         TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
847         assertThat(editTextUiObject).isNotNull();
848         editTextUiObject.click();
849         mInstrumentation.waitForIdleSync();
850     }
851 }
852