Add continuous SKP capture test api

Bug: 122856066
Test: PictureCaptureDemo
Change-Id: Iaf3a4bc1c8a2c18c7dff635c5f1cf726b331f8bf
diff --git a/api/current.txt b/api/current.txt
index 28dd9ec..8f165d8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -14618,8 +14618,8 @@
   public class Picture {
     ctor public Picture();
     ctor public Picture(android.graphics.Picture);
-    method public android.graphics.Canvas beginRecording(int, int);
-    method public void draw(android.graphics.Canvas);
+    method @NonNull public android.graphics.Canvas beginRecording(int, int);
+    method public void draw(@NonNull android.graphics.Canvas);
     method public void endRecording();
     method public int getHeight();
     method public int getWidth();
@@ -14871,7 +14871,7 @@
   }
 
   public final class RenderNode {
-    ctor public RenderNode(String);
+    ctor public RenderNode(@Nullable String);
     method public int computeApproximateMemoryUsage();
     method public void discardDisplayList();
     method public void endRecording();
diff --git a/api/removed.txt b/api/removed.txt
index e232227..9f4b041 100644
--- a/api/removed.txt
+++ b/api/removed.txt
@@ -239,8 +239,8 @@
   }
 
   public class Picture {
-    method @Deprecated public static android.graphics.Picture createFromStream(java.io.InputStream);
-    method @Deprecated public void writeToStream(java.io.OutputStream);
+    method @Deprecated public static android.graphics.Picture createFromStream(@NonNull java.io.InputStream);
+    method @Deprecated public void writeToStream(@NonNull java.io.OutputStream);
   }
 
   @Deprecated public class PixelXorXfermode extends android.graphics.Xfermode {
diff --git a/api/test-current.txt b/api/test-current.txt
index d089831..ba77840 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -2268,6 +2268,10 @@
     method public static int getLongPressTooltipHideTimeout();
   }
 
+  public class ViewDebug {
+    method @Nullable public static AutoCloseable startRenderingCommandsCapture(android.view.View, java.util.concurrent.Executor, java.util.function.Function<android.graphics.Picture,java.lang.Boolean>);
+  }
+
   public interface WindowManager extends android.view.ViewManager {
     method public default void setShouldShowIme(int, boolean);
     method public default void setShouldShowSystemDecors(int, boolean);
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 34d076f..47b206ca 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.HardwareRenderer;
+import android.graphics.Picture;
 import android.graphics.Point;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
@@ -553,6 +554,10 @@
         dumpProfileInfo(fd, flags);
     }
 
