Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 912124b6 authored by JW Wang's avatar JW Wang Committed by Android (Google) Code Review
Browse files

Merge changes from topic "bug147785023"

* 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)
parents 4ec7f296 0d94533e
Loading
Loading
Loading
Loading
+27 −2
Original line number Original line Diff line number Diff line
@@ -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,9 +2189,18 @@ public class Instrumentation {
            } else {
            } else {
                mUiAutomation.disconnect();
                mUiAutomation.disconnect();
            }
            }
            if (getTargetContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.R) {
                mUiAutomation.connect(flags);
                mUiAutomation.connect(flags);
                return mUiAutomation;
                return mUiAutomation;
            }
            }
            try {
                mUiAutomation.connectWithTimeout(flags, CONNECT_TIMEOUT_MILLIS);
                return mUiAutomation;
            } catch (TimeoutException e) {
                mUiAutomation.destroy();
                mUiAutomation = null;
            }
        }
        return null;
        return null;
    }
    }


+99 −34
Original line number Original line Diff line number Diff line
@@ -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,15 +298,15 @@ public final class UiAutomation {


        synchronized (mLock) {
        synchronized (mLock) {
            final long startTimeMillis = SystemClock.uptimeMillis();
            final long startTimeMillis = SystemClock.uptimeMillis();
            try {
            while (true) {
            while (true) {
                    if (isConnectedLocked()) {
                if (mConnectionState == ConnectionState.CONNECTED) {
                    break;
                    break;
                }
                }
                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                    final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis;
                final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
                if (remainingTimeMillis <= 0) {
                if (remainingTimeMillis <= 0) {
                        throw new RuntimeException("Error while connecting " + this);
                    mConnectionState = ConnectionState.FAILED;
                    throw new TimeoutException("Timeout while connecting " + this);
                }
                }
                try {
                try {
                    mLock.wait(remainingTimeMillis);
                    mLock.wait(remainingTimeMillis);
@@ -264,9 +314,6 @@ public final class UiAutomation {
                    /* 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));