Merge "Optionally support accessibility with UiAutomator"
diff --git a/api/current.txt b/api/current.txt
index 943eae0..cf2ea6d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -4642,6 +4642,7 @@
method public android.content.Context getContext();
method public android.content.Context getTargetContext();
method public android.app.UiAutomation getUiAutomation();
+ method public android.app.UiAutomation getUiAutomation(int);
method public boolean invokeContextMenuAction(android.app.Activity, int, int);
method public boolean invokeMenuActionSync(android.app.Activity, int, int);
method public boolean isProfiling();
@@ -5538,6 +5539,7 @@
public final class UiAutomation {
method public void clearWindowAnimationFrameStats();
method public boolean clearWindowContentFrameStats(int);
+ method public void destroy();
method public android.view.accessibility.AccessibilityEvent executeAndWaitForEvent(java.lang.Runnable, android.app.UiAutomation.AccessibilityEventFilter, long) throws java.util.concurrent.TimeoutException;
method public android.os.ParcelFileDescriptor executeShellCommand(java.lang.String);
method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
@@ -5554,6 +5556,7 @@
method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
method public android.graphics.Bitmap takeScreenshot();
method public void waitForIdle(long, long) throws java.util.concurrent.TimeoutException;
+ field public static final int FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES = 1; // 0x1
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
diff --git a/api/system-current.txt b/api/system-current.txt
index 5d2c2e8..6a420e2 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4774,6 +4774,7 @@
method public android.content.Context getContext();
method public android.content.Context getTargetContext();
method public android.app.UiAutomation getUiAutomation();
+ method public android.app.UiAutomation getUiAutomation(int);
method public boolean invokeContextMenuAction(android.app.Activity, int, int);
method public boolean invokeMenuActionSync(android.app.Activity, int, int);
method public boolean isProfiling();
@@ -5670,6 +5671,7 @@
public final class UiAutomation {
method public void clearWindowAnimationFrameStats();
method public boolean clearWindowContentFrameStats(int);
+ method public void destroy();
method public android.view.accessibility.AccessibilityEvent executeAndWaitForEvent(java.lang.Runnable, android.app.UiAutomation.AccessibilityEventFilter, long) throws java.util.concurrent.TimeoutException;
method public android.os.ParcelFileDescriptor executeShellCommand(java.lang.String);
method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
@@ -5686,6 +5688,7 @@
method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
method public android.graphics.Bitmap takeScreenshot();
method public void waitForIdle(long, long) throws java.util.concurrent.TimeoutException;
+ field public static final int FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES = 1; // 0x1
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
diff --git a/api/test-current.txt b/api/test-current.txt
index 4d8d07b..22eeef4 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -4642,6 +4642,7 @@
method public android.content.Context getContext();
method public android.content.Context getTargetContext();
method public android.app.UiAutomation getUiAutomation();
+ method public android.app.UiAutomation getUiAutomation(int);
method public boolean invokeContextMenuAction(android.app.Activity, int, int);
method public boolean invokeMenuActionSync(android.app.Activity, int, int);
method public boolean isProfiling();
@@ -5538,6 +5539,7 @@
public final class UiAutomation {
method public void clearWindowAnimationFrameStats();
method public boolean clearWindowContentFrameStats(int);
+ method public void destroy();
method public android.view.accessibility.AccessibilityEvent executeAndWaitForEvent(java.lang.Runnable, android.app.UiAutomation.AccessibilityEventFilter, long) throws java.util.concurrent.TimeoutException;
method public android.os.ParcelFileDescriptor executeShellCommand(java.lang.String);
method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
@@ -5556,6 +5558,7 @@
method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
method public android.graphics.Bitmap takeScreenshot();
method public void waitForIdle(long, long) throws java.util.concurrent.TimeoutException;
+ field public static final int FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES = 1; // 0x1
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
diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl
index 2caec369..7640e75 100644
--- a/core/java/android/app/IUiAutomationConnection.aidl
+++ b/core/java/android/app/IUiAutomationConnection.aidl
@@ -33,7 +33,7 @@
* {@hide}
*/
interface IUiAutomationConnection {
- void connect(IAccessibilityServiceClient client);
+ void connect(IAccessibilityServiceClient client, int flags);
void disconnect();
boolean injectInputEvent(in InputEvent event, boolean sync);
boolean setRotation(int rotation);
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 53974c1..9a88f2c 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -1837,16 +1837,59 @@
* {@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.
+ * @return The UI automation instance. If none exists, a new one is created with no flags set.
*
* @see UiAutomation
*/
public UiAutomation getUiAutomation() {
if (mUiAutomationConnection != null) {
if (mUiAutomation == null) {
+ return getUiAutomation(0);
+ }
+ return mUiAutomation;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the {@link UiAutomation} instance with flags set.
+ * <p>
+ * <strong>Note:</strong> Only one UiAutomation can be obtained. Calling this method
+ * twice with different flags will fail unless the UiAutomation obtained in the first call
+ * is released with {@link UiAutomation#destroy()}.
+ * </p>
+ * <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>
+ *
+ * @param flags The flags to be passed to the UiAutomation, for example
+ * {@link UiAutomation#FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES}.
+ *
+ * @return The UI automation instance.
+ *
+ * @see UiAutomation
+ */
+ public UiAutomation getUiAutomation(int flags) {
+ if (mUiAutomationConnection != null) {
+ if ((mUiAutomation == null) || (mUiAutomation.isDestroyed())) {
mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(),
mUiAutomationConnection);
- mUiAutomation.connect();
+ mUiAutomation.connect(flags);
+ } else {
+ if (mUiAutomation.getFlags() != flags) {
+ throw new RuntimeException(
+ "Cannot get a UiAutomation with different flags from the existing one");
+ }
}
return mUiAutomation;
}
@@ -1861,8 +1904,8 @@
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
} catch (RuntimeException e) {
- Log.w(TAG, "Exception setting priority of instrumentation thread "
- + Process.myTid(), e);
+ Log.w(TAG, "Exception setting priority of instrumentation thread "
+ + Process.myTid(), e);
}
if (mAutomaticPerformanceSnapshots) {
startPerformanceSnapshot();
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index dce2e51..79d383c 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -105,6 +105,14 @@
/** Rotation constant: Freeze rotation to 270 degrees . */
public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270;
+ /**
+ * UiAutomation supresses accessibility services by default. This flag specifies that
+ * existing accessibility services should continue to run, and that new ones may start.
+ * This flag is set when obtaining the UiAutomation from
+ * {@link Instrumentation#getUiAutomation(int)}.
+ */
+ public static final int FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES = 0x00000001;
+
private final Object mLock = new Object();
private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>();
@@ -123,6 +131,10 @@
private boolean mIsConnecting;
+ private boolean mIsDestroyed;
+
+ private int mFlags;
+
/**
* Listener for observing the {@link AccessibilityEvent} stream.
*/
@@ -182,11 +194,22 @@
}
/**
- * Connects this UiAutomation to the accessibility introspection APIs.
+ * Connects this UiAutomation to the accessibility introspection APIs with default flags.
*
* @hide
*/
public void connect() {
+ connect(0);
+ }
+
+ /**
+ * Connects this UiAutomation to the accessibility introspection APIs.
+ *
+ * @param flags Any flags to apply to the automation as it gets connected
+ *
+ * @hide
+ */
+ public void connect(int flags) {
synchronized (mLock) {
throwIfConnectedLocked();
if (mIsConnecting) {
@@ -197,7 +220,8 @@
try {
// Calling out without a lock held.
- mUiAutomationConnection.connect(mClient);
+ mUiAutomationConnection.connect(mClient, flags);
+ mFlags = flags;
} catch (RemoteException re) {
throw new RuntimeException("Error while connecting UiAutomation", re);
}
@@ -227,6 +251,17 @@
}
/**
+ * Get the flags used to connect the service.
+ *
+ * @return The flags used to connect
+ *
+ * @hide
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
* Disconnects this UiAutomation from the accessibility introspection APIs.
*
* @hide
@@ -263,6 +298,17 @@
}
/**
+ * Reports if the object has been destroyed
+ *
+ * @return {code true} if the object has been destroyed.
+ *
+ * @hide
+ */
+ public boolean isDestroyed() {
+ return mIsDestroyed;
+ }
+
+ /**
* Sets a callback for observing the stream of {@link AccessibilityEvent}s.
*
* @param listener The callback.
@@ -274,6 +320,15 @@
}
/**
+ * Destroy this UiAutomation. After calling this method, attempting to use the object will
+ * result in errors.
+ */
+ public void destroy() {
+ disconnect();
+ mIsDestroyed = true;
+ }
+
+ /**
* Performs a global action. Such an action can be performed at any moment
* regardless of the current application or user location in that application.
* For example going back, going home, opening recents, etc.
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index bd10267..276f774 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -77,7 +77,7 @@
private int mOwningUid;
- public void connect(IAccessibilityServiceClient client) {
+ public void connect(IAccessibilityServiceClient client, int flags) {
if (client == null) {
throw new IllegalArgumentException("Client cannot be null!");
}
@@ -87,7 +87,7 @@
throw new IllegalStateException("Already connected.");
}
mOwningUid = Binder.getCallingUid();
- registerUiTestAutomationServiceLocked(client);
+ registerUiTestAutomationServiceLocked(client, flags);
storeRotationStateLocked();
}
}
@@ -322,7 +322,8 @@
}
}
- private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client) {
+ private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
+ int flags) {
IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
@@ -337,7 +338,7 @@
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(mToken, client, info);
+ manager.registerUiTestAutomationService(mToken, client, info, flags);
mClient = client;
} catch (RemoteException re) {
throw new IllegalStateException("Error while registering UiTestAutomationService.", re);
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 9e79057..655c9b3 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -51,7 +51,7 @@
void removeAccessibilityInteractionConnection(IWindow windowToken);
void registerUiTestAutomationService(IBinder owner, IAccessibilityServiceClient client,
- in AccessibilityServiceInfo info);
+ in AccessibilityServiceInfo info, int flags);
void unregisterUiTestAutomationService(IAccessibilityServiceClient client);
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 03809c0..2a8672d 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -27,6 +27,7 @@
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.StatusBarManager;
+import android.app.UiAutomation;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -51,6 +52,7 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -258,10 +260,9 @@
UserState userState = getCurrentUserStateLocked();
// We have to reload the installed services since some services may
// have different attributes, resolve info (does not support equals),
- // etc. Remove them then to force reload. Do it even if automation is
- // running since when it goes away, we will have to reload as well.
+ // etc. Remove them then to force reload.
userState.mInstalledServices.clear();
- if (userState.mUiAutomationService == null) {
+ if (!userState.isUiAutomationSuppressingOtherServices()) {
if (readConfigurationForUserStateLocked(userState)) {
onUserStateChangedLocked(userState);
}
@@ -296,7 +297,7 @@
TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
userState.mTouchExplorationGrantedServices, userId);
// We will update when the automation service dies.
- if (userState.mUiAutomationService == null) {
+ if (!userState.isUiAutomationSuppressingOtherServices()) {
onUserStateChangedLocked(userState);
}
return;
@@ -330,7 +331,7 @@
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
userState.mEnabledServices, userId);
// We will update when the automation service dies.
- if (userState.mUiAutomationService == null) {
+ if (!userState.isUiAutomationSuppressingOtherServices()) {
onUserStateChangedLocked(userState);
}
}
@@ -362,7 +363,7 @@
} else if (Intent.ACTION_USER_PRESENT.equals(action)) {
// We will update when the automation service dies.
UserState userState = getCurrentUserStateLocked();
- if (userState.mUiAutomationService == null) {
+ if (!userState.isUiAutomationSuppressingOtherServices()) {
if (readConfigurationForUserStateLocked(userState)) {
onUserStateChangedLocked(userState);
}
@@ -473,11 +474,9 @@
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(userId);
- // The automation service is a fake one and should not be reported
- // to clients as being enabled. The automation service is always the
- // only active one, if it exists.
+ // The automation service can suppress other services.
UserState userState = getUserStateLocked(resolvedUserId);
- if (userState.mUiAutomationService != null) {
+ if (userState.isUiAutomationSuppressingOtherServices()) {
return Collections.emptyList();
}
@@ -490,7 +489,9 @@
final int serviceCount = services.size();
for (int i = 0; i < serviceCount; i++) {
Service service = services.get(i);
- if ((service.mFeedbackType & feedbackTypeBit) != 0) {
+ // Don't report the UIAutomation (fake service)
+ if (!sFakeAccessibilityServiceComponentName.equals(service.mComponentName)
+ && (service.mFeedbackType & feedbackTypeBit) != 0) {
result.add(service.mAccessibilityServiceInfo);
}
}
@@ -621,7 +622,8 @@
@Override
public void registerUiTestAutomationService(IBinder owner,
IAccessibilityServiceClient serviceClient,
- AccessibilityServiceInfo accessibilityServiceInfo) {
+ AccessibilityServiceInfo accessibilityServiceInfo,
+ int flags) {
mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT,
FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE);
@@ -645,15 +647,17 @@
userState.mUiAutomationServiceOwner = owner;
userState.mUiAutomationServiceClient = serviceClient;
-
- // Set the temporary state.
+ userState.mUiAutomationFlags = flags;
userState.mIsAccessibilityEnabled = true;
- userState.mIsTouchExplorationEnabled = false;
- userState.mIsEnhancedWebAccessibilityEnabled = false;
- userState.mIsDisplayMagnificationEnabled = false;
- userState.mIsAutoclickEnabled = false;
userState.mInstalledServices.add(accessibilityServiceInfo);
- userState.mEnabledServices.clear();
+ if (userState.isUiAutomationSuppressingOtherServices()) {
+ // Set the temporary state.
+ userState.mIsTouchExplorationEnabled = false;
+ userState.mIsEnhancedWebAccessibilityEnabled = false;
+ userState.mIsDisplayMagnificationEnabled = false;
+ userState.mIsAutoclickEnabled = false;
+ userState.mEnabledServices.clear();
+ }
userState.mEnabledServices.add(sFakeAccessibilityServiceComponentName);
userState.mTouchExplorationGrantedServices.add(sFakeAccessibilityServiceComponentName);
@@ -694,7 +698,7 @@
UserState userState = getCurrentUserStateLocked();
// This is a nop if UI automation is enabled.
- if (userState.mUiAutomationService != null) {
+ if (userState.isUiAutomationSuppressingOtherServices()) {
return;
}
@@ -1027,6 +1031,9 @@
if (!mTempComponentNameSet.equals(userState.mEnabledServices)) {
userState.mEnabledServices.clear();
userState.mEnabledServices.addAll(mTempComponentNameSet);
+ if (userState.mUiAutomationService != null) {
+ userState.mEnabledServices.add(sFakeAccessibilityServiceComponentName);
+ }
mTempComponentNameSet.clear();
return true;
}
@@ -3981,6 +3988,7 @@
public boolean mAccessibilityFocusOnlyInActiveWindow;
private Service mUiAutomationService;
+ private int mUiAutomationFlags;
private IAccessibilityServiceClient mUiAutomationServiceClient;
private IBinder mUiAutomationServiceOwner;
@@ -4044,6 +4052,7 @@
public void destroyUiAutomationService() {
mUiAutomationService = null;
+ mUiAutomationFlags = 0;
mUiAutomationServiceClient = null;
if (mUiAutomationServiceOwner != null) {
mUiAutomationServiceOwner.unlinkToDeath(
@@ -4051,6 +4060,11 @@
mUiAutomationServiceOwner = null;
}
}
+
+ boolean isUiAutomationSuppressingOtherServices() {
+ return ((mUiAutomationService != null) && (mUiAutomationFlags
+ & UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES) == 0);
+ }
}
private final class AccessibilityContentObserver extends ContentObserver {
@@ -4130,8 +4144,8 @@
// we are checking for changes only the parent settings.
UserState userState = getCurrentUserStateLocked();
- // We will update when the automation service dies.
- if (userState.mUiAutomationService != null) {
+ // If the automation service is suppressing, we will update when it dies.
+ if (userState.isUiAutomationSuppressingOtherServices()) {
return;
}