Skip to content

Commit 33cd776

Browse files
authored
[SR] Expose public API for flutter
2 parents db66737 + c6b16ed commit 33cd776

File tree

16 files changed

+373
-43
lines changed

16 files changed

+373
-43
lines changed

sentry-android-replay/api/sentry-android-replay.api

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ public final class io/sentry/android/replay/GeneratedVideo {
2121
public fun toString ()Ljava/lang/String;
2222
}
2323

24+
public abstract interface class io/sentry/android/replay/Recorder : java/io/Closeable {
25+
public abstract fun pause ()V
26+
public abstract fun resume ()V
27+
public abstract fun start (Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
28+
public abstract fun stop ()V
29+
}
30+
2431
public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
2532
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
2633
public final fun addFrame (Ljava/io/File;J)V
@@ -32,11 +39,16 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
3239

3340
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable {
3441
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
42+
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
43+
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
3544
public fun close ()V
45+
public final fun getReplayCacheDir ()Ljava/io/File;
46+
public fun getReplayId ()Lio/sentry/protocol/SentryId;
3647
public fun isRecording ()Z
3748
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
3849
public fun onLowMemory ()V
3950
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
51+
public fun onScreenshotRecorded (Ljava/io/File;J)V
4052
public fun pause ()V
4153
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
4254
public fun resume ()V
@@ -47,6 +59,7 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
4759

4860
public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallback {
4961
public abstract fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
62+
public abstract fun onScreenshotRecorded (Ljava/io/File;J)V
5063
}
5164

5265
public final class io/sentry/android/replay/ScreenshotRecorderConfig {

sentry-android-replay/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ dependencies {
7575
testImplementation(Config.TestLibs.androidxJunit)
7676
testImplementation(Config.TestLibs.mockitoKotlin)
7777
testImplementation(Config.TestLibs.mockitoInline)
78+
testImplementation(Config.TestLibs.awaitility)
7879
}
7980

8081
tasks.withType<Detekt> {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.sentry.android.replay
2+
3+
import java.io.Closeable
4+
5+
interface Recorder : Closeable {
6+
/**
7+
* @param recorderConfig a [ScreenshotRecorderConfig] that can be used to determine frame rate
8+
* at which the screenshots should be taken, and the screenshots size/resolution, which can
9+
* change e.g. in the case of orientation change or window size change
10+
*/
11+
fun start(recorderConfig: ScreenshotRecorderConfig)
12+
13+
fun resume()
14+
15+
fun pause()
16+
17+
fun stop()
18+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ public class ReplayCache internal constructor(
3030
private val options: SentryOptions,
3131
private val replayId: SentryId,
3232
private val recorderConfig: ScreenshotRecorderConfig,
33-
private val encoderCreator: (videoFile: File, height: Int, width: Int) -> SimpleVideoEncoder
33+
private val encoderProvider: (videoFile: File, height: Int, width: Int) -> SimpleVideoEncoder
3434
) : Closeable {
3535

3636
public constructor(
3737
options: SentryOptions,
3838
replayId: SentryId,
3939
recorderConfig: ScreenshotRecorderConfig
40-
) : this(options, replayId, recorderConfig, encoderCreator = { videoFile, height, width ->
40+
) : this(options, replayId, recorderConfig, encoderProvider = { videoFile, height, width ->
4141
SimpleVideoEncoder(
4242
options,
4343
MuxerConfig(
@@ -145,7 +145,7 @@ public class ReplayCache internal constructor(
145145
}
146146

147147
// TODO: reuse instance of encoder and just change file path to create a different muxer
148-
encoder = synchronized(encoderLock) { encoderCreator(videoFile, height, width) }
148+
encoder = synchronized(encoderLock) { encoderProvider(videoFile, height, width) }
149149

150150
val step = 1000 / recorderConfig.frameRate.toLong()
151151
var frameCount = 0

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,37 @@ import io.sentry.protocol.SentryId
2222
import io.sentry.transport.ICurrentDateProvider
2323
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
2424
import java.io.Closeable
25+
import java.io.File
2526
import java.security.SecureRandom
2627
import java.util.concurrent.atomic.AtomicBoolean
2728

28-
class ReplayIntegration(
29+
public class ReplayIntegration(
2930
private val context: Context,
30-
private val dateProvider: ICurrentDateProvider
31+
private val dateProvider: ICurrentDateProvider,
32+
private val recorderProvider: (() -> Recorder)? = null,
33+
private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null,
34+
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
3135
) : Integration, Closeable, ScreenshotRecorderCallback, ReplayController, ComponentCallbacks {
3236

37+
// needed for the Java's call site
38+
constructor(context: Context, dateProvider: ICurrentDateProvider) : this(
39+
context,
40+
dateProvider,
41+
null,
42+
null,
43+
null
44+
)
45+
3346
private lateinit var options: SentryOptions
3447
private var hub: IHub? = null
35-
private var recorder: WindowRecorder? = null
48+
private var recorder: Recorder? = null
3649
private val random by lazy { SecureRandom() }
3750

3851
// TODO: probably not everything has to be thread-safe here
3952
private val isEnabled = AtomicBoolean(false)
4053
private val isRecording = AtomicBoolean(false)
4154
private var captureStrategy: CaptureStrategy? = null
55+
public val replayCacheDir: File? get() = captureStrategy?.replayCacheDir
4256

4357
private lateinit var recorderConfig: ScreenshotRecorderConfig
4458

@@ -58,7 +72,7 @@ class ReplayIntegration(
5872
}
5973

6074
this.hub = hub
61-
recorder = WindowRecorder(options, this)
75+
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this)
6276
isEnabled.set(true)
6377

6478
try {
@@ -94,15 +108,15 @@ class ReplayIntegration(
94108
return
95109
}
96110

97-
recorderConfig = ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
111+
recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
98112
captureStrategy = if (isFullSession) {
99-
SessionCaptureStrategy(options, hub, dateProvider, recorderConfig)
113+
SessionCaptureStrategy(options, hub, dateProvider, recorderConfig, replayCacheProvider = replayCacheProvider)
100114
} else {
101-
BufferCaptureStrategy(options, hub, dateProvider, recorderConfig, random)
115+
BufferCaptureStrategy(options, hub, dateProvider, recorderConfig, random, replayCacheProvider)
102116
}
103117

104118
captureStrategy?.start()
105-
recorder?.startRecording(recorderConfig)
119+
recorder?.start(recorderConfig)
106120
}
107121

108122
override fun resume() {
@@ -133,6 +147,8 @@ class ReplayIntegration(
133147
captureStrategy = captureStrategy?.convert()
134148
}
135149

150+
override fun getReplayId(): SentryId = captureStrategy?.currentReplayId?.get() ?: SentryId.EMPTY_ID
151+
136152
override fun pause() {
137153
if (!isEnabled.get() || !isRecording.get()) {
138154
return
@@ -147,14 +163,22 @@ class ReplayIntegration(
147163
return
148164
}
149165

150-
recorder?.stopRecording()
166+
recorder?.stop()
151167
captureStrategy?.stop()
152168
isRecording.set(false)
153169
captureStrategy = null
154170
}
155171

156172
override fun onScreenshotRecorded(bitmap: Bitmap) {
157-
captureStrategy?.onScreenshotRecorded(bitmap)
173+
captureStrategy?.onScreenshotRecorded { frameTimeStamp ->
174+
addFrame(bitmap, frameTimeStamp)
175+
}
176+
}
177+
178+
override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) {
179+
captureStrategy?.onScreenshotRecorded { _ ->
180+
addFrame(screenshot, frameTimestamp)
181+
}
158182
}
159183

160184
override fun close() {
@@ -169,20 +193,22 @@ class ReplayIntegration(
169193
stop()
170194
captureStrategy?.close()
171195
captureStrategy = null
196+
recorder?.close()
197+
recorder = null
172198
}
173199

174200
override fun onConfigurationChanged(newConfig: Configuration) {
175201
if (!isEnabled.get() || !isRecording.get()) {
176202
return
177203
}
178204

179-
recorder?.stopRecording()
205+
recorder?.stop()
180206

181207
// refresh config based on new device configuration
182-
recorderConfig = ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
208+
recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
183209
captureStrategy?.onConfigurationChanged(recorderConfig)
184210

185-
recorder?.startRecording(recorderConfig)
211+
recorder?.start(recorderConfig)
186212
}
187213

188214
override fun onLowMemory() = Unit

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import io.sentry.SentryLevel.WARNING
2626
import io.sentry.SentryOptions
2727
import io.sentry.SentryReplayOptions
2828
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
29+
import java.io.File
2930
import java.lang.ref.WeakReference
3031
import java.util.concurrent.atomic.AtomicBoolean
3132
import java.util.concurrent.atomic.AtomicReference
@@ -35,7 +36,7 @@ import kotlin.math.roundToInt
3536
internal class ScreenshotRecorder(
3637
val config: ScreenshotRecorderConfig,
3738
val options: SentryOptions,
38-
private val screenshotRecorderCallback: ScreenshotRecorderCallback
39+
private val screenshotRecorderCallback: ScreenshotRecorderCallback?
3940
) : ViewTreeObserver.OnDrawListener {
4041

4142
private var rootView: WeakReference<View>? = null
@@ -68,7 +69,7 @@ internal class ScreenshotRecorder(
6869
options.logger.log(DEBUG, "Content hasn't changed, repeating last known frame")
6970

7071
lastScreenshot?.let {
71-
screenshotRecorderCallback.onScreenshotRecorded(
72+
screenshotRecorderCallback?.onScreenshotRecorded(
7273
it.copy(ARGB_8888, false)
7374
)
7475
}
@@ -140,7 +141,7 @@ internal class ScreenshotRecorder(
140141
}
141142

142143
val screenshot = scaledBitmap.copy(ARGB_8888, false)
143-
screenshotRecorderCallback.onScreenshotRecorded(screenshot)
144+
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
144145
lastScreenshot?.recycle()
145146
lastScreenshot = screenshot
146147
contentChanged.set(false)
@@ -294,6 +295,24 @@ public data class ScreenshotRecorderConfig(
294295
}
295296
}
296297

297-
interface ScreenshotRecorderCallback {
298+
/**
299+
* A callback to be invoked when a new screenshot available. Normally, only one of the
300+
* [onScreenshotRecorded] method overloads should be called by a single recorder, however, it will
301+
* still work of both are used at the same time.
302+
*/
303+
public interface ScreenshotRecorderCallback {
304+
/**
305+
* Called whenever a new frame screenshot is available.
306+
*
307+
* @param bitmap a screenshot taken in the form of [android.graphics.Bitmap]
308+
*/
298309
fun onScreenshotRecorded(bitmap: Bitmap)
310+
311+
/**
312+
* Called whenever a new frame screenshot is available.
313+
*
314+
* @param screenshot file containing the frame screenshot
315+
* @param frameTimestamp the timestamp when the frame screenshot was taken
316+
*/
317+
fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long)
299318
}

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.view.View
55
import io.sentry.SentryOptions
66
import io.sentry.android.replay.util.gracefullyShutdown
77
import io.sentry.android.replay.util.scheduleAtFixedRateSafely
8-
import java.io.Closeable
98
import java.lang.ref.WeakReference
109
import java.util.concurrent.Executors
1110
import java.util.concurrent.ScheduledFuture
@@ -17,8 +16,8 @@ import kotlin.LazyThreadSafetyMode.NONE
1716
@TargetApi(26)
1817
internal class WindowRecorder(
1918
private val options: SentryOptions,
20-
private val screenshotRecorderCallback: ScreenshotRecorderCallback
21-
) : Closeable {
19+
private val screenshotRecorderCallback: ScreenshotRecorderCallback? = null
20+
) : Recorder {
2221

2322
internal companion object {
2423
private const val TAG = "WindowRecorder"
@@ -51,7 +50,7 @@ internal class WindowRecorder(
5150
}
5251
}
5352

54-
fun startRecording(recorderConfig: ScreenshotRecorderConfig) {
53+
override fun start(recorderConfig: ScreenshotRecorderConfig) {
5554
if (isRecording.getAndSet(true)) {
5655
return
5756
}
@@ -69,10 +68,14 @@ internal class WindowRecorder(
6968
}
7069
}
7170

72-
fun resume() = recorder?.resume()
73-
fun pause() = recorder?.pause()
71+
override fun resume() {
72+
recorder?.resume()
73+
}
74+
override fun pause() {
75+
recorder?.pause()
76+
}
7477

75-
fun stopRecording() {
78+
override fun stop() {
7679
rootViewsSpy.listeners -= onRootViewsChangedListener
7780
rootViews.forEach { recorder?.unbind(it.get()) }
7881
recorder?.close()
@@ -93,7 +96,7 @@ internal class WindowRecorder(
9396
}
9497

9598
override fun close() {
96-
stopRecording()
99+
stop()
97100
capturer.gracefullyShutdown(options)
98101
}
99102
}

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ internal abstract class BaseCaptureStrategy(
3333
private val hub: IHub?,
3434
private val dateProvider: ICurrentDateProvider,
3535
protected var recorderConfig: ScreenshotRecorderConfig,
36-
executor: ScheduledExecutorService? = null
36+
executor: ScheduledExecutorService? = null,
37+
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
3738
) : CaptureStrategy {
3839

3940
internal companion object {
@@ -45,6 +46,7 @@ internal abstract class BaseCaptureStrategy(
4546
protected val replayStartTimestamp = AtomicLong()
4647
override val currentReplayId = AtomicReference(SentryId.EMPTY_ID)
4748
override val currentSegment = AtomicInteger(0)
49+
override val replayCacheDir: File? get() = cache?.replayCacheDir
4850

4951
protected val replayExecutor: ScheduledExecutorService by lazy {
5052
executor ?: Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
@@ -72,7 +74,7 @@ internal abstract class BaseCaptureStrategy(
7274
}
7375
}
7476

75-
cache = ReplayCache(options, currentReplayId.get(), recorderConfig)
77+
cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId, recorderConfig)
7678

7779
// TODO: replace it with dateProvider.currentTimeMillis to also test it
7880
segmentTimestamp.set(DateUtils.getCurrentDateTime())

0 commit comments

Comments
 (0)