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