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