Skip to content

Commit 8b7fc28

Browse files
authored
Merge 9cb8201 into b3d8889
2 parents b3d8889 + 9cb8201 commit 8b7fc28

File tree

10 files changed

+1301
-140
lines changed

10 files changed

+1301
-140
lines changed

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

Lines changed: 30 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,55 @@ import android.annotation.SuppressLint
44
import android.annotation.TargetApi
55
import android.content.Context
66
import android.graphics.Bitmap
7-
import android.graphics.Canvas
8-
import android.graphics.Color
9-
import android.graphics.Matrix
10-
import android.graphics.Paint
11-
import android.graphics.Rect
12-
import android.graphics.RectF
13-
import android.view.PixelCopy
147
import android.view.View
158
import android.view.ViewTreeObserver
9+
import io.sentry.ScreenshotStrategyType
1610
import io.sentry.SentryLevel.DEBUG
17-
import io.sentry.SentryLevel.INFO
1811
import io.sentry.SentryLevel.WARNING
1912
import io.sentry.SentryOptions
2013
import io.sentry.SentryReplayOptions
14+
import io.sentry.android.replay.screenshot.CanvasStrategy
15+
import io.sentry.android.replay.screenshot.PixelCopyStrategy
16+
import io.sentry.android.replay.screenshot.ScreenshotStrategy
2117
import io.sentry.android.replay.util.DebugOverlayDrawable
2218
import io.sentry.android.replay.util.MainLooperHandler
2319
import io.sentry.android.replay.util.addOnDrawListenerSafe
24-
import io.sentry.android.replay.util.getVisibleRects
2520
import io.sentry.android.replay.util.removeOnDrawListenerSafe
26-
import io.sentry.android.replay.util.submitSafely
27-
import io.sentry.android.replay.util.traverse
28-
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
29-
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
30-
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
3121
import java.io.File
3222
import java.lang.ref.WeakReference
3323
import java.util.concurrent.ScheduledExecutorService
3424
import java.util.concurrent.atomic.AtomicBoolean
35-
import kotlin.LazyThreadSafetyMode.NONE
3625
import kotlin.math.roundToInt
3726

3827
@SuppressLint("UseKtx")
3928
@TargetApi(26)
4029
internal class ScreenshotRecorder(
4130
val config: ScreenshotRecorderConfig,
4231
val options: SentryOptions,
43-
private val mainLooperHandler: MainLooperHandler,
44-
private val recorder: ScheduledExecutorService,
45-
private val screenshotRecorderCallback: ScreenshotRecorderCallback?,
32+
val handler: MainLooperHandler,
33+
executorService: ScheduledExecutorService,
34+
screenshotRecorderCallback: ScreenshotRecorderCallback?,
4635
) : ViewTreeObserver.OnDrawListener {
4736
private var rootView: WeakReference<View>? = null
48-
private val maskingPaint by lazy(NONE) { Paint() }
49-
private val singlePixelBitmap: Bitmap by
50-
lazy(NONE) { Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) }
51-
private val screenshot =
52-
Bitmap.createBitmap(config.recordingWidth, config.recordingHeight, Bitmap.Config.ARGB_8888)
53-
private val singlePixelBitmapCanvas: Canvas by lazy(NONE) { Canvas(singlePixelBitmap) }
54-
private val prescaledMatrix by
55-
lazy(NONE) { Matrix().apply { preScale(config.scaleFactorX, config.scaleFactorY) } }
56-
private val contentChanged = AtomicBoolean(false)
5737
private val isCapturing = AtomicBoolean(true)
58-
private val lastCaptureSuccessful = AtomicBoolean(false)
5938

6039
private val debugOverlayDrawable = DebugOverlayDrawable()
40+
private val contentChanged = AtomicBoolean(false)
41+
42+
private val screenshotStrategy: ScreenshotStrategy =
43+
when (options.sessionReplay.screenshotStrategy) {
44+
ScreenshotStrategyType.CANVAS ->
45+
CanvasStrategy(executorService, screenshotRecorderCallback, options, config)
46+
ScreenshotStrategyType.PIXEL_COPY ->
47+
PixelCopyStrategy(
48+
executorService,
49+
handler,
50+
screenshotRecorderCallback,
51+
options,
52+
config,
53+
debugOverlayDrawable,
54+
)
55+
}
6156

