From 32afd293461e37c65de98e152a45a55e009cf8e6 Mon Sep 17 00:00:00 2001 From: Daniel Norman Date: Wed, 24 Aug 2022 12:00:14 -0700 Subject: [PATCH] Introduce A11yService#takeScreenshotOfWindow() This enables accessibility services to take a screenshot of a window without any overlay windows drawn on top, which is beneficial for ML-based understanding of the visual content in the window. Bug: 249830826 Test: atest AccessibilityTakeScreenshotTest Test: use in an a11y service, observe screenshots contain only content from the requested windows and no overlays Change-Id: Ic886aeace9cb2984a472273858281b4f2c3a0e46 --- core/api/current.txt | 3 + .../AccessibilityService.java | 53 +++++++- .../IAccessibilityServiceConnection.aidl | 5 + .../AccessibilityInteractionController.java | 40 +++++++ core/java/android/view/ViewRootImpl.java | 21 ++++ .../AccessibilityInteractionClient.java | 113 ++++++++++++++++++ .../IAccessibilityInteractionConnection.aidl | 5 + ...sibilityInteractionConnectionCallback.aidl | 5 + .../AccessibilityServiceConnectionImpl.java | 5 + ...PipAccessibilityInteractionConnection.java | 10 ++ ...bstractAccessibilityServiceConnection.java | 52 ++++++++ .../ActionReplacingCallback.java | 6 + 12 files changed, 315 insertions(+), 3 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index 80bae5bd071dd..7cd7725758fe0 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -3112,10 +3112,13 @@ package android.accessibilityservice { method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region); method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); + method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); field public static final int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 1; // 0x1 field public static final int ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT = 3; // 0x3 field public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4; // 0x4 + field public static final int ERROR_TAKE_SCREENSHOT_INVALID_WINDOW = 5; // 0x5 field public static final int ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS = 2; // 0x2 + field public static final int ERROR_TAKE_SCREENSHOT_SECURE_WINDOW = 6; // 0x6 field public static final int GESTURE_2_FINGER_DOUBLE_TAP = 20; // 0x14 field public static final int GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD = 40; // 0x28 field public static final int GESTURE_2_FINGER_SINGLE_TAP = 19; // 0x13 diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index e86d2f35ef16c..2fe5d5140371a 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -696,7 +696,8 @@ public abstract class AccessibilityService extends Service { ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS, ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT, - ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY + ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, + ERROR_TAKE_SCREENSHOT_INVALID_WINDOW }) public @interface ScreenshotErrorCode {} @@ -727,6 +728,18 @@ public abstract class AccessibilityService extends Service { */ public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4; + /** + * The status of taking screenshot is failure and the reason is invalid accessibility window Id. + */ + public static final int ERROR_TAKE_SCREENSHOT_INVALID_WINDOW = 5; + + /** + * The status of taking screenshot is failure and the reason is the window contains secure + * content. + * @see WindowManager.LayoutParams#FLAG_SECURE + */ + public static final int ERROR_TAKE_SCREENSHOT_SECURE_WINDOW = 6; + /** * The interval time of calling * {@link AccessibilityService#takeScreenshot(int, Executor, Consumer)} API. @@ -2568,6 +2581,7 @@ public abstract class AccessibilityService extends Service { * @param executor Executor on which to run the callback. * @param callback The callback invoked when taking screenshot has succeeded or failed. * See {@link TakeScreenshotCallback} for details. + * @see #takeScreenshotOfWindow */ public void takeScreenshot(int displayId, @NonNull @CallbackExecutor Executor executor, @NonNull TakeScreenshotCallback callback) { @@ -2589,7 +2603,8 @@ public abstract class AccessibilityService extends Service { final HardwareBuffer hardwareBuffer = result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER, android.hardware.HardwareBuffer.class); final ParcelableColorSpace colorSpace = - result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE, android.graphics.ParcelableColorSpace.class); + result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE, + android.graphics.ParcelableColorSpace.class); final ScreenshotResult screenshot = new ScreenshotResult(hardwareBuffer, colorSpace.getColorSpace(), result.getLong(KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP)); @@ -2600,6 +2615,37 @@ public abstract class AccessibilityService extends Service { } } + /** + * Takes a screenshot of the specified window and returns it via an + * {@link AccessibilityService.ScreenshotResult}. You can use {@link Bitmap#wrapHardwareBuffer} + * to construct the bitmap from the ScreenshotResult's payload. + *

