/* * 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. *

* 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)}. *

*

* 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. *

*/ 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 ALL_PERMISSIONS = Set.of("_ALL_PERMISSIONS_"); private final Object mLock = new Object(); private final ArrayList mEventQueue = new ArrayList(); 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}. *

* Note: This method is NOT executed * on the main test thread. The client is responsible for proper * synchronization. *

*

* Note: It is responsibility of the client * to recycle the received events to minimize object creation. *

* * @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. *

* Note: 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. *

* Note: 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 null 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 getAdoptedShellPermissions() { try { final List 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. *

* Note: 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. *

* * @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). * *

* 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. *

* Note: 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 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. *

* Note: 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 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> 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. * *

* Note: It is caller's responsibility to recycle the event. *

* * @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. *

* Note: It is caller's responsibility to recycle the event. *

* * @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 0 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. *

* Note: It is caller's responsibility to recycle the returned event. *

* * @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 receivedEvents = new ArrayList<>(); // Acquire the lock and wait for the event. try { // Wait for the event. final long startTimeMillis = SystemClock.uptimeMillis(); while (true) { List 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 idleTimeoutMillis. * The total time spent to wait for an idle accessibility event stream is bounded * by the globalTimeoutMillis. * * @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 * globalTimeoutMillis. * @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. *

* 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. *

*
     * // 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);
     * 
* * @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 * FrameTimeline * 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. * *

* 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. *

*
     * // Start with a clean slate.
     * uiAutimation.clearWindowAnimationFrameStats();
     *
     * // Do stuff to trigger a window transition.
     *
     * // Get the frame statistics.
     * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats();
     * 
* * @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 " from a host connected to the device. *

* Note: It is your responsibility to close the returned file * descriptor once you are done reading. *

* * @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 " from a host connected to the device. *

* Note: It is your responsibility to close the returned file * descriptors once you are done reading/writing. *

* * @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 " from a host connected to the device. *

* Note: It is your responsibility to close the returned file * descriptors once you are done reading/writing. *

* * @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. * *

NOTE: 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 */ } }); } } }