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