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