Use correct API for screenshotting

This is required to read back hardware bitmaps.

Fixes: 179511592
Test: Updated the relevant test. Removed the transport test, since it
  couldn't work with the current fake-android setup in transport, and
  since it's on the way out anyway.
Change-Id: Ie37630e1ce317e54463d4d44e55089288326d61a
diff --git a/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/os/HandlerThread.java b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/os/HandlerThread.java
new file mode 100644
index 0000000..83bfe65
--- /dev/null
+++ b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/os/HandlerThread.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 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 android.os;
+
+import androidx.annotation.NonNull;
+
+public class HandlerThread extends Thread {
+    public HandlerThread(String name) {
+        Looper.prepare();
+    }
+
+    @NonNull
+    public Looper getLooper() {
+        return Looper.myLooper();
+    }
+}
diff --git a/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/PixelCopy.java b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/PixelCopy.java
new file mode 100644
index 0000000..e2e8da6
--- /dev/null
+++ b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/PixelCopy.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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 android.view;
+
+import android.graphics.Bitmap;
+import android.os.Handler;
+import androidx.annotation.NonNull;
+
+public class PixelCopy {
+    public interface OnPixelCopyFinishedListener {
+        void onPixelCopyFinished(int copyResult);
+    }
+
+    public static final int SUCCESS = 0;
+
+    public static void request(
+            @NonNull Surface source,
+            @NonNull Bitmap dest,
+            @NonNull OnPixelCopyFinishedListener listener,
+            @NonNull Handler listenerThread) {
+        System.arraycopy(source.bitmapBytes, 0, dest.bytes, 0, source.bitmapBytes.length);
+        listener.onPixelCopyFinished(0);
+    }
+}
diff --git a/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/Surface.java b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/Surface.java
new file mode 100644
index 0000000..f4c41ba
--- /dev/null
+++ b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/Surface.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 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 android.view;
+
+import androidx.annotation.VisibleForTesting;
+
+public class Surface {
+    @VisibleForTesting public byte[] bitmapBytes;
+}
diff --git a/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/View.java b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/View.java
index 24574ee..d47ecae 100644
--- a/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/View.java
+++ b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/View.java
@@ -68,6 +68,8 @@
     private int mScrollY = 0;
     private final ViewGroup.LayoutParams mLayoutParams = new ViewGroup.LayoutParams();
 
+    private ViewRootImpl mViewRootImpl;
+
     @VisibleForTesting public final Point locationInSurface = new Point();
 
     @VisibleForTesting public final Point locationOnScreen = new Point();
@@ -216,6 +218,15 @@
         matrix.transformedPoints = mTransformedPoints;
     }
 