+    Picture captureRenderingCommands() {
+        return null;
+    }
+
     @Override
     public boolean loadSystemProperties() {
         boolean changed = super.loadSystemProperties();
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
index 292e933..5afc07f 100644
--- a/core/java/android/view/ViewDebug.java
+++ b/core/java/android/view/ViewDebug.java
@@ -17,17 +17,21 @@
 package android.view;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.HardwareRenderer;
 import android.graphics.Picture;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
 import android.graphics.RenderNode;
 import android.os.Debug;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -48,16 +52,20 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
 
 /**
  * Various debugging/tracing tools related to {@link View} and the view hierarchy.
@@ -741,6 +749,123 @@
         root.getViewRootImpl().outputDisplayList(target);
     }
 
+    private static class PictureCallbackHandler implements AutoCloseable,
+            HardwareRenderer.PictureCapturedCallback, Runnable {
+        private final HardwareRenderer mRenderer;
+        private final Function<Picture, Boolean> mCallback;
+        private final Executor mExecutor;
+        private final ReentrantLock mLock = new ReentrantLock(false);
+        private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
+        private boolean mStopListening;
+        private Thread mRenderThread;
+
+        private PictureCallbackHandler(HardwareRenderer renderer,
+                Function<Picture, Boolean> callback, Executor executor) {
+            mRenderer = renderer;
+            mCallback = callback;
+            mExecutor = executor;
+            mRenderer.setPictureCaptureCallback(this);
+        }
+
+        @Override
+        public void close() {
+            mLock.lock();
+            mStopListening = true;
+            mLock.unlock();
+            mRenderer.setPictureCaptureCallback(null);
+        }
+
+        @Override
+        public void onPictureCaptured(Picture picture) {
+            mLock.lock();
+            if (mStopListening) {
+                mLock.unlock();
+                mRenderer.setPictureCaptureCallback(null);
+                return;
+            }
+            if (mRenderThread == null) {
+                mRenderThread = Thread.currentThread();
+            }
+            Picture toDestroy = null;
+            if (mQueue.size() == 3) {
+                toDestroy = mQueue.removeLast();
+            }
+            mQueue.add(picture);
+            mLock.unlock();
+            if (toDestroy == null) {
+                mExecutor.execute(this);
+            } else {
+                toDestroy.close();
+            }
+        }
+
+        @Override
+        public void run() {
+            mLock.lock();
+            final Picture picture = mQueue.poll();
+            final boolean isStopped = mStopListening;
+            mLock.unlock();
+            if (Thread.currentThread() == mRenderThread) {
+                close();
+                throw new IllegalStateException(
+                        "ViewDebug#startRenderingCommandsCapture must be given an executor that "
+                        + "invokes asynchronously");
+            }
+            if (isStopped) {
+                picture.close();
+                return;
+            }
+            final boolean keepReceiving = mCallback.apply(picture);
+            if (!keepReceiving) {
+                close();
+            }
+        }
+    }
+
+    /**
+     * Begins capturing the entire rendering commands for the view tree referenced by the given
+     * view. The view passed may be any View in the tree as long as it is attached. That is,
+     * {@link View#isAttachedToWindow()} must be true.
+     *
+     * Every time a frame is rendered a Picture will be passed to the given callback via the given
+     * executor. As long as the callback returns 'true' it will continue to receive new frames.
+     * The system will only invoke the callback at a rate that the callback is able to keep up with.
+     * That is, if it takes 48ms for the callback to complete and there is a 60fps animation running
+     * then the callback will only receive 33% of the frames produced.
+     *
+     * This method must be called on the same thread as the View tree.
+     *
+     * @param tree The View tree to capture the rendering commands.
+     * @param callback The callback to invoke on every frame produced. Should return true to
+     *                 continue receiving new frames, false to stop capturing.
+     * @param executor The executor to invoke the callback on. Recommend using a background thread
+     *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
+     *                 exception will be thrown.
+     * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
+     * that the callback may continue to receive another frame or two depending on thread timings.
+     * Returns null if the capture stream cannot be started, such as if there's no
+     * HardwareRenderer for the given view tree.
+     * @hide
+     */
+    @TestApi
+    @Nullable
+    public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
+            Function<Picture, Boolean> callback) {
+        final View.AttachInfo attachInfo = tree.mAttachInfo;
+        if (attachInfo == null) {
+            throw new IllegalArgumentException("Given view isn't attached");
+        }
+        if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
+            throw new IllegalStateException("Called on the wrong thread."
+                    + " Must be called on the thread that owns the given View");
+        }
+        final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
+        if (renderer != null) {
+            return new PictureCallbackHandler(renderer, callback, executor);
+        }
+        return null;
+    }
+
     private static void capture(View root, final OutputStream clientStream, String parameter)
             throws IOException {
 
diff --git a/core/jni/android/graphics/Picture.cpp b/core/jni/android/graphics/Picture.cpp
index fd1d87f..d29857d 100644
--- a/core/jni/android/graphics/Picture.cpp
+++ b/core/jni/android/graphics/Picture.cpp
@@ -37,6 +37,12 @@
     }
 }
 
+Picture::Picture(sk_sp<SkPicture>&& src) {
+    mPicture = std::move(src);
+    mWidth = 0;
+    mHeight = 0;
+}
+
 Canvas* Picture::beginRecording(int width, int height) {
     mPicture.reset(NULL);
     mRecorder.reset(new SkPictureRecorder);
diff --git a/core/jni/android/graphics/Picture.h b/core/jni/android/graphics/Picture.h
index 3068631..536f651 100644
--- a/core/jni/android/graphics/Picture.h
+++ b/core/jni/android/graphics/Picture.h
@@ -37,6 +37,7 @@
 class Picture {
 public:
     explicit Picture(const Picture* src = NULL);
+    explicit Picture(sk_sp<SkPicture>&& src);
 
     Canvas* beginRecording(int width, int height);
 
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 5a8ab3c..318ec9b 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -48,6 +48,7 @@
 #include <FrameInfo.h>
 #include <FrameMetricsObserver.h>
 #include <IContextFactory.h>
+#include <Picture.h>
 #include <Properties.h>
 #include <PropertyValuesAnimatorSet.h>
 #include <RenderNode.h>
@@ -71,6 +72,11 @@
 } gFrameMetricsObserverClassInfo;
 
 struct {
+    jclass clazz;
+    jmethodID invokePictureCapturedCallback;
+} gHardwareRenderer;
+
+struct {
     jmethodID onFrameDraw;
 } gFrameDrawingCallback;
 
@@ -905,6 +911,27 @@
     jobject mObject;
 };
 
