diff options
33 files changed, 1291 insertions, 635 deletions
diff --git a/Android.mk b/Android.mk index f45f9779c9be..1fb6b52d8fc8 100644 --- a/Android.mk +++ b/Android.mk @@ -78,8 +78,9 @@ LOCAL_SRC_FILES += \ core/java/android/app/IThumbnailReceiver.aidl \ core/java/android/app/IThumbnailRetriever.aidl \ core/java/android/app/ITransientNotification.aidl \ + core/java/android/app/IUiAutomationConnection.aidl \ core/java/android/app/IUiModeManager.aidl \ - core/java/android/app/IUserSwitchObserver.aidl \ + core/java/android/app/IUserSwitchObserver.aidl \ core/java/android/app/IWallpaperManager.aidl \ core/java/android/app/IWallpaperManagerCallback.aidl \ core/java/android/app/admin/IDevicePolicyManager.aidl \ diff --git a/api/current.txt b/api/current.txt index d367a00bc347..43c5b0f45b2f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2103,6 +2103,7 @@ package android.accessibilityservice { field public static final int FEEDBACK_SPOKEN = 1; // 0x1 field public static final int FEEDBACK_VISUAL = 8; // 0x8 field public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 2; // 0x2 + field public static final int FLAG_REPORT_VIEW_IDS = 8; // 0x8 field public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 4; // 0x4 field public int eventTypes; field public int feedbackType; @@ -3579,6 +3580,7 @@ package android.app { method public android.content.ComponentName getComponentName(); method public android.content.Context getContext(); method public android.content.Context getTargetContext(); + method public android.app.UiAutomation getUiAutomation(); method public boolean invokeContextMenuAction(android.app.Activity, int, int); method public boolean invokeMenuActionSync(android.app.Activity, int, int); method public boolean isProfiling(); @@ -4136,6 +4138,26 @@ package android.app { method public abstract void onTimeSet(android.widget.TimePicker, int, int); } + public final class UiAutomation { + method public android.view.accessibility.AccessibilityEvent executeAndWaitForEvent(java.lang.Runnable, com.android.internal.util.Predicate<android.view.accessibility.AccessibilityEvent>, long) throws java.util.concurrent.TimeoutException; + method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow(); + method public boolean injectInputEvent(android.view.InputEvent, boolean); + method public void setOnAccessibilityEventListener(android.app.UiAutomation.OnAccessibilityEventListener); + method public boolean setRotation(int); + method public android.graphics.Bitmap takeScreenshot(); + method public void waitForIdle(long, long) throws java.util.concurrent.TimeoutException; + field public static final int ROTATION_FREEZE_0 = 0; // 0x0 + field public static final int ROTATION_FREEZE_180 = 2; // 0x2 + field public static final int ROTATION_FREEZE_270 = 3; // 0x3 + field public static final int ROTATION_FREEZE_90 = 1; // 0x1 + field public static final int ROTATION_FREEZE_CURRENT = -1; // 0xffffffff + field public static final int ROTATION_UNFREEZE = -2; // 0xfffffffe + } + + public static abstract interface UiAutomation.OnAccessibilityEventListener { + method public abstract void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent); + } + public class UiModeManager { method public void disableCarMode(int); method public void enableCarMode(int); @@ -21294,6 +21316,7 @@ package android.test { ctor public InstrumentationTestRunner(); method public junit.framework.TestSuite getAllTests(); method protected android.test.AndroidTestRunner getAndroidTestRunner(); + method protected android.os.Bundle getArguments(); method public java.lang.ClassLoader getLoader(); method public junit.framework.TestSuite getTestSuite(); field public static final java.lang.String REPORT_KEY_NAME_CLASS = "class"; @@ -26240,6 +26263,7 @@ package android.view.accessibility { method public void addChild(android.view.View, int); method public int describeContents(); method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(java.lang.String); + method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(java.lang.String); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); method public android.view.accessibility.AccessibilityNodeInfo focusSearch(int); method public int getActions(); @@ -26255,6 +26279,7 @@ package android.view.accessibility { method public java.lang.CharSequence getPackageName(); method public android.view.accessibility.AccessibilityNodeInfo getParent(); method public java.lang.CharSequence getText(); + method public java.lang.CharSequence getViewId(); method public int getWindowId(); method public boolean isAccessibilityFocused(); method public boolean isCheckable(); @@ -26302,6 +26327,7 @@ package android.view.accessibility { method public void setSource(android.view.View); method public void setSource(android.view.View, int); method public void setText(java.lang.CharSequence); + method public void setViewId(java.lang.CharSequence); method public void setVisibleToUser(boolean); method public void writeToParcel(android.os.Parcel, int); field public static final int ACTION_ACCESSIBILITY_FOCUS = 64; // 0x40 diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java index add7a23e4138..b5574cf9180b 100644 --- a/cmds/am/src/com/android/commands/am/Am.java +++ b/cmds/am/src/com/android/commands/am/Am.java @@ -24,6 +24,7 @@ import android.app.IActivityController; import android.app.IActivityManager; import android.app.IInstrumentationWatcher; import android.app.Instrumentation; +import android.app.UiAutomationConnection; import android.content.ComponentName; import android.content.Context; import android.content.IIntentReceiver; @@ -661,10 +662,13 @@ public class Am { if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg); InstrumentationWatcher watcher = null; + UiAutomationConnection connection = null; if (wait) { watcher = new InstrumentationWatcher(); watcher.setRawOutput(rawMode); + connection = new UiAutomationConnection(); } + float[] oldAnims = null; if (no_window_animation) { oldAnims = wm.getAnimationScales(); @@ -672,7 +676,7 @@ public class Am { wm.setAnimationScale(1, 0.0f); } - if (!mAm.startInstrumentation(cn, profileFile, 0, args, watcher, userId)) { + if (!mAm.startInstrumentation(cn, profileFile, 0, args, watcher, connection, userId)) { throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString()); } diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 7efe189fe015..c5f51dc25a2e 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -339,7 +339,10 @@ public abstract class AccessibilityService extends Service { private static final String LOG_TAG = "AccessibilityService"; - interface Callbacks { + /** + * @hide + */ + public interface Callbacks { public void onAccessibilityEvent(AccessibilityEvent event); public void onInterrupt(); public void onServiceConnected(); @@ -538,8 +541,10 @@ public abstract class AccessibilityService extends Service { /** * Implements the internal {@link IAccessibilityServiceClient} interface to convert * incoming calls to it back to calls on an {@link AccessibilityService}. + * + * @hide */ - static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub + public static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback { static final int NO_ID = -1; diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 75a4f838ac1b..2006bc73f807 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -33,6 +33,7 @@ import android.util.TypedValue; import android.util.Xml; import android.view.View; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -152,6 +153,15 @@ public class AccessibilityServiceInfo implements Parcelable { public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE= 0x0000004; /** + * This flag requests that the {@link AccessibilityNodeInfo}s obtained + * by an {@link AccessibilityService} contain the id of the source view. + * The source view id will be a fully qualified resource name of the + * form "package:id/name", for example "foo.bar:id/my_list", and it is + * useful for UI test automation. This flag is not set by default. + */ + public static final int FLAG_REPORT_VIEW_IDS = 0x00000008; + + /** * The event types an {@link AccessibilityService} is interested in. * <p> * <strong>Can be dynamically set at runtime.</strong> diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index f33f503ef1ff..7a29f359678b 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -85,15 +85,15 @@ interface IAccessibilityServiceConnection { * where to start the search. Use * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} * to start from the root. - * @param id The id of the node. + * @param viewId The fully qualified resource name of the view id to find. * @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 Whether the call succeeded. */ - boolean findAccessibilityNodeInfoByViewId(int accessibilityWindowId, long accessibilityNodeId, - int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - long threadId); + boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, + long accessibilityNodeId, String viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long threadId); /** * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the specified diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java deleted file mode 100644 index 6837386e3af6..000000000000 --- a/core/java/android/accessibilityservice/UiTestAutomationBridge.java +++ /dev/null @@ -1,496 +0,0 @@ -/* - * 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.IAccessibilityServiceClientWrapper; -import android.content.Context; -import android.os.Bundle; -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(); - - private static final int TIMEOUT_REGISTER_SERVICE = 5000; - - public static final int ACTIVE_WINDOW_ID = AccessibilityNodeInfo.ACTIVE_WINDOW_ID; - - public static final long ROOT_NODE_ID = AccessibilityNodeInfo.ROOT_NODE_ID; - - public static final int UNDEFINED = -1; - - private static final int FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS = - AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS - | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS - | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS; - - private final Object mLock = new Object(); - - private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; - - private IAccessibilityServiceClientWrapper mListener; - - private AccessibilityEvent mLastEvent; - - private volatile boolean mWaitingForEventDelivery; - - private volatile boolean mUnprocessedEventAvailable; - - private HandlerThread mHandlerThread; - - /** - * Gets the last received {@link AccessibilityEvent}. - * - * @return The event. - */ - public AccessibilityEvent getLastAccessibilityEvent() { - return mLastEvent; - } - - /** - * Callback for receiving an {@link AccessibilityEvent}. - * - * <strong>Note:</strong> This method is <strong>NOT</strong> - * executed on the application main thread. The client are - * responsible for proper synchronization. - * - * @param event The received event. - */ - public void onAccessibilityEvent(AccessibilityEvent event) { - /* hook - do nothing */ - } - - /** - * Callback for requests to stop feedback. - * - * <strong>Note:</strong> This method is <strong>NOT</strong> - * executed on the application main thread. The client are - * responsible for proper synchronization. - */ - public void onInterrupt() { - /* hook - do nothing */ - } - - /** - * Connects this service. - * - * @throws IllegalStateException If already connected. - */ - public void connect() { - if (isConnected()) { - throw new IllegalStateException("Already connected."); - } - - // Serialize binder calls to a handler on a dedicated thread - // different from the main since we expose APIs that block - // the main thread waiting for a result the deliver of which - // on the main thread will prevent that thread from waking up. - // The serialization is needed also to ensure that events are - // examined in delivery order. Otherwise, a fair locking - // is needed for making sure the binder calls are interleaved - // with check for the expected event and also to make sure the - // binder threads are allowed to proceed in the received order. - mHandlerThread = new HandlerThread("UiTestAutomationBridge"); - mHandlerThread.setDaemon(true); - mHandlerThread.start(); - Looper looper = mHandlerThread.getLooper(); - - mListener = new IAccessibilityServiceClientWrapper(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) { - mLastEvent = AccessibilityEvent.obtain(event); - if (!mWaitingForEventDelivery) { - mLock.notifyAll(); - break; - } - if (!mUnprocessedEventAvailable) { - mUnprocessedEventAvailable = true; - 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(); - } - } - - @Override - public boolean onGesture(int gestureId) { - return false; - } - }); - - 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; - info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; - - 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."); - } - - mHandlerThread.quit(); - - IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( - ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); - - try { - manager.unregisterUiTestAutomationService(mListener); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while unregistering UiTestAutomationService", re); - } - } - - /** - * Gets whether this service is connected. - * - * @return True if connected. - */ - public boolean isConnected() { - return (mConnectionId != AccessibilityInteractionClient.NO_ID); - } - - /** - * Executes a command and waits for a specific accessibility event type up - * to a given timeout. - * - * @param command The command to execute before starting to wait for the event. - * @param predicate Predicate for recognizing the awaited event. - * @param timeoutMillis The max wait time in milliseconds. - */ - public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, - Predicate<AccessibilityEvent> predicate, long timeoutMillis) - throws TimeoutException, Exception { - // TODO: This is broken - remove from here when finalizing this as public APIs. - 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 */ - } - } - } - } - - /** - * Waits for the accessibility event stream to become idle, which is not to - * have received a new accessibility event within <code>idleTimeout</code>, - * and do so within a maximal global timeout as specified by - * <code>globalTimeout</code>. - * - * @param idleTimeout The timeout between two event to consider the device idle. - * @param globalTimeout The maximal global timeout in which to wait for idle. - */ - public void waitForIdle(long idleTimeout, long globalTimeout) { - final long startTimeMillis = SystemClock.uptimeMillis(); - long lastEventTime = (mLastEvent != null) - ? mLastEvent.getEventTime() : SystemClock.uptimeMillis(); - synchronized (mLock) { - while (true) { - final long currentTimeMillis = SystemClock.uptimeMillis(); - final long sinceLastEventTimeMillis = currentTimeMillis - lastEventTime; - if (sinceLastEventTimeMillis > idleTimeout) { - return; - } - if (mLastEvent != null) { - lastEventTime = mLastEvent.getEventTime(); - } - final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; - final long remainingTimeMillis = globalTimeout - elapsedTimeMillis; - if (remainingTimeMillis <= 0) { - return; - } - try { - mLock.wait(idleTimeout); - } catch (InterruptedException e) { - /* 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, - FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS); - } - - /** - * Finds an {@link AccessibilityNodeInfo} by View id in the active - * window. The search is performed from the root node. - * - * @param viewId The id of a View. - * @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. - * @param viewId The id of a View. - * @return The current window scale, where zero means a failure. - */ - public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int accessibilityWindowId, - long accessibilityNodeId, int viewId) { - // Cache the id to avoid locking - final int connectionId = mConnectionId; - ensureValidConnection(connectionId); - return AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewId(connectionId, accessibilityWindowId, - accessibilityNodeId, viewId); - } - - /** - * Finds {@link AccessibilityNodeInfo}s by View text in the active - * window. The search is performed from the root node. - * - * @param text The searched text. - * @return The current window scale, where zero means a failure. - */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(String text) { - return findAccessibilityNodeInfosByText(ACTIVE_WINDOW_ID, ROOT_NODE_ID, text); - } - - /** - * Finds {@link AccessibilityNodeInfo}s by View text. The match is case - * insensitive containment. The search is performed in the window whose - * id is specified and starts from the node whose accessibility id is - * specified. - * - * @param accessibilityWindowId A unique window id. Use - * {@link #ACTIVE_WINDOW_ID} to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use {@link #ROOT_NODE_ID} to start from the root. - * @param text The searched text. - * @return The current window scale, where zero means a failure. - */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int accessibilityWindowId, - long accessibilityNodeId, String text) { - // Cache the id to avoid locking - final int connectionId = mConnectionId; - ensureValidConnection(connectionId); - return AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfosByText(connectionId, accessibilityWindowId, - accessibilityNodeId, text); - } - - /** - * Performs an accessibility action on an {@link AccessibilityNodeInfo} - * in the active window. - * - * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). - * @param action The action to perform. - * @param arguments Optional action arguments. - * @return Whether the action was performed. - */ - public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action, - Bundle arguments) { - return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action, arguments); - } - - /** - * 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. - * @param arguments Optional action arguments. - * @return Whether the action was performed. - */ - public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, - int action, Bundle arguments) { - // Cache the id to avoid locking - final int connectionId = mConnectionId; - ensureValidConnection(connectionId); - return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId, - accessibilityWindowId, accessibilityNodeId, action, arguments); - } - - /** - * Gets the root {@link AccessibilityNodeInfo} in the active window. - * - * @return The root info. - */ - public AccessibilityNodeInfo getRootAccessibilityNodeInfoInActiveWindow() { - // Cache the id to avoid locking - final int connectionId = mConnectionId; - ensureValidConnection(connectionId); - return AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByAccessibilityId(connectionId, ACTIVE_WINDOW_ID, - ROOT_NODE_ID, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); - } - - private void ensureValidConnection(int connectionId) { - if (connectionId == UNDEFINED) { - throw new IllegalStateException("UiAutomationService not connected." - + " Did you call #register()?"); - } - } -} diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 61b206756c68..ee126f440301 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -39,7 +39,6 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; -import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.util.Singleton; @@ -836,8 +835,10 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM Bundle arguments = data.readBundle(); IBinder b = data.readStrongBinder(); IInstrumentationWatcher w = IInstrumentationWatcher.Stub.asInterface(b); + b = data.readStrongBinder(); + IUiAutomationConnection c = IUiAutomationConnection.Stub.asInterface(b); int userId = data.readInt(); - boolean res = startInstrumentation(className, profileFile, fl, arguments, w, userId); + boolean res = startInstrumentation(className, profileFile, fl, arguments, w, c, userId); reply.writeNoException(); reply.writeInt(res ? 1 : 0); return true; @@ -2856,8 +2857,8 @@ class ActivityManagerProxy implements IActivityManager } public boolean startInstrumentation(ComponentName className, String profileFile, - int flags, Bundle arguments, IInstrumentationWatcher watcher, int userId) - throws RemoteException { + int flags, Bundle arguments, IInstrumentationWatcher watcher, + IUiAutomationConnection connection, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -2866,6 +2867,7 @@ class ActivityManagerProxy implements IActivityManager data.writeInt(flags); data.writeBundle(arguments); data.writeStrongBinder(watcher != null ? watcher.asBinder() : null); + data.writeStrongBinder(connection != null ? connection.asBinder() : null); data.writeInt(userId); mRemote.transact(START_INSTRUMENTATION_TRANSACTION, data, reply, 0); reply.readException(); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 127164567e51..5478bb75e925 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -43,7 +43,6 @@ import android.database.sqlite.SQLiteDebug; import android.database.sqlite.SQLiteDebug.DbStats; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.net.IConnectivityManager; import android.net.Proxy; @@ -102,7 +101,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.net.InetAddress; -import java.security.Security; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -420,6 +418,7 @@ public final class ActivityThread { ComponentName instrumentationName; Bundle instrumentationArgs; IInstrumentationWatcher instrumentationWatcher; + IUiAutomationConnection instrumentationUiAutomationConnection; int debugMode; boolean enableOpenGlTrace; boolean restrictedBackupMode; @@ -724,9 +723,10 @@ public final class ActivityThread { ComponentName instrumentationName, String profileFile, ParcelFileDescriptor profileFd, boolean autoStopProfiler, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, - int debugMode, boolean enableOpenGlTrace, boolean isRestrictedBackupMode, - boolean persistent, Configuration config, CompatibilityInfo compatInfo, - Map<String, IBinder> services, Bundle coreSettings) { + IUiAutomationConnection instrumentationUiConnection, int debugMode, + boolean enableOpenGlTrace, boolean isRestrictedBackupMode, boolean persistent, + Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, + Bundle coreSettings) { if (services != null) { // Setup the service cache in the ServiceManager @@ -742,6 +742,7 @@ public final class ActivityThread { data.instrumentationName = instrumentationName; data.instrumentationArgs = instrumentationArgs; data.instrumentationWatcher = instrumentationWatcher; + data.instrumentationUiAutomationConnection = instrumentationUiConnection; data.debugMode = debugMode; data.enableOpenGlTrace = enableOpenGlTrace; data.restrictedBackupMode = isRestrictedBackupMode; @@ -4337,7 +4338,8 @@ public final class ActivityThread { } mInstrumentation.init(this, instrContext, appContext, - new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher); + new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher, + data.instrumentationUiAutomationConnection); if (mProfiler.profileFile != null && !ii.handleProfiling && mProfiler.profileFd == null) { diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 63aa5f9b5f34..386e8d7b641c 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -267,6 +267,9 @@ public abstract class ApplicationThreadNative extends Binder Bundle testArgs = data.readBundle(); IBinder binder = data.readStrongBinder(); IInstrumentationWatcher testWatcher = IInstrumentationWatcher.Stub.asInterface(binder); + binder = data.readStrongBinder(); + IUiAutomationConnection uiAutomationConnection = + IUiAutomationConnection.Stub.asInterface(binder); int testMode = data.readInt(); boolean openGlTrace = data.readInt() != 0; boolean restrictedBackupMode = (data.readInt() != 0); @@ -277,8 +280,9 @@ public abstract class ApplicationThreadNative extends Binder Bundle coreSettings = data.readBundle(); bindApplication(packageName, info, providers, testName, profileName, profileFd, autoStopProfiler, - testArgs, testWatcher, testMode, openGlTrace, restrictedBackupMode, - persistent, config, compatInfo, services, coreSettings); + testArgs, testWatcher, uiAutomationConnection, testMode, + openGlTrace, restrictedBackupMode, persistent, config, compatInfo, + services, coreSettings); return true; } @@ -863,10 +867,11 @@ class ApplicationThreadProxy implements IApplicationThread { public final void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ComponentName testName, String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, Bundle testArgs, - IInstrumentationWatcher testWatcher, int debugMode, boolean openGlTrace, - boolean restrictedBackupMode, boolean persistent, - Configuration config, CompatibilityInfo compatInfo, - Map<String, IBinder> services, Bundle coreSettings) throws RemoteException { + IInstrumentationWatcher testWatcher, + IUiAutomationConnection uiAutomationConnection, int debugMode, + boolean openGlTrace, boolean restrictedBackupMode, boolean persistent, + Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, + Bundle coreSettings) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeString(packageName); @@ -888,6 +893,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeInt(autoStopProfiler ? 1 : 0); data.writeBundle(testArgs); data.writeStrongInterface(testWatcher); + data.writeStrongInterface(uiAutomationConnection); data.writeInt(debugMode); data.writeInt(openGlTrace ? 1 : 0); data.writeInt(restrictedBackupMode ? 1 : 0); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index fd4389e594a2..e03d3fd437fe 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1475,7 +1475,7 @@ class ContextImpl extends Context { arguments.setAllowFds(false); } return ActivityManagerNative.getDefault().startInstrumentation( - className, profileFile, 0, arguments, null, getUserId()); + className, profileFile, 0, arguments, null, null, getUserId()); } catch (RemoteException e) { // System has crashed, nothing we can do. } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 8af17a49ffdb..9b4fe611c8ea 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -158,8 +158,8 @@ public interface IActivityManager extends IInterface { public void killApplicationProcess(String processName, int uid) throws RemoteException; public boolean startInstrumentation(ComponentName className, String profileFile, - int flags, Bundle arguments, IInstrumentationWatcher watcher, int userId) - throws RemoteException; + int flags, Bundle arguments, IInstrumentationWatcher watcher, + IUiAutomationConnection connection, int userId) throws RemoteException; public void finishInstrumentation(IApplicationThread target, int resultCode, Bundle results) throws RemoteException; diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 03a26d4274c4..5dbbab4c97dd 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -90,7 +90,8 @@ public interface IApplicationThread extends IInterface { void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ComponentName testName, String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, Bundle testArguments, IInstrumentationWatcher testWatcher, - int debugMode, boolean openGlTrace, boolean restrictedBackupMode, boolean persistent, + IUiAutomationConnection uiAutomationConnection, int debugMode, + boolean openGlTrace, boolean restrictedBackupMode, boolean persistent, Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, Bundle coreSettings) throws RemoteException; void scheduleExit() throws RemoteException; diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl new file mode 100644 index 000000000000..09bf8296777d --- /dev/null +++ b/core/java/android/app/IUiAutomationConnection.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 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.app; + +import android.accessibilityservice.IAccessibilityServiceClient; +import android.graphics.Bitmap; +import android.view.InputEvent; +import android.os.ParcelFileDescriptor; + +/** + * This interface contains privileged operations a shell program can perform + * on behalf of an instrumentation that it runs. These operations require + * special permissions which the shell user has but the instrumentation does + * not. Running privileged operations by the shell user on behalf of an + * instrumentation is needed for running UiTestCases. + * + * {@hide} + */ +interface IUiAutomationConnection { + void connect(IAccessibilityServiceClient client); + void disconnect(); + boolean injectInputEvent(in InputEvent event, boolean sync); + boolean setRotation(int rotation); + Bitmap takeScreenshot(int width, int height); + void shutdown(); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 39186c654aae..a2eeddde6bee 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -49,7 +49,6 @@ import java.io.File; import java.util.ArrayList; import java.util.List; - /** * Base class for implementing application instrumentation code. When running * with instrumentation turned on, this class will be instantiated for you @@ -59,6 +58,7 @@ import java.util.List; * <instrumentation> tag. */ public class Instrumentation { + /** * If included in the status or final bundle sent to an IInstrumentationWatcher, this key * identifies the class that is writing the report. This can be used to provide more structured @@ -73,7 +73,7 @@ public class Instrumentation { * instrumentation can also be launched, and results collected, by an automated system. */ public static final String REPORT_KEY_STREAMRESULT = "stream"; - + private static final String TAG = "Instrumentation"; private final Object mSync = new Object(); @@ -86,9 +86,11 @@ public class Instrumentation { private List<ActivityWaiter> mWaitingActivities; private List<ActivityMonitor> mActivityMonitors; private IInstrumentationWatcher mWatcher; + private IUiAutomationConnection mUiAutomationConnection; private boolean mAutomaticPerformanceSnapshots = false; private PerformanceCollector mPerformanceCollector; private Bundle mPerfMetrics = new Bundle(); + private UiAutomation mUiAutomation; public Instrumentation() { } @@ -1598,13 +1600,14 @@ public class Instrumentation { /*package*/ final void init(ActivityThread thread, Context instrContext, Context appContext, ComponentName component, - IInstrumentationWatcher watcher) { + IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection) { mThread = thread; mMessageQueue = mThread.getLooper().myQueue(); mInstrContext = instrContext; mAppContext = appContext; mComponent = component; mWatcher = watcher; + mUiAutomationConnection = uiAutomationConnection; } /*package*/ static void checkStartActivityResult(int res, Object intent) { @@ -1644,12 +1647,42 @@ public class Instrumentation { } } + /** + * Gets the {@link UiAutomation} instance. + * <p> + * <strong>Note:</strong> The APIs exposed via the returned {@link UiAutomation} + * work across application boundaries while the APIs exposed by the instrumentation + * do not. For example, {@link Instrumentation#sendPointerSync(MotionEvent)} will + * not allow you to inject the event in an app different from the instrumentation + * target, while {@link UiAutomation#injectInputEvent(android.view.InputEvent, boolean)} + * will work regardless of the current application. + * </p> + * <p> + * A typical test case should be using either the {@link UiAutomation} or + * {@link Instrumentation} APIs. Using both APIs at the same time is not + * a mistake by itself but a client has to be aware of the APIs limitations. + * </p> + * @return The UI automation instance. + * + * @see UiAutomation + */ + public UiAutomation getUiAutomation() { + if (mUiAutomationConnection != null) { + if (mUiAutomation == null) { + mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(), + mUiAutomationConnection); + mUiAutomation.connect(); + } + return mUiAutomation; + } + return null; + } + private final class InstrumentationThread extends Thread { public InstrumentationThread(String name) { super(name); } public void run() { - IActivityManager am = ActivityManagerNative.getDefault(); try { Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); } catch (RuntimeException e) { @@ -1660,9 +1693,13 @@ public class Instrumentation { startPerformanceSnapshot(); } onStart(); + if (mUiAutomation != null) { + mUiAutomation.disconnect(); + mUiAutomation = null; + } } } - + private static final class EmptyRunnable implements Runnable { public void run() { } diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java new file mode 100644 index 000000000000..e611f6da95df --- /dev/null +++ b/core/java/android/app/UiAutomation.java @@ -0,0 +1,597 @@ +/* + * Copyright (C) 2013 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.app; + +import android.accessibilityservice.AccessibilityService.Callbacks; +import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Point; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; +import android.view.Display; +import android.view.InputEvent; +import android.view.Surface; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; + +import com.android.internal.util.Predicate; + +import java.util.ArrayList; +import java.util.concurrent.TimeoutException; + +/** + * Class for interacting with the device's UI by simulation user actions and + * introspection of the screen content. It relies on the platform accessibility + * APIs to introspect the screen and to perform some actions on the remote view + * tree. It also allows injecting of arbitrary raw input events simulating user + * interaction with keyboards and touch devices. + * <p> + * The APIs exposed by this class are low-level to maximize flexibility when + * developing UI test automation tools and libraries. Generally, a UiAutomation + * client should be using a higher-level library or implement high-level functions. + * For example, performing a tap on the screen requires construction and injecting + * of a touch down and up events which have to be delivered to the system by a + * call to {@link #injectInputEvent(InputEvent, boolean)}. + * </p> + * <p> + * The APIs exposed by this class operate across applications enabling a client + * to write tests that cover use cases spanning over multiple applications. For + * example, going to the settings application to change a setting and then + * interacting with another application whose behavior depends on that setting. + * </p> + */ +public final class UiAutomation { + + private static final String LOG_TAG = UiAutomation.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final int CONNECTION_ID_UNDEFINED = -1; + + private static final long CONNECT_TIMEOUT_MILLIS = 5000; + + /** Rotation constant: Unfreeze rotation (rotating the device changes its rotation state). */ + public static final int ROTATION_UNFREEZE = -2; + + /** Rotation constant: Freeze rotation to its current state. */ + public static final int ROTATION_FREEZE_CURRENT = -1; + + /** Rotation constant: Freeze rotation to 0 degrees (natural orientation) */ + public static final int ROTATION_FREEZE_0 = Surface.ROTATION_0; + + /** Rotation constant: Freeze rotation to 90 degrees . */ + public static final int ROTATION_FREEZE_90 = Surface.ROTATION_90; + + /** Rotation constant: Freeze rotation to 180 degrees . */ + public static final int ROTATION_FREEZE_180 = Surface.ROTATION_180; + + /** Rotation constant: Freeze rotation to 270 degrees . */ + public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270; + + private final Object mLock = new Object(); + + private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>(); + + private final IAccessibilityServiceClient mClient; + + private final IUiAutomationConnection mUiAutomationConnection; + + private int mConnectionId = CONNECTION_ID_UNDEFINED; + + private OnAccessibilityEventListener mOnAccessibilityEventListener; + + private boolean mWaitingForEventDelivery; + + private long mLastEventTimeMillis; + + private boolean mIsConnecting; + + /** + * Listener for observing the {@link AccessibilityEvent} stream. + */ + public static interface OnAccessibilityEventListener { + + /** + * Callback for receiving an {@link AccessibilityEvent}. + * <p> + * <strong>Note:</strong> This method is <strong>NOT</strong> executed + * on the main test thread. The client is responsible for proper + * synchronization. + * </p> + * <p> + * <strong>Note:</strong> It is responsibility of the client + * to recycle the received events to minimize object creation. + * </p> + * + * @param event The received event. + */ + public void onAccessibilityEvent(AccessibilityEvent event); + } + + /** + * Creates a new instance that will handle callbacks from the accessibility + * layer on the thread of the provided looper and perform requests for privileged + * operations on the provided connection. + * + * @param looper The looper on which to execute accessibility callbacks. + * @param connection The connection for performing privileged operations. + * + * @hide + */ + public UiAutomation(Looper looper, IUiAutomationConnection connection) { + if (looper == null) { + throw new IllegalArgumentException("Looper cannot be null!"); + } + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null!"); + } + mUiAutomationConnection = connection; + mClient = new IAccessibilityServiceClientImpl(looper); + } + + /** + * Connects this UiAutomation to the accessibility introspection APIs. + * + * @hide + */ + public void connect() { + synchronized (mLock) { + throwIfConnectedLocked(); + if (mIsConnecting) { + return; + } + mIsConnecting = true; + } + + try { + // Calling out without a lock held. + mUiAutomationConnection.connect(mClient); + } catch (RemoteException re) { + throw new RuntimeException("Error while connecting UiAutomation", re); + } + + synchronized (mLock) { + final long startTimeMillis = SystemClock.uptimeMillis(); + try { + while (true) { + if (isConnectedLocked()) { + break; + } + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis; + if (remainingTimeMillis <= 0) { + throw new RuntimeException("Error while connecting UiAutomation"); + } + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } finally { + mIsConnecting = false; + } + } + } + + /** + * Disconnects this UiAutomation from the accessibility introspection APIs. + * + * @hide + */ + public void disconnect() { + synchronized (mLock) { + if (mIsConnecting) { + throw new IllegalStateException( + "Cannot call disconnect() while connecting!"); + } + throwIfNotConnectedLocked(); + mConnectionId = CONNECTION_ID_UNDEFINED; + } + try { + // Calling out without a lock held. + mUiAutomationConnection.disconnect(); + } catch (RemoteException re) { + throw new RuntimeException("Error while disconnecting UiAutomation", re); + } + } + + /** + * The id of the {@link IAccessibilityInteractionConnection} for querying + * the screen content. This is here for legacy purposes since some tools use + * hidden APIs to introspect the screen. + * + * @hide + */ + public int getConnectionId() { + synchronized (mLock) { + throwIfNotConnectedLocked(); + return mConnectionId; + } + } + + /** + * Sets a callback for observing the stream of {@link AccessibilityEvent}s. + * + * @param listener The callback. + */ + public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) { + synchronized (mLock) { + mOnAccessibilityEventListener = listener; + } + } + + /** + * Gets the root {@link AccessibilityNodeInfo} in the active window. + * + * @return The root info. + */ + public AccessibilityNodeInfo getRootInActiveWindow() { + final int connectionId; + synchronized (mLock) { + throwIfNotConnectedLocked(); + connectionId = mConnectionId; + } + // Calling out without a lock held. + return AccessibilityInteractionClient.getInstance() + .getRootInActiveWindow(connectionId); + } + + /** + * A method for injecting an arbitrary input event. + * <p> + * <strong>Note:</strong> It is caller's responsibility to recycle the event. + * </p> + * @param event The event to inject. + * @param sync Whether to inject the event synchronously. + * @return Whether event injection succeeded. + */ + public boolean injectInputEvent(InputEvent event, boolean sync) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + try { + if (DEBUG) { + Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync); + } + // Calling out without a lock held. + return mUiAutomationConnection.injectInputEvent(event, sync); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while injecting input event!", re); + } + return false; + } + + /** + * Sets the device rotation. A client can freeze the rotation in + * desired state or freeze the rotation to its current state or + * unfreeze the rotation (rotating the device changes its rotation + * state). + * + * @param rotation The desired rotation. + * @return Whether the rotation was set successfully. + * + * @see #ROTATION_FREEZE_0 + * @see #ROTATION_FREEZE_90 + * @see #ROTATION_FREEZE_180 + * @see #ROTATION_FREEZE_270 + * @see #ROTATION_FREEZE_CURRENT + * @see #ROTATION_UNFREEZE + */ + public boolean setRotation(int rotation) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + switch (rotation) { + case ROTATION_FREEZE_0: + case ROTATION_FREEZE_90: + case ROTATION_FREEZE_180: + case ROTATION_FREEZE_270: + case ROTATION_UNFREEZE: + case ROTATION_FREEZE_CURRENT: { + try { + // Calling out without a lock held. + mUiAutomationConnection.setRotation(rotation); + return true; + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while setting rotation!", re); + } + } return false; + default: { + throw new IllegalArgumentException("Invalid rotation."); + } + } + } + + /** + * Executes a command and waits for a specific accessibility event up to a + * given wait timeout. To detect a sequence of events one can implement a + * filter that keeps track of seen events of the expected sequence and + * returns true after the last event of that sequence is received. + * <p> + * <strong>Note:</strong> It is caller's responsibility to recycle the returned event. + * </p> + * @param command The command to execute. + * @param filter Filter that recognizes the expected event. + * @param timeoutMillis The wait timeout in milliseconds. + * + * @throws TimeoutException If the expected event is not received within the timeout. + */ + public AccessibilityEvent executeAndWaitForEvent(Runnable command, + Predicate<AccessibilityEvent> filter, long timeoutMillis) throws TimeoutException { + synchronized (mLock) { + throwIfNotConnectedLocked(); + + mEventQueue.clear(); + // Prepare to wait for an event. + mWaitingForEventDelivery = true; + + // We will ignore events from previous interactions. + final long executionStartTimeMillis = SystemClock.uptimeMillis(); + + // Execute the command. + command.run(); + try { + // Wait for the event. + final long startTimeMillis = SystemClock.uptimeMillis(); + while (true) { + // Drain the event queue + while (!mEventQueue.isEmpty()) { + AccessibilityEvent event = mEventQueue.remove(0); + // Ignore events from previous interactions. + if (event.getEventTime() <= executionStartTimeMillis) { + continue; + } + if (filter.apply(event)) { + return event; + } + event.recycle(); + } + // Check if timed out and if not wait. + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; + if (remainingTimeMillis <= 0) { + throw new TimeoutException("Expected event not received within: " + + timeoutMillis + " ms."); + } + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } finally { + mWaitingForEventDelivery = false; + mEventQueue.clear(); + mLock.notifyAll(); + } + } + } + + /** + * Waits for the accessibility event stream to become idle, which is not to + * have received an accessibility event within <code>idleTimeoutMillis</code>. + * The total time spent to wait for an idle accessibility event stream is bounded + * by the <code>globalTimeoutMillis</code>. + * + * @param idleTimeoutMillis The timeout in milliseconds between two events + * to consider the device idle. + * @param globalTimeoutMillis The maximal global timeout in milliseconds in + * which to wait for an idle state. + * + * @throws TimeoutException If no idle state was detected within + * <code>globalTimeoutMillis.</code> + */ + public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis) + throws TimeoutException { + synchronized (mLock) { + throwIfNotConnectedLocked(); + + final long startTimeMillis = SystemClock.uptimeMillis(); + if (mLastEventTimeMillis <= 0) { + mLastEventTimeMillis = startTimeMillis; + } + + while (true) { + final long currentTimeMillis = SystemClock.uptimeMillis(); + // Did we get idle state within the global timeout? + final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis; + final long remainingGlobalTimeMillis = + globalTimeoutMillis - elapsedGlobalTimeMillis; + if (remainingGlobalTimeMillis <= 0) { + throw new TimeoutException("No idle state with idle timeout: " + + idleTimeoutMillis + " within global timeout: " + + globalTimeoutMillis); + } + // Did we get an idle state within the idle timeout? + final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis; + final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis; + if (remainingIdleTimeMillis <= 0) { + return; + } + try { + mLock.wait(remainingIdleTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + } + + /** + * Takes a screenshot. + * + * @return The screenshot bitmap on success, null otherwise. + */ + public Bitmap takeScreenshot() { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + Display display = DisplayManagerGlobal.getInstance() + .getRealDisplay(Display.DEFAULT_DISPLAY); + Point displaySize = new Point(); + display.getRealSize(displaySize); + final int displayWidth = displaySize.x; + final int displayHeight = displaySize.y; + + final float screenshotWidth; + final float screenshotHeight; + + final int rotation = display.getRotation(); + switch (rotation) { + case ROTATION_FREEZE_0: { + screenshotWidth = displayWidth; + screenshotHeight = displayHeight; + } break; + case ROTATION_FREEZE_90: { + screenshotWidth = displayHeight; + screenshotHeight = displayWidth; + } break; + case ROTATION_FREEZE_180: { + screenshotWidth = displayWidth; + screenshotHeight = displayHeight; + } break; + case ROTATION_FREEZE_270: { + screenshotWidth = displayHeight; + screenshotHeight = displayWidth; + } break; + default: { + throw new IllegalArgumentException("Invalid rotation: " + + rotation); + } + } + + // Take the screenshot + Bitmap screenShot = null; + try { + // Calling out without a lock held. + screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth, + (int) screenshotHeight); + if (screenShot == null) { + return null; + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while taking screnshot!", re); + return null; + } + + // Rotate the screenshot to the current orientation + if (rotation != ROTATION_FREEZE_0) { + Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(unrotatedScreenShot); + canvas.translate(unrotatedScreenShot.getWidth() / 2, + unrotatedScreenShot.getHeight() / 2); + canvas.rotate(getDegreesForRotation(rotation)); + canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2); + canvas.drawBitmap(screenShot, 0, 0, null); + canvas.setBitmap(null); + screenShot = unrotatedScreenShot; + } + + // Optimization + screenShot.setHasAlpha(false); + + return screenShot; + } + + private static float getDegreesForRotation(int value) { + switch (value) { + case Surface.ROTATION_90: { + return 360f - 90f; + } + case Surface.ROTATION_180: { + return 360f - 180f; + } + case Surface.ROTATION_270: { + return 360f - 270f; + } default: { + return 0; + } + } + } + + private boolean isConnectedLocked() { + return mConnectionId != CONNECTION_ID_UNDEFINED; + } + + private void throwIfConnectedLocked() { + if (mConnectionId != CONNECTION_ID_UNDEFINED) { + throw new IllegalStateException("UiAutomation not connected!"); + } + } + + private void throwIfNotConnectedLocked() { + if (!isConnectedLocked()) { + throw new IllegalStateException("UiAutomation not connected!"); + } + } + + private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper { + + public IAccessibilityServiceClientImpl(Looper looper) { + super(null, looper, new Callbacks() { + @Override + public void onSetConnectionId(int connectionId) { + synchronized (mLock) { + mConnectionId = connectionId; + mLock.notifyAll(); + } + } + + @Override + public void onServiceConnected() { + /* do nothing */ + } + + @Override + public void onInterrupt() { + /* do nothing */ + } + + @Override + public boolean onGesture(int gestureId) { + /* do nothing */ + return false; + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + mLastEventTimeMillis = event.getEventTime(); + if (mWaitingForEventDelivery) { + mEventQueue.add(AccessibilityEvent.obtain(event)); + } + mLock.notifyAll(); + } + // Calling out only without a lock held. + final OnAccessibilityEventListener listener = mOnAccessibilityEventListener; + if (listener != null) { + listener.onAccessibilityEvent(AccessibilityEvent.obtain(event)); + } + } + }); + } + } +} diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java new file mode 100644 index 000000000000..9b5857ff2e94 --- /dev/null +++ b/core/java/android/app/UiAutomationConnection.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2013 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.app; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.content.Context; +import android.graphics.Bitmap; +import android.hardware.input.InputManager; +import android.os.Binder; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.view.IWindowManager; +import android.view.InputEvent; +import android.view.Surface; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.IAccessibilityManager; + +/** + * This is a remote object that is passed from the shell to an instrumentation + * for enabling access to privileged operations which the shell can do and the + * instrumentation cannot. These privileged operations are needed for implementing + * a {@link UiAutomation} that enables across application testing by simulating + * user actions and performing screen introspection. + * + * @hide + */ +public final class UiAutomationConnection extends IUiAutomationConnection.Stub { + + private static final int INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1; + + private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService(Service.WINDOW_SERVICE)); + + private final Object mLock = new Object(); + + private int mInitialFrozenRotation = INITIAL_FROZEN_ROTATION_UNSPECIFIED; + + private IAccessibilityServiceClient mClient; + + private boolean mIsShutdown; + + private int mOwningUid; + + public void connect(IAccessibilityServiceClient client) { + if (client == null) { + throw new IllegalArgumentException("Client cannot be null!"); + } + synchronized (mLock) { + throwIfShutdownLocked(); + if (isConnectedLocked()) { + throw new IllegalStateException("Already connected."); + } + mOwningUid = Binder.getCallingUid(); + registerUiTestAutomationServiceLocked(client); + storeRotationStateLocked(); + } + } + + @Override + public void disconnect() { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + if (!isConnectedLocked()) { + throw new IllegalStateException("Already disconnected."); + } + mOwningUid = -1; + unregisterUiTestAutomationServiceLocked(); + restoreRotationStateLocked(); + } + } + + @Override + public boolean injectInputEvent(InputEvent event, boolean sync) { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH + : InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; + final long identity = Binder.clearCallingIdentity(); + try { + return InputManager.getInstance().injectInputEvent(event, mode); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public boolean setRotation(int rotation) { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + if (rotation == UiAutomation.ROTATION_UNFREEZE) { + mWindowManager.thawRotation(); + } else { + mWindowManager.freezeRotation(rotation); + } + return true; + } catch (RemoteException re) { + /* ignore */ + } finally { + Binder.restoreCallingIdentity(identity); + } + return false; + } + + @Override + public Bitmap takeScreenshot(int width, int height) { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + return Surface.screenshot(width, height); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void shutdown() { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + mIsShutdown = true; + if (isConnectedLocked()) { + disconnect(); + } + } + } + + private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client) { + IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; + info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; + info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; + try { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + manager.registerUiTestAutomationService(client, info); + mClient = client; + } catch (RemoteException re) { + throw new IllegalStateException("Error while registering UiTestAutomationService.", re); + } + } + + private void unregisterUiTestAutomationServiceLocked() { + IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); + try { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + manager.unregisterUiTestAutomationService(mClient); + mClient = null; + } catch (RemoteException re) { + throw new IllegalStateException("Error while unregistering UiTestAutomationService", + re); + } + } + + private void storeRotationStateLocked() { + try { + if (mWindowManager.isRotationFrozen()) { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + mInitialFrozenRotation = mWindowManager.getRotation(); + } + } catch (RemoteException re) { + /* ignore */ + } + } + + private void restoreRotationStateLocked() { + try { + if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + mWindowManager.freezeRotation(mInitialFrozenRotation); + } else { + // Calling out with a lock held is fine since if the system + // process is gone the client calling in will be killed. + mWindowManager.thawRotation(); + } + } catch (RemoteException re) { + /* ignore */ + } + } + + private boolean isConnectedLocked() { + return mClient != null; + } + + private void throwIfShutdownLocked() { + if (mIsShutdown) { + throw new IllegalStateException("Connection shutdown!"); + } + } + + private void throwIfNotConnectedLocked() { + if (!isConnectedLocked()) { + throw new IllegalStateException("Not connected!"); + } + } + + private void throwIfCalledByNotTrustedUidLocked() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != mOwningUid && mOwningUid != Process.SYSTEM_UID + && callingUid != 0 /*root*/) { + throw new SecurityException("Calling from not trusted UID!"); + } + } +} diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index ba82d7920ab4..81c25d8bbc01 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -16,8 +16,8 @@ package android.view; -import static android.view.accessibility.AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS; - +import android.app.ActivityThread; +import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; @@ -34,6 +34,7 @@ import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import com.android.internal.os.SomeArgs; +import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.HashMap; @@ -49,7 +50,7 @@ import java.util.Map; */ final class AccessibilityInteractionController { - private ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = + private final ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = new ArrayList<AccessibilityNodeInfo>(); private final Handler mHandler; @@ -69,6 +70,8 @@ final class AccessibilityInteractionController { private final Rect mTempRect1 = new Rect(); private final Rect mTempRect2 = new Rect(); + private AddNodeInfosForViewId mAddNodeInfosForViewId; + public AccessibilityInteractionController(ViewRootImpl viewRootImpl) { Looper looper = viewRootImpl.mHandler.getLooper(); mMyLooperThreadId = looper.getThread().getId(); @@ -135,8 +138,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { root = mViewRootImpl.mView; @@ -148,7 +150,7 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; applyAppScaleAndMagnificationSpecIfNeeded(infos, spec); if (spec != null) { spec.recycle(); @@ -161,19 +163,19 @@ final class AccessibilityInteractionController { } } - public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, - int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + public void findAccessibilityNodeInfosByViewIdClientThread(long accessibilityNodeId, + String viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { Message message = mHandler.obtainMessage(); - message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID; message.arg1 = flags; message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); SomeArgs args = SomeArgs.obtain(); - args.argi1 = viewId; - args.argi2 = interactionId; + args.argi1 = interactionId; args.arg1 = callback; args.arg2 = spec; + args.arg3 = viewId; message.obj = args; @@ -189,26 +191,26 @@ final class AccessibilityInteractionController { } } - private void findAccessibilityNodeInfoByViewIdUiThread(Message message) { + private void findAccessibilityNodeInfosByViewIdUiThread(Message message) { final int flags = message.arg1; final int accessibilityViewId = message.arg2; SomeArgs args = (SomeArgs) message.obj; - final int viewId = args.argi1; - final int interactionId = args.argi2; + final int interactionId = args.argi1; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; final MagnificationSpec spec = (MagnificationSpec) args.arg2; + final String viewId = (String) args.arg3; args.recycle(); - AccessibilityNodeInfo info = null; + final List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; + infos.clear(); try { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -216,19 +218,31 @@ final class AccessibilityInteractionController { root = mViewRootImpl.mView; } if (root != null) { - View target = root.findViewById(viewId); - if (target != null && isShown(target)) { - info = target.createAccessibilityNodeInfo(); + int resolvedViewId = root.getContext().getResources().getIdentifier( + viewId, "id", root.getContext().getPackageName()); + if (resolvedViewId <= 0) { + resolvedViewId = ((Context) ActivityThread.currentActivityThread() + .getSystemContext()).getResources() + .getIdentifier(viewId, "id", "android"); } + if (resolvedViewId <= 0) { + return; + } + if (mAddNodeInfosForViewId == null) { + mAddNodeInfosForViewId = new AddNodeInfosForViewId(); + } + mAddNodeInfosForViewId.init(resolvedViewId, infos); + root.findViewByPredicate(mAddNodeInfosForViewId); + mAddNodeInfosForViewId.reset(); } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; - applyAppScaleAndMagnificationSpecIfNeeded(info, spec); + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; + applyAppScaleAndMagnificationSpecIfNeeded(infos, spec); if (spec != null) { spec.recycle(); } - callback.setFindAccessibilityNodeInfoResult(info, interactionId); + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ } @@ -281,8 +295,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -325,7 +338,7 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; applyAppScaleAndMagnificationSpecIfNeeded(infos, spec); if (spec != null) { spec.recycle(); @@ -384,8 +397,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -426,7 +438,7 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; applyAppScaleAndMagnificationSpecIfNeeded(focused, spec); if (spec != null) { spec.recycle(); @@ -484,8 +496,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { root = findViewByAccessibilityId(accessibilityViewId); @@ -500,7 +511,7 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; applyAppScaleAndMagnificationSpecIfNeeded(next, spec); if (spec != null) { spec.recycle(); @@ -561,8 +572,7 @@ final class AccessibilityInteractionController { if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { return; } - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = - (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View target = null; if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { target = findViewByAccessibilityId(accessibilityViewId); @@ -580,7 +590,7 @@ final class AccessibilityInteractionController { } } finally { try { - mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; callback.setPerformAccessibilityActionResult(succeeded, interactionId); } catch (RemoteException re) { /* ignore - the other side will time out */ @@ -690,20 +700,20 @@ final class AccessibilityInteractionController { private final ArrayList<View> mTempViewList = new ArrayList<View>(); - public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int prefetchFlags, + public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int fetchFlags, List<AccessibilityNodeInfo> outInfos) { AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); if (provider == null) { AccessibilityNodeInfo root = view.createAccessibilityNodeInfo(); if (root != null) { outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { prefetchPredecessorsOfRealNode(view, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { prefetchSiblingsOfRealNode(view, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { prefetchDescendantsOfRealNode(view, outInfos); } } @@ -711,13 +721,13 @@ final class AccessibilityInteractionController { AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId); if (root != null) { outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { prefetchSiblingsOfVirtualNode(root, view, provider, outInfos); } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { prefetchDescendantsOfVirtualNode(root, provider, outInfos); } } @@ -920,7 +930,7 @@ final class AccessibilityInteractionController { private class PrivateHandler extends Handler { private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1; private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 3; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID = 3; private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4; private final static int MSG_FIND_FOCUS = 5; private final static int MSG_FOCUS_SEARCH = 6; @@ -937,8 +947,8 @@ final class AccessibilityInteractionController { return "MSG_PERFORM_ACCESSIBILITY_ACTION"; case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; + case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: + return "MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID"; case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; case MSG_FIND_FOCUS: @@ -960,8 +970,8 @@ final class AccessibilityInteractionController { case MSG_PERFORM_ACCESSIBILITY_ACTION: { perfromAccessibilityActionUiThread(message); } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { - findAccessibilityNodeInfoByViewIdUiThread(message); + case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: { + findAccessibilityNodeInfosByViewIdUiThread(message); } break; case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { findAccessibilityNodeInfosByTextUiThread(message); @@ -977,4 +987,27 @@ final class AccessibilityInteractionController { } } } + + private final class AddNodeInfosForViewId implements Predicate<View> { + private int mViewId = View.NO_ID; + private List<AccessibilityNodeInfo> mInfos; + + public void init(int viewId, List<AccessibilityNodeInfo> infos) { + mViewId = viewId; + mInfos = infos; + } + + public void reset() { + mViewId = View.NO_ID; + mInfos = null; + } + + @Override + public boolean apply(View view) { + if (view.getId() == mViewId && isShown(view)) { + mInfos.add(view.createAccessibilityNodeInfo()); + } + return false; + } + } } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 1ee2bb3cfc5c..a9ad97f61ed6 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -190,6 +190,13 @@ interface IWindowManager void thawRotation(); /** + * Gets whether the rotation is frozen. + * + * @return Whether the rotation is frozen. + */ + boolean isRotationFrozen(); + + /** * Create a screenshot of the applications currently displayed. */ Bitmap screenshotApplications(IBinder appToken, int displayId, int maxWidth, int maxHeight); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index a4e4f37927a8..b9babdcc2e03 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,6 +16,7 @@ package android.view; +import android.app.ActivityThread; import android.content.ClipData; import android.content.Context; import android.content.res.Configuration; @@ -5007,6 +5008,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (label != null) { info.setLabeledBy(label); } + + if ((mAttachInfo.mAccessibilityFetchFlags + & AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS) != 0) { + String viewId = null; + try { + viewId = getResources().getResourceName(mID); + } catch (Resources.NotFoundException nfe) { + /* ignore */ + } + if (viewId == null) { + try { + viewId = ((Context) ActivityThread.currentActivityThread() + .getSystemContext()).getResources().getResourceName(mID); + } catch (Resources.NotFoundException nfe) { + /* ignore */ + } + } + info.setViewId(viewId); + } } if (mLabelForId != View.NO_ID) { @@ -6838,7 +6858,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public boolean includeForAccessibility() { if (mAttachInfo != null) { - return mAttachInfo.mIncludeNotImportantViews || isImportantForAccessibility(); + return (mAttachInfo.mAccessibilityFetchFlags + & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0 + || isImportantForAccessibility(); } return false; } @@ -18004,10 +18026,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int mAccessibilityWindowId = View.NO_ID; /** - * Whether to ingore not exposed for accessibility Views when - * reporting the view tree to accessibility services. + * Flags related to accessibility processing. + * + * @see AccessibilityNodeInfo#FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + * @see AccessibilityNodeInfo#FLAG_REPORT_VIEW_IDS */ - boolean mIncludeNotImportantViews; + int mAccessibilityFetchFlags; /** * The drawable for highlighting accessibility focus. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3faac40d6ca3..46f76b856aa0 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -5481,15 +5481,16 @@ public final class ViewRootImpl implements ViewParent, } @Override - public void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, - int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, + String viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfoByViewIdClientThread(accessibilityNodeId, viewId, - interactionId, callback, flags, interrogatingPid, interrogatingTid, - spec); + .findAccessibilityNodeInfosByViewIdClientThread(accessibilityNodeId, + viewId, interactionId, callback, flags, interrogatingPid, + interrogatingTid, spec); } else { // We cannot make the call and notify the caller so it does not wait. try { diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 9377cfaab42c..02be4db477d1 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -1035,6 +1035,16 @@ public interface WindowManagerPolicy { public void keepScreenOnStoppedLw(); /** + * Gets the current user rotation mode. + * + * @return The rotation mode. + * + * @see WindowManagerPolicy#USER_ROTATION_LOCKED + * @see WindowManagerPolicy#USER_ROTATION_FREE + */ + public int getUserRotationMode(); + + /** * Inform the policy that the user has chosen a preferred orientation ("rotation lock"). * * @param mode One of {@link WindowManagerPolicy#USER_ROTATION_LOCKED} or diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 67df684c0777..84d7e720b31f 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -230,23 +230,25 @@ public final class AccessibilityInteractionClient * where to start the search. Use * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} * to start from the root. - * @param viewId The id of the view. - * @return An {@link AccessibilityNodeInfo} if found, null otherwise. + * @param viewId The fully qualified resource name of the view id to find. + * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise. */ - public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int connectionId, - int accessibilityWindowId, long accessibilityNodeId, int viewId) { + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId, + int accessibilityWindowId, long accessibilityNodeId, String viewId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final boolean success =connection.findAccessibilityNodeInfoByViewId( + final boolean success = connection.findAccessibilityNodeInfosByViewId( accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, Thread.currentThread().getId()); if (success) { - AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( + List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId); - return info; + if (infos != null) { + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + return infos; + } } } else { if (DEBUG) { @@ -259,7 +261,7 @@ public final class AccessibilityInteractionClient + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); } } - return null; + return Collections.emptyList(); } /** @@ -291,8 +293,10 @@ public final class AccessibilityInteractionClient if (success) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); - return infos; + if (infos != null) { + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + return infos; + } } } else { if (DEBUG) { diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 6c032804d088..e3d14ec5d567 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -16,6 +16,7 @@ package android.view.accessibility; +import android.accessibilityservice.AccessibilityServiceInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.Parcel; @@ -78,7 +79,10 @@ public class AccessibilityNodeInfo implements Parcelable { public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004; /** @hide */ - public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008; + public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008; + + /** @hide */ + public static final int FLAG_REPORT_VIEW_IDS = 0x00000010; // Actions. @@ -375,6 +379,7 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mClassName; private CharSequence mText; private CharSequence mContentDescription; + private CharSequence mViewId; private final SparseLongArray mChildNodeIds = new SparseLongArray(); private int mActions; @@ -729,6 +734,37 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Finds {@link AccessibilityNodeInfo}s by the fully qualified view id's resource + * name where a fully qualified id is of the from "package:id/id_resource_name". + * For example, if the target application's package is "foo.bar" and the id + * resource name is "baz", the fully qualified resource id is "foo.bar:id/baz". + * + * <p> + * <strong>Note:</strong> It is a client responsibility to recycle the + * received info by calling {@link AccessibilityNodeInfo#recycle()} + * to avoid creating of multiple instances. + * </p> + * <p> + * <strong>Note:</strong> The primary usage of this API is for UI test automation + * and in order to report the fully qualified view id if an {@link AccessibilityNodeInfo} + * the client has to set the {@link AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS} + * flag when configuring his {@link AccessibilityService}. + * </p> + * + * @param viewId The fully qualified resource name of the view id to find. + * @return A list of node info. + */ + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(String viewId) { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return Collections.emptyList(); + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.findAccessibilityNodeInfosByViewId(mConnectionId, mWindowId, mSourceNodeId, + viewId); + } + + /** * Gets the parent. * <p> * <strong>Note:</strong> It is a client responsibility to recycle the @@ -1373,6 +1409,38 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Sets the fully qualified resource name of the source view's id. + * + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param viewId The id resource name. + */ + public void setViewId(CharSequence viewId) { + enforceNotSealed(); + mViewId = viewId; + } + + /** + * Gets the fully qualified resource name of the source view's id. + * + * <p> + * <strong>Note:</strong> The primary usage of this API is for UI test automation + * and in order to report the source view id of an {@link AccessibilityNodeInfo} the + * client has to set the {@link AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS} + * flag when configuring his {@link AccessibilityService}. + * </p> + + * @return The id resource name. + */ + public CharSequence getViewId() { + return mViewId; + } + + /** * Gets the value of a boolean property. * * @param property The property. @@ -1614,6 +1682,7 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeCharSequence(mClassName); parcel.writeCharSequence(mText); parcel.writeCharSequence(mContentDescription); + parcel.writeCharSequence(mViewId); // Since instances of this class are fetched via synchronous i.e. blocking // calls in IPCs we always recycle as soon as the instance is marshaled. @@ -1639,6 +1708,7 @@ public class AccessibilityNodeInfo implements Parcelable { mClassName = other.mClassName; mText = other.mText; mContentDescription = other.mContentDescription; + mViewId = other.mViewId; mActions= other.mActions; mBooleanProperties = other.mBooleanProperties; mMovementGranularities = other.mMovementGranularities; @@ -1689,6 +1759,7 @@ public class AccessibilityNodeInfo implements Parcelable { mClassName = parcel.readCharSequence(); mText = parcel.readCharSequence(); mContentDescription = parcel.readCharSequence(); + mViewId = parcel.readCharSequence(); } /** @@ -1711,6 +1782,7 @@ public class AccessibilityNodeInfo implements Parcelable { mClassName = null; mText = null; mContentDescription = null; + mViewId = null; mActions = 0; } @@ -1855,6 +1927,7 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("; className: ").append(mClassName); builder.append("; text: ").append(mText); builder.append("; contentDescription: ").append(mContentDescription); + builder.append("; viewId: ").append(mViewId); builder.append("; checkable: ").append(isCheckable()); builder.append("; checked: ").append(isChecked()); diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index c313b0798e3f..8d15472f0999 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -33,9 +33,9 @@ oneway interface IAccessibilityInteractionConnection { IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, in MagnificationSpec spec); - void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, - long interrogatingTid, in MagnificationSpec spec); + void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, in MagnificationSpec spec); void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 57bf0d3aee92..396fd6821cde 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -1374,9 +1374,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (isEnabled()) { if (getFirstVisiblePosition() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); } if (getLastVisiblePosition() < getCount() - 1) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.setScrollable(true); } } } diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java index b5cbdd1fd2f7..e28dac05730f 100644 --- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java +++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java @@ -4134,6 +4134,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return rotation == mPortraitRotation || rotation == mUpsideDownRotation; } + public int getUserRotationMode() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.USER_ROTATION, WindowManagerPolicy.USER_ROTATION_FREE, + UserHandle.USER_CURRENT); + } // User rotation: to be used when all else fails in assigning an orientation to the device public void setUserRotationMode(int mode, int rot) { diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java index 0725df069d39..b7c3450bcafd 100644 --- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -17,7 +17,6 @@ package com.android.server.accessibility; import static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT; -import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -518,6 +517,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { ComponentName componentName = new ComponentName("foo.bar", "AutomationAccessibilityService"); synchronized (mLock) { + if (mUiAutomationService != null) { + throw new IllegalStateException("UiAutomationService " + serviceClient + + "already registered!"); + } // If an automation services is connected to the system all services are stopped // so the automation one is the only one running. Settings are not changed so when // the automation service goes away the state is restored from the settings. @@ -556,7 +559,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { synchronized (mLock) { UserState userState = getCurrentUserStateLocked(); // Stash the old state so we can restore it when the keyguard is gone. - mTempStateChangeForCurrentUserMemento.initialize(mCurrentUserId, getCurrentUserStateLocked()); + mTempStateChangeForCurrentUserMemento.initialize(mCurrentUserId, + getCurrentUserStateLocked()); // Set the temporary state. userState.mIsAccessibilityEnabled = true; userState.mIsTouchExplorationEnabled= touchExplorationEnabled; @@ -579,6 +583,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { && serviceClient != null && mUiAutomationService.mServiceInterface .asBinder() == serviceClient.asBinder()) { mUiAutomationService.binderDied(); + } else { + throw new IllegalStateException("UiAutomationService " + serviceClient + + " not registered!"); } } } @@ -935,7 +942,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } if (!event.isImportantForAccessibility() - && !service.mIncludeNotImportantViews) { + && (service.mFetchFlags + & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) == 0) { return false; } @@ -1486,7 +1494,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { boolean mRequestTouchExplorationMode; - boolean mIncludeNotImportantViews; + int mFetchFlags; long mNotificationTimeout; @@ -1565,10 +1573,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) { - mIncludeNotImportantViews = - (info.flags & FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + mFetchFlags |= (info.flags + & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0 ? + AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0; } + mFetchFlags |= (info.flags + & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0 ? + AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS : 0; + mRequestTouchExplorationMode = (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; @@ -1664,8 +1677,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } @Override - public boolean findAccessibilityNodeInfoByViewId(int accessibilityWindowId, - long accessibilityNodeId, int viewId, int interactionId, + public boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, + long accessibilityNodeId, String viewIdResName, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) throws RemoteException { final int resolvedWindowId; @@ -1689,14 +1702,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } } - final int flags = (mIncludeNotImportantViews) ? - AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); try { - connection.findAccessibilityNodeInfoByViewId(accessibilityNodeId, viewId, - interactionId, callback, flags, interrogatingPid, interrogatingTid, spec); + connection.findAccessibilityNodeInfosByViewId(accessibilityNodeId, + viewIdResName, interactionId, callback, mFetchFlags, interrogatingPid, + interrogatingTid, spec); return true; } catch (RemoteException re) { if (DEBUG) { @@ -1735,14 +1747,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } } - final int flags = (mIncludeNotImportantViews) ? - AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); try { connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text, - interactionId, callback, flags, interrogatingPid, interrogatingTid, spec); + interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid, + spec); return true; } catch (RemoteException re) { if (DEBUG) { @@ -1781,15 +1792,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } } - final int allFlags = flags | ((mIncludeNotImportantViews) ? - AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0); final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); try { connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, - interactionId, callback, allFlags, interrogatingPid, interrogatingTid, - spec); + interactionId, callback, mFetchFlags | flags, interrogatingPid, + interrogatingTid, spec); return true; } catch (RemoteException re) { if (DEBUG) { @@ -1828,14 +1837,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } } - final int flags = (mIncludeNotImportantViews) ? - AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); try { connection.findFocus(accessibilityNodeId, focusType, interactionId, callback, - flags, interrogatingPid, interrogatingTid, spec); + mFetchFlags, interrogatingPid, interrogatingTid, spec); return true; } catch (RemoteException re) { if (DEBUG) { @@ -1874,14 +1881,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } } - final int flags = (mIncludeNotImportantViews) ? - AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); try { connection.focusSearch(accessibilityNodeId, direction, interactionId, callback, - flags, interrogatingPid, interrogatingTid, spec); + mFetchFlags, interrogatingPid, interrogatingTid, spec); return true; } catch (RemoteException re) { if (DEBUG) { @@ -1920,13 +1925,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } } - final int flags = (mIncludeNotImportantViews) ? - AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { connection.performAccessibilityAction(accessibilityNodeId, action, arguments, - interactionId, callback, flags, interrogatingPid, interrogatingTid); + interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { Slog.e(LOG_TAG, "Error calling performAccessibilityAction()"); diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index d8e199bafeba..497b3ec51c89 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -51,6 +51,7 @@ import android.app.IProcessObserver; import android.app.IServiceConnection; import android.app.IStopUserCallback; import android.app.IThumbnailReceiver; +import android.app.IUiAutomationConnection; import android.app.IUserSwitchObserver; import android.app.Instrumentation; import android.app.Notification; @@ -162,7 +163,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -public final class ActivityManagerService extends ActivityManagerNative +public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback { private static final String USER_DATA_DIR = "/data/user/"; static final String TAG = "ActivityManager"; @@ -4259,8 +4260,9 @@ public final class ActivityManagerService extends ActivityManagerNative } thread.bindApplication(processName, appInfo, providers, app.instrumentationClass, profileFile, profileFd, profileAutoStop, - app.instrumentationArguments, app.instrumentationWatcher, testMode, - enableOpenGlTrace, isRestrictedBackupMode || !normalMode, app.persistent, + app.instrumentationArguments, app.instrumentationWatcher, + app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace, + isRestrictedBackupMode || !normalMode, app.persistent, new Configuration(mConfiguration), app.compat, getCommonServicesLocked(), mCoreSettingsObserver.getCoreSettingsLocked()); updateLruProcessLocked(app, false); @@ -12147,7 +12149,8 @@ public final class ActivityManagerService extends ActivityManagerNative public boolean startInstrumentation(ComponentName className, String profileFile, int flags, Bundle arguments, - IInstrumentationWatcher watcher, int userId) { + IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection, + int userId) { enforceNotIsolatedCaller("startInstrumentation"); userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, true, "startInstrumentation", null); @@ -12201,6 +12204,7 @@ public final class ActivityManagerService extends ActivityManagerNative app.instrumentationProfileFile = profileFile; app.instrumentationArguments = arguments; app.instrumentationWatcher = watcher; + app.instrumentationUiAutomationConnection = uiAutomationConnection; app.instrumentationResultClass = className; Binder.restoreCallingIdentity(origId); } @@ -12243,7 +12247,15 @@ public final class ActivityManagerService extends ActivityManagerNative } catch (RemoteException e) { } } + if (app.instrumentationUiAutomationConnection != null) { + try { + app.instrumentationUiAutomationConnection.shutdown(); + } catch (RemoteException re) { + /* ignore */ + } + } app.instrumentationWatcher = null; + app.instrumentationUiAutomationConnection = null; app.instrumentationClass = null; app.instrumentationInfo = null; app.instrumentationProfileFile = null; diff --git a/services/java/com/android/server/am/ProcessRecord.java b/services/java/com/android/server/am/ProcessRecord.java index 7fbab044546f..a32af2f8c020 100644 --- a/services/java/com/android/server/am/ProcessRecord.java +++ b/services/java/com/android/server/am/ProcessRecord.java @@ -22,6 +22,7 @@ import android.app.ActivityManager; import android.app.Dialog; import android.app.IApplicationThread; import android.app.IInstrumentationWatcher; +import android.app.IUiAutomationConnection; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -95,6 +96,7 @@ class ProcessRecord { ApplicationInfo instrumentationInfo; // the application being instrumented String instrumentationProfileFile; // where to save profiling IInstrumentationWatcher instrumentationWatcher; // who is waiting + IUiAutomationConnection instrumentationUiAutomationConnection; // Connection to use the UI introspection APIs. Bundle instrumentationArguments;// as given to us ComponentName instrumentationResultClass;// copy of instrumentationClass boolean usingWrapper; // Set to true when process was launched with a wrapper attached diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index ff2dc0f6abd9..8d44f366b003 100644 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -5598,6 +5598,11 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public boolean isRotationFrozen() { + return mPolicy.getUserRotationMode() == WindowManagerPolicy.USER_ROTATION_LOCKED; + } + + @Override public int watchRotation(IRotationWatcher watcher) { final IBinder watcherBinder = watcher.asBinder(); IBinder.DeathRecipient dr = new IBinder.DeathRecipient() { diff --git a/test-runner/src/android/test/AndroidTestRunner.java b/test-runner/src/android/test/AndroidTestRunner.java index 30876d0974f7..aa7c677c968f 100644 --- a/test-runner/src/android/test/AndroidTestRunner.java +++ b/test-runner/src/android/test/AndroidTestRunner.java @@ -21,6 +21,7 @@ import android.content.Context; import android.os.PerformanceCollector.PerformanceResultsWriter; import com.google.android.collect.Lists; + import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestListener; diff --git a/test-runner/src/android/test/InstrumentationTestRunner.java b/test-runner/src/android/test/InstrumentationTestRunner.java index 8e308758ec66..8e833ca10062 100644 --- a/test-runner/src/android/test/InstrumentationTestRunner.java +++ b/test-runner/src/android/test/InstrumentationTestRunner.java @@ -21,6 +21,7 @@ import com.android.internal.util.Predicates; import android.app.Activity; import android.app.Instrumentation; +import android.app.UiAutomation; import android.os.Bundle; import android.os.Debug; import android.os.Looper; @@ -390,12 +391,11 @@ public class InstrumentationTestRunner extends Instrumentation implements TestSu } /** - * Get the Bundle object that contains the arguments + * Get the arguments passed to this instrumentation. * * @return the Bundle object - * @hide */ - public Bundle getBundle(){ + protected Bundle getArguments() { return mArguments; } |