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