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