Allow PixelCopy for a window from any View
Also make it actually async, and allow the bitmap
to be auto-allocated
Bug: 195673633
Test: PixelCopyTest CTS suite
Change-Id: Ie872f20c809eaaeb8dc32f3ec6347f21a9a7bc1a
diff --git a/graphics/java/android/view/PixelCopy.java b/graphics/java/android/view/PixelCopy.java
index 2797a4d..0f82c8f 100644
--- a/graphics/java/android/view/PixelCopy.java
+++ b/graphics/java/android/view/PixelCopy.java
@@ -20,12 +20,15 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Bitmap;
+import android.graphics.HardwareRenderer;
import android.graphics.Rect;
import android.os.Handler;
import android.view.ViewTreeObserver.OnDrawListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Provides a mechanisms to issue pixel copy requests to allow for copy
@@ -183,12 +186,10 @@
if (srcRect != null && srcRect.isEmpty()) {
throw new IllegalArgumentException("sourceRect is empty");
}
- // TODO: Make this actually async and fast and cool and stuff
- int result = ThreadedRenderer.copySurfaceInto(source, srcRect, dest);
- listenerThread.post(new Runnable() {
+ HardwareRenderer.copySurfaceInto(source, new HardwareRenderer.CopyRequest(srcRect, dest) {
@Override
- public void run() {
- listener.onPixelCopyFinished(result);
+ public void onCopyFinished(int result) {
+ listenerThread.post(() -> listener.onPixelCopyFinished(result));
}
});
}
@@ -255,30 +256,10 @@
@NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener,
@NonNull Handler listenerThread) {
validateBitmapDest(dest);
- if (source == null) {
- throw new IllegalArgumentException("source is null");
- }
- if (source.peekDecorView() == null) {
- throw new IllegalArgumentException(
- "Only able to copy windows with decor views");
- }
- Surface surface = null;
- final ViewRootImpl root = source.peekDecorView().getViewRootImpl();
- if (root != null) {
- surface = root.mSurface;
- final Rect surfaceInsets = root.mWindowAttributes.surfaceInsets;
- if (srcRect == null) {
- srcRect = new Rect(surfaceInsets.left, surfaceInsets.top,
- root.mWidth + surfaceInsets.left, root.mHeight + surfaceInsets.top);
- } else {
- srcRect.offset(surfaceInsets.left, surfaceInsets.top);
- }
- }
- if (surface == null || !surface.isValid()) {
- throw new IllegalArgumentException(
- "Window doesn't have a backing surface!");
- }
- request(surface, srcRect, dest, listener, listenerThread);
+ final Rect insets = new Rect();
+ final Surface surface = sourceForWindow(source, insets);
+ request(surface, adjustSourceRectForInsets(insets, srcRect), dest, listener,
+ listenerThread);
}
private static void validateBitmapDest(Bitmap bitmap) {
@@ -294,5 +275,235 @@
}
}
+ private static Surface sourceForWindow(Window source, Rect outInsets) {
+ if (source == null) {
+ throw new IllegalArgumentException("source is null");
+ }
+ if (source.peekDecorView() == null) {
+ throw new IllegalArgumentException(
+ "Only able to copy windows with decor views");
+ }
+ Surface surface = null;
+ final ViewRootImpl root = source.peekDecorView().getViewRootImpl();
+ if (root != null) {
+ surface = root.mSurface;
+ final Rect surfaceInsets = root.mWindowAttributes.surfaceInsets;
+ outInsets.set(surfaceInsets.left, surfaceInsets.top,
+ root.mWidth + surfaceInsets.left, root.mHeight + surfaceInsets.top);
+ }
+ if (surface == null || !surface.isValid()) {
+ throw new IllegalArgumentException(
+ "Window doesn't have a backing surface!");
+ }
+ return surface;
+ }
+
+ private static Rect adjustSourceRectForInsets(Rect insets, Rect srcRect) {
+ if (srcRect == null) {
+ return insets;
+ }
+ if (insets != null) {
+ srcRect.offset(insets.left, insets.top);
+ }
+ return srcRect;
+ }
+
+ /**
+ * Contains the result of a PixelCopy request
+ */
+ public static final class CopyResult {
+ private int mStatus;
+ private Bitmap mBitmap;
+
+ private CopyResult(@CopyResultStatus int status, Bitmap bitmap) {
+ mStatus = status;
+ mBitmap = bitmap;
+ }
+
+ /**
+ * Returns the {@link CopyResultStatus} of the copy request.
+ */
+ public @CopyResultStatus int getStatus() {
+ return mStatus;
+ }
+
+ private void validateStatus() {
+ if (mStatus != SUCCESS) {
+ throw new IllegalStateException("Copy request didn't succeed, status = " + mStatus);
+ }
+ }
+
+ /**
+ * If the PixelCopy {@link Request} was given a destination bitmap with
+ * {@link Request#setDestinationBitmap(Bitmap)} then the returned bitmap will be the same
+ * as the one given. If no destination bitmap was provided, then this
+ * will contain the automatically allocated Bitmap to hold the result.
+ *
+ * @return the Bitmap the copy request was stored in.
+ * @throws IllegalStateException if {@link #getStatus()} is not SUCCESS
+ */
+ public @NonNull Bitmap getBitmap() {
+ validateStatus();
+ return mBitmap;
+ }
+ }
+
+ /**
+ * A builder to create the complete PixelCopy request, which is then executed by calling
+ * {@link #request()}
+ */
+ public static final class Request {
+ private Request(Surface source, Rect sourceInsets, Executor listenerThread,
+ Consumer<CopyResult> listener) {
+ this.mSource = source;
+ this.mSourceInsets = sourceInsets;
+ this.mListenerThread = listenerThread;
+ this.mListener = listener;
+ }
+
+ private final Surface mSource;
+ private final Consumer<CopyResult> mListener;
+ private final Executor mListenerThread;
+ private final Rect mSourceInsets;
+ private Rect mSrcRect;
+ private Bitmap mDest;
+
+ /**
+ * Sets the region of the source to copy from. By default, the entire source is copied to
+ * the output. If only a subset of the source is necessary to be copied, specifying a
+ * srcRect will improve performance by reducing
+ * the amount of data being copied.
+ *
+ * @param srcRect The area of the source to read from. Null or empty will be treated to
+ * mean the entire source
+ * @return this
+ */
+ public @NonNull Request setSourceRect(@Nullable Rect srcRect) {
+ this.mSrcRect = srcRect;
+ return this;
+ }
+
+ /**
+ * Specifies the output bitmap in which to store the result. By default, a Bitmap of format
+ * {@link android.graphics.Bitmap.Config#ARGB_8888} with a width & height matching that
+ * of the {@link #setSourceRect(Rect) source area} will be created to place the result.
+ *
+ * @param destination The bitmap to store the result, or null to have a bitmap
+ * automatically created of the appropriate size. If not null, must not
+ * be {@link Bitmap#isRecycled() recycled} and must be
+ * {@link Bitmap#isMutable() mutable}.
+ * @return this
+ */
+ public @NonNull Request setDestinationBitmap(@Nullable Bitmap destination) {
+ if (destination != null) {
+ validateBitmapDest(destination);
+ }
+ this.mDest = destination;
+ return this;
+ }
+
+ /**
+ * Executes the request.
+ */
+ public void request() {
+ if (!mSource.isValid()) {
+ mListenerThread.execute(() -> mListener.accept(
+ new CopyResult(ERROR_SOURCE_INVALID, null)));
+ return;
+ }
+ HardwareRenderer.copySurfaceInto(mSource, new HardwareRenderer.CopyRequest(
+ adjustSourceRectForInsets(mSourceInsets, mSrcRect), mDest) {
+ @Override
+ public void onCopyFinished(int result) {
+ mListenerThread.execute(() -> mListener.accept(
+ new CopyResult(result, mDestinationBitmap)));
+ }
+ });
+ }
+ }
+
+ /**
+ * Creates a PixelCopy request for the given {@link Window}
+ * @param source The Window to copy from
+ * @param callbackExecutor The executor to run the callback on
+ * @param listener The callback for when the copy request is completed
+ * @return A {@link Request} builder to set the optional params & execute the request
+ */
+ public static @NonNull Request ofWindow(@NonNull Window source,
+ @NonNull Executor callbackExecutor,
+ @NonNull Consumer<CopyResult> listener) {
+ final Rect insets = new Rect();
+ final Surface surface = sourceForWindow(source, insets);
+ return new Request(surface, insets, callbackExecutor, listener);
+ }
+
+ /**
+ * Creates a PixelCopy request for the {@link Window} that the given {@link View} is
+ * attached to.
+ *
+ * Note that this copy request is not cropped to the area the View occupies by default. If that
+ * behavior is desired, use {@link View#getLocationInWindow(int[])} combined with
+ * {@link Request#setSourceRect(Rect)} to set a crop area to restrict the copy operation.
+ *
+ * @param source A View that {@link View#isAttachedToWindow() is attached} to a window that
+ * will be used to retrieve the window to copy from.
+ * @param callbackExecutor The executor to run the callback on
+ * @param listener The callback for when the copy request is completed
+ * @return A {@link Request} builder to set the optional params & execute the request
+ */
+ public static @NonNull Request ofWindow(@NonNull View source,
+ @NonNull Executor callbackExecutor,
+ @NonNull Consumer<CopyResult> listener) {
+ if (source == null || !source.isAttachedToWindow()) {
+ throw new IllegalArgumentException(
+ "View must not be null & must be attached to window");
+ }
+ final Rect insets = new Rect();
+ Surface surface = null;
+ final ViewRootImpl root = source.getViewRootImpl();
+ if (root != null) {
+ surface = root.mSurface;
+ insets.set(root.mWindowAttributes.surfaceInsets);
+ }
+ if (surface == null || !surface.isValid()) {
+ throw new IllegalArgumentException(
+ "Window doesn't have a backing surface!");
+ }
+ return new Request(surface, insets, callbackExecutor, listener);
+ }
+
+ /**
+ * Creates a PixelCopy request for the given {@link Surface}
+ *
+ * @param source The Surface to copy from. Must be {@link Surface#isValid() valid}.
+ * @param callbackExecutor The executor to run the callback on
+ * @param listener The callback for when the copy request is completed
+ * @return A {@link Request} builder to set the optional params & execute the request
+ */
+ public static @NonNull Request ofSurface(@NonNull Surface source,
+ @NonNull Executor callbackExecutor,
+ @NonNull Consumer<CopyResult> listener) {
+ if (source == null || !source.isValid()) {
+ throw new IllegalArgumentException("Source must not be null & must be valid");
+ }
+ return new Request(source, null, callbackExecutor, listener);
+ }
+
+ /**
+ * Creates a PixelCopy request for the {@link Surface} belonging to the
+ * given {@link SurfaceView}
+ *
+ * @param source The SurfaceView to copy from. The backing surface must be
+ * {@link Surface#isValid() valid}
+ * @param callbackExecutor The executor to run the callback on
+ * @param listener The callback for when the copy request is completed
+ * @return A {@link Request} builder to set the optional params & execute the request
+ */
+ public static @NonNull Request ofSurface(@NonNull SurfaceView source,
+ @NonNull Executor callbackExecutor,
+ @NonNull Consumer<CopyResult> listener) {
+ return ofSurface(source.getHolder().getSurface(), callbackExecutor, listener);
+ }
+
private PixelCopy() {}
}