am 2eecea3b: Merge "Speedup the accessibility window querying APIs and clean up."
* commit '2eecea3b48ece6f45b30fef9b41dc20075ccc94f':
Speedup the accessibility window querying APIs and clean up.
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 211be52..a463a62 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -17,8 +17,10 @@
package android.accessibilityservice;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
@@ -218,10 +220,17 @@
private static final String LOG_TAG = "AccessibilityService";
- private AccessibilityServiceInfo mInfo;
+ interface Callbacks {
+ public void onAccessibilityEvent(AccessibilityEvent event);
+ public void onInterrupt();
+ public void onServiceConnected();
+ public void onSetConnectionId(int connectionId);
+ }
private int mConnectionId;
+ private AccessibilityServiceInfo mInfo;
+
/**
* Callback for {@link android.view.accessibility.AccessibilityEvent}s.
*
@@ -282,27 +291,49 @@
*/
@Override
public final IBinder onBind(Intent intent) {
- return new IEventListenerWrapper(this);
+ return new IEventListenerWrapper(this, getMainLooper(), new Callbacks() {
+ @Override
+ public void onServiceConnected() {
+ AccessibilityService.this.onServiceConnected();
+ }
+
+ @Override
+ public void onInterrupt() {
+ AccessibilityService.this.onInterrupt();
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ AccessibilityService.this.onAccessibilityEvent(event);
+ }
+
+ @Override
+ public void onSetConnectionId( int connectionId) {
+ mConnectionId = connectionId;
+ }
+ });
}
/**
* Implements the internal {@link IEventListener} interface to convert
* incoming calls to it back to calls on an {@link AccessibilityService}.
*/
- class IEventListenerWrapper extends IEventListener.Stub
+ static class IEventListenerWrapper extends IEventListener.Stub
implements HandlerCaller.Callback {
+ static final int NO_ID = -1;
+
private static final int DO_SET_SET_CONNECTION = 10;
private static final int DO_ON_INTERRUPT = 20;
private static final int DO_ON_ACCESSIBILITY_EVENT = 30;
private final HandlerCaller mCaller;
- private final AccessibilityService mTarget;
+ private final Callbacks mCallback;
- public IEventListenerWrapper(AccessibilityService context) {
- mTarget = context;
- mCaller = new HandlerCaller(context, this);
+ public IEventListenerWrapper(Context context, Looper looper, Callbacks callback) {
+ mCallback = callback;
+ mCaller = new HandlerCaller(context, looper, this);
}
public void setConnection(IAccessibilityServiceConnection connection, int connectionId) {
@@ -326,12 +357,13 @@
case DO_ON_ACCESSIBILITY_EVENT :
AccessibilityEvent event = (AccessibilityEvent) message.obj;
if (event != null) {
- mTarget.onAccessibilityEvent(event);
+ AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event);
+ mCallback.onAccessibilityEvent(event);
event.recycle();
}
return;
case DO_ON_INTERRUPT :
- mTarget.onInterrupt();
+ mCallback.onInterrupt();
return;
case DO_SET_SET_CONNECTION :
final int connectionId = message.arg1;
@@ -340,12 +372,11 @@
if (connection != null) {
AccessibilityInteractionClient.getInstance().addConnection(connectionId,
connection);
- mConnectionId = connectionId;
- mTarget.onServiceConnected();
+ mCallback.onSetConnectionId(connectionId);
+ mCallback.onServiceConnected();
} else {
AccessibilityInteractionClient.getInstance().removeConnection(connectionId);
- mConnectionId = AccessibilityInteractionClient.NO_ID;
- // TODO: Do we need a onServiceDisconnected callback?
+ mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID);
}
return;
default :
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index e53b313..c9468eb 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -32,8 +32,13 @@
/**
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
- * @param accessibilityWindowId A unique window id.
- * @param accessibilityNodeId A unique view id or virtual descendant id.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#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 com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID}
+ * to start from the root.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
* @param threadId The id of the calling thread.
@@ -46,57 +51,58 @@
/**
* 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 View whose accessibility id is
+ * id is specified and starts from the node whose accessibility id is
* specified.
*
- * @param text The searched text.
- * @param accessibilityWindowId A unique window id.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#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 android.view.View#NO_ID} to start from the root.
- * @param interactionId The id of the interaction for matching with the callback result.
- * @param callback Callback which to receive the result.
- * @param threadId The id of the calling thread.
- * @return The current window scale, where zero means a failure.
- */
- float findAccessibilityNodeInfosByText(String text, int accessibilityWindowId,
- long accessibilityNodeId, int interractionId,
- IAccessibilityInteractionConnectionCallback callback, long threadId);
-
- /**
- * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
- * insensitive containment. The search is performed in the currently
- * active window and start from the root View in the window.
- *
+ * where to start the search. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID}
+ * to start from the root.
* @param text The searched text.
- * @param accessibilityId The id of the view from which to start searching.
- * Use {@link android.view.View#NO_ID} to start from the root.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
* @param threadId The id of the calling thread.
* @return The current window scale, where zero means a failure.
*/
- float findAccessibilityNodeInfosByTextInActiveWindow(String text,
- int interactionId, IAccessibilityInteractionConnectionCallback callback,
+ float findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId,
+ String text, int interactionId, IAccessibilityInteractionConnectionCallback callback,
long threadId);
/**
- * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed
- * in the currently active window and starts from the root View in the window.
+ * 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 com.android.server.accessibility.AccessibilityManagerService#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 com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID}
+ * to start from the root.
* @param id The id of the node.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
* @param threadId The id of the calling thread.
* @return The current window scale, where zero means a failure.
*/
- float findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId, int interactionId,
- IAccessibilityInteractionConnectionCallback callback, long threadId);
+ float findAccessibilityNodeInfoByViewId(int accessibilityWindowId, long accessibilityNodeId,
+ int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback,
+ long threadId);
/**
* Performs an accessibility action on an {@link AccessibilityNodeInfo}.
*
- * @param accessibilityWindowId The id of the window.
- * @param accessibilityNodeId A unique view id or virtual descendant id.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#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 com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID}
+ * to start from the root.
* @param action The action to perform.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java
new file mode 100644
index 0000000..9d48efc
--- /dev/null
+++ b/core/java/android/accessibilityservice/UiTestAutomationBridge.java
@@ -0,0 +1,418 @@
+/*
+ * 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()?");
+ }
+ }
+}
diff --git a/core/java/android/view/AccessibilityNodeInfoCache.java b/core/java/android/view/AccessibilityNodeInfoCache.java
new file mode 100644
index 0000000..244a491
--- /dev/null
+++ b/core/java/android/view/AccessibilityNodeInfoCache.java
@@ -0,0 +1,171 @@
+/*
+ * 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.view;
+
+import android.util.LongSparseArray;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Simple cache for AccessibilityNodeInfos. The cache is mapping an
+ * accessibility id to an info. The cache allows storing of
+ * <code>null</code> values. It also tracks accessibility events
+ * and invalidates accordingly.
+ *
+ * @hide
+ */
+public class AccessibilityNodeInfoCache {
+
+ private final boolean ENABLED = true;
+
+ /**
+ * @return A new <strong>not synchronized</strong> AccessibilityNodeInfoCache.
+ */
+ public static AccessibilityNodeInfoCache newAccessibilityNodeInfoCache() {
+ return new AccessibilityNodeInfoCache();
+ }
+
+ /**
+ * @return A new <strong>synchronized</strong> AccessibilityNodeInfoCache.
+ */
+ public static AccessibilityNodeInfoCache newSynchronizedAccessibilityNodeInfoCache() {
+ return new AccessibilityNodeInfoCache() {
+ private final Object mLock = new Object();
+
+ @Override
+ public void clear() {
+ synchronized(mLock) {
+ super.clear();
+ }
+ }
+
+ @Override
+ public AccessibilityNodeInfo get(long accessibilityNodeId) {
+ synchronized(mLock) {
+ return super.get(accessibilityNodeId);
+ }
+ }
+
+ @Override
+ public void put(long accessibilityNodeId, AccessibilityNodeInfo info) {
+ synchronized(mLock) {
+ super.put(accessibilityNodeId, info);
+ }
+ }
+
+ @Override
+ public void remove(long accessibilityNodeId) {
+ synchronized(mLock) {
+ super.remove(accessibilityNodeId);
+ }
+ }
+ };
+ }
+
+ private final LongSparseArray<AccessibilityNodeInfo> mCacheImpl;
+
+ private AccessibilityNodeInfoCache() {
+ if (ENABLED) {
+ mCacheImpl = new LongSparseArray<AccessibilityNodeInfo>();
+ } else {
+ mCacheImpl = null;
+ }
+ }
+
+ /**
+ * The cache keeps track of {@link AccessibilityEvent}s and invalidates
+ * cached nodes as appropriate.
+ *
+ * @param event An event.
+ */
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ final int eventType = event.getEventType();
+ switch (eventType) {
+ case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
+ case AccessibilityEvent.TYPE_VIEW_SCROLLED:
+ clear();
+ break;
+ case AccessibilityEvent.TYPE_VIEW_FOCUSED:
+ case AccessibilityEvent.TYPE_VIEW_SELECTED:
+ case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
+ case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:
+ final long accessibilityNodeId = event.getSourceNodeId();
+ remove(accessibilityNodeId);
+ break;
+ }
+ }
+
+ /**
+ * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id.
+ *
+ * @param accessibilityNodeId The info accessibility node id.
+ * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
+ */
+ public AccessibilityNodeInfo get(long accessibilityNodeId) {
+ if (ENABLED) {
+ return mCacheImpl.get(accessibilityNodeId);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Caches an {@link AccessibilityNodeInfo} given its accessibility node id.
+ *
+ * @param accessibilityNodeId The info accessibility node id.
+ * @param info The {@link AccessibilityNodeInfo} to cache.
+ */
+ public void put(long accessibilityNodeId, AccessibilityNodeInfo info) {
+ if (ENABLED) {
+ mCacheImpl.put(accessibilityNodeId, info);
+ }
+ }
+
+ /**
+ * Returns whether the cache contains an accessibility node id key.
+ *
+ * @param accessibilityNodeId The key for which to check.
+ * @return True if the key is in the cache.
+ */
+ public boolean containsKey(long accessibilityNodeId) {
+ if (ENABLED) {
+ return (mCacheImpl.indexOfKey(accessibilityNodeId) >= 0);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes a cached {@link AccessibilityNodeInfo}.
+ *
+ * @param accessibilityNodeId The info accessibility node id.
+ */
+ public void remove(long accessibilityNodeId) {
+ if (ENABLED) {
+ mCacheImpl.remove(accessibilityNodeId);
+ }
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public void clear() {
+ if (ENABLED) {
+ mCacheImpl.clear();
+ }
+ }
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 8597017..d80d080 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -12679,6 +12679,11 @@
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.REQUEST_LAYOUT);
}
+ if (getAccessibilityNodeProvider() != null) {
+ throw new IllegalStateException("Views with AccessibilityNodeProvider"
+ + " can't have children.");
+ }
+
mPrivateFlags |= FORCE_LAYOUT;
mPrivateFlags |= INVALIDATED;
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 7559862..d3af618 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -3356,6 +3356,11 @@
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
+ if (getAccessibilityNodeProvider() != null) {
+ throw new IllegalStateException("Views with AccessibilityNodeProvider"
+ + " can't have children.");
+ }
+
if (mTransition != null) {
// Don't prevent other add transitions from completing, but cancel remove
// transitions to let them complete the process before we add to the container
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 2ef843b..3f61e6b 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -52,6 +52,7 @@
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
+import android.util.LongSparseArray;
import android.util.Pool;
import android.util.Poolable;
import android.util.PoolableManager;
@@ -302,6 +303,8 @@
SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
+ AccessibilityPrefetchStrategy mAccessibilityPrefetchStrategy;
+
private final int mDensity;
/**
@@ -3483,6 +3486,17 @@
return mAccessibilityInteractionController;
}
+ public AccessibilityPrefetchStrategy getAccessibilityPrefetchStrategy() {
+ if (mView == null) {
+ throw new IllegalStateException("getAccessibilityPrefetchStrategy"
+ + " called when there is no mView");
+ }
+ if (mAccessibilityPrefetchStrategy == null) {
+ mAccessibilityPrefetchStrategy = new AccessibilityPrefetchStrategy();
+ }
+ return mAccessibilityPrefetchStrategy;
+ }
+
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
@@ -3983,6 +3997,7 @@
if (mView == null) {
return false;
}
+ getAccessibilityPrefetchStrategy().onAccessibilityEvent(event);
mAccessibilityManager.sendAccessibilityEvent(event);
return true;
}
@@ -4542,6 +4557,13 @@
viewRootImpl.getAccessibilityInteractionController()
.findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
interactionId, callback, interrogatingPid, interrogatingTid);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setFindAccessibilityNodeInfosResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
}
}
@@ -4553,28 +4575,49 @@
viewRootImpl.getAccessibilityInteractionController()
.performAccessibilityActionClientThread(accessibilityNodeId, action,
interactionId, callback, interogatingPid, interrogatingTid);
+ } else {
+ // We cannot make the call and notify the caller so it does not
+ try {
+ callback.setPerformAccessibilityActionResult(false, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
}
}
- public void findAccessibilityNodeInfoByViewId(int viewId,
+ public void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid) {
ViewRootImpl viewRootImpl = mViewRootImpl.get();
if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
- .findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback,
- interrogatingPid, interrogatingTid);
- }
- }
-
- public void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId,
- int interactionId, IAccessibilityInteractionConnectionCallback callback,
- int interrogatingPid, long interrogatingTid) {
- ViewRootImpl viewRootImpl = mViewRootImpl.get();
- if (viewRootImpl != null && viewRootImpl.mView != null) {
- viewRootImpl.getAccessibilityInteractionController()
- .findAccessibilityNodeInfosByTextClientThread(text, accessibilityNodeId,
+ .findAccessibilityNodeInfoByViewIdClientThread(accessibilityNodeId, viewId,
interactionId, callback, interrogatingPid, interrogatingTid);
+ } else {
+ // We cannot make the call and notify the caller so it does not
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text,
+ int interactionId, IAccessibilityInteractionConnectionCallback callback,
+ int interrogatingPid, long interrogatingTid) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text,
+ interactionId, callback, interrogatingPid, interrogatingTid);
+ } else {
+ // We cannot make the call and notify the caller so it does not
+ try {
+ callback.setFindAccessibilityNodeInfosResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
}
}
}
@@ -4652,6 +4695,7 @@
long interrogatingTid) {
Message message = Message.obtain();
message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;
+ message.arg1 = interrogatingPid;
SomeArgs args = mPool.acquire();
args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
@@ -4674,40 +4718,47 @@
public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
SomeArgs args = (SomeArgs) message.obj;
+ final int interrogatingPid = message.arg1;
final int accessibilityViewId = args.argi1;
final int virtualDescendantId = args.argi2;
final int interactionId = args.argi3;
final IAccessibilityInteractionConnectionCallback callback =
(IAccessibilityInteractionConnectionCallback) args.arg1;
mPool.release(args);
- AccessibilityNodeInfo info = null;
+ List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
+ infos.clear();
try {
View target = findViewByAccessibilityId(accessibilityViewId);
if (target != null && target.getVisibility() == View.VISIBLE) {
AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
if (provider != null) {
- info = provider.createAccessibilityNodeInfo(virtualDescendantId);
+ infos.add(provider.createAccessibilityNodeInfo(virtualDescendantId));
} else if (virtualDescendantId == View.NO_ID) {
- info = target.createAccessibilityNodeInfo();
+ getAccessibilityPrefetchStrategy().prefetchAccessibilityNodeInfos(
+ interrogatingPid, target, infos);
}
}
} finally {
try {
- callback.setFindAccessibilityNodeInfoResult(info, interactionId);
+ callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
+ infos.clear();
} catch (RemoteException re) {
/* ignore - the other side will time out */
}
}
}
- public void findAccessibilityNodeInfoByViewIdClientThread(int viewId, int interactionId,
- IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
- long interrogatingTid) {
+ public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId,
+ int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback,
+ int interrogatingPid, long interrogatingTid) {
Message message = Message.obtain();
message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID;
- message.arg1 = viewId;
- message.arg2 = interactionId;
- message.obj = callback;
+ message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ SomeArgs args = mPool.acquire();
+ args.argi1 = viewId;
+ args.argi2 = interactionId;
+ args.arg1 = callback;
+ message.obj = args;
// If the interrogation is performed by the same thread as the main UI
// thread in this process, set the message as a static reference so
// after this call completes the same thread but in the interrogating
@@ -4723,17 +4774,26 @@
}
public void findAccessibilityNodeInfoByViewIdUiThread(Message message) {
- final int viewId = message.arg1;
- final int interactionId = message.arg2;
+ final int accessibilityViewId = message.arg1;
+ SomeArgs args = (SomeArgs) message.obj;
+ final int viewId = args.argi1;
+ final int interactionId = args.argi2;
final IAccessibilityInteractionConnectionCallback callback =
- (IAccessibilityInteractionConnectionCallback) message.obj;
-
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ mPool.release(args);
AccessibilityNodeInfo info = null;
try {
- View root = ViewRootImpl.this.mView;
- View target = root.findViewById(viewId);
- if (target != null && target.getVisibility() == View.VISIBLE) {
- info = target.createAccessibilityNodeInfo();
+ View root = null;
+ if (accessibilityViewId != View.NO_ID) {
+ root = findViewByAccessibilityId(accessibilityViewId);
+ } else {
+ root = ViewRootImpl.this.mView;
+ }
+ if (root != null) {
+ View target = root.findViewById(viewId);
+ if (target != null && target.getVisibility() == View.VISIBLE) {
+ info = target.createAccessibilityNodeInfo();
+ }
}
} finally {
try {
@@ -4744,8 +4804,8 @@
}
}
- public void findAccessibilityNodeInfosByTextClientThread(String text,
- long accessibilityNodeId, int interactionId,
+ public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId,
+ String text, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
long interrogatingTid) {
Message message = Message.obtain();
@@ -4937,4 +4997,88 @@
}
}
}
+
+ /**
+ * This class encapsulates a prefetching strategy for the accessibility APIs for
+ * querying window content.It is responsible to prefetch a batch of
+ * AccessibilityNodeInfos in addition to the one for a requested node. It caches
+ * the ids of the prefeteched nodes such that they are fetched only once.
+ */
+ class AccessibilityPrefetchStrategy {
+ private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 100;
+
+ // We need to keep track of what we have sent for each interrogating
+ // process. Usually there will be only one such process but we
+ // should support the general case. Note that the accessibility event
+ // stream will take care of clearing caches of querying processes that
+ // are not longer alive, so we do not waste memory.
+ private final LongSparseArray<AccessibilityNodeInfoCache> mAccessibilityNodeInfoCaches =
+ new LongSparseArray<AccessibilityNodeInfoCache>();
+
+ private AccessibilityNodeInfoCache getCacheForInterrogatingPid(long interrogatingPid) {
+ AccessibilityNodeInfoCache cache = mAccessibilityNodeInfoCaches.get(interrogatingPid);
+ if (cache == null) {
+ cache = AccessibilityNodeInfoCache.newAccessibilityNodeInfoCache();
+ mAccessibilityNodeInfoCaches.put(interrogatingPid, cache);
+ }
+ return cache;
+ }
+
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ final int cacheCount = mAccessibilityNodeInfoCaches.size();
+ for (int i = 0; i < cacheCount; i++) {
+ AccessibilityNodeInfoCache cache = mAccessibilityNodeInfoCaches.valueAt(i);
+ cache.onAccessibilityEvent(event);
+ }
+ }
+
+ public void prefetchAccessibilityNodeInfos(long interrogatingPid, View root,
+ List<AccessibilityNodeInfo> outInfos) {
+ addAndCacheNotCachedNodeInfo(interrogatingPid, root, outInfos);
+ addAndCacheNotCachedPredecessorInfos(interrogatingPid, root, outInfos);
+ addAndCacheNotCachedDescendantInfos(interrogatingPid, root, outInfos);
+ }
+
+ private void addAndCacheNotCachedNodeInfo(long interrogatingPid,
+ View view, List<AccessibilityNodeInfo> outInfos) {
+ final long accessibilityNodeId = AccessibilityNodeInfo.makeNodeId(
+ view.getAccessibilityViewId(), View.NO_ID);
+ AccessibilityNodeInfoCache cache = getCacheForInterrogatingPid(interrogatingPid);
+ if (!cache.containsKey(accessibilityNodeId)) {
+ // Account for the ids of the fetched infos. The infos will be
+ // cached in the window querying process. We just need to know
+ // which infos are cached to avoid fetching a cached one again.
+ cache.put(accessibilityNodeId, null);
+ outInfos.add(view.createAccessibilityNodeInfo());
+ }
+ }
+
+ private void addAndCacheNotCachedPredecessorInfos(long interrogatingPid, View view,
+ List<AccessibilityNodeInfo> outInfos) {
+ ViewParent predecessor = view.getParent();
+ while (predecessor instanceof View
+ && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ View predecessorView = (View) predecessor;
+ addAndCacheNotCachedNodeInfo(interrogatingPid, predecessorView, outInfos);
+ predecessor = predecessor.getParent();
+ }
+ }
+
+ private void addAndCacheNotCachedDescendantInfos(long interrogatingPid, View view,
+ List<AccessibilityNodeInfo> outInfos) {
+ if (outInfos.size() > MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE
+ || view.getAccessibilityNodeProvider() != null) {
+ return;
+ }
+ addAndCacheNotCachedNodeInfo(interrogatingPid, view, outInfos);
+ if (view instanceof ViewGroup) {
+ ViewGroup rootGroup = (ViewGroup) view;
+ final int childCount = rootGroup.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = rootGroup.getChildAt(i);
+ addAndCacheNotCachedDescendantInfos(interrogatingPid, child, outInfos);
+ }
+ }
+ }
+ }
}
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index 95c070c..072fdd8 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -24,7 +24,9 @@
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
+import android.view.AccessibilityNodeInfoCache;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -97,6 +99,11 @@
private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
new SparseArray<IAccessibilityServiceConnection>();
+ // The connection cache is shared between all interrogating threads since
+ // at any given time there is only one window allowing querying.
+ private static final AccessibilityNodeInfoCache sAccessibilityNodeInfoCache =
+ AccessibilityNodeInfoCache.newSynchronizedAccessibilityNodeInfoCache();
+
/**
* @return The client for the current thread.
*/
@@ -145,7 +152,9 @@
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
* @param connectionId The id of a connection for interacting with the system.
- * @param accessibilityWindowId A unique window id.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
* @param accessibilityNodeId A unique node accessibility id
* (accessibility view and virtual descendant id).
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
@@ -155,16 +164,22 @@
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
+ AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(accessibilityNodeId);
+ if (cachedInfo != null) {
+ return cachedInfo;
+ }
final int interactionId = mInteractionIdCounter.getAndIncrement();
final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (windowScale > 0) {
- AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
+ List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
- finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
- return info;
+ finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
+ if (infos != null && !infos.isEmpty()) {
+ return infos.get(0);
+ }
}
} else {
if (DEBUG) {
@@ -181,22 +196,30 @@
}
/**
- * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed
- * in the currently active window and starts from the root View in the window.
+ * 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 connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id from where to start the search. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID}
+ * to start from the root.
* @param viewId The id of the view.
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
*/
- public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId,
- int viewId) {
+ public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int connectionId,
+ int accessibilityWindowId, long accessibilityNodeId, int viewId) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final float windowScale =
- connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId,
- interactionId, this, Thread.currentThread().getId());
+ connection.findAccessibilityNodeInfoByViewId(accessibilityWindowId,
+ accessibilityNodeId, viewId, interactionId, this,
+ Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (windowScale > 0) {
AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
@@ -220,64 +243,27 @@
/**
* Finds {@link AccessibilityNodeInfo}s by View text. The match is case
- * insensitive containment. The search is performed in the currently
- * active window and starts from the root View in the window.
- *
- * @param connectionId The id of a connection for interacting with the system.
- * @param text The searched text.
- * @return A list of found {@link AccessibilityNodeInfo}s.
- */
- public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(
- int connectionId, String text) {
- try {
- IAccessibilityServiceConnection connection = getConnection(connectionId);
- if (connection != null) {
- final int interactionId = mInteractionIdCounter.getAndIncrement();
- final float windowScale =
- connection.findAccessibilityNodeInfosByTextInActiveWindow(text,
- interactionId, this, Thread.currentThread().getId());
- // If the scale is zero the call has failed.
- if (windowScale > 0) {
- List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
- interactionId);
- finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
- return infos;
- }
- } else {
- if (DEBUG) {
- Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
- }
- }
- } catch (RemoteException re) {
- if (DEBUG) {
- Log.w(LOG_TAG, "Error while calling remote"
- + " findAccessibilityNodeInfosByViewTextInActiveWindow", re);
- }
- }
- return null;
- }
-
- /**
- * 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 View whose accessibility id is
+ * id is specified and starts from the node whose accessibility id is
* specified.
*
* @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id from where to start the search. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID}
* @param text The searched text.
- * @param accessibilityWindowId A unique window id.
- * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id) from
- * where to start the search. Use {@link android.view.View#NO_ID} to start from the root.
* @return A list of found {@link AccessibilityNodeInfo}s.
*/
public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
- String text, int accessibilityWindowId, long accessibilityNodeId) {
+ int accessibilityWindowId, long accessibilityNodeId, String text) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
- final float windowScale = connection.findAccessibilityNodeInfosByText(text,
- accessibilityWindowId, accessibilityNodeId, interactionId, this,
+ final float windowScale = connection.findAccessibilityNodeInfosByText(
+ accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (windowScale > 0) {
@@ -304,7 +290,9 @@
* Performs an accessibility action on an {@link AccessibilityNodeInfo}.
*
* @param connectionId The id of a connection for interacting with the system.
- * @param accessibilityWindowId The id of the window.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link com.android.server.accessibility.AccessibilityManagerService#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.
@@ -319,7 +307,7 @@
accessibilityWindowId, accessibilityNodeId, action, interactionId, this,
Thread.currentThread().getId());
if (success) {
- return getPerformAccessibilityActionResult(interactionId);
+ return getPerformAccessibilityActionResultAndClear(interactionId);
}
} else {
if (DEBUG) {
@@ -334,6 +322,24 @@
return false;
}
+ public void clearCache() {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "clearCache()");
+ }
+ sAccessibilityNodeInfoCache.clear();
+ }
+
+ public void removeCachedNode(long accessibilityNodeId) {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "removeCachedNode(" + accessibilityNodeId +")");
+ }
+ sAccessibilityNodeInfoCache.remove(accessibilityNodeId);
+ }
+
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ sAccessibilityNodeInfoCache.onAccessibilityEvent(event);
+ }
+
/**
* Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
*
@@ -358,6 +364,9 @@
if (interactionId > mInteractionId) {
mFindAccessibilityNodeInfoResult = info;
mInteractionId = interactionId;
+ if (info != null) {
+ sAccessibilityNodeInfoCache.put(info.getSourceNodeId(), info);
+ }
}
mInstanceLock.notifyAll();
}
@@ -386,8 +395,20 @@
int interactionId) {
synchronized (mInstanceLock) {
if (interactionId > mInteractionId) {
- mFindAccessibilityNodeInfosResult = infos;
+ // If the call is not an IPC, i.e. it is made from the same process, we need to
+ // instantiate new result list to avoid passing internal instances to clients.
+ final boolean isIpcCall = (queryLocalInterface(getInterfaceDescriptor()) == null);
+ if (!isIpcCall) {
+ mFindAccessibilityNodeInfosResult = new ArrayList<AccessibilityNodeInfo>(infos);
+ } else {
+ mFindAccessibilityNodeInfosResult = infos;
+ }
mInteractionId = interactionId;
+ final int infoCount = infos.size();
+ for (int i = 0; i < infoCount; i ++) {
+ AccessibilityNodeInfo info = infos.get(i);
+ sAccessibilityNodeInfoCache.put(info.getSourceNodeId(), info);
+ }
}
mInstanceLock.notifyAll();
}
@@ -399,7 +420,7 @@
* @param interactionId The interaction id to match the result with the request.
* @return Whether the action was performed.
*/
- private boolean getPerformAccessibilityActionResult(int interactionId) {
+ private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
synchronized (mInstanceLock) {
final boolean success = waitForResultTimedLocked(interactionId);
final boolean result = success ? mPerformAccessibilityActionResult : false;
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 6939c2c..d7d6792 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -380,8 +380,8 @@
return Collections.emptyList();
}
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
- return client.findAccessibilityNodeInfosByText(mConnectionId, text, mWindowId,
- mSourceNodeId);
+ return client.findAccessibilityNodeInfosByText(mConnectionId, mWindowId, mSourceNodeId,
+ text);
}
/**
@@ -903,6 +903,17 @@
}
/**
+ * Gets the id of the source node.
+ *
+ * @return The id.
+ *
+ * @hide
+ */
+ public long getSourceNodeId() {
+ return mSourceNodeId;
+ }
+
+ /**
* Sets if this instance is sealed.
*
* @param sealed Whether is sealed.
diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java
index 07aeb9a..b60f50e 100644
--- a/core/java/android/view/accessibility/AccessibilityRecord.java
+++ b/core/java/android/view/accessibility/AccessibilityRecord.java
@@ -564,6 +564,17 @@
}
/**
+ * Gets the id of the source node.
+ *
+ * @return The id.
+ *
+ * @hide
+ */
+ public long getSourceNodeId() {
+ return mSourceNodeId;
+ }
+
+ /**
* Sets the unique id of the IAccessibilityServiceConnection over which
* this instance can send requests to the system.
*
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
index a90c427..ae6869c 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
@@ -31,13 +31,13 @@
IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid);
- void findAccessibilityNodeInfoByViewId(int id, int interactionId,
+ void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int id, int interactionId,
IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid);
- void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId,
- int interactionId, IAccessibilityInteractionConnectionCallback callback,
- int interrogatingPid, long interrogatingTid);
+ void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
+ long interrogatingTid);
void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index c3794be..320c75d 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -49,5 +49,7 @@
void removeAccessibilityInteractionConnection(IWindow windowToken);
- void registerEventListener(IEventListener client);
+ void registerUiTestAutomationService(IEventListener listener, in AccessibilityServiceInfo info);
+
+ void unregisterUiTestAutomationService(IEventListener listener);
}
diff --git a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java
index b4a0581..a9f144b 100644
--- a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java
+++ b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java
@@ -14,12 +14,12 @@
package android.accessibilityservice;
-import com.android.frameworks.coretests.R;
-
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
+import com.android.frameworks.coretests.R;
+
/**
* Activity for testing the accessibility APIs for "interrogation" of
* the screen content. These APIs allow exploring the screen and
diff --git a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java
index 259a094..fa48093 100644
--- a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java
+++ b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java
@@ -14,26 +14,21 @@
package android.accessibilityservice;
-import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS;
-import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SELECT;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION;
-import android.content.Context;
import android.graphics.Rect;
-import android.os.ServiceManager;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;
-import android.view.View;
import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityInteractionClient;
-import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.IAccessibilityManager;
import com.android.frameworks.coretests.R;
+import com.android.internal.util.Predicate;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -48,21 +43,15 @@
*/
public class InterrogationActivityTest
extends ActivityInstrumentationTestCase2<InterrogationActivity> {
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private static String LOG_TAG = "InterrogationActivityTest";
- // Timeout before give up wait for the system to process an accessibility setting change.
- private static final int TIMEOUT_PROPAGATE_ACCESSIBLITY_SETTING = 2000;
-
// Timeout for the accessibility state of an Activity to be fully initialized.
- private static final int TIMEOUT_ACCESSIBLITY_STATE_INITIALIZED_MILLIS = 100;
+ private static final int TIMEOUT_PROPAGATE_ACCESSIBILITY_EVENT_MILLIS = 5000;
// Handle to a connection to the AccessibilityManagerService
- private static int sConnectionId = View.NO_ID;
-
- // The last received accessibility event
- private volatile AccessibilityEvent mLastAccessibilityEvent;
+ private UiTestAutomationBridge mUiTestAutomationBridge;
public InterrogationActivityTest() {
super(InterrogationActivity.class);
@@ -70,16 +59,39 @@
@Override
public void setUp() throws Exception {
- ensureConnection();
- bringUpActivityWithInitalizedAccessbility();
+ super.setUp();
+ mUiTestAutomationBridge = new UiTestAutomationBridge();
+ mUiTestAutomationBridge.connect();
+ mUiTestAutomationBridge.executeCommandAndWaitForAccessibilityEvent(new Runnable() {
+ // wait for the first accessibility event
+ @Override
+ public void run() {
+ // bring up the activity
+ getActivity();
+ }
+ },
+ new Predicate<AccessibilityEvent>() {
+ @Override
+ public boolean apply(AccessibilityEvent event) {
+ return (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ && event.getPackageName().equals(getActivity().getPackageName()));
+ }
+ },
+ TIMEOUT_PROPAGATE_ACCESSIBILITY_EVENT_MILLIS);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mUiTestAutomationBridge.disconnect();
+ super.tearDown();
}
@LargeTest
public void testFindAccessibilityNodeInfoByViewId() throws Exception {
final long startTimeMillis = SystemClock.uptimeMillis();
try {
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertNotNull(button);
assertEquals(0, button.getChildCount());
@@ -125,8 +137,8 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view by text
- List<AccessibilityNodeInfo> buttons = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfosByTextInActiveWindow(sConnectionId, "butto");
+ List<AccessibilityNodeInfo> buttons = mUiTestAutomationBridge
+ .findAccessibilityNodeInfosByTextInActiveWindow("butto");
assertEquals(9, buttons.size());
} finally {
if (DEBUG) {
@@ -141,12 +153,9 @@
public void testFindAccessibilityNodeInfoByViewTextContentDescription() throws Exception {
final long startTimeMillis = SystemClock.uptimeMillis();
try {
- bringUpActivityWithInitalizedAccessbility();
-
// find a view by text
- List<AccessibilityNodeInfo> buttons = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfosByTextInActiveWindow(sConnectionId,
- "contentDescription");
+ List<AccessibilityNodeInfo> buttons = mUiTestAutomationBridge
+ .findAccessibilityNodeInfosByTextInActiveWindow("contentDescription");
assertEquals(1, buttons.size());
} finally {
if (DEBUG) {
@@ -177,8 +186,8 @@
classNameAndTextList.add("android.widget.ButtonButton8");
classNameAndTextList.add("android.widget.ButtonButton9");
- AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.root);
+ AccessibilityNodeInfo root = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.root);
assertNotNull("We must find the existing root.", root);
Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
@@ -216,16 +225,16 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view and make sure it is not focused
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertFalse(button.isFocused());
// focus the view
assertTrue(button.performAction(ACTION_FOCUS));
// find the view again and make sure it is focused
- button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertTrue(button.isFocused());
} finally {
if (DEBUG) {
@@ -240,24 +249,24 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view and make sure it is not focused
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertFalse(button.isFocused());
// focus the view
assertTrue(button.performAction(ACTION_FOCUS));
// find the view again and make sure it is focused
- button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertTrue(button.isFocused());
// unfocus the view
assertTrue(button.performAction(ACTION_CLEAR_FOCUS));
// find the view again and make sure it is not focused
- button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertFalse(button.isFocused());
} finally {
if (DEBUG) {
@@ -273,16 +282,16 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view and make sure it is not selected
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertFalse(button.isSelected());
// select the view
assertTrue(button.performAction(ACTION_SELECT));
// find the view again and make sure it is selected
- button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertTrue(button.isSelected());
} finally {
if (DEBUG) {
@@ -297,24 +306,24 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view and make sure it is not selected
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertFalse(button.isSelected());
// select the view
assertTrue(button.performAction(ACTION_SELECT));
// find the view again and make sure it is selected
- button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertTrue(button.isSelected());
// unselect the view
assertTrue(button.performAction(ACTION_CLEAR_SELECTION));
// find the view again and make sure it is not selected
- button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
assertFalse(button.isSelected());
} finally {
if (DEBUG) {
@@ -330,23 +339,33 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view and make sure it is not focused
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
- assertFalse(button.isSelected());
+ final AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
+ assertFalse(button.isFocused());
- // focus the view
- assertTrue(button.performAction(ACTION_FOCUS));
-
- synchronized (this) {
- try {
- wait(TIMEOUT_ACCESSIBLITY_STATE_INITIALIZED_MILLIS);
- } catch (InterruptedException ie) {
- /* ignore */
+ AccessibilityEvent event = mUiTestAutomationBridge
+ .executeCommandAndWaitForAccessibilityEvent(new Runnable() {
+ @Override
+ public void run() {
+ // focus the view
+ assertTrue(button.performAction(ACTION_FOCUS));
}
- }
+ },
+ new Predicate<AccessibilityEvent>() {
+ @Override
+ public boolean apply(AccessibilityEvent event) {
+ return (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
+ && event.getPackageName().equals(getActivity().getPackageName())
+ && event.getText().get(0).equals(button.getText()));
+ }
+ },
+ TIMEOUT_PROPAGATE_ACCESSIBILITY_EVENT_MILLIS);
+
+ // check the last event
+ assertNotNull(event);
// check that last event source
- AccessibilityNodeInfo source = mLastAccessibilityEvent.getSource();
+ AccessibilityNodeInfo source = event.getSource();
assertNotNull(source);
// bounds
@@ -389,8 +408,9 @@
final long startTimeMillis = SystemClock.uptimeMillis();
try {
// find a view and make sure it is not focused
- AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance()
- .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5);
+ AccessibilityNodeInfo button = mUiTestAutomationBridge
+ .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5);
+ assertNotNull(button);
AccessibilityNodeInfo parent = button.getParent();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
@@ -410,71 +430,4 @@
}
}
}
-
- private void bringUpActivityWithInitalizedAccessbility() {
- mLastAccessibilityEvent = null;
- // bring up the activity
- getActivity();
-
- final long startTimeMillis = SystemClock.uptimeMillis();
- while (true) {
- if (mLastAccessibilityEvent != null) {
- final int eventType = mLastAccessibilityEvent.getEventType();
- if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
- return;
- }
- }
- final long remainingTimeMillis = TIMEOUT_ACCESSIBLITY_STATE_INITIALIZED_MILLIS
- - (SystemClock.uptimeMillis() - startTimeMillis);
- if (remainingTimeMillis <= 0) {
- return;
- }
- synchronized (this) {
- try {
- wait(remainingTimeMillis);
- } catch (InterruptedException e) {
- /* ignore */
- }
- }
- }
- }
-
- private void ensureConnection() throws Exception {
- if (sConnectionId == View.NO_ID) {
- IEventListener listener = new IEventListener.Stub() {
- public void setConnection(IAccessibilityServiceConnection connection,
- int connectionId) {
- sConnectionId = connectionId;
- if (connection != null) {
- AccessibilityInteractionClient.getInstance().addConnection(connectionId,
- connection);
- } else {
- AccessibilityInteractionClient.getInstance().removeConnection(connectionId);
- }
- synchronized (this) {
- notifyAll();
- }
- }
-
- public void onInterrupt() {}
-
- public void onAccessibilityEvent(AccessibilityEvent event) {
- mLastAccessibilityEvent = AccessibilityEvent.obtain(event);
- synchronized (this) {
- notifyAll();
- }
- }
- };
-
- AccessibilityManager accessibilityManager =
- AccessibilityManager.getInstance(getInstrumentation().getContext());
-
- synchronized (this) {
- IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
- ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
- manager.registerEventListener(listener);
- wait(TIMEOUT_PROPAGATE_ACCESSIBLITY_SETTING);
- }
- }
- }
}
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index 23fa94a..8bda755 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -46,7 +46,6 @@
import android.util.Slog;
import android.util.SparseArray;
import android.view.IWindow;
-import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -96,6 +95,10 @@
private static final int DO_SET_SERVICE_INFO = 10;
+ public static final int ACTIVE_WINDOW_ID = -1;
+
+ public static final long ROOT_NODE_ID = -1;
+
private static int sNextWindowId;
final HandlerCaller mCaller;
@@ -467,7 +470,8 @@
}
}
- public void registerEventListener(IEventListener listener) {
+ public void registerUiTestAutomationService(IEventListener listener,
+ AccessibilityServiceInfo accessibilityServiceInfo) {
mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT,
FUNCTION_REGISTER_EVENT_LISTENER);
ComponentName componentName = new ComponentName("foo.bar",
@@ -490,13 +494,23 @@
}
}
// Hook the automation service up.
- AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
- accessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
- accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
Service service = new Service(componentName, accessibilityServiceInfo, true);
service.onServiceConnected(componentName, listener.asBinder());
}
+ public void unregisterUiTestAutomationService(IEventListener listener) {
+ synchronized (mLock) {
+ final int serviceCount = mServices.size();
+ for (int i = 0; i < serviceCount; i++) {
+ Service service = mServices.get(i);
+ if (service.mServiceInterface == listener && service.mIsAutomation) {
+ // Automation service is not bound, so pretend it died to perform clean up.
+ service.binderDied();
+ }
+ }
+ }
+ }
+
/**
* Removes an AccessibilityInteractionConnection.
*
@@ -1070,10 +1084,11 @@
}
}
- public float findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId,
- int interactionId, IAccessibilityInteractionConnectionCallback callback,
- long interrogatingTid)
+ public float findAccessibilityNodeInfoByViewId(int accessibilityWindowId,
+ long accessibilityNodeId, int viewId, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
+ final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId);
IAccessibilityInteractionConnection connection = null;
synchronized (mLock) {
mSecurityPolicy.enforceCanRetrieveWindowContent(this);
@@ -1081,12 +1096,8 @@
if (!permissionGranted) {
return 0;
} else {
- connection = getConnectionToRetrievalAllowingWindowLocked();
+ connection = getConnectionLocked(resolvedWindowId);
if (connection == null) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "No interaction connection to a retrieve "
- + "allowing window.");
- }
return 0;
}
}
@@ -1094,44 +1105,33 @@
final int interrogatingPid = Binder.getCallingPid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.findAccessibilityNodeInfoByViewId(viewId, interactionId, callback,
- interrogatingPid, interrogatingTid);
+ connection.findAccessibilityNodeInfoByViewId(accessibilityNodeId, viewId,
+ interactionId, callback, interrogatingPid, interrogatingTid);
} catch (RemoteException re) {
if (DEBUG) {
- Slog.e(LOG_TAG, "Error finding node.");
+ Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId().");
}
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- return getCompatibilityScale(mSecurityPolicy.getRetrievalAllowingWindowLocked());
+ return getCompatibilityScale(resolvedWindowId);
}
- public float findAccessibilityNodeInfosByTextInActiveWindow(
- String text, int interactionId,
- IAccessibilityInteractionConnectionCallback callback, long threadId)
- throws RemoteException {
- return findAccessibilityNodeInfosByText(text,
- mSecurityPolicy.mRetrievalAlowingWindowId, View.NO_ID, interactionId, callback,
- threadId);
- }
-
- public float findAccessibilityNodeInfosByText(String text,
- int accessibilityWindowId, long accessibilityNodeId, int interactionId,
+ public float findAccessibilityNodeInfosByText(int accessibilityWindowId,
+ long accessibilityNodeId, String text, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
+ final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId);
IAccessibilityInteractionConnection connection = null;
synchronized (mLock) {
mSecurityPolicy.enforceCanRetrieveWindowContent(this);
final boolean permissionGranted =
- mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, accessibilityWindowId);
+ mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
return 0;
} else {
- connection = getConnectionToRetrievalAllowingWindowLocked();
+ connection = getConnectionLocked(resolvedWindowId);
if (connection == null) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "No interaction connection to focused window.");
- }
return 0;
}
}
@@ -1139,40 +1139,35 @@
final int interrogatingPid = Binder.getCallingPid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.findAccessibilityNodeInfosByText(text, accessibilityNodeId,
+ connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text,
interactionId, callback, interrogatingPid, interrogatingTid);
} catch (RemoteException re) {
if (DEBUG) {
- Slog.e(LOG_TAG, "Error finding node.");
+ Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()");
}
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- return getCompatibilityScale(accessibilityWindowId);
+ return getCompatibilityScale(resolvedWindowId);
}
public float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId,
long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
+ final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId);
IAccessibilityInteractionConnection connection = null;
synchronized (mLock) {
mSecurityPolicy.enforceCanRetrieveWindowContent(this);
final boolean permissionGranted =
- mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, accessibilityWindowId);
+ mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
return 0;
} else {
- AccessibilityConnectionWrapper wrapper =
- mWindowIdToInteractionConnectionWrapperMap.get(accessibilityWindowId);
- if (wrapper == null) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "No interaction connection to window: "
- + accessibilityWindowId);
- }
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
return 0;
}
- connection = wrapper.mConnection;
}
}
final int interrogatingPid = Binder.getCallingPid();
@@ -1182,35 +1177,29 @@
interactionId, callback, interrogatingPid, interrogatingTid);
} catch (RemoteException re) {
if (DEBUG) {
- Slog.e(LOG_TAG, "Error requesting node with accessibilityNodeId: "
- + accessibilityNodeId);
+ Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- return getCompatibilityScale(accessibilityWindowId);
+ return getCompatibilityScale(resolvedWindowId);
}
public boolean performAccessibilityAction(int accessibilityWindowId,
long accessibilityNodeId, int action, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) {
+ final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId);
IAccessibilityInteractionConnection connection = null;
synchronized (mLock) {
final boolean permissionGranted = mSecurityPolicy.canPerformActionLocked(this,
- accessibilityWindowId, action);
+ resolvedWindowId, action);
if (!permissionGranted) {
return false;
} else {
- AccessibilityConnectionWrapper wrapper =
- mWindowIdToInteractionConnectionWrapperMap.get(accessibilityWindowId);
- if (wrapper == null) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "No interaction connection to window: "
- + accessibilityWindowId);
- }
+ connection = getConnectionLocked(resolvedWindowId);
+ if (connection == null) {
return false;
}
- connection = wrapper.mConnection;
}
}
final int interrogatingPid = Binder.getCallingPid();
@@ -1220,8 +1209,7 @@
callback, interrogatingPid, interrogatingTid);
} catch (RemoteException re) {
if (DEBUG) {
- Slog.e(LOG_TAG, "Error requesting node with accessibilityNodeId: "
- + accessibilityNodeId);
+ Slog.e(LOG_TAG, "Error calling performAccessibilityAction()");
}
} finally {
Binder.restoreCallingIdentity(identityToken);
@@ -1265,14 +1253,26 @@
}
}
- private IAccessibilityInteractionConnection getConnectionToRetrievalAllowingWindowLocked() {
- final int windowId = mSecurityPolicy.getRetrievalAllowingWindowLocked();
+ private IAccessibilityInteractionConnection getConnectionLocked(int windowId) {
if (DEBUG) {
Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
}
- AccessibilityConnectionWrapper wrapper =
- mWindowIdToInteractionConnectionWrapperMap.get(windowId);
- return (wrapper != null) ? wrapper.mConnection : null;
+ AccessibilityConnectionWrapper wrapper = mWindowIdToInteractionConnectionWrapperMap.get(
+ windowId);
+ if (wrapper != null && wrapper.mConnection != null) {
+ return wrapper.mConnection;
+ }
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
+ }
+ return null;
+ }
+
+ private int resolveAccessibilityWindowId(int accessibilityWindowId) {
+ if (accessibilityWindowId == ACTIVE_WINDOW_ID) {
+ return mSecurityPolicy.mRetrievalAlowingWindowId;
+ }
+ return accessibilityWindowId;
}
private float getCompatibilityScale(int windowId) {