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