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