From d85adc030f139e09515e660d28c77dc2e2a4e4e5 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Thu, 2 Nov 2023 12:29:07 -0700 Subject: [PATCH] Use pixel copy on activities Summary: Pixel copy is a more reliable and consistent way to take a snapshot rather than drawing into a canvas. It accepts either: Surface SurfaceView Window For root views that belong to an activity its easy to get the window so we do that here. In the next diff we solve this for other root views Reviewed By: lblasa Differential Revision: D50845282 fbshipit-source-id: 3968828dedd1e96a854b907e0fd152ad64993d95 --- .../plugins/uidebugger/common/BitmapPool.kt | 3 +- .../plugins/uidebugger/core/Snapshot.kt | 53 +++++++++++++++++++ .../plugins/uidebugger/core/UIDContext.kt | 12 ++++- .../descriptors/ApplicationRefDescriptor.kt | 2 +- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt index a623fd02b..5a779c1a9 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt @@ -10,7 +10,6 @@ package com.facebook.flipper.plugins.uidebugger.common import android.graphics.Bitmap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch /** BitmapPool is intended to be used on the main thread. In other words, it is not thread-safe. */ class BitmapPool(private val config: Bitmap.Config = Bitmap.Config.RGB_565) { @@ -57,7 +56,7 @@ class BitmapPool(private val config: Bitmap.Config = Bitmap.Config.RGB_565) { override fun readyForReuse() { val key = generateKey(bitmap.width, bitmap.height) - mainScope.launch { + synchronized(this@BitmapPool) { if (isRecycled) { bitmap.recycle() } else { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Snapshot.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Snapshot.kt index 4ce9af9c6..1467d73c9 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Snapshot.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Snapshot.kt @@ -7,9 +7,15 @@ package com.facebook.flipper.plugins.uidebugger.core +import android.app.Activity import android.graphics.Canvas +import android.os.Build +import android.os.Handler +import android.os.Looper import android.util.Log +import android.view.PixelCopy import android.view.View +import androidx.annotation.RequiresApi import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.common.BitmapPool @@ -41,3 +47,50 @@ class CanvasSnapshotter(private val bitmapPool: BitmapPool) : Snapshotter { } } } + +@RequiresApi(Build.VERSION_CODES.O) +class PixelCopySnapshotter( + private val bitmapPool: BitmapPool, + private val applicationRef: ApplicationRef, + private val fallback: Snapshotter +) : Snapshotter { + + override fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? { + + if (view.width <= 0 || view.height <= 0) { + return null + } + + val bitmap = bitmapPool.getBitmap(view.width, view.height) + try { + + val decorViewToActivity: Map = + applicationRef.activitiesStack.toList().associateBy { it.window.decorView } + + val activity = decorViewToActivity[view] + + // if this view belongs to an activity prefer this as it doesn't require private api hacks + if (activity != null) { + PixelCopy.request( + activity.window, + bitmap.bitmap, + { + // no-op this this api is actually synchronous despite how it looks + }, + Handler(Looper.getMainLooper())) + return bitmap + } + } catch (e: OutOfMemoryError) { + Log.e(LogTag, "OOM when taking snapshot") + null + } catch (e: Exception) { + // there was some problem with the pixel copy, fall back to canvas impl + Log.e(LogTag, "Exception when taking snapshot", e) + Log.i(LogTag, "Using fallback snapshot", e) + bitmap.readyForReuse() + fallback.takeSnapshot(view) + } + + return null + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/UIDContext.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/UIDContext.kt index 47dcac79a..6741b4ef7 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/UIDContext.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/UIDContext.kt @@ -8,6 +8,7 @@ package com.facebook.flipper.plugins.uidebugger.core import android.app.Application +import android.os.Build import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.plugins.uidebugger.common.BitmapPool import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister @@ -32,7 +33,16 @@ class UIDContext( ) { val bitmapPool = BitmapPool() - val decorViewTracker = DecorViewTracker(this, CanvasSnapshotter(bitmapPool)) + private val canvasSnapshotter = CanvasSnapshotter(bitmapPool) + + val snapshotter = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PixelCopySnapshotter(bitmapPool, applicationRef, canvasSnapshotter) + } else { + canvasSnapshotter + } + + val decorViewTracker = DecorViewTracker(this, snapshotter) val updateQueue = UpdateQueue(this) val layoutTraversal: LayoutTraversal = LayoutTraversal(this) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt index eea5a20a4..5278561bb 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt @@ -44,7 +44,7 @@ object ApplicationRefDescriptor : ChainedDescriptor() { val activeRoots = node.rootsResolver.rootViews() val decorViewToActivity: Map = - node.activitiesStack.toList().map { it.window.decorView to it }.toMap() + node.activitiesStack.toList().associateBy { it.window.decorView } for (root in activeRoots) { // if there is an activity for this root view use that,