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                     sAccessibilityCache.setWindows(windows);
    232                     return windows;
    233                 }
    234             } else {
    235                 if (DEBUG) {
    236                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    237                 }
    238             }
    239         } catch (RemoteException re) {
    240             Log.e(LOG_TAG, "Error while calling remote getWindows", re);
    241         }
    242         return Collections.emptyList();
    243     }
    244 
    245     /**
    246      * Finds an {@link AccessibilityNodeInfo} by accessibility id.
    247      *
    248      * @param connectionId The id of a connection for interacting with the system.
    249      * @param accessibilityWindowId A unique window id. Use
    250      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    251      *     to query the currently active window.
    252      * @param accessibilityNodeId A unique view id or virtual descendant id from
    253      *     where to start the search. Use
    254      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    255      *     to start from the root.
    256      * @param bypassCache Whether to bypass the cache while looking for the node.
    257      * @param prefetchFlags flags to guide prefetching.
    258      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
    259      */
    260     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
    261             int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
    262             int prefetchFlags) {
    263         if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0
    264                 && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) {
    265             throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS"
    266                 + " requires FLAG_PREFETCH_PREDECESSORS");
    267         }
    268         try {
    269             IAccessibilityServiceConnection connection = getConnection(connectionId);
    270             if (connection != null) {
    271                 if (!bypassCache) {
    272                     AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode(
    273                             accessibilityWindowId, accessibilityNodeId);
    274                     if (cachedInfo != null) {
    275                         if (DEBUG) {
    276                             Log.i(LOG_TAG, "Node cache hit");
    277                         }
    278                         return cachedInfo;
    279                     }
    280                     if (DEBUG) {
    281                         Log.i(LOG_TAG, "Node cache miss");
    282                     }
    283                 }
    284                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    285                 final long identityToken = Binder.clearCallingIdentity();
    286                 final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
    287                         accessibilityWindowId, accessibilityNodeId, interactionId, this,
    288                         prefetchFlags, Thread.currentThread().getId());
    289                 Binder.restoreCallingIdentity(identityToken);
    290                 // If the scale is zero the call has failed.
    291                 if (success) {
    292                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    293                             interactionId);
    294                     finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
    295                     if (infos != null && !infos.isEmpty()) {
    296                         return infos.get(0);
    297                     }
    298                 }
    299             } else {
    300                 if (DEBUG) {
    301                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    302                 }
    303             }
    304         } catch (RemoteException re) {
    305             Log.e(LOG_TAG, "Error while calling remote"
    306                     + " findAccessibilityNodeInfoByAccessibilityId", re);
    307         }
    308         return null;
    309     }
    310 
    311     /**
    312      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
    313      * the window whose id is specified and starts from the node whose accessibility
    314      * id is specified.
    315      *
    316      * @param connectionId The id of a connection for interacting with the system.
    317      * @param accessibilityWindowId A unique window id. Use
    318      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    319      *     to query the currently active window.
    320      * @param accessibilityNodeId A unique view id or virtual descendant id from
    321      *     where to start the search. Use
    322      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    323      *     to start from the root.
    324      * @param viewId The fully qualified resource name of the view id to find.
    325      * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
    326      */
    327     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
    328             int accessibilityWindowId, long accessibilityNodeId, String viewId) {
    329         try {
    330             IAccessibilityServiceConnection connection = getConnection(connectionId);
    331             if (connection != null) {
    332                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    333                 final long identityToken = Binder.clearCallingIdentity();
    334                 final boolean success = connection.findAccessibilityNodeInfosByViewId(
    335                         accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
    336                         Thread.currentThread().getId());
    337                 Binder.restoreCallingIdentity(identityToken);
    338                 if (success) {
    339                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    340                             interactionId);
    341                     if (infos != null) {
    342                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
    343                         return infos;
    344                     }
    345                 }
    346             } else {
    347                 if (DEBUG) {
    348                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    349                 }
    350             }
    351         } catch (RemoteException re) {
    352             Log.w(LOG_TAG, "Error while calling remote"
    353                     + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
    354         }
    355         return Collections.emptyList();
    356     }
    357 
    358     /**
    359      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
    360      * insensitive containment. The search is performed in the window whose
    361      * id is specified and starts from the node whose accessibility id is
    362      * specified.
    363      *
    364      * @param connectionId The id of a connection for interacting with the system.
    365      * @param accessibilityWindowId A unique window id. Use
    366      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    367      *     to query the currently active window.
    368      * @param accessibilityNodeId A unique view id or virtual descendant id from
    369      *     where to start the search. Use
    370      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    371      *     to start from the root.
    372      * @param text The searched text.
    373      * @return A list of found {@link AccessibilityNodeInfo}s.
    374      */
    375     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
    376             int accessibilityWindowId, long accessibilityNodeId, String text) {
    377         try {
    378             IAccessibilityServiceConnection connection = getConnection(connectionId);
    379             if (connection != null) {
    380                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    381                 final long identityToken = Binder.clearCallingIdentity();
    382                 final boolean success = connection.findAccessibilityNodeInfosByText(
    383                         accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
    384                         Thread.currentThread().getId());
    385                 Binder.restoreCallingIdentity(identityToken);
    386                 if (success) {
    387                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    388                             interactionId);
    389                     if (infos != null) {
    390                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
    391                         return infos;
    392                     }
    393                 }
    394             } else {
    395                 if (DEBUG) {
    396                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    397                 }
    398             }
    399         } catch (RemoteException re) {
    400             Log.w(LOG_TAG, "Error while calling remote"
    401                     + " findAccessibilityNodeInfosByViewText", re);
    402         }
    403         return Collections.emptyList();
    404     }
    405 
    406     /**
    407      * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
    408      * specified focus type. The search is performed in the window whose id is specified
    409      * and starts from the node whose accessibility id is specified.
    410      *
    411      * @param connectionId The id of a connection for interacting with the system.
    412      * @param accessibilityWindowId A unique window id. Use
    413      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    414      *     to query the currently active window.
    415      * @param accessibilityNodeId A unique view id or virtual descendant id from
    416      *     where to start the search. Use
    417      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    418      *     to start from the root.
    419      * @param focusType The focus type.
    420      * @return The accessibility focused {@link AccessibilityNodeInfo}.
    421      */
    422     public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
    423             long accessibilityNodeId, int focusType) {
    424         try {
    425             IAccessibilityServiceConnection connection = getConnection(connectionId);
    426             if (connection != null) {
    427                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    428                 final long identityToken = Binder.clearCallingIdentity();
    429                 final boolean success = connection.findFocus(accessibilityWindowId,
    430                         accessibilityNodeId, focusType, interactionId, this,
    431                         Thread.currentThread().getId());
    432                 Binder.restoreCallingIdentity(identityToken);
    433                 if (success) {
    434                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
    435                             interactionId);
    436                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
    437                     return info;
    438                 }
    439             } else {
    440                 if (DEBUG) {
    441                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    442                 }
    443             }
    444         } catch (RemoteException re) {
    445             Log.w(LOG_TAG, "Error while calling remote findFocus", re);
    446         }
    447         return null;
    448     }
    449 
    450     /**
    451      * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
    452      * The search is performed in the window whose id is specified and starts from the
    453      * node whose accessibility id is specified.
    454      *
    455      * @param connectionId The id of a connection for interacting with the system.
    456      * @param accessibilityWindowId A unique window id. Use
    457      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    458      *     to query the currently active window.
    459      * @param accessibilityNodeId A unique view id or virtual descendant id from
    460      *     where to start the search. Use
    461      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    462      *     to start from the root.
    463      * @param direction The direction in which to search for focusable.
    464      * @return The accessibility focused {@link AccessibilityNodeInfo}.
    465      */
    466     public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
    467             long accessibilityNodeId, int direction) {
    468         try {
    469             IAccessibilityServiceConnection connection = getConnection(connectionId);
    470             if (connection != null) {
    471                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    472                 final long identityToken = Binder.clearCallingIdentity();
    473                 final boolean success = connection.focusSearch(accessibilityWindowId,
    474                         accessibilityNodeId, direction, interactionId, this,
    475                         Thread.currentThread().getId());
    476                 Binder.restoreCallingIdentity(identityToken);
    477                 if (success) {
    478                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
    479                             interactionId);
    480                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
    481                     return info;
    482                 }
    483             } else {
    484                 if (DEBUG) {
    485                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    486                 }
    487             }
    488         } catch (RemoteException re) {
    489             Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
    490         }
    491         return null;
    492     }
    493 
    494     /**
    495      * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
    496      *
    497      * @param connectionId The id of a connection for interacting with the system.
    498      * @param accessibilityWindowId A unique window id. Use
    499      *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
    500      *     to query the currently active window.
    501      * @param accessibilityNodeId A unique view id or virtual descendant id from
    502      *     where to start the search. Use
    503      *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
    504      *     to start from the root.
    505      * @param action The action to perform.
    506      * @param arguments Optional action arguments.
    507      * @return Whether the action was performed.
    508      */
    509     public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
    510             long accessibilityNodeId, int action, Bundle arguments) {
    511         try {
    512             IAccessibilityServiceConnection connection = getConnection(connectionId);
    513             if (connection != null) {
    514                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    515                 final long identityToken = Binder.clearCallingIdentity();
    516                 final boolean success = connection.performAccessibilityAction(
    517                         accessibilityWindowId, accessibilityNodeId, action, arguments,
    518                         interactionId, this, Thread.currentThread().getId());
    519                 Binder.restoreCallingIdentity(identityToken);
    520                 if (success) {
    521                     return getPerformAccessibilityActionResultAndClear(interactionId);
    522                 }
    523             } else {
    524                 if (DEBUG) {
    525                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    526                 }
    527             }
    528         } catch (RemoteException re) {
    529             Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
    530         }
    531         return false;
    532     }
    533 
    534     public void clearCache() {
    535         sAccessibilityCache.clear();
    536     }
    537 
    538     public void onAccessibilityEvent(AccessibilityEvent event) {
    539         sAccessibilityCache.onAccessibilityEvent(event);
    540     }
    541 
    542     /**
    543      * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
    544      *
    545      * @param interactionId The interaction id to match the result with the request.
    546      * @return The result {@link AccessibilityNodeInfo}.
    547      */
    548     private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
    549         synchronized (mInstanceLock) {
    550             final boolean success = waitForResultTimedLocked(interactionId);
    551             AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
    552             clearResultLocked();
    553             return result;
    554         }
    555     }
    556 
    557     /**
    558      * {@inheritDoc}
    559      */
    560     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
    561                 int interactionId) {
    562         synchronized (mInstanceLock) {
    563             if (interactionId > mInteractionId) {
    564                 mFindAccessibilityNodeInfoResult = info;
    565                 mInteractionId = interactionId;
    566             }
    567             mInstanceLock.notifyAll();
    568         }
    569     }
    570 
    571     /**
    572      * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
    573      *
    574      * @param interactionId The interaction id to match the result with the request.
    575      * @return The result {@link AccessibilityNodeInfo}s.
    576      */
    577     private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
    578                 int interactionId) {
    579         synchronized (mInstanceLock) {
    580             final boolean success = waitForResultTimedLocked(interactionId);
    581             List<AccessibilityNodeInfo> result = null;
    582             if (success) {
    583                 result = mFindAccessibilityNodeInfosResult;
    584             } else {
    585                 result = Collections.emptyList();
    586             }
    587             clearResultLocked();
    588             if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
    589                 checkFindAccessibilityNodeInfoResultIntegrity(result);
    590             }
    591             return result;
    592         }
    593     }
    594 
    595     /**
    596      * {@inheritDoc}
    597      */
    598     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
    599                 int interactionId) {
    600         synchronized (mInstanceLock) {
    601             if (interactionId > mInteractionId) {
    602                 if (infos != null) {
    603                     // If the call is not an IPC, i.e. it is made from the same process, we need to
    604                     // instantiate new result list to avoid passing internal instances to clients.
    605                     final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
    606                     if (!isIpcCall) {
    607                         mFindAccessibilityNodeInfosResult = new ArrayList<>(infos);
    608                     } else {
    609                         mFindAccessibilityNodeInfosResult = infos;
    610                     }
    611                 } else {
    612                     mFindAccessibilityNodeInfosResult = Collections.emptyList();
    613                 }
    614                 mInteractionId = interactionId;
    615             }
    616             mInstanceLock.notifyAll();
    617         }
    618     }
    619 
    620     /**
    621      * Gets the result of a request to perform an accessibility action.
    622      *
    623      * @param interactionId The interaction id to match the result with the request.
    624      * @return Whether the action was performed.
    625      */
    626     private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
    627         synchronized (mInstanceLock) {
    628             final boolean success = waitForResultTimedLocked(interactionId);
    629             final boolean result = success ? mPerformAccessibilityActionResult : false;
    630             clearResultLocked();
    631             return result;
    632         }
    633     }
    634 
    635     /**
    636      * {@inheritDoc}
    637      */
    638     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
    639         synchronized (mInstanceLock) {
    640             if (interactionId > mInteractionId) {
    641                 mPerformAccessibilityActionResult = succeeded;
    642                 mInteractionId = interactionId;
    643             }
    644             mInstanceLock.notifyAll();
    645         }
    646     }
    647 
    648     /**
    649      * Clears the result state.
    650      */
    651     private void clearResultLocked() {
    652         mInteractionId = -1;
    653         mFindAccessibilityNodeInfoResult = null;
    654         mFindAccessibilityNodeInfosResult = null;
    655         mPerformAccessibilityActionResult = false;
    656     }
    657 
    658     /**
    659      * Waits up to a given bound for a result of a request and returns it.
    660      *
    661      * @param interactionId The interaction id to match the result with the request.
    662      * @return Whether the result was received.
    663      */
    664     private boolean waitForResultTimedLocked(int interactionId) {
    665         long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
    666         final long startTimeMillis = SystemClock.uptimeMillis();
    667         while (true) {
    668             try {
    669                 Message sameProcessMessage = getSameProcessMessageAndClear();
    670                 if (sameProcessMessage != null) {
    671                     sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
    672                 }
    673 
    674                 if (mInteractionId == interactionId) {
    675                     return true;
    676                 }
    677                 if (mInteractionId > interactionId) {
    678                     return false;
    679                 }
    680                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
    681                 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
    682                 if (waitTimeMillis <= 0) {
    683                     return false;
    684                 }
    685                 mInstanceLock.wait(waitTimeMillis);
    686             } catch (InterruptedException ie) {
    687                 /* ignore */
    688             }
    689         }
    690     }
    691 
    692     /**
    693      * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
    694      *
    695      * @param info The info.
    696      * @param connectionId The id of the connection to the system.
    697      */
    698     private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
    699             int connectionId) {
    700         if (info != null) {
    701             info.setConnectionId(connectionId);
    702             info.setSealed(true);
    703             sAccessibilityCache.add(info);
    704         }
    705     }
    706 
    707     /**
    708      * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
    709      *
    710      * @param infos The {@link AccessibilityNodeInfo}s.
    711      * @param connectionId The id of the connection to the system.
    712      */
    713     private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
    714             int connectionId) {
    715         if (infos != null) {
    716             final int infosCount = infos.size();
    717             for (int i = 0; i < infosCount; i++) {
    718                 AccessibilityNodeInfo info = infos.get(i);
    719                 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
    720             }
    721         }
    722     }
    723 
    724     /**
    725      * Gets the message stored if the interacted and interacting
    726      * threads are the same.
    727      *
    728      * @return The message.
    729      */
    730     private Message getSameProcessMessageAndClear() {
    731         synchronized (mInstanceLock) {
    732             Message result = mSameThreadMessage;
    733             mSameThreadMessage = null;
    734             return result;
    735         }
    736     }
    737 
    738     /**
    739      * Gets a cached accessibility service connection.
    740      *
    741      * @param connectionId The connection id.
    742      * @return The cached connection if such.
    743      */
    744     public IAccessibilityServiceConnection getConnection(int connectionId) {
    745         synchronized (sConnectionCache) {
    746             return sConnectionCache.get(connectionId);
    747         }
    748     }
    749 
    750     /**
    751      * Adds a cached accessibility service connection.
    752      *
    753      * @param connectionId The connection id.
    754      * @param connection The connection.
    755      */
    756     public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
    757         synchronized (sConnectionCache) {
    758             sConnectionCache.put(connectionId, connection);
    759         }
    760     }
    761 
    762     /**
    763      * Removes a cached accessibility service connection.
    764      *
    765      * @param connectionId The connection id.
    766      */
    767     public void removeConnection(int connectionId) {
    768         synchronized (sConnectionCache) {
    769             sConnectionCache.remove(connectionId);
    770         }
    771     }
    772 
    773     /**
    774      * Checks whether the infos are a fully connected tree with no duplicates.
    775      *
    776      * @param infos The result list to check.
    777      */
    778     private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
    779         if (infos.size() == 0) {
    780             return;
    781         }
    782         // Find the root node.
    783         AccessibilityNodeInfo root = infos.get(0);
    784         final int infoCount = infos.size();
    785         for (int i = 1; i < infoCount; i++) {
    786             for (int j = i; j < infoCount; j++) {
    787                 AccessibilityNodeInfo candidate = infos.get(j);
    788                 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
    789                     root = candidate;
    790                     break;
    791                 }
    792             }
    793         }
    794         if (root == null) {
    795             Log.e(LOG_TAG, "No root.");
    796         }
    797         // Check for duplicates.
    798         HashSet<AccessibilityNodeInfo> seen = new HashSet<>();
    799         Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
    800         fringe.add(root);
    801         while (!fringe.isEmpty()) {
    802             AccessibilityNodeInfo current = fringe.poll();
    803             if (!seen.add(current)) {
    804                 Log.e(LOG_TAG, "Duplicate node.");
    805                 return;
    806             }
    807             final int childCount = current.getChildCount();
    808             for (int i = 0; i < childCount; i++) {
    809                 final long childId = current.getChildId(i);
    810                 for (int j = 0; j < infoCount; j++) {
    811                     AccessibilityNodeInfo child = infos.get(j);
    812                     if (child.getSourceNodeId() == childId) {
    813                         fringe.add(child);
    814                     }
    815                 }
    816             }
    817         }
    818         final int disconnectedCount = infos.size() - seen.size();
    819         if (disconnectedCount > 0) {
    820             Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
    821         }
    822     }
    823 }
    824