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 android.inputmethodservice.cts.DeviceEvent;
38 import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType;
39 import android.inputmethodservice.cts.common.EditTextAppConstants;
40 import android.inputmethodservice.cts.common.Ime1Constants;
41 import android.inputmethodservice.cts.common.Ime2Constants;
42 import android.inputmethodservice.cts.common.test.ShellCommandUtils;
43 import android.inputmethodservice.cts.devicetest.SequenceMatcher.MatchResult;
44 import android.os.SystemClock;
45 import android.support.test.uiautomator.UiObject2;
46 import android.view.inputmethod.InputMethodSubtype;
47 
48 import androidx.test.runner.AndroidJUnit4;
49 
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 
53 import java.util.Arrays;
54 import java.util.concurrent.TimeUnit;
55 import java.util.function.IntFunction;
56 import java.util.function.Predicate;
57 import java.util.stream.Collector;
58 
59 /**
60  * Test general lifecycle events around InputMethodService.
61  */
62 @RunWith(AndroidJUnit4.class)
63 public class InputMethodServiceDeviceTest {
64 
65     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(7);
66 
67     /** Test to check CtsInputMethod1 receives onCreate and onStartInput. */
68     @Test
testCreateIme1()69     public void testCreateIme1() throws Throwable {
70         final TestHelper helper = new TestHelper();
71 
72         final long startActivityTime = SystemClock.uptimeMillis();
73         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
74                 EditTextAppConstants.URI);
75 
76         pollingCheck(() -> helper.queryAllEvents()
77                         .collect(startingFrom(helper.isStartOfTest()))
78                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_CREATE))),
79                 TIMEOUT, "CtsInputMethod1.onCreate is called");
80         pollingCheck(() -> helper.queryAllEvents()
81                         .filter(isNewerThan(startActivityTime))
82                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
83                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
84     }
85 
86     /** Test to check IME is switched from CtsInputMethod1 to CtsInputMethod2. */
87     @Test
testSwitchIme1ToIme2()88     public void testSwitchIme1ToIme2() throws Throwable {
89         final TestHelper helper = new TestHelper();
90 
91         final long startActivityTime = SystemClock.uptimeMillis();
92         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
93                 EditTextAppConstants.URI);
94 
95         pollingCheck(() -> helper.queryAllEvents()
96                         .collect(startingFrom(helper.isStartOfTest()))
97                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_CREATE))),
98                 TIMEOUT, "CtsInputMethod1.onCreate is called");
99         pollingCheck(() -> helper.queryAllEvents()
100                         .filter(isNewerThan(startActivityTime))
101                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
102                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
103 
104         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
105 
106         // Switch IME from CtsInputMethod1 to CtsInputMethod2.
107         final long switchImeTime = SystemClock.uptimeMillis();
108         helper.shell(ShellCommandUtils.broadcastIntent(
109                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
110                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD,
111                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
112 
113         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
114                         .equals(Ime2Constants.IME_ID),
115                 TIMEOUT, "CtsInputMethod2 is current IME");
116         pollingCheck(() -> helper.queryAllEvents()
117                         .filter(isNewerThan(switchImeTime))
118                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_DESTROY))),
119                 TIMEOUT, "CtsInputMethod1.onDestroy is called");
120         pollingCheck(() -> helper.queryAllEvents()
121                         .filter(isNewerThan(switchImeTime))
122                         .filter(isFrom(Ime2Constants.CLASS))
123                         .collect(sequenceOfTypes(ON_CREATE, ON_BIND_INPUT, ON_START_INPUT))
124                         .matched(),
125                 TIMEOUT,
126                 "CtsInputMethod2.onCreate, onBindInput, and onStartInput are called"
127                         + " in sequence");
128     }
129 
130     /**
131      * Test {@link android.inputmethodservice.InputMethodService#switchInputMethod(String,
132      * InputMethodSubtype)}.
133      */
134     @Test
testSwitchInputMethod()135     public void testSwitchInputMethod() throws Throwable {
136         final TestHelper helper = new TestHelper();
137         final long startActivityTime = SystemClock.uptimeMillis();
138         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
139                 EditTextAppConstants.URI);
140         pollingCheck(() -> helper.queryAllEvents()
141                         .filter(isNewerThan(startActivityTime))
142                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
143                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
144         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
145 
146         final long setImeTime = SystemClock.uptimeMillis();
147         // call setInputMethodAndSubtype(IME2, null)
148         helper.shell(ShellCommandUtils.broadcastIntent(
149                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
150                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD_WITH_SUBTYPE,
151                 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID));
152         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
153                         .equals(Ime2Constants.IME_ID),
154                 TIMEOUT, "CtsInputMethod2 is current IME");
155         pollingCheck(() -> helper.queryAllEvents()
156                         .filter(isNewerThan(setImeTime))
157                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_DESTROY))),
158                 TIMEOUT, "CtsInputMethod1.onDestroy is called");
159     }
160 
161     /**
162      * Test {@link android.inputmethodservice.InputMethodService#switchToNextInputMethod(boolean)}.
163      */
164     @Test
testSwitchToNextInputMethod()165     public void testSwitchToNextInputMethod() throws Throwable {
166         final TestHelper helper = new TestHelper();
167         final long startActivityTime = SystemClock.uptimeMillis();
168         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
169                 EditTextAppConstants.URI);
170         pollingCheck(() -> helper.queryAllEvents()
171                         .filter(isNewerThan(startActivityTime))
172                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
173                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
174         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
175 
176         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
177                         .equals(Ime1Constants.IME_ID),
178                 TIMEOUT, "CtsInputMethod1 is current IME");
179         helper.shell(ShellCommandUtils.broadcastIntent(
180                 ACTION_IME_COMMAND, Ime1Constants.PACKAGE,
181                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_TO_NEXT_INPUT));
182         pollingCheck(() -> !helper.shell(ShellCommandUtils.getCurrentIme())
183                         .equals(Ime1Constants.IME_ID),
184                 TIMEOUT, "CtsInputMethod1 shouldn't be current IME");
185     }
186 
187     /**
188      * Test {@link android.inputmethodservice.InputMethodService#switchToPreviousInputMethod()}.
189      */
190     @Test
switchToPreviousInputMethod()191     public void switchToPreviousInputMethod() throws Throwable {
192         final TestHelper helper = new TestHelper();
193         final long startActivityTime = SystemClock.uptimeMillis();
194         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
195                 EditTextAppConstants.URI);
196         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
197 
198         final String initialIme = helper.shell(ShellCommandUtils.getCurrentIme());
199         helper.shell(ShellCommandUtils.setCurrentImeSync(Ime2Constants.IME_ID));
200         pollingCheck(() -> helper.queryAllEvents()
201                         .filter(isNewerThan(startActivityTime))
202                         .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_START_INPUT))),
203                 TIMEOUT, "CtsInputMethod2.onStartInput is called");
204         helper.shell(ShellCommandUtils.broadcastIntent(
205                 ACTION_IME_COMMAND, Ime2Constants.PACKAGE,
206                 "-e", EXTRA_COMMAND, COMMAND_SWITCH_TO_PREVIOUS_INPUT));
207         pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme())
208                         .equals(initialIme),
209                 TIMEOUT, initialIme + " is current IME");
210     }
211 
212     /**
213      * Test if uninstalling the currently selected IME then selecting another IME triggers standard
214      * startInput/bindInput sequence.
215      */
216     @Test
testInputUnbindsOnImeStopped()217     public void testInputUnbindsOnImeStopped() throws Throwable {
218         final TestHelper helper = new TestHelper();
219         final long startActivityTime = SystemClock.uptimeMillis();
220         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
221                 EditTextAppConstants.URI);
222         final UiObject2 editText = helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME);
223         editText.click();
224 
225         pollingCheck(() -> helper.queryAllEvents()
226                         .filter(isNewerThan(startActivityTime))
227                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
228                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
229         pollingCheck(() -> helper.queryAllEvents()
230                         .filter(isNewerThan(startActivityTime))
231                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_BIND_INPUT))),
232                 TIMEOUT, "CtsInputMethod1.onBindInput is called");
233 
234         final long imeForceStopTime = SystemClock.uptimeMillis();
235         helper.shell(ShellCommandUtils.uninstallPackage(Ime1Constants.PACKAGE));
236 
237         helper.shell(ShellCommandUtils.setCurrentImeSync(Ime2Constants.IME_ID));
238         editText.click();
239         pollingCheck(() -> helper.queryAllEvents()
240                         .filter(isNewerThan(imeForceStopTime))
241                         .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_START_INPUT))),
242                 TIMEOUT, "CtsInputMethod2.onStartInput is called");
243         pollingCheck(() -> helper.queryAllEvents()
244                         .filter(isNewerThan(imeForceStopTime))
245                         .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_BIND_INPUT))),
246                 TIMEOUT, "CtsInputMethod2.onBindInput is called");
247     }
248 
249     /**
250      * Test if uninstalling the currently running IME client triggers
251      * {@link android.inputmethodservice.InputMethodService#onUnbindInput()}.
252      */
253     @Test
testInputUnbindsOnAppStopped()254     public void testInputUnbindsOnAppStopped() throws Throwable {
255         final TestHelper helper = new TestHelper();
256         final long startActivityTime = SystemClock.uptimeMillis();
257         helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS,
258                 EditTextAppConstants.URI);
259         helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click();
260 
261         pollingCheck(() -> helper.queryAllEvents()
262                         .filter(isNewerThan(startActivityTime))
263                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))),
264                 TIMEOUT, "CtsInputMethod1.onStartInput is called");
265         pollingCheck(() -> helper.queryAllEvents()
266                         .filter(isNewerThan(startActivityTime))
267                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_BIND_INPUT))),
268                 TIMEOUT, "CtsInputMethod1.onBindInput is called");
269 
270         helper.shell(ShellCommandUtils.uninstallPackage(EditTextAppConstants.PACKAGE));
271 
272         pollingCheck(() -> helper.queryAllEvents()
273                         .filter(isNewerThan(startActivityTime))
274                         .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_UNBIND_INPUT))),
275                 TIMEOUT, "CtsInputMethod1.onUnBindInput is called");
276     }
277 
278     /**
279      * Build stream collector of {@link DeviceEvent} collecting sequence that elements have
280      * specified types.
281      *
282      * @param types {@link DeviceEventType}s that elements of sequence should have.
283      * @return {@link java.util.stream.Collector} that corrects the sequence.
284      */
sequenceOfTypes( final DeviceEventType... types)285     private static Collector<DeviceEvent, ?, MatchResult<DeviceEvent>> sequenceOfTypes(
286             final DeviceEventType... types) {
287         final IntFunction<Predicate<DeviceEvent>[]> arraySupplier = Predicate[]::new;
288         return SequenceMatcher.of(Arrays.stream(types)
289                 .map(DeviceEvent::isType)
290                 .toArray(arraySupplier));
291     }
292 }
293