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