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.Rect;
     21 import android.os.Message;
     22 import android.os.RemoteException;
     23 import android.os.SystemClock;
     24 import android.util.Log;
     25 import android.util.SparseArray;
     26 
     27 import java.util.Collections;
     28 import java.util.List;
     29 import java.util.concurrent.atomic.AtomicInteger;
     30 
     31 /**
     32  * This class is a singleton that performs accessibility interaction
     33  * which is it queries remote view hierarchies about snapshots of their
     34  * views as well requests from these hierarchies to perform certain
     35  * actions on their views.
     36  *
     37  * Rationale: The content retrieval APIs are synchronous from a client's
     38  *     perspective but internally they are asynchronous. The client thread
     39  *     calls into the system requesting an action and providing a callback
     40  *     to receive the result after which it waits up to a timeout for that
     41  *     result. The system enforces security and the delegates the request
     42  *     to a given view hierarchy where a message is posted (from a binder
     43  *     thread) describing what to be performed by the main UI thread the
     44  *     result of which it delivered via the mentioned callback. However,
     45  *     the blocked client thread and the main UI thread of the target view
     46  *     hierarchy can be the same thread, for example an accessibility service
     47  *     and an activity run in the same process, thus they are executed on the
     48  *     same main thread. In such a case the retrieval will fail since the UI
     49  *     thread that has to process the message describing the work to be done
     50  *     is blocked waiting for a result is has to compute! To avoid this scenario
     51  *     when making a call the client also passes its process and thread ids so
     52  *     the accessed view hierarchy can detect if the client making the request
     53  *     is running in its main UI thread. In such a case the view hierarchy,
     54  *     specifically the binder thread performing the IPC to it, does not post a
     55  *     message to be run on the UI thread but passes it to the singleton
     56  *     interaction client through which all interactions occur and the latter is
     57  *     responsible to execute the message before starting to wait for the
     58  *     asynchronous result delivered via the callback. In this case the expected
     59  *     result is already received so no waiting is performed.
     60  *
     61  * @hide
     62  */
     63 public final class AccessibilityInteractionClient
     64         extends IAccessibilityInteractionConnectionCallback.Stub {
     65 
     66     public static final int NO_ID = -1;
     67 
     68     private static final String LOG_TAG = "AccessibilityInteractionClient";
     69 
     70     private static final boolean DEBUG = false;
     71 
     72     private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
     73 
     74     private static final Object sStaticLock = new Object();
     75 
     76     private static AccessibilityInteractionClient sInstance;
     77 
     78     private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
     79 
     80     private final Object mInstanceLock = new Object();
     81 
     82     private int mInteractionId = -1;
     83 
     84     private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
     85 
     86     private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult;
     87 
     88     private boolean mPerformAccessibilityActionResult;
     89 
     90     private Message mSameThreadMessage;
     91 
     92     private final Rect mTempBounds = new Rect();
     93 
     94     // The connection cache is shared between all interrogating threads.
     95     private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
     96         new SparseArray<IAccessibilityServiceConnection>();
     97 
     98     /**
     99      * @return The singleton of this class.
    100      */
    101     public static AccessibilityInteractionClient getInstance() {
    102         synchronized (sStaticLock) {
    103             if (sInstance == null) {
    104                 sInstance = new AccessibilityInteractionClient();
    105             }
    106             return sInstance;
    107         }
    108     }
    109 
    110     /**
    111      * Sets the message to be processed if the interacted view hierarchy
    112      * and the interacting client are running in the same thread.
    113      *
    114      * @param message The message.
    115      */
    116     public void setSameThreadMessage(Message message) {
    117         synchronized (mInstanceLock) {
    118             mSameThreadMessage = message;
    119             mInstanceLock.notifyAll();
    120         }
    121     }
    122 
    123     /**
    124      * Finds an {@link AccessibilityNodeInfo} by accessibility id.
    125      *
    126      * @param connectionId The id of a connection for interacting with the system.
    127      * @param accessibilityWindowId A unique window id.
    128      * @param accessibilityViewId A unique View accessibility id.
    129      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
    130      */
    131     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
    132             int accessibilityWindowId, int accessibilityViewId) {
    133         try {
    134             IAccessibilityServiceConnection connection = getConnection(connectionId);
    135             if (connection != null) {
    136                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    137                 final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId(
    138                         accessibilityWindowId, accessibilityViewId, interactionId, this,
    139                         Thread.currentThread().getId());
    140                 // If the scale is zero the call has failed.
    141                 if (windowScale > 0) {
    142                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
    143                             interactionId);
    144                     finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
    145                     return info;
    146                 }
    147             } else {
    148                 if (DEBUG) {
    149                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    150                 }
    151             }
    152         } catch (RemoteException re) {
    153             if (DEBUG) {
    154                 Log.w(LOG_TAG, "Error while calling remote"
    155                         + " findAccessibilityNodeInfoByAccessibilityId", re);
    156             }
    157         }
    158         return null;
    159     }
    160 
    161     /**
    162      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed
    163      * in the currently active window and starts from the root View in the window.
    164      *
    165      * @param connectionId The id of a connection for interacting with the system.
    166      * @param viewId The id of the view.
    167      * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
    168      */
    169     public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId,
    170             int viewId) {
    171         try {
    172             IAccessibilityServiceConnection connection = getConnection(connectionId);
    173             if (connection != null) {
    174                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    175                 final float windowScale =
    176                     connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId,
    177                             interactionId, this, Thread.currentThread().getId());
    178                 // If the scale is zero the call has failed.
    179                 if (windowScale > 0) {
    180                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
    181                             interactionId);
    182                     finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
    183                     return info;
    184                 }
    185             } else {
    186                 if (DEBUG) {
    187                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    188                 }
    189             }
    190         } catch (RemoteException re) {
    191             if (DEBUG) {
    192                 Log.w(LOG_TAG, "Error while calling remote"
    193                         + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
    194             }
    195         }
    196         return null;
    197     }
    198 
    199     /**
    200      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
    201      * insensitive containment. The search is performed in the currently
    202      * active window and starts from the root View in the window.
    203      *
    204      * @param connectionId The id of a connection for interacting with the system.
    205      * @param text The searched text.
    206      * @return A list of found {@link AccessibilityNodeInfo}s.
    207      */
    208     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow(
    209             int connectionId, String text) {
    210         try {
    211             IAccessibilityServiceConnection connection = getConnection(connectionId);
    212             if (connection != null) {
    213                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    214                 final float windowScale =
    215                     connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text,
    216                             interactionId, this, Thread.currentThread().getId());
    217                 // If the scale is zero the call has failed.
    218                 if (windowScale > 0) {
    219                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    220                             interactionId);
    221                     finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
    222                     return infos;
    223                 }
    224             } else {
    225                 if (DEBUG) {
    226                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    227                 }
    228             }
    229         } catch (RemoteException re) {
    230             if (DEBUG) {
    231                 Log.w(LOG_TAG, "Error while calling remote"
    232                         + " findAccessibilityNodeInfosByViewTextInActiveWindow", re);
    233             }
    234         }
    235         return null;
    236     }
    237 
    238     /**
    239      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
    240      * insensitive containment. The search is performed in the window whose
    241      * id is specified and starts from the View whose accessibility id is
    242      * specified.
    243      *
    244      * @param connectionId The id of a connection for interacting with the system.
    245      * @param text The searched text.
    246      * @param accessibilityWindowId A unique window id.
    247      * @param accessibilityViewId A unique View accessibility id from where to start the search.
    248      *        Use {@link android.view.View#NO_ID} to start from the root.
    249      * @return A list of found {@link AccessibilityNodeInfo}s.
    250      */
    251     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId,
    252             String text, int accessibilityWindowId, int accessibilityViewId) {
    253         try {
    254             IAccessibilityServiceConnection connection = getConnection(connectionId);
    255             if (connection != null) {
    256                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    257                 final float windowScale = connection.findAccessibilityNodeInfosByViewText(text,
    258                         accessibilityWindowId, accessibilityViewId, interactionId, this,
    259                         Thread.currentThread().getId());
    260                 // If the scale is zero the call has failed.
    261                 if (windowScale > 0) {
    262                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
    263                             interactionId);
    264                     finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
    265                     return infos;
    266                 }
    267             } else {
    268                 if (DEBUG) {
    269                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    270                 }
    271             }
    272         } catch (RemoteException re) {
    273             if (DEBUG) {
    274                 Log.w(LOG_TAG, "Error while calling remote"
    275                         + " findAccessibilityNodeInfosByViewText", re);
    276             }
    277         }
    278         return Collections.emptyList();
    279     }
    280 
    281     /**
    282      * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
    283      *
    284      * @param connectionId The id of a connection for interacting with the system.
    285      * @param accessibilityWindowId The id of the window.
    286      * @param accessibilityViewId A unique View accessibility id.
    287      * @param action The action to perform.
    288      * @return Whether the action was performed.
    289      */
    290     public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
    291             int accessibilityViewId, int action) {
    292         try {
    293             IAccessibilityServiceConnection connection = getConnection(connectionId);
    294             if (connection != null) {
    295                 final int interactionId = mInteractionIdCounter.getAndIncrement();
    296                 final boolean success = connection.performAccessibilityAction(
    297                         accessibilityWindowId, accessibilityViewId, action, interactionId, this,
    298                         Thread.currentThread().getId());
    299                 if (success) {
    300                     return getPerformAccessibilityActionResult(interactionId);
    301                 }
    302             } else {
    303                 if (DEBUG) {
    304                     Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
    305                 }
    306             }
    307         } catch (RemoteException re) {
    308             if (DEBUG) {
    309                 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
    310             }
    311         }
    312         return false;
    313     }
    314 
    315     /**
    316      * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
    317      *
    318      * @param interactionId The interaction id to match the result with the request.
    319      * @return The result {@link AccessibilityNodeInfo}.
    320      */
    321     private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
    322         synchronized (mInstanceLock) {
    323             final boolean success = waitForResultTimedLocked(interactionId);
    324             AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
    325             clearResultLocked();
    326             return result;
    327         }
    328     }
    329 
    330     /**
    331      * {@inheritDoc}
    332      */
    333     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
    334                 int interactionId) {
    335         synchronized (mInstanceLock) {
    336             if (interactionId > mInteractionId) {
    337                 mFindAccessibilityNodeInfoResult = info;
    338                 mInteractionId = interactionId;
    339             }
    340             mInstanceLock.notifyAll();
    341         }
    342     }
    343 
    344     /**
    345      * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
    346      *
    347      * @param interactionId The interaction id to match the result with the request.
    348      * @return The result {@link AccessibilityNodeInfo}s.
    349      */
    350     private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
    351                 int interactionId) {
    352         synchronized (mInstanceLock) {
    353             final boolean success = waitForResultTimedLocked(interactionId);
    354             List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null;
    355             clearResultLocked();
    356             return result;
    357         }
    358     }
    359 
    360     /**
    361      * {@inheritDoc}
    362      */
    363     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
    364                 int interactionId) {
    365         synchronized (mInstanceLock) {
    366             if (interactionId > mInteractionId) {
    367                 mFindAccessibilityNodeInfosResult = infos;
    368                 mInteractionId = interactionId;
    369             }
    370             mInstanceLock.notifyAll();
    371         }
    372     }
    373 
    374     /**
    375      * Gets the result of a request to perform an accessibility action.
    376      *
    377      * @param interactionId The interaction id to match the result with the request.
    378      * @return Whether the action was performed.
    379      */
    380     private boolean getPerformAccessibilityActionResult(int interactionId) {
    381         synchronized (mInstanceLock) {
    382             final boolean success = waitForResultTimedLocked(interactionId);
    383             final boolean result = success ? mPerformAccessibilityActionResult : false;
    384             clearResultLocked();
    385             return result;
    386         }
    387     }
    388 
    389     /**
    390      * {@inheritDoc}
    391      */
    392     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
    393         synchronized (mInstanceLock) {
    394             if (interactionId > mInteractionId) {
    395                 mPerformAccessibilityActionResult = succeeded;
    396                 mInteractionId = interactionId;
    397             }
    398             mInstanceLock.notifyAll();
    399         }
    400     }
    401 
    402     /**
    403      * Clears the result state.
    404      */
    405     private void clearResultLocked() {
    406         mInteractionId = -1;
    407         mFindAccessibilityNodeInfoResult = null;
    408         mFindAccessibilityNodeInfosResult = null;
    409         mPerformAccessibilityActionResult = false;
    410     }
    411 
    412     /**
    413      * Waits up to a given bound for a result of a request and returns it.
    414      *
    415      * @param interactionId The interaction id to match the result with the request.
    416      * @return Whether the result was received.
    417      */
    418     private boolean waitForResultTimedLocked(int interactionId) {
    419         long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
    420         final long startTimeMillis = SystemClock.uptimeMillis();
    421         while (true) {
    422             try {
    423                 Message sameProcessMessage = getSameProcessMessageAndClear();
    424                 if (sameProcessMessage != null) {
    425                     sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
    426                 }
    427 
    428                 if (mInteractionId == interactionId) {
    429                     return true;
    430                 }
    431                 if (mInteractionId > interactionId) {
    432                     return false;
    433                 }
    434                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
    435                 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
    436                 if (waitTimeMillis <= 0) {
    437                     return false;
    438                 }
    439                 mInstanceLock.wait(waitTimeMillis);
    440             } catch (InterruptedException ie) {
    441                 /* ignore */
    442             }
    443         }
    444     }
    445 
    446     /**
    447      * Applies compatibility scale to the info bounds if it is not equal to one.
    448      *
    449      * @param info The info whose bounds to scale.
    450      * @param scale The scale to apply.
    451      */
    452     private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) {
    453         if (scale == 1.0f) {
    454             return;
    455         }
    456         Rect bounds = mTempBounds;
    457         info.getBoundsInParent(bounds);
    458         bounds.scale(scale);
    459         info.setBoundsInParent(bounds);
    460 
    461         info.getBoundsInScreen(bounds);
    462         bounds.scale(scale);
    463         info.setBoundsInScreen(bounds);
    464     }
    465 
    466     /**
    467      * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
    468      *
    469      * @param info The info.
    470      * @param connectionId The id of the connection to the system.
    471      * @param windowScale The source window compatibility scale.
    472      */
    473     private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId,
    474             float windowScale) {
    475         if (info != null) {
    476             applyCompatibilityScaleIfNeeded(info, windowScale);
    477             info.setConnectionId(connectionId);
    478             info.setSealed(true);
    479         }
    480     }
    481 
    482     /**
    483      * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
    484      *
    485      * @param infos The {@link AccessibilityNodeInfo}s.
    486      * @param connectionId The id of the connection to the system.
    487      * @param windowScale The source window compatibility scale.
    488      */
    489     private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
    490             int connectionId, float windowScale) {
    491         if (infos != null) {
    492             final int infosCount = infos.size();
    493             for (int i = 0; i < infosCount; i++) {
    494                 AccessibilityNodeInfo info = infos.get(i);
    495                 finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
    496             }
    497         }
    498     }
    499 
    500     /**
    501      * Gets the message stored if the interacted and interacting
    502      * threads are the same.
    503      *
    504      * @return The message.
    505      */
    506     private Message getSameProcessMessageAndClear() {
    507         synchronized (mInstanceLock) {
    508             Message result = mSameThreadMessage;
    509             mSameThreadMessage = null;
    510             return result;
    511         }
    512     }
    513 
    514     /**
    515      * Gets a cached accessibility service connection.
    516      *
    517      * @param connectionId The connection id.
    518      * @return The cached connection if such.
    519      */
    520     public IAccessibilityServiceConnection getConnection(int connectionId) {
    521         synchronized (sConnectionCache) {
    522             return sConnectionCache.get(connectionId);
    523         }
    524     }
    525 
    526     /**
    527      * Adds a cached accessibility service connection.
    528      *
    529      * @param connectionId The connection id.
    530      * @param connection The connection.
    531      */
    532     public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
    533         synchronized (sConnectionCache) {
    534             sConnectionCache.put(connectionId, connection);
    535         }
    536     }
    537 
    538     /**
    539      * Removes a cached accessibility service connection.
    540      *
    541      * @param connectionId The connection id.
    542      */
    543     public void removeConnection(int connectionId) {
    544         synchronized (sConnectionCache) {
    545             sConnectionCache.remove(connectionId);
    546         }
    547     }
    548 }
    549