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