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

View File

@@ -10,7 +10,6 @@ package com.facebook.flipper.plugins.uidebugger.common
import android.graphics.Bitmap import android.graphics.Bitmap
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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. */ /** 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) { 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() { override fun readyForReuse() {
val key = generateKey(bitmap.width, bitmap.height) val key = generateKey(bitmap.width, bitmap.height)
mainScope.launch { synchronized(this@BitmapPool) {
if (isRecycled) { if (isRecycled) {
bitmap.recycle() bitmap.recycle()
} else { } else {

View File

@@ -7,9 +7,15 @@
package com.facebook.flipper.plugins.uidebugger.core package com.facebook.flipper.plugins.uidebugger.core
import android.app.Activity
import android.graphics.Canvas import android.graphics.Canvas
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.PixelCopy
import android.view.View import android.view.View
import androidx.annotation.RequiresApi
import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.common.BitmapPool 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<View, Activity> =
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
}
}

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.uidebugger.core package com.facebook.flipper.plugins.uidebugger.core
import android.app.Application import android.app.Application
import android.os.Build
import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.core.FlipperConnection
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
@@ -32,7 +33,16 @@ class UIDContext(
) { ) {
val bitmapPool = BitmapPool() 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 updateQueue = UpdateQueue(this)
val layoutTraversal: LayoutTraversal = LayoutTraversal(this) val layoutTraversal: LayoutTraversal = LayoutTraversal(this)

View File

@@ -44,7 +44,7 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
val activeRoots = node.rootsResolver.rootViews() val activeRoots = node.rootsResolver.rootViews()
val decorViewToActivity: Map<View, Activity> = val decorViewToActivity: Map<View, Activity> =
node.activitiesStack.toList().map { it.window.decorView to it }.toMap() node.activitiesStack.toList().associateBy { it.window.decorView }
for (root in activeRoots) { for (root in activeRoots) {
// if there is an activity for this root view use that, // if there is an activity for this root view use that,