• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.inputmethodservice.cts.devicetest;
18 
19 import static android.inputmethodservice.cts.DeviceEvent.isFrom;
20 import static android.inputmethodservice.cts.DeviceEvent.isNewerThan;
21 import static android.inputmethodservice.cts.DeviceEvent.isType;
22 import static android.inputmethodservice.cts.common.BusyWaitUtils.pollingCheck;
23 import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.ON_BIND_INPUT;
24 import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.ON_CREATE;
25 import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.ON_DESTROY;
26 import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.ON_START_INPUT;
27 import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.ON_UNBIND_INPUT;
28 import static android.inputmethodservice.cts.common.ImeCommandConstants.ACTION_IME_COMMAND;
29 import static android.inputmethodservice.cts.common.ImeCommandConstants.COMMAND_SWITCH_INPUT_METHOD;
30 import static android.inputmethodservice.cts.common.ImeCommandConstants.COMMAND_SWITCH_INPUT_METHOD_WITH_SUBTYPE;
31 import static android.inputmethodservice.cts.common.ImeCommandConstants.COMMAND_SWITCH_TO_NEXT_INPUT;
32 import static android.inputmethodservice.cts.common.ImeCommandConstants.COMMAND_SWITCH_TO_PREVIOUS_INPUT;
33 import static android.inputmethodservice.cts.common.ImeCommandConstants.EXTRA_ARG_STRING1;
34 import static android.inputmethodservice.cts.common.ImeCommandConstants.EXTRA_COMMAND;
35 import static android.inputmethodservice.cts.devicetest.MoreCollectors.startingFrom;
36 
37 import static org.junit.Assume.assumeNotNull;
38 import static org.junit.Assume.assumeTrue;
39 
40 import android.app.UiAutomation;
41 import android.content.Context;
42 import android.inputmethodservice.cts.DeviceEvent;
43 import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType;
44 import android.inputmethodservice.cts.common.EditTextAppConstants;
45 import android.inputmethodservice.cts.common.Ime1Constants;
46 import android.inputmethodservice.cts.common.Ime2Constants;
47 import android.inputmethodservice.cts.common.test.ShellCommandUtils;
48 import android.inputmethodservice.cts.devicetest.SequenceMatcher.MatchResult;
49 import android.os.PowerManager;
50 import android.os.SystemClock;
51 import android.view.inputmethod.InputMethodManager;
52 import android.view.inputmethod.InputMethodSubtype;
53 
54 import androidx.test.platform.app.InstrumentationRegistry;
55 import androidx.test.runner.AndroidJUnit4;
56 import androidx.test.uiautomator.UiObject2;
57 
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
60 
61 import java.util.Arrays;
62 import java.util.concurrent.TimeUnit;
63 import java.util.function.IntFunction;
64 import java.util.function.Predicate;
65 import java.util.stream.Collector;
66 
67 /**
68  * Test general lifecycle events around InputMethodService.
69  */
70 @RunWith(AndroidJUnit4.class)
71 public class InputMethodServiceDeviceTest {
72 
73     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(20);
74 
75     /** Test to check CtsInputMethod1 receives onCreate and onStartInput. */
76     @Test
testCreateIme1()77     public void testCreateIme1() throws Throwable {
78         final TestHelper helper = new TestHelper();
79 
80         final long startActivityTime = SystemClock.uptimeMillis();
81         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
82                 EditTextAppConstants.URI);
83 
84         pollingCheck(() -> helper.queryAllEvents()
85                         .collect(startingFrom(helper.isStartOfTest()))
86                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_CREATE))),
87                 TIMEOUT, "CtsInputMethod1.onCreate is called");
88         pollingCheck(() -> helper.queryAllEvents()
89                         .filter(isNewerThan(startActivityTime))
90                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
91                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
92     }
93 
94     /** Test to check IME is switched from CtsInputMethod1 to CtsInputMethod2. */
95     @Test
testSwitchIme1ToIme2()96     public void testSwitchIme1ToIme2() throws Throwable {
97         final TestHelper helper = new TestHelper();
98 
99         final long startActivityTime = SystemClock.uptimeMillis();
100         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
101                 EditTextAppConstants.URI);
102 
103         pollingCheck(() -> helper.queryAllEvents()
104                         .collect(startingFrom(helper.isStartOfTest()))
105                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_CREATE))),
106                 TIMEOUT, "CtsInputMethod1.onCreate is called");
107         pollingCheck(() -> helper.queryAllEvents()
108                         .filter(isNewerThan(startActivityTime))
109                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
110                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
111 
112         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
113 
114         // Switch IME from CtsInputMethod1 to CtsInputMethod2.
115         final long switchImeTime = SystemClock.uptimeMillis();
116         helper.shell(ShellCommandUtils.broadcastIntent(
117                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
118                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD,
119                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
120 
121         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
122                         .equals(Ime2Constants.IME_ID),
123                 TIMEOUT, "CtsInputMethod2 is current IME");
124         pollingCheck(() -> helper.queryAllEvents()
125                         .filter(isNewerThan(switchImeTime))
126                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_DESTROY))),
127                 TIMEOUT, "CtsInputMethod1.onDestroy is called");
128         pollingCheck(() -> helper.queryAllEvents()
129                         .filter(isNewerThan(switchImeTime))
130                         .filter(isFrom(Ime2Constants.CLASS))
131                         .collect(sequenceOfTypes(ON_CREATE, ON_BIND_INPUT, ON_START_INPUT))
132                         .matched(),
133                 TIMEOUT,
134                 "CtsInputMethod2.onCreate, onBindInput, and onStartInput are called"
135                         + " in sequence");
136     }
137 
138     /**
139      * Test {@link android.inputmethodservice.InputMethodService#switchInputMethod(String,
140      * InputMethodSubtype)}.
141      */
142     @Test
testSwitchInputMethod()143     public void testSwitchInputMethod() throws Throwable {
144         final TestHelper helper = new TestHelper();
145         final long startActivityTime = SystemClock.uptimeMillis();
146         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
147                 EditTextAppConstants.URI);
148         pollingCheck(() -> helper.queryAllEvents()
149                         .filter(isNewerThan(startActivityTime))
150                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
151                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
152         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
153 
154         final long setImeTime = SystemClock.uptimeMillis();
155         // call setInputMethodAndSubtype(IME2, null)
156         helper.shell(ShellCommandUtils.broadcastIntent(
157                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
158                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD_WITH_SUBTYPE,
159                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
160         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
161                         .equals(Ime2Constants.IME_ID),
162                 TIMEOUT, "CtsInputMethod2 is current IME");
163         pollingCheck(() -> helper.queryAllEvents()
164                         .filter(isNewerThan(setImeTime))
165                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_DESTROY))),
166                 TIMEOUT, "CtsInputMethod1.onDestroy is called");
167     }
168 
169     /**
170      * Test {@link android.inputmethodservice.InputMethodService#switchToNextInputMethod(boolean)}.
171      */
172     @Test
testSwitchToNextInputMethod()173     public void testSwitchToNextInputMethod() throws Throwable {
174         final TestHelper helper = new TestHelper();
175         final long startActivityTime = SystemClock.uptimeMillis();
176         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
177                 EditTextAppConstants.URI);
178         pollingCheck(() -> helper.queryAllEvents()
179                         .filter(isNewerThan(startActivityTime))
180                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
181                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
182         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
183 
184         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
185                         .equals(Ime1Constants.IME_ID),
186                 TIMEOUT, "CtsInputMethod1 is current IME");
187         helper.shell(ShellCommandUtils.broadcastIntent(
188                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
189                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_TO_NEXT_INPUT));
190         pollingCheck(() -> !helper.shell(ShellCommandUtils.getCurrentIme())
191                         .equals(Ime1Constants.IME_ID),
192                 TIMEOUT, "CtsInputMethod1 shouldn't be current IME");
193     }
194 
195     /**
196      * Test {@link android.inputmethodservice.InputMethodService#switchToPreviousInputMethod()}.
197      */
198     @Test
switchToPreviousInputMethod()199     public void switchToPreviousInputMethod() throws Throwable {
200         final TestHelper helper = new TestHelper();
201         final long startActivityTime = SystemClock.uptimeMillis();
202         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
203                 EditTextAppConstants.URI);
204         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
205 
206         final String initialIme = helper.shell(ShellCommandUtils.getCurrentIme());
207         helper.shell(ShellCommandUtils.setCurrentImeSync(Ime2Constants.IME_ID));
208         pollingCheck(() -> helper.queryAllEvents()
209                         .filter(isNewerThan(startActivityTime))
210                         .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_START_INPUT))),
211                 TIMEOUT, "CtsInputMethod2.onStartInput is called");
212         helper.shell(ShellCommandUtils.broadcastIntent(
213                 ACTION_IME_COMMAND, Ime2Constants.PACKAGE,
214                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_TO_PREVIOUS_INPUT));
215         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
216                         .equals(initialIme),
217                 TIMEOUT, initialIme + " is current IME");
218     }
219 
220     /**
221      * Test if uninstalling the currently selected IME then selecting another IME triggers standard
222      * startInput/bindInput sequence.
223      */
224     @Test
testInputUnbindsOnImeStopped()225     public void testInputUnbindsOnImeStopped() throws Throwable {
226         final TestHelper helper = new TestHelper();
227         final long startActivityTime = SystemClock.uptimeMillis();
228         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
229                 EditTextAppConstants.URI);
230         final UiObject2 editText = helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME);
231         editText.click();
232 
233         pollingCheck(() -> helper.queryAllEvents()
234                         .filter(isNewerThan(startActivityTime))
235                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
236                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
237         pollingCheck(() -> helper.queryAllEvents()
238                         .filter(isNewerThan(startActivityTime))
239                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_BIND_INPUT))),
240                 TIMEOUT, "CtsInputMethod1.onBindInput is called");
241 
242         final long imeForceStopTime = SystemClock.uptimeMillis();
243         helper.shell(ShellCommandUtils.uninstallPackage(Ime1Constants.PACKAGE));
244 
245         helper.shell(ShellCommandUtils.setCurrentImeSync(Ime2Constants.IME_ID));
246         editText.click();
247         pollingCheck(() -> helper.queryAllEvents()
248                         .filter(isNewerThan(imeForceStopTime))
249                         .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_START_INPUT))),
250                 TIMEOUT, "CtsInputMethod2.onStartInput is called");
251         pollingCheck(() -> helper.queryAllEvents()
252                         .filter(isNewerThan(imeForceStopTime))
253                         .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_BIND_INPUT))),
254                 TIMEOUT, "CtsInputMethod2.onBindInput is called");
255     }
256 
257     /**
258      * Test if uninstalling the currently running IME client triggers
259      * {@link android.inputmethodservice.InputMethodService#onUnbindInput()}.
260      */
261     @Test
testInputUnbindsOnAppStopped()262     public void testInputUnbindsOnAppStopped() throws Throwable {
263         final TestHelper helper = new TestHelper();
264         final long startActivityTime = SystemClock.uptimeMillis();
265         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
266                 EditTextAppConstants.URI);
267         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
268 
269         pollingCheck(() -> helper.queryAllEvents()
270                         .filter(isNewerThan(startActivityTime))
271                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
272                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
273         pollingCheck(() -> helper.queryAllEvents()
274                         .filter(isNewerThan(startActivityTime))
275                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_BIND_INPUT))),
276                 TIMEOUT, "CtsInputMethod1.onBindInput is called");
277 
278         helper.shell(ShellCommandUtils.uninstallPackage(EditTextAppConstants.PACKAGE));
279 
280         pollingCheck(() -> helper.queryAllEvents()
281                         .filter(isNewerThan(startActivityTime))
282                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_UNBIND_INPUT))),
283                 TIMEOUT, "CtsInputMethod1.onUnBindInput is called");
284     }
285 
286     /**
287      * Test if IMEs remain to be visible after switching to other IMEs.
288      *
289      * <p>Regression test for Bug 152876819.</p>
290      */
291     @Test
testImeVisibilityAfterImeSwitching()292     public void testImeVisibilityAfterImeSwitching() throws Throwable {
293         final TestHelper helper = new TestHelper();
294 
295         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
296                 EditTextAppConstants.URI);
297 
298         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
299 
300         InputMethodVisibilityVerifier.assertIme1Visible(TIMEOUT);
301 
302         // Switch IME from CtsInputMethod1 to CtsInputMethod2.
303         helper.shell(ShellCommandUtils.broadcastIntent(
304                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
305                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD,
306                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
307 
308         InputMethodVisibilityVerifier.assertIme2Visible(TIMEOUT);
309 
310         // Switch IME from CtsInputMethod2 to CtsInputMethod1.
311         helper.shell(ShellCommandUtils.broadcastIntent(
312                 ACTION_IME_COMMAND, Ime2Constants.PACKAGE,
313                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD,
314                 "-e", EXTRA_ARG_STRING1, Ime1Constants.IME_ID));
315 
316         InputMethodVisibilityVerifier.assertIme1Visible(TIMEOUT);
317 
318         // Switch IME from CtsInputMethod1 to CtsInputMethod2.
319         helper.shell(ShellCommandUtils.broadcastIntent(
320                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
321                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD,
322                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
323 
324         InputMethodVisibilityVerifier.assertIme2Visible(TIMEOUT);
325     }
326 
327     /**
328      * Test IME switcher dialog after turning off/on the screen.
329      *
330      * <p>Regression test for Bug 160391516.</p>
331      */
332     @Test
testImeSwitchingWithoutWindowFocusAfterDisplayOffOn()333     public void testImeSwitchingWithoutWindowFocusAfterDisplayOffOn() throws Throwable {
334         final TestHelper helper = new TestHelper();
335 
336         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
337                 EditTextAppConstants.URI);
338 
339         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
340 
341         InputMethodVisibilityVerifier.assertIme1Visible(TIMEOUT);
342 
343         turnScreenOff(helper);
344         turnScreenOn(helper);
345         helper.shell(ShellCommandUtils.dismissKeyguard());
346         helper.shell(ShellCommandUtils.unlockScreen());
347         {
348             final UiObject2 editText = helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME);
349             assumeNotNull("App's view focus behavior after turning off/on the screen is not fully"
350                             + " guaranteed. If the IME is not shown here, just skip this test.",
351                     editText);
352             assumeTrue("App's view focus behavior after turning off/on the screen is not fully"
353                             + " guaranteed. If the IME is not shown here, just skip this test.",
354                     editText.isFocused());
355         }
356 
357         InputMethodVisibilityVerifier.assumeIme1Visible("IME behavior after turning off/on the"
358                 + " screen is not fully guaranteed. If the IME is not shown here, just skip this.",
359                 TIMEOUT);
360 
361         // Emulating IME switching with the IME switcher dialog.  An interesting point is that
362         // the IME target window is not focused when the IME switcher dialog is shown.
363         showInputMethodPicker(helper);
364         helper.shell(ShellCommandUtils.broadcastIntent(
365                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
366                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD,
367                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
368 
369         InputMethodVisibilityVerifier.assertIme2Visible(TIMEOUT);
370     }
371 
372     /**
373      * Build stream collector of {@link DeviceEvent} collecting sequence that elements have
374      * specified types.
375      *
376      * @param types {@link DeviceEventType}s that elements of sequence should have.
377      * @return {@link java.util.stream.Collector} that corrects the sequence.
378      */
sequenceOfTypes( final DeviceEventType... types)379     private static Collector<DeviceEvent, ?, MatchResult<DeviceEvent>> sequenceOfTypes(
380             final DeviceEventType... types) {
381         final IntFunction<Predicate<DeviceEvent>[]> arraySupplier = Predicate[]::new;
382         return SequenceMatcher.of(Arrays.stream(types)
383                 .map(DeviceEvent::isType)
384                 .toArray(arraySupplier));
385     }
386 
387     /**
388      * Call a command to turn screen On.
389      *
390      * This method will wait until the power state is interactive with {@link
391      * PowerManager#isInteractive()}.
392      */
turnScreenOn(TestHelper helper)393     private static void turnScreenOn(TestHelper helper) throws Exception {
394         final Context context = InstrumentationRegistry.getInstrumentation().getContext();
395         final PowerManager pm = context.getSystemService(PowerManager.class);
396         helper.shell(ShellCommandUtils.wakeUp());
397         pollingCheck(() -> pm != null && pm.isInteractive(), TIMEOUT,
398                 "Device does not wake up within the timeout period");
399     }
400 
401     /**
402      * Call a command to turn screen off.
403      *
404      * This method will wait until the power state is *NOT* interactive with
405      * {@link PowerManager#isInteractive()}.
406      * Note that {@link PowerManager#isInteractive()} may not return {@code true} when the device
407      * enables Aod mode, recommend to add (@link DisableScreenDozeRule} in the test to disable Aod
408      * for making power state reliable.
409      */
turnScreenOff(TestHelper helper)410     private static void turnScreenOff(TestHelper helper) throws Exception {
411         final Context context = InstrumentationRegistry.getInstrumentation().getContext();
412         final PowerManager pm = context.getSystemService(PowerManager.class);
413         helper.shell(ShellCommandUtils.sleepDevice());
414         pollingCheck(() -> pm != null && !pm.isInteractive(), TIMEOUT,
415                 "Device does not sleep within the timeout period");
416     }
417 
showInputMethodPicker(TestHelper helper)418     private static void showInputMethodPicker(TestHelper helper) throws Exception {
419         // Test InputMethodManager#showInputMethodPicker() works as expected.
420         helper.shell(ShellCommandUtils.showImePicker());
421         pollingCheck(InputMethodServiceDeviceTest::isInputMethodPickerShown, TIMEOUT,
422                 "InputMethod picker should be shown");
423     }
424 
isInputMethodPickerShown()425     private static boolean isInputMethodPickerShown() {
426         final InputMethodManager imm = InstrumentationRegistry.getInstrumentation().getContext()
427                 .getSystemService(InputMethodManager.class);
428         final UiAutomation uiAutomation =
429                 InstrumentationRegistry.getInstrumentation().getUiAutomation();
430         try {
431             uiAutomation.adoptShellPermissionIdentity();
432             return imm.isInputMethodPickerShown();
433         } catch (Exception e) {
434             throw new RuntimeException("Caught exception", e);
435         } finally {
436             uiAutomation.dropShellPermissionIdentity();
437         }
438     }
439 }
440