Ensure SKP serialization occurs on RenderThread

Instead of doing lazy serialization of SKP on the
background executor serialize to a byte[] immediately
at callback invocation. This ensures no potential
for later mutations, race conditions, or wrong-thread issues
at the expense of potentially impacting app rendering performance.

However it seems preferable for a debug-only tool to be a slow
instead of very crashy.

Bug: 141772764
Test: test app
Change-Id: I3316d49970b96f1c59bb0a28ff7335db608e539e
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
index a390db2..1ee3366 100644
--- a/core/java/android/view/ViewDebug.java
+++ b/core/java/android/view/ViewDebug.java
@@ -868,6 +868,94 @@
         return null;
     }
 
+    private static class StreamingPictureCallbackHandler implements AutoCloseable,
+            HardwareRenderer.PictureCapturedCallback, Runnable {
+        private final HardwareRenderer mRenderer;
+        private final Callable<OutputStream> mCallback;
+        private final Executor mExecutor;
+        private final ReentrantLock mLock = new ReentrantLock(false);
+        private final ArrayDeque<byte[]> mQueue = new ArrayDeque<>(3);
+        private final ByteArrayOutputStream mByteStream = new ByteArrayOutputStream();
+        private boolean mStopListening;
+        private Thread mRenderThread;
+
+        private StreamingPictureCallbackHandler(HardwareRenderer renderer,
+                Callable<OutputStream> 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();
+            }
+            boolean needsInvoke = true;
+            if (mQueue.size() == 3) {
+                mQueue.removeLast();
+                needsInvoke = false;
+            }
+            picture.writeToStream(mByteStream);
+            mQueue.add(mByteStream.toByteArray());
+            mByteStream.reset();
+            mLock.unlock();
+
+            if (needsInvoke) {
+                mExecutor.execute(this);
+            }
+        }
+
+        @Override
+        public void run() {
+            mLock.lock();
+            final byte[] 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) {
+                return;
+            }
+            OutputStream stream = null;
+            try {
+                stream = mCallback.call();
+            } catch (Exception ex) {
+                Log.w("ViewDebug", "Aborting rendering commands capture "
+                        + "because callback threw exception", ex);
+            }
+            if (stream != null) {
+                try {
+                    stream.write(picture);
+                } catch (IOException ex) {
+                    Log.w("ViewDebug", "Aborting rendering commands capture "
+                            + "due to IOException writing to output stream", ex);
+                }
+            } else {
+                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,
@@ -913,18 +1001,7 @@
         }
         final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
         if (renderer != null) {
-            return new PictureCallbackHandler(renderer, (picture -> {
-                try {
-                    OutputStream stream = callback.call();
-                    if (stream != null) {
-                        picture.writeToStream(stream);
-                        return true;
-                    }
-                } catch (Exception ex) {
-                    // fall through
-                }
-                return false;
-            }), executor);
+            return new StreamingPictureCallbackHandler(renderer, callback, executor);
         }
         return null;
     }