blob: b119d7d117cdc0e301e83974fe6f08d2e817e9a6 [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.accessibilityservice.AccessibilityService;
import android.os.Binder;
import android.os.RemoteException;
import android.util.Slog;
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;
/**
* 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.
*/
@SuppressWarnings("MissingPermissionAnnotation")
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 int mNodeWithReplacementActionsInteractionId;
private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mReplacementNodeIsReadyOrFailed;
@GuardedBy("mLock")
AccessibilityNodeInfo mNodeWithReplacementActions;
@GuardedBy("mLock")
List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
@GuardedBy("mLock")
boolean mSetFindNodeFromOriginalWindowCalled = false;
@GuardedBy("mLock")
AccessibilityNodeInfo mNodeFromOriginalWindow;
@GuardedBy("mLock")
boolean mSetFindNodesFromOriginalWindowCalled = false;
@GuardedBy("mLock")
List<AccessibilityNodeInfo> mPrefetchedNodesFromOriginalWindow;
@GuardedBy("mLock")
boolean mSetPrefetchFromOriginalWindowCalled = false;
public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
IAccessibilityInteractionConnection connectionWithReplacementActions,
int interactionId, int interrogatingPid, long interrogatingTid) {
mServiceCallback = serviceCallback;
mConnectionWithReplacementActions = connectionWithReplacementActions;
mInteractionId = interactionId;
mNodeWithReplacementActionsInteractionId = interactionId + 1;
// Request the root node of the replacing window
final long identityToken = Binder.clearCallingIdentity();
try {
mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
AccessibilityNodeInfo.ROOT_NODE_ID, null,
mNodeWithReplacementActionsInteractionId, this, 0,
interrogatingPid, interrogatingTid, null, null, null);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
mReplacementNodeIsReadyOrFailed = true;
} finally {
Binder.restoreCallingIdentity(identityToken);
}
}
@Override
public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
synchronized (mLock) {
if (interactionId == mInteractionId) {
mNodeFromOriginalWindow = info;
mSetFindNodeFromOriginalWindowCalled = true;
} else if (interactionId == mNodeWithReplacementActionsInteractionId) {
mNodeWithReplacementActions = info;
mReplacementNodeIsReadyOrFailed = true;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
}
replaceInfoActionsAndCallServiceIfReady();
}
@Override
public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
int interactionId) {
synchronized (mLock) {
if (interactionId == mInteractionId) {
mNodesFromOriginalWindow = infos;
mSetFindNodesFromOriginalWindowCalled = true;
} else if (interactionId == mNodeWithReplacementActionsInteractionId) {
setNodeWithReplacementActionsFromList(infos);
mReplacementNodeIsReadyOrFailed = true;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
}
replaceInfoActionsAndCallServiceIfReady();
}
@Override
public void setPrefetchAccessibilityNodeInfoResult(List<AccessibilityNodeInfo> infos,
int interactionId)
throws RemoteException {
synchronized (mLock) {
if (interactionId == mInteractionId) {
mPrefetchedNodesFromOriginalWindow = infos;
mSetPrefetchFromOriginalWindowCalled = true;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
}
replaceInfoActionsAndCallServiceIfReady();
}
private void replaceInfoActionsAndCallServiceIfReady() {
replaceInfoActionsAndCallService();
replaceInfosActionsAndCallService();
replacePrefetchInfosActionsAndCallService();
}
private void setNodeWithReplacementActionsFromList(List<AccessibilityNodeInfo> infos) {
for (int i = 0; i < infos.size(); i++) {
AccessibilityNodeInfo info = infos.get(i);
if (info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID) {
mNodeWithReplacementActions = info;
}
}
}
@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);
}
@Override
public void sendTakeScreenshotOfWindowError(int errorCode, int interactionId)
throws RemoteException {
mServiceCallback.sendTakeScreenshotOfWindowError(errorCode, interactionId);
}
private void replaceInfoActionsAndCallService() {
final AccessibilityNodeInfo nodeToReturn;
boolean doCallback = false;
synchronized (mLock) {
doCallback = mReplacementNodeIsReadyOrFailed
&& mSetFindNodeFromOriginalWindowCalled;
if (doCallback && mNodeFromOriginalWindow != null) {
replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
mSetFindNodeFromOriginalWindowCalled = false;
}
nodeToReturn = mNodeFromOriginalWindow;
}
if (doCallback) {
try {
mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
}
}
}
}
private void replaceInfosActionsAndCallService() {
List<AccessibilityNodeInfo> nodesToReturn = null;
boolean doCallback = false;
synchronized (mLock) {
doCallback = mReplacementNodeIsReadyOrFailed
&& mSetFindNodesFromOriginalWindowCalled;
if (doCallback) {
nodesToReturn = replaceActionsLocked(mNodesFromOriginalWindow);
mSetFindNodesFromOriginalWindowCalled = false;
}
}
if (doCallback) {
try {
mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
}
}
}
}
private void replacePrefetchInfosActionsAndCallService() {
List<AccessibilityNodeInfo> nodesToReturn = null;
boolean doCallback = false;
synchronized (mLock) {
doCallback = mReplacementNodeIsReadyOrFailed
&& mSetPrefetchFromOriginalWindowCalled;
if (doCallback) {
nodesToReturn = replaceActionsLocked(mPrefetchedNodesFromOriginalWindow);
mSetPrefetchFromOriginalWindowCalled = false;
}
}
if (doCallback) {
try {
mServiceCallback.setPrefetchAccessibilityNodeInfoResult(
nodesToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
}
}
}
}
@GuardedBy("mLock")
private List<AccessibilityNodeInfo> replaceActionsLocked(List<AccessibilityNodeInfo> infos) {
if (infos != null) {
for (int i = 0; i < infos.size(); i++) {
replaceActionsOnInfoLocked(infos.get(i));
}
}
return (infos == null)
? null : new ArrayList<>(infos);
}
@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)
&& mNodeWithReplacementActions != null) {
List<AccessibilityAction> actions = mNodeWithReplacementActions.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(mNodeWithReplacementActions.isClickable());
info.setFocusable(mNodeWithReplacementActions.isFocusable());
info.setContextClickable(mNodeWithReplacementActions.isContextClickable());
info.setScrollable(mNodeWithReplacementActions.isScrollable());
info.setLongClickable(mNodeWithReplacementActions.isLongClickable());
info.setDismissable(mNodeWithReplacementActions.isDismissable());
}
}
@Override
public void sendAttachOverlayResult(
@AccessibilityService.AttachOverlayResult int result, int interactionId)
throws RemoteException {
mServiceCallback.sendAttachOverlayResult(result, interactionId);
}
}