ActionReplacingCallbackTest.java revision 22e0d48bbe15293da067eefe3a73ef59fa66b062
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
17package com.android.server.accessibility;
18
19import android.graphics.Region;
20import android.os.RemoteException;
21import android.view.MagnificationSpec;
22import android.view.accessibility.AccessibilityNodeInfo;
23import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
24import android.view.accessibility.AccessibilityWindowInfo;
25import android.view.accessibility.IAccessibilityInteractionConnection;
26import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
27
28import org.hamcrest.BaseMatcher;
29import org.hamcrest.Description;
30import org.hamcrest.Matcher;
31import org.junit.Before;
32import org.junit.Test;
33import org.mockito.ArgumentCaptor;
34import org.mockito.Captor;
35import org.mockito.Mock;
36
37import java.util.Arrays;
38import java.util.HashSet;
39import java.util.List;
40
41import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS;
42import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
43import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK;
44import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE;
45import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND;
46import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CONTEXT_CLICK;
47import static junit.framework.TestCase.assertTrue;
48import static org.junit.Assert.assertEquals;
49import static org.junit.Assert.assertFalse;
50import static org.junit.Assert.assertNotEquals;
51import static org.junit.Assert.assertThat;
52import static org.mockito.Matchers.anyInt;
53import static org.mockito.Matchers.anyObject;
54import static org.mockito.Matchers.eq;
55import static org.mockito.Mockito.doThrow;
56import static org.mockito.Mockito.verify;
57import static org.mockito.Mockito.verifyNoMoreInteractions;
58import static org.mockito.MockitoAnnotations.initMocks;
59
60/**
61 * Tests for ActionReplacingCallback
62 */
63public 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