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