blob: 9d48efc66dc39e7313ad840e6cdf114147d02fb7 [file] [log] [blame]
/*
* 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.IEventListenerWrapper;
import android.content.Context;
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();
public static final int ACTIVE_WINDOW_ID = -1;
public static final long ROOT_NODE_ID = -1;
private static final int TIMEOUT_REGISTER_SERVICE = 5000;
private final Object mLock = new Object();
private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID;
private IEventListenerWrapper mListener;
private AccessibilityEvent mLastEvent;
private volatile boolean mWaitingForEventDelivery;
private volatile boolean mUnprocessedEventAvailable;
/**
* Gets the last received {@link AccessibilityEvent}.
*
* @return The event.
*/
public AccessibilityEvent getLastAccessibilityEvent() {
return mLastEvent;
}
/**
* Callback for receiving an {@link AccessibilityEvent}.
*
* <strong>Note:</strong> This method is <strong>NOT</strong>
* 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.
*
* <strong>Note:</strong> This method is <strong>NOT</strong>
* 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.
HandlerThread handlerThread = new HandlerThread("UiTestAutomationBridge");
handlerThread.start();
Looper looper = handlerThread.getLooper();
mListener = new IEventListenerWrapper(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) {
if (!mWaitingForEventDelivery) {
break;
}
if (!mUnprocessedEventAvailable) {
mUnprocessedEventAvailable = true;
mLastEvent = AccessibilityEvent.obtain(event);
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();
}
}
});
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;
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.");
}
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<AccessibilityEvent> predicate, long timeoutMillis)
throws TimeoutException, Exception {
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 */
}
}
}
}
/**
* 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);
}
/**
* Finds an {@link AccessibilityNodeInfo} by View id in the active
* window. The search is performed from the root node.
*
* @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.
* @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<AccessibilityNodeInfo> 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<AccessibilityNodeInfo> 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.
* @return Whether the action was performed.
*/
public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action) {
return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action);
}
/**
* 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.
* @return Whether the action was performed.
*/
public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId,
int action) {
// Cache the id to avoid locking
final int connectionId = mConnectionId;
ensureValidConnection(connectionId);
return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId,
accessibilityWindowId, accessibilityNodeId, action);
}
private void ensureValidConnection(int connectionId) {
if (connectionId == AccessibilityInteractionClient.NO_ID) {
throw new IllegalStateException("UiAutomationService not connected."
+ " Did you call #register()?");
}
}
}