77
88package com.facebook.react.devsupport.inspector
99
10+ import android.graphics.Bitmap
1011import android.os.Build
1112import android.os.Handler
1213import android.os.Looper
1314import android.os.Process
15+ import android.util.Base64
1416import android.view.FrameMetrics
17+ import android.view.PixelCopy
1518import android.view.Window
1619import com.facebook.proguard.annotations.DoNotStripAny
20+ import java.io.ByteArrayOutputStream
21+ import kotlin.coroutines.resume
22+ import kotlin.coroutines.suspendCoroutine
23+ import kotlinx.coroutines.CoroutineScope
24+ import kotlinx.coroutines.Dispatchers
25+ import kotlinx.coroutines.launch
1726
1827@DoNotStripAny
1928internal class FrameTimingsObserver (
2029 private val window : Window ,
21- onFrameTimingSequence : (sequence: FrameTimingSequence ) -> Unit ,
30+ private val screenshotsEnabled : Boolean ,
31+ private val onFrameTimingSequence : (sequence: FrameTimingSequence ) -> Unit ,
2232) {
2333 private val handler = Handler (Looper .getMainLooper())
2434 private var frameCounter: Int = 0
35+ private var bitmapBuffer: Bitmap ? = null
2536
2637 private val frameMetricsListener =
2738 Window .OnFrameMetricsAvailableListener { _, frameMetrics, _dropCount ->
@@ -35,17 +46,77 @@ internal class FrameTimingsObserver(
3546 val endDrawingTimestamp =
3647 beginDrawingTimestamp + frameMetrics.getMetric(FrameMetrics .TOTAL_DURATION )
3748
38- onFrameTimingSequence(
39- FrameTimingSequence (
40- frameCounter++ ,
41- Process .myTid(),
42- beginDrawingTimestamp,
43- commitTimestamp,
44- endDrawingTimestamp,
45- )
46- )
49+ val frameId = frameCounter++
50+ val threadId = Process .myTid()
51+
52+ CoroutineScope (Dispatchers .Default ).launch {
53+ val screenshot = if (screenshotsEnabled) captureScreenshot() else null
54+
55+ onFrameTimingSequence(
56+ FrameTimingSequence (
57+ frameId,
58+ threadId,
59+ beginDrawingTimestamp,
60+ commitTimestamp,
61+ endDrawingTimestamp,
62+ screenshot,
63+ )
64+ )
65+ }
4766 }
4867
68+ private suspend fun captureScreenshot (): String? = suspendCoroutine { continuation ->
69+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .O ) {
70+ continuation.resume(null )
71+ return @suspendCoroutine
72+ }
73+
74+ val decorView = window.decorView
75+ val width = decorView.width
76+ val height = decorView.height
77+
78+ // Reuse bitmap if dimensions haven't changed
79+ val bitmap =
80+ bitmapBuffer?.let {
81+ if (it.width == width && it.height == height) {
82+ it
83+ } else {
84+ null
85+ }
86+ } ? : Bitmap .createBitmap(width, height, Bitmap .Config .ARGB_8888 ).also { bitmapBuffer = it }
87+
88+ PixelCopy .request(
89+ window,
90+ bitmap,
91+ { copyResult ->
92+ if (copyResult == PixelCopy .SUCCESS ) {
93+ CoroutineScope (Dispatchers .Default ).launch {
94+ try {
95+ val scaleFactor = 0.5f
96+ val scaledWidth = (width * scaleFactor).toInt()
97+ val scaledHeight = (height * scaleFactor).toInt()
98+ val scaledBitmap =
99+ Bitmap .createScaledBitmap(bitmap, scaledWidth, scaledHeight, true )
100+
101+ val outputStream = ByteArrayOutputStream ()
102+ scaledBitmap.compress(Bitmap .CompressFormat .JPEG , 80 , outputStream)
103+ val jpegBytes = outputStream.toByteArray()
104+ val jpegBase64 = Base64 .encodeToString(jpegBytes, Base64 .NO_WRAP )
105+ continuation.resume(jpegBase64)
106+
107+ scaledBitmap.recycle()
108+ } catch (e: Exception ) {
109+ continuation.resume(null )
110+ }
111+ }
112+ } else {
113+ continuation.resume(null )
114+ }
115+ },
116+ handler,
117+ )
118+ }
119+
49120 fun start () {
50121 frameCounter = 0
51122 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .N ) {
@@ -62,5 +133,8 @@ internal class FrameTimingsObserver(
62133
63134 window.removeOnFrameMetricsAvailableListener(frameMetricsListener)
64135 handler.removeCallbacksAndMessages(null )
136+
137+ bitmapBuffer?.recycle()
138+ bitmapBuffer = null
65139 }
66140}
0 commit comments