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