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