1 /*
2  * Copyright (C) 2018 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.view;
18 
19 import static junit.framework.Assert.assertFalse;
20 import static junit.framework.Assert.assertTrue;
21 
22 import android.accessibilityservice.AccessibilityServiceInfo;
23 import android.app.Activity;
24 import android.app.Instrumentation;
25 import android.app.Service;
26 import android.app.UiAutomation;
27 import android.graphics.Rect;
28 import android.os.SystemClock;
29 import android.text.TextUtils;
30 import android.view.accessibility.AccessibilityEvent;
31 import android.view.accessibility.AccessibilityManager;
32 import android.view.accessibility.AccessibilityNodeInfo;
33 import android.view.accessibility.AccessibilityTestActivity;
34 import android.view.accessibility.AccessibilityWindowInfo;
35 
36 import androidx.test.InstrumentationRegistry;
37 import androidx.test.ext.junit.runners.AndroidJUnit4;
38 import androidx.test.rule.ActivityTestRule;
39 
40 import com.android.compatibility.common.util.TestUtils;
41 import com.android.frameworks.coretests.R;
42 
43 import org.junit.After;
44 import org.junit.AfterClass;
45 import org.junit.Before;
46 import org.junit.BeforeClass;
47 import org.junit.Rule;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 
51 import java.util.List;
52 import java.util.concurrent.TimeoutException;
53 
54 @RunWith(AndroidJUnit4.class)
55 public class AccessibilityInteractionControllerTest {
56     static final long TIMEOUT_DEFAULT = 10000; // 10 seconds
57 
58     private static Instrumentation sInstrumentation;
59     private static UiAutomation sUiAutomation;
60 
61     @Rule
62     public ActivityTestRule<AccessibilityTestActivity> mActivityRule = new ActivityTestRule<>(
63             AccessibilityTestActivity.class, false, false);
64 
65     private AccessibilityInteractionController mAccessibilityInteractionController;
66     private ViewRootImpl mViewRootImpl;
67     private View mButton;
68 
69     @BeforeClass
oneTimeSetup()70     public static void oneTimeSetup() {
71         sInstrumentation = InstrumentationRegistry.getInstrumentation();
72         sUiAutomation = sInstrumentation.getUiAutomation();
73     }
74 
75     @AfterClass
postTestTearDown()76     public static void postTestTearDown() {
77         sUiAutomation.destroy();
78     }
79 
80     @Before
setUp()81     public void setUp() throws Throwable {
82         launchActivity();
83         enableTouchExploration(true);
84         mActivityRule.runOnUiThread(() -> {
85             mViewRootImpl = mActivityRule.getActivity().getWindow().getDecorView()
86                     .getViewRootImpl();
87             mButton = mActivityRule.getActivity().findViewById(R.id.appNameBtn);
88         });
89         mAccessibilityInteractionController =
90                 mViewRootImpl.getAccessibilityInteractionController();
91     }
92 
93     @After
tearDown()94     public void tearDown() {
95         enableTouchExploration(false);
96     }
97 
98     @Test
clearAccessibilityFocus_shouldClearFocus()99     public void clearAccessibilityFocus_shouldClearFocus() throws Exception {
100         performAccessibilityFocus("com.android.frameworks.coretests:id/appNameBtn");
101         assertTrue("Button should have a11y focus",
102                 mButton.isAccessibilityFocused());
103         mAccessibilityInteractionController.clearAccessibilityFocusClientThread();
104         sInstrumentation.waitForIdleSync();
105         assertFalse("Button should not have a11y focus",
106                 mButton.isAccessibilityFocused());
107     }
108 
109     @Test
clearAccessibilityFocus_uiThread_shouldClearFocus()110     public void clearAccessibilityFocus_uiThread_shouldClearFocus() throws Exception {
111         performAccessibilityFocus("com.android.frameworks.coretests:id/appNameBtn");
112         assertTrue("Button should have a11y focus",
113                 mButton.isAccessibilityFocused());
114         sInstrumentation.runOnMainSync(() -> {
115             mAccessibilityInteractionController.clearAccessibilityFocusClientThread();
116         });
117         assertFalse("Button should not have a11y focus",
118                 mButton.isAccessibilityFocused());
119     }
120 
launchActivity()121     private void launchActivity() {
122         final Object waitObject = new Object();
123         final int[] location = new int[2];
124         final StringBuilder activityPackage = new StringBuilder();
125         final Rect bounds = new Rect();
126         final StringBuilder activityTitle = new StringBuilder();
127         try {
128             final long executionStartTimeMillis = SystemClock.uptimeMillis();
129             sUiAutomation.setOnAccessibilityEventListener((event) -> {
130                 if (event.getEventTime() < executionStartTimeMillis) {
131                     return;
132                 }
133                 synchronized (waitObject) {
134                     waitObject.notifyAll();
135                 }
136             });
137             enableRetrieveAccessibilityWindows();
138 
139             final Activity activity = mActivityRule.launchActivity(null);
140             sInstrumentation.runOnMainSync(() -> {
141                 activity.getWindow().getDecorView().getLocationOnScreen(location);
142                 activityPackage.append(activity.getPackageName());
143                 activityTitle.append(activity.getTitle());
144             });
145             sInstrumentation.waitForIdleSync();
146 
147             TestUtils.waitOn(waitObject, () -> {
148                 final AccessibilityWindowInfo window = findWindowByTitle(activityTitle);
149                 if (window == null) return false;
150                 window.getBoundsInScreen(bounds);
151                 activity.getWindow().getDecorView().getLocationOnScreen(location);
152                 if (bounds.isEmpty()) {
153                     return false;
154                 }
155                 return (!bounds.isEmpty())
156                         && (bounds.left == location[0]) && (bounds.top == location[1]);
157             }, TIMEOUT_DEFAULT, "Launch Activity");
158         } finally {
159             sUiAutomation.setOnAccessibilityEventListener(null);
160         }
161     }
162 
enableRetrieveAccessibilityWindows()163     private void enableRetrieveAccessibilityWindows() {
164         AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
165         info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
166         sUiAutomation.setServiceInfo(info);
167     }
168 
enableTouchExploration(boolean enabled)169     private void enableTouchExploration(boolean enabled) {
170         final Object waitObject = new Object();
171         final AccessibilityManager accessibilityManager =
172                 (AccessibilityManager) sInstrumentation.getContext().getSystemService(
173                         Service.ACCESSIBILITY_SERVICE);
174         final AccessibilityManager.TouchExplorationStateChangeListener listener = status -> {
175             synchronized (waitObject) {
176                 waitObject.notifyAll();
177             }
178         };
179         try {
180             accessibilityManager.addTouchExplorationStateChangeListener(listener);
181             final AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
182             if (enabled) {
183                 info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
184             } else {
185                 info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
186             }
187             sUiAutomation.setServiceInfo(info);
188             TestUtils.waitOn(waitObject,
189                     () -> accessibilityManager.isTouchExplorationEnabled() == enabled,
190                     TIMEOUT_DEFAULT,
191                     (enabled ? "Enable" : "Disable") + "touch exploration");
192         } finally {
193             accessibilityManager.removeTouchExplorationStateChangeListener(listener);
194         }
195     }
196 
performAccessibilityFocus(String viewId)197     private void performAccessibilityFocus(String viewId) throws TimeoutException {
198         final AccessibilityNodeInfo node = sUiAutomation.getRootInActiveWindow()
199                 .findAccessibilityNodeInfosByViewId(viewId).get(0);
200         // Perform an action and wait for an event
201         sUiAutomation.executeAndWaitForEvent(
202                 () -> node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS),
203                 event -> event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
204                 TIMEOUT_DEFAULT);
205         node.refresh();
206     }
207 
findWindowByTitle(CharSequence title)208     private AccessibilityWindowInfo findWindowByTitle(CharSequence title) {
209         final List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
210         AccessibilityWindowInfo returnValue = null;
211         for (int i = 0; i < windows.size(); i++) {
212             final AccessibilityWindowInfo window = windows.get(i);
213             if (TextUtils.equals(title, window.getTitle())) {
214                 returnValue = window;
215             } else {
216                 window.recycle();
217             }
218         }
219         return returnValue;
220     }
221 }
222