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