Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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.16.1-alpha.2

### Fixes
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