1 /*
2  ** Copyright 2011, 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 android.view.accessibility;
18 
19 import android.accessibilityservice.IAccessibilityServiceConnection;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.os.Binder;
22 import android.os.Build;
23 import android.os.Bundle;
24 import android.os.Message;
25 import android.os.Process;
26 import android.os.RemoteException;
27 import android.os.SystemClock;
28 import android.util.Log;
29 import android.util.LongSparseArray;
30 import android.util.SparseArray;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.util.ArrayUtils;
34 
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.HashSet;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Queue;
41 import java.util.concurrent.atomic.AtomicInteger;
42 
43 /**
44  * This class is a singleton that performs accessibility interaction
45  * which is it queries remote view hierarchies about snapshots of their
46  * views as well requests from these hierarchies to perform certain
47  * actions on their views.
48  *
49  * Rationale: The content retrieval APIs are synchronous from a client's
50  *     perspective but internally they are asynchronous. The client thread
51  *     calls into the system requesting an action and providing a callback
52  *     to receive the result after which it waits up to a timeout for that
53  *     result. The system enforces security and the delegates the request
54  *     to a given view hierarchy where a message is posted (from a binder
55  *     thread) describing what to be performed by the main UI thread the
56  *     result of which it delivered via the mentioned callback. However,
57  *     the blocked client thread and the main UI thread of the target view
58  *     hierarchy can be the same thread, for example an accessibility service
59  *     and an activity run in the same process, thus they are executed on the
60  *     same main thread. In such a case the retrieval will fail since the UI
61  *     thread that has to process the message describing the work to be done
62  *     is blocked waiting for a result is has to compute! To avoid this scenario
63  *     when making a call the client also passes its process and thread ids so
64  *     the accessed view hierarchy can detect if the client making the request
65  *     is running in its main UI thread. In such a case the view hierarchy,
66  *     specifically the binder thread performing the IPC to it, does not post a
67  *     message to be run on the UI thread but passes it to the singleton
68  *     interaction client through which all interactions occur and the latter is
69  *     responsible to execute the message before starting to wait for the
70  *     asynchronous result delivered via the callback. In this case the expected
71  *     result is already received so no waiting is performed.
72  *
73  * @hide
74  */
75 public final class AccessibilityInteractionClient
76         extends IAccessibilityInteractionConnectionCallback.Stub {
77 
78     public static final int NO_ID = -1;
79 
80     private static final String LOG_TAG = "AccessibilityInteractionClient";
81 
82     private static final boolean DEBUG = false;
83 
84     private static final boolean CHECK_INTEGRITY = true;
85 
86     private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
87 
88     private static final Object sStaticLock = new Object();
89 
90     private static final LongSparseArray<AccessibilityInteractionClient> sClients =
91         new LongSparseArray<>();
92 
93     private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
94             new SparseArray<>();
95 
96     private static AccessibilityCache sAccessibilityCache =
97             new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher());
98 
99     private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
100 
101     private final Object mInstanceLock = new Object();
102 
103     private volatile int mInteractionId = -1;
104 
105     private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
106 
107     private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult;
108 
109     private boolean mPerformAccessibilityActionResult;
110 
111     private Message mSameThreadMessage;
112 
113     /**
114      * @return The client for the current thread.
115      */
116     @UnsupportedAppUsage
getInstance()117     public static AccessibilityInteractionClient getInstance() {
118         final long threadId = Thread.currentThread().getId();
119         return getInstanceForThread(threadId);
120     }
121 
122     /**
123      * <strong>Note:</strong> We keep one instance per interrogating thread since
124      * the instance contains state which can lead to undesired thread interleavings.
125      * We do not have a thread local variable since other threads should be able to
126      * look up the correct client knowing a thread id. See ViewRootImpl for details.
127      *
128      * @return The client for a given <code>threadId</code>.
129      */
getInstanceForThread(long threadId)130     public static AccessibilityInteractionClient getInstanceForThread(long threadId) {
131         synchronized (sStaticLock) {
132             AccessibilityInteractionClient client = sClients.get(threadId);
133             if (client == null) {
134                 client = new AccessibilityInteractionClient();
135                 sClients.put(threadId, client);
136             }
137             return client;
138         }
139     }
140 
141     /**
142      * Gets a cached accessibility service connection.
143      *
144      * @param connectionId The connection id.
145      * @return The cached connection if such.
146      */
getConnection(int connectionId)147     public static IAccessibilityServiceConnection getConnection(int connectionId) {
148         synchronized (sConnectionCache) {
149             return sConnectionCache.get(connectionId);
150         }
151     }
152 
153     /**
154      * Adds a cached accessibility service connection.
155      *
156      * @param connectionId The connection id.
157      * @param connection The connection.
158      */
addConnection(int connectionId, IAccessibilityServiceConnection connection)159     public static void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
160         synchronized (sConnectionCache) {
161             sConnectionCache.put(connectionId, connection);
162         }
163     }
164 
165     /**
166      * Removes a cached accessibility service connection.
167      *
168      * @param connectionId The connection id.
169      */
removeConnection(int connectionId)170     public static void removeConnection(int connectionId) {
171         synchronized (sConnectionCache) {
172             sConnectionCache.remove(connectionId);
173         }
174     }
175 
176     /**
177      * This method is only for testing. Replacing the cache is a generally terrible idea, but
178      * tests need to be able to verify this class's interactions with the cache
179      */
180     @VisibleForTesting
setCache(AccessibilityCache cache)181     public static void setCache(AccessibilityCache cache) {
182         sAccessibilityCache = cache;
183     }
184 
AccessibilityInteractionClient()185     private AccessibilityInteractionClient() {
186         /* reducing constructor visibility */
187     }
188 
189     /**
190      * Sets the message to be processed if the interacted view hierarchy
191      * and the interacting client are running in the same thread.
192      *
193      * @param message The message.
194      */
195     @UnsupportedAppUsage
setSameThreadMessage(Message message)196     public void setSameThreadMessage(Message message) {
197         synchronized (mInstanceLock) {
198             mSameThreadMessage = message;
199             mInstanceLock.notifyAll();
200         }
201     }
202 
203     /**
204      * Gets the root {@link AccessibilityNodeInfo} in the currently active window.
205      *
206      * @param connectionId The id of a connection for interacting with the system.
207      * @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
208      */
getRootInActiveWindow(int connectionId)209     public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
210         return findAccessibilityNodeInfoByAccessibilityId(connectionId,
211                 AccessibilityWindowInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
212                 false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
213     }
214 
215     /**
216      * Gets the info for a window.
217      *
218      * @param connectionId The id of a connection for interacting with the system.
219      * @param accessibilityWindowId A unique window id. Use
220      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
221      *     to query the currently active window.
222      * @return The {@link AccessibilityWindowInfo}.
223      */
getWindow(int connectionId, int accessibilityWindowId)224     public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId) {
225         try {
226             IAccessibilityServiceConnection connection = getConnection(connectionId);
227             if (connection != null) {
228                 AccessibilityWindowInfo window = sAccessibilityCache.getWindow(
229                         accessibilityWindowId);
230                 if (window != null) {
231                     if (DEBUG) {
232                         Log.i(LOG_TAG, "Window cache hit");
233                     }
234                     return window;
235                 }
236                 if (DEBUG) {
237                     Log.i(LOG_TAG, "Window cache miss");
238                 }
239                 final long identityToken = Binder.clearCallingIdentity();
240                 try {
241                     window = connection.getWindow(accessibilityWindowId);
242                 } finally {
243                     Binder.restoreCallingIdentity(identityToken);
244                 }
245                 if (window != null) {
246                     sAccessibilityCache.addWindow(window);
247                     return window;
248                 }
249             } else {
250                 if (DEBUG) {
251                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
252                 }
253             }
254         } catch (RemoteException re) {
255             Log.e(LOG_TAG, "Error while calling remote getWindow", re);
256         }
257         return null;
258     }
259 
260     /**
261      * Gets the info for all windows.
262      *
263      * @param connectionId The id of a connection for interacting with the system.
264      * @return The {@link AccessibilityWindowInfo} list.
265      */
getWindows(int connectionId)266     public List<AccessibilityWindowInfo> getWindows(int connectionId) {
267         try {
268             IAccessibilityServiceConnection connection = getConnection(connectionId);
269             if (connection != null) {
270                 List<AccessibilityWindowInfo> windows = sAccessibilityCache.getWindows();
271                 if (windows != null) {
272                     if (DEBUG) {
273                         Log.i(LOG_TAG, "Windows cache hit");
274                     }
275                     return windows;
276                 }
277                 if (DEBUG) {
278                     Log.i(LOG_TAG, "Windows cache miss");
279                 }
280                 final long identityToken = Binder.clearCallingIdentity();
281                 try {
282                     windows = connection.getWindows();
283                 } finally {
284                     Binder.restoreCallingIdentity(identityToken);
285                 }
286                 if (windows != null) {
287                     sAccessibilityCache.setWindows(windows);
288                     return windows;
289                 }
290             } else {
291                 if (DEBUG) {
292                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
293                 }
294             }
295         } catch (RemoteException re) {
296             Log.e(LOG_TAG, "Error while calling remote getWindows", re);
297         }
298         return Collections.emptyList();
299     }
300 
301     /**
302      * Finds an {@link AccessibilityNodeInfo} by accessibility id.
303      *
304      * @param connectionId The id of a connection for interacting with the system.
305      * @param accessibilityWindowId A unique window id. Use
306      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
307      *     to query the currently active window.
308      * @param accessibilityNodeId A unique view id or virtual descendant id from
309      *     where to start the search. Use
310      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
311      *     to start from the root.
312      * @param bypassCache Whether to bypass the cache while looking for the node.
313      * @param prefetchFlags flags to guide prefetching.
314      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
315      */
findAccessibilityNodeInfoByAccessibilityId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, int prefetchFlags, Bundle arguments)316     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
317             int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
318             int prefetchFlags, Bundle arguments) {
319         if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0
320                 && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) {
321             throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS"
322                 + " requires FLAG_PREFETCH_PREDECESSORS");
323         }
324         try {
325             IAccessibilityServiceConnection connection = getConnection(connectionId);
326             if (connection != null) {
327                 if (!bypassCache) {
328                     AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode(
329                             accessibilityWindowId, accessibilityNodeId);
330                     if (cachedInfo != null) {
331                         if (DEBUG) {
332                             Log.i(LOG_TAG, "Node cache hit for "
333                                     + idToString(accessibilityWindowId, accessibilityNodeId));
334                         }
335                         return cachedInfo;
336                     }
337                     if (DEBUG) {
338                         Log.i(LOG_TAG, "Node cache miss for "
339                                 + idToString(accessibilityWindowId, accessibilityNodeId));
340                     }
341                 }
342                 final int interactionId = mInteractionIdCounter.getAndIncrement();
343                 final long identityToken = Binder.clearCallingIdentity();
344                 final String[] packageNames;
345                 try {
346                     packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(
347                             accessibilityWindowId, accessibilityNodeId, interactionId, this,
348                             prefetchFlags, Thread.currentThread().getId(), arguments);
349                 } finally {
350                     Binder.restoreCallingIdentity(identityToken);
351                 }
352                 if (packageNames != null) {
353                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
354                             interactionId);
355                     finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
356                             bypassCache, packageNames);
357                     if (infos != null && !infos.isEmpty()) {
358                         for (int i = 1; i < infos.size(); i++) {
359                             infos.get(i).recycle();
360                         }
361                         return infos.get(0);
362                     }
363                 }
364             } else {
365                 if (DEBUG) {
366                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
367                 }
368             }
369         } catch (RemoteException re) {
370             Log.e(LOG_TAG, "Error while calling remote"
371                     + " findAccessibilityNodeInfoByAccessibilityId", re);
372         }
373         return null;
374     }
375 
idToString(int accessibilityWindowId, long accessibilityNodeId)376     private static String idToString(int accessibilityWindowId, long accessibilityNodeId) {
377         return accessibilityWindowId + "/"
378                 + AccessibilityNodeInfo.idToString(accessibilityNodeId);
379     }
380 
381     /**
382      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
383      * the window whose id is specified and starts from the node whose accessibility
384      * id is specified.
385      *
386      * @param connectionId The id of a connection for interacting with the system.
387      * @param accessibilityWindowId A unique window id. Use
388      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
389      *     to query the currently active window.
390      * @param accessibilityNodeId A unique view id or virtual descendant id from
391      *     where to start the search. Use
392      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
393      *     to start from the root.
394      * @param viewId The fully qualified resource name of the view id to find.
395      * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
396      */
findAccessibilityNodeInfosByViewId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String viewId)397     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
398             int accessibilityWindowId, long accessibilityNodeId, String viewId) {
399         try {
400             IAccessibilityServiceConnection connection = getConnection(connectionId);
401             if (connection != null) {
402                 final int interactionId = mInteractionIdCounter.getAndIncrement();
403                 final long identityToken = Binder.clearCallingIdentity();
404                 final String[] packageNames;
405                 try {
406                     packageNames = connection.findAccessibilityNodeInfosByViewId(
407                             accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
408                             Thread.currentThread().getId());
409                 } finally {
410                     Binder.restoreCallingIdentity(identityToken);
411                 }
412 
413                 if (packageNames != null) {
414                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
415                             interactionId);
416                     if (infos != null) {
417                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
418                                 false, packageNames);
419                         return infos;
420                     }
421                 }
422             } else {
423                 if (DEBUG) {
424                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
425                 }
426             }
427         } catch (RemoteException re) {
428             Log.w(LOG_TAG, "Error while calling remote"
429                     + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
430         }
431         return Collections.emptyList();
432     }
433 
434     /**
435      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
436      * insensitive containment. The search is performed in the window whose
437      * id is specified and starts from the node whose accessibility id is
438      * specified.
439      *
440      * @param connectionId The id of a connection for interacting with the system.
441      * @param accessibilityWindowId A unique window id. Use
442      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
443      *     to query the currently active window.
444      * @param accessibilityNodeId A unique view id or virtual descendant id from
445      *     where to start the search. Use
446      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
447      *     to start from the root.
448      * @param text The searched text.
449      * @return A list of found {@link AccessibilityNodeInfo}s.
450      */
findAccessibilityNodeInfosByText(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String text)451     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
452             int accessibilityWindowId, long accessibilityNodeId, String text) {
453         try {
454             IAccessibilityServiceConnection connection = getConnection(connectionId);
455             if (connection != null) {
456                 final int interactionId = mInteractionIdCounter.getAndIncrement();
457                 final long identityToken = Binder.clearCallingIdentity();
458                 final String[] packageNames;
459                 try {
460                     packageNames = connection.findAccessibilityNodeInfosByText(
461                             accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
462                             Thread.currentThread().getId());
463                 } finally {
464                     Binder.restoreCallingIdentity(identityToken);
465                 }
466 
467                 if (packageNames != null) {
468                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
469                             interactionId);
470                     if (infos != null) {
471                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
472                                 false, packageNames);
473                         return infos;
474                     }
475                 }
476             } else {
477                 if (DEBUG) {
478                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
479                 }
480             }
481         } catch (RemoteException re) {
482             Log.w(LOG_TAG, "Error while calling remote"
483                     + " findAccessibilityNodeInfosByViewText", re);
484         }
485         return Collections.emptyList();
486     }
487 
488     /**
489      * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
490      * specified focus type. The search is performed in the window whose id is specified
491      * and starts from the node whose accessibility id is specified.
492      *
493      * @param connectionId The id of a connection for interacting with the system.
494      * @param accessibilityWindowId A unique window id. Use
495      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
496      *     to query the currently active window.
497      * @param accessibilityNodeId A unique view id or virtual descendant id from
498      *     where to start the search. Use
499      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
500      *     to start from the root.
501      * @param focusType The focus type.
502      * @return The accessibility focused {@link AccessibilityNodeInfo}.
503      */
findFocus(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int focusType)504     public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
505             long accessibilityNodeId, int focusType) {
506         try {
507             IAccessibilityServiceConnection connection = getConnection(connectionId);
508             if (connection != null) {
509                 final int interactionId = mInteractionIdCounter.getAndIncrement();
510                 final long identityToken = Binder.clearCallingIdentity();
511                 final String[] packageNames;
512                 try {
513                     packageNames = connection.findFocus(accessibilityWindowId,
514                             accessibilityNodeId, focusType, interactionId, this,
515                             Thread.currentThread().getId());
516                 } finally {
517                     Binder.restoreCallingIdentity(identityToken);
518                 }
519 
520                 if (packageNames != null) {
521                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
522                             interactionId);
523                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
524                     return info;
525                 }
526             } else {
527                 if (DEBUG) {
528                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
529                 }
530             }
531         } catch (RemoteException re) {
532             Log.w(LOG_TAG, "Error while calling remote findFocus", re);
533         }
534         return null;
535     }
536 
537     /**
538      * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
539      * The search is performed in the window whose id is specified and starts from the
540      * node whose accessibility id is specified.
541      *
542      * @param connectionId The id of a connection for interacting with the system.
543      * @param accessibilityWindowId A unique window id. Use
544      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
545      *     to query the currently active window.
546      * @param accessibilityNodeId A unique view id or virtual descendant id from
547      *     where to start the search. Use
548      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
549      *     to start from the root.
550      * @param direction The direction in which to search for focusable.
551      * @return The accessibility focused {@link AccessibilityNodeInfo}.
552      */
focusSearch(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int direction)553     public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
554             long accessibilityNodeId, int direction) {
555         try {
556             IAccessibilityServiceConnection connection = getConnection(connectionId);
557             if (connection != null) {
558                 final int interactionId = mInteractionIdCounter.getAndIncrement();
559                 final long identityToken = Binder.clearCallingIdentity();
560                 final String[] packageNames;
561                 try {
562                     packageNames = connection.focusSearch(accessibilityWindowId,
563                             accessibilityNodeId, direction, interactionId, this,
564                             Thread.currentThread().getId());
565                 } finally {
566                     Binder.restoreCallingIdentity(identityToken);
567                 }
568 
569                 if (packageNames != null) {
570                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
571                             interactionId);
572                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
573                     return info;
574                 }
575             } else {
576                 if (DEBUG) {
577                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
578                 }
579             }
580         } catch (RemoteException re) {
581             Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
582         }
583         return null;
584     }
585 
586     /**
587      * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
588      *
589      * @param connectionId The id of a connection for interacting with the system.
590      * @param accessibilityWindowId A unique window id. Use
591      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
592      *     to query the currently active window.
593      * @param accessibilityNodeId A unique view id or virtual descendant id from
594      *     where to start the search. Use
595      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
596      *     to start from the root.
597      * @param action The action to perform.
598      * @param arguments Optional action arguments.
599      * @return Whether the action was performed.
600      */
performAccessibilityAction(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int action, Bundle arguments)601     public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
602             long accessibilityNodeId, int action, Bundle arguments) {
603         try {
604             IAccessibilityServiceConnection connection = getConnection(connectionId);
605             if (connection != null) {
606                 final int interactionId = mInteractionIdCounter.getAndIncrement();
607                 final long identityToken = Binder.clearCallingIdentity();
608                 final boolean success;
609                 try {
610                     success = connection.performAccessibilityAction(
611                             accessibilityWindowId, accessibilityNodeId, action, arguments,
612                             interactionId, this, Thread.currentThread().getId());
613                 } finally {
614                     Binder.restoreCallingIdentity(identityToken);
615                 }
616 
617                 if (success) {
618                     return getPerformAccessibilityActionResultAndClear(interactionId);
619                 }
620             } else {
621                 if (DEBUG) {
622                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
623                 }
624             }
625         } catch (RemoteException re) {
626             Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
627         }
628         return false;
629     }
630 
631     @UnsupportedAppUsage
clearCache()632     public void clearCache() {
633         sAccessibilityCache.clear();
634     }
635 
onAccessibilityEvent(AccessibilityEvent event)636     public void onAccessibilityEvent(AccessibilityEvent event) {
637         sAccessibilityCache.onAccessibilityEvent(event);
638     }
639 
640     /**
641      * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
642      *
643      * @param interactionId The interaction id to match the result with the request.
644      * @return The result {@link AccessibilityNodeInfo}.
645      */
getFindAccessibilityNodeInfoResultAndClear(int interactionId)646     private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
647         synchronized (mInstanceLock) {
648             final boolean success = waitForResultTimedLocked(interactionId);
649             AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
650             clearResultLocked();
651             return result;
652         }
653     }
654 
655     /**
656      * {@inheritDoc}
657      */
setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId)658     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
659                 int interactionId) {
660         synchronized (mInstanceLock) {
661             if (interactionId > mInteractionId) {
662                 mFindAccessibilityNodeInfoResult = info;
663                 mInteractionId = interactionId;
664             }
665             mInstanceLock.notifyAll();
666         }
667     }
668 
669     /**
670      * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
671      *
672      * @param interactionId The interaction id to match the result with the request.
673      * @return The result {@link AccessibilityNodeInfo}s.
674      */
getFindAccessibilityNodeInfosResultAndClear( int interactionId)675     private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
676                 int interactionId) {
677         synchronized (mInstanceLock) {
678             final boolean success = waitForResultTimedLocked(interactionId);
679             final List<AccessibilityNodeInfo> result;
680             if (success) {
681                 result = mFindAccessibilityNodeInfosResult;
682             } else {
683                 result = Collections.emptyList();
684             }
685             clearResultLocked();
686             if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
687                 checkFindAccessibilityNodeInfoResultIntegrity(result);
688             }
689             return result;
690         }
691     }
692 
693     /**
694      * {@inheritDoc}
695      */
setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId)696     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
697                 int interactionId) {
698         synchronized (mInstanceLock) {
699             if (interactionId > mInteractionId) {
700                 if (infos != null) {
701                     // If the call is not an IPC, i.e. it is made from the same process, we need to
702                     // instantiate new result list to avoid passing internal instances to clients.
703                     final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
704                     if (!isIpcCall) {
705                         mFindAccessibilityNodeInfosResult = new ArrayList<>(infos);
706                     } else {
707                         mFindAccessibilityNodeInfosResult = infos;
708                     }
709                 } else {
710                     mFindAccessibilityNodeInfosResult = Collections.emptyList();
711                 }
712                 mInteractionId = interactionId;
713             }
714             mInstanceLock.notifyAll();
715         }
716     }
717 
718     /**
719      * Gets the result of a request to perform an accessibility action.
720      *
721      * @param interactionId The interaction id to match the result with the request.
722      * @return Whether the action was performed.
723      */
getPerformAccessibilityActionResultAndClear(int interactionId)724     private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
725         synchronized (mInstanceLock) {
726             final boolean success = waitForResultTimedLocked(interactionId);
727             final boolean result = success ? mPerformAccessibilityActionResult : false;
728             clearResultLocked();
729             return result;
730         }
731     }
732 
733     /**
734      * {@inheritDoc}
735      */
setPerformAccessibilityActionResult(boolean succeeded, int interactionId)736     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
737         synchronized (mInstanceLock) {
738             if (interactionId > mInteractionId) {
739                 mPerformAccessibilityActionResult = succeeded;
740                 mInteractionId = interactionId;
741             }
742             mInstanceLock.notifyAll();
743         }
744     }
745 
746     /**
747      * Clears the result state.
748      */
clearResultLocked()749     private void clearResultLocked() {
750         mInteractionId = -1;
751         mFindAccessibilityNodeInfoResult = null;
752         mFindAccessibilityNodeInfosResult = null;
753         mPerformAccessibilityActionResult = false;
754     }
755 
756     /**
757      * Waits up to a given bound for a result of a request and returns it.
758      *
759      * @param interactionId The interaction id to match the result with the request.
760      * @return Whether the result was received.
761      */
waitForResultTimedLocked(int interactionId)762     private boolean waitForResultTimedLocked(int interactionId) {
763         long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
764         final long startTimeMillis = SystemClock.uptimeMillis();
765         while (true) {
766             try {
767                 Message sameProcessMessage = getSameProcessMessageAndClear();
768                 if (sameProcessMessage != null) {
769                     sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
770                 }
771 
772                 if (mInteractionId == interactionId) {
773                     return true;
774                 }
775                 if (mInteractionId > interactionId) {
776                     return false;
777                 }
778                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
779                 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
780                 if (waitTimeMillis <= 0) {
781                     return false;
782                 }
783                 mInstanceLock.wait(waitTimeMillis);
784             } catch (InterruptedException ie) {
785                 /* ignore */
786             }
787         }
788     }
789 
790     /**
791      * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
792      *
793      * @param info The info.
794      * @param connectionId The id of the connection to the system.
795      * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if
796      *                    this value is {@code false}
797      * @param packageNames The valid package names a node can come from.
798      */
finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, boolean bypassCache, String[] packageNames)799     private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
800             int connectionId, boolean bypassCache, String[] packageNames) {
801         if (info != null) {
802             info.setConnectionId(connectionId);
803             // Empty array means any package name is Okay
804             if (!ArrayUtils.isEmpty(packageNames)) {
805                 CharSequence packageName = info.getPackageName();
806                 if (packageName == null
807                         || !ArrayUtils.contains(packageNames, packageName.toString())) {
808                     // If the node package not one of the valid ones, pick the top one - this
809                     // is one of the packages running in the introspected UID.
810                     info.setPackageName(packageNames[0]);
811                 }
812             }
813             info.setSealed(true);
814             if (!bypassCache) {
815                 sAccessibilityCache.add(info);
816             }
817         }
818     }
819 
820     /**
821      * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
822      *
823      * @param infos The {@link AccessibilityNodeInfo}s.
824      * @param connectionId The id of the connection to the system.
825      * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if
826      *                    this value is {@code false}
827      * @param packageNames The valid package names a node can come from.
828      */
finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, int connectionId, boolean bypassCache, String[] packageNames)829     private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
830             int connectionId, boolean bypassCache, String[] packageNames) {
831         if (infos != null) {
832             final int infosCount = infos.size();
833             for (int i = 0; i < infosCount; i++) {
834                 AccessibilityNodeInfo info = infos.get(i);
835                 finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
836                         bypassCache, packageNames);
837             }
838         }
839     }
840 
841     /**
842      * Gets the message stored if the interacted and interacting
843      * threads are the same.
844      *
845      * @return The message.
846      */
getSameProcessMessageAndClear()847     private Message getSameProcessMessageAndClear() {
848         synchronized (mInstanceLock) {
849             Message result = mSameThreadMessage;
850             mSameThreadMessage = null;
851             return result;
852         }
853     }
854 
855     /**
856      * Checks whether the infos are a fully connected tree with no duplicates.
857      *
858      * @param infos The result list to check.
859      */
checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos)860     private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
861         if (infos.size() == 0) {
862             return;
863         }
864         // Find the root node.
865         AccessibilityNodeInfo root = infos.get(0);
866         final int infoCount = infos.size();
867         for (int i = 1; i < infoCount; i++) {
868             for (int j = i; j < infoCount; j++) {
869                 AccessibilityNodeInfo candidate = infos.get(j);
870                 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
871                     root = candidate;
872                     break;
873                 }
874             }
875         }
876         if (root == null) {
877             Log.e(LOG_TAG, "No root.");
878         }
879         // Check for duplicates.
880         HashSet<AccessibilityNodeInfo> seen = new HashSet<>();
881         Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
882         fringe.add(root);
883         while (!fringe.isEmpty()) {
884             AccessibilityNodeInfo current = fringe.poll();
885             if (!seen.add(current)) {
886                 Log.e(LOG_TAG, "Duplicate node.");
887                 return;
888             }
889             final int childCount = current.getChildCount();
890             for (int i = 0; i < childCount; i++) {
891                 final long childId = current.getChildId(i);
892                 for (int j = 0; j < infoCount; j++) {
893                     AccessibilityNodeInfo child = infos.get(j);
894                     if (child.getSourceNodeId() == childId) {
895                         fringe.add(child);
896                     }
897                 }
898             }
899         }
900         final int disconnectedCount = infos.size() - seen.size();
901         if (disconnectedCount > 0) {
902             Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
903         }
904     }
905 }
906