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