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