Home | History | Annotate | Download | only in accessibility
      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