+ * Note: In order to take screenshots your service has + * to declare the capability to take screenshot by setting the + * {@link android.R.styleable#AccessibilityService_canTakeScreenshot} + * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}. + *

+ *

+ * Both this method and {@link #takeScreenshot} can be used for machine learning-based visual + * screen understanding. Use takeScreenshotOfWindow if your target window might be + * visually underneath an accessibility overlay (from your or another accessibility service) in + * order to capture the window contents without the screenshot being covered by the overlay + * contents drawn on the screen. + *

+ * + * @param accessibilityWindowId The window id, from {@link AccessibilityWindowInfo#getId()}. + * @param executor Executor on which to run the callback. + * @param callback The callback invoked when taking screenshot has succeeded or failed. + * See {@link TakeScreenshotCallback} for details. + * @see #takeScreenshot + */ + public void takeScreenshotOfWindow(int accessibilityWindowId, + @NonNull @CallbackExecutor Executor executor, + @NonNull TakeScreenshotCallback callback) { + AccessibilityInteractionClient.getInstance(this).takeScreenshotOfWindow( + mConnectionId, accessibilityWindowId, executor, callback); + } + /** * Sets the strokeWidth and color of the accessibility focus rectangle. *

@@ -3113,7 +3159,8 @@ public abstract class AccessibilityService extends Service { private final @NonNull ColorSpace mColorSpace; private final long mTimestamp; - private ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer, + /** @hide */ + public ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer, @NonNull ColorSpace colorSpace, long timestamp) { Preconditions.checkNotNull(hardwareBuffer, "hardwareBuffer cannot be null"); Preconditions.checkNotNull(colorSpace, "colorSpace cannot be null"); diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 9abce3a399441..da14b50e64817 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -30,6 +30,7 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.accessibility.AccessibilityWindowInfo; import java.util.List; +import android.window.ScreenCapture; /** * Interface given to an AccessibilitySerivce to talk to the AccessibilityManagerService. @@ -122,6 +123,10 @@ interface IAccessibilityServiceConnection { void takeScreenshot(int displayId, in RemoteCallback callback); + void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId, + in ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback); + void setGestureDetectionPassthroughRegion(int displayId, in Region region); void setTouchExplorationPassthroughRegion(int displayId, in Region region); diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 510bde1deb5d8..44168ca096cfa 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -21,6 +21,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_A import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_REQUESTED_KEY; import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; +import android.accessibilityservice.AccessibilityService; import android.annotation.NonNull; import android.graphics.Matrix; import android.graphics.Rect; @@ -46,11 +47,13 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityRequestPreparer; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; +import com.android.internal.util.function.pooled.PooledLambda; import java.util.ArrayDeque; import java.util.ArrayList; @@ -588,6 +591,43 @@ public final class AccessibilityInteractionController { } } + /** + * Take a screenshot using {@link ScreenCapture} of this {@link ViewRootImpl}'s {@link + * SurfaceControl}. + */ + public void takeScreenshotOfWindowClientThread(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) { + Message message = PooledLambda.obtainMessage( + AccessibilityInteractionController::takeScreenshotOfWindowUiThread, + this, interactionId, listener, callback); + + // Screenshot results are returned to the service asynchronously, so the same-thread + // message wait logic from #scheduleMessage() is not needed. + mHandler.sendMessage(message); + } + + private void takeScreenshotOfWindowUiThread(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) { + try { + if ((mViewRootImpl.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_SECURE_WINDOW, interactionId); + return; + } + final ScreenCapture.LayerCaptureArgs captureArgs = + new ScreenCapture.LayerCaptureArgs.Builder(mViewRootImpl.getSurfaceControl()) + .setChildrenOnly(false).setUid(Process.myUid()).build(); + if (ScreenCapture.captureLayers(captureArgs, listener) != 0) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId); + } + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + public void findFocusClientThread(long accessibilityNodeId, int focusType, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e664ebf4c4849..fdc6412602a62 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -88,6 +88,7 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER; import android.Manifest; +import android.accessibilityservice.AccessibilityService; import android.animation.AnimationHandler; import android.animation.LayoutTransition; import android.annotation.AnyThread; @@ -198,6 +199,7 @@ import android.window.ClientWindowFrames; import android.window.CompatOnBackInvokedCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; +import android.window.ScreenCapture; import android.window.SurfaceSyncGroup; import android.window.WindowOnBackInvokedDispatcher; @@ -10691,6 +10693,25 @@ public final class ViewRootImpl implements ViewParent, .notifyOutsideTouchClientThread(); } } + + @Override + public void takeScreenshotOfWindow(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .takeScreenshotOfWindowClientThread(interactionId, listener, callback); + } else { + try { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, + interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + } } /** diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index e3ffc9dcbbdec..7030ab54b7905 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -22,7 +22,9 @@ import static android.os.Build.VERSION_CODES.S; import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_MASK; import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_MASK; +import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.IAccessibilityServiceConnection; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -31,17 +33,21 @@ import android.content.Context; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import android.util.LongSparseArray; +import android.util.Pair; import android.util.SparseArray; import android.util.SparseLongArray; import android.view.Display; import android.view.ViewConfiguration; +import android.window.ScreenCapture; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -53,6 +59,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Queue; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** @@ -146,6 +153,10 @@ public final class AccessibilityInteractionClient private boolean mPerformAccessibilityActionResult; + // SparseArray of interaction ID -> screenshot executor+callback. + private final SparseArray> + mTakeScreenshotOfWindowCallbacks = new SparseArray<>(); + private Message mSameThreadMessage; private int mInteractionIdWaitingForPrefetchResult = -1; @@ -778,6 +789,59 @@ public final class AccessibilityInteractionClient return Collections.emptyList(); } + /** + * Takes a screenshot of the window with the provided {@code accessibilityWindowId} and + * returns the answer asynchronously. This async behavior is similar to {@link + * AccessibilityService#takeScreenshot} but unlike other methods in this class which perform + * synchronous waiting in the AccessibilityService client. + * + * @see AccessibilityService#takeScreenshotOfWindow + */ + public void takeScreenshotOfWindow(int connectionId, int accessibilityWindowId, + @NonNull @CallbackExecutor Executor executor, + @NonNull AccessibilityService.TakeScreenshotCallback callback) { + synchronized (mInstanceLock) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection == null) { + executor.execute(() -> callback.onFailure( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR)); + return; + } + final long identityToken = Binder.clearCallingIdentity(); + try { + final int interactionId = mInteractionIdCounter.getAndIncrement(); + mTakeScreenshotOfWindowCallbacks.put(interactionId, + Pair.create(executor, callback)); + // Create a ScreenCaptureListener to receive the screenshot directly from + // SurfaceFlinger instead of requiring an extra IPC from the app: + // A11yService -> App -> SurfaceFlinger -> A11yService + ScreenCapture.ScreenCaptureListener listener = + new ScreenCapture.ScreenCaptureListener( + screenshot -> sendWindowScreenshotSuccess(screenshot, + interactionId)); + connection.takeScreenshotOfWindow(accessibilityWindowId, interactionId, + listener, this); + new Handler(Looper.getMainLooper()).postDelayed(() -> { + synchronized (mInstanceLock) { + // Notify failure if we still haven't sent a response after timeout. + if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) { + sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, + interactionId); + } + } + }, TIMEOUT_INTERACTION_MILLIS); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + } catch (RemoteException re) { + executor.execute(() -> callback.onFailure( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR)); + } + } + } + /** * Finds {@link AccessibilityNodeInfo}s by View text. The match is case * insensitive containment. The search is performed in the window whose @@ -1253,6 +1317,55 @@ public final class AccessibilityInteractionClient } } + /** + * Sends the result of a window screenshot request to the requesting client. + * + * {@link #takeScreenshotOfWindow} does not perform synchronous waiting, so this method + * does not notify any wait lock. + */ + private void sendWindowScreenshotSuccess(ScreenCapture.ScreenshotHardwareBuffer screenshot, + int interactionId) { + if (screenshot == null) { + sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId); + return; + } + synchronized (mInstanceLock) { + if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) { + final AccessibilityService.ScreenshotResult result = + new AccessibilityService.ScreenshotResult(screenshot.getHardwareBuffer(), + screenshot.getColorSpace(), SystemClock.uptimeMillis()); + final Pair pair = + mTakeScreenshotOfWindowCallbacks.get(interactionId); + final Executor executor = pair.first; + final AccessibilityService.TakeScreenshotCallback callback = pair.second; + executor.execute(() -> callback.onSuccess(result)); + mTakeScreenshotOfWindowCallbacks.remove(interactionId); + } + } + } + + /** + * Sends an error code for a window screenshot request to the requesting client. + * + * @param errorCode The error code from {@link AccessibilityService.ScreenshotErrorCode}. + * @param interactionId The interaction id of the request. + */ + @Override + public void sendTakeScreenshotOfWindowError( + @AccessibilityService.ScreenshotErrorCode int errorCode, int interactionId) { + synchronized (mInstanceLock) { + if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) { + final Pair pair = + mTakeScreenshotOfWindowCallbacks.get(interactionId); + final Executor executor = pair.first; + final AccessibilityService.TakeScreenshotCallback callback = pair.second; + executor.execute(() -> callback.onFailure(errorCode)); + mTakeScreenshotOfWindowCallbacks.remove(interactionId); + } + } + } + /** * Clears the result state. */ diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index 472a36361585d..fb01921dbaa16 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -22,6 +22,7 @@ import android.os.Bundle; import android.view.MagnificationSpec; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; /** * Interface for interaction between the AccessibilityManagerService @@ -60,4 +61,8 @@ oneway interface IAccessibilityInteractionConnection { void clearAccessibilityFocus(); void notifyOutsideTouch(); + + void takeScreenshotOfWindow(int interactionId, + in ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback); } diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl index 231e75a19a068..456bf5893ea02 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl @@ -63,4 +63,9 @@ oneway interface IAccessibilityInteractionConnectionCallback { */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) void setPerformAccessibilityActionResult(boolean succeeded, int interactionId); + + /** + * Sends an error code for a window screenshot request to the requesting client. + */ + void sendTakeScreenshotOfWindowError(int errorCode, int interactionId); } diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java index 35d5948bc2af6..7a5ab0458406a 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; +import android.window.ScreenCapture; import java.util.Collections; import java.util.List; @@ -180,6 +181,10 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon public void takeScreenshot(int displayId, RemoteCallback callback) {} + public void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) {} + public void setFocusAppearance(int strokeWidth, int color) {} public void setCacheEnabled(boolean enabled) {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java index 7365b95259194..1f7a7fc9ed4c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -29,6 +29,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; import androidx.annotation.BinderThread; @@ -361,6 +362,15 @@ public class PipAccessibilityInteractionConnection { }); } + @Override + public void takeScreenshotOfWindow(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) throws RemoteException { + // AbstractAccessibilityServiceConnection uses the standard + // IAccessibilityInteractionConnection for takeScreenshotOfWindow for Pip windows, + // so do nothing here. + } + @Override public void clearAccessibilityFocus() throws RemoteException { // Do nothing diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index f35de17088d16..c77b59717678c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -16,6 +16,7 @@ package com.android.server.accessibility; +import static android.accessibilityservice.AccessibilityService.ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS; import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE; import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER; import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_STATUS; @@ -83,6 +84,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.inputmethod.EditorInfo; +import android.window.ScreenCapture; import android.window.ScreenCapture.ScreenshotHardwareBuffer; import com.android.internal.annotations.GuardedBy; @@ -211,6 +213,11 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ /** The timestamp of requesting to take screenshot in milliseconds */ private long mRequestTakeScreenshotTimestampMs; + /** + * The timestamps of requesting to take a window screenshot in milliseconds, + * mapping from accessibility window id -> timestamp. + */ + private SparseArray mRequestTakeScreenshotOfWindowTimestampMs = new SparseArray<>(); public interface SystemSupport { /** @@ -1251,6 +1258,51 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ mInvocationHandler.setSoftKeyboardCallbackEnabled(enabled); } + @Override + public void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) throws RemoteException { + final long currentTimestamp = SystemClock.uptimeMillis(); + if ((currentTimestamp + - mRequestTakeScreenshotOfWindowTimestampMs.get(accessibilityWindowId, 0L)) + <= ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT, interactionId); + return; + } + mRequestTakeScreenshotOfWindowTimestampMs.put(accessibilityWindowId, currentTimestamp); + + synchronized (mLock) { + if (!hasRightsToCurrentUserLocked()) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId); + return; + } + if (!mSecurityPolicy.canTakeScreenshotLocked(this)) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS, + interactionId); + return; + } + } + if (!mSecurityPolicy.checkAccessibilityAccess(this)) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS, + interactionId); + return; + } + + RemoteAccessibilityConnection connection = mA11yWindowManager.getConnectionLocked( + mSystemSupport.getCurrentUserIdLocked(), + resolveAccessibilityWindowIdLocked(accessibilityWindowId)); + if (connection == null) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_WINDOW, interactionId); + return; + } + connection.getRemote().takeScreenshotOfWindow(interactionId, listener, callback); + } + @Override public void takeScreenshot(int displayId, RemoteCallback callback) { if (svcConnTracingEnabled()) { diff --git a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java index 6958b667da372..c08b6ab56f557 100644 --- a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java +++ b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java @@ -167,6 +167,12 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId); } + @Override + public void sendTakeScreenshotOfWindowError(int errorCode, int interactionId) + throws RemoteException { + mServiceCallback.sendTakeScreenshotOfWindowError(errorCode, interactionId); + } + private void replaceInfoActionsAndCallService() { final AccessibilityNodeInfo nodeToReturn; boolean doCallback = false;