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