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