Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Improvements

- Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542))

## 8.18.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,25 @@ internal class ScreenshotRecorder(
private val debugOverlayDrawable = DebugOverlayDrawable()

fun capture() {
if (options.sessionReplay.isDebug) {
options.logger.log(DEBUG, "Capturing screenshot, isCapturing: %s", isCapturing.get())
}
if (!isCapturing.get()) {
if (options.sessionReplay.isDebug) {
options.logger.log(DEBUG, "ScreenshotRecorder is paused, not capturing screenshot")
}
return
}

if (options.sessionReplay.isDebug) {
options.logger.log(
DEBUG,
"Capturing screenshot, contentChanged: %s, lastCaptureSuccessful: %s",
contentChanged.get(),
lastCaptureSuccessful.get(),
)
}

if (!contentChanged.get() && lastCaptureSuccessful.get()) {
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
return
Expand All @@ -84,99 +96,95 @@ internal class ScreenshotRecorder(
return
}

// postAtFrontOfQueue to ensure the view hierarchy and bitmap are ase close in-sync as possible
mainLooperHandler.post {
try {
contentChanged.set(false)
PixelCopy.request(
window,
screenshot,
{ copyResult: Int ->
if (copyResult != PixelCopy.SUCCESS) {
options.logger.log(INFO, "Failed to capture replay recording: %d", copyResult)
lastCaptureSuccessful.set(false)
return@request
}

// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times
// in a row, we should capture)
if (contentChanged.get()) {
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
lastCaptureSuccessful.set(false)
return@request
}
try {
contentChanged.set(false)
PixelCopy.request(
window,
screenshot,
{ copyResult: Int ->
if (copyResult != PixelCopy.SUCCESS) {
options.logger.log(INFO, "Failed to capture replay recording: %d", copyResult)
lastCaptureSuccessful.set(false)
return@request
}

// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times
// in a row, we should capture)
if (contentChanged.get()) {
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
lastCaptureSuccessful.set(false)
return@request
}

// TODO: disableAllMasking here and dont traverse?
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
root.traverse(viewHierarchy, options)

recorder.submitSafely(options, "screenshot_recorder.mask") {
val debugMasks = mutableListOf<Rect>()

val canvas = Canvas(screenshot)
canvas.setMatrix(prescaledMatrix)
viewHierarchy.traverse { node ->
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
// if (viewHierarchy.isObscured(node)) {
// return@traverse true
// }

val (visibleRects, color) =
when (node) {
is ImageViewHierarchyNode -> {
listOf(node.visibleRect) to screenshot.dominantColorForRect(node.visibleRect)
}

// TODO: disableAllMasking here and dont traverse?
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
root.traverse(viewHierarchy, options)

recorder.submitSafely(options, "screenshot_recorder.mask") {
val debugMasks = mutableListOf<Rect>()

val canvas = Canvas(screenshot)
canvas.setMatrix(prescaledMatrix)
viewHierarchy.traverse { node ->
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
// if (viewHierarchy.isObscured(node)) {
// return@traverse true
// }

val (visibleRects, color) =
when (node) {
is ImageViewHierarchyNode -> {
listOf(node.visibleRect) to
screenshot.dominantColorForRect(node.visibleRect)
}

is TextViewHierarchyNode -> {
val textColor =
node.layout?.dominantTextColor ?: node.dominantColor ?: Color.BLACK
node.layout.getVisibleRects(
node.visibleRect,
node.paddingLeft,
node.paddingTop,
) to textColor
}

else -> {
listOf(node.visibleRect) to Color.BLACK
}
is TextViewHierarchyNode -> {
val textColor =
node.layout?.dominantTextColor ?: node.dominantColor ?: Color.BLACK
node.layout.getVisibleRects(
node.visibleRect,
node.paddingLeft,
node.paddingTop,
) to textColor
}

maskingPaint.setColor(color)
visibleRects.forEach { rect ->
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
}
if (options.replayController.isDebugMaskingOverlayEnabled()) {
debugMasks.addAll(visibleRects)
else -> {
listOf(node.visibleRect) to Color.BLACK
}
}

maskingPaint.setColor(color)
visibleRects.forEach { rect ->
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
}
if (options.replayController.isDebugMaskingOverlayEnabled()) {
debugMasks.addAll(visibleRects)
}
return@traverse true
}
return@traverse true
}

if (options.replayController.isDebugMaskingOverlayEnabled()) {
mainLooperHandler.post {
if (debugOverlayDrawable.callback == null) {
root.overlay.add(debugOverlayDrawable)
}
debugOverlayDrawable.updateMasks(debugMasks)
root.postInvalidate()
if (options.replayController.isDebugMaskingOverlayEnabled()) {
mainLooperHandler.post {
if (debugOverlayDrawable.callback == null) {
root.overlay.add(debugOverlayDrawable)
}
debugOverlayDrawable.updateMasks(debugMasks)
root.postInvalidate()
}
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
lastCaptureSuccessful.set(true)
contentChanged.set(false)
}
},
mainLooperHandler.handler,
)
} catch (e: Throwable) {
options.logger.log(WARNING, "Failed to capture replay recording", e)
lastCaptureSuccessful.set(false)
}
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
lastCaptureSuccessful.set(true)
contentChanged.set(false)
}
},
mainLooperHandler.handler,
)
} catch (e: Throwable) {
options.logger.log(WARNING, "Failed to capture replay recording", e)
lastCaptureSuccessful.set(false)
}
}

Expand Down
Loading
Loading