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