• 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();
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                 AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
162                 false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
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                     final int windowCount = windows.size();
232                     for (int i = 0; i < windowCount; i++) {
233                         AccessibilityWindowInfo window = windows.get(i);
234                         sAccessibilityCache.addWindow(window);
235                     }
236                     return windows;
237                 }
238             } else {
239                 if (DEBUG) {
240                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
241                 }
242             }
243         } catch (RemoteException re) {
244             Log.e(LOG_TAG, "Error while calling remote getWindows", re);
245         }
246         return Collections.emptyList();
247     }
248 
249     /**
250      * Finds an {@link AccessibilityNodeInfo} by accessibility id.
251      *
252      * @param connectionId The id of a connection for interacting with the system.
253      * @param accessibilityWindowId A unique window id. Use
254      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
255      *     to query the currently active window.
256      * @param accessibilityNodeId A unique view id or virtual descendant id from
257      *     where to start the search. Use
258      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
259      *     to start from the root.
260      * @param bypassCache Whether to bypass the cache while looking for the node.
261      * @param prefetchFlags flags to guide prefetching.
262      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
263      */
findAccessibilityNodeInfoByAccessibilityId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, int prefetchFlags)264     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
265             int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
266             int prefetchFlags) {
267         if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0
268                 && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) {
269             throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS"
270                 + " requires FLAG_PREFETCH_PREDECESSORS");
271         }
272         try {
273             IAccessibilityServiceConnection connection = getConnection(connectionId);
274             if (connection != null) {
275                 if (!bypassCache) {
276                     AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode(
277                             accessibilityWindowId, accessibilityNodeId);
278                     if (cachedInfo != null) {
279                         if (DEBUG) {
280                             Log.i(LOG_TAG, "Node cache hit");
281                         }
282                         return cachedInfo;
283                     }
284                     if (DEBUG) {
285                         Log.i(LOG_TAG, "Node cache miss");
286                     }
287                 }
288                 final int interactionId = mInteractionIdCounter.getAndIncrement();
289                 final long identityToken = Binder.clearCallingIdentity();
290                 final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
291                         accessibilityWindowId, accessibilityNodeId, interactionId, this,
292                         prefetchFlags, Thread.currentThread().getId());
293                 Binder.restoreCallingIdentity(identityToken);
294                 // If the scale is zero the call has failed.
295                 if (success) {
296                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
297                             interactionId);
298                     finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
299                     if (infos != null && !infos.isEmpty()) {
300                         return infos.get(0);
301                     }
302                 }
303             } else {
304                 if (DEBUG) {
305                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
306                 }
307             }
308         } catch (RemoteException re) {
309             Log.e(LOG_TAG, "Error while calling remote"
310                     + " findAccessibilityNodeInfoByAccessibilityId", re);
311         }
312         return null;
313     }
314 
315     /**
316      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
317      * the window whose id is specified and starts from the node whose accessibility
318      * id is specified.
319      *
320      * @param connectionId The id of a connection for interacting with the system.
321      * @param accessibilityWindowId A unique window id. Use
322      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
323      *     to query the currently active window.
324      * @param accessibilityNodeId A unique view id or virtual descendant id from
325      *     where to start the search. Use
326      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
327      *     to start from the root.
328      * @param viewId The fully qualified resource name of the view id to find.
329      * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
330      */
findAccessibilityNodeInfosByViewId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String viewId)331     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
332             int accessibilityWindowId, long accessibilityNodeId, String viewId) {
333         try {
334             IAccessibilityServiceConnection connection = getConnection(connectionId);
335             if (connection != null) {
336                 final int interactionId = mInteractionIdCounter.getAndIncrement();
337                 final long identityToken = Binder.clearCallingIdentity();
338                 final boolean success = connection.findAccessibilityNodeInfosByViewId(
339                         accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
340                         Thread.currentThread().getId());
341                 Binder.restoreCallingIdentity(identityToken);
342                 if (success) {
343                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
344                             interactionId);
345                     if (infos != null) {
346                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
347                         return infos;
348                     }
349                 }
350             } else {
351                 if (DEBUG) {
352                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
353                 }
354             }
355         } catch (RemoteException re) {
356             Log.w(LOG_TAG, "Error while calling remote"
357                     + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
358         }
359         return Collections.emptyList();
360     }
361 
362     /**
363      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
364      * insensitive containment. The search is performed in the window whose
365      * id is specified and starts from the node whose accessibility id is
366      * specified.
367      *
368      * @param connectionId The id of a connection for interacting with the system.
369      * @param accessibilityWindowId A unique window id. Use
370      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
371      *     to query the currently active window.
372      * @param accessibilityNodeId A unique view id or virtual descendant id from
373      *     where to start the search. Use
374      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
375      *     to start from the root.
376      * @param text The searched text.
377      * @return A list of found {@link AccessibilityNodeInfo}s.
378      */
findAccessibilityNodeInfosByText(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String text)379     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
380             int accessibilityWindowId, long accessibilityNodeId, String text) {
381         try {
382             IAccessibilityServiceConnection connection = getConnection(connectionId);
383             if (connection != null) {
384                 final int interactionId = mInteractionIdCounter.getAndIncrement();
385                 final long identityToken = Binder.clearCallingIdentity();
386                 final boolean success = connection.findAccessibilityNodeInfosByText(
387                         accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
388                         Thread.currentThread().getId());
389                 Binder.restoreCallingIdentity(identityToken);
390                 if (success) {
391                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
392                             interactionId);
393                     if (infos != null) {
394                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
395                         return infos;
396                     }
397                 }
398             } else {
399                 if (DEBUG) {
400                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
401                 }
402             }
403         } catch (RemoteException re) {
404             Log.w(LOG_TAG, "Error while calling remote"
405                     + " findAccessibilityNodeInfosByViewText", re);
406         }
407         return Collections.emptyList();
408     }
409 
410     /**
411      * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
412      * specified focus type. The search is performed in the window whose id is specified
413      * and starts from the node whose accessibility id is specified.
414      *
415      * @param connectionId The id of a connection for interacting with the system.
416      * @param accessibilityWindowId A unique window id. Use
417      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
418      *     to query the currently active window.
419      * @param accessibilityNodeId A unique view id or virtual descendant id from
420      *     where to start the search. Use
421      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
422      *     to start from the root.
423      * @param focusType The focus type.
424      * @return The accessibility focused {@link AccessibilityNodeInfo}.
425      */
findFocus(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int focusType)426     public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
427             long accessibilityNodeId, int focusType) {
428         try {
429             IAccessibilityServiceConnection connection = getConnection(connectionId);
430             if (connection != null) {
431                 final int interactionId = mInteractionIdCounter.getAndIncrement();
432                 final long identityToken = Binder.clearCallingIdentity();
433                 final boolean success = connection.findFocus(accessibilityWindowId,
434                         accessibilityNodeId, focusType, interactionId, this,
435                         Thread.currentThread().getId());
436                 Binder.restoreCallingIdentity(identityToken);
437                 if (success) {
438                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
439                             interactionId);
440                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
441                     return info;
442                 }
443             } else {
444                 if (DEBUG) {
445                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
446                 }
447             }
448         } catch (RemoteException re) {
449             Log.w(LOG_TAG, "Error while calling remote findFocus", re);
450         }
451         return null;
452     }
453 
454     /**
455      * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
456      * The search is performed in the window whose id is specified and starts from the
457      * node whose accessibility id is specified.
458      *
459      * @param connectionId The id of a connection for interacting with the system.
460      * @param accessibilityWindowId A unique window id. Use
461      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
462      *     to query the currently active window.
463      * @param accessibilityNodeId A unique view id or virtual descendant id from
464      *     where to start the search. Use
465      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
466      *     to start from the root.
467      * @param direction The direction in which to search for focusable.
468      * @return The accessibility focused {@link AccessibilityNodeInfo}.
469      */
focusSearch(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int direction)470     public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
471             long accessibilityNodeId, int direction) {
472         try {
473             IAccessibilityServiceConnection connection = getConnection(connectionId);
474             if (connection != null) {
475                 final int interactionId = mInteractionIdCounter.getAndIncrement();
476                 final long identityToken = Binder.clearCallingIdentity();
477                 final boolean success = connection.focusSearch(accessibilityWindowId,
478                         accessibilityNodeId, direction, interactionId, this,
479                         Thread.currentThread().getId());
480                 Binder.restoreCallingIdentity(identityToken);
481                 if (success) {
482                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
483                             interactionId);
484                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
485                     return info;
486                 }
487             } else {
488                 if (DEBUG) {
489                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
490                 }
491             }
492         } catch (RemoteException re) {
493             Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
494         }
495         return null;
496     }
497 
498     /**
499      * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
500      *
501      * @param connectionId The id of a connection for interacting with the system.
502      * @param accessibilityWindowId A unique window id. Use
503      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
504      *     to query the currently active window.
505      * @param accessibilityNodeId A unique view id or virtual descendant id from
506      *     where to start the search. Use
507      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
508      *     to start from the root.
509      * @param action The action to perform.
510      * @param arguments Optional action arguments.
511      * @return Whether the action was performed.
512      */
performAccessibilityAction(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int action, Bundle arguments)513     public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
514             long accessibilityNodeId, int action, Bundle arguments) {
515         try {
516             IAccessibilityServiceConnection connection = getConnection(connectionId);
517             if (connection != null) {
518                 final int interactionId = mInteractionIdCounter.getAndIncrement();
519                 final long identityToken = Binder.clearCallingIdentity();
520                 final boolean success = connection.performAccessibilityAction(
521                         accessibilityWindowId, accessibilityNodeId, action, arguments,
522                         interactionId, this, Thread.currentThread().getId());
523                 Binder.restoreCallingIdentity(identityToken);
524                 if (success) {
525                     return getPerformAccessibilityActionResultAndClear(interactionId);
526                 }
527             } else {
528                 if (DEBUG) {
529                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
530                 }
531             }
532         } catch (RemoteException re) {
533             Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
534         }
535         return false;
536     }
537 
clearCache()538     public void clearCache() {
539         sAccessibilityCache.clear();
540     }
541 
onAccessibilityEvent(AccessibilityEvent event)542     public void onAccessibilityEvent(AccessibilityEvent event) {
543         sAccessibilityCache.onAccessibilityEvent(event);
544     }
545 
546     /**
547      * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
548      *
549      * @param interactionId The interaction id to match the result with the request.
550      * @return The result {@link AccessibilityNodeInfo}.
551      */
getFindAccessibilityNodeInfoResultAndClear(int interactionId)552     private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
553         synchronized (mInstanceLock) {
554             final boolean success = waitForResultTimedLocked(interactionId);
555             AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
556             clearResultLocked();
557             return result;
558         }
559     }
560 
561     /**
562      * {@inheritDoc}
563      */
setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId)564     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
565                 int interactionId) {
566         synchronized (mInstanceLock) {
567             if (interactionId > mInteractionId) {
568                 mFindAccessibilityNodeInfoResult = info;
569                 mInteractionId = interactionId;
570             }
571             mInstanceLock.notifyAll();
572         }
573     }
574 
575     /**
576      * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
577      *
578      * @param interactionId The interaction id to match the result with the request.
579      * @return The result {@link AccessibilityNodeInfo}s.
580      */
getFindAccessibilityNodeInfosResultAndClear( int interactionId)581     private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
582                 int interactionId) {
583         synchronized (mInstanceLock) {
584             final boolean success = waitForResultTimedLocked(interactionId);
585             List<AccessibilityNodeInfo> result = null;
586             if (success) {
587                 result = mFindAccessibilityNodeInfosResult;
588             } else {
589                 result = Collections.emptyList();
590             }
591             clearResultLocked();
592             if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
593                 checkFindAccessibilityNodeInfoResultIntegrity(result);
594             }
595             return result;
596         }
597     }
598 
599     /**
600      * {@inheritDoc}
601      */
setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId)602     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
603                 int interactionId) {
604         synchronized (mInstanceLock) {
605             if (interactionId > mInteractionId) {
606                 if (infos != null) {
607                     // If the call is not an IPC, i.e. it is made from the same process, we need to
608                     // instantiate new result list to avoid passing internal instances to clients.
609                     final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
610                     if (!isIpcCall) {
611                         mFindAccessibilityNodeInfosResult = new ArrayList<>(infos);
612                     } else {
613                         mFindAccessibilityNodeInfosResult = infos;
614                     }
615                 } else {
616                     mFindAccessibilityNodeInfosResult = Collections.emptyList();
617                 }
618                 mInteractionId = interactionId;
619             }
620             mInstanceLock.notifyAll();
621         }
622     }
623 
624     /**
625      * Gets the result of a request to perform an accessibility action.
626      *
627      * @param interactionId The interaction id to match the result with the request.
628      * @return Whether the action was performed.
629      */
getPerformAccessibilityActionResultAndClear(int interactionId)630     private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
631         synchronized (mInstanceLock) {
632             final boolean success = waitForResultTimedLocked(interactionId);
633             final boolean result = success ? mPerformAccessibilityActionResult : false;
634             clearResultLocked();
635             return result;
636         }
637     }
638 
639     /**
640      * {@inheritDoc}
641      */
setPerformAccessibilityActionResult(boolean succeeded, int interactionId)642     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
643         synchronized (mInstanceLock) {
644             if (interactionId > mInteractionId) {
645                 mPerformAccessibilityActionResult = succeeded;
646                 mInteractionId = interactionId;
647             }
648             mInstanceLock.notifyAll();
649         }
650     }
651 
652     /**
653      * Clears the result state.
654      */
clearResultLocked()655     private void clearResultLocked() {
656         mInteractionId = -1;
657         mFindAccessibilityNodeInfoResult = null;
658         mFindAccessibilityNodeInfosResult = null;
659         mPerformAccessibilityActionResult = false;
660     }
661 
662     /**
663      * Waits up to a given bound for a result of a request and returns it.
664      *
665      * @param interactionId The interaction id to match the result with the request.
666      * @return Whether the result was received.
667      */
waitForResultTimedLocked(int interactionId)668     private boolean waitForResultTimedLocked(int interactionId) {
669         long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
670         final long startTimeMillis = SystemClock.uptimeMillis();
671         while (true) {
672             try {
673                 Message sameProcessMessage = getSameProcessMessageAndClear();
674                 if (sameProcessMessage != null) {
675                     sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
676                 }
677 
678                 if (mInteractionId == interactionId) {
679                     return true;
680                 }
681                 if (mInteractionId > interactionId) {
682                     return false;
683                 }
684                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
685                 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
686                 if (waitTimeMillis <= 0) {
687                     return false;
688                 }
689                 mInstanceLock.wait(waitTimeMillis);
690             } catch (InterruptedException ie) {
691                 /* ignore */
692             }
693         }
694     }
695 
696     /**
697      * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
698      *
699      * @param info The info.
700      * @param connectionId The id of the connection to the system.
701      */
finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId)702     private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
703             int connectionId) {
704         if (info != null) {
705             info.setConnectionId(connectionId);
706             info.setSealed(true);
707             sAccessibilityCache.add(info);
708         }
709     }
710 
711     /**
712      * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
713      *
714      * @param infos The {@link AccessibilityNodeInfo}s.
715      * @param connectionId The id of the connection to the system.
716      */
finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, int connectionId)717     private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
718             int connectionId) {
719         if (infos != null) {
720             final int infosCount = infos.size();
721             for (int i = 0; i < infosCount; i++) {
722                 AccessibilityNodeInfo info = infos.get(i);
723                 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
724             }
725         }
726     }
727 
728     /**
729      * Gets the message stored if the interacted and interacting
730      * threads are the same.
731      *
732      * @return The message.
733      */
getSameProcessMessageAndClear()734     private Message getSameProcessMessageAndClear() {
735         synchronized (mInstanceLock) {
736             Message result = mSameThreadMessage;
737             mSameThreadMessage = null;
738             return result;
739         }
740     }
741 
742     /**
743      * Gets a cached accessibility service connection.
744      *
745      * @param connectionId The connection id.
746      * @return The cached connection if such.
747      */
getConnection(int connectionId)748     public IAccessibilityServiceConnection getConnection(int connectionId) {
749         synchronized (sConnectionCache) {
750             return sConnectionCache.get(connectionId);
751         }
752     }
753 
754     /**
755      * Adds a cached accessibility service connection.
756      *
757      * @param connectionId The connection id.
758      * @param connection The connection.
759      */
addConnection(int connectionId, IAccessibilityServiceConnection connection)760     public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
761         synchronized (sConnectionCache) {
762             sConnectionCache.put(connectionId, connection);
763         }
764     }
765 
766     /**
767      * Removes a cached accessibility service connection.
768      *
769      * @param connectionId The connection id.
770      */
removeConnection(int connectionId)771     public void removeConnection(int connectionId) {
772         synchronized (sConnectionCache) {
773             sConnectionCache.remove(connectionId);
774         }
775     }
776 
777     /**
778      * Checks whether the infos are a fully connected tree with no duplicates.
779      *
780      * @param infos The result list to check.
781      */
checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos)782     private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
783         if (infos.size() == 0) {
784             return;
785         }
786         // Find the root node.
787         AccessibilityNodeInfo root = infos.get(0);
788         final int infoCount = infos.size();
789         for (int i = 1; i < infoCount; i++) {
790             for (int j = i; j < infoCount; j++) {
791                 AccessibilityNodeInfo candidate = infos.get(j);
792                 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
793                     root = candidate;
794                     break;
795                 }
796             }
797         }
798         if (root == null) {
799             Log.e(LOG_TAG, "No root.");
800         }
801         // Check for duplicates.
802         HashSet<AccessibilityNodeInfo> seen = new HashSet<>();
803         Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
804         fringe.add(root);
805         while (!fringe.isEmpty()) {
806             AccessibilityNodeInfo current = fringe.poll();
807             if (!seen.add(current)) {
808                 Log.e(LOG_TAG, "Duplicate node.");
809                 return;
810             }
811             final int childCount = current.getChildCount();
812             for (int i = 0; i < childCount; i++) {
813                 final long childId = current.getChildId(i);
814                 for (int j = 0; j < infoCount; j++) {
815                     AccessibilityNodeInfo child = infos.get(j);
816                     if (child.getSourceNodeId() == childId) {
817                         fringe.add(child);
818                     }
819                 }
820             }
821         }
822         final int disconnectedCount = infos.size() - seen.size();
823         if (disconnectedCount > 0) {
824             Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
825         }
826     }
827 }
828