/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.accessibilityservice; import android.accessibilityservice.AccessibilityService.Callbacks; import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; import android.content.Context; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityManager; import com.android.internal.util.Predicate; import java.util.List; import java.util.concurrent.TimeoutException; /** * This class represents a bridge that can be used for UI test * automation. It is responsible for connecting to the system, * keeping track of the last accessibility event, and exposing * window content querying APIs. This class is designed to be * used from both an Android application and a Java program * run from the shell. * * @hide */ public class UiTestAutomationBridge { private static final String LOG_TAG = UiTestAutomationBridge.class.getSimpleName(); private static final int TIMEOUT_REGISTER_SERVICE = 5000; public static final int ACTIVE_WINDOW_ID = AccessibilityNodeInfo.ACTIVE_WINDOW_ID; public static final long ROOT_NODE_ID = AccessibilityNodeInfo.ROOT_NODE_ID; public static final int UNDEFINED = -1; private static final int FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS = AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS; private final Object mLock = new Object(); private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; private IAccessibilityServiceClientWrapper mListener; private AccessibilityEvent mLastEvent; private volatile boolean mWaitingForEventDelivery; private volatile boolean mUnprocessedEventAvailable; private HandlerThread mHandlerThread; /** * Gets the last received {@link AccessibilityEvent}. * * @return The event. */ public AccessibilityEvent getLastAccessibilityEvent() { return mLastEvent; } /** * Callback for receiving an {@link AccessibilityEvent}. * * Note: This method is NOT * executed on the application main thread. The client are * responsible for proper synchronization. * * @param event The received event. */ public void onAccessibilityEvent(AccessibilityEvent event) { /* hook - do nothing */ } /** * Callback for requests to stop feedback. * * Note: This method is NOT * executed on the application main thread. The client are * responsible for proper synchronization. */ public void onInterrupt() { /* hook - do nothing */ } /** * Connects this service. * * @throws IllegalStateException If already connected. */ public void connect() { if (isConnected()) { throw new IllegalStateException("Already connected."); } // Serialize binder calls to a handler on a dedicated thread // different from the main since we expose APIs that block // the main thread waiting for a result the deliver of which // on the main thread will prevent that thread from waking up. // The serialization is needed also to ensure that events are // examined in delivery order. Otherwise, a fair locking // is needed for making sure the binder calls are interleaved // with check for the expected event and also to make sure the // binder threads are allowed to proceed in the received order. mHandlerThread = new HandlerThread("UiTestAutomationBridge"); mHandlerThread.setDaemon(true); mHandlerThread.start(); Looper looper = mHandlerThread.getLooper(); mListener = new IAccessibilityServiceClientWrapper(null, looper, new Callbacks() { @Override public void onServiceConnected() { /* do nothing */ } @Override public void onInterrupt() { UiTestAutomationBridge.this.onInterrupt(); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { synchronized (mLock) { while (true) { mLastEvent = AccessibilityEvent.obtain(event); if (!mWaitingForEventDelivery) { mLock.notifyAll(); break; } if (!mUnprocessedEventAvailable) { mUnprocessedEventAvailable = true; mLock.notifyAll(); break; } try { mLock.wait(); } catch (InterruptedException ie) { /* ignore */ } } } UiTestAutomationBridge.this.onAccessibilityEvent(event); } @Override public void onSetConnectionId(int connectionId) { synchronized (mLock) { mConnectionId = connectionId; mLock.notifyAll(); } } @Override public boolean onGesture(int gestureId) { return false; } }); final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; try { manager.registerUiTestAutomationService(mListener, info); } catch (RemoteException re) { throw new IllegalStateException("Cound not register UiAutomationService.", re); } synchronized (mLock) { final long startTimeMillis = SystemClock.uptimeMillis(); while (true) { if (isConnected()) { return; } final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; final long remainingTimeMillis = TIMEOUT_REGISTER_SERVICE - elapsedTimeMillis; if (remainingTimeMillis <= 0) { throw new IllegalStateException("Cound not register UiAutomationService."); } try { mLock.wait(remainingTimeMillis); } catch (InterruptedException ie) { /* ignore */ } } } } /** * Disconnects this service. * * @throws IllegalStateException If already disconnected. */ public void disconnect() { if (!isConnected()) { throw new IllegalStateException("Already disconnected."); } mHandlerThread.quit(); IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); try { manager.unregisterUiTestAutomationService(mListener); } catch (RemoteException re) { Log.e(LOG_TAG, "Error while unregistering UiTestAutomationService", re); } } /** * Gets whether this service is connected. * * @return True if connected. */ public boolean isConnected() { return (mConnectionId != AccessibilityInteractionClient.NO_ID); } /** * Executes a command and waits for a specific accessibility event type up * to a given timeout. * * @param command The command to execute before starting to wait for the event. * @param predicate Predicate for recognizing the awaited event. * @param timeoutMillis The max wait time in milliseconds. */ public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, Predicate predicate, long timeoutMillis) throws TimeoutException, Exception { // TODO: This is broken - remove from here when finalizing this as public APIs. synchronized (mLock) { // Prepare to wait for an event. mWaitingForEventDelivery = true; mUnprocessedEventAvailable = false; if (mLastEvent != null) { mLastEvent.recycle(); mLastEvent = null; } // Execute the command. command.run(); // Wait for the event. final long startTimeMillis = SystemClock.uptimeMillis(); while (true) { // If the expected event is received, that's it. if ((mUnprocessedEventAvailable && predicate.apply(mLastEvent))) { mWaitingForEventDelivery = false; mUnprocessedEventAvailable = false; mLock.notifyAll(); return mLastEvent; } // Ask for another event. mWaitingForEventDelivery = true; mUnprocessedEventAvailable = false; mLock.notifyAll(); // Check if timed out and if not wait. final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; if (remainingTimeMillis <= 0) { mWaitingForEventDelivery = false; mUnprocessedEventAvailable = false; mLock.notifyAll(); throw new TimeoutException("Expacted event not received within: " + timeoutMillis + " ms."); } try { mLock.wait(remainingTimeMillis); } catch (InterruptedException ie) { /* ignore */ } } } } /** * Waits for the accessibility event stream to become idle, which is not to * have received a new accessibility event within idleTimeout, * and do so within a maximal global timeout as specified by * globalTimeout. * * @param idleTimeout The timeout between two event to consider the device idle. * @param globalTimeout The maximal global timeout in which to wait for idle. */ public void waitForIdle(long idleTimeout, long globalTimeout) { final long startTimeMillis = SystemClock.uptimeMillis(); long lastEventTime = (mLastEvent != null) ? mLastEvent.getEventTime() : SystemClock.uptimeMillis(); synchronized (mLock) { while (true) { final long currentTimeMillis = SystemClock.uptimeMillis(); final long sinceLastEventTimeMillis = currentTimeMillis - lastEventTime; if (sinceLastEventTimeMillis > idleTimeout) { return; } if (mLastEvent != null) { lastEventTime = mLastEvent.getEventTime(); } final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; final long remainingTimeMillis = globalTimeout - elapsedTimeMillis; if (remainingTimeMillis <= 0) { return; } try { mLock.wait(idleTimeout); } catch (InterruptedException e) { /* ignore */ } } } } /** * Finds an {@link AccessibilityNodeInfo} by accessibility id in the active * window. The search is performed from the root node. * * @param accessibilityNodeId A unique view id or virtual descendant id for * which to search. * @return The current window scale, where zero means a failure. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityIdInActiveWindow( long accessibilityNodeId) { return findAccessibilityNodeInfoByAccessibilityId(ACTIVE_WINDOW_ID, accessibilityNodeId); } /** * Finds an {@link AccessibilityNodeInfo} by accessibility id. * * @param accessibilityWindowId A unique window id. Use {@link #ACTIVE_WINDOW_ID} to query * the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id for * which to search. * @return The current window scale, where zero means a failure. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId( int accessibilityWindowId, long accessibilityNodeId) { // Cache the id to avoid locking final int connectionId = mConnectionId; ensureValidConnection(connectionId); return AccessibilityInteractionClient.getInstance() .findAccessibilityNodeInfoByAccessibilityId(mConnectionId, accessibilityWindowId, accessibilityNodeId, FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS); } /** * Finds an {@link AccessibilityNodeInfo} by View id in the active * window. The search is performed from the root node. * * @param viewId The id of a View. * @return The current window scale, where zero means a failure. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId) { return findAccessibilityNodeInfoByViewId(ACTIVE_WINDOW_ID, ROOT_NODE_ID, viewId); } /** * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in * the window whose id is specified and starts from the node whose accessibility * id is specified. * * @param accessibilityWindowId A unique window id. Use * {@link #ACTIVE_WINDOW_ID} to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use {@link #ROOT_NODE_ID} to start from the root. * @param viewId The id of a View. * @return The current window scale, where zero means a failure. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int accessibilityWindowId, long accessibilityNodeId, int viewId) { // Cache the id to avoid locking final int connectionId = mConnectionId; ensureValidConnection(connectionId); return AccessibilityInteractionClient.getInstance() .findAccessibilityNodeInfoByViewId(connectionId, accessibilityWindowId, accessibilityNodeId, viewId); } /** * Finds {@link AccessibilityNodeInfo}s by View text in the active * window. The search is performed from the root node. * * @param text The searched text. * @return The current window scale, where zero means a failure. */ public List findAccessibilityNodeInfosByTextInActiveWindow(String text) { return findAccessibilityNodeInfosByText(ACTIVE_WINDOW_ID, ROOT_NODE_ID, text); } /** * Finds {@link AccessibilityNodeInfo}s by View text. The match is case * insensitive containment. The search is performed in the window whose * id is specified and starts from the node whose accessibility id is * specified. * * @param accessibilityWindowId A unique window id. Use * {@link #ACTIVE_WINDOW_ID} to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use {@link #ROOT_NODE_ID} to start from the root. * @param text The searched text. * @return The current window scale, where zero means a failure. */ public List findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, String text) { // Cache the id to avoid locking final int connectionId = mConnectionId; ensureValidConnection(connectionId); return AccessibilityInteractionClient.getInstance() .findAccessibilityNodeInfosByText(connectionId, accessibilityWindowId, accessibilityNodeId, text); } /** * Performs an accessibility action on an {@link AccessibilityNodeInfo} * in the active window. * * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). * @param action The action to perform. * @param arguments Optional action arguments. * @return Whether the action was performed. */ public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action, Bundle arguments) { return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action, arguments); } /** * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * * @param accessibilityWindowId A unique window id. Use * {@link #ACTIVE_WINDOW_ID} to query the currently active window. * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). * @param action The action to perform. * @param arguments Optional action arguments. * @return Whether the action was performed. */ public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, Bundle arguments) { // Cache the id to avoid locking final int connectionId = mConnectionId; ensureValidConnection(connectionId); return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId, accessibilityWindowId, accessibilityNodeId, action, arguments); } /** * Gets the root {@link AccessibilityNodeInfo} in the active window. * * @return The root info. */ public AccessibilityNodeInfo getRootAccessibilityNodeInfoInActiveWindow() { // Cache the id to avoid locking final int connectionId = mConnectionId; ensureValidConnection(connectionId); return AccessibilityInteractionClient.getInstance() .findAccessibilityNodeInfoByAccessibilityId(connectionId, ACTIVE_WINDOW_ID, ROOT_NODE_ID, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); } private void ensureValidConnection(int connectionId) { if (connectionId == UNDEFINED) { throw new IllegalStateException("UiAutomationService not connected." + " Did you call #register()?"); } } }