+    @VisibleForTesting
+    public void setViewRootImpl(ViewRootImpl viewRootImpl) {
+        mViewRootImpl = viewRootImpl;
+    }
+
+    public ViewRootImpl getViewRootImpl() {
+        return mViewRootImpl;
+    }
+
     // Only works with views where setAttachInfo was called on them
     @VisibleForTesting
     public void forcePictureCapture(Picture picture) {
diff --git a/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/ViewRootImpl.java b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/ViewRootImpl.java
new file mode 100644
index 0000000..5c3a23d
--- /dev/null
+++ b/dynamic-layout-inspector/agent/appinspection/fake-android/src/android/view/ViewRootImpl.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2021 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 android.view;
+
+public class ViewRootImpl {
+    public Surface mSurface;
+}
diff --git a/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/ViewLayoutInspector.kt b/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/ViewLayoutInspector.kt
index 1fd71fa..db6f057 100644
--- a/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/ViewLayoutInspector.kt
+++ b/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/ViewLayoutInspector.kt
@@ -141,9 +141,7 @@
     private fun checkRoots(captureNewRoots: Boolean): Boolean {
         val currRoots =
             ThreadUtils.runOnMainThread {
-                getRootViews()
-                    .map { v -> v.uniqueDrawingId to v }
-                    .toMap()
+                getRootViews().associateBy { it.uniqueDrawingId }
             }.get()
 
         val currRootIds = currRoots.keys
diff --git a/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/SynchronousPixelCopy.java b/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/SynchronousPixelCopy.java
new file mode 100644
index 0000000..2421171
--- /dev/null
+++ b/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/SynchronousPixelCopy.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.tools.agent.appinspection.framework;
+
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.PixelCopy;
+import android.view.PixelCopy.OnPixelCopyFinishedListener;
+import android.view.Surface;
+
+/**
+ * Adapted from
+ * cts/libs/deviceutillegacy/src/com/android/compatibility/common/util/SynchronousPixelCopy.java
+ */
+public class SynchronousPixelCopy implements OnPixelCopyFinishedListener {
+    private static final Handler sHandler;
+
+    static {
+        HandlerThread thread = new HandlerThread("PixelCopyHelper");
+        thread.start();
+        sHandler = new Handler(thread.getLooper());
+    }
+
+    private int mStatus = -1;
+
+    public int request(Surface source, Bitmap dest) throws InterruptedException {
+        synchronized (this) {
+            PixelCopy.request(source, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    private int getResultLocked() throws InterruptedException {
+        // The normal amount of time should be much less--around 10ms. However it's possible for
+        // other things that are going on at the same time to delay substantially if e.g. an
+        // activity is launching.
+        this.wait(1000);
+        return mStatus;
+    }
+
+    @Override
+    public void onPixelCopyFinished(int copyResult) {
+        synchronized (this) {
+            mStatus = copyResult;
+            this.notify();
+        }
+    }
+}
diff --git a/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/ViewExtensions.kt b/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/ViewExtensions.kt
index 6ccd971..8061031 100644
--- a/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/ViewExtensions.kt
+++ b/dynamic-layout-inspector/agent/appinspection/src/main/com/android/tools/agent/appinspection/framework/ViewExtensions.kt
@@ -18,8 +18,8 @@
 
 import android.content.res.Resources
 import android.graphics.Bitmap
-import android.graphics.Canvas
 import android.util.Log
+import android.view.PixelCopy
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
@@ -88,21 +88,26 @@
 /**
  * Convert this view into a bitmap.
  *
- * This method may return null if the app runs out of memory trying to create it.
+ * This method may return null if the app runs out of memory or has a reflection issue.
  */
 fun View.takeScreenshot(scale: Float): Bitmap? {
+    // We use RGB_565 here since we get significantly better framerate in the inspector with
+    // smaller payloads.
     val bitmap = Bitmap.createBitmap(
         (width * scale).roundToInt(),
         (height * scale).roundToInt(),
         Bitmap.Config.RGB_565
     )
     return try {
-        val canvas = Canvas(bitmap)
-        canvas.scale(scale, scale)
-        ThreadUtils.runOnMainThread { draw(canvas) }.get()
-        bitmap
-    } catch (e: OutOfMemoryError) {
-        Log.w("ViewLayoutInspector", "Out of memory for bitmap")
+        val resultCode = SynchronousPixelCopy().request(viewRootImpl.mSurface, bitmap)
+        if (resultCode == PixelCopy.SUCCESS) {
+            bitmap
+        } else {
+            Log.w("ViewLayoutInspector", "PixelCopy got error code $resultCode")
+            null
+        }
+    } catch (t: Throwable) {
+        Log.w("ViewLayoutInspector", t)
         null
     }
 }
diff --git a/dynamic-layout-inspector/agent/appinspection/src/test/com/android/tools/agent/appinspection/ViewLayoutInspectorTest.kt b/dynamic-layout-inspector/agent/appinspection/src/test/com/android/tools/agent/appinspection/ViewLayoutInspectorTest.kt
index 9303652..5acfa6a 100644
--- a/dynamic-layout-inspector/agent/appinspection/src/test/com/android/tools/agent/appinspection/ViewLayoutInspectorTest.kt
+++ b/dynamic-layout-inspector/agent/appinspection/src/test/com/android/tools/agent/appinspection/ViewLayoutInspectorTest.kt
@@ -19,8 +19,10 @@
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Picture
+import android.view.Surface
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewRootImpl
 import android.view.WindowManagerGlobal
 import android.widget.TextView
 import com.android.tools.agent.appinspection.proto.StringTable
@@ -38,7 +40,6 @@
 import org.junit.Rule
 import org.junit.Test
 import java.util.concurrent.ArrayBlockingQueue
-import java.util.function.Consumer
 
 class ViewLayoutInspectorTest {
 
@@ -542,11 +543,6 @@
         val root = ViewGroup(context).apply {
             width = 100
             height = 200
-            drawHandler = Consumer { canvas ->
-                assertThat(canvas.bitmap.width).isEqualTo(width * scale)
-                assertThat(canvas.bitmap.height).isEqualTo(height * scale)
-                fakeBitmapHeader.copyInto(canvas.bitmap.bytes)
-            }
             setAttachInfo(View.AttachInfo())
         }
         WindowManagerGlobal.getInstance().rootViews.addAll(listOf(root))
@@ -582,7 +578,9 @@
                 val response = Response.parseFrom(bytes)
                 assertThat(response.specializedCase).isEqualTo(Response.SpecializedCase.UPDATE_SCREENSHOT_TYPE_RESPONSE)
             }
-
+            root.viewRootImpl = ViewRootImpl()
+            root.viewRootImpl.mSurface = Surface()
+            root.viewRootImpl.mSurface.bitmapBytes = fakeBitmapHeader
             root.forcePictureCapture(fakePicture1)
             eventQueue.take().let { bytes ->
                 val event = Event.parseFrom(bytes)
diff --git a/dynamic-layout-inspector/agent/transport/src/main/com/android/tools/agent/layoutinspector/LayoutInspectorService.java b/dynamic-layout-inspector/agent/transport/src/main/com/android/tools/agent/layoutinspector/LayoutInspectorService.java
index 0a66d39..bf429da 100644
--- a/dynamic-layout-inspector/agent/transport/src/main/com/android/tools/agent/layoutinspector/LayoutInspectorService.java
+++ b/dynamic-layout-inspector/agent/transport/src/main/com/android/tools/agent/layoutinspector/LayoutInspectorService.java
@@ -17,8 +17,11 @@
 package com.android.tools.agent.layoutinspector;
 
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
+import android.os.Handler;
+import android.os.Looper;
 import android.util.Log;
+import android.view.PixelCopy;
+import android.view.Surface;
 import android.view.View;
 import android.view.inspector.WindowInspector;
 import androidx.annotation.NonNull;
@@ -36,9 +39,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.zip.Deflater;
 
@@ -404,18 +409,37 @@
     private static Bitmap performViewCapture(final View captureView, float scale) {
         Bitmap bitmap =
                 Bitmap.createBitmap(
-                        (int) (captureView.getWidth() * scale),
-                        (int) (captureView.getHeight() * scale),
+                        Math.round(captureView.getWidth() * scale),
+                        Math.round(captureView.getHeight() * scale),
                         Bitmap.Config.RGB_565);
         try {
-            Canvas canvas = new Canvas(bitmap);
-            canvas.scale(scale, scale);
-            captureView.draw(canvas);
-            return bitmap;
-        } catch (OutOfMemoryError e) {
-            Log.w("LayoutInspectorService", "Out of memory for bitmap");
+            CompletableFuture<Integer> resultFuture = new CompletableFuture<>();
+            Object viewRootImpl =
+                    View.class.getDeclaredMethod("getViewRootImpl").invoke(captureView);
+            Object surface =
+                    Class.forName("android.view.ViewRootImpl")
+                            .getDeclaredField("mSurface")
+                            .get(viewRootImpl);
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                throw new IllegalStateException(
+                        "takeScreenshot cannot be called on the main thread");
+            }
+            PixelCopy.request(
+                    (Surface) surface,
+                    bitmap,
+                    resultFuture::complete,
+                    new Handler(Looper.getMainLooper()));
+            int resultCode = resultFuture.get(1, TimeUnit.SECONDS);
+            if (resultCode == PixelCopy.SUCCESS) {
+                return bitmap;
+            } else {
+                Log.w("ViewLayoutInspector", "PixelCopy got error code " + resultCode);
+                return null;
+            }
+        } catch (Throwable t) {
+            Log.w("ViewLayoutInspector", "Exception while getting screenshot", t);
+            return null;
         }
-        return null;
     }
 
     /**
diff --git a/dynamic-layout-inspector/agent/transport/src/test/com/android/tools/agent/layoutinspector/LayoutInspectorServiceTest.kt b/dynamic-layout-inspector/agent/transport/src/test/com/android/tools/agent/layoutinspector/LayoutInspectorServiceTest.kt
index f6ed5ca..b59c96a 100644
--- a/dynamic-layout-inspector/agent/transport/src/test/com/android/tools/agent/layoutinspector/LayoutInspectorServiceTest.kt
+++ b/dynamic-layout-inspector/agent/transport/src/test/com/android/tools/agent/layoutinspector/LayoutInspectorServiceTest.kt
@@ -139,45 +139,6 @@
             .isEqualTo(pictureBytes)
     }
 
-    @Test
-    fun testUseScreenshotMode() {
-        val bitmap = mock(Bitmap::class.java)
-        Bitmap.INSTANCE = bitmap
-        val bitmapBytes = (1 .. 1_000_000).map { (it % 256).toByte() }.toByteArray()
-        `when`(bitmap.byteCount).thenReturn(1_000_000)
-        `when`(bitmap.copyPixelsToBuffer(any()))
-            .then { invocation ->
-                invocation.getArgument<ByteBuffer>(0).put(bitmapBytes)
-                true
-            }
-        val (service, callback) = setUpInspectorService()
-
-        service.onUseScreenshotModeCommand(true)
-
-        val event = onPictureCaptured(callback, Picture())
-        assertThat(event.groupId).isEqualTo(1101)
-        assertThat(event.kind).isEqualTo(Common.Event.Kind.LAYOUT_INSPECTOR)
-        val tree = event.layoutInspectorEvent.tree
-        assertThat(tree.payloadType)
-            .isEqualTo(LayoutInspectorProto.ComponentTreeEvent.PayloadType.BITMAP_AS_REQUESTED)
-        val payload = agentRule.payloads[event.layoutInspectorEvent.tree.payloadId]
-        val inf = Inflater().also { it.setInput(payload) }
-        val baos = ByteArrayOutputStream()
-        val buffer = ByteArray(4096)
-        var total = 0
-        while (!inf.finished()) {
-            val count = inf.inflate(buffer)
-            if (count <= 0) {
-                break
-            }
-            baos.write(buffer, 0, count)
-            total += count
-        }
-
-        assertThat(total).isEqualTo(1_000_000)
-        assertThat(baos.toByteArray()).isEqualTo(bitmapBytes)
-    }
-
     private fun setUpInspectorService()
             : Pair<LayoutInspectorService, HardwareRenderer.PictureCapturedCallback> {
         val handler = mock(Handler::class.java)