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 8597017a6498..d80d0801f2c9 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12679,6 +12679,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 75598620a362..d3af61841295 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3356,6 +3356,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 2ef843ba5e17..3f61e6b38fe8 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; @@ -302,6 +303,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent; + AccessibilityPrefetchStrategy mAccessibilityPrefetchStrategy; + private final int mDensity; /** @@ -3483,6 +3486,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 { @@ -3983,6 +3997,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (mView == null) { return false; } + getAccessibilityPrefetchStrategy().onAccessibilityEvent(event); mAccessibilityManager.sendAccessibilityEvent(event); return true; } @@ -4542,6 +4557,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 */ + } } } @@ -4553,28 +4575,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); - } - } - - 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 @@ 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); @@ -4674,40 +4718,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 @@ -4723,17 +4774,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 { @@ -4744,8 +4804,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(); @@ -4937,4 +4997,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()); + 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() { + @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()); + 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 @@ 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) {