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