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)