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:
committed by
Facebook GitHub Bot
parent
6e64f53046
commit
d26612d840
@@ -11,7 +11,6 @@ import android.app.Activity
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.PixelCopy
|
import android.view.PixelCopy
|
||||||
@@ -36,22 +35,47 @@ interface Snapshotter {
|
|||||||
class CanvasSnapshotter(private val bitmapPool: BitmapPool) : Snapshotter {
|
class CanvasSnapshotter(private val bitmapPool: BitmapPool) : Snapshotter {
|
||||||
override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? {
|
override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? {
|
||||||
|
|
||||||
if (view.width <= 0 || view.height <= 0) {
|
return SnapshotCommon.doSnapshotWithErrorHandling(bitmapPool, view, fallback = null) { bitmap ->
|
||||||
return null
|
val canvas = Canvas(bitmap.bitmap)
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val reuseAbleBitmap = bitmapPool.getBitmap(view.width, view.height)
|
|
||||||
val canvas = Canvas(reuseAbleBitmap.bitmap)
|
|
||||||
view.draw(canvas)
|
view.draw(canvas)
|
||||||
reuseAbleBitmap
|
true
|
||||||
} catch (e: OutOfMemoryError) {
|
|
||||||
Log.e(LogTag, "OOM when taking snapshot")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class PixelCopySnapshotter(
|
class PixelCopySnapshotter(
|
||||||
private val bitmapPool: BitmapPool,
|
private val bitmapPool: BitmapPool,
|
||||||
@@ -62,29 +86,9 @@ class PixelCopySnapshotter(
|
|||||||
|
|
||||||
override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? {
|
override suspend fun takeSnapshot(view: View): BitmapPool.ReusableBitmap? {
|
||||||
|
|
||||||
if (view.width <= 0 || view.height <= 0) {
|
return SnapshotCommon.doSnapshotWithErrorHandling(bitmapPool, view, fallback) {
|
||||||
return null
|
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(
|
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) {
|
if (result == PixelCopy.SUCCESS) {
|
||||||
continuation.resume(true)
|
continuation.resume(true)
|
||||||
} else {
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ package com.facebook.flipper.plugins.uidebugger.core
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import com.facebook.flipper.core.FlipperConnection
|
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.common.BitmapPool
|
||||||
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
|
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
|
||||||
import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent
|
import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent
|
||||||
@@ -35,15 +37,20 @@ class UIDContext(
|
|||||||
val bitmapPool = BitmapPool()
|
val bitmapPool = BitmapPool()
|
||||||
private val canvasSnapshotter = CanvasSnapshotter(bitmapPool)
|
private val canvasSnapshotter = CanvasSnapshotter(bitmapPool)
|
||||||
|
|
||||||
val snapshotter =
|
private val snapshotter =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
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)
|
PixelCopySnapshotter(bitmapPool, applicationRef, canvasSnapshotter)
|
||||||
} else {
|
} else {
|
||||||
|
Log.w(
|
||||||
|
LogTag,
|
||||||
|
"Using legacy snapshot mode, use device with API level >=26 to for pixel copy snapshot ")
|
||||||
canvasSnapshotter
|
canvasSnapshotter
|
||||||
}
|
}
|
||||||
|
|
||||||
val decorViewTracker = DecorViewTracker(this, snapshotter)
|
val decorViewTracker: DecorViewTracker = DecorViewTracker(this, snapshotter)
|
||||||
val updateQueue = UpdateQueue(this)
|
val updateQueue: UpdateQueue = UpdateQueue(this)
|
||||||
val layoutTraversal: LayoutTraversal = LayoutTraversal(this)
|
val layoutTraversal: LayoutTraversal = LayoutTraversal(this)
|
||||||
|
|
||||||
fun addFrameworkEvent(frameworkEvent: FrameworkEvent) {
|
fun addFrameworkEvent(frameworkEvent: FrameworkEvent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user