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