1 /* 2 * Copyright (C) 2017 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 com.android.server.accessibility; 18 19 import android.graphics.Region; 20 import android.os.RemoteException; 21 import android.view.MagnificationSpec; 22 import android.view.accessibility.AccessibilityNodeInfo; 23 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 24 import android.view.accessibility.AccessibilityWindowInfo; 25 import android.view.accessibility.IAccessibilityInteractionConnection; 26 import android.view.accessibility.IAccessibilityInteractionConnectionCallback; 27 28 import org.hamcrest.BaseMatcher; 29 import org.hamcrest.Description; 30 import org.hamcrest.Matcher; 31 import org.junit.Before; 32 import org.junit.Test; 33 import org.mockito.ArgumentCaptor; 34 import org.mockito.Captor; 35 import org.mockito.Mock; 36 37 import java.util.Arrays; 38 import java.util.HashSet; 39 import java.util.List; 40 41 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS; 42 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS; 43 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK; 44 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE; 45 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND; 46 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CONTEXT_CLICK; 47 import static junit.framework.TestCase.assertTrue; 48 import static org.junit.Assert.assertEquals; 49 import static org.junit.Assert.assertFalse; 50 import static org.junit.Assert.assertNotEquals; 51 import static org.junit.Assert.assertThat; 52 import static org.mockito.Matchers.anyInt; 53 import static org.mockito.Matchers.anyObject; 54 import static org.mockito.Matchers.eq; 55 import static org.mockito.Mockito.doThrow; 56 import static org.mockito.Mockito.verify; 57 import static org.mockito.Mockito.verifyNoMoreInteractions; 58 import static org.mockito.MockitoAnnotations.initMocks; 59 60 /** 61 * Tests for ActionReplacingCallback 62 */ 63 public class ActionReplacingCallbackTest { 64 private static final int INTERACTION_ID = 0xBEEF; 65 private static final int INTERROGATING_PID = 0xFEED; 66 private static final int APP_WINDOW_ID = 0xACE; 67 private static final int NON_ROOT_NODE_ID = 0xAAAA5555; 68 private static final long INTERROGATING_TID = 0x1234FACE; 69 70 // We expect both the replacer actions and a11y focus actions to appear 71 private static final AccessibilityAction[] REQUIRED_ACTIONS_ON_ROOT_TO_SERVICE = 72 {ACTION_CLICK, ACTION_EXPAND, ACTION_ACCESSIBILITY_FOCUS, 73 ACTION_CLEAR_ACCESSIBILITY_FOCUS}; 74 75 private static final Matcher<AccessibilityNodeInfo> HAS_NO_ACTIONS = 76 new BaseMatcher<AccessibilityNodeInfo>() { 77 @Override 78 public boolean matches(Object o) { 79 AccessibilityNodeInfo node = (AccessibilityNodeInfo) o; 80 if (!node.getActionList().isEmpty()) return false; 81 return (!node.isScrollable() && !node.isLongClickable() && !node.isClickable() 82 && !node.isContextClickable() && !node.isDismissable() && !node.isFocusable()); 83 } 84 85 @Override 86 public void describeTo(Description description) { 87 description.appendText("Has no actions"); 88 } 89 }; 90 91 private static final Matcher<AccessibilityNodeInfo> HAS_EXPECTED_ACTIONS_ON_ROOT = 92 new BaseMatcher<AccessibilityNodeInfo>() { 93 @Override 94 public boolean matches(Object o) { 95 AccessibilityNodeInfo node = (AccessibilityNodeInfo) o; 96 List<AccessibilityAction> actions = node.getActionList(); 97 if ((actions.size() != 4) || !actions.contains(ACTION_CLICK) 98 || !actions.contains(ACTION_EXPAND) 99 || !actions.contains(ACTION_ACCESSIBILITY_FOCUS)) { 100 return false; 101 } 102 return (!node.isScrollable() && !node.isLongClickable() 103 && !node.isLongClickable() && node.isClickable() 104 && !node.isContextClickable() && !node.isDismissable() 105 && !node.isFocusable()); 106 } 107 108 @Override 109 public void describeTo(Description description) { 110 description.appendText("Has only 4 actions expected on root"); 111 } 112 }; 113 114 @Mock IAccessibilityInteractionConnectionCallback mMockServiceCallback; 115 @Mock IAccessibilityInteractionConnection mMockReplacerConnection; 116 117 @Captor private ArgumentCaptor<Integer> mInteractionIdCaptor; 118 @Captor private ArgumentCaptor<AccessibilityNodeInfo> mInfoCaptor; 119 @Captor private ArgumentCaptor<List<AccessibilityNodeInfo>> mInfoListCaptor; 120 121 private ActionReplacingCallback mActionReplacingCallback; 122 private int mReplacerInteractionId; 123 124 @Before 125 public void setUp() throws RemoteException { 126 initMocks(this); 127 mActionReplacingCallback = new ActionReplacingCallback( 128 mMockServiceCallback, mMockReplacerConnection, INTERACTION_ID, INTERROGATING_PID, 129 INTERROGATING_TID); 130 verify(mMockReplacerConnection).findAccessibilityNodeInfoByAccessibilityId( 131 eq(AccessibilityNodeInfo.ROOT_NODE_ID), (Region) anyObject(), 132 mInteractionIdCaptor.capture(), eq(mActionReplacingCallback), eq(0), 133 eq(INTERROGATING_PID), eq(INTERROGATING_TID), (MagnificationSpec) anyObject(), 134 eq(null)); 135 mReplacerInteractionId = mInteractionIdCaptor.getValue().intValue(); 136 } 137 138 @Test 139 public void testConstructor_registersToGetRootNodeOfActionReplacer() throws RemoteException { 140 assertNotEquals(INTERACTION_ID, mReplacerInteractionId); 141 verifyNoMoreInteractions(mMockServiceCallback); 142 } 143 144 @Test 145 public void testCallbacks_singleRootNodeThenReplacer_returnsNodeWithReplacedActions() 146 throws RemoteException { 147 AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain(); 148 infoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID); 149 infoFromApp.addAction(ACTION_CONTEXT_CLICK); 150 mActionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID); 151 verifyNoMoreInteractions(mMockServiceCallback); 152 153 mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(), 154 mReplacerInteractionId); 155 156 verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(), 157 eq(INTERACTION_ID)); 158 AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue(); 159 assertEquals(AccessibilityNodeInfo.ROOT_NODE_ID, infoSentToService.getSourceNodeId()); 160 assertThat(infoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT); 161 } 162 163 @Test 164 public void testCallbacks_singleNonrootNodeThenReplacer_returnsNodeWithNoActions() 165 throws RemoteException { 166 AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain(); 167 infoFromApp.setSourceNodeId(NON_ROOT_NODE_ID, APP_WINDOW_ID); 168 infoFromApp.addAction(ACTION_CONTEXT_CLICK); 169 mActionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID); 170 verifyNoMoreInteractions(mMockServiceCallback); 171 172 mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(), 173 mReplacerInteractionId); 174 175 verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(), 176 eq(INTERACTION_ID)); 177 AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue(); 178 assertEquals(NON_ROOT_NODE_ID, infoSentToService.getSourceNodeId()); 179 assertThat(infoSentToService, HAS_NO_ACTIONS); 180 } 181 182 @Test 183 public void testCallbacks_replacerThenSingleRootNode_returnsNodeWithReplacedActions() 184 throws RemoteException { 185 mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(), 186 mReplacerInteractionId); 187 verifyNoMoreInteractions(mMockServiceCallback); 188 189 AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain(); 190 infoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID); 191 infoFromApp.addAction(ACTION_CONTEXT_CLICK); 192 mActionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID); 193 194 verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(), 195 eq(INTERACTION_ID)); 196 AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue(); 197 assertEquals(AccessibilityNodeInfo.ROOT_NODE_ID, infoSentToService.getSourceNodeId()); 198 assertThat(infoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT); 199 } 200 201 @Test 202 public void testCallbacks_multipleNodesThenReplacer_clearsActionsAndAddsSomeToRoot() 203 throws RemoteException { 204 mActionReplacingCallback 205 .setFindAccessibilityNodeInfosResult(getAppNodeList(), INTERACTION_ID); 206 verifyNoMoreInteractions(mMockServiceCallback); 207 208 mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(), 209 mReplacerInteractionId); 210 211 verify(mMockServiceCallback).setFindAccessibilityNodeInfosResult(mInfoListCaptor.capture(), 212 eq(INTERACTION_ID)); 213 assertEquals(2, mInfoListCaptor.getValue().size()); 214 AccessibilityNodeInfo rootInfoSentToService = getNodeWithIdFromList( 215 mInfoListCaptor.getValue(), AccessibilityNodeInfo.ROOT_NODE_ID); 216 AccessibilityNodeInfo otherInfoSentToService = getNodeWithIdFromList( 217 mInfoListCaptor.getValue(), NON_ROOT_NODE_ID); 218 assertThat(rootInfoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT); 219 assertThat(otherInfoSentToService, HAS_NO_ACTIONS); 220 } 221 222 @Test 223 public void testCallbacks_replacerThenMultipleNodes_clearsActionsAndAddsSomeToRoot() 224 throws RemoteException { 225 mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(), 226 mReplacerInteractionId); 227 verifyNoMoreInteractions(mMockServiceCallback); 228 229 mActionReplacingCallback 230 .setFindAccessibilityNodeInfosResult(getAppNodeList(), INTERACTION_ID); 231 232 verify(mMockServiceCallback).setFindAccessibilityNodeInfosResult(mInfoListCaptor.capture(), 233 eq(INTERACTION_ID)); 234 assertEquals(2, mInfoListCaptor.getValue().size()); 235 AccessibilityNodeInfo rootInfoSentToService = getNodeWithIdFromList( 236 mInfoListCaptor.getValue(), AccessibilityNodeInfo.ROOT_NODE_ID); 237 AccessibilityNodeInfo otherInfoSentToService = getNodeWithIdFromList( 238 mInfoListCaptor.getValue(), NON_ROOT_NODE_ID); 239 assertThat(rootInfoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT); 240 assertThat(otherInfoSentToService, HAS_NO_ACTIONS); 241 } 242 243 @Test 244 public void testConstructor_actionReplacerThrowsException_passesDataToService() 245 throws RemoteException { 246 doThrow(RemoteException.class).when(mMockReplacerConnection) 247 .findAccessibilityNodeInfoByAccessibilityId(eq(AccessibilityNodeInfo.ROOT_NODE_ID), 248 (Region) anyObject(), anyInt(), (ActionReplacingCallback) anyObject(), 249 eq(0), eq(INTERROGATING_PID), eq(INTERROGATING_TID), 250 (MagnificationSpec) anyObject(), eq(null)); 251 ActionReplacingCallback actionReplacingCallback = new ActionReplacingCallback( 252 mMockServiceCallback, mMockReplacerConnection, INTERACTION_ID, INTERROGATING_PID, 253 INTERROGATING_TID); 254 255 verifyNoMoreInteractions(mMockServiceCallback); 256 AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain(); 257 infoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID); 258 infoFromApp.addAction(ACTION_CONTEXT_CLICK); 259 infoFromApp.setContextClickable(true); 260 actionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID); 261 262 verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(), 263 eq(INTERACTION_ID)); 264 AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue(); 265 assertEquals(AccessibilityNodeInfo.ROOT_NODE_ID, infoSentToService.getSourceNodeId()); 266 assertThat(infoSentToService, HAS_NO_ACTIONS); 267 } 268 269 @Test 270 public void testSetPerformAccessibilityActionResult_actsAsPassThrough() throws RemoteException { 271 mActionReplacingCallback.setPerformAccessibilityActionResult(true, INTERACTION_ID); 272 verify(mMockServiceCallback).setPerformAccessibilityActionResult(true, INTERACTION_ID); 273 mActionReplacingCallback.setPerformAccessibilityActionResult(false, INTERACTION_ID); 274 verify(mMockServiceCallback).setPerformAccessibilityActionResult(false, INTERACTION_ID); 275 } 276 277 278 private List<AccessibilityNodeInfo> getReplacerNodes() { 279 AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain(); 280 root.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, 281 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 282 root.addAction(ACTION_CLICK); 283 root.addAction(ACTION_EXPAND); 284 root.setClickable(true); 285 286 // Second node should have no effect 287 AccessibilityNodeInfo other = AccessibilityNodeInfo.obtain(); 288 other.setSourceNodeId(NON_ROOT_NODE_ID, 289 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 290 other.addAction(ACTION_COLLAPSE); 291 292 return Arrays.asList(root, other); 293 } 294 295 private AccessibilityNodeInfo getNodeWithIdFromList( 296 List<AccessibilityNodeInfo> infos, long id) { 297 for (AccessibilityNodeInfo info : infos) { 298 if (info.getSourceNodeId() == id) { 299 return info; 300 } 301 } 302 assertTrue("Didn't find node", false); 303 return null; 304 } 305 306 private List<AccessibilityNodeInfo> getAppNodeList() { 307 AccessibilityNodeInfo rootInfoFromApp = AccessibilityNodeInfo.obtain(); 308 rootInfoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID); 309 rootInfoFromApp.addAction(ACTION_CONTEXT_CLICK); 310 AccessibilityNodeInfo otherInfoFromApp = AccessibilityNodeInfo.obtain(); 311 otherInfoFromApp.setSourceNodeId(NON_ROOT_NODE_ID, APP_WINDOW_ID); 312 otherInfoFromApp.addAction(ACTION_CLICK); 313 return Arrays.asList(rootInfoFromApp, otherInfoFromApp); 314 } 315 } 316