@@ -15,34 +15,34 @@ 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
32+ import io.sentry.SentryLevel.DEBUG
3133import io.sentry.SentryOptions
3234import io.sentry.android.replay.ExecutorProvider
3335import io.sentry.android.replay.ScreenshotRecorderCallback
3436import io.sentry.android.replay.ScreenshotRecorderConfig
3537import io.sentry.android.replay.util.ReplayRunnable
3638import io.sentry.util.AutoClosableReentrantLock
3739import io.sentry.util.IntegrationUtils
38- import java.io.Closeable
3940import java.util.WeakHashMap
4041import java.util.concurrent.atomic.AtomicBoolean
4142import java.util.concurrent.atomic.AtomicReference
4243import kotlin.LazyThreadSafetyMode.NONE
43- import kotlin.use
4444
45- @SuppressLint(" UseKtx" )
45+ @SuppressLint(" NewApi " , " UseKtx" )
4646internal class CanvasStrategy (
4747 private val executor : ExecutorProvider ,
4848 private val screenshotRecorderCallback : ScreenshotRecorderCallback ? ,
@@ -51,73 +51,19 @@ internal class CanvasStrategy(
5151) : ScreenshotStrategy {
5252
5353 @Volatile private var screenshot: Bitmap ? = null
54-
55- // Lock to synchronize screenshot creation
54+ private var unprocessedPictureRef = AtomicReference <Picture >(null )
5655 private val screenshotLock = AutoClosableReentrantLock ()
5756 private val prescaledMatrix by
5857 lazy(NONE ) { Matrix ().apply { preScale(config.scaleFactorX, config.scaleFactorY) } }
5958 private val lastCaptureSuccessful = AtomicBoolean (false )
6059 private val textIgnoringCanvas = TextIgnoringDelegateCanvas ()
61-
6260 private val isClosed = AtomicBoolean (false )
6361
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- }
62+ private val surfaceTexture =
63+ SurfaceTexture (false ).apply {
64+ setDefaultBufferSize(config.recordingWidth, config.recordingHeight)
11265 }
113- }
114-
115- private var freePictureRef =
116- AtomicReference (
117- PictureReaderHolder (config.recordingWidth, config.recordingHeight, onImageAvailableListener)
118- )
119-
120- private var unprocessedPictureRef = AtomicReference <PictureReaderHolder >(null )
66+ private val surface = Surface (surfaceTexture)
12167
12268 init {
12369 IntegrationUtils .addIntegrationToSdkVersion(" ReplayCanvasStrategy" )
@@ -126,59 +72,88 @@ internal class CanvasStrategy(
12672 @SuppressLint(" NewApi" )
12773 private val pictureRenderTask = Runnable {
12874 if (isClosed.get()) {
129- options.logger.log(
130- SentryLevel .DEBUG ,
131- " Canvas Strategy already closed, skipping picture render" ,
132- )
75+ options.logger.log(DEBUG , " Canvas Strategy already closed, skipping picture render" )
13376 return @Runnable
13477 }
135- val holder = unprocessedPictureRef.getAndSet(null ) ? : return @Runnable
78+ val picture = unprocessedPictureRef.getAndSet(null ) ? : return @Runnable
13679
13780 try {
138- if (! holder.setup.getAndSet(true )) {
139- holder.reader.setOnImageAvailableListener(holder, executor.getBackgroundHandler())
140- }
141-
142- val surface = holder.reader.surface
143- val canvas = surface.lockHardwareCanvas()
81+ // It's safe to access the surface because the render task,
82+ // as well as surface release are executed on the same single threaded executor
83+ // Draw picture to the Surface for PixelCopy
84+ val surfaceCanvas = surface.lockHardwareCanvas()
14485 try {
145- canvas .drawColor(Color .BLACK , PorterDuff .Mode .CLEAR )
146- holder. picture.draw(canvas )
86+ surfaceCanvas .drawColor(Color .BLACK , PorterDuff .Mode .CLEAR )
87+ picture.draw(surfaceCanvas )
14788 } finally {
148- surface.unlockCanvasAndPost(canvas )
89+ surface.unlockCanvasAndPost(surfaceCanvas )
14990 }
150- } catch (t: Throwable ) {
151- if (isClosed.get()) {
152- holder.close()
153- } else {
154- freePictureRef.set(holder)
91+
92+ if (screenshot == null ) {
93+ screenshotLock.acquire().use {
94+ if (screenshot == null ) {
95+ screenshot = Bitmap .createBitmap(picture.width, picture.height, Bitmap .Config .ARGB_8888 )
96+ }
97+ }
15598 }
99+ PixelCopy .request(
100+ surface,
101+ screenshot!! ,
102+ { result ->
103+ if (isClosed.get()) {
104+ options.logger.log(DEBUG , " CanvasStrategy is closed, ignoring capture result" )
105+ return @request
106+ }
107+ executor
108+ .getExecutor()
109+ .submit(
110+ ReplayRunnable (" screenshot_recorder.mask" ) {
111+ if (isClosed.get()) {
112+ options.logger.log(DEBUG , " CanvasStrategy is closed, ignoring capture result" )
113+ return @ReplayRunnable
114+ }
115+ if (result == PixelCopy .SUCCESS ) {
116+ lastCaptureSuccessful.set(true )
117+ val bitmap = screenshot
118+ if (bitmap != null ) {
119+ synchronized(bitmap) {
120+ if (! bitmap.isRecycled)
121+ screenshotRecorderCallback?.onScreenshotRecorded(bitmap)
122+ }
123+ }
124+ } else {
125+ options.logger.log(
126+ SentryLevel .ERROR ,
127+ " Canvas Strategy: PixelCopy failed with code $result " ,
128+ )
129+ lastCaptureSuccessful.set(false )
130+ }
131+ }
132+ )
133+ },
134+ executor.getMainLooperHandler().handler,
135+ )
136+ } catch (t: Throwable ) {
156137 options.logger.log(SentryLevel .ERROR , " Canvas Strategy: picture render failed" , t)
138+ lastCaptureSuccessful.set(false )
157139 }
158140 }
159141
160- @SuppressLint(" UnclosedTrace " )
142+ @SuppressLint(" NewApi " )
161143 override fun capture (root : View ) {
162144 if (isClosed.get()) {
163145 return
164146 }
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- }
171147
172- val pictureCanvas = holder.picture.beginRecording(config.recordingWidth, config.recordingHeight)
173- textIgnoringCanvas.delegate = pictureCanvas
148+ val picture = Picture ()
149+ val canvas = picture.beginRecording(config.recordingWidth, config.recordingHeight)
150+ textIgnoringCanvas.delegate = canvas
174151 textIgnoringCanvas.setMatrix(prescaledMatrix)
175152 root.draw(textIgnoringCanvas)
176- holder. picture.endRecording()
153+ picture.endRecording()
177154
178- if (isClosed.get()) {
179- holder.close()
180- } else {
181- unprocessedPictureRef.set(holder)
155+ if (! isClosed.get()) {
156+ unprocessedPictureRef.set(picture)
182157 executor.getExecutor().submit(ReplayRunnable (" screenshot_recorder.canvas" , pictureRenderTask))
183158 }
184159 }
@@ -192,28 +167,15 @@ internal class CanvasStrategy(
192167 executor
193168 .getExecutor()
194169 .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- )
170+ ReplayRunnable (" CanvasStrategy.close" ) {
171+ screenshot?.let { synchronized(it) { if (! it.isRecycled) it.recycle() } }
172+ surface.release()
173+ surfaceTexture.release()
174+ }
207175 )
208-
209- // the image can be free, unprocessed or in transit
210- freePictureRef.getAndSet(null )?.reader?.close()
211- unprocessedPictureRef.getAndSet(null )?.reader?.close()
212176 }
213177
214- override fun lastCaptureSuccessful (): Boolean {
215- return lastCaptureSuccessful.get()
216- }
178+ override fun lastCaptureSuccessful (): Boolean = lastCaptureSuccessful.get()
217179
218180 override fun emitLastScreenshot () {
219181 if (lastCaptureSuccessful()) {
@@ -1031,30 +993,3 @@ private class TextIgnoringDelegateCanvas : Canvas() {
1031993 }
1032994 }
1033995}
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