| /* |
| * 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 static android.view.Display.DEFAULT_DISPLAY; |
| |
| import android.accessibilityservice.AccessibilityGestureEvent; |
| import android.accessibilityservice.AccessibilityService; |
| import android.accessibilityservice.AccessibilityService.Callbacks; |
| import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; |
| import android.accessibilityservice.AccessibilityServiceInfo; |
| import android.accessibilityservice.IAccessibilityServiceClient; |
| import android.accessibilityservice.IAccessibilityServiceConnection; |
| import android.accessibilityservice.MagnificationConfig; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.annotation.TestApi; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.graphics.Bitmap; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.Region; |
| import android.hardware.HardwareBuffer; |
| import android.hardware.display.DisplayManagerGlobal; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.ParcelFileDescriptor; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.util.ArraySet; |
| import android.util.DebugUtils; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.Display; |
| import android.view.InputEvent; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| import android.view.SurfaceControl; |
| import android.view.View; |
| import android.view.ViewRootImpl; |
| import android.view.Window; |
| import android.view.WindowAnimationFrameStats; |
| import android.view.WindowContentFrameStats; |
| import android.view.accessibility.AccessibilityCache; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityInteractionClient; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityWindowInfo; |
| import android.view.accessibility.IAccessibilityInteractionConnection; |
| import android.view.inputmethod.EditorInfo; |
| import android.window.ScreenCapture; |
| import android.window.ScreenCapture.ScreenshotHardwareBuffer; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback; |
| import com.android.internal.inputmethod.RemoteAccessibilityInputConnection; |
| import com.android.internal.util.Preconditions; |
| import com.android.internal.util.function.pooled.PooledLambda; |
| |
| import libcore.io.IoUtils; |
| |
| import java.io.IOException; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| 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. One can think of a UiAutomation |
| * as a special type of {@link android.accessibilityservice.AccessibilityService} |
| * which does not provide hooks for the service life cycle and exposes other |
| * APIs that are useful for UI test automation. |
| * <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 boolean VERBOSE = 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; |
| |
| @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 suppresses 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; |
| |
| /** |
| * UiAutomation uses the accessibility subsystem by default. This flag provides an option to |
| * eliminate the overhead of engaging the accessibility subsystem for tests that do not need to |
| * interact with the user interface. Setting this flag disables methods that rely on |
| * accessibility. This flag is set when obtaining the UiAutomation from |
| * {@link Instrumentation#getUiAutomation(int)}. |
| */ |
| public static final int FLAG_DONT_USE_ACCESSIBILITY = 0x00000002; |
| |
| /** |
| * UiAutomation sets {@link AccessibilityServiceInfo#isAccessibilityTool()} true by default. |
| * This flag provides the option to set this field false for tests exercising that property. |
| * |
| * @hide |
| */ |
| @TestApi |
| public static final int FLAG_NOT_ACCESSIBILITY_TOOL = 0x00000004; |
| |
| /** |
| * Returned by {@link #getAdoptedShellPermissions} to indicate that all permissions have been |
| * adopted using {@link #adoptShellPermissionIdentity}. |
| * |
| * @hide |
| */ |
| @TestApi |
| @NonNull |
| public static final Set<String> ALL_PERMISSIONS = Set.of("_ALL_PERMISSIONS_"); |
| |
| private final Object mLock = new Object(); |
| |
| private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>(); |
| |
| private final Handler mLocalCallbackHandler; |
| |
| private final IUiAutomationConnection mUiAutomationConnection; |
| |
| private final int mDisplayId; |
| |
| private HandlerThread mRemoteCallbackThread; |
| |
| private IAccessibilityServiceClient mClient; |
| |
| private int mConnectionId = CONNECTION_ID_UNDEFINED; |
| |
| private OnAccessibilityEventListener mOnAccessibilityEventListener; |
| |
| private boolean mWaitingForEventDelivery; |
| |
| private long mLastEventTimeMillis; |
| |
| private @ConnectionState int mConnectionState = ConnectionState.DISCONNECTED; |
| |
| private boolean mIsDestroyed; |
| |
| private int mFlags; |
| |
| private int mGenerationId = 0; |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * Listener for filtering accessibility events. |
| */ |
| public static interface AccessibilityEventFilter { |
| |
| /** |
| * Callback for determining whether an event is accepted or |
| * it is filtered out. |
| * |
| * @param event The event to process. |
| * @return True if the event is accepted, false to filter it out. |
| */ |
| public boolean accept(AccessibilityEvent event); |
| } |
| |
| /** |
| * Creates a new instance that will handle callbacks from the accessibility |
| * layer on the thread of the provided context main looper and perform requests for privileged |
| * operations on the provided connection, and filtering display-related features to the display |
| * associated with the context (or the user running the test, on devices that |
| * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}). |
| * |
| * @param context the context associated with the automation |
| * @param connection The connection for performing privileged operations. |
| * |
| * @hide |
| */ |
| public UiAutomation(Context context, IUiAutomationConnection connection) { |
| this(getDisplayId(context), context.getMainLooper(), connection); |
| } |
| |
| /** |
| * 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. |
| * |
| * @deprecated use {@link #UiAutomation(Context, IUiAutomationConnection)} instead |
| * |
| * @hide |
| */ |
| @Deprecated |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| public UiAutomation(Looper looper, IUiAutomationConnection connection) { |
| this(DEFAULT_DISPLAY, looper, connection); |
| Log.w(LOG_TAG, "Created with deprecatead constructor, assumes DEFAULT_DISPLAY"); |
| } |
| |
| private UiAutomation(int displayId, Looper looper, IUiAutomationConnection connection) { |
| Preconditions.checkArgument(looper != null, "Looper cannot be null!"); |
| Preconditions.checkArgument(connection != null, "Connection cannot be null!"); |
| |
| mLocalCallbackHandler = new Handler(looper); |
| mUiAutomationConnection = connection; |
| mDisplayId = displayId; |
| |
| Log.i(LOG_TAG, "Initialized for user " + Process.myUserHandle().getIdentifier() |
| + " on display " + mDisplayId); |
| } |
| |
| /** |
| * Connects this UiAutomation to the accessibility introspection APIs with default flags |
| * and default timeout. |
| * |
| * @hide |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| public void connect() { |
| 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. |
| * |
| * @param flags Any flags to apply to the automation as it gets connected while |
| * {@link UiAutomation#FLAG_DONT_USE_ACCESSIBILITY} would keep the |
| * connection disconnected and not to register UiAutomation service. |
| * @param timeoutMillis The wait timeout in milliseconds |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is already |
| * established. |
| * @throws TimeoutException If not connected within the timeout |
| * @hide |
| */ |
| public void connectWithTimeout(int flags, long timeoutMillis) throws TimeoutException { |
| if (DEBUG) { |
| Log.d(LOG_TAG, "connectWithTimeout: user=" + Process.myUserHandle().getIdentifier() |
| + ", flags=" + DebugUtils.flagsToString(UiAutomation.class, "FLAG_", flags) |
| + ", timeout=" + timeoutMillis + "ms"); |
| } |
| synchronized (mLock) { |
| throwIfConnectedLocked(); |
| if (mConnectionState == ConnectionState.CONNECTING) { |
| if (DEBUG) Log.d(LOG_TAG, "already connecting"); |
| return; |
| } |
| if (DEBUG) Log.d(LOG_TAG, "setting state to CONNECTING"); |
| mConnectionState = ConnectionState.CONNECTING; |
| mRemoteCallbackThread = new HandlerThread("UiAutomation"); |
| mRemoteCallbackThread.start(); |
| // Increment the generation since we are about to interact with a new client |
| mClient = new IAccessibilityServiceClientImpl( |
| mRemoteCallbackThread.getLooper(), ++mGenerationId); |
| } |
| |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.connect(mClient, flags); |
| mFlags = flags; |
| // If UiAutomation is not allowed to use the accessibility subsystem, the |
| // connection state should keep disconnected and not to start the client connection. |
| if (!useAccessibility()) { |
| if (DEBUG) Log.d(LOG_TAG, "setting state to DISCONNECTED"); |
| mConnectionState = ConnectionState.DISCONNECTED; |
| return; |
| } |
| } catch (RemoteException re) { |
| throw new RuntimeException("Error while connecting " + this, re); |
| } |
| |
| synchronized (mLock) { |
| final long startTimeMillis = SystemClock.uptimeMillis(); |
| while (true) { |
| if (mConnectionState == ConnectionState.CONNECTED) { |
| break; |
| } |
| final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; |
| final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; |
| if (remainingTimeMillis <= 0) { |
| if (DEBUG) Log.d(LOG_TAG, "setting state to FAILED"); |
| mConnectionState = ConnectionState.FAILED; |
| throw new TimeoutException("Timeout while connecting " + this); |
| } |
| try { |
| mLock.wait(remainingTimeMillis); |
| } catch (InterruptedException ie) { |
| /* ignore */ |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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 |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| public void disconnect() { |
| synchronized (mLock) { |
| if (mConnectionState == ConnectionState.CONNECTING) { |
| throw new IllegalStateException( |
| "Cannot call disconnect() while connecting " + this); |
| } |
| if (useAccessibility() && mConnectionState == ConnectionState.DISCONNECTED) { |
| return; |
| } |
| mConnectionState = ConnectionState.DISCONNECTED; |
| mConnectionId = CONNECTION_ID_UNDEFINED; |
| // Increment the generation so we no longer interact with the existing client |
| ++mGenerationId; |
| } |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.disconnect(); |
| } catch (RemoteException re) { |
| throw new RuntimeException("Error while disconnecting " + this, re); |
| } finally { |
| if (mRemoteCallbackThread != null) { |
| mRemoteCallbackThread.quit(); |
| mRemoteCallbackThread = null; |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| |
| /** |
| * 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. |
| * The callbacks are delivered on the main application thread. |
| * |
| * @param listener The callback. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| */ |
| public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) { |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| mOnAccessibilityEventListener = listener; |
| } |
| } |
| |
| /** |
| * Destroy this UiAutomation. After calling this method, attempting to use the object will |
| * result in errors. |
| * |
| * @hide |
| */ |
| @TestApi |
| public void destroy() { |
| disconnect(); |
| mIsDestroyed = true; |
| } |
| |
| /** |
| * Clears the accessibility cache. |
| * |
| * @return {@code true} if the cache was cleared |
| * @see AccessibilityService#clearCache() |
| */ |
| public boolean clearCache() { |
| final int connectionId; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connectionId = mConnectionId; |
| } |
| final AccessibilityCache cache = AccessibilityInteractionClient.getCache(connectionId); |
| if (cache == null) { |
| return false; |
| } |
| cache.clear(); |
| return true; |
| } |
| |
| /** |
| * Checks if {@code node} is in the accessibility cache. |
| * |
| * @param node the node to check. |
| * @return {@code true} if {@code node} is in the cache. |
| * @hide |
| * @see AccessibilityService#isNodeInCache(AccessibilityNodeInfo) |
| */ |
| @TestApi |
| public boolean isNodeInCache(@NonNull AccessibilityNodeInfo node) { |
| final int connectionId; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connectionId = mConnectionId; |
| } |
| final AccessibilityCache cache = AccessibilityInteractionClient.getCache(connectionId); |
| if (cache == null) { |
| return false; |
| } |
| return cache.isNodeInCache(node); |
| } |
| |
| /** |
| * Provides reference to the cache through a locked connection. |
| * |
| * @return the accessibility cache. |
| * @hide |
| */ |
| public @Nullable AccessibilityCache getCache() { |
| final int connectionId; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connectionId = mConnectionId; |
| } |
| return AccessibilityInteractionClient.getCache(connectionId); |
| } |
| |
| /** |
| * Adopt the permission identity of the shell UID for all permissions. This allows |
| * you to call APIs protected permissions which normal apps cannot hold but are |
| * granted to the shell UID. If you already adopted all shell permissions by calling |
| * this method or {@link #adoptShellPermissionIdentity(String...)} a subsequent call will |
| * replace any previous adoption. Note that your permission state becomes that of the shell UID |
| * and it is not a combination of your and the shell UID permissions. |
| * <p> |
| * <strong>Note:<strong/> Calling this method adopts all shell permissions and overrides |
| * any subset of adopted permissions via {@link #adoptShellPermissionIdentity(String...)}. |
| * |
| * @see #adoptShellPermissionIdentity(String...) |
| * @see #dropShellPermissionIdentity() |
| */ |
| public void adoptShellPermissionIdentity() { |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid(), null); |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Adopt the permission identity of the shell UID only for the provided permissions. |
| * This allows you to call APIs protected permissions which normal apps cannot hold |
| * but are granted to the shell UID. If you already adopted shell permissions by calling |
| * this method, or {@link #adoptShellPermissionIdentity()} a subsequent call will replace any |
| * previous adoption. |
| * <p> |
| * <strong>Note:<strong/> This method behave differently from |
| * {@link #adoptShellPermissionIdentity()}. Only the listed permissions will use the shell |
| * identity and other permissions will still check against the original UID |
| * |
| * @param permissions The permissions to adopt or <code>null</code> to adopt all. |
| * |
| * @see #adoptShellPermissionIdentity() |
| * @see #dropShellPermissionIdentity() |
| */ |
| public void adoptShellPermissionIdentity(@Nullable String... permissions) { |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid(), permissions); |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Drop the shell permission identity adopted by a previous call to |
| * {@link #adoptShellPermissionIdentity()}. If you did not adopt the shell permission |
| * identity this method would be a no-op. |
| * |
| * @see #adoptShellPermissionIdentity() |
| */ |
| public void dropShellPermissionIdentity() { |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.dropShellPermissionIdentity(); |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Returns a list of adopted shell permissions using {@link #adoptShellPermissionIdentity}, |
| * returns and empty set if no permissions are adopted and {@link #ALL_PERMISSIONS} if all |
| * permissions are adopted. |
| * |
| * @hide |
| */ |
| @TestApi |
| @NonNull |
| public Set<String> getAdoptedShellPermissions() { |
| try { |
| final List<String> permissions = mUiAutomationConnection.getAdoptedShellPermissions(); |
| return permissions == null ? ALL_PERMISSIONS : new ArraySet<>(permissions); |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Adds permission to be overridden to the given state. UiAutomation must be connected to |
| * root user. |
| * |
| * @param uid The UID of the app whose permission will be overridden |
| * @param permission The permission whose state will be overridden |
| * @param result The state to override the permission to |
| * |
| * @see PackageManager#PERMISSION_GRANTED |
| * @see PackageManager#PERMISSION_DENIED |
| * |
| * @hide |
| */ |
| @TestApi |
| @SuppressLint("UnflaggedApi") |
| public void addOverridePermissionState(int uid, @NonNull String permission, |
| @PackageManager.PermissionResult int result) { |
| try { |
| mUiAutomationConnection.addOverridePermissionState(uid, permission, result); |
| } catch (RemoteException re) { |
| re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Removes overridden permission. UiAutomation must be connected to root user. |
| * |
| * @param uid The UID of the app whose permission is overridden |
| * @param permission The permission whose state will no longer be overridden |
| * |
| * @hide |
| */ |
| @TestApi |
| @SuppressLint("UnflaggedApi") |
| public void removeOverridePermissionState(int uid, @NonNull String permission) { |
| try { |
| mUiAutomationConnection.removeOverridePermissionState(uid, permission); |
| } catch (RemoteException re) { |
| re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Clears all overridden permissions for the given UID. UiAutomation must be connected to |
| * root user. |
| * |
| * @param uid The UID of the app whose permissions will no longer be overridden |
| * |
| * @hide |
| */ |
| @TestApi |
| @SuppressLint("UnflaggedApi") |
| public void clearOverridePermissionStates(int uid) { |
| try { |
| mUiAutomationConnection.clearOverridePermissionStates(uid); |
| } catch (RemoteException re) { |
| re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Clears all overridden permissions on the device. UiAutomation must be connected to root user. |
| * |
| * @hide |
| */ |
| @TestApi |
| @SuppressLint("UnflaggedApi") |
| public void clearAllOverridePermissionStates() { |
| try { |
| mUiAutomationConnection.clearAllOverridePermissionStates(); |
| } catch (RemoteException re) { |
| re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * @param action The action to perform. |
| * @return Whether the action was successfully performed. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK |
| * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME |
| * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS |
| * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS |
| */ |
| public final boolean performGlobalAction(int action) { |
| final IAccessibilityServiceConnection connection; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connection = AccessibilityInteractionClient.getInstance() |
| .getConnection(mConnectionId); |
| } |
| // Calling out without a lock held. |
| if (connection != null) { |
| try { |
| return connection.performGlobalAction(action); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while calling performGlobalAction", re); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Find the view that has the specified focus type. The search is performed |
| * across all windows. |
| * <p> |
| * <strong>Note:</strong> In order to access the windows you have to opt-in |
| * to retrieve the interactive windows by setting the |
| * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag. |
| * Otherwise, the search will be performed only in the active window. |
| * </p> |
| * |
| * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or |
| * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. |
| * @return The node info of the focused view or null. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * @see AccessibilityNodeInfo#FOCUS_INPUT |
| * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY |
| */ |
| public AccessibilityNodeInfo findFocus(int focus) { |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| } |
| return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, |
| AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus); |
| } |
| |
| /** |
| * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation. |
| * This method is useful if one wants to change some of the dynamically |
| * configurable properties at runtime. |
| * |
| * @return The accessibility service info. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * @see AccessibilityServiceInfo |
| */ |
| public final AccessibilityServiceInfo getServiceInfo() { |
| final IAccessibilityServiceConnection connection; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connection = AccessibilityInteractionClient.getInstance() |
| .getConnection(mConnectionId); |
| } |
| // Calling out without a lock held. |
| if (connection != null) { |
| try { |
| return connection.getServiceInfo(); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Sets the {@link AccessibilityServiceInfo} that describes how this |
| * UiAutomation will be handled by the platform accessibility layer. |
| * |
| * @param info The info. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * @see AccessibilityServiceInfo |
| */ |
| public final void setServiceInfo(AccessibilityServiceInfo info) { |
| final IAccessibilityServiceConnection connection; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| AccessibilityInteractionClient.getInstance().clearCache(mConnectionId); |
| connection = AccessibilityInteractionClient.getInstance() |
| .getConnection(mConnectionId); |
| } |
| // Calling out without a lock held. |
| if (connection != null) { |
| try { |
| connection.setServiceInfo(info); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re); |
| } |
| } |
| } |
| |
| /** |
| * Gets the windows on the screen associated with the {@link UiAutomation} context (usually the |
| * {@link android.view.Display#DEFAULT_DISPLAY default display). |
| * |
| * <p> |
| * This method returns only the windows that a sighted user can interact with, as opposed to |
| * all windows. |
| |
| * <p> |
| * For example, if there is a modal dialog shown and the user cannot touch |
| * anything behind it, then only the modal window will be reported |
| * (assuming it is the top one). For convenience the returned windows |
| * are ordered in a descending layer order, which is the windows that |
| * are higher in the Z-order are reported first. |
| * <p> |
| * <strong>Note:</strong> In order to access the windows you have to opt-in |
| * to retrieve the interactive windows by setting the |
| * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag. |
| * |
| * @return The windows if there are windows such, otherwise an empty list. |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| */ |
| public List<AccessibilityWindowInfo> getWindows() { |
| if (DEBUG) { |
| Log.d(LOG_TAG, "getWindows(): returning windows for display " + mDisplayId); |
| } |
| final int connectionId; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connectionId = mConnectionId; |
| } |
| // Calling out without a lock held. |
| return AccessibilityInteractionClient.getInstance().getWindowsOnDisplay(connectionId, |
| mDisplayId); |
| } |
| |
| /** |
| * Gets the windows on the screen of all displays. This method returns only the windows |
| * that a sighted user can interact with, as opposed to all windows. |
| * For example, if there is a modal dialog shown and the user cannot touch |
| * anything behind it, then only the modal window will be reported |
| * (assuming it is the top one). For convenience the returned windows |
| * are ordered in a descending layer order, which is the windows that |
| * are higher in the Z-order are reported first. |
| * <p> |
| * <strong>Note:</strong> In order to access the windows you have to opt-in |
| * to retrieve the interactive windows by setting the |
| * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag. |
| * </p> |
| * |
| * @return The windows of all displays if there are windows and the service is can retrieve |
| * them, otherwise an empty list. The key of SparseArray is display ID. |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| */ |
| @NonNull |
| public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() { |
| final int connectionId; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connectionId = mConnectionId; |
| } |
| // Calling out without a lock held. |
| return AccessibilityInteractionClient.getInstance() |
| .getWindowsOnAllDisplays(connectionId); |
| } |
| |
| /** |
| * Gets the root {@link AccessibilityNodeInfo} in the active window. |
| * |
| * @return The root info. |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| */ |
| public AccessibilityNodeInfo getRootInActiveWindow() { |
| return getRootInActiveWindow(AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_HYBRID); |
| } |
| |
| /** |
| * Gets the root {@link AccessibilityNodeInfo} in the active window. |
| * |
| * @param prefetchingStrategy the prefetching strategy. |
| * @return The root info. |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * |
| * @hide |
| */ |
| @Nullable |
| public AccessibilityNodeInfo getRootInActiveWindow( |
| @AccessibilityNodeInfo.PrefetchingStrategy int prefetchingStrategy) { |
| final int connectionId; |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| connectionId = mConnectionId; |
| } |
| // Calling out without a lock held. |
| return AccessibilityInteractionClient.getInstance() |
| .getRootInActiveWindow(connectionId, prefetchingStrategy); |
| } |
| |
| /** |
| * A method for injecting an arbitrary input event. |
| * |
| * This method waits for all window container animations and surface operations to complete. |
| * |
| * <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) { |
| return injectInputEvent(event, sync, true /* waitForAnimations */); |
| } |
| |
| /** |
| * A method for injecting an arbitrary input event, optionally waiting for window animations to |
| * complete. |
| * <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. |
| * @param waitForAnimations Whether to wait for all window container animations and surface |
| * operations to complete. |
| * @return Whether event injection succeeded. |
| * |
| * @hide |
| */ |
| @TestApi |
| public boolean injectInputEvent(@NonNull InputEvent event, boolean sync, |
| boolean waitForAnimations) { |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync + " waitForAnimations: " |
| + waitForAnimations); |
| } |
| // Calling out without a lock held. |
| return mUiAutomationConnection.injectInputEvent(event, sync, waitForAnimations); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while injecting input event!", re); |
| } |
| return false; |
| } |
| |
| /** |
| * Injects an arbitrary {@link InputEvent} to the accessibility input filter, for use in testing |
| * the accessibility input filter. |
| * |
| * Events injected to the input subsystem using the standard {@link #injectInputEvent} method |
| * skip the accessibility input filter to avoid feedback loops. |
| * |
| * @hide |
| */ |
| @TestApi |
| public void injectInputEventToInputFilter(@NonNull InputEvent event) { |
| try { |
| mUiAutomationConnection.injectInputEventToInputFilter(event); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while injecting input event to input filter", re); |
| } |
| } |
| |
| /** |
| * Sets the system settings values that control the scaling factor for animations. The scale |
| * controls the animation playback speed for animations that respect these settings. Animations |
| * that do not respect the settings values will not be affected by this function. A lower scale |
| * value results in a faster speed. A value of <code>0</code> disables animations entirely. When |
| * animations are disabled services receive window change events more quickly which can reduce |
| * the potential by confusion by reducing the time during which windows are in transition. |
| * |
| * @see AccessibilityEvent#TYPE_WINDOWS_CHANGED |
| * @see AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED |
| * @see android.provider.Settings.Global#WINDOW_ANIMATION_SCALE |
| * @see android.provider.Settings.Global#TRANSITION_ANIMATION_SCALE |
| * @see android.provider.Settings.Global#ANIMATOR_DURATION_SCALE |
| * @param scale The scaling factor for all animations. |
| */ |
| public void setAnimationScale(float scale) { |
| final IAccessibilityServiceConnection connection = |
| AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); |
| if (connection != null) { |
| try { |
| connection.setAnimationScale(scale); |
| } catch (RemoteException re) { |
| throw new RuntimeException(re); |
| } |
| } |
| } |
| |
| /** |
| * A request for WindowManagerService to wait until all animations have completed and input |
| * information has been sent from WindowManager to native InputManager. |
| * |
| * @hide |
| */ |
| @TestApi |
| public void syncInputTransactions() { |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.syncInputTransactions(true /* waitForAnimations */); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while syncing input transactions!", re); |
| } |
| } |
| |
| /** |
| * A request for WindowManagerService to wait until all input information has been sent from |
| * WindowManager to native InputManager and optionally wait for animations to complete. |
| * |
| * @param waitForAnimations Whether to wait for all window container animations and surface |
| * operations to complete. |
| * |
| * @hide |
| */ |
| @TestApi |
| public void syncInputTransactions(boolean waitForAnimations) { |
| try { |
| // Calling out without a lock held. |
| mUiAutomationConnection.syncInputTransactions(waitForAnimations); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while syncing input transactions!", re); |
| } |
| } |
| |
| /** |
| * 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) { |
| 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. |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| */ |
| public AccessibilityEvent executeAndWaitForEvent(Runnable command, |
| AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException { |
| // Acquire the lock and prepare for receiving events. |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| mEventQueue.clear(); |
| // Prepare to wait for an event. |
| mWaitingForEventDelivery = true; |
| } |
| |
| // Note: We have to release the lock since calling out with this lock held |
| // can bite. We will correctly filter out events from other interactions, |
| // so starting to collect events before running the action is just fine. |
| |
| // We will ignore events from previous interactions. |
| final long executionStartTimeMillis = SystemClock.uptimeMillis(); |
| // Execute the command *without* the lock being held. |
| command.run(); |
| |
| List<AccessibilityEvent> receivedEvents = new ArrayList<>(); |
| |
| // Acquire the lock and wait for the event. |
| try { |
| // Wait for the event. |
| final long startTimeMillis = SystemClock.uptimeMillis(); |
| while (true) { |
| List<AccessibilityEvent> localEvents = new ArrayList<>(); |
| synchronized (mLock) { |
| localEvents.addAll(mEventQueue); |
| mEventQueue.clear(); |
| } |
| // Drain the event queue |
| while (!localEvents.isEmpty()) { |
| AccessibilityEvent event = localEvents.remove(0); |
| // Ignore events from previous interactions. |
| if (event.getEventTime() < executionStartTimeMillis) { |
| continue; |
| } |
| if (filter.accept(event)) { |
| return event; |
| } |
| receivedEvents.add(event); |
| } |
| // 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 among: " + receivedEvents); |
| } |
| synchronized (mLock) { |
| if (mEventQueue.isEmpty()) { |
| try { |
| mLock.wait(remainingTimeMillis); |
| } catch (InterruptedException ie) { |
| /* ignore */ |
| } |
| } |
| } |
| } |
| } finally { |
| int size = receivedEvents.size(); |
| for (int i = 0; i < size; i++) { |
| receivedEvents.get(i).recycle(); |
| } |
| |
| synchronized (mLock) { |
| 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> |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| */ |
| 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() { |
| if (DEBUG) { |
| Log.d(LOG_TAG, "Taking screenshot of display " + mDisplayId); |
| } |
| Display display = DisplayManagerGlobal.getInstance().getRealDisplay(mDisplayId); |
| Point displaySize = new Point(); |
| display.getRealSize(displaySize); |
| |
| // Take the screenshot |
| ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture = |
| ScreenCapture.createSyncCaptureListener(); |
| try { |
| if (!mUiAutomationConnection.takeScreenshot( |
| new Rect(0, 0, displaySize.x, displaySize.y), syncScreenCapture)) { |
| return null; |
| } |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while taking screenshot of display " + mDisplayId, re); |
| return null; |
| } |
| |
| final ScreenshotHardwareBuffer screenshotBuffer = syncScreenCapture.getBuffer(); |
| if (screenshotBuffer == null) { |
| Log.e(LOG_TAG, "Failed to take screenshot for display=" + mDisplayId); |
| return null; |
| } |
| Bitmap screenShot = screenshotBuffer.asBitmap(); |
| if (screenShot == null) { |
| Log.e(LOG_TAG, "Failed to take screenshot for display=" + mDisplayId); |
| return null; |
| } |
| Bitmap swBitmap; |
| try (HardwareBuffer buffer = screenshotBuffer.getHardwareBuffer()) { |
| swBitmap = screenShot.copy(Bitmap.Config.ARGB_8888, false); |
| } |
| screenShot.recycle(); |
| |
| // Optimization |
| swBitmap.setHasAlpha(false); |
| return swBitmap; |
| } |
| |
| /** |
| * Used to capture a screenshot of a Window. This can return null in the following cases: |
| * 1. Window content hasn't been layed out. |
| * 2. Window doesn't have a valid SurfaceControl |
| * 3. An error occurred in SurfaceFlinger when trying to take the screenshot. |
| * |
| * @param window Window to take a screenshot of |
| * |
| * @return The screenshot bitmap on success, null otherwise. |
| */ |
| @Nullable |
| public Bitmap takeScreenshot(@NonNull Window window) { |
| if (window == null) { |
| return null; |
| } |
| |
| View decorView = window.peekDecorView(); |
| if (decorView == null) { |
| return null; |
| } |
| |
| ViewRootImpl viewRoot = decorView.getViewRootImpl(); |
| if (viewRoot == null) { |
| return null; |
| } |
| |
| SurfaceControl sc = viewRoot.getSurfaceControl(); |
| if (!sc.isValid()) { |
| return null; |
| } |
| |
| // Apply a sync transaction to ensure SurfaceFlinger is flushed before capturing a |
| // screenshot. |
| new SurfaceControl.Transaction().apply(true); |
| ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture = |
| ScreenCapture.createSyncCaptureListener(); |
| try { |
| if (!mUiAutomationConnection.takeSurfaceControlScreenshot(sc, syncScreenCapture)) { |
| Log.e(LOG_TAG, "Failed to take screenshot for window=" + window); |
| return null; |
| } |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while taking screenshot!", re); |
| return null; |
| } |
| ScreenCapture.ScreenshotHardwareBuffer captureBuffer = syncScreenCapture.getBuffer(); |
| if (captureBuffer == null) { |
| Log.e(LOG_TAG, "Failed to take screenshot for window=" + window); |
| return null; |
| } |
| Bitmap screenShot = captureBuffer.asBitmap(); |
| if (screenShot == null) { |
| Log.e(LOG_TAG, "Failed to take screenshot for window=" + window); |
| return null; |
| } |
| Bitmap swBitmap; |
| try (HardwareBuffer buffer = captureBuffer.getHardwareBuffer()) { |
| swBitmap = screenShot.copy(Bitmap.Config.ARGB_8888, false); |
| } |
| |
| screenShot.recycle(); |
| return swBitmap; |
| } |
| |
| /** |
| * Sets whether this UiAutomation to run in a "monkey" mode. Applications can query whether |
| * they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing |
| * potentially undesirable actions such as calling 911 or posting on public forums etc. |
| * |
| * @param enable whether to run in a "monkey" mode or not. Default is not. |
| * @see ActivityManager#isUserAMonkey() |
| */ |
| public void setRunAsMonkey(boolean enable) { |
| try { |
| ActivityManager.getService().setUserIsMonkey(enable); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error while setting run as monkey!", re); |
| } |
| } |
| |
| /** |
| * Clears the frame statistics for the content of a given window. These |
| * statistics contain information about the most recently rendered content |
| * frames. |
| * |
| * @param windowId The window id. |
| * @return Whether the window is present and its frame statistics |
| * were cleared. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * @see android.view.WindowContentFrameStats |
| * @see #getWindowContentFrameStats(int) |
| * @see #getWindows() |
| * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId() |
| */ |
| public boolean clearWindowContentFrameStats(int windowId) { |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| } |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Clearing content frame stats for window: " + windowId); |
| } |
| // Calling out without a lock held. |
| return mUiAutomationConnection.clearWindowContentFrameStats(windowId); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error clearing window content frame stats!", re); |
| } |
| return false; |
| } |
| |
| /** |
| * Gets the frame statistics for a given window. These statistics contain |
| * information about the most recently rendered content frames. |
| * <p> |
| * A typical usage requires clearing the window frame statistics via {@link |
| * #clearWindowContentFrameStats(int)} followed by an interaction with the UI and |
| * finally getting the window frame statistics via calling this method. |
| * </p> |
| * <pre> |
| * // Assume we have at least one window. |
| * final int windowId = getWindows().get(0).getId(); |
| * |
| * // Start with a clean slate. |
| * uiAutimation.clearWindowContentFrameStats(windowId); |
| * |
| * // Do stuff with the UI. |
| * |
| * // Get the frame statistics. |
| * WindowContentFrameStats stats = uiAutomation.getWindowContentFrameStats(windowId); |
| * </pre> |
| * |
| * @param windowId The window id. |
| * @return The window frame statistics, or null if the window is not present. |
| * |
| * @throws IllegalStateException If the connection to the accessibility subsystem is not |
| * established. |
| * @see android.view.WindowContentFrameStats |
| * @see #clearWindowContentFrameStats(int) |
| * @see #getWindows() |
| * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId() |
| */ |
| public WindowContentFrameStats getWindowContentFrameStats(int windowId) { |
| synchronized (mLock) { |
| throwIfNotConnectedLocked(); |
| } |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Getting content frame stats for window: " + windowId); |
| } |
| // Calling out without a lock held. |
| return mUiAutomationConnection.getWindowContentFrameStats(windowId); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error getting window content frame stats!", re); |
| } |
| return null; |
| } |
| |
| /** |
| * Clears the window animation rendering statistics. These statistics contain |
| * information about the most recently rendered window animation frames, i.e. |
| * for window transition animations. |
| * |
| * @see android.view.WindowAnimationFrameStats |
| * @see #getWindowAnimationFrameStats() |
| * @see android.R.styleable#WindowAnimation |
| * @deprecated animation-frames are no-longer used. Use Shared |
| * <a href="https://perfetto.dev/docs/data-sources/frametimeline">FrameTimeline</a> |
| * jank metrics instead. |
| */ |
| @Deprecated |
| public void clearWindowAnimationFrameStats() { |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Clearing window animation frame stats"); |
| } |
| // Calling out without a lock held. |
| mUiAutomationConnection.clearWindowAnimationFrameStats(); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error clearing window animation frame stats!", re); |
| } |
| } |
| |
| /** |
| * Gets the window animation frame statistics. These statistics contain |
| * information about the most recently rendered window animation frames, i.e. |
| * for window transition animations. |
| * |
| * <p> |
| * A typical usage requires clearing the window animation frame statistics via |
| * {@link #clearWindowAnimationFrameStats()} followed by an interaction that causes |
| * a window transition which uses a window animation and finally getting the window |
| * animation frame statistics by calling this method. |
| * </p> |
| * <pre> |
| * // Start with a clean slate. |
| * uiAutimation.clearWindowAnimationFrameStats(); |
| * |
| * // Do stuff to trigger a window transition. |
| * |
| * // Get the frame statistics. |
| * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats(); |
| * </pre> |
| * |
| * @return The window animation frame statistics. |
| * |
| * @see android.view.WindowAnimationFrameStats |
| * @see #clearWindowAnimationFrameStats() |
| * @see android.R.styleable#WindowAnimation |
| * @deprecated animation-frames are no-longer used. |
| */ |
| @Deprecated |
| public WindowAnimationFrameStats getWindowAnimationFrameStats() { |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Getting window animation frame stats"); |
| } |
| // Calling out without a lock held. |
| return mUiAutomationConnection.getWindowAnimationFrameStats(); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error getting window animation frame stats!", re); |
| } |
| return null; |
| } |
| |
| /** |
| * Grants a runtime permission to a package. |
| * |
| * @param packageName The package to which to grant. |
| * @param permission The permission to grant. |
| * @throws SecurityException if unable to grant the permission. |
| */ |
| public void grantRuntimePermission(String packageName, String permission) { |
| grantRuntimePermissionAsUser(packageName, permission, android.os.Process.myUserHandle()); |
| } |
| |
| /** |
| * @deprecated replaced by |
| * {@link #grantRuntimePermissionAsUser(String, String, UserHandle)}. |
| * @hide |
| */ |
| @Deprecated |
| @TestApi |
| public boolean grantRuntimePermission(String packageName, String permission, |
| UserHandle userHandle) { |
| grantRuntimePermissionAsUser(packageName, permission, userHandle); |
| return true; |
| } |
| |
| /** |
| * Grants a runtime permission to a package for a user. |
| * |
| * @param packageName The package to which to grant. |
| * @param permission The permission to grant. |
| * @throws SecurityException if unable to grant the permission. |
| */ |
| public void grantRuntimePermissionAsUser(String packageName, String permission, |
| UserHandle userHandle) { |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Granting runtime permission (" + permission + ") to package " |
| + packageName + " on user " + userHandle); |
| } |
| // Calling out without a lock held. |
| mUiAutomationConnection.grantRuntimePermission(packageName, |
| permission, userHandle.getIdentifier()); |
| } catch (Exception e) { |
| throw new SecurityException("Error granting runtime permission", e); |
| } |
| } |
| |
| /** |
| * Revokes a runtime permission from a package. |
| * |
| * @param packageName The package to which to grant. |
| * @param permission The permission to grant. |
| * @throws SecurityException if unable to revoke the permission. |
| */ |
| public void revokeRuntimePermission(String packageName, String permission) { |
| revokeRuntimePermissionAsUser(packageName, permission, android.os.Process.myUserHandle()); |
| } |
| |
| /** |
| * @deprecated replaced by |
| * {@link #revokeRuntimePermissionAsUser(String, String, UserHandle)}. |
| * @hide |
| */ |
| @Deprecated |
| @TestApi |
| public boolean revokeRuntimePermission(String packageName, String permission, |
| UserHandle userHandle) { |
| revokeRuntimePermissionAsUser(packageName, permission, userHandle); |
| return true; |
| } |
| |
| /** |
| * Revokes a runtime permission from a package. |
| * |
| * @param packageName The package to which to grant. |
| * @param permission The permission to grant. |
| * @throws SecurityException if unable to revoke the permission. |
| */ |
| public void revokeRuntimePermissionAsUser(String packageName, String permission, |
| UserHandle userHandle) { |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "Revoking runtime permission"); |
| } |
| // Calling out without a lock held. |
| mUiAutomationConnection.revokeRuntimePermission(packageName, |
| permission, userHandle.getIdentifier()); |
| } catch (Exception e) { |
| throw new SecurityException("Error granting runtime permission", e); |
| } |
| } |
| |
| /** |
| * Executes a shell command. This method returns a file descriptor that points |
| * to the standard output stream. The command execution is similar to running |
| * "adb shell <command>" from a host connected to the device. |
| * <p> |
| * <strong>Note:</strong> It is your responsibility to close the returned file |
| * descriptor once you are done reading. |
| * </p> |
| * |
| * @param command The command to execute. |
| * @return A file descriptor to the standard output stream. |
| * |
| * @see #adoptShellPermissionIdentity() |
| */ |
| public ParcelFileDescriptor executeShellCommand(String command) { |
| warnIfBetterCommand(command); |
| |
| ParcelFileDescriptor source = null; |
| ParcelFileDescriptor sink = null; |
| |
| try { |
| ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); |
| source = pipe[0]; |
| sink = pipe[1]; |
| |
| // Calling out without a lock held. |
| mUiAutomationConnection.executeShellCommand(command, sink, null); |
| } catch (IOException ioe) { |
| Log.e(LOG_TAG, "Error executing shell command!", ioe); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error executing shell command!", re); |
| } finally { |
| IoUtils.closeQuietly(sink); |
| } |
| |
| return source; |
| } |
| |
| /** |
| * Executes a shell command. This method returns two file descriptors, |
| * one that points to the standard output stream (element at index 0), and one that points |
| * to the standard input stream (element at index 1). The command execution is similar |
| * to running "adb shell <command>" from a host connected to the device. |
| * <p> |
| * <strong>Note:</strong> It is your responsibility to close the returned file |
| * descriptors once you are done reading/writing. |
| * </p> |
| * |
| * @param command The command to execute. |
| * @return File descriptors (out, in) to the standard output/input streams. |
| */ |
| @SuppressLint("ArrayReturn") // For consistency with other APIs |
| public @NonNull ParcelFileDescriptor[] executeShellCommandRw(@NonNull String command) { |
| return executeShellCommandInternal(command, false /* includeStderr */); |
| } |
| |
| /** |
| * Executes a shell command. This method returns three file descriptors, |
| * one that points to the standard output stream (element at index 0), one that points |
| * to the standard input stream (element at index 1), and one points to |
| * standard error stream (element at index 2). The command execution is similar |
| * to running "adb shell <command>" from a host connected to the device. |
| * <p> |
| * <strong>Note:</strong> It is your responsibility to close the returned file |
| * descriptors once you are done reading/writing. |
| * </p> |
| * |
| * @param command The command to execute. |
| * @return File descriptors (out, in, err) to the standard output/input/error streams. |
| */ |
| @SuppressLint("ArrayReturn") // For consistency with other APIs |
| public @NonNull ParcelFileDescriptor[] executeShellCommandRwe(@NonNull String command) { |
| return executeShellCommandInternal(command, true /* includeStderr */); |
| } |
| |
| /** |
| * @hide |
| */ |
| @VisibleForTesting |
| public int getDisplayId() { |
| return mDisplayId; |
| } |
| |
| private ParcelFileDescriptor[] executeShellCommandInternal( |
| String command, boolean includeStderr) { |
| warnIfBetterCommand(command); |
| |
| ParcelFileDescriptor source_read = null; |
| ParcelFileDescriptor sink_read = null; |
| |
| ParcelFileDescriptor source_write = null; |
| ParcelFileDescriptor sink_write = null; |
| |
| ParcelFileDescriptor stderr_source_read = null; |
| ParcelFileDescriptor stderr_sink_read = null; |
| |
| try { |
| ParcelFileDescriptor[] pipe_read = ParcelFileDescriptor.createPipe(); |
| source_read = pipe_read[0]; |
| sink_read = pipe_read[1]; |
| |
| ParcelFileDescriptor[] pipe_write = ParcelFileDescriptor.createPipe(); |
| source_write = pipe_write[0]; |
| sink_write = pipe_write[1]; |
| |
| if (includeStderr) { |
| ParcelFileDescriptor[] stderr_read = ParcelFileDescriptor.createPipe(); |
| stderr_source_read = stderr_read[0]; |
| stderr_sink_read = stderr_read[1]; |
| } |
| |
| // Calling out without a lock held. |
| mUiAutomationConnection.executeShellCommandWithStderr( |
| command, sink_read, source_write, stderr_sink_read); |
| } catch (IOException ioe) { |
| Log.e(LOG_TAG, "Error executing shell command!", ioe); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error executing shell command!", re); |
| } finally { |
| IoUtils.closeQuietly(sink_read); |
| IoUtils.closeQuietly(source_write); |
| IoUtils.closeQuietly(stderr_sink_read); |
| } |
| |
| ParcelFileDescriptor[] result = new ParcelFileDescriptor[includeStderr ? 3 : 2]; |
| result[0] = source_read; |
| result[1] = sink_write; |
| if (includeStderr) { |
| result[2] = stderr_source_read; |
| } |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| final StringBuilder stringBuilder = new StringBuilder(); |
| stringBuilder.append("UiAutomation@").append(Integer.toHexString(hashCode())); |
| stringBuilder.append("[id=").append(mConnectionId); |
| stringBuilder.append(", displayId=").append(mDisplayId); |
| stringBuilder.append(", flags=").append(mFlags); |
| stringBuilder.append("]"); |
| return stringBuilder.toString(); |
| } |
| |
| @GuardedBy("mLock") |
| private void throwIfConnectedLocked() { |
| if (mConnectionState == ConnectionState.CONNECTED) { |
| throw new IllegalStateException("UiAutomation connected, " + this); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void throwIfNotConnectedLocked() { |
| if (mConnectionState != ConnectionState.CONNECTED) { |
| final String msg = useAccessibility() |
| ? "UiAutomation not connected, " |
| : "UiAutomation not connected: Accessibility-dependent method called with " |
| + "FLAG_DONT_USE_ACCESSIBILITY set, "; |
| throw new IllegalStateException(msg + this); |
| } |
| } |
| |
| private void warnIfBetterCommand(String cmd) { |
| if (cmd.startsWith("pm grant ")) { |
| Log.w(LOG_TAG, "UiAutomation.grantRuntimePermission() " |
| + "is more robust and should be used instead of 'pm grant'"); |
| } else if (cmd.startsWith("pm revoke ")) { |
| Log.w(LOG_TAG, "UiAutomation.revokeRuntimePermission() " |
| + "is more robust and should be used instead of 'pm revoke'"); |
| } |
| } |
| |
| private boolean useAccessibility() { |
| return (mFlags & UiAutomation.FLAG_DONT_USE_ACCESSIBILITY) == 0; |
| } |
| |
| /** |
| * Gets the display id associated with the UiAutomation context. |
| * |
| * <p><b>NOTE: </b> must be a static method because it's called from a constructor to call |
| * another one. |
| */ |
| private static int getDisplayId(Context context) { |
| Preconditions.checkArgument(context != null, "Context cannot be null!"); |
| |
| UserManager userManager = context.getSystemService(UserManager.class); |
| // TODO(b/255426725): given that this is a temporary solution until a11y supports multiple |
| // users, the display is only set on devices that support that |
| if (!userManager.isVisibleBackgroundUsersSupported()) { |
| return DEFAULT_DISPLAY; |
| } |
| |
| int displayId = context.getDisplayId(); |
| if (displayId == Display.INVALID_DISPLAY) { |
| // Shouldn't happen, but we better handle it |
| Log.e(LOG_TAG, "UiAutomation created UI context with invalid display id, assuming it's" |
| + " running in the display assigned to the user"); |
| return getMainDisplayIdAssignedToUser(context, userManager); |
| } |
| |
| if (displayId != DEFAULT_DISPLAY) { |
| if (DEBUG) { |
| Log.d(LOG_TAG, "getDisplayId(): returning context's display (" + displayId + ")"); |
| } |
| // Context is explicitly setting the display, so we respect that... |
| return displayId; |
| } |
| // ...otherwise, we need to get the display the test's user is running on |
| int userDisplayId = getMainDisplayIdAssignedToUser(context, userManager); |
| if (DEBUG) { |
| Log.d(LOG_TAG, "getDisplayId(): returning user's display (" + userDisplayId + ")"); |
| } |
| return userDisplayId; |
| } |
| |
| private static int getMainDisplayIdAssignedToUser(Context context, UserManager userManager) { |
| if (!userManager.isUserVisible()) { |
| // Should also not happen, but ... |
| Log.e(LOG_TAG, "User (" + context.getUserId() + ") is not visible, using " |
| + "DEFAULT_DISPLAY"); |
| return DEFAULT_DISPLAY; |
| } |
| return userManager.getMainDisplayIdAssignedToUser(); |
| } |
| |
| private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper { |
| |
| public IAccessibilityServiceClientImpl(Looper looper, int generationId) { |
| super(/* context= */ 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 |
| public void init(int connectionId, IBinder windowToken) { |
| if (DEBUG) { |
| Log.d(LOG_TAG, "init(): connectionId=" + connectionId + ", windowToken=" |
| + windowToken + ", user=" + Process.myUserHandle() |
| + ", UiAutomation.mDisplay=" + UiAutomation.this.mDisplayId |
| + ", mGenerationId=" + mGenerationId |
| + ", UiAutomation.mGenerationId=" |
| + UiAutomation.this.mGenerationId); |
| } |
| synchronized (mLock) { |
| if (isGenerationChangedLocked()) { |
| if (DEBUG) { |
| Log.d(LOG_TAG, "init(): returning because generation id changed"); |
| } |
| return; |
| } |
| if (DEBUG) Log.d(LOG_TAG, "setting state to CONNECTED"); |
| mConnectionState = ConnectionState.CONNECTED; |
| mConnectionId = connectionId; |
| mLock.notifyAll(); |
| } |
| if (Build.IS_DEBUGGABLE) { |
| Log.v(LOG_TAG, "Init " + UiAutomation.this); |
| } |
| } |
| |
| @Override |
| public void onServiceConnected() { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onInterrupt() { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onSystemActionsChanged() { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void createImeSession(IAccessibilityInputMethodSessionCallback callback) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void startInput( |
| @Nullable RemoteAccessibilityInputConnection inputConnection, |
| @NonNull EditorInfo editorInfo, boolean restarting) { |
| } |
| |
| @Override |
| public boolean onGesture(AccessibilityGestureEvent gestureEvent) { |
| /* do nothing */ |
| return false; |
| } |
| |
| public void onMotionEvent(MotionEvent event) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onTouchStateChanged(int displayId, int state) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onAccessibilityEvent(AccessibilityEvent event) { |
| if (VERBOSE) { |
| Log.v(LOG_TAG, "onAccessibilityEvent(" + Process.myUserHandle() + "): " |
| + event); |
| } |
| |
| final OnAccessibilityEventListener listener; |
| synchronized (mLock) { |
| if (isGenerationChangedLocked()) { |
| if (VERBOSE) { |
| Log.v(LOG_TAG, "onAccessibilityEvent(): returning because " |
| + "generation id changed (from " |
| + UiAutomation.this.mGenerationId + " to " |
| + mGenerationId + ")"); |
| } |
| return; |
| } |
| // It is not guaranteed that the accessibility framework sends events by the |
| // order of event timestamp. |
| mLastEventTimeMillis = Math.max(mLastEventTimeMillis, event.getEventTime()); |
| if (mWaitingForEventDelivery) { |
| mEventQueue.add(AccessibilityEvent.obtain(event)); |
| } |
| mLock.notifyAll(); |
| listener = mOnAccessibilityEventListener; |
| } |
| if (listener != null) { |
| // Calling out only without a lock held. |
| mLocalCallbackHandler.sendMessage(PooledLambda.obtainMessage( |
| OnAccessibilityEventListener::onAccessibilityEvent, |
| listener, AccessibilityEvent.obtain(event))); |
| } |
| } |
| |
| @Override |
| public boolean onKeyEvent(KeyEvent event) { |
| return false; |
| } |
| |
| @Override |
| public void onMagnificationChanged(int displayId, @NonNull Region region, |
| MagnificationConfig config) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onSoftKeyboardShowModeChanged(int showMode) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onPerformGestureResult(int sequence, boolean completedSuccessfully) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onFingerprintCapturingGesturesChanged(boolean active) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onFingerprintGesture(int gesture) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onAccessibilityButtonClicked(int displayId) { |
| /* do nothing */ |
| } |
| |
| @Override |
| public void onAccessibilityButtonAvailabilityChanged(boolean available) { |
| /* do nothing */ |
| } |
| }); |
| } |
| } |
| } |