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