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() {}
 }