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.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertSame;
23 import static org.junit.Assert.assertTrue;
24 
25 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
26 import android.accessibilityservice.AccessibilityServiceInfo;
27 import android.accessibilityservice.cts.activities.AccessibilityViewTreeReportingActivity;
28 import android.app.Instrumentation;
29 import android.app.UiAutomation;
30 import android.content.Context;
31 import android.platform.test.annotations.Presubmit;
32 import android.text.TextUtils;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.accessibility.AccessibilityEvent;
36 import android.view.accessibility.AccessibilityNodeInfo;
37 import android.widget.Button;
38 import android.widget.LinearLayout;
39 
40 import androidx.test.InstrumentationRegistry;
41 import androidx.test.rule.ActivityTestRule;
42 import androidx.test.runner.AndroidJUnit4;
43 
44 import com.android.compatibility.common.util.CddTest;
45 
46 import org.junit.AfterClass;
47 import org.junit.Before;
48 import org.junit.BeforeClass;
49 import org.junit.Rule;
50 import org.junit.Test;
51 import org.junit.rules.RuleChain;
52 import org.junit.runner.RunWith;
53 
54 /**
55  * Test cases for testing the accessibility focus APIs exposed to accessibility
56  * services. This test checks how the view hierarchy is reported to accessibility
57  * services.
58  */
59 @RunWith(AndroidJUnit4.class)
60 @CddTest(requirements = {"3.10/C-1-1,C-1-2"})
61 @Presubmit
62 public class AccessibilityViewTreeReportingTest {
63     private static final int TIMEOUT_ASYNC_PROCESSING = 5000;
64     private static final long TIMEOUT_ACCESSIBILITY_STATE_IDLE = 200;
65 
66     private static Instrumentation sInstrumentation;
67     private static UiAutomation sUiAutomation;
68 
69     private AccessibilityViewTreeReportingActivity mActivity;
70 
71     private ActivityTestRule<AccessibilityViewTreeReportingActivity> mActivityRule =
72             new ActivityTestRule<>(AccessibilityViewTreeReportingActivity.class, false, false);
73 
74     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
75             new AccessibilityDumpOnFailureRule();
76 
77     @Rule
78     public final RuleChain mRuleChain = RuleChain
79             .outerRule(mActivityRule)
80             .around(mDumpOnFailureRule);
81 
82     @BeforeClass
oneTimeSetup()83     public static void oneTimeSetup() throws Exception {
84         sInstrumentation = InstrumentationRegistry.getInstrumentation();
85         sUiAutomation = sInstrumentation.getUiAutomation();
86     }
87 
88     @AfterClass
finalTearDown()89     public static void finalTearDown() throws Exception {
90         sUiAutomation.destroy();
91     }
92 
93     @Before
setUp()94     public void setUp() throws Exception {
95         mActivity = (AccessibilityViewTreeReportingActivity) launchActivityAndWaitForItToBeOnscreen(
96                 sInstrumentation, sUiAutomation, mActivityRule);
97         setGetNonImportantViews(false);
98     }
99 
100 
101     @Test
testDescendantsOfNotImportantViewReportedInOrder1()102     public void testDescendantsOfNotImportantViewReportedInOrder1() throws Exception {
103         AccessibilityNodeInfo firstFrameLayout = getNodeByText(R.string.firstFrameLayout);
104         assertNotNull(firstFrameLayout);
105         assertSame(3, firstFrameLayout.getChildCount());
106 
107         // Check if the first child is the right one.
108         AccessibilityNodeInfo firstTextView = getNodeByText(R.string.firstTextView);
109         assertEquals(firstTextView, firstFrameLayout.getChild(0));
110 
111         // Check if the second child is the right one.
112         AccessibilityNodeInfo firstEditText = getNodeByText(R.string.firstEditText);
113         assertEquals(firstEditText, firstFrameLayout.getChild(1));
114 
115         // Check if the third child is the right one.
116         AccessibilityNodeInfo firstButton = getNodeByText(R.string.firstButton);
117         assertEquals(firstButton, firstFrameLayout.getChild(2));
118     }
119 
120     @Test
testDescendantsOfNotImportantViewReportedInOrder2()121     public void testDescendantsOfNotImportantViewReportedInOrder2() throws Exception {
122         AccessibilityNodeInfo secondFrameLayout = getNodeByText(R.string.secondFrameLayout);
123         assertNotNull(secondFrameLayout);
124         assertSame(3, secondFrameLayout.getChildCount());
125 
126         // Check if the first child is the right one.
127         AccessibilityNodeInfo secondTextView = getNodeByText(R.string.secondTextView);
128         assertEquals(secondTextView, secondFrameLayout.getChild(0));
129 
130         // Check if the second child is the right one.
131         AccessibilityNodeInfo secondEditText = getNodeByText(R.string.secondEditText);
132         assertEquals(secondEditText, secondFrameLayout.getChild(1));
133 
134         // Check if the third child is the right one.
135         AccessibilityNodeInfo secondButton = getNodeByText(R.string.secondButton);
136         assertEquals(secondButton, secondFrameLayout.getChild(2));
137     }
138 
139     @Test
testDescendantsOfNotImportantViewReportedInOrder3()140     public void testDescendantsOfNotImportantViewReportedInOrder3() throws Exception {
141         AccessibilityNodeInfo rootLinearLayout =
142                 getNodeByText(R.string.rootLinearLayout);
143         assertNotNull(rootLinearLayout);
144         assertSame(4, rootLinearLayout.getChildCount());
145 
146         // Check if the first child is the right one.
147         AccessibilityNodeInfo firstFrameLayout =
148                 getNodeByText(R.string.firstFrameLayout);
149         assertEquals(firstFrameLayout, rootLinearLayout.getChild(0));
150 
151         // Check if the second child is the right one.
152         AccessibilityNodeInfo secondTextView = getNodeByText(R.string.secondTextView);
153         assertEquals(secondTextView, rootLinearLayout.getChild(1));
154 
155         // Check if the third child is the right one.
156         AccessibilityNodeInfo secondEditText = getNodeByText(R.string.secondEditText);
157         assertEquals(secondEditText, rootLinearLayout.getChild(2));
158 
159         // Check if the fourth child is the right one.
160         AccessibilityNodeInfo secondButton = getNodeByText(R.string.secondButton);
161         assertEquals(secondButton, rootLinearLayout.getChild(3));
162     }
163 
164     @Test
testDrawingOrderInImportantParentFollowsXmlOrder()165     public void testDrawingOrderInImportantParentFollowsXmlOrder() throws Exception {
166         sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.firstLinearLayout)
167                 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES));
168 
169         AccessibilityNodeInfo firstTextView = getNodeByText(R.string.firstTextView);
170         AccessibilityNodeInfo firstEditText = getNodeByText(R.string.firstEditText);
171         AccessibilityNodeInfo firstButton = getNodeByText(R.string.firstButton);
172 
173         // Drawing order is: firstTextView, firstEditText, firstButton
174         assertTrue(firstTextView.getDrawingOrder() < firstEditText.getDrawingOrder());
175         assertTrue(firstEditText.getDrawingOrder() < firstButton.getDrawingOrder());
176 
177         // Confirm that obtaining copies doesn't change our results
178         AccessibilityNodeInfo copyOfFirstEditText = AccessibilityNodeInfo.obtain(firstEditText);
179         assertTrue(firstTextView.getDrawingOrder() < copyOfFirstEditText.getDrawingOrder());
180         assertTrue(copyOfFirstEditText.getDrawingOrder() < firstButton.getDrawingOrder());
181     }
182 
183     @Test
184     public void testDrawingOrderGettingAllViewsFollowsXmlOrder() throws Exception {
185         setGetNonImportantViews(true);
186         AccessibilityNodeInfo firstTextView = getNodeByText(R.string.firstTextView);
187         AccessibilityNodeInfo firstEditText = getNodeByText(R.string.firstEditText);
188         AccessibilityNodeInfo firstButton = getNodeByText(R.string.firstButton);
189 
190         // Drawing order is: firstTextView, firstEditText, firstButton
191         assertTrue(firstTextView.getDrawingOrder() < firstEditText.getDrawingOrder());
192         assertTrue(firstEditText.getDrawingOrder() < firstButton.getDrawingOrder());
193     }
194 
195     @Test
196     public void testDrawingOrderWithZCoordsDrawsHighestZLast() throws Exception {
197         setGetNonImportantViews(true);
198         sInstrumentation.runOnMainSync(() -> {
199             mActivity.findViewById(R.id.firstTextView).setZ(50);
200             mActivity.findViewById(R.id.firstEditText).setZ(100);
201         });
202 
203         AccessibilityNodeInfo firstTextView = getNodeByText(R.string.firstTextView);
204         AccessibilityNodeInfo firstEditText = getNodeByText(R.string.firstEditText);
205         AccessibilityNodeInfo firstButton = getNodeByText(R.string.firstButton);
206 
207         // Drawing order is firstButton (no z), firstTextView (z=50), firstEditText (z=100)
208         assertTrue(firstButton.getDrawingOrder() < firstTextView.getDrawingOrder());
209         assertTrue(firstTextView.getDrawingOrder() < firstEditText.getDrawingOrder());
210     }
211 
212     @Test
213     public void testDrawingOrderWithCustomDrawingOrder() throws Exception {
214         setGetNonImportantViews(true);
215         sInstrumentation.runOnMainSync(() -> {
216             // Reorganize the hiearchy to replace firstLinearLayout with one that allows us to
217             // control the draw order
218             LinearLayout rootLinearLayout =
219                     (LinearLayout) mActivity.findViewById(R.id.rootLinearLayout);
220             LinearLayout firstLinearLayout =
221                     (LinearLayout) mActivity.findViewById(R.id.firstLinearLayout);
222             View firstTextView = mActivity.findViewById(R.id.firstTextView);
223             View firstEditText = mActivity.findViewById(R.id.firstEditText);
224             View firstButton = mActivity.findViewById(R.id.firstButton);
225             firstLinearLayout.removeAllViews();
226             LinearLayoutWithDrawingOrder layoutWithDrawingOrder =
227                     new LinearLayoutWithDrawingOrder(mActivity);
228             rootLinearLayout.addView(layoutWithDrawingOrder);
229             layoutWithDrawingOrder.addView(firstTextView);
230             layoutWithDrawingOrder.addView(firstEditText);
231             layoutWithDrawingOrder.addView(firstButton);
232             layoutWithDrawingOrder.childDrawingOrder = new int[] {2, 0, 1};
233         });
234 
235         AccessibilityNodeInfo firstTextView = getNodeByText(R.string.firstTextView);
236         AccessibilityNodeInfo firstEditText = getNodeByText(R.string.firstEditText);
237         AccessibilityNodeInfo firstButton = getNodeByText(R.string.firstButton);
238 
239         // Drawing order is firstEditText, firstButton, firstTextView
240         assertTrue(firstEditText.getDrawingOrder() < firstButton.getDrawingOrder());
241         assertTrue(firstButton.getDrawingOrder() < firstTextView.getDrawingOrder());
242     }
243 
244     @Test
245     public void testDrawingOrderWithNotImportantSiblingConsidersItsChildren() throws Exception {
246         // Make the first frame layout a higher Z so it's drawn last
247         sInstrumentation.runOnMainSync(
248                 () -> mActivity.findViewById(R.id.firstFrameLayout).setZ(100));
249         AccessibilityNodeInfo secondTextView = getNodeByText(R.string.secondTextView);
250         AccessibilityNodeInfo secondEditText = getNodeByText(R.string.secondEditText);
251         AccessibilityNodeInfo secondButton = getNodeByText(R.string.secondButton);
252         AccessibilityNodeInfo firstFrameLayout = getNodeByText( R.string.firstFrameLayout);
253         assertTrue(secondTextView.getDrawingOrder() < firstFrameLayout.getDrawingOrder());
254         assertTrue(secondEditText.getDrawingOrder() < firstFrameLayout.getDrawingOrder());
255         assertTrue(secondButton.getDrawingOrder() < firstFrameLayout.getDrawingOrder());
256     }
257 
258     @Test
259     public void testDrawingOrderWithNotImportantParentConsidersParentSibling() throws Exception {
260         AccessibilityNodeInfo firstFrameLayout = getNodeByText(R.string.firstFrameLayout);
261         AccessibilityNodeInfo secondTextView = getNodeByText(R.string.secondTextView);
262         AccessibilityNodeInfo secondEditText = getNodeByText(R.string.secondEditText);
263         AccessibilityNodeInfo secondButton = getNodeByText(R.string.secondButton);
264 
265         assertTrue(secondTextView.getDrawingOrder() > firstFrameLayout.getDrawingOrder());
266         assertTrue(secondEditText.getDrawingOrder() > firstFrameLayout.getDrawingOrder());
267         assertTrue(secondButton.getDrawingOrder() > firstFrameLayout.getDrawingOrder());
268     }
269 
270     @Test
271     public void testDrawingOrderRootNodeHasIndex0() throws Exception {
272         assertEquals(0, sUiAutomation.getRootInActiveWindow().getDrawingOrder());
273     }
274 
275     @Test
276     public void testAccessibilityImportanceReportingForImportantView() throws Exception {
277         setGetNonImportantViews(true);
278         sInstrumentation.runOnMainSync(() -> {
279             // Manually control importance for firstButton
280             View firstButton = mActivity.findViewById(R.id.firstButton);
281             firstButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
282         });
283 
284         AccessibilityNodeInfo firstButtonNode = getNodeByText(R.string.firstButton);
285         assertTrue(firstButtonNode.isImportantForAccessibility());
286     }
287 
288     @Test
289     public void testAccessibilityImportanceReportingForUnimportantView() throws Exception {
290         setGetNonImportantViews(true);
291         sInstrumentation.runOnMainSync(() -> {
292             View firstButton = mActivity.findViewById(R.id.firstButton);
293             firstButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
294         });
295 
296         AccessibilityNodeInfo firstButtonNode = getNodeByText(R.string.firstButton);
297         assertFalse(firstButtonNode.isImportantForAccessibility());
298     }
299 
300     @Test
301     public void testAddViewToLayout_receiveSubtreeEvent() throws Throwable {
302         final LinearLayout layout =
303                 (LinearLayout) mActivity.findViewById(R.id.secondLinearLayout);
304         final Button newButton = new Button(mActivity);
305         newButton.setText("New Button");
306         newButton.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
307         newButton.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
308         AccessibilityEvent awaitedEvent =
309                 sUiAutomation.executeAndWaitForEvent(
310                         () -> mActivity.runOnUiThread(() -> layout.addView(newButton)),
311                         (event) -> {
312                             boolean isContentChanged = event.getEventType()
313                                     == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
314                             int isSubTree = (event.getContentChangeTypes()
315                                     & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
316                             boolean isFromThisPackage = TextUtils.equals(event.getPackageName(),
317                                     mActivity.getPackageName());
318                             return isContentChanged && (isSubTree != 0) && isFromThisPackage;
319                         }, TIMEOUT_ASYNC_PROCESSING);
320         // The event should come from a view that's important for accessibility, even though the
321         // layout we added it to isn't important. Otherwise services may not find out about the
322         // new button.
323         assertTrue(awaitedEvent.getSource().isImportantForAccessibility());
324     }
325 
326     @Test
327     public void testSetViewInvisible_receiveSubtreeEvent() throws Throwable {
328         final View view = mActivity.findViewById(R.id.secondButton);
329         receiveSubtreeEventWhenViewChangesVisibility(view, (View) view.getParentForAccessibility(), View.INVISIBLE);
330     }
331 
332     @Test
333     public void testSetViewGone_receiveSubtreeEvent() throws Throwable {
334         final View view = mActivity.findViewById(R.id.secondButton);
335         receiveSubtreeEventWhenViewChangesVisibility(view, (View) view.getParentForAccessibility(), View.GONE);
336     }
337 
338     @Test
339     public void testSetViewVisible_receiveSubtreeEvent() throws Throwable {
340         final View view = mActivity.findViewById(R.id.hiddenButton);
341         receiveSubtreeEventWhenViewChangesVisibility(view, view, View.VISIBLE);
342     }
343 
344     private void receiveSubtreeEventWhenViewChangesVisibility(View view, View sendA11yEventParent,
345             int visibility) throws Throwable {
346         // This wait prevents the expected event being merged with other events by throttling.
347         sUiAutomation.waitForIdle(TIMEOUT_ACCESSIBILITY_STATE_IDLE, TIMEOUT_ASYNC_PROCESSING);
348 
349         AccessibilityEvent awaitedEvent =
350                 sUiAutomation.executeAndWaitForEvent(
351                         () -> {
352                             mActivity.runOnUiThread(() -> view.setVisibility(visibility));
353                         },
354                         (event) -> {
355                             boolean isContentChanged = event.getEventType()
356                                     == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
357                             boolean isSubTree = event.getContentChangeTypes()
358                                     == AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
359                             boolean isFromThisPackage = TextUtils.equals(event.getPackageName(),
360                                     mActivity.getPackageName());
361                             boolean isFromThisNode;
362                             if (event.getSource() != null) {
363                                 isFromThisNode = TextUtils.equals(
364                                         event.getSource().getViewIdResourceName(),
365                                         sInstrumentation.getTargetContext().getResources()
366                                                 .getResourceName(sendA11yEventParent.getId()));
367                             } else {
368                                 isFromThisNode = TextUtils.equals(event.getClassName(),
369                                         sendA11yEventParent.getAccessibilityClassName());
370                             }
371                             return isContentChanged && isSubTree && isFromThisPackage
372                                     && isFromThisNode;
373                         }, TIMEOUT_ASYNC_PROCESSING);
awaitedEvent.recycle()374         awaitedEvent.recycle();
375     }
376 
setGetNonImportantViews(boolean getNonImportantViews)377     private void setGetNonImportantViews(boolean getNonImportantViews) {
378         AccessibilityServiceInfo serviceInfo = sUiAutomation.getServiceInfo();
379         serviceInfo.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
380         serviceInfo.flags |= getNonImportantViews ?
381                 AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0;
382         sUiAutomation.setServiceInfo(serviceInfo);
383     }
384 
getNodeByText(int stringId)385     private AccessibilityNodeInfo getNodeByText(int stringId) {
386         return sUiAutomation.getRootInActiveWindow().findAccessibilityNodeInfosByText(
387                 sInstrumentation.getContext().getString(stringId)).get(0);
388     }
389 
390     class LinearLayoutWithDrawingOrder extends LinearLayout {
391         public int[] childDrawingOrder;
LinearLayoutWithDrawingOrder(Context context)392         LinearLayoutWithDrawingOrder(Context context) {
393             super(context);
394             setChildrenDrawingOrderEnabled(true);
395         }
396 
397         @Override
getChildDrawingOrder(int childCount, int i)398         protected int getChildDrawingOrder(int childCount, int i) {
399             return childDrawingOrder[i];
400         }
401     }
402 }
403