From 79311c4af8b54d3cd47ab37a120c648bfc990511 Mon Sep 17 00:00:00 2001 From: Svetoslav Ganov Date: Tue, 17 Jan 2012 20:24:26 -0800 Subject: Speedup the accessibility window querying APIs and clean up. 1. Now when an interrogating client requires an AccessibilibtyNodeInfo we aggressively prefetch all the predecessors of that node and its descendants. The number of fetched nodes in one call is limited to keep the APIs responsive. The prefetched nodes infos are cached in the client process. The node info cache is invalidated partially or completely based on the fired accessibility events. For example, TYPE_WINDOW_STATE_CHANGED event clears the cache while TYPE_VIEW_FOCUSED removed the focused node from the cache, etc. Note that the cache is only for the currently active window. The ViewRootImple also keeps track of only the ids of the node infos it has sent to each querying process to avoid duplicating work. Usually only one process will query the screen content but we support the general case. Also all the caches are automatically invalidated so not additional bookkeeping is required. This simple strategy leads to 10X improving the speed of the querying APIs. 2. The Monkey and UI test automation framework were registering a raw event listener for accessibility events and hence perform connection and cache management in similar way to an AccessibilityService. This is fragile and requires the implementer to know internal framework stuff. Now the functionality required by the Monkey and the UI automation is encapsulated in a new UiTestAutomationBridge class. To enable this was requited some refactoring of AccessibilityService. 3. Removed the *doSomethiong*InActiveWindow methods from the AccessibilityInteractionClient and the AccessibilityInteractionConnection. The function of these methods is implemented by the not *InActiveWindow version while passing appropriate constants. 4. Updated the internal window Querying tests to use the new UiTestAutomationBridge. 5. If the ViewRootImple was not initialized the querying APIs of the IAccessibilityInteractionConnection implementation were returning immediately without calling the callback with null. This was causing the client side to wait until it times out. Now the client is notified as soon as the call fails. 6. Added a check to guarantee that Views with AccessibilityNodeProvider do not have children. bug:5879530 Change-Id: I3ee43718748fec6e570992c7073c8f6f1fc269b3 --- .../accessibilityservice/AccessibilityService.java | 57 ++- .../IAccessibilityServiceConnection.aidl | 66 ++-- .../UiTestAutomationBridge.java | 418 +++++++++++++++++++++ .../android/view/AccessibilityNodeInfoCache.java | 171 +++++++++ core/java/android/view/View.java | 5 + core/java/android/view/ViewGroup.java | 5 + core/java/android/view/ViewRootImpl.java | 194 ++++++++-- .../AccessibilityInteractionClient.java | 141 ++++--- .../view/accessibility/AccessibilityNodeInfo.java | 15 +- .../view/accessibility/AccessibilityRecord.java | 11 + .../IAccessibilityInteractionConnection.aidl | 8 +- .../view/accessibility/IAccessibilityManager.aidl | 4 +- .../InterrogationActivity.java | 4 +- .../InterrogationActivityTest.java | 217 +++++------ .../accessibility/AccessibilityManagerService.java | 128 +++---- 15 files changed, 1111 insertions(+), 333 deletions(-) create mode 100644 core/java/android/accessibilityservice/UiTestAutomationBridge.java create mode 100644 core/java/android/view/AccessibilityNodeInfoCache.java diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 211be52e0622..a463a62c5045 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 @@ public abstract class AccessibilityService extends Service { 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 @@ public abstract class AccessibilityService extends Service { */ @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 @@ public abstract class AccessibilityService extends Service { 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 @@ public abstract class AccessibilityService extends Service { 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 e53b31395bb1..c9468eb11f23 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -32,8 +32,13 @@ interface IAccessibilityServiceConnection { /** * 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 @@ interface IAccessibilityServiceConnection { /** * 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 000000000000..9d48efc66dc3 --- /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}. + * + * Note: This method is NOT + * executed on the application main thread. The client are + * responsible for proper synchronization. + * + * @param event The received event. + */ + public void onAccessibilityEvent(AccessibilityEvent event) { + /* hook - do nothing */ + } + + /** + * Callback for requests to stop feedback. + * + * Note: This method is NOT + * executed on the application main thread. The client are + * responsible for proper synchronization. + */ + public void onInterrupt() { + /* hook - do nothing */ + } + + /** + * Connects this service. + * + * @throws IllegalStateException If already connected. + */ + public void connect() { + if (isConnected()) { + throw new IllegalStateException("Already connected."); + } + + // Serialize binder calls to a handler on a dedicated thread + // different from the main since we expose APIs that block + // the main thread waiting for a result the deliver of which + // on the main thread will prevent that thread from waking up. + // The serialization is needed also to ensure that events are + // examined in delivery order. Otherwise, a fair locking + // is needed for making sure the binder calls are interleaved + // with check for the expected event and also to make sure the + // binder threads are allowed to proceed in the received order. + 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 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 findAccessibilityNodeInfosByTextInActiveWindow(String text) { + return findAccessibilityNodeInfosByText(ACTIVE_WINDOW_ID, ROOT_NODE_ID, text); + } + + /** + * Finds {@link AccessibilityNodeInfo}s by View text. The match is case + * insensitive containment. The search is performed in the window whose + * id is specified and starts from the node whose accessibility id is + * specified. + * + * @param accessibilityWindowId A unique window id. Use {@link #ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use {@link #ROOT_NODE_ID} to start from the root. + * @param text The searched text. + * @return The current window scale, where zero means a failure. + */ + public List findAccessibilityNodeInfosByText(int accessibilityWindowId, + long accessibilityNodeId, String text) { + // Cache the id to avoid locking + final int connectionId = mConnectionId; + ensureValidConnection(connectionId); + return AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfosByText(connectionId, accessibilityWindowId, + accessibilityNodeId, text); + } + + /** + * Performs an accessibility action on an {@link AccessibilityNodeInfo} + * in the active window. + * + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). + * @param action The action to perform. + * @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 000000000000..244a49191f74 --- /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 + * null values. It also tracks accessibility events + * and invalidates accordingly. + * + * @hide + */ +public class AccessibilityNodeInfoCache { + + private final boolean ENABLED = true; + + /** + * @return A new not synchronized AccessibilityNodeInfoCache. + */ + public static AccessibilityNodeInfoCache newAccessibilityNodeInfoCache() { + return new AccessibilityNodeInfoCache(); + } + + /** + * @return A new synchronized 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 mCacheImpl; + + private AccessibilityNodeInfoCache() { + if (ENABLED) { + mCacheImpl = new LongSparseArray(); + } 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 8cac57d21572..1b6e0ac12f11 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12654,6 +12654,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal 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 5c63366f1b40..8652366517dd 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3352,6 +3352,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager 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 1a4bdf4bc373..e43258fba87b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -52,6 +52,7 @@ import android.util.AndroidRuntimeException; 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; @@ -301,6 +302,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent; + AccessibilityPrefetchStrategy mAccessibilityPrefetchStrategy; + private final int mDensity; /** @@ -3480,6 +3483,17 @@ public final class ViewRootImpl extends Handler implements ViewParent, 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 { @@ -3980,6 +3994,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (mView == null) { return false; } + getAccessibilityPrefetchStrategy().onAccessibilityEvent(event); mAccessibilityManager.sendAccessibilityEvent(event); return true; } @@ -4539,6 +4554,13 @@ public final class ViewRootImpl extends Handler implements ViewParent, 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 */ + } } } @@ -4550,28 +4572,49 @@ public final class ViewRootImpl extends Handler implements ViewParent, 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); + .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(String text, long accessibilityNodeId, + 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(text, accessibilityNodeId, + .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 */ + } } } } @@ -4649,6 +4692,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, 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); @@ -4671,40 +4715,47 @@ public final class ViewRootImpl extends Handler implements ViewParent, 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 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 @@ -4720,17 +4771,26 @@ public final class ViewRootImpl extends Handler implements ViewParent, } 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 { @@ -4741,8 +4801,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - 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(); @@ -4934,4 +4994,88 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } } + + /** + * 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 mAccessibilityNodeInfoCaches = + new LongSparseArray(); + + 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 outInfos) { + addAndCacheNotCachedNodeInfo(interrogatingPid, root, outInfos); + addAndCacheNotCachedPredecessorInfos(interrogatingPid, root, outInfos); + addAndCacheNotCachedDescendantInfos(interrogatingPid, root, outInfos); + } + + private void addAndCacheNotCachedNodeInfo(long interrogatingPid, + View view, List 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 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 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 95c070cf5b2c..072fdd86ed08 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -24,7 +24,9 @@ import android.os.SystemClock; 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 @@ public final class AccessibilityInteractionClient private static final SparseArray sConnectionCache = new SparseArray(); + // 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 @@ public final class AccessibilityInteractionClient * 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 @@ public final class AccessibilityInteractionClient 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 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 @@ public final class AccessibilityInteractionClient } /** - * 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( @@ -218,66 +241,29 @@ public final class AccessibilityInteractionClient return null; } - /** - * 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 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 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 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 @@ public final class AccessibilityInteractionClient * 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 @@ public final class AccessibilityInteractionClient accessibilityWindowId, accessibilityNodeId, action, interactionId, this, Thread.currentThread().getId()); if (success) { - return getPerformAccessibilityActionResult(interactionId); + return getPerformAccessibilityActionResultAndClear(interactionId); } } else { if (DEBUG) { @@ -334,6 +322,24 @@ public final class AccessibilityInteractionClient 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 @@ public final class AccessibilityInteractionClient if (interactionId > mInteractionId) { mFindAccessibilityNodeInfoResult = info; mInteractionId = interactionId; + if (info != null) { + sAccessibilityNodeInfoCache.put(info.getSourceNodeId(), info); + } } mInstanceLock.notifyAll(); } @@ -386,8 +395,20 @@ public final class AccessibilityInteractionClient 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(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 @@ public final class AccessibilityInteractionClient * @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 6939c2cf1ddf..d7d67928e158 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -380,8 +380,8 @@ public class AccessibilityNodeInfo implements Parcelable { return Collections.emptyList(); } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfosByText(mConnectionId, text, mWindowId, - mSourceNodeId); + return client.findAccessibilityNodeInfosByText(mConnectionId, mWindowId, mSourceNodeId, + text); } /** @@ -902,6 +902,17 @@ public class AccessibilityNodeInfo implements Parcelable { return 0; } + /** + * Gets the id of the source node. + * + * @return The id. + * + * @hide + */ + public long getSourceNodeId() { + return mSourceNodeId; + } + /** * Sets if this instance is sealed. * diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 07aeb9ae5b7d..b60f50eb5d9d 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -563,6 +563,17 @@ public class AccessibilityRecord { mParcelableData = parcelableData; } + /** + * 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 a90c427f4ab8..ae6869cad9f5 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -31,13 +31,13 @@ oneway interface IAccessibilityInteractionConnection { 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 c3794bec3ef0..320c75da0233 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -49,5 +49,7 @@ interface IAccessibilityManager { 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 b4a05819020b..a9f144bdca49 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 259a09448e2f..fa4809331ac3 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 @@ import java.util.Queue; */ public class InterrogationActivityTest extends ActivityInstrumentationTestCase2 { - 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 @@ public class InterrogationActivityTest @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() { + @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 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view by text - List buttons = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfosByTextInActiveWindow(sConnectionId, "butto"); + List buttons = mUiTestAutomationBridge + .findAccessibilityNodeInfosByTextInActiveWindow("butto"); assertEquals(9, buttons.size()); } finally { if (DEBUG) { @@ -141,12 +153,9 @@ public class InterrogationActivityTest public void testFindAccessibilityNodeInfoByViewTextContentDescription() throws Exception { final long startTimeMillis = SystemClock.uptimeMillis(); try { - bringUpActivityWithInitalizedAccessbility(); - // find a view by text - List buttons = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfosByTextInActiveWindow(sConnectionId, - "contentDescription"); + List buttons = mUiTestAutomationBridge + .findAccessibilityNodeInfosByTextInActiveWindow("contentDescription"); assertEquals(1, buttons.size()); } finally { if (DEBUG) { @@ -177,8 +186,8 @@ public class InterrogationActivityTest 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 fringe = new LinkedList(); @@ -216,16 +225,16 @@ public class InterrogationActivityTest 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 @@ public class InterrogationActivityTest 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 @@ public class InterrogationActivityTest 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 @@ public class InterrogationActivityTest 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 @@ public class InterrogationActivityTest 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()); - - // focus the view - assertTrue(button.performAction(ACTION_FOCUS)); + final AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); + assertFalse(button.isFocused()); - 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() { + @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 @@ public class InterrogationActivityTest 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 @@ public class InterrogationActivityTest } } } - - 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 23fa94a5a888..8bda7559b729 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.text.TextUtils.SimpleStringSplitter; 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 class AccessibilityManagerService extends IAccessibilityManager.Stub } } - 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } // 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 class AccessibilityManagerService extends IAccessibilityManager.Stub } } - 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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()); - } - - public float findAccessibilityNodeInfosByTextInActiveWindow( - String text, int interactionId, - IAccessibilityInteractionConnectionCallback callback, long threadId) - throws RemoteException { - return findAccessibilityNodeInfosByText(text, - mSecurityPolicy.mRetrievalAlowingWindowId, View.NO_ID, interactionId, callback, - threadId); + return getCompatibilityScale(resolvedWindowId); } - 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - 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) { -- cgit v1.2.3-59-g8ed1b