Home | History | Annotate | Download | only in accessibilityservice
      1 /*
      2  * Copyright (C) 2012 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.accessibilityservice;
     18 
     19 import android.accessibilityservice.AccessibilityService.Callbacks;
     20 import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper;
     21 import android.content.Context;
     22 import android.os.Bundle;
     23 import android.os.HandlerThread;
     24 import android.os.Looper;
     25 import android.os.RemoteException;
     26 import android.os.ServiceManager;
     27 import android.os.SystemClock;
     28 import android.util.Log;
     29 import android.view.accessibility.AccessibilityEvent;
     30 import android.view.accessibility.AccessibilityInteractionClient;
     31 import android.view.accessibility.AccessibilityNodeInfo;
     32 import android.view.accessibility.IAccessibilityManager;
     33 
     34 import com.android.internal.util.Predicate;
     35 
     36 import java.util.List;
     37 import java.util.concurrent.TimeoutException;
     38 
     39 /**
     40  * This class represents a bridge that can be used for UI test
     41  * automation. It is responsible for connecting to the system,
     42  * keeping track of the last accessibility event, and exposing
     43  * window content querying APIs. This class is designed to be
     44  * used from both an Android application and a Java program
     45  * run from the shell.
     46  *
     47  * @hide
     48  */
     49 public class UiTestAutomationBridge {
     50 
     51     private static final String LOG_TAG = UiTestAutomationBridge.class.getSimpleName();
     52 
     53     private static final int TIMEOUT_REGISTER_SERVICE = 5000;
     54 
     55     public static final int ACTIVE_WINDOW_ID = AccessibilityNodeInfo.ACTIVE_WINDOW_ID;
     56 
     57     public static final long ROOT_NODE_ID = AccessibilityNodeInfo.ROOT_NODE_ID;
     58 
     59     public static final int UNDEFINED = -1;
     60 
     61     private static final int FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS =
     62         AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS
     63         | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS
     64         | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS;
     65 
     66     private final Object mLock = new Object();
     67 
     68     private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID;
     69 
     70     private IAccessibilityServiceClientWrapper mListener;
     71 
     72     private AccessibilityEvent mLastEvent;
     73 
     74     private volatile boolean mWaitingForEventDelivery;
     75 
     76     private volatile boolean mUnprocessedEventAvailable;
     77 
     78     private HandlerThread mHandlerThread;
     79 
     80     /**
     81      * Gets the last received {@link AccessibilityEvent}.
     82      *
     83      * @return The event.
     84      */
     85     public AccessibilityEvent getLastAccessibilityEvent() {
     86         return mLastEvent;
     87     }
     88 
     89     /**
     90      * Callback for receiving an {@link AccessibilityEvent}.
     91      *
     92      * <strong>Note:</strong> This method is <strong>NOT</strong>
     93      * executed on the application main thread. The client are
     94      * responsible for proper synchronization.
     95      *
     96      * @param event The received event.
     97      */
     98     public void onAccessibilityEvent(AccessibilityEvent event) {
     99         /* hook - do nothing */
    100     }
    101 
    102     /**
    103      * Callback for requests to stop feedback.
    104      *
    105      * <strong>Note:</strong> This method is <strong>NOT</strong>
    106      * executed on the application main thread. The client are
    107      * responsible for proper synchronization.
    108      */
    109     public void onInterrupt() {
    110         /* hook - do nothing */
    111     }
    112 
    113     /**
    114      * Connects this service.
    115      *
    116      * @throws IllegalStateException If already connected.
    117      */
    118     public void connect() {
    119         if (isConnected()) {
    120             throw new IllegalStateException("Already connected.");
    121         }
    122 
    123         // Serialize binder calls to a handler on a dedicated thread
    124         // different from the main since we expose APIs that block
    125         // the main thread waiting for a result the deliver of which
    126         // on the main thread will prevent that thread from waking up.
    127         // The serialization is needed also to ensure that events are
    128         // examined in delivery order. Otherwise, a fair locking
    129         // is needed for making sure the binder calls are interleaved
    130         // with check for the expected event and also to make sure the
    131         // binder threads are allowed to proceed in the received order.
    132         mHandlerThread = new HandlerThread("UiTestAutomationBridge");
    133         mHandlerThread.setDaemon(true);
    134         mHandlerThread.start();
    135         Looper looper = mHandlerThread.getLooper();
    136 
    137         mListener = new IAccessibilityServiceClientWrapper(null, looper, new Callbacks() {
    138             @Override
    139             public void onServiceConnected() {
    140                 /* do nothing */
    141             }
    142 
    143             @Override
    144             public void onInterrupt() {
    145                 UiTestAutomationBridge.this.onInterrupt();
    146             }
    147 
    148             @Override
    149             public void onAccessibilityEvent(AccessibilityEvent event) {
    150                 synchronized (mLock) {
    151                     while (true) {
    152                         mLastEvent = AccessibilityEvent.obtain(event);
    153                         if (!mWaitingForEventDelivery) {
    154                             mLock.notifyAll();
    155                             break;
    156                         }
    157                         if (!mUnprocessedEventAvailable) {
    158                             mUnprocessedEventAvailable = true;
    159                             mLock.notifyAll();
    160                             break;
    161                         }
    162                         try {
    163                             mLock.wait();
    164                         } catch (InterruptedException ie) {
    165                             /* ignore */
    166                         }
    167                     }
    168                 }
    169                 UiTestAutomationBridge.this.onAccessibilityEvent(event);
    170             }
    171 
    172             @Override
    173             public void onSetConnectionId(int connectionId) {
    174                 synchronized (mLock) {
    175                     mConnectionId = connectionId;
    176                     mLock.notifyAll();
    177                 }
    178             }
    179 
    180             @Override
    181             public boolean onGesture(int gestureId) {
    182                 return false;
    183             }
    184         });
    185 
    186         final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
    187                 ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
    188 
    189         final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
    190         info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    191         info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
    192         info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
    193 
    194         try {
    195             manager.registerUiTestAutomationService(mListener, info);
    196         } catch (RemoteException re) {
    197             throw new IllegalStateException("Cound not register UiAutomationService.", re);
    198         }
    199 
    200         synchronized (mLock) {
    201             final long startTimeMillis = SystemClock.uptimeMillis();
    202             while (true) {
    203                 if (isConnected()) {
    204                     return;
    205                 }
    206                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
    207                 final long remainingTimeMillis = TIMEOUT_REGISTER_SERVICE - elapsedTimeMillis;
    208                 if (remainingTimeMillis <= 0) {
    209                     throw new IllegalStateException("Cound not register UiAutomationService.");
    210                 }
    211                 try {
    212                     mLock.wait(remainingTimeMillis);
    213                 } catch (InterruptedException ie) {
    214                     /* ignore */
    215                 }
    216             }
    217         }
    218     }
    219 
    220     /**
    221      * Disconnects this service.
    222      *
    223      * @throws IllegalStateException If already disconnected.
    224      */
    225     public void disconnect() {
    226         if (!isConnected()) {
    227             throw new IllegalStateException("Already disconnected.");
    228         }
    229 
    230         mHandlerThread.quit();
    231 
    232         IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
    233               ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
    234 
    235         try {
    236             manager.unregisterUiTestAutomationService(mListener);
    237         } catch (RemoteException re) {
    238             Log.e(LOG_TAG, "Error while unregistering UiTestAutomationService", re);
    239         }
    240     }
    241 
    242     /**
    243      * Gets whether this service is connected.
    244      *
    245      * @return True if connected.
    246      */
    247     public boolean isConnected() {
    248         return (mConnectionId != AccessibilityInteractionClient.NO_ID);
    249     }
    250 
    251     /**
    252      * Executes a command and waits for a specific accessibility event type up
    253      * to a given timeout.
    254      *
    255      * @param command The command to execute before starting to wait for the event.
    256      * @param predicate Predicate for recognizing the awaited event.
    257      * @param timeoutMillis The max wait time in milliseconds.
    258      */
    259     public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command,
    260             Predicate<AccessibilityEvent> predicate, long timeoutMillis)
    261             throws TimeoutException, Exception {
    262         // TODO: This is broken - remove from here when finalizing this as public APIs.
    263         synchronized (mLock) {
    264             // Prepare to wait for an event.
    265             mWaitingForEventDelivery = true;
    266             mUnprocessedEventAvailable = false;
    267             if (mLastEvent != null) {
    268                 mLastEvent.recycle();
    269                 mLastEvent = null;
    270             }
    271             // Execute the command.
    272             command.run();
    273             // Wait for the event.
    274             final long startTimeMillis = SystemClock.uptimeMillis();
    275             while (true) {
    276                 // If the expected event is received, that's it.
    277                 if ((mUnprocessedEventAvailable && predicate.apply(mLastEvent))) {
    278                     mWaitingForEventDelivery = false;
    279                     mUnprocessedEventAvailable = false;
    280                     mLock.notifyAll();
    281                     return mLastEvent;
    282                 }
    283                 // Ask for another event.
    284                 mWaitingForEventDelivery = true;
    285                 mUnprocessedEventAvailable = false;
    286                 mLock.notifyAll();
    287                 // Check if timed out and if not wait.
    288                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
    289                 final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
    290                 if (remainingTimeMillis <= 0) {
    291                     mWaitingForEventDelivery = false;
    292                     mUnprocessedEventAvailable = false;
    293                     mLock.notifyAll();
    294                     throw new TimeoutException("Expacted event not received within: "
    295                             + timeoutMillis + " ms.");
    296                 }
    297                 try {
    298                     mLock.wait(remainingTimeMillis);
    299                 } catch (InterruptedException ie) {
    300                     /* ignore */
    301                 }
    302             }
    303         }
    304     }
    305 
    306     /**
    307      * Waits for the accessibility event stream to become idle, which is not to
    308      * have received a new accessibility event within <code>idleTimeout</code>,
    309      * and do so within a maximal global timeout as specified by
    310      * <code>globalTimeout</code>.
    311      *
    312      * @param idleTimeout The timeout between two event to consider the device idle.
    313      * @param globalTimeout The maximal global timeout in which to wait for idle.
    314      */
    315     public void waitForIdle(long idleTimeout, long globalTimeout) {
    316         final long startTimeMillis = SystemClock.uptimeMillis();
    317         long lastEventTime = (mLastEvent != null)
    318                 ? mLastEvent.getEventTime() : SystemClock.uptimeMillis();
    319         synchronized (mLock) {
    320             while (true) {
    321                 final long currentTimeMillis = SystemClock.uptimeMillis();
    322                 final long sinceLastEventTimeMillis = currentTimeMillis - lastEventTime;
    323                 if (sinceLastEventTimeMillis > idleTimeout) {
    324                     return;
    325                 }
    326                 if (mLastEvent != null) {
    327                     lastEventTime = mLastEvent.getEventTime();
    328                 }
    329                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
    330                 final long remainingTimeMillis = globalTimeout - elapsedTimeMillis;
    331                 if (remainingTimeMillis <= 0) {
    332                     return;
    333                 }
    334                 try {
    335                      mLock.wait(idleTimeout);
    336                 } catch (InterruptedException e) {
    337                      /* ignore */
    338                 }
    339             }
    340         }
    341     }
    342 
    343     /**
    344      * Finds an {@link AccessibilityNodeInfo} by accessibility id in the active
    345      * window. The search is performed from the root node.
    346      *
    347      * @param accessibilityNodeId A unique view id or virtual descendant id for
    348      *     which to search.
    349      * @return The current window scale, where zero means a failure.
    350      */
    351     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityIdInActiveWindow(
    352             long accessibilityNodeId) {
    353         return findAccessibilityNodeInfoByAccessibilityId(ACTIVE_WINDOW_ID, accessibilityNodeId);
    354     }
    355 
    356     /**
    357      * Finds an {@link AccessibilityNodeInfo} by accessibility id.
    358      *
    359      * @param accessibilityWindowId A unique window id. Use {@link #ACTIVE_WINDOW_ID} to query
    360      *     the currently active window.
    361      * @param accessibilityNodeId A unique view id or virtual descendant id for
    362      *     which to search.
    363      * @return The current window scale, where zero means a failure.
    364      */
    365     public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(
    366             int accessibilityWindowId, long accessibilityNodeId) {
    367         // Cache the id to avoid locking
    368         final int connectionId = mConnectionId;
    369         ensureValidConnection(connectionId);
    370         return AccessibilityInteractionClient.getInstance()
    371                 .findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
    372                         accessibilityWindowId, accessibilityNodeId,
    373                         FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS);
    374     }
    375 
    376     /**
    377      * Finds an {@link AccessibilityNodeInfo} by View id in the active
    378      * window. The search is performed from the root node.
    379      *
    380      * @param viewId The id of a View.
    381      * @return The current window scale, where zero means a failure.
    382      */
    383     public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId) {
    384         return findAccessibilityNodeInfoByViewId(ACTIVE_WINDOW_ID, ROOT_NODE_ID, viewId);
    385     }
    386 
    387     /**
    388      * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
    389      * the window whose id is specified and starts from the node whose accessibility
    390      * id is specified.
    391      *
    392      * @param accessibilityWindowId A unique window id. Use
    393      *     {@link  #ACTIVE_WINDOW_ID} to query the currently active window.
    394      * @param accessibilityNodeId A unique view id or virtual descendant id from
    395      *     where to start the search. Use {@link  #ROOT_NODE_ID} to start from the root.
    396      * @param viewId The id of a View.
    397      * @return The current window scale, where zero means a failure.
    398      */
    399     public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int accessibilityWindowId,
    400             long accessibilityNodeId, int viewId) {
    401         // Cache the id to avoid locking
    402         final int connectionId = mConnectionId;
    403         ensureValidConnection(connectionId);
    404         return AccessibilityInteractionClient.getInstance()
    405                 .findAccessibilityNodeInfoByViewId(connectionId, accessibilityWindowId,
    406                         accessibilityNodeId, viewId);
    407     }
    408 
    409     /**
    410      * Finds {@link AccessibilityNodeInfo}s by View text in the active
    411      * window. The search is performed from the root node.
    412      *
    413      * @param text The searched text.
    414      * @return The current window scale, where zero means a failure.
    415      */
    416     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(String text) {
    417         return findAccessibilityNodeInfosByText(ACTIVE_WINDOW_ID, ROOT_NODE_ID, text);
    418     }
    419 
    420     /**
    421      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
    422      * insensitive containment. The search is performed in the window whose
    423      * id is specified and starts from the node whose accessibility id is
    424      * specified.
    425      *
    426      * @param accessibilityWindowId A unique window id. Use
    427      *     {@link #ACTIVE_WINDOW_ID} to query the currently active window.
    428      * @param accessibilityNodeId A unique view id or virtual descendant id from
    429      *     where to start the search. Use {@link #ROOT_NODE_ID} to start from the root.
    430      * @param text The searched text.
    431      * @return The current window scale, where zero means a failure.
    432      */
    433     public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int accessibilityWindowId,
    434             long accessibilityNodeId, String text) {
    435         // Cache the id to avoid locking
    436         final int connectionId = mConnectionId;
    437         ensureValidConnection(connectionId);
    438         return AccessibilityInteractionClient.getInstance()
    439                 .findAccessibilityNodeInfosByText(connectionId, accessibilityWindowId,
    440                         accessibilityNodeId, text);
    441     }
    442 
    443     /**
    444      * Performs an accessibility action on an {@link AccessibilityNodeInfo}
    445      * in the active window.
    446      *
    447      * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id).
    448      * @param action The action to perform.
    449      * @param arguments Optional action arguments.
    450      * @return Whether the action was performed.
    451      */
    452     public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action,
    453             Bundle arguments) {
    454         return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action, arguments);
    455     }
    456 
    457     /**
    458      * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
    459      *
    460      * @param accessibilityWindowId A unique window id. Use
    461      *     {@link #ACTIVE_WINDOW_ID} to query the currently active window.
    462      * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id).
    463      * @param action The action to perform.
    464      * @param arguments Optional action arguments.
    465      * @return Whether the action was performed.
    466      */
    467     public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId,
    468             int action, Bundle arguments) {
    469         // Cache the id to avoid locking
    470         final int connectionId = mConnectionId;
    471         ensureValidConnection(connectionId);
    472         return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId,
    473                 accessibilityWindowId, accessibilityNodeId, action, arguments);
    474     }
    475 
    476     /**
    477      * Gets the root {@link AccessibilityNodeInfo} in the active window.
    478      *
    479      * @return The root info.
    480      */
    481     public AccessibilityNodeInfo getRootAccessibilityNodeInfoInActiveWindow() {
    482         // Cache the id to avoid locking
    483         final int connectionId = mConnectionId;
    484         ensureValidConnection(connectionId);
    485         return AccessibilityInteractionClient.getInstance()
    486                 .findAccessibilityNodeInfoByAccessibilityId(connectionId, ACTIVE_WINDOW_ID,
    487                         ROOT_NODE_ID, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
    488     }
    489 
    490     private void ensureValidConnection(int connectionId) {
    491         if (connectionId == UNDEFINED) {
    492             throw new IllegalStateException("UiAutomationService not connected."
    493                     + " Did you call #register()?");
    494         }
    495     }
    496 }
    497