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