| /* |
| * 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()?"); |
| } |
| } |
| } |