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