From d26612d840dd88e0bd0b43bdbd865d1c75efcf9c Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Thu, 2 Nov 2023 12:29:07 -0700 Subject: [PATCH] Add modern snapshot approach Summary: Since api level 34 there is a way to snapshot any view, prior to this you needed a window which you can only get from an activity, or a surface which required hacks. The hacks are in place for older verisons of android but ive added modern pixel copy snapshotter for future proofing in case google decide to make any of the previously used hack not work in future version of android Since there was a bunch of common code in each snap shot impl this has been pull into a utility function Reviewed By: lblasa Differential Revision: D50845284 fbshipit-source-id: c7910c45ff51fcf8636adc3d7272198ac3d4aefe --- .../plugins/uidebugger/core/Snapshot.kt | 116 ++++++++++++------ .../plugins/uidebugger/core/UIDContext.kt | 15 ++- 2 files changed, 87 insertions(+), 44 deletions(-) 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 b8335866a..814c68793 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 @@ -11,7 +11,6 @@ import android.app.Activity import android.graphics.Canvas import android.os.Build import android.os.Handler -import android.os.HandlerThread import android.os.Looper import android.util.Log import android.view.PixelCopy @@ -36,22 +35,47 @@ interface Snapshotter { class CanvasSnapshotter(private val bitmapPool: BitmapPool) : Snapshotter { override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? { - if (view.width <= 0 || view.height <= 0) { - return null - } - - return try { - val reuseAbleBitmap = bitmapPool.getBitmap(view.width, view.height) - val canvas = Canvas(reuseAbleBitmap.bitmap) + return SnapshotCommon.doSnapshotWithErrorHandling(bitmapPool, view, fallback = null) { bitmap -> + val canvas = Canvas(bitmap.bitmap) view.draw(canvas) - reuseAbleBitmap - } catch (e: OutOfMemoryError) { - Log.e(LogTag, "OOM when taking snapshot") - null + true } } } +/** + * Uses the new api to snapshot any view regardless whether its attached to a activity or not, + * requires no hacks + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +class ModernPixelCopySnapshotter( + private val bitmapPool: BitmapPool, + private val fallback: Snapshotter +) : Snapshotter { + private var handler = Handler(Looper.getMainLooper()) + + override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? { + + return SnapshotCommon.doSnapshotWithErrorHandling(bitmapPool, view, fallback) { reusableBitmap + -> + suspendCoroutine { continuation -> + // Since android U this api is actually async + val request = + PixelCopy.Request.Builder.ofWindow(view) + .setDestinationBitmap(reusableBitmap.bitmap) + .build() + PixelCopy.request( + request, { handler.post(it) }, { continuation.resume(it.status == PixelCopy.SUCCESS) }) + } + } + } +} + +/** + * Uses pixel copy api to do a snapshot, this is accurate but prior to android U we have to use a + * bit of hack to get the surface for root views not associated to an activity (added directly to + * the window manager) + */ @RequiresApi(Build.VERSION_CODES.O) class PixelCopySnapshotter( private val bitmapPool: BitmapPool, @@ -62,29 +86,9 @@ class PixelCopySnapshotter( override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? { - if (view.width <= 0 || view.height <= 0) { - return null + return SnapshotCommon.doSnapshotWithErrorHandling(bitmapPool, view, fallback) { + tryCopyViaActivityWindow(view, it) || tryCopyViaInternalSurface(view, it) } - - val bitmap = bitmapPool.getBitmap(view.width, view.height) - try { - if (tryCopyViaActivityWindow(view, bitmap)) { - // if this view belongs to an activity prefer this as it doesn't require private api hacks - return bitmap - } else if (tryCopyViaInternalSurface(view, bitmap)) { - return bitmap - } - } catch (e: OutOfMemoryError) { - Log.e(LogTag, "OOM when taking snapshot") - } catch (e: Exception) { - // there was some problem with the pixel copy, fall back to canvas impl - Log.e(LogTag, "Exception when taking snapshot", e) - } - - // something went wrong, use fallback - Log.i(LogTag, "Using fallback snapshot method") - bitmap.readyForReuse() - return fallback.takeSnapshot(view) } private suspend fun tryCopyViaActivityWindow( @@ -114,12 +118,44 @@ class PixelCopySnapshotter( } } - private fun pixelCopyCallback(continuation: Continuation) = { result: Int -> - if (result == PixelCopy.SUCCESS) { - continuation.resume(true) - } else { - Log.w(LogTag, "Pixel copy failed, code $result") - continuation.resume(false) + private fun pixelCopyCallback(continuation: Continuation): (Int) -> Unit = + { result: Int -> + if (result == PixelCopy.SUCCESS) { + continuation.resume(true) + } else { + Log.w(LogTag, "Pixel copy failed, code $result") + continuation.resume(false) + } + } +} + +internal object SnapshotCommon { + + internal suspend fun doSnapshotWithErrorHandling( + bitmapPool: BitmapPool, + view: View, + fallback: Snapshotter?, + snapshotStrategy: suspend (reuseableBitmap: BitmapPool.ReusableBitmap) -> Boolean + ): BitmapPool.ReusableBitmap? { + if (view.width <= 0 || view.height <= 0) { + return null } + var reusableBitmap: BitmapPool.ReusableBitmap? = null + try { + reusableBitmap = bitmapPool.getBitmap(view.width, view.height) + if (snapshotStrategy(reusableBitmap)) { + return reusableBitmap + } + } catch (e: OutOfMemoryError) { + Log.e(LogTag, "OOM when taking snapshot") + } catch (e: Exception) { + // there was some problem with the pixel copy, fall back to canvas impl + Log.e(LogTag, "Exception when taking snapshot", e) + } + + // something went wrong, use fallback, make sure to give bitmap back to pool first + Log.i(LogTag, "Using fallback snapshot method") + reusableBitmap?.readyForReuse() + return fallback?.takeSnapshot(view) } } 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 6741b4ef7..32737c736 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 @@ -9,7 +9,9 @@ package com.facebook.flipper.plugins.uidebugger.core import android.app.Application import android.os.Build +import android.util.Log import com.facebook.flipper.core.FlipperConnection +import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.common.BitmapPool import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent @@ -35,15 +37,20 @@ class UIDContext( val bitmapPool = BitmapPool() private val canvasSnapshotter = CanvasSnapshotter(bitmapPool) - val snapshotter = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + private val snapshotter = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ModernPixelCopySnapshotter(bitmapPool, canvasSnapshotter) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PixelCopySnapshotter(bitmapPool, applicationRef, canvasSnapshotter) } else { + Log.w( + LogTag, + "Using legacy snapshot mode, use device with API level >=26 to for pixel copy snapshot ") canvasSnapshotter } - val decorViewTracker = DecorViewTracker(this, snapshotter) - val updateQueue = UpdateQueue(this) + val decorViewTracker: DecorViewTracker = DecorViewTracker(this, snapshotter) + val updateQueue: UpdateQueue = UpdateQueue(this) val layoutTraversal: LayoutTraversal = LayoutTraversal(this) fun addFrameworkEvent(frameworkEvent: FrameworkEvent) {