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 
17 package com.android.server.accessibility;
18 
19 import android.os.Binder;
20 import android.os.RemoteException;
21 import android.util.Slog;
22 import android.view.accessibility.AccessibilityNodeInfo;
23 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
24 import android.view.accessibility.IAccessibilityInteractionConnection;
25 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
26 
27 import com.android.internal.annotations.GuardedBy;
28 
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 /**
33  * If we are stripping and/or replacing the actions from a window, we need to intercept the
34  * nodes heading back to the service and swap out the actions.
35  */
36 public class ActionReplacingCallback extends IAccessibilityInteractionConnectionCallback.Stub {
37     private static final boolean DEBUG = false;
38     private static final String LOG_TAG = "ActionReplacingCallback";
39 
40     private final IAccessibilityInteractionConnectionCallback mServiceCallback;
41     private final IAccessibilityInteractionConnection mConnectionWithReplacementActions;
42     private final int mInteractionId;
43     private final int mNodeWithReplacementActionsInteractionId;
44     private final Object mLock = new Object();
45 
46     @GuardedBy("mLock")
47     private boolean mReplacementNodeIsReadyOrFailed;
48 
49     @GuardedBy("mLock")
50     AccessibilityNodeInfo mNodeWithReplacementActions;
51 
52     @GuardedBy("mLock")
53     List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
54 
55     @GuardedBy("mLock")
56     boolean mSetFindNodeFromOriginalWindowCalled = false;
57 
58     @GuardedBy("mLock")
59     AccessibilityNodeInfo mNodeFromOriginalWindow;
60 
61     @GuardedBy("mLock")
62     boolean mSetFindNodesFromOriginalWindowCalled = false;
63 
64 
65     @GuardedBy("mLock")
66     List<AccessibilityNodeInfo> mPrefetchedNodesFromOriginalWindow;
67 
68     @GuardedBy("mLock")
69     boolean mSetPrefetchFromOriginalWindowCalled = false;
70 
71 
ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback, IAccessibilityInteractionConnection connectionWithReplacementActions, int interactionId, int interrogatingPid, long interrogatingTid)72     public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
73             IAccessibilityInteractionConnection connectionWithReplacementActions,
74             int interactionId, int interrogatingPid, long interrogatingTid) {
75         mServiceCallback = serviceCallback;
76         mConnectionWithReplacementActions = connectionWithReplacementActions;
77         mInteractionId = interactionId;
78         mNodeWithReplacementActionsInteractionId = interactionId + 1;
79 
80         // Request the root node of the replacing window
81         final long identityToken = Binder.clearCallingIdentity();
82         try {
83             mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
84                     AccessibilityNodeInfo.ROOT_NODE_ID, null,
85                     mNodeWithReplacementActionsInteractionId, this, 0,
86                     interrogatingPid, interrogatingTid, null, null, null);
87         } catch (RemoteException re) {
88             if (DEBUG) {
89                 Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
90             }
91             mReplacementNodeIsReadyOrFailed = true;
92         } finally {
93             Binder.restoreCallingIdentity(identityToken);
94         }
95     }
96 
97     @Override
setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId)98     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
99         synchronized (mLock) {
100             if (interactionId == mInteractionId) {
101                 mNodeFromOriginalWindow = info;
102                 mSetFindNodeFromOriginalWindowCalled = true;
103             } else if (interactionId == mNodeWithReplacementActionsInteractionId) {
104                 mNodeWithReplacementActions = info;
105                 mReplacementNodeIsReadyOrFailed = true;
106             } else {
107                 Slog.e(LOG_TAG, "Callback with unexpected interactionId");
108                 return;
109             }
110         }
111         replaceInfoActionsAndCallServiceIfReady();
112     }
113 
114     @Override
setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId)115     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
116             int interactionId) {
117         synchronized (mLock) {
118             if (interactionId == mInteractionId) {
119                 mNodesFromOriginalWindow = infos;
120                 mSetFindNodesFromOriginalWindowCalled = true;
121             } else if (interactionId == mNodeWithReplacementActionsInteractionId) {
122                 setNodeWithReplacementActionsFromList(infos);
123                 mReplacementNodeIsReadyOrFailed = true;
124             } else {
125                 Slog.e(LOG_TAG, "Callback with unexpected interactionId");
126                 return;
127             }
128         }
129         replaceInfoActionsAndCallServiceIfReady();
130     }
131 
132     @Override
setPrefetchAccessibilityNodeInfoResult(List<AccessibilityNodeInfo> infos, int interactionId)133     public void setPrefetchAccessibilityNodeInfoResult(List<AccessibilityNodeInfo> infos,
134                                                        int interactionId)
135             throws RemoteException {
136         synchronized (mLock) {
137             if (interactionId == mInteractionId) {
138                 mPrefetchedNodesFromOriginalWindow = infos;
139                 mSetPrefetchFromOriginalWindowCalled = true;
140             }  else {
141                 Slog.e(LOG_TAG, "Callback with unexpected interactionId");
142                 return;
143             }
144         }
145         replaceInfoActionsAndCallServiceIfReady();
146     }
147 
replaceInfoActionsAndCallServiceIfReady()148     private void replaceInfoActionsAndCallServiceIfReady() {
149         replaceInfoActionsAndCallService();
150         replaceInfosActionsAndCallService();
151         replacePrefetchInfosActionsAndCallService();
152     }
153 
setNodeWithReplacementActionsFromList(List<AccessibilityNodeInfo> infos)154     private void setNodeWithReplacementActionsFromList(List<AccessibilityNodeInfo> infos) {
155         for (int i = 0; i < infos.size(); i++) {
156             AccessibilityNodeInfo info = infos.get(i);
157             if (info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID) {
158                 mNodeWithReplacementActions = info;
159             }
160         }
161     }
162 
163     @Override
setPerformAccessibilityActionResult(boolean succeeded, int interactionId)164     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId)
165             throws RemoteException {
166         // There's no reason to use this class when performing actions. Do something reasonable.
167         mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId);
168     }
169 
170     @Override
sendTakeScreenshotOfWindowError(int errorCode, int interactionId)171     public void sendTakeScreenshotOfWindowError(int errorCode, int interactionId)
172             throws RemoteException {
173         mServiceCallback.sendTakeScreenshotOfWindowError(errorCode, interactionId);
174     }
175 
replaceInfoActionsAndCallService()176     private void replaceInfoActionsAndCallService() {
177         final AccessibilityNodeInfo nodeToReturn;
178         boolean doCallback = false;
179         synchronized (mLock) {
180             doCallback = mReplacementNodeIsReadyOrFailed
181                     && mSetFindNodeFromOriginalWindowCalled;
182             if (doCallback && mNodeFromOriginalWindow != null) {
183                 replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
184                 mSetFindNodeFromOriginalWindowCalled = false;
185             }
186             nodeToReturn = mNodeFromOriginalWindow;
187         }
188         if (doCallback) {
189             try {
190                 mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
191             } catch (RemoteException re) {
192                 if (DEBUG) {
193                     Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
194                 }
195             }
196         }
197     }
198 
replaceInfosActionsAndCallService()199     private void replaceInfosActionsAndCallService() {
200         List<AccessibilityNodeInfo> nodesToReturn = null;
201         boolean doCallback = false;
202         synchronized (mLock) {
203             doCallback = mReplacementNodeIsReadyOrFailed
204                     && mSetFindNodesFromOriginalWindowCalled;
205             if (doCallback) {
206                 nodesToReturn = replaceActionsLocked(mNodesFromOriginalWindow);
207                 mSetFindNodesFromOriginalWindowCalled = false;
208             }
209         }
210         if (doCallback) {
211             try {
212                 mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
213             } catch (RemoteException re) {
214                 if (DEBUG) {
215                     Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
216                 }
217             }
218         }
219     }
220 
replacePrefetchInfosActionsAndCallService()221     private void replacePrefetchInfosActionsAndCallService() {
222         List<AccessibilityNodeInfo> nodesToReturn = null;
223         boolean doCallback = false;
224         synchronized (mLock) {
225             doCallback = mReplacementNodeIsReadyOrFailed
226                     && mSetPrefetchFromOriginalWindowCalled;
227             if (doCallback) {
228                 nodesToReturn = replaceActionsLocked(mPrefetchedNodesFromOriginalWindow);
229                 mSetPrefetchFromOriginalWindowCalled = false;
230             }
231         }
232         if (doCallback) {
233             try {
234                 mServiceCallback.setPrefetchAccessibilityNodeInfoResult(
235                         nodesToReturn, mInteractionId);
236             } catch (RemoteException re) {
237                 if (DEBUG) {
238                     Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
239                 }
240             }
241         }
242     }
243 
244     @GuardedBy("mLock")
replaceActionsLocked(List<AccessibilityNodeInfo> infos)245     private List<AccessibilityNodeInfo> replaceActionsLocked(List<AccessibilityNodeInfo> infos) {
246         if (infos != null) {
247             for (int i = 0; i < infos.size(); i++) {
248                 replaceActionsOnInfoLocked(infos.get(i));
249             }
250         }
251         return (infos == null)
252                 ? null : new ArrayList<>(infos);
253     }
254 
255     @GuardedBy("mLock")
replaceActionsOnInfoLocked(AccessibilityNodeInfo info)256     private void replaceActionsOnInfoLocked(AccessibilityNodeInfo info) {
257         info.removeAllActions();
258         info.setClickable(false);
259         info.setFocusable(false);
260         info.setContextClickable(false);
261         info.setScrollable(false);
262         info.setLongClickable(false);
263         info.setDismissable(false);
264         // We currently only replace actions for the root node
265         if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
266                 && mNodeWithReplacementActions != null) {
267             List<AccessibilityAction> actions = mNodeWithReplacementActions.getActionList();
268             if (actions != null) {
269                 for (int j = 0; j < actions.size(); j++) {
270                     info.addAction(actions.get(j));
271                 }
272                 // The PIP needs to be able to take accessibility focus
273                 info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
274                 info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
275             }
276             info.setClickable(mNodeWithReplacementActions.isClickable());
277             info.setFocusable(mNodeWithReplacementActions.isFocusable());
278             info.setContextClickable(mNodeWithReplacementActions.isContextClickable());
279             info.setScrollable(mNodeWithReplacementActions.isScrollable());
280             info.setLongClickable(mNodeWithReplacementActions.isLongClickable());
281             info.setDismissable(mNodeWithReplacementActions.isDismissable());
282         }
283     }
284 }
285