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