Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit bd9a863

Browse filesBrowse files
authored
Merge 835d1aa into 1bfeacc
2 parents 1bfeacc + 835d1aa commit bd9a863
Copy full SHA for bd9a863

5 files changed

+82-167Lines changed: 82 additions & 167 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎CHANGELOG.md‎

Copy file name to clipboardExpand all lines: CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix visual artifacts for the Canvas strategy on some devices ([#4861](https://github.com/getsentry/sentry-java/pull/4861))
8+
59
### Improvements
610

711
- Fallback to distinct-id as user.id logging attribute when user is not set ([#4847](https://github.com/getsentry/sentry-java/pull/4847))
Collapse file

‎sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt‎

Copy file name to clipboardExpand all lines: sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt
-18Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -244,21 +244,6 @@ internal class WindowRecorder(
244244
override fun getExecutor(): ScheduledExecutorService = replayExecutor
245245

246246
override fun getMainLooperHandler(): MainLooperHandler = mainLooperHandler
247-
248-
override fun getBackgroundHandler(): Handler {
249-
// only start the background thread if it's actually needed, as it's only used by Canvas Capture
250-
// Strategy
251-
if (backgroundProcessingHandler == null) {
252-
backgroundProcessingHandlerLock.acquire().use {
253-
if (backgroundProcessingHandler == null) {
254-
backgroundProcessingHandlerThread = HandlerThread("SentryReplayBackgroundProcessing")
255-
backgroundProcessingHandlerThread?.start()
256-
backgroundProcessingHandler = Handler(backgroundProcessingHandlerThread!!.looper)
257-
}
258-
}
259-
}
260-
return backgroundProcessingHandler!!
261-
}
262247
}
263248

264249
internal interface ExecutorProvider {
@@ -267,7 +252,4 @@ internal interface ExecutorProvider {
267252

268253
/** Returns a handler associated with the main thread looper. */
269254
fun getMainLooperHandler(): MainLooperHandler
270-
271-
/** Returns a handler associated with a background thread looper. */
272-
fun getBackgroundHandler(): Handler
273255
}
Collapse file

‎sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt‎

Copy file name to clipboardExpand all lines: sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt
+78-143Lines changed: 78 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,34 @@ import android.graphics.NinePatch
1515
import android.graphics.Paint
1616
import android.graphics.Path
1717
import android.graphics.Picture
18-
import android.graphics.PixelFormat
1918
import android.graphics.PorterDuff
2019
import android.graphics.Rect
2120
import android.graphics.RectF
2221
import android.graphics.Region
2322
import android.graphics.RenderNode
23+
import android.graphics.SurfaceTexture
2424
import android.graphics.fonts.Font
2525
import android.graphics.text.MeasuredText
26-
import android.media.ImageReader
2726
import android.os.Build
27+
import android.view.PixelCopy
28+
import android.view.Surface
2829
import android.view.View
2930
import androidx.annotation.RequiresApi
3031
import io.sentry.SentryLevel
32+
import io.sentry.SentryLevel.DEBUG
3133
import io.sentry.SentryOptions
3234
import io.sentry.android.replay.ExecutorProvider
3335
import io.sentry.android.replay.ScreenshotRecorderCallback
3436
import io.sentry.android.replay.ScreenshotRecorderConfig
3537
import io.sentry.android.replay.util.ReplayRunnable
3638
import io.sentry.util.AutoClosableReentrantLock
3739
import io.sentry.util.IntegrationUtils
38-
import java.io.Closeable
3940
import java.util.WeakHashMap
4041
import java.util.concurrent.atomic.AtomicBoolean
4142
import java.util.concurrent.atomic.AtomicReference
4243
import kotlin.LazyThreadSafetyMode.NONE
43-
import kotlin.use
4444

45-
@SuppressLint("UseKtx")
45+
@SuppressLint("NewApi", "UseKtx")
4646
internal 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-
}
Collapse file

‎sentry-android-replay/src/test/java/io/sentry/android/replay/ScreenshotRecorderTest.kt‎

Copy file name to clipboardExpand all lines: sentry-android-replay/src/test/java/io/sentry/android/replay/ScreenshotRecorderTest.kt
-3Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.sentry.android.replay
22

3-
import android.os.Handler
43
import androidx.test.ext.junit.runners.AndroidJUnit4
54
import io.sentry.ScreenshotStrategyType
65
import io.sentry.SentryOptions
@@ -30,8 +29,6 @@ class ScreenshotRecorderTest {
3029
override fun getExecutor(): ScheduledExecutorService = mock<ScheduledExecutorService>()
3130

3231
override fun getMainLooperHandler(): MainLooperHandler = mock<MainLooperHandler>()
33-
34-
override fun getBackgroundHandler(): Handler = mock<Handler>()
3532
},
3633
null,
3734
)
Collapse file

‎sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt‎

Copy file name to clipboardExpand all lines: sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt
-3Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package io.sentry.android.replay.screenshot
22

33
import android.app.Activity
44
import android.os.Bundle
5-
import android.os.Handler
65
import android.os.Looper
76
import android.widget.LinearLayout
87
import android.widget.LinearLayout.LayoutParams
@@ -45,8 +44,6 @@ class PixelCopyStrategyTest {
4544
override fun getExecutor(): ScheduledExecutorService = executor
4645

4746
override fun getMainLooperHandler(): MainLooperHandler = MainLooperHandler()
48-
49-
override fun getBackgroundHandler(): Handler = mock()
5047
},
5148
callback,
5249
options,

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.