1 /* 2 * Copyright (C) 2012 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.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType; 20 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangedWithChangeTypes; 21 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen; 22 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS; 23 import static android.accessibilityservice.cts.utils.DisplayUtils.getStatusBarHeight; 24 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; 25 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; 26 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED; 27 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; 28 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_LONG_CLICKED; 29 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED; 30 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED; 31 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ADDED; 32 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS; 33 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION; 34 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; 35 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; 36 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; 37 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SELECT; 38 39 import static junit.framework.TestCase.assertNull; 40 41 import static org.hamcrest.Matchers.lessThan; 42 import static org.junit.Assert.assertEquals; 43 import static org.junit.Assert.assertFalse; 44 import static org.junit.Assert.assertNotNull; 45 import static org.junit.Assert.assertNotSame; 46 import static org.junit.Assert.assertSame; 47 import static org.junit.Assert.assertThat; 48 import static org.junit.Assert.assertTrue; 49 import static org.junit.Assert.fail; 50 51 import android.accessibilityservice.AccessibilityService; 52 import android.accessibilityservice.AccessibilityServiceInfo; 53 import android.accessibilityservice.cts.activities.AccessibilityEndToEndActivity; 54 import android.accessibilityservice.cts.activities.AccessibilityWindowQueryActivity; 55 import android.app.ActivityTaskManager; 56 import android.app.Instrumentation; 57 import android.app.UiAutomation; 58 import android.app.UiAutomation.AccessibilityEventFilter; 59 import android.content.pm.PackageManager; 60 import android.content.res.Resources; 61 import android.graphics.Rect; 62 import android.platform.test.annotations.AppModeFull; 63 import android.test.suitebuilder.annotation.MediumTest; 64 import android.view.Gravity; 65 import android.view.View; 66 import android.view.WindowManager; 67 import android.view.accessibility.AccessibilityEvent; 68 import android.view.accessibility.AccessibilityNodeInfo; 69 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 70 import android.view.accessibility.AccessibilityWindowInfo; 71 import android.widget.Button; 72 73 import androidx.test.InstrumentationRegistry; 74 import androidx.test.rule.ActivityTestRule; 75 import androidx.test.runner.AndroidJUnit4; 76 77 import org.hamcrest.Description; 78 import org.hamcrest.TypeSafeMatcher; 79 import org.junit.AfterClass; 80 import org.junit.Before; 81 import org.junit.BeforeClass; 82 import org.junit.Rule; 83 import org.junit.Test; 84 import org.junit.runner.RunWith; 85 86 import java.util.ArrayList; 87 import java.util.LinkedList; 88 import java.util.List; 89 import java.util.Queue; 90 import java.util.concurrent.TimeoutException; 91 import java.util.concurrent.atomic.AtomicReference; 92 import java.util.function.Consumer; 93 import java.util.function.Function; 94 95 /** 96 * Test cases for testing the accessibility APIs for querying of the screen content. 97 * These APIs allow exploring the screen and requesting an action to be performed 98 * on a given view from an AccessibilityService. 99 */ 100 @AppModeFull 101 @RunWith(AndroidJUnit4.class) 102 public class AccessibilityWindowQueryTest { 103 private static final String LOG_TAG = "AccessibilityWindowQueryTest"; 104 private static String CONTENT_VIEW_RES_NAME = 105 "android.accessibilityservice.cts:id/added_content"; 106 private static final long TIMEOUT_WINDOW_STATE_IDLE = 500; 107 private static Instrumentation sInstrumentation; 108 private static UiAutomation sUiAutomation; 109 110 private AccessibilityWindowQueryActivity mActivity; 111 112 @Rule 113 public ActivityTestRule<AccessibilityWindowQueryActivity> mActivityRule = 114 new ActivityTestRule<>(AccessibilityWindowQueryActivity.class, false, false); 115 116 @Rule 117 public ActivityTestRule<AccessibilityEndToEndActivity> mEndToEndActivityRule = 118 new ActivityTestRule<>(AccessibilityEndToEndActivity.class, false, false); 119 120 @BeforeClass 121 public static void oneTimeSetup() throws Exception { 122 sInstrumentation = InstrumentationRegistry.getInstrumentation(); 123 sUiAutomation = sInstrumentation.getUiAutomation(); 124 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 125 info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; 126 sUiAutomation.setServiceInfo(info); 127 } 128 129 @AfterClass 130 public static void postTestTearDown() { 131 sUiAutomation.destroy(); 132 } 133 134 @Before 135 public void setUp() throws Exception { 136 mActivity = launchActivityAndWaitForItToBeOnscreen( 137 sInstrumentation, sUiAutomation, mActivityRule); 138 } 139 140 private final AccessibilityEventFilter mDividerPresentFilter = (event) -> 141 (event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED) 142 && isDividerWindowPresent(); 143 144 private final AccessibilityEventFilter mDividerAbsentFilter = (event) -> 145 (event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED) 146 && !isDividerWindowPresent(); 147 148 @MediumTest 149 @Test 150 public void testFindByText() throws Throwable { 151 // First, make the root view of the activity an accessibility node. This allows us to 152 // later exclude views that are part of the activity's DecorView. 153 sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.added_content) 154 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES)); 155 156 // Start looking from the added content instead of from the root accessibility node so 157 // that nodes that we don't expect (i.e. window control buttons) are not included in the 158 // list of accessibility nodes returned by findAccessibilityNodeInfosByText. 159 final AccessibilityNodeInfo addedContent = sUiAutomation 160 .getRootInActiveWindow().findAccessibilityNodeInfosByViewId(CONTENT_VIEW_RES_NAME) 161 .get(0); 162 163 // find a view by text 164 List<AccessibilityNodeInfo> buttons = addedContent.findAccessibilityNodeInfosByText("b"); 165 166 assertEquals(9, buttons.size()); 167 } 168 169 @MediumTest 170 @Test 171 public void testFindByContentDescription() throws Exception { 172 // find a view by text 173 AccessibilityNodeInfo button = sUiAutomation.getRootInActiveWindow() 174 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.contentDescription)) 175 .get(0); 176 assertNotNull(button); 177 } 178 179 @MediumTest 180 @Test 181 public void testTraverseWindow() throws Exception { 182 verifyNodesInAppWindow(sUiAutomation.getRootInActiveWindow()); 183 } 184 185 @MediumTest 186 @Test 187 public void testNoWindowsAccessIfFlagNotSet() throws Exception { 188 // Clear window access flag 189 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 190 info.flags &= ~AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 191 sUiAutomation.setServiceInfo(info); 192 193 // Make sure the windows cannot be accessed. 194 assertTrue(sUiAutomation.getWindows().isEmpty()); 195 196 // Find a button to click on. 197 final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow() 198 .findAccessibilityNodeInfosByViewId( 199 "android.accessibilityservice.cts:id/button1").get(0); 200 201 // Click the button to generate an event 202 AccessibilityEvent event = sUiAutomation.executeAndWaitForEvent( 203 () -> button1.performAction(ACTION_CLICK), 204 filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS); 205 206 // Make sure the source window cannot be accessed. 207 assertNull(event.getSource().getWindow()); 208 } 209 210 @MediumTest 211 @Test 212 public void testTraverseAllWindows() throws Exception { 213 setAccessInteractiveWindowsFlag(); 214 try { 215 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 216 Rect boundsInScreen = new Rect(); 217 218 final int windowCount = windows.size(); 219 for (int i = 0; i < windowCount; i++) { 220 AccessibilityWindowInfo window = windows.get(i); 221 222 window.getBoundsInScreen(boundsInScreen); 223 assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, emptiness check. 224 assertNull(window.getParent()); 225 assertSame(0, window.getChildCount()); 226 assertNull(window.getParent()); 227 assertNotNull(window.getRoot()); 228 229 if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) { 230 assertTrue(window.isFocused()); 231 assertTrue(window.isActive()); 232 verifyNodesInAppWindow(window.getRoot()); 233 } else if (window.getType() == AccessibilityWindowInfo.TYPE_SYSTEM) { 234 assertFalse(window.isFocused()); 235 assertFalse(window.isActive()); 236 } 237 } 238 } finally { 239 clearAccessInteractiveWindowsFlag(); 240 } 241 } 242 243 @MediumTest 244 @Test 245 public void testTraverseWindowFromEvent() throws Exception { 246 setAccessInteractiveWindowsFlag(); 247 try { 248 // Find a button to click on. 249 final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow() 250 .findAccessibilityNodeInfosByViewId( 251 "android.accessibilityservice.cts:id/button1").get(0); 252 253 // Click the button. 254 AccessibilityEvent event = sUiAutomation.executeAndWaitForEvent( 255 () -> button1.performAction(ACTION_CLICK), 256 filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS); 257 258 // Get the source window. 259 AccessibilityWindowInfo window = event.getSource().getWindow(); 260 261 // Verify the application window. 262 Rect boundsInScreen = new Rect(); 263 window.getBoundsInScreen(boundsInScreen); 264 assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, so just emptiness check 265 assertSame(window.getType(), AccessibilityWindowInfo.TYPE_APPLICATION); 266 assertTrue(window.isFocused()); 267 assertTrue(window.isActive()); 268 assertNull(window.getParent()); 269 assertSame(0, window.getChildCount()); 270 assertNotNull(window.getRoot()); 271 272 // Verify the window content. 273 verifyNodesInAppWindow(window.getRoot()); 274 } finally { 275 clearAccessInteractiveWindowsFlag(); 276 } 277 } 278 279 @MediumTest 280 @Test 281 public void testInteractWithAppWindow() throws Exception { 282 setAccessInteractiveWindowsFlag(); 283 try { 284 // Find a button to click on. 285 final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow() 286 .findAccessibilityNodeInfosByViewId( 287 "android.accessibilityservice.cts:id/button1").get(0); 288 289 // Click the button. 290 AccessibilityEvent event = sUiAutomation.executeAndWaitForEvent( 291 () -> button1.performAction(ACTION_CLICK), 292 filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS); 293 294 // Get the source window. 295 AccessibilityWindowInfo window = event.getSource().getWindow(); 296 297 // Find a another button from the event's window. 298 final AccessibilityNodeInfo button2 = window.getRoot() 299 .findAccessibilityNodeInfosByViewId( 300 "android.accessibilityservice.cts:id/button2").get(0); 301 302 // Click the second button. 303 sUiAutomation.executeAndWaitForEvent(() -> button2.performAction(ACTION_CLICK), 304 filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS); 305 } finally { 306 clearAccessInteractiveWindowsFlag(); 307 } 308 } 309 310 @MediumTest 311 @Test 312 public void testSingleAccessibilityFocusAcrossWindows() throws Exception { 313 try { 314 // Add two more windows. 315 final View views[]; 316 views = addTwoAppPanelWindows(); 317 318 try { 319 // Put accessibility focus in the first app window. 320 ensureAppWindowFocusedOrFail(0); 321 // Make sure there only one accessibility focus. 322 assertSingleAccessibilityFocus(); 323 324 // Put accessibility focus in the second app window. 325 ensureAppWindowFocusedOrFail(1); 326 // Make sure there only one accessibility focus. 327 assertSingleAccessibilityFocus(); 328 329 // Put accessibility focus in the third app window. 330 ensureAppWindowFocusedOrFail(2); 331 // Make sure there only one accessibility focus. 332 assertSingleAccessibilityFocus(); 333 } finally { 334 // Clean up panel windows 335 sInstrumentation.runOnMainSync(() -> { 336 WindowManager wm = 337 sInstrumentation.getContext().getSystemService(WindowManager.class); 338 for (View view : views) { 339 wm.removeView(view); 340 } 341 }); 342 } 343 } finally { 344 ensureAccessibilityFocusCleared(); 345 clearAccessInteractiveWindowsFlag(); 346 } 347 } 348 349 @MediumTest 350 @Test 351 public void testPerformActionSetAndClearFocus() throws Exception { 352 // find a view and make sure it is not focused 353 AccessibilityNodeInfo button = sUiAutomation 354 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 355 mActivity.getString(R.string.button5)).get(0); 356 assertFalse(button.isFocused()); 357 358 // focus the view 359 assertTrue(button.performAction(ACTION_FOCUS)); 360 361 // find the view again and make sure it is focused 362 button = sUiAutomation.getRootInActiveWindow() 363 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); 364 assertTrue(button.isFocused()); 365 366 // unfocus the view 367 assertTrue(button.performAction(ACTION_CLEAR_FOCUS)); 368 369 // find the view again and make sure it is not focused 370 button = sUiAutomation.getRootInActiveWindow() 371 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); 372 assertFalse(button.isFocused()); 373 } 374 375 @MediumTest 376 @Test 377 public void testPerformActionSelect() throws Exception { 378 // find a view and make sure it is not selected 379 AccessibilityNodeInfo button = sUiAutomation 380 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 381 mActivity.getString(R.string.button5)).get(0); 382 assertFalse(button.isSelected()); 383 384 // select the view 385 assertTrue(button.performAction(ACTION_SELECT)); 386 387 // find the view again and make sure it is selected 388 button = sUiAutomation.getRootInActiveWindow() 389 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); 390 assertTrue(button.isSelected()); 391 } 392 393 @MediumTest 394 @Test 395 public void testPerformActionClearSelection() throws Exception { 396 // find a view and make sure it is not selected 397 AccessibilityNodeInfo button = sUiAutomation 398 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 399 mActivity.getString(R.string.button5)).get(0); 400 assertFalse(button.isSelected()); 401 402 // select the view 403 assertTrue(button.performAction(ACTION_SELECT)); 404 405 // find the view again and make sure it is selected 406 button = sUiAutomation.getRootInActiveWindow() 407 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); 408 409 assertTrue(button.isSelected()); 410 411 // unselect the view 412 assertTrue(button.performAction(ACTION_CLEAR_SELECTION)); 413 414 // find the view again and make sure it is not selected 415 button = sUiAutomation.getRootInActiveWindow() 416 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); 417 assertFalse(button.isSelected()); 418 } 419 420 @MediumTest 421 @Test 422 public void testPerformActionClick() throws Exception { 423 // find a view and make sure it is not selected 424 final AccessibilityNodeInfo button = sUiAutomation 425 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 426 mActivity.getString(R.string.button5)).get(0); 427 assertFalse(button.isSelected()); 428 429 // Perform an action and wait for an event 430 AccessibilityEvent expected = sUiAutomation.executeAndWaitForEvent( 431 () -> button.performAction(ACTION_CLICK), 432 filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS); 433 434 // Make sure the expected event was received. 435 assertNotNull(expected); 436 } 437 438 @MediumTest 439 @Test 440 public void testPerformActionLongClick() throws Exception { 441 // find a view and make sure it is not selected 442 final AccessibilityNodeInfo button = sUiAutomation 443 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 444 mActivity.getString(R.string.button5)).get(0); 445 assertFalse(button.isSelected()); 446 447 // Perform an action and wait for an event. 448 AccessibilityEvent expected = sUiAutomation.executeAndWaitForEvent( 449 () -> button.performAction(ACTION_LONG_CLICK), 450 filterForEventType(TYPE_VIEW_LONG_CLICKED), DEFAULT_TIMEOUT_MS); 451 452 // Make sure the expected event was received. 453 assertNotNull(expected); 454 } 455 456 457 @MediumTest 458 @Test 459 public void testPerformCustomAction() throws Exception { 460 // find a view and make sure it is not selected 461 AccessibilityNodeInfo button = sUiAutomation 462 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 463 mActivity.getString(R.string.button5)).get(0); 464 465 // find the custom action and perform it 466 List<AccessibilityAction> actions = button.getActionList(); 467 final int actionCount = actions.size(); 468 for (int i = 0; i < actionCount; i++) { 469 AccessibilityAction action = actions.get(i); 470 if (action.getId() == R.id.foo_custom_action) { 471 assertSame(action.getLabel(), "Foo"); 472 // perform the action 473 assertTrue(button.performAction(action.getId())); 474 return; 475 } 476 } 477 } 478 479 @MediumTest 480 @Test 481 public void testGetEventSource() throws Exception { 482 // find a view and make sure it is not focused 483 final AccessibilityNodeInfo button = sUiAutomation 484 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 485 mActivity.getString(R.string.button5)).get(0); 486 assertFalse(button.isSelected()); 487 488 // focus and wait for the event 489 AccessibilityEvent awaitedEvent = sUiAutomation 490 .executeAndWaitForEvent(() -> button.performAction(ACTION_FOCUS), 491 filterForEventType(TYPE_VIEW_FOCUSED), DEFAULT_TIMEOUT_MS); 492 493 assertNotNull(awaitedEvent); 494 495 // check that last event source 496 AccessibilityNodeInfo source = awaitedEvent.getSource(); 497 assertNotNull(source); 498 499 // bounds 500 Rect buttonBounds = new Rect(); 501 button.getBoundsInParent(buttonBounds); 502 Rect sourceBounds = new Rect(); 503 source.getBoundsInParent(sourceBounds); 504 505 assertEquals(buttonBounds.left, sourceBounds.left); 506 assertEquals(buttonBounds.right, sourceBounds.right); 507 assertEquals(buttonBounds.top, sourceBounds.top); 508 assertEquals(buttonBounds.bottom, sourceBounds.bottom); 509 510 // char sequence attributes 511 assertEquals(button.getPackageName(), source.getPackageName()); 512 assertEquals(button.getClassName(), source.getClassName()); 513 assertEquals(button.getText().toString(), source.getText().toString()); 514 assertSame(button.getContentDescription(), source.getContentDescription()); 515 516 // boolean attributes 517 assertSame(button.isFocusable(), source.isFocusable()); 518 assertSame(button.isClickable(), source.isClickable()); 519 assertSame(button.isEnabled(), source.isEnabled()); 520 assertNotSame(button.isFocused(), source.isFocused()); 521 assertSame(button.isLongClickable(), source.isLongClickable()); 522 assertSame(button.isPassword(), source.isPassword()); 523 assertSame(button.isSelected(), source.isSelected()); 524 assertSame(button.isCheckable(), source.isCheckable()); 525 assertSame(button.isChecked(), source.isChecked()); 526 } 527 528 @MediumTest 529 @Test 530 public void testObjectContract() throws Exception { 531 try { 532 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 533 info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; 534 sUiAutomation.setServiceInfo(info); 535 536 // find a view and make sure it is not focused 537 AccessibilityNodeInfo button = sUiAutomation 538 .getRootInActiveWindow().findAccessibilityNodeInfosByText( 539 mActivity.getString(R.string.button5)).get(0); 540 AccessibilityNodeInfo parent = button.getParent(); 541 final int childCount = parent.getChildCount(); 542 for (int i = 0; i < childCount; i++) { 543 AccessibilityNodeInfo child = parent.getChild(i); 544 assertNotNull(child); 545 if (child.equals(button)) { 546 assertEquals("Equal objects must have same hasCode.", button.hashCode(), 547 child.hashCode()); 548 return; 549 } 550 } 551 fail("Parent's children do not have the info whose parent is the parent."); 552 } finally { 553 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 554 info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; 555 sUiAutomation.setServiceInfo(info); 556 } 557 } 558 559 @MediumTest 560 @Test 561 public void testWindowDockAndUndock_dividerWindowAppearsAndDisappears() throws Exception { 562 if (!ActivityTaskManager.supportsSplitScreenMultiWindow(sInstrumentation.getContext())) { 563 // Skipping test: no multi-window support 564 return; 565 } 566 567 if (sInstrumentation.getContext().getPackageManager() 568 .hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 569 // Android TV doesn't support the divider window 570 return; 571 } 572 573 setAccessInteractiveWindowsFlag(); 574 assertFalse(isDividerWindowPresent()); 575 576 Runnable toggleSplitScreenRunnable = () -> assertTrue(sUiAutomation.performGlobalAction( 577 AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN)); 578 579 sUiAutomation.executeAndWaitForEvent(toggleSplitScreenRunnable, mDividerPresentFilter, 580 DEFAULT_TIMEOUT_MS); 581 582 sUiAutomation.executeAndWaitForEvent(toggleSplitScreenRunnable, mDividerAbsentFilter, 583 DEFAULT_TIMEOUT_MS); 584 } 585 586 @Test 587 public void testFindPictureInPictureWindow() throws Exception { 588 if (!sInstrumentation.getContext().getPackageManager() 589 .hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { 590 return; 591 } 592 sUiAutomation.executeAndWaitForEvent(() -> { 593 sInstrumentation.runOnMainSync(() -> { 594 mActivity.enterPictureInPictureMode(); 595 }); 596 }, filterForEventType(TYPE_WINDOWS_CHANGED), DEFAULT_TIMEOUT_MS); 597 sInstrumentation.waitForIdleSync(); 598 599 // We should be able to find a picture-in-picture window now 600 int numPictureInPictureWindows = 0; 601 final List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 602 final int windowCount = windows.size(); 603 for (int i = 0; i < windowCount; i++) { 604 final AccessibilityWindowInfo window = windows.get(i); 605 if (window.isInPictureInPictureMode()) { 606 numPictureInPictureWindows++; 607 } 608 } 609 assertTrue(numPictureInPictureWindows >= 1); 610 } 611 612 @Test 613 public void testGetWindows_resultIsSortedByLayerDescending() throws TimeoutException { 614 addTwoAppPanelWindows(); 615 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 616 617 AccessibilityWindowInfo windowAddedFirst = findWindow(windows, R.string.button1); 618 AccessibilityWindowInfo windowAddedSecond = findWindow(windows, R.string.button2); 619 assertThat(windowAddedFirst.getLayer(), lessThan(windowAddedSecond.getLayer())); 620 621 assertThat(windows, new IsSortedBy<>(w -> w.getLayer(), /* ascending */ false)); 622 } 623 624 private AccessibilityWindowInfo findWindow(List<AccessibilityWindowInfo> windows, 625 int btnTextRes) { 626 return windows.stream() 627 .filter(w -> w.getRoot() 628 .findAccessibilityNodeInfosByText(mActivity.getString(btnTextRes)) 629 .size() == 1) 630 .findFirst() 631 .get(); 632 } 633 634 private boolean isDividerWindowPresent() { 635 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 636 final int windowCount = windows.size(); 637 for (int i = 0; i < windowCount; i++) { 638 AccessibilityWindowInfo window = windows.get(i); 639 if (window.getType() == AccessibilityWindowInfo.TYPE_SPLIT_SCREEN_DIVIDER) { 640 return true; 641 } 642 } 643 return false; 644 } 645 646 private void assertSingleAccessibilityFocus() { 647 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 648 AccessibilityWindowInfo focused = null; 649 650 final int windowCount = windows.size(); 651 for (int i = 0; i < windowCount; i++) { 652 AccessibilityWindowInfo window = windows.get(i); 653 654 if (window.isAccessibilityFocused()) { 655 if (focused == null) { 656 focused = window; 657 658 AccessibilityNodeInfo root = window.getRoot(); 659 assertEquals(sUiAutomation.findFocus( 660 AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), root); 661 assertEquals(root.findFocus( 662 AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), root); 663 } else { 664 throw new AssertionError("Duplicate accessibility focus"); 665 } 666 } else { 667 AccessibilityNodeInfo root = window.getRoot(); 668 if (root != null) { 669 assertNull(root.findFocus( 670 AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)); 671 } 672 } 673 } 674 } 675 676 private void ensureAppWindowFocusedOrFail(int appWindowIndex) throws TimeoutException { 677 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 678 AccessibilityWindowInfo focusTarget = null; 679 680 int visitedAppWindows = -1; 681 final int windowCount = windows.size(); 682 for (int i = 0; i < windowCount; i++) { 683 AccessibilityWindowInfo window = windows.get(i); 684 if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) { 685 visitedAppWindows++; 686 if (appWindowIndex <= visitedAppWindows) { 687 focusTarget = window; 688 break; 689 } 690 } 691 } 692 693 if (focusTarget == null) { 694 throw new IllegalStateException("Couldn't find app window: " + appWindowIndex); 695 } 696 697 if (focusTarget.isAccessibilityFocused()) { 698 return; 699 } 700 701 final AccessibilityWindowInfo finalFocusTarget = focusTarget; 702 sUiAutomation.executeAndWaitForEvent(() -> assertTrue(finalFocusTarget.getRoot() 703 .performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)), 704 filterWindowsChangedWithChangeTypes(WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED), 705 DEFAULT_TIMEOUT_MS); 706 707 windows = sUiAutomation.getWindows(); 708 for (int i = 0; i < windowCount; i++) { 709 AccessibilityWindowInfo window = windows.get(i); 710 if (window.getId() == focusTarget.getId()) { 711 assertTrue(window.isAccessibilityFocused()); 712 break; 713 } 714 } 715 } 716 717 private View[] addTwoAppPanelWindows() throws TimeoutException { 718 setAccessInteractiveWindowsFlag(); 719 sUiAutomation 720 .waitForIdle(TIMEOUT_WINDOW_STATE_IDLE, DEFAULT_TIMEOUT_MS); 721 722 return new View[] { 723 addWindow(R.string.button1, params -> { 724 params.gravity = Gravity.TOP; 725 params.y = getStatusBarHeight(mActivity); 726 }), 727 addWindow(R.string.button2, params -> { 728 params.gravity = Gravity.BOTTOM; 729 }) 730 }; 731 } 732 733 private Button addWindow(int btnTextRes, Consumer<WindowManager.LayoutParams> configure) 734 throws TimeoutException { 735 AtomicReference<Button> result = new AtomicReference<>(); 736 sUiAutomation.executeAndWaitForEvent(() -> { 737 sInstrumentation.runOnMainSync(() -> { 738 final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); 739 params.width = WindowManager.LayoutParams.MATCH_PARENT; 740 params.height = WindowManager.LayoutParams.WRAP_CONTENT; 741 params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 742 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR 743 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 744 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 745 params.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 746 params.token = mActivity.getWindow().getDecorView().getWindowToken(); 747 configure.accept(params); 748 749 final Button button = new Button(mActivity); 750 button.setText(btnTextRes); 751 result.set(button); 752 mActivity.getWindowManager().addView(button, params); 753 }); 754 }, filterWindowsChangedWithChangeTypes(WINDOWS_CHANGE_ADDED), DEFAULT_TIMEOUT_MS); 755 return result.get(); 756 } 757 758 private void setAccessInteractiveWindowsFlag () { 759 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 760 info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 761 sUiAutomation.setServiceInfo(info); 762 } 763 764 private void clearAccessInteractiveWindowsFlag () { 765 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 766 info.flags &= ~AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 767 sUiAutomation.setServiceInfo(info); 768 } 769 770 private void ensureAccessibilityFocusCleared() { 771 try { 772 sUiAutomation.executeAndWaitForEvent(() -> { 773 List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); 774 final int windowCount = windows.size(); 775 for (int i = 0; i < windowCount; i++) { 776 AccessibilityWindowInfo window = windows.get(i); 777 if (window.isAccessibilityFocused()) { 778 AccessibilityNodeInfo root = window.getRoot(); 779 if (root != null) { 780 root.performAction( 781 AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 782 } 783 } 784 } 785 }, filterForEventType(TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED), DEFAULT_TIMEOUT_MS); 786 } catch (TimeoutException te) { 787 /* ignore */ 788 } 789 } 790 791 private void verifyNodesInAppWindow(AccessibilityNodeInfo root) throws Exception { 792 try { 793 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 794 info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; 795 sUiAutomation.setServiceInfo(info); 796 797 root.refresh(); 798 799 // make list of expected nodes 800 List<String> classNameAndTextList = new ArrayList<String>(); 801 classNameAndTextList.add("android.widget.LinearLayout"); 802 classNameAndTextList.add("android.widget.LinearLayout"); 803 classNameAndTextList.add("android.widget.LinearLayout"); 804 classNameAndTextList.add("android.widget.LinearLayout"); 805 classNameAndTextList.add("android.widget.ButtonB1"); 806 classNameAndTextList.add("android.widget.ButtonB2"); 807 classNameAndTextList.add("android.widget.ButtonB3"); 808 classNameAndTextList.add("android.widget.ButtonB4"); 809 classNameAndTextList.add("android.widget.ButtonB5"); 810 classNameAndTextList.add("android.widget.ButtonB6"); 811 classNameAndTextList.add("android.widget.ButtonB7"); 812 classNameAndTextList.add("android.widget.ButtonB8"); 813 classNameAndTextList.add("android.widget.ButtonB9"); 814 815 boolean verifyContent = false; 816 817 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); 818 fringe.add(root); 819 820 // do a BFS traversal and check nodes 821 while (!fringe.isEmpty()) { 822 AccessibilityNodeInfo current = fringe.poll(); 823 824 if (!verifyContent 825 && CONTENT_VIEW_RES_NAME.equals(current.getViewIdResourceName())) { 826 verifyContent = true; 827 } 828 829 if (verifyContent) { 830 CharSequence text = current.getText(); 831 String receivedClassNameAndText = current.getClassName().toString() 832 + ((text != null) ? text.toString() : ""); 833 String expectedClassNameAndText = classNameAndTextList.remove(0); 834 835 assertEquals("Did not get the expected node info", 836 expectedClassNameAndText, receivedClassNameAndText); 837 } 838 839 final int childCount = current.getChildCount(); 840 for (int i = 0; i < childCount; i++) { 841 AccessibilityNodeInfo child = current.getChild(i); 842 fringe.add(child); 843 } 844 } 845 } finally { 846 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 847 info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; 848 sUiAutomation.setServiceInfo(info); 849 } 850 } 851 852 private static class IsSortedBy<T> extends TypeSafeMatcher<List<T>> { 853 854 private final Function<T, ? extends Comparable> mProperty; 855 private final boolean mAscending; 856 857 private IsSortedBy(Function<T, ? extends Comparable> comparator, boolean ascending) { 858 mProperty = comparator; 859 mAscending = ascending; 860 } 861 862 @Override 863 public void describeTo(Description description) { 864 description.appendText("is sorted"); 865 } 866 867 @Override 868 protected boolean matchesSafely(List<T> item) { 869 for (int i = 0; i < item.size() - 1; i++) { 870 Comparable a = mProperty.apply(item.get(i)); 871 Comparable b = mProperty.apply(item.get(i)); 872 int aMinusB = a.compareTo(b); 873 if (aMinusB != 0 && (aMinusB < 0) != mAscending) { 874 return false; 875 } 876 } 877 return true; 878 } 879 } 880 } 881