/* * 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* 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
* 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.
*
* 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
* 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.
*
* Note: It is caller's responsibility to recycle the event.
*
* Note: It is caller's responsibility to recycle the event.
*
* Note: It is caller's responsibility to recycle the returned event.
*
* 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.
*
* 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.
*
* Note: It is your responsibility to close the returned file
* descriptor once you are done reading.
*
* Note: It is your responsibility to close the returned file
* descriptors once you are done reading/writing.
*
* Note: It is your responsibility to close the returned file
* descriptors once you are done reading/writing.
* 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 */
}
});
}
}
}
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> 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.
*
*
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.
* 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.
*
* // 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.
*
*
* // 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