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.os.Binder;
20import android.os.RemoteException;
21import android.util.Slog;
22import android.view.MagnificationSpec;
23import android.view.accessibility.AccessibilityNodeInfo;
24import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
25import android.view.accessibility.IAccessibilityInteractionConnection;
26import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
27import com.android.internal.annotations.GuardedBy;
28
29import java.util.ArrayList;
30import java.util.List;
31import java.util.concurrent.atomic.AtomicInteger;
32
33/**
34 * If we are stripping and/or replacing the actions from a window, we need to intercept the
35 * nodes heading back to the service and swap out the actions.
36 */
37public class ActionReplacingCallback extends IAccessibilityInteractionConnectionCallback.Stub {
38    private static final boolean DEBUG = false;
39    private static final String LOG_TAG = "ActionReplacingCallback";
40
41    private final IAccessibilityInteractionConnectionCallback mServiceCallback;
42    private final IAccessibilityInteractionConnection mConnectionWithReplacementActions;
43    private final int mInteractionId;
44    private final Object mLock = new Object();
45
46    @GuardedBy("mLock")
47    List<AccessibilityNodeInfo> mNodesWithReplacementActions;
48
49    @GuardedBy("mLock")
50    List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
51
52    @GuardedBy("mLock")
53    AccessibilityNodeInfo mNodeFromOriginalWindow;
54
55    // Keep track of whether or not we've been called back for a single node
56    @GuardedBy("mLock")
57    boolean mSingleNodeCallbackHappened;
58
59    // Keep track of whether or not we've been called back for multiple node
60    @GuardedBy("mLock")
61    boolean mMultiNodeCallbackHappened;
62
63    // We shouldn't get any more callbacks after we've called back the original service, but
64    // keep track to make sure we catch such strange things
65    @GuardedBy("mLock")
66    boolean mDone;
67
68    public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
69            IAccessibilityInteractionConnection connectionWithReplacementActions,
70            int interactionId, int interrogatingPid, long interrogatingTid) {
71        mServiceCallback = serviceCallback;
72        mConnectionWithReplacementActions = connectionWithReplacementActions;
73        mInteractionId = interactionId;
74
75        // Request the root node of the replacing window
76        final long identityToken = Binder.clearCallingIdentity();
77        try {
78            mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
79                    AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0,
80                    interrogatingPid, interrogatingTid, null, null);
81        } catch (RemoteException re) {
82            if (DEBUG) {
83                Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
84            }
85            // Pretend we already got a (null) list of replacement nodes
86            mMultiNodeCallbackHappened = true;
87        } finally {
88            Binder.restoreCallingIdentity(identityToken);
89        }
90    }
91
92    @Override
93    public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
94        boolean readyForCallback;
95        synchronized(mLock) {
96            if (interactionId == mInteractionId) {
97                mNodeFromOriginalWindow = info;
98            } else {
99                Slog.e(LOG_TAG, "Callback with unexpected interactionId");
100                return;
101            }
102
103            mSingleNodeCallbackHappened = true;
104            readyForCallback = mMultiNodeCallbackHappened;
105        }
106        if (readyForCallback) {
107            replaceInfoActionsAndCallService();
108        }
109    }
110
111    @Override
112    public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
113            int interactionId) {
114        boolean callbackForSingleNode;
115        boolean callbackForMultipleNodes;
116        synchronized(mLock) {
117            if (interactionId == mInteractionId) {
118                mNodesFromOriginalWindow = infos;
119            } else if (interactionId == mInteractionId + 1) {
120                mNodesWithReplacementActions = infos;
121            } else {
122                Slog.e(LOG_TAG, "Callback with unexpected interactionId");
123                return;
124            }
125            callbackForSingleNode = mSingleNodeCallbackHappened;
126            callbackForMultipleNodes = mMultiNodeCallbackHappened;
127            mMultiNodeCallbackHappened = true;
128        }
129        if (callbackForSingleNode) {
130            replaceInfoActionsAndCallService();
131        }
132        if (callbackForMultipleNodes) {
133            replaceInfosActionsAndCallService();
134        }
135    }
136
137    @Override
138    public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId)
139            throws RemoteException {
140        // There's no reason to use this class when performing actions. Do something reasonable.
141        mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId);
142    }
143
144    private void replaceInfoActionsAndCallService() {
145        final AccessibilityNodeInfo nodeToReturn;
146        synchronized (mLock) {
147            if (mDone) {
148                if (DEBUG) {
149                    Slog.e(LOG_TAG, "Extra callback");
150                }
151                return;
152            }
153            if (mNodeFromOriginalWindow != null) {
154                replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
155            }
156            recycleReplaceActionNodesLocked();
157            nodeToReturn = mNodeFromOriginalWindow;
158            mDone = true;
159        }
160        try {
161            mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
162        } catch (RemoteException re) {
163            if (DEBUG) {
164                Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
165            }
166        }
167    }
168
169    private void replaceInfosActionsAndCallService() {
170        final List<AccessibilityNodeInfo> nodesToReturn;
171        synchronized (mLock) {
172            if (mDone) {
173                if (DEBUG) {
174                    Slog.e(LOG_TAG, "Extra callback");
175                }
176                return;
177            }
178            if (mNodesFromOriginalWindow != null) {
179                for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
180                    replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
181                }
182            }
183            recycleReplaceActionNodesLocked();
184            nodesToReturn = (mNodesFromOriginalWindow == null)
185                    ? null : new ArrayList<>(mNodesFromOriginalWindow);
186            mDone = true;
187        }
188        try {
189            mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
190        } catch (RemoteException re) {
191            if (DEBUG) {
192                Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
193            }
194        }
195    }
196
197    @GuardedBy("mLock")
198    private void replaceActionsOnInfoLocked(AccessibilityNodeInfo info) {
199        info.removeAllActions();
200        info.setClickable(false);
201        info.setFocusable(false);
202        info.setContextClickable(false);
203        info.setScrollable(false);
204        info.setLongClickable(false);
205        info.setDismissable(false);
206        // We currently only replace actions for the root node
207        if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
208                && mNodesWithReplacementActions != null) {
209            // This list should always contain a single node with the root ID
210            for (int i = 0; i < mNodesWithReplacementActions.size(); i++) {
211                AccessibilityNodeInfo nodeWithReplacementActions =
212                        mNodesWithReplacementActions.get(i);
213                if (nodeWithReplacementActions.getSourceNodeId()
214                        == AccessibilityNodeInfo.ROOT_NODE_ID) {
215                    List<AccessibilityAction> actions = nodeWithReplacementActions.getActionList();
216                    if (actions != null) {
217                        for (int j = 0; j < actions.size(); j++) {
218                            info.addAction(actions.get(j));
219                        }
220                        // The PIP needs to be able to take accessibility focus
221                        info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
222                        info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
223                    }
224                    info.setClickable(nodeWithReplacementActions.isClickable());
225                    info.setFocusable(nodeWithReplacementActions.isFocusable());
226                    info.setContextClickable(nodeWithReplacementActions.isContextClickable());
227                    info.setScrollable(nodeWithReplacementActions.isScrollable());
228                    info.setLongClickable(nodeWithReplacementActions.isLongClickable());
229                    info.setDismissable(nodeWithReplacementActions.isDismissable());
230                }
231            }
232        }
233    }
234
235    @GuardedBy("mLock")
236    private void recycleReplaceActionNodesLocked() {
237        if (mNodesWithReplacementActions == null) return;
238        for (int i = mNodesWithReplacementActions.size() - 1; i >= 0; i--) {
239            AccessibilityNodeInfo nodeWithReplacementAction = mNodesWithReplacementActions.get(i);
240            nodeWithReplacementAction.recycle();
241        }
242        mNodesWithReplacementActions = null;
243    }
244}
245