1 /**
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11  * express or implied. See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 
15 package android.accessibilityservice.cts;
16 
17 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction;
18 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
19 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
20 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
21 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
22 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
23 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.assertFalse;
27 import static org.junit.Assert.assertNotNull;
28 import static org.junit.Assert.assertNull;
29 import static org.junit.Assert.assertTrue;
30 import static org.junit.Assert.fail;
31 
32 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
33 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
34 import android.accessibilityservice.AccessibilityServiceInfo;
35 import android.accessibilityservice.cts.activities.AccessibilityFocusAndInputFocusSyncActivity;
36 import android.app.Instrumentation;
37 import android.app.UiAutomation;
38 import android.content.Context;
39 import android.graphics.Bitmap;
40 import android.graphics.Color;
41 import android.graphics.Point;
42 import android.os.Environment;
43 import android.os.SystemClock;
44 import android.platform.test.annotations.Presubmit;
45 import android.view.Display;
46 import android.view.View;
47 import android.view.accessibility.AccessibilityManager;
48 import android.view.accessibility.AccessibilityNodeInfo;
49 
50 import androidx.test.InstrumentationRegistry;
51 import androidx.test.filters.MediumTest;
52 import androidx.test.rule.ActivityTestRule;
53 import androidx.test.runner.AndroidJUnit4;
54 
55 import com.android.compatibility.common.util.BitmapUtils;
56 import com.android.compatibility.common.util.CddTest;
57 import com.android.compatibility.common.util.PollingCheck;
58 
59 import org.junit.AfterClass;
60 import org.junit.Before;
61 import org.junit.BeforeClass;
62 import org.junit.Rule;
63 import org.junit.Test;
64 import org.junit.rules.RuleChain;
65 import org.junit.rules.TestName;
66 import org.junit.runner.RunWith;
67 
68 import java.util.LinkedList;
69 import java.util.Queue;
70 import java.util.concurrent.atomic.AtomicBoolean;
71 
72 /**
73  * Test cases for testing the accessibility focus APIs exposed to accessibility
74  * services. These APIs allow moving accessibility focus in the view tree from
75  * an AccessiiblityService. Specifically, this activity is for verifying the the
76  * sync between accessibility and input focus.
77  */
78 @RunWith(AndroidJUnit4.class)
79 @CddTest(requirements = {"3.10/C-1-1,C-1-2"})
80 @Presubmit
81 public class AccessibilityFocusAndInputFocusSyncTest {
82     /**
83      * The delay time is for next UI frame rendering out.
84      */
85     private static final long SCREEN_FRAME_RENDERING_OUT_TIME_MILLIS = 500;
86 
87     private static Instrumentation sInstrumentation;
88     private static UiAutomation sUiAutomation;
89     private static Context sContext;
90     private static AccessibilityManager sAccessibilityManager;
91     private static int sFocusStrokeWidthDefaultValue;
92     private static int sFocusColorDefaultValue;
93 
94     private AccessibilityFocusAndInputFocusSyncActivity mActivity;
95 
96     private ActivityTestRule<AccessibilityFocusAndInputFocusSyncActivity> mActivityRule =
97             new ActivityTestRule<>(AccessibilityFocusAndInputFocusSyncActivity.class, false, false);
98 
99     private InstrumentedAccessibilityServiceTestRule<StubFocusIndicatorService>
100             mFocusIndicatorServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
101             StubFocusIndicatorService.class, false);
102 
103     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
104             new AccessibilityDumpOnFailureRule();
105 
106     @Rule
107     public final RuleChain mRuleChain = RuleChain
108             .outerRule(mActivityRule)
109             .around(mFocusIndicatorServiceRule)
110             .around(mDumpOnFailureRule);
111 
112     /* Test name rule that tracks the current test method under execution */
113     @Rule public TestName mTestName = new TestName();
114 
115     @BeforeClass
oneTimeSetup()116     public static void oneTimeSetup() throws Exception {
117         sInstrumentation = InstrumentationRegistry.getInstrumentation();
118         sUiAutomation = sInstrumentation.getUiAutomation(
119                 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
120 
121         sContext = sInstrumentation.getContext();
122         sAccessibilityManager = sContext.getSystemService(AccessibilityManager.class);
123         assertNotNull(sAccessibilityManager);
124         sFocusStrokeWidthDefaultValue = sAccessibilityManager.getAccessibilityFocusStrokeWidth();
125         sFocusColorDefaultValue = sAccessibilityManager.getAccessibilityFocusColor();
126     }
127 
128     @AfterClass
postTestTearDown()129     public static void postTestTearDown() {
130         sUiAutomation.destroy();
131     }
132 
133     @Before
setUp()134     public void setUp() throws Exception {
135         AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
136         info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
137         info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
138         sUiAutomation.setServiceInfo(info);
139 
140         mActivity = launchActivityAndWaitForItToBeOnscreen(
141                 sInstrumentation, sUiAutomation, mActivityRule);
142     }
143 
144     @MediumTest
145     @Test
testFindAccessibilityFocus()146     public void testFindAccessibilityFocus() throws Exception {
147         sInstrumentation.runOnMainSync(() -> {
148             mActivity.findViewById(R.id.firstEditText).requestFocus();
149         });
150         // Get the view that has input and accessibility focus.
151         final AccessibilityNodeInfo expected = sUiAutomation
152                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
153                         sContext.getString(R.string.firstEditText)).get(0);
154         assertNotNull(expected);
155         assertFalse(expected.isAccessibilityFocused());
156         assertTrue(expected.isFocused());
157 
158         sUiAutomation.executeAndWaitForEvent(
159                 () -> assertTrue(expected.performAction(ACTION_ACCESSIBILITY_FOCUS)),
160                 filterForEventTypeWithAction(
161                         TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
162                 DEFAULT_TIMEOUT_MS);
163 
164         // Get the second expected node info.
165         AccessibilityNodeInfo received = sUiAutomation
166                 .getRootInActiveWindow().findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY);
167         assertNotNull(received);
168         assertTrue(received.isAccessibilityFocused());
169 
170         // Make sure we got the expected focusable.
171         assertEquals(expected, received);
172     }
173 
174     @MediumTest
175     @Test
testInitialStateNoAccessibilityFocus()176     public void testInitialStateNoAccessibilityFocus() throws Exception {
177         // Get the root which is only accessibility focused.
178         AccessibilityNodeInfo focused = sUiAutomation
179                 .getRootInActiveWindow().findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY);
180         assertNull(focused);
181     }
182 
183     @MediumTest
184     @Test
testActionAccessibilityFocus()185     public void testActionAccessibilityFocus() throws Exception {
186         // Get the root linear layout info.
187         final AccessibilityNodeInfo rootLinearLayout = sUiAutomation
188                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
189                         sContext.getString(R.string.rootLinearLayout)).get(0);
190         assertNotNull(rootLinearLayout);
191         assertFalse(rootLinearLayout.isAccessibilityFocused());
192 
193         sUiAutomation.executeAndWaitForEvent(
194                 () -> assertTrue(rootLinearLayout.performAction(ACTION_ACCESSIBILITY_FOCUS)),
195                 filterForEventTypeWithAction(
196                         TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
197                 DEFAULT_TIMEOUT_MS);
198 
199         // Get the node info again.
200         rootLinearLayout.refresh();
201 
202         // Check if the node info is focused.
203         assertTrue(rootLinearLayout.isAccessibilityFocused());
204     }
205 
206     @MediumTest
207     @Test
testActionClearAccessibilityFocus()208     public void testActionClearAccessibilityFocus() throws Exception {
209         // Get the root linear layout info.
210         final AccessibilityNodeInfo rootLinearLayout = sUiAutomation
211                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
212                         sContext.getString(R.string.rootLinearLayout)).get(0);
213         assertNotNull(rootLinearLayout);
214 
215         sUiAutomation.executeAndWaitForEvent(
216                 () -> assertTrue(rootLinearLayout.performAction(ACTION_ACCESSIBILITY_FOCUS)),
217                 filterForEventTypeWithAction(
218                         TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
219                 DEFAULT_TIMEOUT_MS);
220 
221         // Refresh the node info.
222         rootLinearLayout.refresh();
223 
224         // Check if the node info is focused.
225         assertTrue(rootLinearLayout.isAccessibilityFocused());
226 
227         sUiAutomation.executeAndWaitForEvent(
228                 () -> assertTrue(rootLinearLayout.performAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS)),
229                 filterForEventTypeWithAction(
230                         TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, ACTION_CLEAR_ACCESSIBILITY_FOCUS),
231                 DEFAULT_TIMEOUT_MS);
232 
233         // Refresh the node info.
234         rootLinearLayout.refresh();
235 
236         // Check if the node info is not focused.
237         assertFalse(rootLinearLayout.isAccessibilityFocused());
238     }
239 
240     @MediumTest
241     @Test
testOnlyOneNodeHasAccessibilityFocus()242     public void testOnlyOneNodeHasAccessibilityFocus() throws Exception {
243         // Get the first not focused edit text.
244         final AccessibilityNodeInfo firstEditText = sUiAutomation
245                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
246                         sContext.getString(R.string.firstEditText)).get(0);
247         assertNotNull(firstEditText);
248         assertTrue(firstEditText.isFocusable());
249         assertFalse(firstEditText.isAccessibilityFocused());
250 
251         sUiAutomation.executeAndWaitForEvent(
252                 () -> assertTrue(firstEditText.performAction(ACTION_ACCESSIBILITY_FOCUS)),
253                 filterForEventTypeWithAction(
254                         TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
255                 DEFAULT_TIMEOUT_MS);
256 
257         // Get the second not focused edit text.
258         final AccessibilityNodeInfo secondEditText = sUiAutomation
259                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
260                         sContext.getString(R.string.secondEditText)).get(0);
261         assertNotNull(secondEditText);
262         assertTrue(secondEditText.isFocusable());
263         assertFalse(secondEditText.isFocused());
264         assertFalse(secondEditText.isAccessibilityFocused());
265 
266         sUiAutomation.executeAndWaitForEvent(
267                 () -> assertTrue(secondEditText.performAction(ACTION_ACCESSIBILITY_FOCUS)),
268                 filterForEventTypeWithAction(
269                         TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
270                 DEFAULT_TIMEOUT_MS);
271 
272         // Get the node info again.
273         secondEditText.refresh();
274 
275         // Make sure no other node has accessibility focus.
276         AccessibilityNodeInfo root = sUiAutomation.getRootInActiveWindow();
277         Queue<AccessibilityNodeInfo> workQueue = new LinkedList<AccessibilityNodeInfo>();
278         workQueue.add(root);
279         while (!workQueue.isEmpty()) {
280             AccessibilityNodeInfo current = workQueue.poll();
281             if (current.isAccessibilityFocused() && !current.equals(secondEditText)) {
282                 fail();
283             }
284             final int childCount = current.getChildCount();
285             for (int i = 0; i < childCount; i++) {
286                 AccessibilityNodeInfo child = current.getChild(i);
287                 if (child != null) {
288                     workQueue.offer(child);
289                 }
290             }
291         }
292     }
293 
294     @Test
testScreenReaderFocusableAttribute_reportedToAccessibility()295     public void testScreenReaderFocusableAttribute_reportedToAccessibility() {
296         final AccessibilityNodeInfo secondButton = sUiAutomation.getRootInActiveWindow()
297                 .findAccessibilityNodeInfosByText(
298                         sContext.getString(R.string.secondButton)).get(0);
299         assertTrue("Screen reader focusability not propagated from xml to accessibility",
300                 secondButton.isScreenReaderFocusable());
301 
302         // Verify the setter and getter work
303         final AtomicBoolean isScreenReaderFocusableAtomic = new AtomicBoolean(false);
304         sInstrumentation.runOnMainSync(() -> {
305             View secondButtonView = mActivity.findViewById(R.id.secondButton);
306             secondButtonView.setScreenReaderFocusable(false);
307             isScreenReaderFocusableAtomic.set(secondButtonView.isScreenReaderFocusable());
308         });
309 
310         assertFalse("isScreenReaderFocusable did not change after value set",
311                 isScreenReaderFocusableAtomic.get());
312 
313         secondButton.refresh();
314         assertFalse(
315                 "Screen reader focusability not propagated to accessibility after calling setter",
316                 secondButton.isScreenReaderFocusable());
317     }
318 
319     @Test
testSetFocusAppearanceDataAfterServiceEnabled()320     public void testSetFocusAppearanceDataAfterServiceEnabled() {
321         final StubFocusIndicatorService service =
322                 mFocusIndicatorServiceRule.enableService();
323         final int focusColor = sFocusColorDefaultValue == Color.BLUE ? Color.RED : Color.BLUE;
324 
325         try {
326             setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue + 10,
327                     focusColor);
328         } finally {
329             setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue,
330                     sFocusColorDefaultValue);
331 
332             service.disableSelfAndRemove();
333         }
334     }
335 
336     @Test
testChangeFocusColor_expectedColorIsChanged()337     public void testChangeFocusColor_expectedColorIsChanged() throws Exception {
338         final StubFocusIndicatorService service =
339                 mFocusIndicatorServiceRule.enableService();
340 
341         try {
342             // Get the root linear layout info.
343             final AccessibilityNodeInfo rootLinearLayout = sUiAutomation
344                     .getRootInActiveWindow().findAccessibilityNodeInfosByText(
345                             sContext.getString(R.string.rootLinearLayout)).get(0);
346 
347             final Bitmap blueColorFocusScreenshot = screenshotAfterChangeFocusColor(service,
348                     rootLinearLayout, Color.BLUE);
349 
350             final Bitmap redColorFocusScreenshot = screenshotAfterChangeFocusColor(service,
351                     rootLinearLayout, Color.RED);
352 
353             assertTrue(isBitmapDifferent(blueColorFocusScreenshot, redColorFocusScreenshot));
354         } finally {
355             setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue,
356                     sFocusColorDefaultValue);
357 
358             service.disableSelfAndRemove();
359         }
360     }
361 
screenshotAfterChangeFocusColor(StubFocusIndicatorService service, AccessibilityNodeInfo unAccessibilityFocusedNode, int color)362     private Bitmap screenshotAfterChangeFocusColor(StubFocusIndicatorService service,
363             AccessibilityNodeInfo unAccessibilityFocusedNode, int color) throws Exception {
364         assertFalse(unAccessibilityFocusedNode.isAccessibilityFocused());
365 
366         setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue, color);
367         sUiAutomation.executeAndWaitForEvent(
368                 () -> assertTrue(unAccessibilityFocusedNode.performAction(
369                         ACTION_ACCESSIBILITY_FOCUS)),
370                 filterForEventTypeWithAction(TYPE_VIEW_ACCESSIBILITY_FOCUSED,
371                         ACTION_ACCESSIBILITY_FOCUS),
372                 DEFAULT_TIMEOUT_MS);
373         Thread.sleep(SCREEN_FRAME_RENDERING_OUT_TIME_MILLIS);
374 
375         final Bitmap screenshot = sUiAutomation.takeScreenshot();
376 
377         sUiAutomation.executeAndWaitForEvent(
378                 () -> assertTrue(unAccessibilityFocusedNode.performAction(
379                         ACTION_CLEAR_ACCESSIBILITY_FOCUS)),
380                 filterForEventTypeWithAction(TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
381                         ACTION_CLEAR_ACCESSIBILITY_FOCUS),
382                 DEFAULT_TIMEOUT_MS);
383 
384         return screenshot;
385     }
386 
isBitmapDifferent(Bitmap bitmap1, Bitmap bitmap2)387     private boolean isBitmapDifferent(Bitmap bitmap1, Bitmap bitmap2) {
388         final Display display = mActivity.getWindowManager().getDefaultDisplay();
389         final Point displaySize = new Point();
390         display.getRealSize(displaySize);
391 
392         final int[] pixelsOne = new int[displaySize.x * displaySize.y];
393         final Bitmap bitmapOne = bitmap1.copy(Bitmap.Config.ARGB_8888, false);
394         bitmapOne.getPixels(pixelsOne, 0, displaySize.x, 0, 0, displaySize.x,
395                 displaySize.y);
396 
397         final int[] pixelsTwo = new int[displaySize.x * displaySize.y];
398         final Bitmap bitmapTwo = bitmap2.copy(Bitmap.Config.ARGB_8888, false);
399         bitmapTwo.getPixels(pixelsTwo, 0, displaySize.x, 0, 0, displaySize.x,
400                 displaySize.y);
401 
402         for (int i = pixelsOne.length - 1; i > 0; i--) {
403             if ((Color.red(pixelsOne[i]) != Color.red(pixelsTwo[i]))
404                     || (Color.green(pixelsOne[i]) != Color.green(pixelsTwo[i]))
405                     || (Color.blue(pixelsOne[i]) != Color.blue(pixelsTwo[i]))) {
406                 return true;
407             }
408         }
409 
410         saveFailureScreenshot(bitmap1, bitmap2);
411         return false;
412     }
413 
saveFailureScreenshot(Bitmap bitmap1, Bitmap bitmap2)414     private void saveFailureScreenshot(Bitmap bitmap1, Bitmap bitmap2) {
415         final String directoryName = Environment.getExternalStorageDirectory()
416                 + "/" + getClass().getSimpleName();
417 
418         final String fileName1 = String.format("%s_%s_%s.png", mTestName.getMethodName(), "Bitmap1",
419                 SystemClock.uptimeMillis());
420         BitmapUtils.saveBitmap(bitmap1, directoryName, fileName1);
421 
422         final String fileName2 = String.format("%s_%s_%s.png", mTestName.getMethodName(), "Bitmap2",
423                 SystemClock.uptimeMillis());
424         BitmapUtils.saveBitmap(bitmap2, directoryName, fileName2);
425     }
426 
setFocusAppearanceDataAndCheckItCorrect(StubFocusIndicatorService service, int focusStrokeWidthValue, int focusColorValue)427     private void setFocusAppearanceDataAndCheckItCorrect(StubFocusIndicatorService service,
428             int focusStrokeWidthValue, int focusColorValue) {
429         service.setAccessibilityFocusAppearance(focusStrokeWidthValue,
430                 focusColorValue);
431         // Checks if the color and the stroke values from AccessibilityManager is
432         // updated as in expectation.
433         PollingCheck.waitFor(()->isFocusAppearanceDataUpdated(sAccessibilityManager,
434                 focusStrokeWidthValue, focusColorValue));
435     }
436 
isFocusAppearanceDataUpdated(AccessibilityManager manager, int strokeWidth, int color)437     private static boolean isFocusAppearanceDataUpdated(AccessibilityManager manager,
438             int strokeWidth, int color) {
439         return manager.getAccessibilityFocusStrokeWidth() == strokeWidth
440                 && manager.getAccessibilityFocusColor() == color;
441     }
442 }
443