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 android.inputmethodservice.cts.DeviceEvent; 38 import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType; 39 import android.inputmethodservice.cts.common.EditTextAppConstants; 40 import android.inputmethodservice.cts.common.Ime1Constants; 41 import android.inputmethodservice.cts.common.Ime2Constants; 42 import android.inputmethodservice.cts.common.test.ShellCommandUtils; 43 import android.inputmethodservice.cts.devicetest.SequenceMatcher.MatchResult; 44 import android.os.SystemClock; 45 import android.support.test.uiautomator.UiObject2; 46 import android.view.inputmethod.InputMethodSubtype; 47 48 import androidx.test.runner.AndroidJUnit4; 49 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 53 import java.util.Arrays; 54 import java.util.concurrent.TimeUnit; 55 import java.util.function.IntFunction; 56 import java.util.function.Predicate; 57 import java.util.stream.Collector; 58 59 /** 60 * Test general lifecycle events around InputMethodService. 61 */ 62 @RunWith(AndroidJUnit4.class) 63 public class InputMethodServiceDeviceTest { 64 65 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(7); 66 67 /** Test to check CtsInputMethod1 receives onCreate and onStartInput. */ 68 @Test testCreateIme1()69 public void testCreateIme1() throws Throwable { 70 final TestHelper helper = new TestHelper(); 71 72 final long startActivityTime = SystemClock.uptimeMillis(); 73 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 74 EditTextAppConstants.URI); 75 76 pollingCheck(() -> helper.queryAllEvents() 77 .collect(startingFrom(helper.isStartOfTest())) 78 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_CREATE))), 79 TIMEOUT, "CtsInputMethod1.onCreate is called"); 80 pollingCheck(() -> helper.queryAllEvents() 81 .filter(isNewerThan(startActivityTime)) 82 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))), 83 TIMEOUT, "CtsInputMethod1.onStartInput is called"); 84 } 85 86 /** Test to check IME is switched from CtsInputMethod1 to CtsInputMethod2. */ 87 @Test testSwitchIme1ToIme2()88 public void testSwitchIme1ToIme2() throws Throwable { 89 final TestHelper helper = new TestHelper(); 90 91 final long startActivityTime = SystemClock.uptimeMillis(); 92 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 93 EditTextAppConstants.URI); 94 95 pollingCheck(() -> helper.queryAllEvents() 96 .collect(startingFrom(helper.isStartOfTest())) 97 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_CREATE))), 98 TIMEOUT, "CtsInputMethod1.onCreate is called"); 99 pollingCheck(() -> helper.queryAllEvents() 100 .filter(isNewerThan(startActivityTime)) 101 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))), 102 TIMEOUT, "CtsInputMethod1.onStartInput is called"); 103 104 helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click(); 105 106 // Switch IME from CtsInputMethod1 to CtsInputMethod2. 107 final long switchImeTime = SystemClock.uptimeMillis(); 108 helper.shell(ShellCommandUtils.broadcastIntent( 109 ACTION_IME_COMMAND, Ime1Constants.PACKAGE, 110 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD, 111 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID)); 112 113 pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme()) 114 .equals(Ime2Constants.IME_ID), 115 TIMEOUT, "CtsInputMethod2 is current IME"); 116 pollingCheck(() -> helper.queryAllEvents() 117 .filter(isNewerThan(switchImeTime)) 118 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_DESTROY))), 119 TIMEOUT, "CtsInputMethod1.onDestroy is called"); 120 pollingCheck(() -> helper.queryAllEvents() 121 .filter(isNewerThan(switchImeTime)) 122 .filter(isFrom(Ime2Constants.CLASS)) 123 .collect(sequenceOfTypes(ON_CREATE, ON_BIND_INPUT, ON_START_INPUT)) 124 .matched(), 125 TIMEOUT, 126 "CtsInputMethod2.onCreate, onBindInput, and onStartInput are called" 127 + " in sequence"); 128 } 129 130 /** 131 * Test {@link android.inputmethodservice.InputMethodService#switchInputMethod(String, 132 * InputMethodSubtype)}. 133 */ 134 @Test testSwitchInputMethod()135 public void testSwitchInputMethod() throws Throwable { 136 final TestHelper helper = new TestHelper(); 137 final long startActivityTime = SystemClock.uptimeMillis(); 138 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 139 EditTextAppConstants.URI); 140 pollingCheck(() -> helper.queryAllEvents() 141 .filter(isNewerThan(startActivityTime)) 142 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))), 143 TIMEOUT, "CtsInputMethod1.onStartInput is called"); 144 helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click(); 145 146 final long setImeTime = SystemClock.uptimeMillis(); 147 // call setInputMethodAndSubtype(IME2, null) 148 helper.shell(ShellCommandUtils.broadcastIntent( 149 ACTION_IME_COMMAND, Ime1Constants.PACKAGE, 150 "-e", EXTRA_COMMAND, COMMAND_SWITCH_INPUT_METHOD_WITH_SUBTYPE, 151 "-e", EXTRA_ARG_STRING1, Ime2Constants.IME_ID)); 152 pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme()) 153 .equals(Ime2Constants.IME_ID), 154 TIMEOUT, "CtsInputMethod2 is current IME"); 155 pollingCheck(() -> helper.queryAllEvents() 156 .filter(isNewerThan(setImeTime)) 157 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_DESTROY))), 158 TIMEOUT, "CtsInputMethod1.onDestroy is called"); 159 } 160 161 /** 162 * Test {@link android.inputmethodservice.InputMethodService#switchToNextInputMethod(boolean)}. 163 */ 164 @Test testSwitchToNextInputMethod()165 public void testSwitchToNextInputMethod() throws Throwable { 166 final TestHelper helper = new TestHelper(); 167 final long startActivityTime = SystemClock.uptimeMillis(); 168 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 169 EditTextAppConstants.URI); 170 pollingCheck(() -> helper.queryAllEvents() 171 .filter(isNewerThan(startActivityTime)) 172 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))), 173 TIMEOUT, "CtsInputMethod1.onStartInput is called"); 174 helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click(); 175 176 pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme()) 177 .equals(Ime1Constants.IME_ID), 178 TIMEOUT, "CtsInputMethod1 is current IME"); 179 helper.shell(ShellCommandUtils.broadcastIntent( 180 ACTION_IME_COMMAND, Ime1Constants.PACKAGE, 181 "-e", EXTRA_COMMAND, COMMAND_SWITCH_TO_NEXT_INPUT)); 182 pollingCheck(() -> !helper.shell(ShellCommandUtils.getCurrentIme()) 183 .equals(Ime1Constants.IME_ID), 184 TIMEOUT, "CtsInputMethod1 shouldn't be current IME"); 185 } 186 187 /** 188 * Test {@link android.inputmethodservice.InputMethodService#switchToPreviousInputMethod()}. 189 */ 190 @Test switchToPreviousInputMethod()191 public void switchToPreviousInputMethod() throws Throwable { 192 final TestHelper helper = new TestHelper(); 193 final long startActivityTime = SystemClock.uptimeMillis(); 194 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 195 EditTextAppConstants.URI); 196 helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click(); 197 198 final String initialIme = helper.shell(ShellCommandUtils.getCurrentIme()); 199 helper.shell(ShellCommandUtils.setCurrentImeSync(Ime2Constants.IME_ID)); 200 pollingCheck(() -> helper.queryAllEvents() 201 .filter(isNewerThan(startActivityTime)) 202 .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_START_INPUT))), 203 TIMEOUT, "CtsInputMethod2.onStartInput is called"); 204 helper.shell(ShellCommandUtils.broadcastIntent( 205 ACTION_IME_COMMAND, Ime2Constants.PACKAGE, 206 "-e", EXTRA_COMMAND, COMMAND_SWITCH_TO_PREVIOUS_INPUT)); 207 pollingCheck(() -> helper.shell(ShellCommandUtils.getCurrentIme()) 208 .equals(initialIme), 209 TIMEOUT, initialIme + " is current IME"); 210 } 211 212 /** 213 * Test if uninstalling the currently selected IME then selecting another IME triggers standard 214 * startInput/bindInput sequence. 215 */ 216 @Test testInputUnbindsOnImeStopped()217 public void testInputUnbindsOnImeStopped() throws Throwable { 218 final TestHelper helper = new TestHelper(); 219 final long startActivityTime = SystemClock.uptimeMillis(); 220 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 221 EditTextAppConstants.URI); 222 final UiObject2 editText = helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME); 223 editText.click(); 224 225 pollingCheck(() -> helper.queryAllEvents() 226 .filter(isNewerThan(startActivityTime)) 227 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))), 228 TIMEOUT, "CtsInputMethod1.onStartInput is called"); 229 pollingCheck(() -> helper.queryAllEvents() 230 .filter(isNewerThan(startActivityTime)) 231 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_BIND_INPUT))), 232 TIMEOUT, "CtsInputMethod1.onBindInput is called"); 233 234 final long imeForceStopTime = SystemClock.uptimeMillis(); 235 helper.shell(ShellCommandUtils.uninstallPackage(Ime1Constants.PACKAGE)); 236 237 helper.shell(ShellCommandUtils.setCurrentImeSync(Ime2Constants.IME_ID)); 238 editText.click(); 239 pollingCheck(() -> helper.queryAllEvents() 240 .filter(isNewerThan(imeForceStopTime)) 241 .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_START_INPUT))), 242 TIMEOUT, "CtsInputMethod2.onStartInput is called"); 243 pollingCheck(() -> helper.queryAllEvents() 244 .filter(isNewerThan(imeForceStopTime)) 245 .anyMatch(isFrom(Ime2Constants.CLASS).and(isType(ON_BIND_INPUT))), 246 TIMEOUT, "CtsInputMethod2.onBindInput is called"); 247 } 248 249 /** 250 * Test if uninstalling the currently running IME client triggers 251 * {@link android.inputmethodservice.InputMethodService#onUnbindInput()}. 252 */ 253 @Test testInputUnbindsOnAppStopped()254 public void testInputUnbindsOnAppStopped() throws Throwable { 255 final TestHelper helper = new TestHelper(); 256 final long startActivityTime = SystemClock.uptimeMillis(); 257 helper.launchActivity(EditTextAppConstants.PACKAGE, EditTextAppConstants.CLASS, 258 EditTextAppConstants.URI); 259 helper.findUiObject(EditTextAppConstants.EDIT_TEXT_RES_NAME).click(); 260 261 pollingCheck(() -> helper.queryAllEvents() 262 .filter(isNewerThan(startActivityTime)) 263 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT))), 264 TIMEOUT, "CtsInputMethod1.onStartInput is called"); 265 pollingCheck(() -> helper.queryAllEvents() 266 .filter(isNewerThan(startActivityTime)) 267 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_BIND_INPUT))), 268 TIMEOUT, "CtsInputMethod1.onBindInput is called"); 269 270 helper.shell(ShellCommandUtils.uninstallPackage(EditTextAppConstants.PACKAGE)); 271 272 pollingCheck(() -> helper.queryAllEvents() 273 .filter(isNewerThan(startActivityTime)) 274 .anyMatch(isFrom(Ime1Constants.CLASS).and(isType(ON_UNBIND_INPUT))), 275 TIMEOUT, "CtsInputMethod1.onUnBindInput is called"); 276 } 277 278 /** 279 * Build stream collector of {@link DeviceEvent} collecting sequence that elements have 280 * specified types. 281 * 282 * @param types {@link DeviceEventType}s that elements of sequence should have. 283 * @return {@link java.util.stream.Collector} that corrects the sequence. 284 */ sequenceOfTypes( final DeviceEventType... types)285 private static Collector<DeviceEvent, ?, MatchResult<DeviceEvent>> sequenceOfTypes( 286 final DeviceEventType... types) { 287 final IntFunction<Predicate<DeviceEvent>[]> arraySupplier = Predicate[]::new; 288 return SequenceMatcher.of(Arrays.stream(types) 289 .map(DeviceEvent::isType) 290 .toArray(arraySupplier)); 291 } 292 } 293