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