| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.accessibilityservice.cts; |
| |
| import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType; |
| import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction; |
| import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangTypesAndWindowId; |
| import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangeTypesAndWindowTitle; |
| import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangedWithChangeTypes; |
| import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen; |
| import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen; |
| import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.supportsMultiDisplay; |
| import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS; |
| import static android.accessibilityservice.cts.utils.DisplayUtils.VirtualDisplaySession; |
| import static android.accessibilityservice.cts.utils.DisplayUtils.getStatusBarHeight; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; |
| import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; |
| import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; |
| import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED; |
| import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; |
| import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_LONG_CLICKED; |
| import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED; |
| import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED; |
| import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ADDED; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SELECT; |
| |
| import static junit.framework.TestCase.assertNull; |
| |
| import static org.hamcrest.Matchers.lessThan; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertNotSame; |
| import static org.junit.Assert.assertSame; |
| import static org.junit.Assert.assertThat; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.accessibility.cts.common.AccessibilityDumpOnFailureRule; |
| import android.accessibilityservice.AccessibilityService; |
| import android.accessibilityservice.AccessibilityServiceInfo; |
| import android.accessibilityservice.cts.activities.AccessibilityWindowQueryActivity; |
| import android.accessibilityservice.cts.activities.NonDefaultDisplayActivity; |
| import android.app.Activity; |
| import android.app.ActivityTaskManager; |
| import android.app.Instrumentation; |
| import android.app.UiAutomation; |
| import android.graphics.Rect; |
| import android.platform.test.annotations.AppModeFull; |
| import android.test.suitebuilder.annotation.MediumTest; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.Display; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.WindowManager; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; |
| import android.view.accessibility.AccessibilityWindowInfo; |
| import android.widget.Button; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.rule.ActivityTestRule; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.compatibility.common.util.SystemUtil; |
| import com.android.compatibility.common.util.TestUtils; |
| |
| import org.hamcrest.Description; |
| import org.hamcrest.TypeSafeMatcher; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.RuleChain; |
| import org.junit.runner.RunWith; |
| |
| import java.util.ArrayList; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| |
| /** |
| * Test cases for testing the accessibility APIs for querying of the screen content. |
| * These APIs allow exploring the screen and requesting an action to be performed |
| * on a given view from an AccessibilityService. |
| */ |
| @AppModeFull |
| @RunWith(AndroidJUnit4.class) |
| public class AccessibilityWindowQueryTest { |
| private static final String LOG_TAG = "AccessibilityWindowQueryTest"; |
| private static String CONTENT_VIEW_RES_NAME = |
| "android.accessibilityservice.cts:id/added_content"; |
| private static final long TIMEOUT_WINDOW_STATE_IDLE = 500; |
| private static Instrumentation sInstrumentation; |
| private static UiAutomation sUiAutomation; |
| |
| private AccessibilityWindowQueryActivity mActivity; |
| private Activity mActivityOnVirtualDisplay; |
| |
| private ActivityTestRule<AccessibilityWindowQueryActivity> mActivityRule = |
| new ActivityTestRule<>(AccessibilityWindowQueryActivity.class, false, false); |
| |
| private AccessibilityDumpOnFailureRule mDumpOnFailureRule = |
| new AccessibilityDumpOnFailureRule(); |
| |
| @Rule |
| public final RuleChain mRuleChain = RuleChain |
| .outerRule(mActivityRule) |
| .around(mDumpOnFailureRule); |
| |
| @BeforeClass |
| public static void oneTimeSetup() throws Exception { |
| sInstrumentation = InstrumentationRegistry.getInstrumentation(); |
| sUiAutomation = sInstrumentation.getUiAutomation(); |
| AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); |
| info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; |
| info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; |
| sUiAutomation.setServiceInfo(info); |
| } |
| |
| @AfterClass |
| public static void postTestTearDown() { |
| sUiAutomation.destroy(); |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mActivity = launchActivityAndWaitForItToBeOnscreen( |
| sInstrumentation, sUiAutomation, mActivityRule); |
| } |
| |
| @MediumTest |
| @Test |
| public void testFindByText() throws Throwable { |
| // First, make the root view of the activity an accessibility node. This allows us to |
| // later exclude views that are part of the activity's DecorView. |
| sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.added_content) |
| .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES)); |
| |
| // Start looking from the added content instead of from the root accessibility node so |
| // that nodes that we don't expect (i.e. window control buttons) are not included in the |
| // list of accessibility nodes returned by findAccessibilityNodeInfosByText. |
| final AccessibilityNodeInfo addedContent = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByViewId(CONTENT_VIEW_RES_NAME) |
| .get(0); |
| |
| // find a view by text |
| List<AccessibilityNodeInfo> buttons = addedContent.findAccessibilityNodeInfosByText("b"); |
| |
| assertEquals(9, buttons.size()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testFindByContentDescription() throws Exception { |
| // find a view by text |
| AccessibilityNodeInfo button = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByText(mActivity.getString(R.string.contentDescription)) |
| .get(0); |
| assertNotNull(button); |
| } |
| |
| @MediumTest |
| @Test |
| public void testTraverseWindow() throws Exception { |
| verifyNodesInAppWindow(sUiAutomation.getRootInActiveWindow()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testNoWindowsAccessIfFlagNotSet() throws Exception { |
| // Clear window access flag |
| AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); |
| info.flags &= ~AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; |
| sUiAutomation.setServiceInfo(info); |
| |
| // Make sure the windows cannot be accessed. |
| assertTrue(sUiAutomation.getWindows().isEmpty()); |
| |
| // Find a button to click on. |
| final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByViewId( |
| "android.accessibilityservice.cts:id/button1").get(0); |
| |
| // Click the button to generate an event |
| AccessibilityEvent event = |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button1.performAction(ACTION_CLICK), |
| filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK), |
| DEFAULT_TIMEOUT_MS); |
| |
| // Make sure the source window cannot be accessed. |
| assertNull(event.getSource().getWindow()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testTraverseAllWindows() throws Exception { |
| List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| Rect boundsInScreen = new Rect(); |
| |
| final int windowCount = windows.size(); |
| for (int i = 0; i < windowCount; i++) { |
| AccessibilityWindowInfo window = windows.get(i); |
| // Skip other Apps windows since their state might not be stable while querying. |
| if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION |
| && !TextUtils.equals(window.getTitle(), mActivity.getTitle())) { |
| continue; |
| } |
| window.getBoundsInScreen(boundsInScreen); |
| assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, emptiness check. |
| assertNull(window.getParent()); |
| assertSame(0, window.getChildCount()); |
| assertNull(window.getParent()); |
| assertNotNull(window.getRoot()); |
| |
| if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) { |
| assertTrue(window.isFocused()); |
| assertTrue(window.isActive()); |
| verifyNodesInAppWindow(window.getRoot()); |
| } else if (window.getType() == AccessibilityWindowInfo.TYPE_SYSTEM) { |
| assertFalse(window.isFocused()); |
| assertFalse(window.isActive()); |
| } |
| } |
| } |
| |
| @MediumTest |
| @Test |
| public void testTraverseWindowFromEvent() throws Exception { |
| // Find a button to click on. |
| final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByViewId( |
| "android.accessibilityservice.cts:id/button1").get(0); |
| |
| // Click the button. |
| AccessibilityEvent event = |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button1.performAction(ACTION_CLICK), |
| filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK), |
| DEFAULT_TIMEOUT_MS); |
| |
| // Get the source window. |
| AccessibilityWindowInfo window = event.getSource().getWindow(); |
| |
| // Verify the application window. |
| Rect boundsInScreen = new Rect(); |
| window.getBoundsInScreen(boundsInScreen); |
| assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, so just emptiness check |
| assertSame(window.getType(), AccessibilityWindowInfo.TYPE_APPLICATION); |
| assertTrue(window.isFocused()); |
| assertTrue(window.isActive()); |
| assertNull(window.getParent()); |
| assertSame(0, window.getChildCount()); |
| assertNotNull(window.getRoot()); |
| |
| // Verify the window content. |
| verifyNodesInAppWindow(window.getRoot()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testInteractWithAppWindow() throws Exception { |
| // Find a button to click on. |
| final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByViewId( |
| "android.accessibilityservice.cts:id/button1").get(0); |
| |
| // Click the button. |
| AccessibilityEvent event = |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button1.performAction(ACTION_CLICK), |
| filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK), |
| DEFAULT_TIMEOUT_MS); |
| |
| // Get the source window. |
| AccessibilityWindowInfo window = event.getSource().getWindow(); |
| |
| // Find a another button from the event's window. |
| final AccessibilityNodeInfo button2 = window.getRoot() |
| .findAccessibilityNodeInfosByViewId( |
| "android.accessibilityservice.cts:id/button2").get(0); |
| |
| // Click the second button. |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button2.performAction(ACTION_CLICK), |
| filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK), |
| DEFAULT_TIMEOUT_MS); |
| } |
| |
| @MediumTest |
| @Test |
| public void testSingleAccessibilityFocusAcrossWindows() throws Exception { |
| try { |
| // Add two more windows. |
| final View views[]; |
| views = addTwoAppPanelWindows(mActivity); |
| |
| try { |
| // Put accessibility focus in the first app window. |
| ensureAppWindowFocusedOrFail(0); |
| // Make sure there only one accessibility focus. |
| assertSingleAccessibilityFocus(); |
| |
| // Put accessibility focus in the second app window. |
| ensureAppWindowFocusedOrFail(1); |
| // Make sure there only one accessibility focus. |
| assertSingleAccessibilityFocus(); |
| |
| // Put accessibility focus in the third app window. |
| ensureAppWindowFocusedOrFail(2); |
| // Make sure there only one accessibility focus. |
| assertSingleAccessibilityFocus(); |
| } finally { |
| // Clean up panel windows |
| sInstrumentation.runOnMainSync(() -> { |
| WindowManager wm = |
| sInstrumentation.getContext().getSystemService(WindowManager.class); |
| for (View view : views) { |
| wm.removeView(view); |
| } |
| }); |
| } |
| } finally { |
| ensureAccessibilityFocusCleared(); |
| } |
| } |
| |
| @MediumTest |
| @Test |
| public void testPerformActionSetAndClearFocus() throws Exception { |
| // find a view and make sure it is not focused |
| AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isFocused()); |
| |
| // focus the view |
| assertTrue(button.performAction(ACTION_FOCUS)); |
| |
| // find the view again and make sure it is focused |
| button = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); |
| assertTrue(button.isFocused()); |
| |
| // unfocus the view |
| assertTrue(button.performAction(ACTION_CLEAR_FOCUS)); |
| |
| // find the view again and make sure it is not focused |
| button = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isFocused()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testPerformActionSelect() throws Exception { |
| // find a view and make sure it is not selected |
| AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isSelected()); |
| |
| // select the view |
| assertTrue(button.performAction(ACTION_SELECT)); |
| |
| // find the view again and make sure it is selected |
| button = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); |
| assertTrue(button.isSelected()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testPerformActionClearSelection() throws Exception { |
| // find a view and make sure it is not selected |
| AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isSelected()); |
| |
| // select the view |
| assertTrue(button.performAction(ACTION_SELECT)); |
| |
| // find the view again and make sure it is selected |
| button = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); |
| |
| assertTrue(button.isSelected()); |
| |
| // unselect the view |
| assertTrue(button.performAction(ACTION_CLEAR_SELECTION)); |
| |
| // find the view again and make sure it is not selected |
| button = sUiAutomation.getRootInActiveWindow() |
| .findAccessibilityNodeInfosByText(mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isSelected()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testPerformActionClick() throws Exception { |
| // find a view and make sure it is not selected |
| final AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isSelected()); |
| |
| // Perform an action and wait for an event |
| AccessibilityEvent expected = |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button.performAction(ACTION_CLICK), |
| filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK), |
| DEFAULT_TIMEOUT_MS); |
| |
| // Make sure the expected event was received. |
| assertNotNull(expected); |
| } |
| |
| @MediumTest |
| @Test |
| public void testPerformActionLongClick() throws Exception { |
| // find a view and make sure it is not selected |
| final AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isSelected()); |
| |
| // Perform an action and wait for an event. |
| AccessibilityEvent expected = |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button.performAction(ACTION_LONG_CLICK), |
| filterForEventTypeWithAction(TYPE_VIEW_LONG_CLICKED, ACTION_LONG_CLICK), |
| DEFAULT_TIMEOUT_MS); |
| |
| // Make sure the expected event was received. |
| assertNotNull(expected); |
| } |
| |
| |
| @MediumTest |
| @Test |
| public void testPerformCustomAction() throws Exception { |
| // find a view and make sure it is not selected |
| AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| |
| // find the custom action and perform it |
| List<AccessibilityAction> actions = button.getActionList(); |
| final int actionCount = actions.size(); |
| for (int i = 0; i < actionCount; i++) { |
| AccessibilityAction action = actions.get(i); |
| if (action.getId() == R.id.foo_custom_action) { |
| assertSame(action.getLabel(), "Foo"); |
| // perform the action |
| assertTrue(button.performAction(action.getId())); |
| return; |
| } |
| } |
| } |
| |
| @MediumTest |
| @Test |
| public void testGetEventSource() throws Exception { |
| // find a view and make sure it is not focused |
| final AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| assertFalse(button.isSelected()); |
| |
| // focus and wait for the event |
| AccessibilityEvent awaitedEvent = |
| sUiAutomation.executeAndWaitForEvent( |
| () -> button.performAction(ACTION_FOCUS), |
| filterForEventTypeWithAction(TYPE_VIEW_FOCUSED, ACTION_FOCUS), |
| DEFAULT_TIMEOUT_MS); |
| |
| assertNotNull(awaitedEvent); |
| |
| // check that last event source |
| AccessibilityNodeInfo source = awaitedEvent.getSource(); |
| assertNotNull(source); |
| |
| // bounds |
| Rect buttonBounds = new Rect(); |
| button.getBoundsInParent(buttonBounds); |
| Rect sourceBounds = new Rect(); |
| source.getBoundsInParent(sourceBounds); |
| |
| assertEquals(buttonBounds.left, sourceBounds.left); |
| assertEquals(buttonBounds.right, sourceBounds.right); |
| assertEquals(buttonBounds.top, sourceBounds.top); |
| assertEquals(buttonBounds.bottom, sourceBounds.bottom); |
| |
| // char sequence attributes |
| assertEquals(button.getPackageName(), source.getPackageName()); |
| assertEquals(button.getClassName(), source.getClassName()); |
| assertEquals(button.getText().toString(), source.getText().toString()); |
| assertSame(button.getContentDescription(), source.getContentDescription()); |
| |
| // boolean attributes |
| assertSame(button.isFocusable(), source.isFocusable()); |
| assertSame(button.isClickable(), source.isClickable()); |
| assertSame(button.isEnabled(), source.isEnabled()); |
| assertNotSame(button.isFocused(), source.isFocused()); |
| assertSame(button.isLongClickable(), source.isLongClickable()); |
| assertSame(button.isPassword(), source.isPassword()); |
| assertSame(button.isSelected(), source.isSelected()); |
| assertSame(button.isCheckable(), source.isCheckable()); |
| assertSame(button.isChecked(), source.isChecked()); |
| } |
| |
| @MediumTest |
| @Test |
| public void testObjectContract() throws Exception { |
| try { |
| AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); |
| info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; |
| sUiAutomation.setServiceInfo(info); |
| |
| // find a view and make sure it is not focused |
| AccessibilityNodeInfo button = sUiAutomation |
| .getRootInActiveWindow().findAccessibilityNodeInfosByText( |
| mActivity.getString(R.string.button5)).get(0); |
| AccessibilityNodeInfo parent = button.getParent(); |
| final int childCount = parent.getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| AccessibilityNodeInfo child = parent.getChild(i); |
| assertNotNull(child); |
| if (child.equals(button)) { |
| assertEquals("Equal objects must have same hasCode.", button.hashCode(), |
| child.hashCode()); |
| return; |
| } |
| } |
| fail("Parent's children do not have the info whose parent is the parent."); |
| } finally { |
| AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); |
| info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; |
| sUiAutomation.setServiceInfo(info); |
| } |
| } |
| |
| @Test |
| public void testFindPictureInPictureWindow() throws Exception { |
| if (!sInstrumentation.getContext().getPackageManager() |
| .hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { |
| return; |
| } |
| sUiAutomation.executeAndWaitForEvent(() -> { |
| sInstrumentation.runOnMainSync(() -> { |
| mActivity.enterPictureInPictureMode(); |
| }); |
| }, filterForEventType(TYPE_WINDOWS_CHANGED), DEFAULT_TIMEOUT_MS); |
| sInstrumentation.waitForIdleSync(); |
| |
| // We should be able to find a picture-in-picture window now |
| int numPictureInPictureWindows = 0; |
| final List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| final int windowCount = windows.size(); |
| for (int i = 0; i < windowCount; i++) { |
| final AccessibilityWindowInfo window = windows.get(i); |
| if (window.isInPictureInPictureMode()) { |
| numPictureInPictureWindows++; |
| } |
| } |
| assertTrue(numPictureInPictureWindows >= 1); |
| } |
| |
| @Test |
| public void testGetWindows_resultIsSortedByLayerDescending() throws TimeoutException { |
| addTwoAppPanelWindows(mActivity); |
| List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| |
| AccessibilityWindowInfo windowAddedFirst = findWindow(windows, R.string.button1); |
| AccessibilityWindowInfo windowAddedSecond = findWindow(windows, R.string.button2); |
| assertThat(windowAddedFirst.getLayer(), lessThan(windowAddedSecond.getLayer())); |
| |
| assertThat(windows, new IsSortedBy<>(w -> w.getLayer(), /* ascending */ false)); |
| } |
| |
| @Test |
| public void testGetWindowsOnAllDisplays_resultIsSortedByLayerDescending() throws Exception { |
| assumeTrue(supportsMultiDisplay(sInstrumentation.getContext())); |
| |
| addTwoAppPanelWindows(mActivity); |
| // Creates a virtual display. |
| try (final VirtualDisplaySession displaySession = new VirtualDisplaySession()) { |
| final int virtualDisplayId = |
| displaySession.createDisplayWithDefaultDisplayMetricsAndWait( |
| sInstrumentation.getContext(), false).getDisplayId(); |
| // Launches an activity on virtual display. |
| mActivityOnVirtualDisplay = launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( |
| sInstrumentation, sUiAutomation, |
| NonDefaultDisplayActivity.class, |
| virtualDisplayId); |
| // Adds two app panel windows on activity of virtual display. |
| addTwoAppPanelWindows(mActivityOnVirtualDisplay); |
| |
| // Gets all windows. |
| SparseArray<List<AccessibilityWindowInfo>> allWindows = |
| sUiAutomation.getWindowsOnAllDisplays(); |
| assertNotNull(allWindows); |
| |
| // Gets windows on default display. |
| assertTrue(allWindows.contains(Display.DEFAULT_DISPLAY)); |
| List<AccessibilityWindowInfo> windowsOnDefaultDisplay = |
| allWindows.get(Display.DEFAULT_DISPLAY); |
| assertNotNull(windowsOnDefaultDisplay); |
| assertTrue(windowsOnDefaultDisplay.size() > 0); |
| |
| AccessibilityWindowInfo windowAddedFirst = |
| findWindow(windowsOnDefaultDisplay, R.string.button1); |
| AccessibilityWindowInfo windowAddedSecond = |
| findWindow(windowsOnDefaultDisplay, R.string.button2); |
| assertThat(windowAddedFirst.getLayer(), lessThan(windowAddedSecond.getLayer())); |
| assertThat(windowsOnDefaultDisplay, |
| new IsSortedBy<>(w -> w.getLayer(), /* ascending */ false)); |
| |
| // Gets windows on virtual display. |
| assertTrue(allWindows.contains(virtualDisplayId)); |
| List<AccessibilityWindowInfo> windowsOnVirtualDisplay = |
| allWindows.get(virtualDisplayId); |
| assertNotNull(windowsOnVirtualDisplay); |
| assertTrue(windowsOnVirtualDisplay.size() > 0); |
| |
| AccessibilityWindowInfo windowAddedFirstOnVirtualDisplay = |
| findWindow(windowsOnVirtualDisplay, R.string.button1); |
| AccessibilityWindowInfo windowAddedSecondOnVirtualDisplay = |
| findWindow(windowsOnVirtualDisplay, R.string.button2); |
| assertThat(windowAddedFirstOnVirtualDisplay.getLayer(), |
| lessThan(windowAddedSecondOnVirtualDisplay.getLayer())); |
| assertThat(windowsOnVirtualDisplay, |
| new IsSortedBy<>(w -> w.getLayer(), /* ascending */ false)); |
| |
| mActivityOnVirtualDisplay.finish(); |
| } |
| } |
| |
| @Test |
| public void testShowInputMethodDialogWindow_resultIsApplicationType() |
| throws TimeoutException { |
| final WindowManager wm = |
| sInstrumentation.getContext().getSystemService(WindowManager.class); |
| final View view = new View(sInstrumentation.getContext()); |
| final String windowTitle = "Input Method Dialog"; |
| |
| try { |
| sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync( |
| () -> { |
| WindowManager.LayoutParams params = new WindowManager.LayoutParams( |
| WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); |
| params.accessibilityTitle = windowTitle; |
| |
| SystemUtil.runWithShellPermissionIdentity(sUiAutomation, |
| () -> wm.addView(view, params), |
| "android.permission.INTERNAL_SYSTEM_WINDOW"); |
| }), |
| filterWindowsChangeTypesAndWindowTitle(sUiAutomation, |
| WINDOWS_CHANGE_ADDED, windowTitle), DEFAULT_TIMEOUT_MS); |
| |
| |
| final List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| assertTrue(windows.stream().anyMatch(window -> window.getType() |
| == AccessibilityWindowInfo.TYPE_APPLICATION)); |
| } finally { |
| try { |
| wm.removeView(view); |
| } catch (IllegalStateException e) { |
| Log.e(LOG_TAG, "remove view fail:" + e.toString()); |
| } |
| } |
| } |
| |
| private AccessibilityWindowInfo findWindow(List<AccessibilityWindowInfo> windows, |
| int btnTextRes) { |
| return windows.stream() |
| .filter(w -> w.getRoot() |
| .findAccessibilityNodeInfosByText( |
| sInstrumentation.getTargetContext().getString(btnTextRes)) |
| .size() == 1) |
| .findFirst() |
| .get(); |
| } |
| |
| private void assertSingleAccessibilityFocus() { |
| List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| AccessibilityWindowInfo focused = null; |
| |
| final int windowCount = windows.size(); |
| for (int i = 0; i < windowCount; i++) { |
| AccessibilityWindowInfo window = windows.get(i); |
| |
| if (window.isAccessibilityFocused()) { |
| if (focused == null) { |
| focused = window; |
| |
| AccessibilityNodeInfo root = window.getRoot(); |
| assertEquals(sUiAutomation.findFocus( |
| AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), root); |
| assertEquals(root.findFocus( |
| AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), root); |
| } else { |
| throw new AssertionError("Duplicate accessibility focus"); |
| } |
| } else { |
| AccessibilityNodeInfo root = window.getRoot(); |
| if (root != null) { |
| assertNull(root.findFocus( |
| AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)); |
| } |
| } |
| } |
| } |
| |
| private void ensureAppWindowFocusedOrFail(int appWindowIndex) throws TimeoutException { |
| List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| AccessibilityWindowInfo focusTarget = null; |
| |
| int visitedAppWindows = -1; |
| final int windowCount = windows.size(); |
| for (int i = 0; i < windowCount; i++) { |
| AccessibilityWindowInfo window = windows.get(i); |
| if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) { |
| visitedAppWindows++; |
| if (appWindowIndex <= visitedAppWindows) { |
| focusTarget = window; |
| break; |
| } |
| } |
| } |
| |
| if (focusTarget == null) { |
| throw new IllegalStateException("Couldn't find app window: " + appWindowIndex); |
| } |
| |
| if (focusTarget.isAccessibilityFocused()) { |
| return; |
| } |
| |
| final AccessibilityWindowInfo finalFocusTarget = focusTarget; |
| sUiAutomation.executeAndWaitForEvent(() -> assertTrue(finalFocusTarget.getRoot() |
| .performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)), |
| filterWindowsChangTypesAndWindowId(finalFocusTarget.getId(), |
| WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED), |
| DEFAULT_TIMEOUT_MS); |
| |
| windows = sUiAutomation.getWindows(); |
| for (int i = 0; i < windowCount; i++) { |
| AccessibilityWindowInfo window = windows.get(i); |
| if (window.getId() == focusTarget.getId()) { |
| assertTrue(window.isAccessibilityFocused()); |
| break; |
| } |
| } |
| } |
| |
| private View[] addTwoAppPanelWindows(Activity activity) throws TimeoutException { |
| sUiAutomation |
| .waitForIdle(TIMEOUT_WINDOW_STATE_IDLE, DEFAULT_TIMEOUT_MS); |
| |
| return new View[]{ |
| addWindow(R.string.button1, params -> { |
| params.gravity = Gravity.TOP; |
| params.y = getStatusBarHeight(activity); |
| }, activity), |
| addWindow(R.string.button2, params -> { |
| params.gravity = Gravity.BOTTOM; |
| }, activity) |
| }; |
| } |
| |
| private Button addWindow(int btnTextRes, Consumer<WindowManager.LayoutParams> configure, |
| Activity activity) throws TimeoutException { |
| AtomicReference<Button> result = new AtomicReference<>(); |
| sUiAutomation.executeAndWaitForEvent(() -> { |
| sInstrumentation.runOnMainSync(() -> { |
| final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); |
| params.width = WindowManager.LayoutParams.MATCH_PARENT; |
| params.height = WindowManager.LayoutParams.WRAP_CONTENT; |
| params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
| | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
| | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
| | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; |
| params.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; |
| params.token = activity.getWindow().getDecorView().getWindowToken(); |
| configure.accept(params); |
| |
| final Button button = new Button(activity); |
| button.setText(btnTextRes); |
| result.set(button); |
| activity.getWindowManager().addView(button, params); |
| }); |
| }, filterWindowsChangedWithChangeTypes(WINDOWS_CHANGE_ADDED), DEFAULT_TIMEOUT_MS); |
| return result.get(); |
| } |
| |
| private void ensureAccessibilityFocusCleared() { |
| try { |
| sUiAutomation.executeAndWaitForEvent( |
| () -> { |
| List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows(); |
| final int windowCount = windows.size(); |
| for (int i = 0; i < windowCount; i++) { |
| AccessibilityWindowInfo window = windows.get(i); |
| if (window.isAccessibilityFocused()) { |
| AccessibilityNodeInfo root = window.getRoot(); |
| if (root != null) { |
| root.performAction( |
| AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); |
| } |
| } |
| } |
| }, |
| filterForEventTypeWithAction( |
| TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, |
| ACTION_CLEAR_ACCESSIBILITY_FOCUS), |
| DEFAULT_TIMEOUT_MS); |
| } catch (TimeoutException te) { |
| /* ignore */ |
| } |
| } |
| |
| private void verifyNodesInAppWindow(AccessibilityNodeInfo root) throws Exception { |
| try { |
| AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); |
| info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; |
| sUiAutomation.setServiceInfo(info); |
| |
| root.refresh(); |
| |
| // make list of expected nodes |
| List<String> classNameAndTextList = new ArrayList<String>(); |
| classNameAndTextList.add("android.widget.LinearLayout"); |
| classNameAndTextList.add("android.widget.LinearLayout"); |
| classNameAndTextList.add("android.widget.LinearLayout"); |
| classNameAndTextList.add("android.widget.LinearLayout"); |
| classNameAndTextList.add("android.widget.ButtonB1"); |
| classNameAndTextList.add("android.widget.ButtonB2"); |
| classNameAndTextList.add("android.widget.ButtonB3"); |
| classNameAndTextList.add("android.widget.ButtonB4"); |
| classNameAndTextList.add("android.widget.ButtonB5"); |
| classNameAndTextList.add("android.widget.ButtonB6"); |
| classNameAndTextList.add("android.widget.ButtonB7"); |
| classNameAndTextList.add("android.widget.ButtonB8"); |
| classNameAndTextList.add("android.widget.ButtonB9"); |
| |
| boolean verifyContent = false; |
| |
| Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); |
| fringe.add(root); |
| |
| // do a BFS traversal and check nodes |
| while (!fringe.isEmpty()) { |
| AccessibilityNodeInfo current = fringe.poll(); |
| |
| if (!verifyContent |
| && CONTENT_VIEW_RES_NAME.equals(current.getViewIdResourceName())) { |
| verifyContent = true; |
| } |
| |
| if (verifyContent) { |
| CharSequence text = current.getText(); |
| String receivedClassNameAndText = current.getClassName().toString() |
| + ((text != null) ? text.toString() : ""); |
| String expectedClassNameAndText = classNameAndTextList.remove(0); |
| |
| assertEquals("Did not get the expected node info", |
| expectedClassNameAndText, receivedClassNameAndText); |
| } |
| |
| final int childCount = current.getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| AccessibilityNodeInfo child = current.getChild(i); |
| fringe.add(child); |
| } |
| } |
| } finally { |
| AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); |
| info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; |
| sUiAutomation.setServiceInfo(info); |
| } |
| } |
| |
| private static class IsSortedBy<T> extends TypeSafeMatcher<List<T>> { |
| |
| private final Function<T, ? extends Comparable> mProperty; |
| private final boolean mAscending; |
| |
| private IsSortedBy(Function<T, ? extends Comparable> comparator, boolean ascending) { |
| mProperty = comparator; |
| mAscending = ascending; |
| } |
| |
| @Override |
| public void describeTo(Description description) { |
| description.appendText("is sorted"); |
| } |
| |
| @Override |
| protected boolean matchesSafely(List<T> item) { |
| for (int i = 0; i < item.size() - 1; i++) { |
| Comparable a = mProperty.apply(item.get(i)); |
| Comparable b = mProperty.apply(item.get(i)); |
| int aMinusB = a.compareTo(b); |
| if (aMinusB != 0 && (aMinusB < 0) != mAscending) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| } |