blob: 72820f15315effa81821559e9629d8f2eb8850af [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import android.graphics.Region;
import android.os.RemoteException;
import android.view.MagnificationSpec;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CONTEXT_CLICK;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.MockitoAnnotations.initMocks;
/**
* Tests for ActionReplacingCallback
*/
public class ActionReplacingCallbackTest {
private static final int INTERACTION_ID = 0xBEEF;
private static final int INTERROGATING_PID = 0xFEED;
private static final int APP_WINDOW_ID = 0xACE;
private static final int NON_ROOT_NODE_ID = 0xAAAA5555;
private static final long INTERROGATING_TID = 0x1234FACE;
// We expect both the replacer actions and a11y focus actions to appear
private static final AccessibilityAction[] REQUIRED_ACTIONS_ON_ROOT_TO_SERVICE =
{ACTION_CLICK, ACTION_EXPAND, ACTION_ACCESSIBILITY_FOCUS,
ACTION_CLEAR_ACCESSIBILITY_FOCUS};
private static final Matcher<AccessibilityNodeInfo> HAS_NO_ACTIONS =
new BaseMatcher<AccessibilityNodeInfo>() {
@Override
public boolean matches(Object o) {
AccessibilityNodeInfo node = (AccessibilityNodeInfo) o;
if (!node.getActionList().isEmpty()) return false;
return (!node.isScrollable() && !node.isLongClickable() && !node.isClickable()
&& !node.isContextClickable() && !node.isDismissable() && !node.isFocusable());
}
@Override
public void describeTo(Description description) {
description.appendText("Has no actions");
}
};
private static final Matcher<AccessibilityNodeInfo> HAS_EXPECTED_ACTIONS_ON_ROOT =
new BaseMatcher<AccessibilityNodeInfo>() {
@Override
public boolean matches(Object o) {
AccessibilityNodeInfo node = (AccessibilityNodeInfo) o;
List<AccessibilityAction> actions = node.getActionList();
if ((actions.size() != 4) || !actions.contains(ACTION_CLICK)
|| !actions.contains(ACTION_EXPAND)
|| !actions.contains(ACTION_ACCESSIBILITY_FOCUS)) {
return false;
}
return (!node.isScrollable() && !node.isLongClickable()
&& !node.isLongClickable() && node.isClickable()
&& !node.isContextClickable() && !node.isDismissable()
&& !node.isFocusable());
}
@Override
public void describeTo(Description description) {
description.appendText("Has only 4 actions expected on root");
}
};
@Mock IAccessibilityInteractionConnectionCallback mMockServiceCallback;
@Mock IAccessibilityInteractionConnection mMockReplacerConnection;
@Captor private ArgumentCaptor<Integer> mInteractionIdCaptor;
@Captor private ArgumentCaptor<AccessibilityNodeInfo> mInfoCaptor;
@Captor private ArgumentCaptor<List<AccessibilityNodeInfo>> mInfoListCaptor;
private ActionReplacingCallback mActionReplacingCallback;
private int mReplacerInteractionId;
@Before
public void setUp() throws RemoteException {
initMocks(this);
mActionReplacingCallback = new ActionReplacingCallback(
mMockServiceCallback, mMockReplacerConnection, INTERACTION_ID, INTERROGATING_PID,
INTERROGATING_TID);
verify(mMockReplacerConnection).findAccessibilityNodeInfoByAccessibilityId(
eq(AccessibilityNodeInfo.ROOT_NODE_ID), (Region) anyObject(),
mInteractionIdCaptor.capture(), eq(mActionReplacingCallback), eq(0),
eq(INTERROGATING_PID), eq(INTERROGATING_TID), (MagnificationSpec) anyObject(),
eq(null));
mReplacerInteractionId = mInteractionIdCaptor.getValue().intValue();
}
@Test
public void testConstructor_registersToGetRootNodeOfActionReplacer() throws RemoteException {
assertNotEquals(INTERACTION_ID, mReplacerInteractionId);
verifyNoMoreInteractions(mMockServiceCallback);
}
@Test
public void testCallbacks_singleRootNodeThenReplacer_returnsNodeWithReplacedActions()
throws RemoteException {
AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain();
infoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID);
infoFromApp.addAction(ACTION_CONTEXT_CLICK);
mActionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID);
verifyNoMoreInteractions(mMockServiceCallback);
mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(),
mReplacerInteractionId);
verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(),
eq(INTERACTION_ID));
AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue();
assertEquals(AccessibilityNodeInfo.ROOT_NODE_ID, infoSentToService.getSourceNodeId());
assertThat(infoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT);
}
@Test
public void testCallbacks_singleNonrootNodeThenReplacer_returnsNodeWithNoActions()
throws RemoteException {
AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain();
infoFromApp.setSourceNodeId(NON_ROOT_NODE_ID, APP_WINDOW_ID);
infoFromApp.addAction(ACTION_CONTEXT_CLICK);
mActionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID);
verifyNoMoreInteractions(mMockServiceCallback);
mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(),
mReplacerInteractionId);
verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(),
eq(INTERACTION_ID));
AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue();
assertEquals(NON_ROOT_NODE_ID, infoSentToService.getSourceNodeId());
assertThat(infoSentToService, HAS_NO_ACTIONS);
}
@Test
public void testCallbacks_replacerThenSingleRootNode_returnsNodeWithReplacedActions()
throws RemoteException {
mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(),
mReplacerInteractionId);
verifyNoMoreInteractions(mMockServiceCallback);
AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain();
infoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID);
infoFromApp.addAction(ACTION_CONTEXT_CLICK);
mActionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID);
verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(),
eq(INTERACTION_ID));
AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue();
assertEquals(AccessibilityNodeInfo.ROOT_NODE_ID, infoSentToService.getSourceNodeId());
assertThat(infoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT);
}
@Test
public void testCallbacks_multipleNodesThenReplacer_clearsActionsAndAddsSomeToRoot()
throws RemoteException {
mActionReplacingCallback
.setFindAccessibilityNodeInfosResult(getAppNodeList(), INTERACTION_ID);
verifyNoMoreInteractions(mMockServiceCallback);
mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(),
mReplacerInteractionId);
verify(mMockServiceCallback).setFindAccessibilityNodeInfosResult(mInfoListCaptor.capture(),
eq(INTERACTION_ID));
assertEquals(2, mInfoListCaptor.getValue().size());
AccessibilityNodeInfo rootInfoSentToService = getNodeWithIdFromList(
mInfoListCaptor.getValue(), AccessibilityNodeInfo.ROOT_NODE_ID);
AccessibilityNodeInfo otherInfoSentToService = getNodeWithIdFromList(
mInfoListCaptor.getValue(), NON_ROOT_NODE_ID);
assertThat(rootInfoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT);
assertThat(otherInfoSentToService, HAS_NO_ACTIONS);
}
@Test
public void testCallbacks_replacerThenMultipleNodes_clearsActionsAndAddsSomeToRoot()
throws RemoteException {
mActionReplacingCallback.setFindAccessibilityNodeInfosResult(getReplacerNodes(),
mReplacerInteractionId);
verifyNoMoreInteractions(mMockServiceCallback);
mActionReplacingCallback
.setFindAccessibilityNodeInfosResult(getAppNodeList(), INTERACTION_ID);
verify(mMockServiceCallback).setFindAccessibilityNodeInfosResult(mInfoListCaptor.capture(),
eq(INTERACTION_ID));
assertEquals(2, mInfoListCaptor.getValue().size());
AccessibilityNodeInfo rootInfoSentToService = getNodeWithIdFromList(
mInfoListCaptor.getValue(), AccessibilityNodeInfo.ROOT_NODE_ID);
AccessibilityNodeInfo otherInfoSentToService = getNodeWithIdFromList(
mInfoListCaptor.getValue(), NON_ROOT_NODE_ID);
assertThat(rootInfoSentToService, HAS_EXPECTED_ACTIONS_ON_ROOT);
assertThat(otherInfoSentToService, HAS_NO_ACTIONS);
}
@Test
public void testConstructor_actionReplacerThrowsException_passesDataToService()
throws RemoteException {
doThrow(RemoteException.class).when(mMockReplacerConnection)
.findAccessibilityNodeInfoByAccessibilityId(eq(AccessibilityNodeInfo.ROOT_NODE_ID),
(Region) anyObject(), anyInt(), (ActionReplacingCallback) anyObject(),
eq(0), eq(INTERROGATING_PID), eq(INTERROGATING_TID),
(MagnificationSpec) anyObject(), eq(null));
ActionReplacingCallback actionReplacingCallback = new ActionReplacingCallback(
mMockServiceCallback, mMockReplacerConnection, INTERACTION_ID, INTERROGATING_PID,
INTERROGATING_TID);
verifyNoMoreInteractions(mMockServiceCallback);
AccessibilityNodeInfo infoFromApp = AccessibilityNodeInfo.obtain();
infoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID);
infoFromApp.addAction(ACTION_CONTEXT_CLICK);
infoFromApp.setContextClickable(true);
actionReplacingCallback.setFindAccessibilityNodeInfoResult(infoFromApp, INTERACTION_ID);
verify(mMockServiceCallback).setFindAccessibilityNodeInfoResult(mInfoCaptor.capture(),
eq(INTERACTION_ID));
AccessibilityNodeInfo infoSentToService = mInfoCaptor.getValue();
assertEquals(AccessibilityNodeInfo.ROOT_NODE_ID, infoSentToService.getSourceNodeId());
assertThat(infoSentToService, HAS_NO_ACTIONS);
}
@Test
public void testSetPerformAccessibilityActionResult_actsAsPassThrough() throws RemoteException {
mActionReplacingCallback.setPerformAccessibilityActionResult(true, INTERACTION_ID);
verify(mMockServiceCallback).setPerformAccessibilityActionResult(true, INTERACTION_ID);
mActionReplacingCallback.setPerformAccessibilityActionResult(false, INTERACTION_ID);
verify(mMockServiceCallback).setPerformAccessibilityActionResult(false, INTERACTION_ID);
}
private List<AccessibilityNodeInfo> getReplacerNodes() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID,
AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
root.addAction(ACTION_CLICK);
root.addAction(ACTION_EXPAND);
root.setClickable(true);
// Second node should have no effect
AccessibilityNodeInfo other = AccessibilityNodeInfo.obtain();
other.setSourceNodeId(NON_ROOT_NODE_ID,
AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
other.addAction(ACTION_COLLAPSE);
return Arrays.asList(root, other);
}
private AccessibilityNodeInfo getNodeWithIdFromList(
List<AccessibilityNodeInfo> infos, long id) {
for (AccessibilityNodeInfo info : infos) {
if (info.getSourceNodeId() == id) {
return info;
}
}
assertTrue("Didn't find node", false);
return null;
}
private List<AccessibilityNodeInfo> getAppNodeList() {
AccessibilityNodeInfo rootInfoFromApp = AccessibilityNodeInfo.obtain();
rootInfoFromApp.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, APP_WINDOW_ID);
rootInfoFromApp.addAction(ACTION_CONTEXT_CLICK);
AccessibilityNodeInfo otherInfoFromApp = AccessibilityNodeInfo.obtain();
otherInfoFromApp.setSourceNodeId(NON_ROOT_NODE_ID, APP_WINDOW_ID);
otherInfoFromApp.addAction(ACTION_CLICK);
return Arrays.asList(rootInfoFromApp, otherInfoFromApp);
}
}