@@ -4,60 +4,51 @@ import android.annotation.SuppressLint
44import android.annotation.TargetApi
55import android.content.Context
66import 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
147import android.view.View
158import android.view.ViewTreeObserver
9+ import io.sentry.ScreenshotStrategyType
1610import io.sentry.SentryLevel.DEBUG
17- import io.sentry.SentryLevel.INFO
1811import io.sentry.SentryLevel.WARNING
1912import io.sentry.SentryOptions
2013import 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
2117import io.sentry.android.replay.util.DebugOverlayDrawable
22- import io.sentry.android.replay.util.MainLooperHandler
2318import io.sentry.android.replay.util.addOnDrawListenerSafe
24- import io.sentry.android.replay.util.getVisibleRects
2519import 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
3120import java.io.File
3221import java.lang.ref.WeakReference
33- import java.util.concurrent.ScheduledExecutorService
3422import java.util.concurrent.atomic.AtomicBoolean
35- import kotlin.LazyThreadSafetyMode.NONE
3623import kotlin.math.roundToInt
3724
3825@SuppressLint(" UseKtx" )
3926@TargetApi(26 )
4027internal class ScreenshotRecorder (
4128 val config : ScreenshotRecorderConfig ,
4229 val options : SentryOptions ,
43- private val mainLooperHandler : MainLooperHandler ,
44- private val recorder : ScheduledExecutorService ,
45- private val screenshotRecorderCallback : ScreenshotRecorderCallback ? ,
30+ val executorProvider : ExecutorProvider ,
31+ screenshotRecorderCallback : ScreenshotRecorderCallback ? ,
4632) : ViewTreeObserver.OnDrawListener {
4733 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 )
5734 private val isCapturing = AtomicBoolean (true )
58- private val lastCaptureSuccessful = AtomicBoolean (false )
5935
6036 private val debugOverlayDrawable = DebugOverlayDrawable ()
37+ private val contentChanged = AtomicBoolean (false )
38+
39+ private val screenshotStrategy: ScreenshotStrategy =
40+ when (options.sessionReplay.screenshotStrategy) {
41+ ScreenshotStrategyType .CANVAS ->
42+ CanvasStrategy (executorProvider, screenshotRecorderCallback, options, config)
43+ ScreenshotStrategyType .PIXEL_COPY ->
44+ PixelCopyStrategy (
45+ executorProvider,
46+ screenshotRecorderCallback,
47+ options,
48+ config,
49+ debugOverlayDrawable,
50+ )
51+ }
6152
6253 fun capture () {
6354 if (options.sessionReplay.isDebug) {
@@ -75,12 +66,12 @@ internal class ScreenshotRecorder(
7566 DEBUG ,
7667 " Capturing screenshot, contentChanged: %s, lastCaptureSuccessful: %s" ,
7768 contentChanged.get(),
78- lastCaptureSuccessful.get (),
69+ screenshotStrategy.lastCaptureSuccessful (),
7970 )
8071 }
8172
82- if (! contentChanged.get() && lastCaptureSuccessful.get() ) {
83- screenshotRecorderCallback?.onScreenshotRecorded(screenshot )
73+ if (! contentChanged.get()) {
74+ screenshotStrategy.emitLastScreenshot( )
8475 return
8576 }
8677
@@ -98,93 +89,9 @@ internal class ScreenshotRecorder(
9889
9990 try {
10091 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- )
92+ screenshotStrategy.capture(root)
18593 } catch (e: Throwable ) {
18694 options.logger.log(WARNING , " Failed to capture replay recording" , e)
187- lastCaptureSuccessful.set(false )
18895 }
18996 }
19097
@@ -199,6 +106,7 @@ internal class ScreenshotRecorder(
199106 }
200107
201108 contentChanged.set(true )
109+ screenshotStrategy.onContentChanged()
202110 }
203111
204112 fun bind (root : View ) {
@@ -212,6 +120,7 @@ internal class ScreenshotRecorder(
212120
213121 // invalidate the flag to capture the first frame after new window is attached
214122 contentChanged.set(true )
123+ screenshotStrategy.onContentChanged()
215124 }
216125
217126 fun unbind (root : View ? ) {
@@ -235,29 +144,9 @@ internal class ScreenshotRecorder(
235144 fun close () {
236145 unbind(rootView?.get())
237146 rootView?.clear()
238- if (! screenshot.isRecycled) {
239- screenshot.recycle()
240- }
147+ screenshotStrategy.close()
241148 isCapturing.set(false )
242149 }
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- }
261150}
262151
263152public data class ScreenshotRecorderConfig (
0 commit comments