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