Merge changes from topic "BackportUiAutomatorRetry"

* changes:
  Let #getUiAutomation return null if UiAutomation fails to connect (3/n)
  Allow #disconnect to be called safely on connection timeout (2/n)
  Add #connectWithTimeout (1/n)
This commit is contained in:
Treehugger Robot
2020-10-27 17:48:30 +00:00
committed by Gerrit Code Review
2 changed files with 126 additions and 36 deletions

View File

@@ -62,6 +62,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeoutException;
/** /**
* Base class for implementing application instrumentation code. When running * Base class for implementing application instrumentation code. When running
@@ -90,6 +91,8 @@ public class Instrumentation {
private static final String TAG = "Instrumentation"; private static final String TAG = "Instrumentation";
private static final long CONNECT_TIMEOUT_MILLIS = 5000;
/** /**
* @hide * @hide
*/ */
@@ -2125,6 +2128,13 @@ public class Instrumentation {
* Equivalent to {@code getUiAutomation(0)}. If a {@link UiAutomation} exists with different * Equivalent to {@code getUiAutomation(0)}. If a {@link UiAutomation} exists with different
* flags, the flags on that instance will be changed, and then it will be returned. * flags, the flags on that instance will be changed, and then it will be returned.
* </p> * </p>
* <p>
* Compatibility mode: This method is infallible for apps targeted for
* {@link Build.VERSION_CODES#R} and earlier versions; for apps targeted for later versions, it
* will return null if {@link UiAutomation} fails to connect. The caller can check the return
* value and retry on error.
* </p>
*
* @return The UI automation instance. * @return The UI automation instance.
* *
* @see UiAutomation * @see UiAutomation
@@ -2152,6 +2162,12 @@ public class Instrumentation {
* If a {@link UiAutomation} exists with different flags, the flags on that instance will be * If a {@link UiAutomation} exists with different flags, the flags on that instance will be
* changed, and then it will be returned. * changed, and then it will be returned.
* </p> * </p>
* <p>
* Compatibility mode: This method is infallible for apps targeted for
* {@link Build.VERSION_CODES#R} and earlier versions; for apps targeted for later versions, it
* will return null if {@link UiAutomation} fails to connect. The caller can check the return
* value and retry on error.
* </p>
* *
* @param flags The flags to be passed to the UiAutomation, for example * @param flags The flags to be passed to the UiAutomation, for example
* {@link UiAutomation#FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES}. * {@link UiAutomation#FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES}.
@@ -2173,8 +2189,17 @@ public class Instrumentation {
} else { } else {
mUiAutomation.disconnect(); mUiAutomation.disconnect();
} }
mUiAutomation.connect(flags); if (getTargetContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.R) {
return mUiAutomation; mUiAutomation.connect(flags);
return mUiAutomation;
}
try {
mUiAutomation.connectWithTimeout(flags, CONNECT_TIMEOUT_MILLIS);
return mUiAutomation;
} catch (TimeoutException e) {
mUiAutomation.destroy();
mUiAutomation = null;
}
} }
return null; return null;
} }

View File

