• 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.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.content.Context;
24 import android.os.Binder;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.IBinder;
28 import android.os.Message;
29 import android.os.Process;
30 import android.os.RemoteException;
31 import android.os.SystemClock;
32 import android.util.Log;
33 import android.util.LongSparseArray;
34 import android.util.SparseArray;
35 import android.util.SparseLongArray;
36 import android.view.Display;
37 import android.view.ViewConfiguration;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.util.ArrayUtils;
41 
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collections;
45 import java.util.HashSet;
46 import java.util.LinkedList;
47 import java.util.List;
48 import java.util.Queue;
49 import java.util.concurrent.atomic.AtomicInteger;
50 
51 /**
52  * This class is a singleton that performs accessibility interaction
53  * which is it queries remote view hierarchies about snapshots of their
54  * views as well requests from these hierarchies to perform certain
55  * actions on their views.
56  *
57  * Rationale: The content retrieval APIs are synchronous from a client's
58  *     perspective but internally they are asynchronous. The client thread
59  *     calls into the system requesting an action and providing a callback
60  *     to receive the result after which it waits up to a timeout for that
61  *     result. The system enforces security and the delegates the request
62  *     to a given view hierarchy where a message is posted (from a binder
63  *     thread) describing what to be performed by the main UI thread the
64  *     result of which it delivered via the mentioned callback. However,
65  *     the blocked client thread and the main UI thread of the target view
66  *     hierarchy can be the same thread, for example an accessibility service
67  *     and an activity run in the same process, thus they are executed on the
68  *     same main thread. In such a case the retrieval will fail since the UI
69  *     thread that has to process the message describing the work to be done
70  *     is blocked waiting for a result is has to compute! To avoid this scenario
71  *     when making a call the client also passes its process and thread ids so
72  *     the accessed view hierarchy can detect if the client making the request
73  *     is running in its main UI thread. In such a case the view hierarchy,
74  *     specifically the binder thread performing the IPC to it, does not post a
75  *     message to be run on the UI thread but passes it to the singleton
76  *     interaction client through which all interactions occur and the latter is
77  *     responsible to execute the message before starting to wait for the
78  *     asynchronous result delivered via the callback. In this case the expected
79  *     result is already received so no waiting is performed.
80  *
81  * @hide
82  */
83 public final class AccessibilityInteractionClient
84         extends IAccessibilityInteractionConnectionCallback.Stub {
85 
86     public static final int NO_ID = -1;
87 
88     public static final String CALL_STACK = "call_stack";
89 
90     private static final String LOG_TAG = "AccessibilityInteractionClient";
91 
92     private static final boolean DEBUG = false;
93 
94     private static final boolean CHECK_INTEGRITY = true;
95 
96     private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
97 
98     private static final long DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS =
99             (long) (ViewConfiguration.getSendRecurringAccessibilityEventsInterval() * 1.5);
100 
101     private static final Object sStaticLock = new Object();
102 
103     private static final LongSparseArray<AccessibilityInteractionClient> sClients =
104         new LongSparseArray<>();
105 
106     private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
107             new SparseArray<>();
108 
109     /** List of timestamps which indicate the latest time an a11y service receives a scroll event
110         from a window, mapping from windowId -> timestamp. */
111     private static final SparseLongArray sScrollingWindows = new SparseLongArray();
112 
113     private static AccessibilityCache sAccessibilityCache =
114             new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher());
115 
116     private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
117 
118     private final Object mInstanceLock = new Object();
119 
120     private final AccessibilityManager  mAccessibilityManager;
121 
122     private volatile int mInteractionId = -1;
123     private volatile int mCallingUid = Process.INVALID_UID;
124 
125     private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
126 
127     private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult;
128 
129     private boolean mPerformAccessibilityActionResult;
130 
131     private Message mSameThreadMessage;
132 
133     private int mInteractionIdWaitingForPrefetchResult = -1;
134     private int mConnectionIdWaitingForPrefetchResult;
135     private String[] mPackageNamesForNextPrefetchResult;
136 
137     /**
138      * @return The client for the current thread.
139      */
140     @UnsupportedAppUsage()
getInstance()141     public static AccessibilityInteractionClient getInstance() {
142         final long threadId = Thread.currentThread().getId();
143         return getInstanceForThread(threadId);
144     }
145 
146     /**
147      * <strong>Note:</strong> We keep one instance per interrogating thread since
148      * the instance contains state which can lead to undesired thread interleavings.
149      * We do not have a thread local variable since other threads should be able to
150      * look up the correct client knowing a thread id. See ViewRootImpl for details.
151      *
152      * @return The client for a given <code>threadId</code>.
153      */
getInstanceForThread(long threadId)154     public static AccessibilityInteractionClient getInstanceForThread(long threadId) {
155         synchronized (sStaticLock) {
156             AccessibilityInteractionClient client = sClients.get(threadId);
157             if (client == null) {
158                 client = new AccessibilityInteractionClient();
159                 sClients.put(threadId, client);
160             }
161             return client;
162         }
163     }
164 
165     /**
166      * @return The client for the current thread.
167      */
getInstance(Context context)168     public static AccessibilityInteractionClient getInstance(Context context) {
169         final long threadId = Thread.currentThread().getId();
170         if (context != null) {
171             return getInstanceForThread(threadId, context);
172         }
173         return getInstanceForThread(threadId);
174     }
175 
176     /**
177      * <strong>Note:</strong> We keep one instance per interrogating thread since
178      * the instance contains state which can lead to undesired thread interleavings.
179      * We do not have a thread local variable since other threads should be able to
180      * look up the correct client knowing a thread id. See ViewRootImpl for details.
181      *
182      * @return The client for a given <code>threadId</code>.
183      */
getInstanceForThread( long threadId, Context context)184     public static AccessibilityInteractionClient getInstanceForThread(
185             long threadId, Context context) {
186         synchronized (sStaticLock) {
187             AccessibilityInteractionClient client = sClients.get(threadId);
188             if (client == null) {
189                 client = new AccessibilityInteractionClient(context);
190                 sClients.put(threadId, client);
191             }
192             return client;
193         }
194     }
195 
196     /**
197      * Gets a cached accessibility service connection.
198      *
199      * @param connectionId The connection id.
200      * @return The cached connection if such.
201      */
getConnection(int connectionId)202     public static IAccessibilityServiceConnection getConnection(int connectionId) {
203         synchronized (sConnectionCache) {
204             return sConnectionCache.get(connectionId);
205         }
206     }
207 
208     /**
209      * Adds a cached accessibility service connection.
210      *
211      * @param connectionId The connection id.
212      * @param connection The connection.
213      */
addConnection(int connectionId, IAccessibilityServiceConnection connection)214     public static void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
215         synchronized (sConnectionCache) {
216             sConnectionCache.put(connectionId, connection);
217         }
218     }
219 
220     /**
221      * Removes a cached accessibility service connection.
222      *
223      * @param connectionId The connection id.
224      */
removeConnection(int connectionId)225     public static void removeConnection(int connectionId) {
226         synchronized (sConnectionCache) {
227             sConnectionCache.remove(connectionId);
228         }
229     }
230 
231     /**
232      * This method is only for testing. Replacing the cache is a generally terrible idea, but
233      * tests need to be able to verify this class's interactions with the cache
234      */
235     @VisibleForTesting
setCache(AccessibilityCache cache)236     public static void setCache(AccessibilityCache cache) {
237         sAccessibilityCache = cache;
238     }
239 
AccessibilityInteractionClient()240     private AccessibilityInteractionClient() {
241         /* reducing constructor visibility */
242         mAccessibilityManager = null;
243     }
244 
AccessibilityInteractionClient(Context context)245     private AccessibilityInteractionClient(Context context) {
246         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
247     }
248 
249     /**
250      * Sets the message to be processed if the interacted view hierarchy
251      * and the interacting client are running in the same thread.
252      *
253      * @param message The message.
254      */
255     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
setSameThreadMessage(Message message)256     public void setSameThreadMessage(Message message) {
257         synchronized (mInstanceLock) {
258             mSameThreadMessage = message;
259             mInstanceLock.notifyAll();
260         }
261     }
262 
263     /**
264      * Gets the root {@link AccessibilityNodeInfo} in the currently active window.
265      *
266      * @param connectionId The id of a connection for interacting with the system.
267      * @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
268      */
getRootInActiveWindow(int connectionId)269     public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
270         return findAccessibilityNodeInfoByAccessibilityId(connectionId,
271                 AccessibilityWindowInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
272                 false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
273     }
274 
275     /**
276      * Gets the info for a window.
277      *
278      * @param connectionId The id of a connection for interacting with the system.
279      * @param accessibilityWindowId A unique window id. Use
280      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
281      *     to query the currently active window.
282      * @return The {@link AccessibilityWindowInfo}.
283      */
getWindow(int connectionId, int accessibilityWindowId)284     public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId) {
285         return getWindow(connectionId, accessibilityWindowId, /* bypassCache */ false);
286     }
287 
288     /**
289      * Gets the info for a window.
290      *
291      * @param connectionId The id of a connection for interacting with the system.
292      * @param accessibilityWindowId A unique window id. Use
293      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
294      *     to query the currently active window.
295      * @param bypassCache Whether to bypass the cache.
296      * @return The {@link AccessibilityWindowInfo}.
297      */
getWindow(int connectionId, int accessibilityWindowId, boolean bypassCache)298     public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId,
299             boolean bypassCache) {
300         try {
301             IAccessibilityServiceConnection connection = getConnection(connectionId);
302             if (connection != null) {
303                 AccessibilityWindowInfo window;
304                 if (!bypassCache) {
305                     window = sAccessibilityCache.getWindow(accessibilityWindowId);
306                     if (window != null) {
307                         if (DEBUG) {
308                             Log.i(LOG_TAG, "Window cache hit");
309                         }
310                         return window;
311                     }
312                     if (DEBUG) {
313                         Log.i(LOG_TAG, "Window cache miss");
314                     }
315                 }
316                 final long identityToken = Binder.clearCallingIdentity();
317                 try {
318                     window = connection.getWindow(accessibilityWindowId);
319                 } finally {
320                     Binder.restoreCallingIdentity(identityToken);
321                 }
322                 if (window != null) {
323                     if (!bypassCache) {
324                         sAccessibilityCache.addWindow(window);
325                     }
326                     return window;
327                 }
328             } else {
329                 if (DEBUG) {
330                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
331                 }
332             }
333         } catch (RemoteException re) {
334             Log.e(LOG_TAG, "Error while calling remote getWindow", re);
335         }
336         return null;
337     }
338 
339     /**
340      * Gets the info for all windows of the default display.
341      *
342      * @param connectionId The id of a connection for interacting with the system.
343      * @return The {@link AccessibilityWindowInfo} list.
344      */
getWindows(int connectionId)345     public List<AccessibilityWindowInfo> getWindows(int connectionId) {
346         final SparseArray<List<AccessibilityWindowInfo>> windows =
347                 getWindowsOnAllDisplays(connectionId);
348         if (windows.size() > 0) {
349             return windows.valueAt(Display.DEFAULT_DISPLAY);
350         }
351         return Collections.emptyList();
352     }
353 
354     /**
355      * Gets the info for all windows of all displays.
356      *
357      * @param connectionId The id of a connection for interacting with the system.
358      * @return The SparseArray of {@link AccessibilityWindowInfo} list.
359      *         The key of SparseArray is display ID.
360      */
getWindowsOnAllDisplays(int connectionId)361     public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays(int connectionId) {
362         try {
363             IAccessibilityServiceConnection connection = getConnection(connectionId);
364             if (connection != null) {
365                 SparseArray<List<AccessibilityWindowInfo>> windows =
366                         sAccessibilityCache.getWindowsOnAllDisplays();
367                 if (windows != null) {
368                     if (DEBUG) {
369                         Log.i(LOG_TAG, "Windows cache hit");
370                     }
371                     return windows;
372                 }
373                 if (DEBUG) {
374                     Log.i(LOG_TAG, "Windows cache miss");
375                 }
376                 final long identityToken = Binder.clearCallingIdentity();
377                 try {
378                     windows = connection.getWindows();
379                 } finally {
380                     Binder.restoreCallingIdentity(identityToken);
381                 }
382                 if (windows != null) {
383                     sAccessibilityCache.setWindowsOnAllDisplays(windows);
384                     return windows;
385                 }
386             } else {
387                 if (DEBUG) {
388                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
389                 }
390             }
391         } catch (RemoteException re) {
392             Log.e(LOG_TAG, "Error while calling remote getWindowsOnAllDisplays", re);
393         }
394 
395         final SparseArray<List<AccessibilityWindowInfo>> emptyWindows = new SparseArray<>();
396         return emptyWindows;
397     }
398 
399 
400     /**
401      * Finds an {@link AccessibilityNodeInfo} by accessibility id and given leash token instead of
402      * window id. This method is used to find the leashed node on the embedded view hierarchy.
403      *
404      * @param connectionId The id of a connection for interacting with the system.
405      * @param leashToken The token of the embedded hierarchy.
406      * @param accessibilityNodeId A unique view id or virtual descendant id from
407      *     where to start the search. Use
408      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
409      *     to start from the root.
410      * @param bypassCache Whether to bypass the cache while looking for the node.
411      * @param prefetchFlags flags to guide prefetching.
412      * @param arguments Optional action arguments.
413      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
414      */
findAccessibilityNodeInfoByAccessibilityId( int connectionId, @NonNull IBinder leashToken, long accessibilityNodeId, boolean bypassCache, int prefetchFlags, Bundle arguments)415     public @Nullable AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(
416             int connectionId, @NonNull IBinder leashToken, long accessibilityNodeId,
417             boolean bypassCache, int prefetchFlags, Bundle arguments) {
418         if (leashToken == null) {
419             return null;
420         }
421         int windowId = -1;
422         try {
423             IAccessibilityServiceConnection connection = getConnection(connectionId);
424             if (connection != null) {
425                 windowId = connection.getWindowIdForLeashToken(leashToken);
426             } else {
427                 if (DEBUG) {
428                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
429                 }
430             }
431         } catch (RemoteException re) {
432             Log.e(LOG_TAG, "Error while calling remote getWindowIdForLeashToken", re);
433         }
434         if (windowId == -1) {
435             return null;
436         }
437         return findAccessibilityNodeInfoByAccessibilityId(connectionId, windowId,
438                 accessibilityNodeId, bypassCache, prefetchFlags, arguments);
439     }
440 
441     /**
442      * Finds an {@link AccessibilityNodeInfo} by accessibility id.
443      *
444      * @param connectionId The id of a connection for interacting with the system.
445      * @param accessibilityWindowId A unique window id. Use
446      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
447      *     to query the currently active window.
448      * @param accessibilityNodeId A unique view id or virtual descendant id from
449      *     where to start the search. Use
450      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
451      *     to start from the root.
452      * @param bypassCache Whether to bypass the cache while looking for the node.
453      * @param prefetchFlags flags to guide prefetching.
454      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
455      */
findAccessibilityNodeInfoByAccessibilityId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, int prefetchFlags, Bundle arguments)456     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
457             int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
458             int prefetchFlags, Bundle arguments) {
459         if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0
460                 && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) {
461             throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS"
462                 + " requires FLAG_PREFETCH_PREDECESSORS");
463         }
464         try {
465             IAccessibilityServiceConnection connection = getConnection(connectionId);
466             if (connection != null) {
467                 if (!bypassCache) {
468                     AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode(
469                             accessibilityWindowId, accessibilityNodeId);
470                     if (cachedInfo != null) {
471                         if (DEBUG) {
472                             Log.i(LOG_TAG, "Node cache hit for "
473                                     + idToString(accessibilityWindowId, accessibilityNodeId));
474                         }
475                         return cachedInfo;
476                     }
477                     if (DEBUG) {
478                         Log.i(LOG_TAG, "Node cache miss for "
479                                 + idToString(accessibilityWindowId, accessibilityNodeId));
480                     }
481                 } else {
482                     // No need to prefech nodes in bypass cache case.
483                     prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
484                 }
485                 // Skip prefetching if window is scrolling.
486                 if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
487                         && isWindowScrolling(accessibilityWindowId)) {
488                     prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
489                 }
490                 final int interactionId = mInteractionIdCounter.getAndIncrement();
491                 final String[] packageNames;
492                 final long identityToken = Binder.clearCallingIdentity();
493                 try {
494                     packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(
495                             accessibilityWindowId, accessibilityNodeId, interactionId, this,
496                             prefetchFlags, Thread.currentThread().getId(), arguments);
497                 } finally {
498                     Binder.restoreCallingIdentity(identityToken);
499                 }
500                 if (packageNames != null) {
501                     AccessibilityNodeInfo info =
502                             getFindAccessibilityNodeInfoResultAndClear(interactionId);
503                     if (mAccessibilityManager != null
504                             && mAccessibilityManager.isAccessibilityTracingEnabled()) {
505                         logTrace(connection, "findAccessibilityNodeInfoByAccessibilityId",
506                                 "InteractionId:" + interactionId + ";Result: " + info
507                                         + ";connectionId=" + connectionId
508                                         + ";accessibilityWindowId="
509                                         + accessibilityWindowId + ";accessibilityNodeId="
510                                         + accessibilityNodeId + ";bypassCache=" + bypassCache
511                                         + ";prefetchFlags=" + prefetchFlags
512                                         + ";arguments=" + arguments);
513                     }
514                     if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
515                             && info != null) {
516                         setInteractionWaitingForPrefetchResult(interactionId, connectionId,
517                                 packageNames);
518                     }
519                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
520                             bypassCache, packageNames);
521                     return info;
522                 }
523             } else {
524                 if (DEBUG) {
525                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
526                 }
527             }
528         } catch (RemoteException re) {
529             Log.e(LOG_TAG, "Error while calling remote"
530                     + " findAccessibilityNodeInfoByAccessibilityId", re);
531         }
532         return null;
533     }
534 
setInteractionWaitingForPrefetchResult(int interactionId, int connectionId, String[] packageNames)535     private void setInteractionWaitingForPrefetchResult(int interactionId, int connectionId,
536             String[] packageNames) {
537         synchronized (mInstanceLock) {
538             mInteractionIdWaitingForPrefetchResult = interactionId;
539             mConnectionIdWaitingForPrefetchResult = connectionId;
540             mPackageNamesForNextPrefetchResult = packageNames;
541         }
542     }
543 
idToString(int accessibilityWindowId, long accessibilityNodeId)544     private static String idToString(int accessibilityWindowId, long accessibilityNodeId) {
545         return accessibilityWindowId + "/"
546                 + AccessibilityNodeInfo.idToString(accessibilityNodeId);
547     }
548 
549     /**
550      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
551      * the window whose id is specified and starts from the node whose accessibility
552      * id is specified.
553      *
554      * @param connectionId The id of a connection for interacting with the system.
555      * @param accessibilityWindowId A unique window id. Use
556      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
557      *     to query the currently active window.
558      * @param accessibilityNodeId A unique view id or virtual descendant id from
559      *     where to start the search. Use
560      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
561      *     to start from the root.
562      * @param viewId The fully qualified resource name of the view id to find.
563      * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
564      */
findAccessibilityNodeInfosByViewId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String viewId)565     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
566             int accessibilityWindowId, long accessibilityNodeId, String viewId) {
567         try {
568             IAccessibilityServiceConnection connection = getConnection(connectionId);
569             if (connection != null) {
570                 final int interactionId = mInteractionIdCounter.getAndIncrement();
571                 final String[] packageNames;
572                 final long identityToken = Binder.clearCallingIdentity();
573                 try {
574                     packageNames = connection.findAccessibilityNodeInfosByViewId(
575                             accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
576                             Thread.currentThread().getId());
577                 } finally {
578                     Binder.restoreCallingIdentity(identityToken);
579                 }
580 
581                 if (packageNames != null) {
582                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
583                             interactionId);
584                     if (mAccessibilityManager != null
585                             && mAccessibilityManager.isAccessibilityTracingEnabled()) {
586                         logTrace(connection, "findAccessibilityNodeInfosByViewId", "InteractionId="
587                                 + interactionId + ":Result: " + infos + ";connectionId="
588                                 + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
589                                 + ";accessibilityNodeId=" + accessibilityNodeId + ";viewId="
590                                 + viewId);
591                     }
592                     if (infos != null) {
593                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
594                                 false, packageNames);
595                         return infos;
596                     }
597                 }
598             } else {
599                 if (DEBUG) {
600                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
601                 }
602             }
603         } catch (RemoteException re) {
604             Log.w(LOG_TAG, "Error while calling remote"
605                     + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
606         }
607         return Collections.emptyList();
608     }
609 
610     /**
611      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
612      * insensitive containment. The search is performed in the window whose
613      * id is specified and starts from the node whose accessibility id is
614      * specified.
615      *
616      * @param connectionId The id of a connection for interacting with the system.
617      * @param accessibilityWindowId A unique window id. Use
618      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
619      *     to query the currently active window.
620      * @param accessibilityNodeId A unique view id or virtual descendant id from
621      *     where to start the search. Use
622      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
623      *     to start from the root.
624      * @param text The searched text.
625      * @return A list of found {@link AccessibilityNodeInfo}s.
626      */
findAccessibilityNodeInfosByText(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String text)627     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
628             int accessibilityWindowId, long accessibilityNodeId, String text) {
629         try {
630             IAccessibilityServiceConnection connection = getConnection(connectionId);
631             if (connection != null) {
632                 final int interactionId = mInteractionIdCounter.getAndIncrement();
633                 final String[] packageNames;
634                 final long identityToken = Binder.clearCallingIdentity();
635                 try {
636                     packageNames = connection.findAccessibilityNodeInfosByText(
637                             accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
638                             Thread.currentThread().getId());
639                 } finally {
640                     Binder.restoreCallingIdentity(identityToken);
641                 }
642 
643                 if (packageNames != null) {
644                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
645                             interactionId);
646                     if (mAccessibilityManager != null
647                             && mAccessibilityManager.isAccessibilityTracingEnabled()) {
648                         logTrace(connection, "findAccessibilityNodeInfosByText", "InteractionId="
649                                 + interactionId + ":Result: " + infos + ";connectionId="
650                                 + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
651                                 + ";accessibilityNodeId=" + accessibilityNodeId + ";text=" + text);
652                     }
653                     if (infos != null) {
654                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
655                                 false, packageNames);
656                         return infos;
657                     }
658                 }
659             } else {
660                 if (DEBUG) {
661                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
662                 }
663             }
664         } catch (RemoteException re) {
665             Log.w(LOG_TAG, "Error while calling remote"
666                     + " findAccessibilityNodeInfosByViewText", re);
667         }
668         return Collections.emptyList();
669     }
670 
671     /**
672      * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
673      * specified focus type. The search is performed in the window whose id is specified
674      * and starts from the node whose accessibility id is specified.
675      *
676      * @param connectionId The id of a connection for interacting with the system.
677      * @param accessibilityWindowId A unique window id. Use
678      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
679      *     to query the currently active window.
680      * @param accessibilityNodeId A unique view id or virtual descendant id from
681      *     where to start the search. Use
682      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
683      *     to start from the root.
684      * @param focusType The focus type.
685      * @return The accessibility focused {@link AccessibilityNodeInfo}.
686      */
findFocus(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int focusType)687     public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
688             long accessibilityNodeId, int focusType) {
689         try {
690             IAccessibilityServiceConnection connection = getConnection(connectionId);
691             if (connection != null) {
692                 final int interactionId = mInteractionIdCounter.getAndIncrement();
693                 final String[] packageNames;
694                 final long identityToken = Binder.clearCallingIdentity();
695                 try {
696                     packageNames = connection.findFocus(accessibilityWindowId,
697                             accessibilityNodeId, focusType, interactionId, this,
698                             Thread.currentThread().getId());
699                 } finally {
700                     Binder.restoreCallingIdentity(identityToken);
701                 }
702 
703                 if (packageNames != null) {
704                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
705                             interactionId);
706                     if (mAccessibilityManager != null
707                             && mAccessibilityManager.isAccessibilityTracingEnabled()) {
708                         logTrace(connection, "findFocus", "InteractionId=" + interactionId
709                                 + ":Result: " + info + ";connectionId=" + connectionId
710                                 + ";accessibilityWindowId=" + accessibilityWindowId
711                                 + ";accessibilityNodeId=" + accessibilityNodeId + ";focusType="
712                                 + focusType);
713                     }
714                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
715                     return info;
716                 }
717             } else {
718                 if (DEBUG) {
719                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
720                 }
721             }
722         } catch (RemoteException re) {
723             Log.w(LOG_TAG, "Error while calling remote findFocus", re);
724         }
725         return null;
726     }
727 
728     /**
729      * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
730      * The search is performed in the window whose id is specified and starts from the
731      * node whose accessibility id is specified.
732      *
733      * @param connectionId The id of a connection for interacting with the system.
734      * @param accessibilityWindowId A unique window id. Use
735      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
736      *     to query the currently active window.
737      * @param accessibilityNodeId A unique view id or virtual descendant id from
738      *     where to start the search. Use
739      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
740      *     to start from the root.
741      * @param direction The direction in which to search for focusable.
742      * @return The accessibility focused {@link AccessibilityNodeInfo}.
743      */
focusSearch(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int direction)744     public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
745             long accessibilityNodeId, int direction) {
746         try {
747             IAccessibilityServiceConnection connection = getConnection(connectionId);
748             if (connection != null) {
749                 final int interactionId = mInteractionIdCounter.getAndIncrement();
750                 final String[] packageNames;
751                 final long identityToken = Binder.clearCallingIdentity();
752                 try {
753                     packageNames = connection.focusSearch(accessibilityWindowId,
754                             accessibilityNodeId, direction, interactionId, this,
755                             Thread.currentThread().getId());
756                 } finally {
757                     Binder.restoreCallingIdentity(identityToken);
758                 }
759 
760                 if (packageNames != null) {
761                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
762                             interactionId);
763                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
764                     if (mAccessibilityManager != null
765                             && mAccessibilityManager.isAccessibilityTracingEnabled()) {
766                         logTrace(connection, "focusSearch", "InteractionId=" + interactionId
767                                 + ":Result: " + info + ";connectionId=" + connectionId
768                                 + ";accessibilityWindowId=" + accessibilityWindowId
769                                 + ";accessibilityNodeId=" + accessibilityNodeId + ";direction="
770                                 + direction);
771                     }
772                     return info;
773                 }
774             } else {
775                 if (DEBUG) {
776                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
777                 }
778             }
779         } catch (RemoteException re) {
780             Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
781         }
782         return null;
783     }
784 
785     /**
786      * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
787      *
788      * @param connectionId The id of a connection for interacting with the system.
789      * @param accessibilityWindowId A unique window id. Use
790      *     {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
791      *     to query the currently active window.
792      * @param accessibilityNodeId A unique view id or virtual descendant id from
793      *     where to start the search. Use
794      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
795      *     to start from the root.
796      * @param action The action to perform.
797      * @param arguments Optional action arguments.
798      * @return Whether the action was performed.
799      */
performAccessibilityAction(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int action, Bundle arguments)800     public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
801             long accessibilityNodeId, int action, Bundle arguments) {
802         try {
803             IAccessibilityServiceConnection connection = getConnection(connectionId);
804             if (connection != null) {
805                 final int interactionId = mInteractionIdCounter.getAndIncrement();
806                 final boolean success;
807                 final long identityToken = Binder.clearCallingIdentity();
808                 try {
809                     success = connection.performAccessibilityAction(
810                             accessibilityWindowId, accessibilityNodeId, action, arguments,
811                             interactionId, this, Thread.currentThread().getId());
812                 } finally {
813                     Binder.restoreCallingIdentity(identityToken);
814                 }
815 
816                 if (success) {
817                     final boolean result =
818                             getPerformAccessibilityActionResultAndClear(interactionId);
819                     if (mAccessibilityManager != null
820                             && mAccessibilityManager.isAccessibilityTracingEnabled()) {
821                         logTrace(connection, "performAccessibilityAction", "InteractionId="
822                                 + interactionId + ":Result: " + result + ";connectionId="
823                                 + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
824                                 + ";accessibilityNodeId=" + accessibilityNodeId + ";action="
825                                 + action + ";arguments=" + arguments);
826                     }
827                     return result;
828                 }
829             } else {
830                 if (DEBUG) {
831                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
832                 }
833             }
834         } catch (RemoteException re) {
835             Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
836         }
837         return false;
838     }
839 
840     /**
841      * Clears the accessibility cache.
842      */
843     @UnsupportedAppUsage()
clearCache()844     public void clearCache() {
845         sAccessibilityCache.clear();
846     }
847 
onAccessibilityEvent(AccessibilityEvent event)848     public void onAccessibilityEvent(AccessibilityEvent event) {
849         switch (event.getEventType()) {
850             case AccessibilityEvent.TYPE_VIEW_SCROLLED:
851                 updateScrollingWindow(event.getWindowId(), SystemClock.uptimeMillis());
852                 break;
853             case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
854                 if (event.getWindowChanges() == AccessibilityEvent.WINDOWS_CHANGE_REMOVED) {
855                     deleteScrollingWindow(event.getWindowId());
856                 }
857                 break;
858             default:
859                 break;
860         }
861         sAccessibilityCache.onAccessibilityEvent(event);
862     }
863 
864     /**
865      * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
866      *
867      * @param interactionId The interaction id to match the result with the request.
868      * @return The result {@link AccessibilityNodeInfo}.
869      */
getFindAccessibilityNodeInfoResultAndClear(int interactionId)870     private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
871         synchronized (mInstanceLock) {
872             final boolean success = waitForResultTimedLocked(interactionId);
873             AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
874             clearResultLocked();
875             return result;
876         }
877     }
878 
879     /**
880      * {@inheritDoc}
881      */
setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId)882     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
883                 int interactionId) {
884         synchronized (mInstanceLock) {
885             if (interactionId > mInteractionId) {
886                 mFindAccessibilityNodeInfoResult = info;
887                 mInteractionId = interactionId;
888                 mCallingUid = Binder.getCallingUid();
889             }
890             mInstanceLock.notifyAll();
891         }
892     }
893 
894     /**
895      * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
896      *
897      * @param interactionId The interaction id to match the result with the request.
898      * @return The result {@link AccessibilityNodeInfo}s.
899      */
getFindAccessibilityNodeInfosResultAndClear( int interactionId)900     private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
901                 int interactionId) {
902         synchronized (mInstanceLock) {
903             final boolean success = waitForResultTimedLocked(interactionId);
904             final List<AccessibilityNodeInfo> result;
905             if (success) {
906                 result = mFindAccessibilityNodeInfosResult;
907             } else {
908                 result = Collections.emptyList();
909             }
910             clearResultLocked();
911             if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
912                 checkFindAccessibilityNodeInfoResultIntegrity(result);
913             }
914             return result;
915         }
916     }
917 
918     /**
919      * {@inheritDoc}
920      */
setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId)921     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
922                 int interactionId) {
923         synchronized (mInstanceLock) {
924             if (interactionId > mInteractionId) {
925                 if (infos != null) {
926                     // If the call is not an IPC, i.e. it is made from the same process, we need to
927                     // instantiate new result list to avoid passing internal instances to clients.
928                     final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
929                     if (!isIpcCall) {
930                         mFindAccessibilityNodeInfosResult = new ArrayList<>(infos);
931                     } else {
932                         mFindAccessibilityNodeInfosResult = infos;
933                     }
934                 } else {
935                     mFindAccessibilityNodeInfosResult = Collections.emptyList();
936                 }
937                 mInteractionId = interactionId;
938                 mCallingUid = Binder.getCallingUid();
939             }
940             mInstanceLock.notifyAll();
941         }
942     }
943 
944     /**
945      * {@inheritDoc}
946      */
947     @Override
setPrefetchAccessibilityNodeInfoResult(@onNull List<AccessibilityNodeInfo> infos, int interactionId)948     public void setPrefetchAccessibilityNodeInfoResult(@NonNull List<AccessibilityNodeInfo> infos,
949                                                        int interactionId) {
950         int interactionIdWaitingForPrefetchResultCopy = -1;
951         int connectionIdWaitingForPrefetchResultCopy = -1;
952         String[] packageNamesForNextPrefetchResultCopy = null;
953 
954         if (infos.isEmpty()) {
955             return;
956         }
957 
958         synchronized (mInstanceLock) {
959             if (mInteractionIdWaitingForPrefetchResult == interactionId) {
960                 interactionIdWaitingForPrefetchResultCopy = mInteractionIdWaitingForPrefetchResult;
961                 connectionIdWaitingForPrefetchResultCopy =
962                         mConnectionIdWaitingForPrefetchResult;
963                 if (mPackageNamesForNextPrefetchResult != null) {
964                     packageNamesForNextPrefetchResultCopy =
965                             new String[mPackageNamesForNextPrefetchResult.length];
966                     for (int i = 0; i < mPackageNamesForNextPrefetchResult.length; i++) {
967                         packageNamesForNextPrefetchResultCopy[i] =
968                                 mPackageNamesForNextPrefetchResult[i];
969                     }
970                 }
971             }
972         }
973 
974         if (interactionIdWaitingForPrefetchResultCopy == interactionId) {
975             finalizeAndCacheAccessibilityNodeInfos(
976                     infos, connectionIdWaitingForPrefetchResultCopy, false,
977                     packageNamesForNextPrefetchResultCopy);
978             if (mAccessibilityManager != null
979                     && mAccessibilityManager.isAccessibilityTracingEnabled()) {
980                 logTrace(getConnection(connectionIdWaitingForPrefetchResultCopy),
981                         "setPrefetchAccessibilityNodeInfoResult",
982                         "InteractionId:" + interactionId + ";Result: " + infos
983                                 + ";connectionId=" + connectionIdWaitingForPrefetchResultCopy,
984                         Binder.getCallingUid());
985             }
986         } else if (DEBUG) {
987             Log.w(LOG_TAG, "Prefetching for interaction with id " + interactionId + " dropped "
988                     + infos.size() + " nodes");
989         }
990     }
991 
992     /**
993      * Gets the result of a request to perform an accessibility action.
994      *
995      * @param interactionId The interaction id to match the result with the request.
996      * @return Whether the action was performed.
997      */
getPerformAccessibilityActionResultAndClear(int interactionId)998     private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
999         synchronized (mInstanceLock) {
1000             final boolean success = waitForResultTimedLocked(interactionId);
1001             final boolean result = success ? mPerformAccessibilityActionResult : false;
1002             clearResultLocked();
1003             return result;
1004         }
1005     }
1006 
1007     /**
1008      * {@inheritDoc}
1009      */
setPerformAccessibilityActionResult(boolean succeeded, int interactionId)1010     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
1011         synchronized (mInstanceLock) {
1012             if (interactionId > mInteractionId) {
1013                 mPerformAccessibilityActionResult = succeeded;
1014                 mInteractionId = interactionId;
1015                 mCallingUid = Binder.getCallingUid();
1016             }
1017             mInstanceLock.notifyAll();
1018         }
1019     }
1020 
1021     /**
1022      * Clears the result state.
1023      */
clearResultLocked()1024     private void clearResultLocked() {
1025         mInteractionId = -1;
1026         mFindAccessibilityNodeInfoResult = null;
1027         mFindAccessibilityNodeInfosResult = null;
1028         mPerformAccessibilityActionResult = false;
1029     }
1030 
1031     /**
1032      * Waits up to a given bound for a result of a request and returns it.
1033      *
1034      * @param interactionId The interaction id to match the result with the request.
1035      * @return Whether the result was received.
1036      */
waitForResultTimedLocked(int interactionId)1037     private boolean waitForResultTimedLocked(int interactionId) {
1038         long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
1039         final long startTimeMillis = SystemClock.uptimeMillis();
1040         while (true) {
1041             try {
1042                 Message sameProcessMessage = getSameProcessMessageAndClear();
1043                 if (sameProcessMessage != null) {
1044                     sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
1045                 }
1046 
1047                 if (mInteractionId == interactionId) {
1048                     return true;
1049                 }
1050                 if (mInteractionId > interactionId) {
1051                     return false;
1052                 }
1053                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
1054                 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
1055                 if (waitTimeMillis <= 0) {
1056                     return false;
1057                 }
1058                 mInstanceLock.wait(waitTimeMillis);
1059             } catch (InterruptedException ie) {
1060                 /* ignore */
1061             }
1062         }
1063     }
1064 
1065     /**
1066      * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
1067      *
1068      * @param info The info.
1069      * @param connectionId The id of the connection to the system.
1070      * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if
1071      *                    this value is {@code false}
1072      * @param packageNames The valid package names a node can come from.
1073      */
finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, boolean bypassCache, String[] packageNames)1074     private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
1075             int connectionId, boolean bypassCache, String[] packageNames) {
1076         if (info != null) {
1077             info.setConnectionId(connectionId);
1078             // Empty array means any package name is Okay
1079             if (!ArrayUtils.isEmpty(packageNames)) {
1080                 CharSequence packageName = info.getPackageName();
1081                 if (packageName == null
1082                         || !ArrayUtils.contains(packageNames, packageName.toString())) {
1083                     // If the node package not one of the valid ones, pick the top one - this
1084                     // is one of the packages running in the introspected UID.
1085                     info.setPackageName(packageNames[0]);
1086                 }
1087             }
1088             info.setSealed(true);
1089             if (!bypassCache) {
1090                 sAccessibilityCache.add(info);
1091             }
1092         }
1093     }
1094 
1095     /**
1096      * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
1097      *
1098      * @param infos The {@link AccessibilityNodeInfo}s.
1099      * @param connectionId The id of the connection to the system.
1100      * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if
1101      *                    this value is {@code false}
1102      * @param packageNames The valid package names a node can come from.
1103      */
finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, int connectionId, boolean bypassCache, String[] packageNames)1104     private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
1105             int connectionId, boolean bypassCache, String[] packageNames) {
1106         if (infos != null) {
1107             final int infosCount = infos.size();
1108             for (int i = 0; i < infosCount; i++) {
1109                 AccessibilityNodeInfo info = infos.get(i);
1110                 finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
1111                         bypassCache, packageNames);
1112             }
1113         }
1114     }
1115 
1116     /**
1117      * Gets the message stored if the interacted and interacting
1118      * threads are the same.
1119      *
1120      * @return The message.
1121      */
getSameProcessMessageAndClear()1122     private Message getSameProcessMessageAndClear() {
1123         synchronized (mInstanceLock) {
1124             Message result = mSameThreadMessage;
1125             mSameThreadMessage = null;
1126             return result;
1127         }
1128     }
1129 
1130     /**
1131      * Checks whether the infos are a fully connected tree with no duplicates.
1132      *
1133      * @param infos The result list to check.
1134      */
checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos)1135     private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
1136         if (infos.size() == 0) {
1137             return;
1138         }
1139         // Find the root node.
1140         AccessibilityNodeInfo root = infos.get(0);
1141         final int infoCount = infos.size();
1142         for (int i = 1; i < infoCount; i++) {
1143             for (int j = i; j < infoCount; j++) {
1144                 AccessibilityNodeInfo candidate = infos.get(j);
1145                 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
1146                     root = candidate;
1147                     break;
1148                 }
1149             }
1150         }
1151         if (root == null) {
1152             Log.e(LOG_TAG, "No root.");
1153         }
1154         // Check for duplicates.
1155         HashSet<AccessibilityNodeInfo> seen = new HashSet<>();
1156         Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
1157         fringe.add(root);
1158         while (!fringe.isEmpty()) {
1159             AccessibilityNodeInfo current = fringe.poll();
1160             if (!seen.add(current)) {
1161                 Log.e(LOG_TAG, "Duplicate node.");
1162                 return;
1163             }
1164             final int childCount = current.getChildCount();
1165             for (int i = 0; i < childCount; i++) {
1166                 final long childId = current.getChildId(i);
1167                 for (int j = 0; j < infoCount; j++) {
1168                     AccessibilityNodeInfo child = infos.get(j);
1169                     if (child.getSourceNodeId() == childId) {
1170                         fringe.add(child);
1171                     }
1172                 }
1173             }
1174         }
1175         final int disconnectedCount = infos.size() - seen.size();
1176         if (disconnectedCount > 0) {
1177             Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
1178         }
1179     }
1180 
1181     /**
1182      * Update scroll event timestamp of a given window.
1183      *
1184      * @param windowId The window id.
1185      * @param uptimeMillis Device uptime millis.
1186      */
updateScrollingWindow(int windowId, long uptimeMillis)1187     private void updateScrollingWindow(int windowId, long uptimeMillis) {
1188         synchronized (sScrollingWindows) {
1189             sScrollingWindows.put(windowId, uptimeMillis);
1190         }
1191     }
1192 
1193     /**
1194      * Remove a window from the scrolling windows list.
1195      *
1196      * @param windowId The window id.
1197      */
deleteScrollingWindow(int windowId)1198     private void deleteScrollingWindow(int windowId) {
1199         synchronized (sScrollingWindows) {
1200             sScrollingWindows.delete(windowId);
1201         }
1202     }
1203 
1204     /**
1205      * Whether or not the window is scrolling.
1206      *
1207      * @param windowId
1208      * @return true if it's scrolling.
1209      */
isWindowScrolling(int windowId)1210     private boolean isWindowScrolling(int windowId) {
1211         synchronized (sScrollingWindows) {
1212             final long latestScrollingTime = sScrollingWindows.get(windowId);
1213             if (latestScrollingTime == 0) {
1214                 return false;
1215             }
1216             final long currentUptime = SystemClock.uptimeMillis();
1217             if (currentUptime > (latestScrollingTime + DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS)) {
1218                 sScrollingWindows.delete(windowId);
1219                 return false;
1220             }
1221         }
1222         return true;
1223     }
1224 
logTrace( IAccessibilityServiceConnection connection, String method, String params, int callingUid)1225     private void logTrace(
1226             IAccessibilityServiceConnection connection, String method, String params,
1227             int callingUid) {
1228         try {
1229             Bundle b = new Bundle();
1230             ArrayList<StackTraceElement> callStack = new ArrayList<StackTraceElement>(
1231                     Arrays.asList(Thread.currentThread().getStackTrace()));
1232             b.putSerializable(CALL_STACK, callStack);
1233             connection.logTrace(SystemClock.elapsedRealtimeNanos(),
1234                     LOG_TAG + ".callback for " + method, params, Process.myPid(),
1235                     Thread.currentThread().getId(), callingUid, b);
1236         } catch (RemoteException e) {
1237             Log.e(LOG_TAG, "Failed to log trace. " + e);
1238         }
1239     }
1240 
logTrace( IAccessibilityServiceConnection connection, String method, String params)1241     private void logTrace(
1242             IAccessibilityServiceConnection connection, String method, String params) {
1243         logTrace(connection, method, params, mCallingUid);
1244     }
1245 }
1246