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