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