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