+static void android_view_ThreadedRenderer_setPictureCapturedCallbackJNI(JNIEnv* env,
+        jobject clazz, jlong proxyPtr, jobject pictureCallback) {
+    RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
+    if (!pictureCallback) {
+        proxy->setPictureCapturedCallback(nullptr);
+    } else {
+        JavaVM* vm = nullptr;
+        LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM");
+        auto globalCallbackRef = std::make_shared<JGlobalRefHolder>(vm,
+                env->NewGlobalRef(pictureCallback));
+        proxy->setPictureCapturedCallback([globalCallbackRef](sk_sp<SkPicture>&& picture) {
+            JNIEnv* env = getenv(globalCallbackRef->vm());
+            Picture* wrapper = new Picture{std::move(picture)};
+            env->CallStaticVoidMethod(gHardwareRenderer.clazz,
+                    gHardwareRenderer.invokePictureCapturedCallback,
+                    static_cast<jlong>(reinterpret_cast<intptr_t>(wrapper)),
+                    globalCallbackRef->object());
+        });
+    }
+}
+
 static void android_view_ThreadedRenderer_setFrameCallback(JNIEnv* env,
         jobject clazz, jlong proxyPtr, jobject frameCallback) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
@@ -1145,6 +1172,8 @@
     { "nRemoveRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_removeRenderNode},
     { "nDrawRenderNode", "(JJ)V", (void*) android_view_ThreadedRendererd_drawRenderNode},
     { "nSetContentDrawBounds", "(JIIII)V", (void*)android_view_ThreadedRenderer_setContentDrawBounds},
+    { "nSetPictureCaptureCallback", "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V",
+            (void*) android_view_ThreadedRenderer_setPictureCapturedCallbackJNI },
     { "nSetFrameCallback", "(JLandroid/graphics/HardwareRenderer$FrameDrawingCallback;)V",
             (void*)android_view_ThreadedRenderer_setFrameCallback},
     { "nSetFrameCompleteCallback", "(JLandroid/graphics/HardwareRenderer$FrameCompleteCallback;)V",
@@ -1198,6 +1227,13 @@
     gFrameMetricsObserverClassInfo.timingDataBuffer = GetFieldIDOrDie(
             env, metricsClass, "mTimingData", "[J");
 
+    jclass hardwareRenderer = FindClassOrDie(env,
+            "android/graphics/HardwareRenderer");
+    gHardwareRenderer.clazz = reinterpret_cast<jclass>(env->NewGlobalRef(hardwareRenderer));
+    gHardwareRenderer.invokePictureCapturedCallback = GetStaticMethodIDOrDie(env, hardwareRenderer,
+            "invokePictureCapturedCallback",
+            "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V");
+
     jclass frameCallbackClass = FindClassOrDie(env,
             "android/graphics/HardwareRenderer$FrameDrawingCallback");
     gFrameDrawingCallback.onFrameDraw = GetMethodIDOrDie(env, frameCallbackClass,
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index e402055..c4ddd50 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -20,6 +20,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.os.IBinder;
@@ -667,6 +668,17 @@
         nSetContentDrawBounds(mNativeProxy, left, top, right, bottom);
     }
 
+    /** @hide */
+    public void setPictureCaptureCallback(@Nullable PictureCapturedCallback callback) {
+        nSetPictureCaptureCallback(mNativeProxy, callback);
+    }
+
+    /** called by native */
+    static void invokePictureCapturedCallback(long picturePtr, PictureCapturedCallback callback) {
+        Picture picture = new Picture(picturePtr);
+        callback.onPictureCaptured(picture);
+    }
+
     /**
      * Interface used to receive callbacks when a frame is being drawn.
      *
@@ -695,6 +707,17 @@
         void onFrameComplete(long frameNr);
     }
 
+    /**
+     * Interface for listening to picture captures
+     * @hide
+     */
+    @TestApi
+    public interface PictureCapturedCallback {
+        /** @hide */
+        @TestApi
+        void onPictureCaptured(Picture picture);
+    }
+
     private static void validateAlpha(float alpha, String argumentName) {
         if (!(alpha >= 0.0f && alpha <= 1.0f)) {
             throw new IllegalArgumentException(argumentName + " must be a valid alpha, "
@@ -998,6 +1021,9 @@
     private static native void nSetContentDrawBounds(long nativeProxy, int left,
             int top, int right, int bottom);
 
+    private static native void nSetPictureCaptureCallback(long nativeProxy,
+            PictureCapturedCallback callback);
+
     private static native void nSetFrameCallback(long nativeProxy, FrameDrawingCallback callback);
 
     private static native void nSetFrameCompleteCallback(long nativeProxy,
diff --git a/graphics/java/android/graphics/Picture.java b/graphics/java/android/graphics/Picture.java
index f6d801b..8d12cbf 100644
--- a/graphics/java/android/graphics/Picture.java
+++ b/graphics/java/android/graphics/Picture.java
@@ -16,6 +16,7 @@
 
 package android.graphics;
 
+import android.annotation.NonNull;
 import android.annotation.UnsupportedAppUsage;
 
 import java.io.InputStream;
@@ -34,7 +35,8 @@
  */
 public class Picture {
     private PictureCanvas mRecordingCanvas;
-    @UnsupportedAppUsage
+    // TODO: Figure out if this was a false-positive
+    @UnsupportedAppUsage(maxTargetSdk = 28)
     private long mNativePicture;
     private boolean mRequiresHwAcceleration;
 
@@ -56,23 +58,43 @@
         this(nativeConstructor(src != null ? src.mNativePicture : 0));
     }
 
-    private Picture(long nativePicture) {
+    /** @hide */
+    public Picture(long nativePicture) {
         if (nativePicture == 0) {
-            throw new RuntimeException();
+            throw new IllegalArgumentException();
         }
         mNativePicture = nativePicture;
     }
 
+    /**
+     * Immediately releases the backing data of the Picture. This object will no longer
+     * be usable after calling this, and any further calls on the Picture will throw an
+     * IllegalStateException.
+     * // TODO: Support?
+     * @hide
+     */
+    public void close() {
+        if (mNativePicture != 0) {
+            nativeDestructor(mNativePicture);
+            mNativePicture = 0;
+        }
+    }
+
     @Override
     protected void finalize() throws Throwable {
         try {
-            nativeDestructor(mNativePicture);
-            mNativePicture = 0;
+            close();
         } finally {
             super.finalize();
         }
     }
 
+    private void verifyValid() {
+        if (mNativePicture == 0) {
+            throw new IllegalStateException("Picture is destroyed");
+        }
+    }
+
     /**
      * To record a picture, call beginRecording() and then draw into the Canvas
      * that is returned. Nothing we appear on screen, but all of the draw
@@ -81,7 +103,9 @@
      * that was returned must no longer be used, and nothing should be drawn
      * into it.
      */
+    @NonNull
     public Canvas beginRecording(int width, int height) {
+        verifyValid();
         if (mRecordingCanvas != null) {
             throw new IllegalStateException("Picture already recording, must call #endRecording()");
         }
@@ -98,6 +122,7 @@
      * or {@link Canvas#drawPicture(Picture)} is called.
      */
     public void endRecording() {
+        verifyValid();
         if (mRecordingCanvas != null) {
             mRequiresHwAcceleration = mRecordingCanvas.mHoldsHwBitmap;
             mRecordingCanvas = null;
@@ -110,7 +135,8 @@
      * does not reflect (per se) the content of the picture.
      */
     public int getWidth() {
-      return nativeGetWidth(mNativePicture);
+        verifyValid();
+        return nativeGetWidth(mNativePicture);
     }
 
     /**
@@ -118,7 +144,8 @@
      * does not reflect (per se) the content of the picture.
      */
     public int getHeight() {
-      return nativeGetHeight(mNativePicture);
+        verifyValid();
+        return nativeGetHeight(mNativePicture);
     }
 
     /**
@@ -133,6 +160,7 @@
      *         false otherwise.
      */
     public boolean requiresHardwareAcceleration() {
+        verifyValid();
         return mRequiresHwAcceleration;
     }
 
@@ -149,7 +177,8 @@
      *
      * @param canvas  The picture is drawn to this canvas
      */
-    public void draw(Canvas canvas) {
+    public void draw(@NonNull Canvas canvas) {
+        verifyValid();
         if (mRecordingCanvas != null) {
             endRecording();
         }
@@ -172,7 +201,7 @@
      * raw or compressed pixels.
      */
     @Deprecated
-    public static Picture createFromStream(InputStream stream) {
+    public static Picture createFromStream(@NonNull InputStream stream) {
         return new Picture(nativeCreateFromStream(stream, new byte[WORKING_STREAM_STORAGE]));
     }
 
@@ -188,10 +217,11 @@
      * Bitmap from which you can persist it as raw or compressed pixels.
      */
     @Deprecated
-    public void writeToStream(OutputStream stream) {
+    public void writeToStream(@NonNull OutputStream stream) {
+        verifyValid();
         // do explicit check before calling the native method
         if (stream == null) {
-            throw new NullPointerException();
+            throw new IllegalArgumentException("stream cannot be null");
         }
         if (!nativeWriteToStream(mNativePicture, stream, new byte[WORKING_STREAM_STORAGE])) {
             throw new RuntimeException();
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index 3b1d44b..09b18b7 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -184,7 +184,7 @@
      *
      * @param name The name of the RenderNode, used for debugging purpose. May be null.
      */
-    public RenderNode(String name) {
+    public RenderNode(@Nullable String name) {
         this(name, null);
     }
 
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index dd62bbb..7265692 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -551,6 +551,19 @@
     SkPaint paint = inPaint;
     paint.setAlpha(mProperties.getRootAlpha() * 255);
 
+    if (canvas->getGrContext() == nullptr) {
+        // Recording to picture, don't use the SkSurface which won't work off of renderthread.
+        Bitmap& bitmap = getBitmapUpdateIfDirty();
+        SkBitmap skiaBitmap;
+        bitmap.getSkBitmap(&skiaBitmap);
+
+        int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth());
+        int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight());
+        canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds,
+                               &paint, SkCanvas::kFast_SrcRectConstraint);
+        return;
+    }
+
     SkRect src;
     sk_sp<SkSurface> vdSurface = mCache.getSurface(&src);
     if (vdSurface) {
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
index 240efb4..60c8057 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
@@ -74,7 +74,13 @@
 
 void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
     if (canvas->getGrContext() == nullptr) {
-        SkDEBUGF(("Attempting to draw GLFunctor into an unsupported surface"));
+        // We're dumping a picture, render a light-blue rectangle instead
+        // TODO: Draw the WebView text on top? Seemingly complicated as SkPaint doesn't
+        // seem to have a default typeface that works. We only ever use drawGlyphs, which
+        // requires going through minikin & hwui's canvas which we don't have here.
+        SkPaint paint;
+        paint.setColor(0xFF81D4FA);
+        canvas->drawRect(mBounds, paint);
         return;
     }
 
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index df82243..47c9094 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -111,7 +111,7 @@
 
             const Rect& layerDamage = layers.entries()[i].damage;
 
-            SkCanvas* layerCanvas = tryCapture(layerNode->getLayerSurface());
+            SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();
 
             int saveCount = layerCanvas->save();
             SkASSERT(saveCount == 1);
@@ -139,8 +139,6 @@
             layerCanvas->restoreToCount(saveCount);
             mLightCenter = savedLightCenter;
 
-            endCapture(layerNode->getLayerSurface());
-
             // cache the current context so that we can defer flushing it until
             // either all the layers have been rendered or the context changes
             GrContext* currentContext = layerNode->getLayerSurface()->getCanvas()->getGrContext();
@@ -244,6 +242,7 @@
     }
 
     virtual void onProcess(const sp<Task<bool>>& task) override {
+        ATRACE_NAME("SavePictureTask");
         SavePictureTask* t = static_cast<SavePictureTask*>(task.get());
 
         if (0 == access(t->filename.c_str(), F_OK)) {
@@ -265,46 +264,56 @@
 
 SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) {
     if (CC_UNLIKELY(Properties::skpCaptureEnabled)) {
-        bool recordingPicture = mCaptureSequence > 0;
         char prop[PROPERTY_VALUE_MAX] = {'\0'};
-        if (!recordingPicture) {
+        if (mCaptureSequence <= 0) {
             property_get(PROPERTY_CAPTURE_SKP_FILENAME, prop, "0");
-            recordingPicture = prop[0] != '0' &&
-                               mCapturedFile != prop;  // ensure we capture only once per filename
-            if (recordingPicture) {
+            if (prop[0] != '0' && mCapturedFile != prop) {
                 mCapturedFile = prop;
                 mCaptureSequence = property_get_int32(PROPERTY_CAPTURE_SKP_FRAMES, 1);
             }
         }
-        if (recordingPicture) {
+        if (mCaptureSequence > 0 || mPictureCapturedCallback) {
             mRecorder.reset(new SkPictureRecorder());
-            return mRecorder->beginRecording(surface->width(), surface->height(), nullptr,
-                                             SkPictureRecorder::kPlaybackDrawPicture_RecordFlag);
+            SkCanvas* pictureCanvas = mRecorder->beginRecording(surface->width(), surface->height(), nullptr,
+                                                                SkPictureRecorder::kPlaybackDrawPicture_RecordFlag);
+            mNwayCanvas = std::make_unique<SkNWayCanvas>(surface->width(), surface->height());
+            mNwayCanvas->addCanvas(surface->getCanvas());
+            mNwayCanvas->addCanvas(pictureCanvas);
+            return mNwayCanvas.get();
         }
     }
     return surface->getCanvas();
 }
 
 void SkiaPipeline::endCapture(SkSurface* surface) {
+    mNwayCanvas.reset();
     if (CC_UNLIKELY(mRecorder.get())) {
+        ATRACE_CALL();
         sk_sp<SkPicture> picture = mRecorder->finishRecordingAsPicture();
-        surface->getCanvas()->drawPicture(picture);
         if (picture->approximateOpCount() > 0) {
-            auto data = picture->serialize();
+            if (mCaptureSequence > 0) {
+                ATRACE_BEGIN("picture->serialize");
+                auto data = picture->serialize();
+                ATRACE_END();
 
-            // offload saving to file in a different thread
-            if (!mSavePictureProcessor.get()) {
-                TaskManager* taskManager = getTaskManager();
-                mSavePictureProcessor = new SavePictureProcessor(
-                        taskManager->canRunTasks() ? taskManager : nullptr);
+                // offload saving to file in a different thread
+                if (!mSavePictureProcessor.get()) {
+                    TaskManager* taskManager = getTaskManager();
+                    mSavePictureProcessor = new SavePictureProcessor(
+                            taskManager->canRunTasks() ? taskManager : nullptr);
+                }
+                if (1 == mCaptureSequence) {
+                    mSavePictureProcessor->savePicture(data, mCapturedFile);
+                } else {
+                    mSavePictureProcessor->savePicture(
+                            data,
+                            mCapturedFile + "_" + std::to_string(mCaptureSequence));
+                }
+                mCaptureSequence--;
             }
-            if (1 == mCaptureSequence) {
-                mSavePictureProcessor->savePicture(data, mCapturedFile);
-            } else {
-                mSavePictureProcessor->savePicture(
-                        data, mCapturedFile + "_" + std::to_string(mCaptureSequence));
+            if (mPictureCapturedCallback) {
+                std::invoke(mPictureCapturedCallback, std::move(picture));
             }
-            mCaptureSequence--;
         }
         mRecorder.reset();
     }
@@ -314,6 +323,11 @@
                                const std::vector<sp<RenderNode>>& nodes, bool opaque,
                                const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
                                const SkMatrix& preTransform) {
+    bool previousSkpEnabled = Properties::skpCaptureEnabled;
+    if (mPictureCapturedCallback) {
+        Properties::skpCaptureEnabled = true;
+    }
+
     renderVectorDrawableCache();
 
     // draw all layers up front
@@ -334,6 +348,8 @@
 
     ATRACE_NAME("flush commands");
     surface->getCanvas()->flush();
+
+    Properties::skpCaptureEnabled = previousSkpEnabled;
 }
 
 namespace {
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index cf6f5b2..e9957df 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -105,6 +105,11 @@
         mLightCenter = lightGeometry.center;
     }
 
+    void setPictureCapturedCallback(
+            const std::function<void(sk_sp<SkPicture>&&)>& callback) override {
+        mPictureCapturedCallback = callback;
+    }
+
 protected:
     void dumpResourceCacheUsage() const;
     void setSurfaceColorProperties(renderthread::ColorMode colorMode);
@@ -163,6 +168,8 @@
      *  parallel tryCapture calls (not really needed).
      */
     std::unique_ptr<SkPictureRecorder> mRecorder;
+    std::unique_ptr<SkNWayCanvas> mNwayCanvas;
+    std::function<void(sk_sp<SkPicture>&&)> mPictureCapturedCallback;
 
     static float mLightRadius;
     static uint8_t mAmbientShadowAlpha;
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 9e7abf4..db97763 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -184,6 +184,10 @@
         mFrameCompleteCallbacks.push_back(std::move(func));
     }
 
+    void setPictureCapturedCallback(const std::function<void(sk_sp<SkPicture>&&)>& callback) {
+        mRenderPipeline->setPictureCapturedCallback(callback);
+    }
+
     void setForceDark(bool enable) {
         mUseForceDark = enable;
     }
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index d4dd629..2cfc8df 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -59,15 +59,15 @@
     virtual MakeCurrentResult makeCurrent() = 0;
     virtual Frame getFrame() = 0;
     virtual bool draw(const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
-                      const LightGeometry& lightGeometry,
-                      LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds,
-                      bool opaque, const LightInfo& lightInfo,
+                      const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+                      const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
                       const std::vector<sp<RenderNode>>& renderNodes,
                       FrameInfoVisualizer* profiler) = 0;
     virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty,
                              FrameInfo* currentFrameInfo, bool* requireSwap) = 0;
     virtual DeferredLayerUpdater* createTextureLayer() = 0;
-    virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior, ColorMode colorMode) = 0;
+    virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior,
+                            ColorMode colorMode) = 0;
     virtual void onStop() = 0;
     virtual bool isSurfaceReady() = 0;
     virtual bool isContextReady() = 0;
@@ -85,6 +85,8 @@
     virtual SkColorType getSurfaceColorType() const = 0;
     virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0;
     virtual GrSurfaceOrigin getSurfaceOrigin() = 0;
+    virtual void setPictureCapturedCallback(
+            const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0;
 
     virtual ~IRenderPipeline() {}
 };
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index aa6af23..ab59af7 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -21,6 +21,7 @@
 #include "Properties.h"
 #include "Readback.h"
 #include "Rect.h"
+#include "WebViewFunctorManager.h"
 #include "pipeline/skia/SkiaOpenGLPipeline.h"
 #include "pipeline/skia/VectorDrawableAtlas.h"
 #include "renderstate/RenderState.h"
@@ -30,7 +31,6 @@
 #include "renderthread/RenderThread.h"
 #include "utils/Macros.h"
 #include "utils/TimeUtils.h"
-#include "WebViewFunctorManager.h"
 
 #include <ui/GraphicBuffer.h>
 
@@ -147,9 +147,7 @@
 void RenderProxy::destroyFunctor(int functor) {
     ATRACE_CALL();
     RenderThread& thread = RenderThread::getInstance();
-    thread.queue().post([=]() {
-        WebViewFunctorManager::instance().destroyFunctor(functor);
-    });
+    thread.queue().post([=]() { WebViewFunctorManager::instance().destroyFunctor(functor); });
 }
 
 DeferredLayerUpdater* RenderProxy::createTextureLayer() {
@@ -164,9 +162,9 @@
 
 bool RenderProxy::copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap) {
     auto& thread = RenderThread::getInstance();
-    return thread.queue().runSync(
-            [&]() -> bool { return thread.readback().copyLayerInto(layer, &bitmap)
-                                   == CopyResult::Success; });
+    return thread.queue().runSync([&]() -> bool {
+        return thread.readback().copyLayerInto(layer, &bitmap) == CopyResult::Success;
+    });
 }
 
 void RenderProxy::pushLayerUpdate(DeferredLayerUpdater* layer) {
@@ -204,9 +202,8 @@
 }
 
 int RenderProxy::maxTextureSize() {
-    static int maxTextureSize = RenderThread::getInstance().queue().runSync([]() {
-        return DeviceInfo::get()->maxTextureSize();
-    });
+    static int maxTextureSize = RenderThread::getInstance().queue().runSync(
+            []() { return DeviceInfo::get()->maxTextureSize(); });
     return maxTextureSize;
 }
 
@@ -281,6 +278,12 @@
     mDrawFrameTask.setContentDrawBounds(left, top, right, bottom);
 }
 
+void RenderProxy::setPictureCapturedCallback(
+        const std::function<void(sk_sp<SkPicture>&&)>& callback) {
+    mRenderThread.queue().post(
+            [ this, cb = callback ]() { mContext->setPictureCapturedCallback(cb); });
+}
+
 void RenderProxy::setFrameCallback(std::function<void(int64_t)>&& callback) {
     mDrawFrameTask.setFrameCallback(std::move(callback));
 }
@@ -302,9 +305,7 @@
 }
 
 void RenderProxy::setForceDark(bool enable) {
-    mRenderThread.queue().post([this, enable]() {
-        mContext->setForceDark(enable);
-    });
+    mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
 }
 
 int RenderProxy::copySurfaceInto(sp<Surface>& surface, int left, int top, int right, int bottom,
@@ -348,9 +349,8 @@
         // TODO: fix everything that hits this. We should never be triggering a readback ourselves.
         return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap);
     } else {
-        return thread.queue().runSync([&]() -> int {
-            return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap);
-        });
+        return thread.queue().runSync(
+                [&]() -> int { return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap); });
     }
 }
 
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 9dc9181..6e1bfd7 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -114,6 +114,8 @@
     ANDROID_API void removeRenderNode(RenderNode* node);
     ANDROID_API void drawRenderNode(RenderNode* node);
     ANDROID_API void setContentDrawBounds(int left, int top, int right, int bottom);
+    ANDROID_API void setPictureCapturedCallback(
+            const std::function<void(sk_sp<SkPicture>&&)>& callback);
     ANDROID_API void setFrameCallback(std::function<void(int64_t)>&& callback);
     ANDROID_API void setFrameCompleteCallback(std::function<void(int64_t)>&& callback);
 
diff --git a/tests/HwAccelerationTest/Android.mk b/tests/HwAccelerationTest/Android.mk
index 11ea954..79072fa 100644
--- a/tests/HwAccelerationTest/Android.mk
+++ b/tests/HwAccelerationTest/Android.mk
@@ -21,6 +21,7 @@
 
 LOCAL_PACKAGE_NAME := HwAccelerationTest
 LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_CERTIFICATE := platform
 
 LOCAL_MODULE_TAGS := tests
 
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index c8f96c9..f330b83 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -310,6 +310,15 @@
                 <category android:name="com.android.test.hwui.TEST" />
             </intent-filter>
         </activity>
+
+        <activity
+            android:name="PictureCaptureDemo"
+            android:label="Debug/Picture Capture">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.android.test.hwui.TEST" />
+            </intent-filter>
+        </activity>
         
         <activity
                 android:name="SmallCircleActivity"
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java b/tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java
new file mode 100644
index 0000000..029e302
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 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 com.android.test.hwui;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Picture;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewDebug;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ProgressBar;
+
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+public class PictureCaptureDemo extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final LinearLayout layout = new LinearLayout(this);
+        layout.setOrientation(LinearLayout.VERTICAL);
+
+        final LinearLayout inner = new LinearLayout(this);
+        inner.setOrientation(LinearLayout.HORIZONTAL);
+        ProgressBar spinner = new ProgressBar(this, null, android.R.attr.progressBarStyleLarge);
+        inner.addView(spinner,
+                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+        inner.addView(new View(this), new LayoutParams(50, 1));
+
+        Picture picture = new Picture();
+        Canvas canvas = picture.beginRecording(100, 100);
+        canvas.drawColor(Color.RED);
+        Paint paint = new Paint();
+        paint.setTextSize(32);
+        paint.setColor(Color.BLACK);
+        canvas.drawText("Hello", 0, 50, paint);
+        picture.endRecording();
+
+        ImageView iv1 = new ImageView(this);
+        iv1.setImageBitmap(Bitmap.createBitmap(picture, 100, 100, Bitmap.Config.ARGB_8888));
+        inner.addView(iv1, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+        inner.addView(new View(this), new LayoutParams(50, 1));
+
+        ImageView iv2 = new ImageView(this);
+        iv2.setImageBitmap(Bitmap.createBitmap(picture, 100, 100, Bitmap.Config.HARDWARE));
+        inner.addView(iv2, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+        layout.addView(inner,
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+        // For testing with a functor in the tree
+        WebView wv = new WebView(this);
+        wv.setWebViewClient(new WebViewClient());
+        wv.setWebChromeClient(new WebChromeClient());
+        wv.loadUrl("https://google.com");
+        layout.addView(wv, new LayoutParams(LayoutParams.MATCH_PARENT, 400));
+
+        SurfaceView mySurfaceView = new SurfaceView(this);
+        layout.addView(mySurfaceView,
+                new LayoutParams(LayoutParams.MATCH_PARENT, 600));
+
+        setContentView(layout);
+
+        mySurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
+            private AutoCloseable mStopCapture;
+
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {
+                final Random rand = new Random();
+                mStopCapture = ViewDebug.startRenderingCommandsCapture(mySurfaceView,
+                        mCaptureThread, (picture) -> {
+                            if (rand.nextInt(20) == 0) {
+                                try {
+                                    Thread.sleep(100);
+                                } catch (InterruptedException e) {
+                                }
+                            }
+                            Canvas canvas = holder.lockCanvas();
+                            if (canvas == null) {
+                                return false;
+                            }
+                            canvas.drawPicture(picture);
+                            holder.unlockCanvasAndPost(canvas);
+                            picture.close();
+                            return true;
+                        });
+            }
+
+            @Override
+            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+
+            }
+
+            @Override
+            public void surfaceDestroyed(SurfaceHolder holder) {
+                if (mStopCapture != null) {
+                    try {
+                        mStopCapture.close();
+                    } catch (Exception e) {
+                    }
+                    mStopCapture = null;
+                }
+            }
+        });
+    }
+
+    ExecutorService mCaptureThread = Executors.newSingleThreadExecutor();
+    ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+    Picture deepCopy(Picture src) {
+        try {
+            PipedInputStream inputStream = new PipedInputStream();
+            PipedOutputStream outputStream = new PipedOutputStream(inputStream);
+            Future<Picture> future = mExecutor.submit(() -> Picture.createFromStream(inputStream));
+            src.writeToStream(outputStream);
+            outputStream.close();
+            return future.get();
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+}