6257
fun capture() {
6358
if (options.sessionReplay.isDebug) {
@@ -75,12 +70,12 @@ internal class ScreenshotRecorder(
7570
DEBUG,
7671
"Capturing screenshot, contentChanged: %s, lastCaptureSuccessful: %s",
7772
contentChanged.get(),
78-
lastCaptureSuccessful.get(),
73+
screenshotStrategy.lastCaptureSuccessful(),
7974
)
8075
}
8176

82-
if (!contentChanged.get() && lastCaptureSuccessful.get()) {
83-
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
77+
if (!contentChanged.get()) {
78+
screenshotStrategy.emitLastScreenshot()
8479
return
8580
}
8681

@@ -98,93 +93,9 @@ internal class ScreenshotRecorder(
9893

9994
try {
10095
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-
}
142-
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
151-
}
152-
153-
else -> {
154-
listOf(node.visibleRect) to Color.BLACK
155-
}
156-
}
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)
164-
}
165-
}
166-
return@traverse true
167-
}
168-
169-
if (options.replayController.isDebugMaskingOverlayEnabled()) {
170-
mainLooperHandler.post {
171-
if (debugOverlayDrawable.callback == null) {
172-
root.overlay.add(debugOverlayDrawable)
173-
}
174-
debugOverlayDrawable.updateMasks(debugMasks)
175-
root.postInvalidate()
176-
}
177-
}
178-
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
179-
lastCaptureSuccessful.set(true)
180-
contentChanged.set(false)
181-
}
182-
},
183-
mainLooperHandler.handler,
184-
)
96+
screenshotStrategy.capture(root)
18597
} catch (e: Throwable) {
18698
options.logger.log(WARNING, "Failed to capture replay recording", e)
187-
lastCaptureSuccessful.set(false)
18899
}
189100
}
190101

@@ -199,6 +110,7 @@ internal class ScreenshotRecorder(
199110
}
200111

201112
contentChanged.set(true)
113+
screenshotStrategy.onContentChanged()
202114
}
203115

204116
fun bind(root: View) {
@@ -212,6 +124,7 @@ internal class ScreenshotRecorder(
212124

213125
// invalidate the flag to capture the first frame after new window is attached
214126
contentChanged.set(true)
127+
screenshotStrategy.onContentChanged()
215128
}
216129

217130
fun unbind(root: View?) {
@@ -235,29 +148,9 @@ internal class ScreenshotRecorder(
235148
fun close() {
236149
unbind(rootView?.get())
237150
rootView?.clear()
238-
if (!screenshot.isRecycled) {
239-
screenshot.recycle()
240-
}
151+
screenshotStrategy.close()
241152
isCapturing.set(false)
242153
}
243-
244-
private fun Bitmap.dominantColorForRect(rect: Rect): Int {
245-
// TODO: maybe this ceremony can be just simplified to
246-
// TODO: multiplying the visibleRect by the prescaledMatrix
247-
val visibleRect = Rect(rect)
248-
val visibleRectF = RectF(visibleRect)
249-
250-
// since we take screenshot with lower scale, we also
251-
// have to apply the same scale to the visibleRect to get the
252-
// correct screenshot part to determine the dominant color
253-
prescaledMatrix.mapRect(visibleRectF)
254-
// round it back to integer values, because drawBitmap below accepts Rect only
255-
visibleRectF.round(visibleRect)
256-
// draw part of the screenshot (visibleRect) to a single pixel bitmap
257-
singlePixelBitmapCanvas.drawBitmap(this, visibleRect, Rect(0, 0, 1, 1), null)
258-
// get the pixel color (= dominant color)
259-
return singlePixelBitmap.getPixel(0, 0)
260-
}
261154
}
262155

263156
public data class ScreenshotRecorderConfig(

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ internal class WindowRecorder(
2525
private val mainLooperHandler: MainLooperHandler,
2626
private val replayExecutor: ScheduledExecutorService,
2727
) : Recorder, OnRootViewsChangedListener {
28-
internal companion object {
29-
private const val TAG = "WindowRecorder"
30-
}
3128

3229
private val isRecording = AtomicBoolean(false)
3330
private val rootViews = ArrayList<WeakReference<View>>()
@@ -45,6 +42,10 @@ internal class WindowRecorder(
4542
var config: ScreenshotRecorderConfig? = null
4643
private val isRecording = AtomicBoolean(true)
4744

45+
private var currentCaptureDelay = 0L
46+
47+
private var rootView = WeakReference<View>(null)
48+
4849
fun resume() {
4950
if (options.sessionReplay.isDebug) {
5051
options.logger.log(DEBUG, "Resuming the capture runnable.")
@@ -105,12 +106,17 @@ internal class WindowRecorder(
105106
)
106107
}
107108
}
109+
110+
fun bind(newRoot: View) {
111+
rootView = WeakReference(newRoot)
112+
}
108113
}
109114

110115
override fun onRootViewsChanged(root: View, added: Boolean) {
111116
rootViewsLock.acquire().use {
112117
if (added) {
113118
rootViews.add(WeakReference(root))
119+
capturer?.bind(root)
114120
capturer?.recorder?.bind(root)
115121
determineWindowSize(root)
116122
} else {
@@ -188,6 +194,7 @@ internal class WindowRecorder(
188194

189195
val newRoot = rootViews.lastOrNull()?.get()
190196
if (newRoot != null) {
197+
capturer?.bind(newRoot)
191198
capturer?.recorder?.bind(newRoot)
192199
}
193200

0 commit comments

Comments
 (0)