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 com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
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         assertWithMessage("Button should have a11y focus").that(
102                 mButton.isAccessibilityFocused()).isTrue();
103         mAccessibilityInteractionController.clearAccessibilityFocusClientThread();
104         sInstrumentation.waitForIdleSync();
105         assertWithMessage("Button should not have a11y focus").that(
106                 mButton.isAccessibilityFocused()).isFalse();
107     }
108 
109     @Test
clearAccessibilityFocus_uiThread_shouldClearFocus()110     public void clearAccessibilityFocus_uiThread_shouldClearFocus() throws Exception {
111         performAccessibilityFocus("com.android.frameworks.coretests:id/appNameBtn");
112         assertWithMessage("Button should have a11y focus").that(
113                 mButton.isAccessibilityFocused()).isTrue();
114         sInstrumentation.runOnMainSync(() ->
115                 mAccessibilityInteractionController.clearAccessibilityFocusClientThread());
116         assertWithMessage("Button should not have a11y focus").that(
117                 mButton.isAccessibilityFocused()).isFalse();
118     }
119 
120     @Test
clearAccessibilityFocus_sensitiveRootView_shouldClearFocus()121     public void clearAccessibilityFocus_sensitiveRootView_shouldClearFocus()
122             throws Exception {
123         final View rootView = mButton.getRootView();
124         assertThat(rootView).isNotNull();
125         try {
126             rootView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES);
127             performAccessibilityFocus("com.android.frameworks.coretests:id/appNameBtn");
128             assertWithMessage("Button should have a11y focus").that(
129                     mButton.isAccessibilityFocused()).isTrue();
130             sInstrumentation.runOnMainSync(() ->
131                     mAccessibilityInteractionController.clearAccessibilityFocusClientThread());
132             assertWithMessage("Button should not have a11y focus").that(
133                     mButton.isAccessibilityFocused()).isFalse();
134         } finally {
135             rootView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_AUTO);
136         }
137     }
138 
launchActivity()139     private void launchActivity() {
140         final Object waitObject = new Object();
141         final int[] location = new int[2];
142         final StringBuilder activityPackage = new StringBuilder();
143         final Rect bounds = new Rect();
144         final StringBuilder activityTitle = new StringBuilder();
145         try {
146             final long executionStartTimeMillis = SystemClock.uptimeMillis();
147             sUiAutomation.setOnAccessibilityEventListener((event) -> {
148                 if (event.getEventTime() < executionStartTimeMillis) {
149                     return;
150                 }
151                 synchronized (waitObject) {
152                     waitObject.notifyAll();
153                 }
154             });
155             enableRetrieveAccessibilityWindows();
156 
157             final Activity activity = mActivityRule.launchActivity(null);
158             sInstrumentation.runOnMainSync(() -> {
159                 activity.getWindow().getDecorView().getLocationOnScreen(location);
160                 activityPackage.append(activity.getPackageName());
161                 activityTitle.append(activity.getTitle());
162             });
163             sInstrumentation.waitForIdleSync();
164 
165             TestUtils.waitOn(waitObject, () -> {
166                 final AccessibilityWindowInfo window = findWindowByTitle(activityTitle);
167                 if (window == null) return false;
168                 window.getBoundsInScreen(bounds);
169                 activity.getWindow().getDecorView().getLocationOnScreen(location);
170                 if (bounds.isEmpty()) {
171                     return false;
172                 }
173                 return (!bounds.isEmpty())
174                         && (bounds.left == location[0]) && (bounds.top == location[1]);
175             }, TIMEOUT_DEFAULT, "Launch Activity");
176         } finally {
177             sUiAutomation.setOnAccessibilityEventListener(null);
178         }
179     }
180 
enableRetrieveAccessibilityWindows()181     private void enableRetrieveAccessibilityWindows() {
182         AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
183         info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
184         sUiAutomation.setServiceInfo(info);
185     }
186 
enableTouchExploration(boolean enabled)187     private void enableTouchExploration(boolean enabled) {
188         final Object waitObject = new Object();
189         final AccessibilityManager accessibilityManager =
190                 (AccessibilityManager) sInstrumentation.getContext().getSystemService(
191                         Service.ACCESSIBILITY_SERVICE);
192         final AccessibilityManager.TouchExplorationStateChangeListener listener = status -> {
193             synchronized (waitObject) {
194                 waitObject.notifyAll();
195             }
196         };
197         try {
198             accessibilityManager.addTouchExplorationStateChangeListener(listener);
199             final AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
200             if (enabled) {
201                 info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
202             } else {
203                 info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
204             }
205             sUiAutomation.setServiceInfo(info);
206             TestUtils.waitOn(waitObject,
207                     () -> accessibilityManager.isTouchExplorationEnabled() == enabled,
208                     TIMEOUT_DEFAULT,
209                     (enabled ? "Enable" : "Disable") + "touch exploration");
210         } finally {
211             accessibilityManager.removeTouchExplorationStateChangeListener(listener);
212         }
213     }
214 
performAccessibilityFocus(String viewId)215     private void performAccessibilityFocus(String viewId) throws TimeoutException {
216         final AccessibilityNodeInfo node = sUiAutomation.getRootInActiveWindow()
217                 .findAccessibilityNodeInfosByViewId(viewId).get(0);
218         // Perform an action and wait for an event
219         sUiAutomation.executeAndWaitForEvent(
220                 () -> node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS),
221                 event -> event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
222                 TIMEOUT_DEFAULT);
223         node.refresh();
224     }
225 
findWindowByTitle(CharSequence title)226     private AccessibilityWindowInfo findWindowByTitle(CharSequence title) {
227         final List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
228         AccessibilityWindowInfo returnValue = null;
229         for (int i = 0; i < windows.size(); i++) {
230             final AccessibilityWindowInfo window = windows.get(i);
231             if (TextUtils.equals(title, window.getTitle())) {
232                 returnValue = window;
233             } else {
234                 window.recycle();
235             }
236         }
237         return returnValue;
238     }
239 }
240