Home | History | Annotate | Download | only in cts
      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