@@ -15,16 +15,17 @@ import android.graphics.NinePatch
1515import android.graphics.Paint
1616import android.graphics.Path
1717import android.graphics.Picture
18- import android.graphics.PixelFormat
1918import android.graphics.PorterDuff
2019import android.graphics.Rect
2120import android.graphics.RectF
2221import android.graphics.Region
2322import android.graphics.RenderNode
23+ import android.graphics.SurfaceTexture
2424import android.graphics.fonts.Font
2525import android.graphics.text.MeasuredText
26- import android.media.ImageReader
2726import android.os.Build
27+ import android.view.PixelCopy
28+ import android.view.Surface
2829import android.view.View
2930import androidx.annotation.RequiresApi
3031import io.sentry.SentryLevel
@@ -35,14 +36,12 @@ import io.sentry.android.replay.ScreenshotRecorderConfig
3536import io.sentry.android.replay.util.ReplayRunnable
3637import io.sentry.util.AutoClosableReentrantLock
3738import io.sentry.util.IntegrationUtils
38- import java.io.Closeable
3939import java.util.WeakHashMap
4040import java.util.concurrent.atomic.AtomicBoolean
4141import java.util.concurrent.atomic.AtomicReference
4242import kotlin.LazyThreadSafetyMode.NONE
43- import kotlin.use
4443
45- @SuppressLint(" UseKtx" )
44+ @SuppressLint(" NewApi " , " UseKtx" )
4645internal class CanvasStrategy (
4746 private val executor : ExecutorProvider ,
4847 private val screenshotRecorderCallback : ScreenshotRecorderCallback ? ,
@@ -51,73 +50,19 @@ internal class CanvasStrategy(
5150) : ScreenshotStrategy {
5251
5352 @Volatile private var screenshot: Bitmap ? = null
54-
55- // Lock to synchronize screenshot creation
53+ private var unprocessedPictureRef = AtomicReference <Picture >(null )
5654 private val screenshotLock = AutoClosableReentrantLock ()
5755 private val prescaledMatrix by
5856 lazy(NONE ) { Matrix ().apply { preScale(config.scaleFactorX, config.scaleFactorY) } }
5957 private val lastCaptureSuccessful = AtomicBoolean (false )
6058 private val textIgnoringCanvas = TextIgnoringDelegateCanvas ()
61-
6259 private val isClosed = AtomicBoolean (false )
6360
64- private val onImageAvailableListener: (holder: PictureReaderHolder ) -> Unit = { holder ->
65- if (isClosed.get()) {
66- options.logger.log(SentryLevel .ERROR , " CanvasStrategy already closed, skipping image" )
67- holder.close()
68- } else {
69- try {
70- val image = holder.reader.acquireLatestImage()
71- try {
72- if (image.planes.size > 0 ) {
73- val plane = image.planes[0 ]
74-
75- if (screenshot == null ) {
76- screenshotLock.acquire().use {
77- if (screenshot == null ) {
78- screenshot =
79- Bitmap .createBitmap(holder.width, holder.height, Bitmap .Config .ARGB_8888 )
80- }
81- }
82- }
83-
84- val bitmap = screenshot
85- if (bitmap != null ) {
86- val buffer = plane.buffer.rewind()
87- synchronized(bitmap) {
88- if (! bitmap.isRecycled) {
89- bitmap.copyPixelsFromBuffer(buffer)
90- lastCaptureSuccessful.set(true )
91- }
92- }
93- screenshotRecorderCallback?.onScreenshotRecorded(bitmap)
94- }
95- }
96- } finally {
97- try {
98- image.close()
99- } catch (_: Throwable ) {
100- // ignored
101- }
102- }
103- } catch (e: Throwable ) {
104- options.logger.log(SentryLevel .ERROR , " CanvasStrategy: image processing failed" , e)
105- } finally {
106- if (isClosed.get()) {
107- holder.close()
108- } else {
109- freePictureRef.set(holder)
110- }
111- }
61+ private val surfaceTexture =
62+ SurfaceTexture (false ).apply {
63+ setDefaultBufferSize(config.recordingWidth, config.recordingHeight)
11264 }
113- }
114-
115- private var freePictureRef =
116- AtomicReference (
117- PictureReaderHolder (config.recordingWidth, config.recordingHeight, onImageAvailableListener)
118- )
119-
120- private var unprocessedPictureRef = AtomicReference <PictureReaderHolder >(null )
65+ private val surface = Surface (surfaceTexture)
12166
12267 init {
12368 IntegrationUtils .addIntegrationToSdkVersion(" ReplayCanvasStrategy" )
@@ -132,54 +77,64 @@ internal class CanvasStrategy(
13277 )
13378 return @Runnable
13479 }
135- val holder = unprocessedPictureRef.getAndSet(null ) ? : return @Runnable
80+ val picture = unprocessedPictureRef.getAndSet(null ) ? : return @Runnable
13681
82+ // Draw picture to the Surface for PixelCopy
83+ val surfaceCanvas = surface.lockHardwareCanvas()
13784 try {
138- if (! holder.setup.getAndSet(true )) {
139- holder.reader.setOnImageAvailableListener(holder, executor.getBackgroundHandler())
140- }
85+ surfaceCanvas.drawColor(Color .BLACK , PorterDuff .Mode .CLEAR )
86+ picture.draw(surfaceCanvas)
87+ } finally {
88+ surface.unlockCanvasAndPost(surfaceCanvas)
89+ }
14190
142- val surface = holder.reader.surface
143- val canvas = surface.lockHardwareCanvas()
144- try {
145- canvas.drawColor(Color .BLACK , PorterDuff .Mode .CLEAR )
146- holder.picture.draw(canvas)
147- } finally {
148- surface.unlockCanvasAndPost(canvas)
149- }
150- } catch (t: Throwable ) {
151- if (isClosed.get()) {
152- holder.close()
153- } else {
154- freePictureRef.set(holder)
91+ if (screenshot == null ) {
92+ screenshotLock.acquire().use {
93+ if (screenshot == null ) {
94+ screenshot = Bitmap .createBitmap(picture.width, picture.height, Bitmap .Config .ARGB_8888 )
95+ }
15596 }
156- options.logger.log(SentryLevel .ERROR , " Canvas Strategy: picture render failed" , t)
15797 }
98+
99+ // Trigger PixelCopy capture
100+ PixelCopy .request(
101+ surface,
102+ screenshot!! ,
103+ { result ->
104+ if (result == PixelCopy .SUCCESS ) {
105+ lastCaptureSuccessful.set(true )
106+ val bitmap = screenshot
107+ if (bitmap != null && ! bitmap.isRecycled) {
108+ screenshotRecorderCallback?.onScreenshotRecorded(bitmap)
109+ }
110+ } else {
111+ options.logger.log(SentryLevel .ERROR , " PixelCopy failed with code $result " )
112+ lastCaptureSuccessful.set(false )
113+ }
114+ },
115+ executor.getBackgroundHandler(),
116+ )
158117 }
159118
160- @SuppressLint(" UnclosedTrace " )
119+ @SuppressLint(" NewApi " )
161120 override fun capture (root : View ) {
162121 if (isClosed.get()) {
163122 return
164123 }
165- val holder = freePictureRef.getAndSet(null )
166- if (holder == null ) {
167- options.logger.log(SentryLevel .DEBUG , " No free Picture available, skipping capture" )
168- lastCaptureSuccessful.set(false )
169- return
170- }
171124
172- val pictureCanvas = holder.picture.beginRecording(config.recordingWidth, config.recordingHeight)
173- textIgnoringCanvas.delegate = pictureCanvas
125+ val picture = Picture ()
126+ val canvas = picture.beginRecording(config.recordingWidth, config.recordingHeight)
127+ textIgnoringCanvas.delegate = canvas
174128 textIgnoringCanvas.setMatrix(prescaledMatrix)
175129 root.draw(textIgnoringCanvas)
176- holder.picture.endRecording()
177-
178- if (isClosed.get()) {
179- holder.close()
180- } else {
181- unprocessedPictureRef.set(holder)
182- executor.getExecutor().submit(ReplayRunnable (" screenshot_recorder.canvas" , pictureRenderTask))
130+ picture.endRecording()
131+
132+ if (! isClosed.get()) {
133+ unprocessedPictureRef.set(picture)
134+ // use the same handler for PixelCopy and pictureRenderTask
135+ executor
136+ .getBackgroundHandler()
137+ .post(ReplayRunnable (" screenshot_recorder.canvas" , pictureRenderTask))
183138 }
184139 }
185140
@@ -192,28 +147,15 @@ internal class CanvasStrategy(
192147 executor
193148 .getExecutor()
194149 .submit(
195- ReplayRunnable (
196- " CanvasStrategy.close" ,
197- {
198- screenshot?.let {
199- synchronized(it) {
200- if (! it.isRecycled) {
201- it.recycle()
202- }
203- }
204- }
205- },
206- )
150+ ReplayRunnable (" CanvasStrategy.close" ) {
151+ screenshot?.let { synchronized(it) { if (! it.isRecycled) it.recycle() } }
152+ surface.release()
153+ surfaceTexture.release()
154+ }
207155 )
208-
209- // the image can be free, unprocessed or in transit
210- freePictureRef.getAndSet(null )?.reader?.close()
211- unprocessedPictureRef.getAndSet(null )?.reader?.close()
212156 }
213157
214- override fun lastCaptureSuccessful (): Boolean {
215- return lastCaptureSuccessful.get()
216- }
158+ override fun lastCaptureSuccessful (): Boolean = lastCaptureSuccessful.get()
217159
218160 override fun emitLastScreenshot () {
219161 if (lastCaptureSuccessful()) {
@@ -1031,30 +973,3 @@ private class TextIgnoringDelegateCanvas : Canvas() {
1031973 }
1032974 }
1033975}
1034-
1035- private class PictureReaderHolder (
1036- val width : Int ,
1037- val height : Int ,
1038- val listener : (holder: PictureReaderHolder ) -> Unit ,
1039- ) : ImageReader.OnImageAvailableListener, Closeable {
1040- val picture = Picture ()
1041-
1042- @SuppressLint(" InlinedApi" )
1043- val reader: ImageReader = ImageReader .newInstance(width, height, PixelFormat .RGBA_8888 , 1 )
1044-
1045- var setup = AtomicBoolean (false )
1046-
1047- override fun onImageAvailable (reader : ImageReader ? ) {
1048- if (reader != null ) {
1049- listener(this )
1050- }
1051- }
1052-
1053- override fun close () {
1054- try {
1055- reader.close()
1056- } catch (_: Throwable ) {
1057- // ignored
1058- }
1059- }
1060- }
0 commit comments