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                 false, 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 bypassCache Whether to bypass the cache while looking for the node.
    181      * @param prefetchFlags flags to guide prefetching.
    182      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
    183      */
    184     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
    185             int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
    186             int prefetchFlags) {
    187         try {
    188             IAccessibilityServiceConnection connection = getConnection(connectionId);
    189             if (connection != null) {
    190                 if (!bypassCache) {
    191                     AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
    192                             accessibilityNodeId);
    193                     if (cachedInfo != null) {
    194                         return cachedInfo;
    195                     }
    196                 }
    197                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    198                 final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
    199                         accessibilityWindowId, accessibilityNodeId, interactionId, this,
    200                         prefetchFlags, Thread.currentThread().getId());
    201                 // If the scale is zero the call has failed.
    202                 if (success) {
    203                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    204                             interactionId);
    205                     finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
    206                     if (infos != null && !infos.isEmpty()) {
    207                         return infos.get(0);
    208                     }
    209                 }
    210             } else {
    211                 if (DEBUG) {
    212                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    213                 }
    214             }
    215         } catch (RemoteException re) {
    216             if (DEBUG) {
    217                 Log.w(LOG_TAG, "Error while calling remote"
    218                         + " findAccessibilityNodeInfoByAccessibilityId", re);
    219             }
    220         }
    221         return null;
    222     }
    223 
    224     /**
    225      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
    226      * the window whose id is specified and starts from the node whose accessibility
    227      * id is specified.
    228      *
    229      * @param connectionId The id of a connection for interacting with the system.
    230      * @param accessibilityWindowId A unique window id. Use
    231      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    232      *     to query the currently active window.
    233      * @param accessibilityNodeId A unique view id or virtual descendant id from
    234      *     where to start the search. Use
    235      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    236      *     to start from the root.
    237      * @param viewId The fully qualified resource name of the view id to find.
    238      * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
    239      */
    240     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
    241             int accessibilityWindowId, long accessibilityNodeId, String viewId) {
    242         try {
    243             IAccessibilityServiceConnection connection = getConnection(connectionId);
    244             if (connection != null) {
    245                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    246                 final boolean success = connection.findAccessibilityNodeInfosByViewId(
    247                         accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
    248                         Thread.currentThread().getId());
    249                 if (success) {
    250                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    251                             interactionId);
    252                     if (infos != null) {
    253                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
    254                         return infos;
    255                     }
    256                 }
    257             } else {
    258                 if (DEBUG) {
    259                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    260                 }
    261             }
    262         } catch (RemoteException re) {
    263             if (DEBUG) {
    264                 Log.w(LOG_TAG, "Error while calling remote"
    265                         + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
    266             }
    267         }
    268         return Collections.emptyList();
    269     }
    270 
    271     /**
    272      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
    273      * insensitive containment. The search is performed in the window whose
    274      * id is specified and starts from the node whose accessibility id is
    275      * specified.
    276      *
    277      * @param connectionId The id of a connection for interacting with the system.
    278      * @param accessibilityWindowId A unique window id. Use
    279      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    280      *     to query the currently active window.
    281      * @param accessibilityNodeId A unique view id or virtual descendant id from
    282      *     where to start the search. Use
    283      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    284      *     to start from the root.
    285      * @param text The searched text.
    286      * @return A list of found {@link AccessibilityNodeInfo}s.
    287      */
    288     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
    289             int accessibilityWindowId, long accessibilityNodeId, String text) {
    290         try {
    291             IAccessibilityServiceConnection connection = getConnection(connectionId);
    292             if (connection != null) {
    293                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    294                 final boolean success = connection.findAccessibilityNodeInfosByText(
    295                         accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
    296                         Thread.currentThread().getId());
    297                 if (success) {
    298                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    299                             interactionId);
    300                     if (infos != null) {
    301                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
    302                         return infos;
    303                     }
    304                 }
    305             } else {
    306                 if (DEBUG) {
    307                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    308                 }
    309             }
    310         } catch (RemoteException re) {
    311             if (DEBUG) {
    312                 Log.w(LOG_TAG, "Error while calling remote"
    313                         + " findAccessibilityNodeInfosByViewText", re);
    314             }
    315         }
    316         return Collections.emptyList();
    317     }
    318 
    319     /**
    320      * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
    321      * specified focus type. The search is performed in the window whose id is specified
    322      * and starts from the node whose accessibility id is specified.
    323      *
    324      * @param connectionId The id of a connection for interacting with the system.
    325      * @param accessibilityWindowId A unique window id. Use
    326      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    327      *     to query the currently active window.
    328      * @param accessibilityNodeId A unique view id or virtual descendant id from
    329      *     where to start the search. Use
    330      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    331      *     to start from the root.
    332      * @param focusType The focus type.
    333      * @return The accessibility focused {@link AccessibilityNodeInfo}.
    334      */
    335     public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
    336             long accessibilityNodeId, int focusType) {
    337         try {
    338             IAccessibilityServiceConnection connection = getConnection(connectionId);
    339             if (connection != null) {
    340                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    341                 final boolean success = connection.findFocus(accessibilityWindowId,
    342                         accessibilityNodeId, focusType, interactionId, this,
    343                         Thread.currentThread().getId());
    344                 if (success) {
    345                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
    346                             interactionId);
    347                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
    348                     return info;
    349                 }
    350             } else {
    351                 if (DEBUG) {
    352                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    353                 }
    354             }
    355         } catch (RemoteException re) {
    356             if (DEBUG) {
    357                 Log.w(LOG_TAG, "Error while calling remote findFocus", re);
    358             }
    359         }
    360         return null;
    361     }
    362 
    363     /**
    364      * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
    365      * The search is performed in the window whose id is specified and starts from the
    366      * node whose accessibility id is specified.
    367      *
    368      * @param connectionId The id of a connection for interacting with the system.
    369      * @param accessibilityWindowId A unique window id. Use
    370      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    371      *     to query the currently active window.
    372      * @param accessibilityNodeId A unique view id or virtual descendant id from
    373      *     where to start the search. Use
    374      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    375      *     to start from the root.
    376      * @param direction The direction in which to search for focusable.
    377      * @return The accessibility focused {@link AccessibilityNodeInfo}.
    378      */
    379     public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
    380             long accessibilityNodeId, int direction) {
    381         try {
    382             IAccessibilityServiceConnection connection = getConnection(connectionId);
    383             if (connection != null) {
    384                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    385                 final boolean success = connection.focusSearch(accessibilityWindowId,
    386                         accessibilityNodeId, direction, interactionId, this,
    387                         Thread.currentThread().getId());
    388                 if (success) {
    389                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
    390                             interactionId);
    391                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
    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      * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
    608      *
    609      * @param info The info.
    610      * @param connectionId The id of the connection to the system.
    611      */
    612     private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
    613             int connectionId) {
    614         if (info != null) {
    615             info.setConnectionId(connectionId);
    616             info.setSealed(true);
    617             sAccessibilityNodeInfoCache.add(info);
    618         }
    619     }
    620 
    621     /**
    622      * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
    623      *
    624      * @param infos The {@link AccessibilityNodeInfo}s.
    625      * @param connectionId The id of the connection to the system.
    626      */
    627     private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
    628             int connectionId) {
    629         if (infos != null) {
    630             final int infosCount = infos.size();
    631             for (int i = 0; i < infosCount; i++) {
    632                 AccessibilityNodeInfo info = infos.get(i);
    633                 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
    634             }
    635         }
    636     }
    637 
    638     /**
    639      * Gets the message stored if the interacted and interacting
    640      * threads are the same.
    641      *
    642      * @return The message.
    643      */
    644     private Message getSameProcessMessageAndClear() {
    645         synchronized (mInstanceLock) {
    646             Message result = mSameThreadMessage;
    647             mSameThreadMessage = null;
    648             return result;
    649         }
    650     }
    651 
    652     /**
    653      * Gets a cached accessibility service connection.
    654      *
    655      * @param connectionId The connection id.
    656      * @return The cached connection if such.
    657      */
    658     public IAccessibilityServiceConnection getConnection(int connectionId) {
    659         synchronized (sConnectionCache) {
    660             return sConnectionCache.get(connectionId);
    661         }
    662     }
    663 
    664     /**
    665      * Adds a cached accessibility service connection.
    666      *
    667      * @param connectionId The connection id.
    668      * @param connection The connection.
    669      */
    670     public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
    671         synchronized (sConnectionCache) {
    672             sConnectionCache.put(connectionId, connection);
    673         }
    674     }
    675 
    676     /**
    677      * Removes a cached accessibility service connection.
    678      *
    679      * @param connectionId The connection id.
    680      */
    681     public void removeConnection(int connectionId) {
    682         synchronized (sConnectionCache) {
    683             sConnectionCache.remove(connectionId);
    684         }
    685     }
    686 
    687     /**
    688      * Checks whether the infos are a fully connected tree with no duplicates.
    689      *
    690      * @param infos The result list to check.
    691      */
    692     private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
    693         if (infos.size() == 0) {
    694             return;
    695         }
    696         // Find the root node.
    697         AccessibilityNodeInfo root = infos.get(0);
    698         final int infoCount = infos.size();
    699         for (int i = 1; i < infoCount; i++) {
    700             for (int j = i; j < infoCount; j++) {
    701                 AccessibilityNodeInfo candidate = infos.get(j);
    702                 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
    703                     root = candidate;
    704                     break;
    705                 }
    706             }
    707         }
    708         if (root == null) {
    709             Log.e(LOG_TAG, "No root.");
    710         }
    711         // Check for duplicates.
    712         HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
    713         Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
    714         fringe.add(root);
    715         while (!fringe.isEmpty()) {
    716             AccessibilityNodeInfo current = fringe.poll();
    717             if (!seen.add(current)) {
    718                 Log.e(LOG_TAG, "Duplicate node.");
    719                 return;
    720             }
    721             SparseLongArray childIds = current.getChildNodeIds();
    722             final int childCount = childIds.size();
    723             for (int i = 0; i < childCount; i++) {
    724                 final long childId = childIds.valueAt(i);
    725                 for (int j = 0; j < infoCount; j++) {
    726                     AccessibilityNodeInfo child = infos.get(j);
    727                     if (child.getSourceNodeId() == childId) {
    728                         fringe.add(child);
    729                     }
    730                 }
    731             }
    732         }
    733         final int disconnectedCount = infos.size() - seen.size();
    734         if (disconnectedCount > 0) {
    735             Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
    736         }
    737     }
    738 }
    739