@@ -22,6 +22,7 @@ import android.accessibilityservice.AccessibilityService.IAccessibilityServiceCl
import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient; import android.accessibilityservice.IAccessibilityServiceClient;
import android.accessibilityservice.IAccessibilityServiceConnection; import android.accessibilityservice.IAccessibilityServiceConnection;
import android.annotation.IntDef;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.annotation.TestApi; import android.annotation.TestApi;
@@ -60,6 +61,8 @@ import com.android.internal.util.function.pooled.PooledLambda;
import libcore.io.IoUtils; import libcore.io.IoUtils;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@@ -116,6 +119,28 @@ public final class UiAutomation {
/** Rotation constant: Freeze rotation to 270 degrees . */ /** Rotation constant: Freeze rotation to 270 degrees . */
public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270; public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
ConnectionState.DISCONNECTED,
ConnectionState.CONNECTING,
ConnectionState.CONNECTED,
ConnectionState.FAILED
})
private @interface ConnectionState {
/** The initial state before {@link #connect} or after {@link #disconnect} is called. */
int DISCONNECTED = 0;
/**
* The temporary state after {@link #connect} is called. Will transition to
* {@link #CONNECTED} or {@link #FAILED} depending on whether {@link #connect} succeeds or
* not.
*/
int CONNECTING = 1;
/** The state when {@link #connect} has succeeded. */
int CONNECTED = 2;
/** The state when {@link #connect} has failed. */
int FAILED = 3;
}
/** /**
* UiAutomation supresses accessibility services by default. This flag specifies that * UiAutomation supresses accessibility services by default. This flag specifies that
* existing accessibility services should continue to run, and that new ones may start. * existing accessibility services should continue to run, and that new ones may start.
@@ -144,12 +169,14 @@ public final class UiAutomation {
private long mLastEventTimeMillis; private long mLastEventTimeMillis;
private boolean mIsConnecting; private @ConnectionState int mConnectionState = ConnectionState.DISCONNECTED;
private boolean mIsDestroyed; private boolean mIsDestroyed;
private int mFlags; private int mFlags;
private int mGenerationId = 0;
/** /**
* Listener for observing the {@link AccessibilityEvent} stream. * Listener for observing the {@link AccessibilityEvent} stream.
*/ */
@@ -210,32 +237,55 @@ public final class UiAutomation {
} }
/** /**
* Connects this UiAutomation to the accessibility introspection APIs with default flags. * Connects this UiAutomation to the accessibility introspection APIs with default flags
* and default timeout.
* *
* @hide * @hide
*/ */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public void connect() { public void connect() {
connect(0); try {
connectWithTimeout(0, CONNECT_TIMEOUT_MILLIS);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
/**
* Connects this UiAutomation to the accessibility introspection APIs with default timeout.
*
* @hide
*/
public void connect(int flags) {
try {
connectWithTimeout(flags, CONNECT_TIMEOUT_MILLIS);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
} }
/** /**
* Connects this UiAutomation to the accessibility introspection APIs. * Connects this UiAutomation to the accessibility introspection APIs.
* *
* @param flags Any flags to apply to the automation as it gets connected * @param flags Any flags to apply to the automation as it gets connected
* @param timeoutMillis The wait timeout in milliseconds
*
* @throws TimeoutException If not connected within the timeout
* *
* @hide * @hide
*/ */
public void connect(int flags) { public void connectWithTimeout(int flags, long timeoutMillis) throws TimeoutException {
synchronized (mLock) { synchronized (mLock) {
throwIfConnectedLocked(); throwIfConnectedLocked();
if (mIsConnecting) { if (mConnectionState == ConnectionState.CONNECTING) {
return; return;
} }
mIsConnecting = true; mConnectionState = ConnectionState.CONNECTING;
mRemoteCallbackThread = new HandlerThread("UiAutomation"); mRemoteCallbackThread = new HandlerThread("UiAutomation");
mRemoteCallbackThread.start(); mRemoteCallbackThread.start();
mClient = new IAccessibilityServiceClientImpl(mRemoteCallbackThread.getLooper()); // Increment the generation since we are about to interact with a new client
mClient = new IAccessibilityServiceClientImpl(
mRemoteCallbackThread.getLooper(), ++mGenerationId);
} }
try { try {
@@ -248,24 +298,21 @@ public final class UiAutomation {
synchronized (mLock) { synchronized (mLock) {
final long startTimeMillis = SystemClock.uptimeMillis(); final long startTimeMillis = SystemClock.uptimeMillis();
try { while (true) {
while (true) { if (mConnectionState == ConnectionState.CONNECTED) {
if (isConnectedLocked()) { break;
break; }
} final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis; if (remainingTimeMillis <= 0) {
if (remainingTimeMillis <= 0) { mConnectionState = ConnectionState.FAILED;
throw new RuntimeException("Error while connecting " + this); throw new TimeoutException("Timeout while connecting " + this);
} }
try { try {
mLock.wait(remainingTimeMillis); mLock.wait(remainingTimeMillis);
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
/* ignore */ /* ignore */
}
} }
} finally {
mIsConnecting = false;
} }
} }
} }
@@ -289,12 +336,17 @@ public final class UiAutomation {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public void disconnect() { public void disconnect() {
synchronized (mLock) { synchronized (mLock) {
if (mIsConnecting) { if (mConnectionState == ConnectionState.CONNECTING) {
throw new IllegalStateException( throw new IllegalStateException(
"Cannot call disconnect() while connecting " + this); "Cannot call disconnect() while connecting " + this);
} }
throwIfNotConnectedLocked(); if (mConnectionState == ConnectionState.DISCONNECTED) {
return;
}
mConnectionState = ConnectionState.DISCONNECTED;
mConnectionId = CONNECTION_ID_UNDEFINED; mConnectionId = CONNECTION_ID_UNDEFINED;
// Increment the generation so we no longer interact with the existing client
++mGenerationId;
} }
try { try {
// Calling out without a lock held. // Calling out without a lock held.
@@ -1224,18 +1276,14 @@ public final class UiAutomation {
return stringBuilder.toString(); return stringBuilder.toString();
} }
private boolean isConnectedLocked() {
return mConnectionId != CONNECTION_ID_UNDEFINED;
}
private void throwIfConnectedLocked() { private void throwIfConnectedLocked() {
if (mConnectionId != CONNECTION_ID_UNDEFINED) { if (mConnectionState == ConnectionState.CONNECTED) {
throw new IllegalStateException("UiAutomation not connected, " + this); throw new IllegalStateException("UiAutomation connected, " + this);
} }
} }
private void throwIfNotConnectedLocked() { private void throwIfNotConnectedLocked() {
if (!isConnectedLocked()) { if (mConnectionState != ConnectionState.CONNECTED) {
throw new IllegalStateException("UiAutomation not connected, " + this); throw new IllegalStateException("UiAutomation not connected, " + this);
} }
} }
@@ -1252,11 +1300,25 @@ public final class UiAutomation {
private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper { private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper {
public IAccessibilityServiceClientImpl(Looper looper) { public IAccessibilityServiceClientImpl(Looper looper, int generationId) {
super(null, looper, new Callbacks() { super(null, looper, new Callbacks() {
private final int mGenerationId = generationId;
/**
* True if UiAutomation doesn't interact with this client anymore.
* Used by methods below to stop sending notifications or changing members
* of {@link UiAutomation}.
*/
private boolean isGenerationChangedLocked() {
return mGenerationId != UiAutomation.this.mGenerationId;
}
@Override @Override
public void init(int connectionId, IBinder windowToken) { public void init(int connectionId, IBinder windowToken) {
synchronized (mLock) { synchronized (mLock) {
if (isGenerationChangedLocked()) {
return;
}
mConnectionState = ConnectionState.CONNECTED;
mConnectionId = connectionId; mConnectionId = connectionId;
mLock.notifyAll(); mLock.notifyAll();
} }
@@ -1290,6 +1352,9 @@ public final class UiAutomation {
public void onAccessibilityEvent(AccessibilityEvent event) { public void onAccessibilityEvent(AccessibilityEvent event) {
final OnAccessibilityEventListener listener; final OnAccessibilityEventListener listener;
synchronized (mLock) { synchronized (mLock) {
if (isGenerationChangedLocked()) {
return;
}
mLastEventTimeMillis = event.getEventTime(); mLastEventTimeMillis = event.getEventTime();
if (mWaitingForEventDelivery) { if (mWaitingForEventDelivery) {
mEventQueue.add(AccessibilityEvent.obtain(event)); mEventQueue.add(AccessibilityEvent.obtain(event));