1 /* 2 * Copyright (C) 2022 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.accessibilityservice.cts; 18 19 import static android.accessibility.cts.common.InstrumentedAccessibilityService.enableService; 20 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen; 21 22 import static org.junit.Assert.assertTrue; 23 24 import static java.util.concurrent.TimeUnit.MILLISECONDS; 25 26 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule; 27 import android.accessibilityservice.InputMethod; 28 import android.accessibilityservice.cts.activities.AccessibilityEndToEndActivity; 29 import android.accessibilityservice.cts.utils.AsyncUtils; 30 import android.accessibilityservice.cts.utils.InputConnectionSplitter; 31 import android.accessibilityservice.cts.utils.NoOpInputConnection; 32 import android.accessibilityservice.cts.utils.RunOnMainUtils; 33 import android.app.Instrumentation; 34 import android.app.UiAutomation; 35 import android.os.SystemClock; 36 import android.platform.test.annotations.AppModeFull; 37 import android.platform.test.annotations.Presubmit; 38 import android.text.InputType; 39 import android.text.TextUtils; 40 import android.view.KeyCharacterMap; 41 import android.view.KeyEvent; 42 import android.view.inputmethod.EditorInfo; 43 import android.view.inputmethod.InputConnection; 44 import android.widget.EditText; 45 import android.widget.LinearLayout; 46 47 import androidx.test.InstrumentationRegistry; 48 import androidx.test.filters.FlakyTest; 49 import androidx.test.filters.LargeTest; 50 import androidx.test.rule.ActivityTestRule; 51 import androidx.test.runner.AndroidJUnit4; 52 53 import com.android.compatibility.common.util.CddTest; 54 55 import org.junit.AfterClass; 56 import org.junit.Before; 57 import org.junit.BeforeClass; 58 import org.junit.Rule; 59 import org.junit.Test; 60 import org.junit.rules.RuleChain; 61 import org.junit.runner.RunWith; 62 import org.mockito.Mockito; 63 64 import java.util.concurrent.CountDownLatch; 65 import java.util.concurrent.atomic.AtomicReference; 66 67 /** 68 * Tests for {@link InputMethod.AccessibilityInputConnection}. 69 */ 70 @LargeTest 71 @AppModeFull 72 @RunWith(AndroidJUnit4.class) 73 @CddTest(requirements = {"3.10/C-1-1,C-1-2"}) 74 @Presubmit 75 public final class AccessibilityInputConnectionTest { 76 private static Instrumentation sInstrumentation; 77 private static UiAutomation sUiAutomation; 78 79 private static StubImeAccessibilityService sStubImeAccessibilityService; 80 81 private ActivityTestRule<AccessibilityEndToEndActivity> mActivityRule = 82 new ActivityTestRule<>(AccessibilityEndToEndActivity.class, false, false); 83 84 private AccessibilityDumpOnFailureRule mDumpOnFailureRule = 85 new AccessibilityDumpOnFailureRule(); 86 87 private AtomicReference<InputConnection> mLastInputConnectionSpy = new AtomicReference<>(); 88 89 @Rule 90 public final RuleChain mRuleChain = RuleChain 91 .outerRule(mActivityRule) 92 .around(mDumpOnFailureRule); 93 94 @BeforeClass oneTimeSetup()95 public static void oneTimeSetup() throws Exception { 96 sInstrumentation = InstrumentationRegistry.getInstrumentation(); 97 sUiAutomation = sInstrumentation.getUiAutomation(); 98 sInstrumentation 99 .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); 100 sStubImeAccessibilityService = enableService(StubImeAccessibilityService.class); 101 } 102 103 @AfterClass postTestTearDown()104 public static void postTestTearDown() { 105 sStubImeAccessibilityService.disableSelfAndRemove(); 106 sUiAutomation.destroy(); 107 } 108 109 @Before setUp()110 public void setUp() throws Exception { 111 final String markerValue = "Test-" + SystemClock.elapsedRealtimeNanos(); 112 final CountDownLatch startInputLatch = new CountDownLatch(1); 113 sStubImeAccessibilityService.setOnStartInputCallback(((editorInfo, restarting) -> { 114 if (editorInfo != null && TextUtils.equals(markerValue, editorInfo.privateImeOptions)) { 115 startInputLatch.countDown(); 116 } 117 })); 118 119 final AccessibilityEndToEndActivity activity = launchActivityAndWaitForItToBeOnscreen( 120 sInstrumentation, sUiAutomation, mActivityRule); 121 122 final LinearLayout layout = (LinearLayout) activity.findViewById(R.id.edittext).getParent(); 123 sInstrumentation.runOnMainSync(() -> { 124 final EditText editText = new EditText(activity) { 125 @Override 126 public InputConnection onCreateInputConnection(EditorInfo editorInfo) { 127 final InputConnection ic = super.onCreateInputConnection(editorInfo); 128 // For some reasons, Mockito.spy() for real Framework classes did not work... 129 // Use NoOpInputConnection/InputConnectionSplitter instead. 130 final InputConnection spy = Mockito.spy(new NoOpInputConnection()); 131 if (mLastInputConnectionSpy.get() == null) { 132 mLastInputConnectionSpy.set(spy); 133 } 134 return new InputConnectionSplitter(ic, spy); 135 } 136 }; 137 editText.setPrivateImeOptions(markerValue); 138 layout.addView(editText); 139 editText.requestFocus(); 140 }); 141 142 // Wait until EditorInfo#privateImeOptions becomes the expected marker value. 143 assertTrue("time out waiting for input to start", 144 startInputLatch.await(AsyncUtils.DEFAULT_TIMEOUT_MS, MILLISECONDS)); 145 } 146 getInputConnection()147 private InputMethod.AccessibilityInputConnection getInputConnection() { 148 return RunOnMainUtils.getOnMain( 149 sInstrumentation, 150 () -> sStubImeAccessibilityService.getInputMethod().getCurrentInputConnection()); 151 } 152 resetAndGetLastInputConnectionSpy()153 private InputConnection resetAndGetLastInputConnectionSpy() { 154 final InputConnection spy = mLastInputConnectionSpy.get(); 155 Mockito.reset(spy); 156 return spy; 157 } 158 159 @Test testCommitText()160 public void testCommitText() { 161 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 162 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 163 164 ic.commitText("test", 1, null); 165 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 166 .commitText("test", 1, null); 167 } 168 169 @Test testSetSelection()170 public void testSetSelection() { 171 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 172 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 173 174 ic.setSelection(1, 2); 175 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)).setSelection(1, 2); 176 } 177 178 @Test testGetSurroundingText()179 public void testGetSurroundingText() { 180 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 181 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 182 183 ic.getSurroundingText(1, 2, InputConnection.GET_TEXT_WITH_STYLES); 184 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 185 .getSurroundingText(1, 2, InputConnection.GET_TEXT_WITH_STYLES); 186 } 187 188 @Test testDeleteSurroundingText()189 public void testDeleteSurroundingText() { 190 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 191 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 192 193 ic.deleteSurroundingText(2, 1); 194 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 195 .deleteSurroundingText(2, 1); 196 } 197 198 @Test testSendKeyEvent()199 public void testSendKeyEvent() { 200 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 201 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 202 203 final long eventTime = SystemClock.uptimeMillis(); 204 final KeyEvent keyEvent = new KeyEvent(eventTime, eventTime, 205 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, 206 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 207 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); 208 209 ic.sendKeyEvent(keyEvent); 210 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 211 .sendKeyEvent(keyEvent); 212 } 213 214 @Test 215 @FlakyTest testPerformEditorAction()216 public void testPerformEditorAction() { 217 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 218 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 219 220 ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 221 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 222 .performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 223 } 224 225 @Test testPerformContextMenuAction()226 public void testPerformContextMenuAction() { 227 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 228 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 229 230 ic.performContextMenuAction(android.R.id.selectAll); 231 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 232 .performContextMenuAction(android.R.id.selectAll); 233 } 234 235 @Test testGetCursorCapsMode()236 public void testGetCursorCapsMode() { 237 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 238 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 239 240 ic.getCursorCapsMode(InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); 241 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 242 .getCursorCapsMode(InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); 243 } 244 245 @Test testClearMetaKeyStates()246 public void testClearMetaKeyStates() { 247 final InputMethod.AccessibilityInputConnection ic = getInputConnection(); 248 final InputConnection spy = resetAndGetLastInputConnectionSpy(); 249 250 ic.clearMetaKeyStates(KeyEvent.META_SHIFT_ON); 251 Mockito.verify(spy, Mockito.timeout(AsyncUtils.DEFAULT_TIMEOUT_MS)) 252 .clearMetaKeyStates(KeyEvent.META_SHIFT_ON); 253 } 254 } 255