blob: de8a5181b2cffb86d7c99573e8f8da59f5088047 [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.os.Binder;
import android.os.RemoteException;
import android.util.Slog;
import android.view.MagnificationSpec;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* If we are stripping and/or replacing the actions from a window, we need to intercept the
* nodes heading back to the service and swap out the actions.
*/
public class ActionReplacingCallback extends IAccessibilityInteractionConnectionCallback.Stub {
private static final boolean DEBUG = false;
private static final String LOG_TAG = "ActionReplacingCallback";
private final IAccessibilityInteractionConnectionCallback mServiceCallback;
private final IAccessibilityInteractionConnection mConnectionWithReplacementActions;
private final int mInteractionId;
private final Object mLock = new Object();
@GuardedBy("mLock")
List<AccessibilityNodeInfo> mNodesWithReplacementActions;
@GuardedBy("mLock")
List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
@GuardedBy("mLock")
AccessibilityNodeInfo mNodeFromOriginalWindow;
// Keep track of whether or not we've been called back for a single node
@GuardedBy("mLock")
boolean mSingleNodeCallbackHappened;
// Keep track of whether or not we've been called back for multiple node
@GuardedBy("mLock")
boolean mMultiNodeCallbackHappened;
// We shouldn't get any more callbacks after we've called back the original service, but
// keep track to make sure we catch such strange things
@GuardedBy("mLock")
boolean mDone;
public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
IAccessibilityInteractionConnection connectionWithReplacementActions,
int interactionId, int interrogatingPid, long interrogatingTid) {
mServiceCallback = serviceCallback;
mConnectionWithReplacementActions = connectionWithReplacementActions;
mInteractionId = interactionId;
// Request the root node of the replacing window
final long identityToken = Binder.clearCallingIdentity();
try {
mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0,
interrogatingPid, interrogatingTid, null, null);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
// Pretend we already got a (null) list of replacement nodes
mMultiNodeCallbackHappened = true;
} finally {
Binder.restoreCallingIdentity(identityToken);
}
}
@Override
public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
boolean readyForCallback;
synchronized(mLock) {
if (interactionId == mInteractionId) {
mNodeFromOriginalWindow = info;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
mSingleNodeCallbackHappened = true;
readyForCallback = mMultiNodeCallbackHappened;
}
if (readyForCallback) {
replaceInfoActionsAndCallService();
}
}
@Override
public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
int interactionId) {
boolean callbackForSingleNode;
boolean callbackForMultipleNodes;
synchronized(mLock) {
if (interactionId == mInteractionId) {
mNodesFromOriginalWindow = infos;
} else if (interactionId == mInteractionId + 1) {
mNodesWithReplacementActions = infos;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
callbackForSingleNode = mSingleNodeCallbackHappened;
callbackForMultipleNodes = mMultiNodeCallbackHappened;
mMultiNodeCallbackHappened = true;
}
if (callbackForSingleNode) {
replaceInfoActionsAndCallService();
}
if (callbackForMultipleNodes) {
replaceInfosActionsAndCallService();
}
}
@Override
public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId)
throws RemoteException {
// There's no reason to use this class when performing actions. Do something reasonable.
mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId);
}
private void replaceInfoActionsAndCallService() {
final AccessibilityNodeInfo nodeToReturn;
synchronized (mLock) {
if (mDone) {
if (DEBUG) {
Slog.e(LOG_TAG, "Extra callback");
}
return;
}
if (mNodeFromOriginalWindow != null) {
replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
}
recycleReplaceActionNodesLocked();
nodeToReturn = mNodeFromOriginalWindow;
mDone = true;
}
try {
mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
}
}
}
private void replaceInfosActionsAndCallService() {
final List<AccessibilityNodeInfo> nodesToReturn;
synchronized (mLock) {
if (mDone) {
if (DEBUG) {
Slog.e(LOG_TAG, "Extra callback");
}
return;
}
if (mNodesFromOriginalWindow != null) {
for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
}
}
recycleReplaceActionNodesLocked();
nodesToReturn = (mNodesFromOriginalWindow == null)
? null : new ArrayList<>(mNodesFromOriginalWindow);
mDone = true;
}
try {
mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
}
}
}
@GuardedBy("mLock")
private void replaceActionsOnInfoLocked(AccessibilityNodeInfo info) {
info.removeAllActions();
info.setClickable(false);
info.setFocusable(false);
info.setContextClickable(false);
info.setScrollable(false);
info.setLongClickable(false);
info.setDismissable(false);
// We currently only replace actions for the root node
if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
&& mNodesWithReplacementActions != null) {
// This list should always contain a single node with the root ID
for (int i = 0; i < mNodesWithReplacementActions.size(); i++) {
AccessibilityNodeInfo nodeWithReplacementActions =
mNodesWithReplacementActions.get(i);
if (nodeWithReplacementActions.getSourceNodeId()
== AccessibilityNodeInfo.ROOT_NODE_ID) {
List<AccessibilityAction> actions = nodeWithReplacementActions.getActionList();
if (actions != null) {
for (int j = 0; j < actions.size(); j++) {
info.addAction(actions.get(j));
}
// The PIP needs to be able to take accessibility focus
info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
}
info.setClickable(nodeWithReplacementActions.isClickable());
info.setFocusable(nodeWithReplacementActions.isFocusable());
info.setContextClickable(nodeWithReplacementActions.isContextClickable());
info.setScrollable(nodeWithReplacementActions.isScrollable());
info.setLongClickable(nodeWithReplacementActions.isLongClickable());
info.setDismissable(nodeWithReplacementActions.isDismissable());
}
}
}
}
@GuardedBy("mLock")
private void recycleReplaceActionNodesLocked() {
if (mNodesWithReplacementActions == null) return;
for (int i = mNodesWithReplacementActions.size() - 1; i >= 0; i--) {
AccessibilityNodeInfo nodeWithReplacementAction = mNodesWithReplacementActions.get(i);
nodeWithReplacementAction.recycle();
}
mNodesWithReplacementActions = null;
}
}