@@ -18,8 +18,6 @@ import android.view.PixelCopy
1818import android.view.Window
1919import com.facebook.proguard.annotations.DoNotStripAny
2020import java.io.ByteArrayOutputStream
21- import kotlin.coroutines.resume
22- import kotlin.coroutines.suspendCoroutine
2321import kotlinx.coroutines.CoroutineScope
2422import kotlinx.coroutines.Dispatchers
2523import kotlinx.coroutines.launch
@@ -31,7 +29,7 @@ internal class FrameTimingsObserver(
3129) {
3230 private val isSupported = Build .VERSION .SDK_INT >= Build .VERSION_CODES .N
3331
34- private val handler = Handler (Looper .getMainLooper())
32+ private val mainHandler = Handler (Looper .getMainLooper())
3533 private var frameCounter: Int = 0
3634 private var bitmapBuffer: Bitmap ? = null
3735 private var isStarted: Boolean = false
@@ -51,7 +49,7 @@ internal class FrameTimingsObserver(
5149 val timestamp = System .nanoTime()
5250 emitFrameTiming(timestamp, timestamp)
5351
54- currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, handler )
52+ currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler )
5553 }
5654
5755 fun stop () {
@@ -62,7 +60,7 @@ internal class FrameTimingsObserver(
6260 isStarted = false
6361
6462 currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener)
65- handler .removeCallbacksAndMessages(null )
63+ mainHandler .removeCallbacksAndMessages(null )
6664
6765 bitmapBuffer?.recycle()
6866 bitmapBuffer = null
@@ -76,7 +74,7 @@ internal class FrameTimingsObserver(
7674 currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener)
7775 currentWindow = window
7876 if (isStarted) {
79- currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, handler )
77+ currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler )
8078 }
8179 }
8280
@@ -91,31 +89,36 @@ internal class FrameTimingsObserver(
9189 val frameId = frameCounter++
9290 val threadId = Process .myTid()
9391
94- CoroutineScope (Dispatchers .Default ).launch {
95- val screenshot = if (screenshotsEnabled) captureScreenshot() else null
96-
97- onFrameTimingSequence(
98- FrameTimingSequence (
99- frameId,
100- threadId,
101- beginTimestamp,
102- endTimestamp,
103- screenshot,
92+ if (screenshotsEnabled) {
93+ // Initiate PixelCopy immediately on the main thread, while still in the current frame,
94+ // then process and emit asynchronously once the copy is complete.
95+ captureScreenshot { screenshot ->
96+ CoroutineScope (Dispatchers .Default ).launch {
97+ onFrameTimingSequence(
98+ FrameTimingSequence (frameId, threadId, beginTimestamp, endTimestamp, screenshot)
10499 )
105- )
100+ }
101+ }
102+ } else {
103+ CoroutineScope (Dispatchers .Default ).launch {
104+ onFrameTimingSequence(
105+ FrameTimingSequence (frameId, threadId, beginTimestamp, endTimestamp, null )
106+ )
107+ }
106108 }
107109 }
108110
109- private suspend fun captureScreenshot (): String? = suspendCoroutine { continuation ->
111+ // Must be called from the main thread so that PixelCopy captures the current frame.
112+ private fun captureScreenshot (callback : (String? ) -> Unit ) {
110113 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .O ) {
111- continuation.resume (null )
112- return @suspendCoroutine
114+ callback (null )
115+ return
113116 }
114117
115118 val window = currentWindow
116119 if (window == null ) {
117- continuation.resume (null )
118- return @suspendCoroutine
120+ callback (null )
121+ return
119122 }
120123
121124 val decorView = window.decorView
@@ -136,39 +139,39 @@ internal class FrameTimingsObserver(
136139 { copyResult ->
137140 if (copyResult == PixelCopy .SUCCESS ) {
138141 CoroutineScope (Dispatchers .Default ).launch {
139- var scaledBitmap: Bitmap ? = null
140- try {
141- val density = window.context.resources.displayMetrics.density
142- val scaledWidth = (width / density * SCREENSHOT_SCALE_FACTOR ).toInt()
143- val scaledHeight = (height / density * SCREENSHOT_SCALE_FACTOR ).toInt()
144- scaledBitmap = Bitmap .createScaledBitmap(bitmap, scaledWidth, scaledHeight, true )
145-
146- val compressFormat =
147- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R )
148- Bitmap .CompressFormat .WEBP_LOSSY
149- else Bitmap .CompressFormat .JPEG
150-
151- val base64 =
152- ByteArrayOutputStream ().use { outputStream ->
153- scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY , outputStream)
154- Base64 .encodeToString(outputStream.toByteArray(), Base64 .NO_WRAP )
155- }
156-
157- continuation.resume(base64)
158- } catch (e: Exception ) {
159- continuation.resume(null )
160- } finally {
161- scaledBitmap?.recycle()
162- }
142+ callback(encodeScreenshot(window, bitmap, width, height))
163143 }
164144 } else {
165- continuation.resume (null )
145+ callback (null )
166146 }
167147 },
168- handler ,
148+ mainHandler ,
169149 )
170150 }
171151
152+ private fun encodeScreenshot (window : Window , bitmap : Bitmap , width : Int , height : Int ): String? {
153+ var scaledBitmap: Bitmap ? = null
154+ return try {
155+ val density = window.context.resources.displayMetrics.density
156+ val scaledWidth = (width / density * SCREENSHOT_SCALE_FACTOR ).toInt()
157+ val scaledHeight = (height / density * SCREENSHOT_SCALE_FACTOR ).toInt()
158+ scaledBitmap = Bitmap .createScaledBitmap(bitmap, scaledWidth, scaledHeight, true )
159+
160+ val compressFormat =
161+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) Bitmap .CompressFormat .WEBP_LOSSY
162+ else Bitmap .CompressFormat .JPEG
163+
164+ ByteArrayOutputStream ().use { outputStream ->
165+ scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY , outputStream)
166+ Base64 .encodeToString(outputStream.toByteArray(), Base64 .NO_WRAP )
167+ }
168+ } catch (e: Exception ) {
169+ null
170+ } finally {
171+ scaledBitmap?.recycle()
172+ }
173+ }
174+
172175 companion object {
173176 private const val SCREENSHOT_SCALE_FACTOR = 0.75f
174177 private const val SCREENSHOT_QUALITY = 80
0 commit comments