Skip to content

Commit dfd5da0

Browse files
authored
ref(replay): Use main thread to schedule capture (#4542)
1 parent 6bae0c8 commit dfd5da0

File tree

5 files changed

+229
-133
lines changed

5 files changed

+229
-133
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Improvements
6+
7+
- Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542))
8+
39
## 8.18.0
410

511
### Features

sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt

Lines changed: 91 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,25 @@ internal class ScreenshotRecorder(
6060
private val debugOverlayDrawable = DebugOverlayDrawable()
6161

6262
fun capture() {
63+
if (options.sessionReplay.isDebug) {
64+
options.logger.log(DEBUG, "Capturing screenshot, isCapturing: %s", isCapturing.get())
65+
}
6366
if (!isCapturing.get()) {
6467
if (options.sessionReplay.isDebug) {
6568
options.logger.log(DEBUG, "ScreenshotRecorder is paused, not capturing screenshot")
6669
}
6770
return
6871
}
6972

73+
if (options.sessionReplay.isDebug) {
74+
options.logger.log(
75+
DEBUG,
76+
"Capturing screenshot, contentChanged: %s, lastCaptureSuccessful: %s",
77+
contentChanged.get(),
78+
lastCaptureSuccessful.get(),
79+
)
80+
}
81+
7082
if (!contentChanged.get() && lastCaptureSuccessful.get()) {
7183
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
7284
return
@@ -84,99 +96,95 @@ internal class ScreenshotRecorder(
8496
return
8597
}
8698

87-
// postAtFrontOfQueue to ensure the view hierarchy and bitmap are ase close in-sync as possible
88-
mainLooperHandler.post {
89-
try {
90-
contentChanged.set(false)
91-
PixelCopy.request(
92-
window,
93-
screenshot,
94-
{ copyResult: Int ->
95-
if (copyResult != PixelCopy.SUCCESS) {
96-
options.logger.log(INFO, "Failed to capture replay recording: %d", copyResult)
97-
lastCaptureSuccessful.set(false)
98-
return@request
99-
}
100-
101-
// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times
102-
// in a row, we should capture)
103-
if (contentChanged.get()) {
104-
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
105-
lastCaptureSuccessful.set(false)
106-
return@request
107-
}
99+
try {
100+
contentChanged.set(false)
101+
PixelCopy.request(
102+
window,
103+
screenshot,
104+
{ copyResult: Int ->
105+
if (copyResult != PixelCopy.SUCCESS) {
106+
options.logger.log(INFO, "Failed to capture replay recording: %d", copyResult)
107+
lastCaptureSuccessful.set(false)
108+
return@request
109+
}
110+
111+
// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times
112+
// in a row, we should capture)
113+
if (contentChanged.get()) {
114+
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
115+
lastCaptureSuccessful.set(false)
116+
return@request
117+
}
118+
119+
// TODO: disableAllMasking here and dont traverse?
120+
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
121+
root.traverse(viewHierarchy, options)
122+
123+
recorder.submitSafely(options, "screenshot_recorder.mask") {
124+
val debugMasks = mutableListOf<Rect>()
125+
126+
val canvas = Canvas(screenshot)
127+
canvas.setMatrix(prescaledMatrix)
128+
viewHierarchy.traverse { node ->
129+
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
130+
node.visibleRect ?: return@traverse false
131+
132+
// TODO: investigate why it returns true on RN when it shouldn't
133+
// if (viewHierarchy.isObscured(node)) {
134+
// return@traverse true
135+
// }
136+
137+
val (visibleRects, color) =
138+
when (node) {
139+
is ImageViewHierarchyNode -> {
140+
listOf(node.visibleRect) to screenshot.dominantColorForRect(node.visibleRect)
141+
}
108142

109-
// TODO: disableAllMasking here and dont traverse?
110-
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
111-
root.traverse(viewHierarchy, options)
112-
113-
recorder.submitSafely(options, "screenshot_recorder.mask") {
114-
val debugMasks = mutableListOf<Rect>()
115-
116-
val canvas = Canvas(screenshot)
117-
canvas.setMatrix(prescaledMatrix)
118-
viewHierarchy.traverse { node ->
119-
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
120-
node.visibleRect ?: return@traverse false
121-
122-
// TODO: investigate why it returns true on RN when it shouldn't
123-
// if (viewHierarchy.isObscured(node)) {
124-
// return@traverse true
125-
// }
126-
127-
val (visibleRects, color) =
128-
when (node) {
129-
is ImageViewHierarchyNode -> {
130-
listOf(node.visibleRect) to
131-
screenshot.dominantColorForRect(node.visibleRect)
132-
}
133-
134-
is TextViewHierarchyNode -> {
135-
val textColor =
136-
node.layout?.dominantTextColor ?: node.dominantColor ?: Color.BLACK
137-
node.layout.getVisibleRects(
138-
node.visibleRect,
139-
node.paddingLeft,
140-
node.paddingTop,
141-
) to textColor
142-
}
143-
144-
else -> {
145-
listOf(node.visibleRect) to Color.BLACK
146-
}
143+
is TextViewHierarchyNode -> {
144+
val textColor =
145+
node.layout?.dominantTextColor ?: node.dominantColor ?: Color.BLACK
146+
node.layout.getVisibleRects(
147+
node.visibleRect,
148+
node.paddingLeft,
149+
node.paddingTop,
150+
) to textColor
147151
}
148152

149-
maskingPaint.setColor(color)
150-
visibleRects.forEach { rect ->
151-
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
152-
}
153-
if (options.replayController.isDebugMaskingOverlayEnabled()) {
154-
debugMasks.addAll(visibleRects)
153+
else -> {
154+
listOf(node.visibleRect) to Color.BLACK
155+
}
155156
}
157+
158+
maskingPaint.setColor(color)
159+
visibleRects.forEach { rect ->
160+
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
161+
}
162+
if (options.replayController.isDebugMaskingOverlayEnabled()) {
163+
debugMasks.addAll(visibleRects)
156164
}
157-
return@traverse true
158165
}
166+
return@traverse true
167+
}
159168

160-
if (options.replayController.isDebugMaskingOverlayEnabled()) {
161-
mainLooperHandler.post {
162-
if (debugOverlayDrawable.callback == null) {
163-
root.overlay.add(debugOverlayDrawable)
164-
}
165-
debugOverlayDrawable.updateMasks(debugMasks)
166-
root.postInvalidate()
169+
if (options.replayController.isDebugMaskingOverlayEnabled()) {
170+
mainLooperHandler.post {
171+
if (debugOverlayDrawable.callback == null) {
172+
root.overlay.add(debugOverlayDrawable)
167173
}
174+
debugOverlayDrawable.updateMasks(debugMasks)
175+
root.postInvalidate()
168176
}
169-
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
170-
lastCaptureSuccessful.set(true)
171-
contentChanged.set(false)
172177
}
173-
},
174-
mainLooperHandler.handler,
175-
)
176-
} catch (e: Throwable) {
177-
options.logger.log(WARNING, "Failed to capture replay recording", e)
178-
lastCaptureSuccessful.set(false)
179-
}
178+
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
179+
lastCaptureSuccessful.set(true)
180+
contentChanged.set(false)
181+
}
182+
},
183+
mainLooperHandler.handler,
184+
)
185+
} catch (e: Throwable) {
186+
options.logger.log(WARNING, "Failed to capture replay recording", e)
187+
lastCaptureSuccessful.set(false)
180188
}
181189
}
182190

0 commit comments

Comments
 (0)