Skip to content

Commit d1a9c45

Browse files
committed
Add masking debug overlay
1 parent b779765 commit d1a9c45

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.sentry.android.replay
22

3+
import android.annotation.SuppressLint
34
import android.annotation.TargetApi
45
import android.content.Context
56
import android.graphics.Bitmap
@@ -21,6 +22,7 @@ import io.sentry.SentryLevel.INFO
2122
import io.sentry.SentryLevel.WARNING
2223
import io.sentry.SentryOptions
2324
import io.sentry.SentryReplayOptions
25+
import io.sentry.android.replay.util.DebugOverlayDrawable
2426
import io.sentry.android.replay.util.MainLooperHandler
2527
import io.sentry.android.replay.util.addOnDrawListenerSafe
2628
import io.sentry.android.replay.util.getVisibleRects
@@ -37,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean
3739
import kotlin.LazyThreadSafetyMode.NONE
3840
import kotlin.math.roundToInt
3941

42+
@SuppressLint("UseKtx")
4043
@TargetApi(26)
4144
internal class ScreenshotRecorder(
4245
val config: ScreenshotRecorderConfig,
@@ -46,6 +49,10 @@ internal class ScreenshotRecorder(
4649
private val screenshotRecorderCallback: ScreenshotRecorderCallback?
4750
) : ViewTreeObserver.OnDrawListener {
4851

52+
private companion object {
53+
private const val DEBUG_MODE = false
54+
}
55+
4956
private var rootView: WeakReference<View>? = null
5057
private val maskingPaint by lazy(NONE) { Paint() }
5158
private val singlePixelBitmap: Bitmap by lazy(NONE) {
@@ -70,6 +77,8 @@ internal class ScreenshotRecorder(
7077
private val isCapturing = AtomicBoolean(true)
7178
private val lastCaptureSuccessful = AtomicBoolean(false)
7279

80+
private val debugOverlayDrawable = DebugOverlayDrawable()
81+
7382
fun capture() {
7483
if (!isCapturing.get()) {
7584
if (options.sessionReplay.isDebug) {
@@ -121,6 +130,8 @@ internal class ScreenshotRecorder(
121130
root.traverse(viewHierarchy, options)
122131

123132
recorder.submitSafely(options, "screenshot_recorder.mask") {
133+
val debugMasks = mutableListOf<Rect>()
134+
124135
val canvas = Canvas(screenshot)
125136
canvas.setMatrix(prescaledMatrix)
126137
viewHierarchy.traverse { node ->
@@ -158,10 +169,16 @@ internal class ScreenshotRecorder(
158169
visibleRects.forEach { rect ->
159170
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
160171
}
172+
if (DEBUG_MODE) {
173+
debugMasks.addAll(visibleRects)
174+
}
161175
}
162176
return@traverse true
163177
}
164178

179+
if (DEBUG_MODE) {
180+
mainLooperHandler.post { debugOverlayDrawable.update(debugMasks) }
181+
}
165182
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
166183
lastCaptureSuccessful.set(true)
167184
contentChanged.set(false)
@@ -194,11 +211,18 @@ internal class ScreenshotRecorder(
194211
// next bind the new root
195212
rootView = WeakReference(root)
196213
root.addOnDrawListenerSafe(this)
214+
if (DEBUG_MODE) {
215+
root.overlay.add(debugOverlayDrawable)
216+
}
217+
197218
// invalidate the flag to capture the first frame after new window is attached
198219
contentChanged.set(true)
199220
}
200221

201222
fun unbind(root: View?) {
223+
if (DEBUG_MODE) {
224+
root?.overlay?.remove(debugOverlayDrawable)
225+
}
202226
root?.removeOnDrawListenerSafe(this)
203227
}
204228

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.sentry.android.replay.util
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Color
5+
import android.graphics.ColorFilter
6+
import android.graphics.Paint
7+
import android.graphics.PixelFormat
8+
import android.graphics.Rect
9+
import android.graphics.drawable.Drawable
10+
11+
internal class DebugOverlayDrawable : Drawable() {
12+
13+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
14+
private val padding = 6f
15+
private val tmpRect = Rect()
16+
private var masks: List<Rect> = emptyList()
17+
18+
override fun draw(canvas: Canvas) {
19+
paint.textSize = 32f
20+
paint.setColor(Color.BLACK)
21+
22+
paint.strokeWidth = 4f
23+
24+
for (mask in masks) {
25+
paint.setColor(Color.BLACK)
26+
paint.style = Paint.Style.STROKE
27+
canvas.drawRect(mask, paint)
28+
29+
paint.style = Paint.Style.FILL
30+
val label = "${mask.left} ${mask.top}"
31+
paint.getTextBounds(label, 0, label.length, tmpRect)
32+
33+
paint.setColor(Color.WHITE)
34+
canvas.drawRect(
35+
mask.left.toFloat() + paint.strokeWidth / 2,
36+
mask.top.toFloat() + paint.strokeWidth / 2,
37+
mask.left.toFloat() + tmpRect.width() + padding + padding,
38+
mask.top.toFloat() + tmpRect.height() + padding + padding,
39+
paint
40+
)
41+
paint.setColor(Color.BLACK)
42+
canvas.drawText(
43+
label,
44+
mask.left.toFloat() + padding + paint.strokeWidth / 2,
45+
mask.top.toFloat() + tmpRect.height() - tmpRect.bottom + padding + paint.strokeWidth / 2,
46+
paint
47+
)
48+
}
49+
}
50+
51+
override fun setAlpha(alpha: Int) {
52+
// no-op
53+
}
54+
55+
override fun setColorFilter(colorFilter: ColorFilter?) {
56+
// no-op
57+
}
58+
59+
@Deprecated("Deprecated in Java")
60+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
61+
62+
fun update(masks: List<Rect>) {
63+
this.masks = masks
64+
invalidateSelf()
65+
}
66+
}

0 commit comments

Comments
 (0)