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
This commit is contained in:
Luke De Feo
2023-11-02 12:29:07 -07:00
committed by Facebook GitHub Bot
parent 6e64f53046
commit d26612d840
2 changed files with 87 additions and 44 deletions

View File

@@ -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,7 +118,8 @@ class PixelCopySnapshotter(
}
}
private fun pixelCopyCallback(continuation: Continuation<Boolean>) = { result: Int ->
private fun pixelCopyCallback(continuation: Continuation<Boolean>): (Int) -> Unit =
{ result: Int ->
if (result == PixelCopy.SUCCESS) {
continuation.resume(true)
} else {
@@ -123,3 +128,34 @@ class PixelCopySnapshotter(
}
}
}
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)
}
}

View File

@@ -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) {