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