Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR] Expose public API for flutter #3373

Merged
merged 7 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public final class io/sentry/android/replay/GeneratedVideo {
public fun toString ()Ljava/lang/String;
}

public abstract interface class io/sentry/android/replay/Recorder : java/io/Closeable {
public abstract fun pause ()V
public abstract fun resume ()V
public abstract fun start (Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
public abstract fun stop ()V
}

public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
public final fun addFrame (Ljava/io/File;J)V
Expand All @@ -32,11 +39,16 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {

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 {
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
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
public fun close ()V
public final fun getReplayCacheDir ()Ljava/io/File;
public fun getReplayId ()Lio/sentry/protocol/SentryId;
public fun isRecording ()Z
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
public fun onLowMemory ()V
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
public fun onScreenshotRecorded (Ljava/io/File;J)V
public fun pause ()V
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
public fun resume ()V
Expand All @@ -47,6 +59,7 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/

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

public final class io/sentry/android/replay/ScreenshotRecorderConfig {
Expand Down
1 change: 1 addition & 0 deletions sentry-android-replay/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {
testImplementation(Config.TestLibs.androidxJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockitoInline)
testImplementation(Config.TestLibs.awaitility)
}

tasks.withType<Detekt> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sentry.android.replay

import java.io.Closeable

interface Recorder : Closeable {
/**
* @param recorderConfig a [ScreenshotRecorderConfig] that can be used to determine frame rate
* at which the screenshots should be taken, and the screenshots size/resolution, which can
* change e.g. in the case of orientation change or window size change
*/
fun start(recorderConfig: ScreenshotRecorderConfig)

fun resume()

fun pause()

fun stop()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ public class ReplayCache internal constructor(
private val options: SentryOptions,
private val replayId: SentryId,
private val recorderConfig: ScreenshotRecorderConfig,
private val encoderCreator: (videoFile: File, height: Int, width: Int) -> SimpleVideoEncoder
private val encoderProvider: (videoFile: File, height: Int, width: Int) -> SimpleVideoEncoder
) : Closeable {

public constructor(
options: SentryOptions,
replayId: SentryId,
recorderConfig: ScreenshotRecorderConfig
) : this(options, replayId, recorderConfig, encoderCreator = { videoFile, height, width ->
) : this(options, replayId, recorderConfig, encoderProvider = { videoFile, height, width ->
SimpleVideoEncoder(
options,
MuxerConfig(
Expand Down Expand Up @@ -145,7 +145,7 @@ public class ReplayCache internal constructor(
}

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

val step = 1000 / recorderConfig.frameRate.toLong()
var frameCount = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,37 @@ import io.sentry.protocol.SentryId
import io.sentry.transport.ICurrentDateProvider
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import java.io.Closeable
import java.io.File
import java.security.SecureRandom
import java.util.concurrent.atomic.AtomicBoolean

class ReplayIntegration(
public class ReplayIntegration(
private val context: Context,
private val dateProvider: ICurrentDateProvider
private val dateProvider: ICurrentDateProvider,
private val recorderProvider: (() -> Recorder)? = null,
private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null,
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
) : Integration, Closeable, ScreenshotRecorderCallback, ReplayController, ComponentCallbacks {

// needed for the Java's call site
constructor(context: Context, dateProvider: ICurrentDateProvider) : this(
context,
dateProvider,
null,
null,
null
)

private lateinit var options: SentryOptions
private var hub: IHub? = null
private var recorder: WindowRecorder? = null
private var recorder: Recorder? = null
private val random by lazy { SecureRandom() }

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

private lateinit var recorderConfig: ScreenshotRecorderConfig

Expand All @@ -58,7 +72,7 @@ class ReplayIntegration(
}

this.hub = hub
recorder = WindowRecorder(options, this)
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this)
isEnabled.set(true)

try {
Expand Down Expand Up @@ -94,15 +108,15 @@ class ReplayIntegration(
return
}

recorderConfig = ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
captureStrategy = if (isFullSession) {
SessionCaptureStrategy(options, hub, dateProvider, recorderConfig)
SessionCaptureStrategy(options, hub, dateProvider, recorderConfig, replayCacheProvider = replayCacheProvider)
} else {
BufferCaptureStrategy(options, hub, dateProvider, recorderConfig, random)
BufferCaptureStrategy(options, hub, dateProvider, recorderConfig, random, replayCacheProvider)
}

captureStrategy?.start()
recorder?.startRecording(recorderConfig)
recorder?.start(recorderConfig)
}

override fun resume() {
Expand Down Expand Up @@ -133,6 +147,8 @@ class ReplayIntegration(
captureStrategy = captureStrategy?.convert()
}

override fun getReplayId(): SentryId = captureStrategy?.currentReplayId?.get() ?: SentryId.EMPTY_ID

override fun pause() {
if (!isEnabled.get() || !isRecording.get()) {
return
Expand All @@ -147,14 +163,22 @@ class ReplayIntegration(
return
}

recorder?.stopRecording()
recorder?.stop()
captureStrategy?.stop()
isRecording.set(false)
captureStrategy = null
}

override fun onScreenshotRecorded(bitmap: Bitmap) {
captureStrategy?.onScreenshotRecorded(bitmap)
captureStrategy?.onScreenshotRecorded { frameTimeStamp ->
addFrame(bitmap, frameTimeStamp)
}
}

override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) {
captureStrategy?.onScreenshotRecorded { _ ->
addFrame(screenshot, frameTimestamp)
}
}

override fun close() {
Expand All @@ -169,20 +193,22 @@ class ReplayIntegration(
stop()
captureStrategy?.close()
captureStrategy = null
recorder?.close()
recorder = null
}

override fun onConfigurationChanged(newConfig: Configuration) {
if (!isEnabled.get() || !isRecording.get()) {
return
}

recorder?.stopRecording()
recorder?.stop()

// refresh config based on new device configuration
recorderConfig = ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
captureStrategy?.onConfigurationChanged(recorderConfig)

recorder?.startRecording(recorderConfig)
recorder?.start(recorderConfig)
}

override fun onLowMemory() = Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.sentry.SentryLevel.WARNING
import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
Expand All @@ -35,7 +36,7 @@ import kotlin.math.roundToInt
internal class ScreenshotRecorder(
val config: ScreenshotRecorderConfig,
val options: SentryOptions,
private val screenshotRecorderCallback: ScreenshotRecorderCallback
private val screenshotRecorderCallback: ScreenshotRecorderCallback?
) : ViewTreeObserver.OnDrawListener {

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

lastScreenshot?.let {
screenshotRecorderCallback.onScreenshotRecorded(
screenshotRecorderCallback?.onScreenshotRecorded(
it.copy(ARGB_8888, false)
)
}
Expand Down Expand Up @@ -140,7 +141,7 @@ internal class ScreenshotRecorder(
}

val screenshot = scaledBitmap.copy(ARGB_8888, false)
screenshotRecorderCallback.onScreenshotRecorded(screenshot)
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
lastScreenshot?.recycle()
lastScreenshot = screenshot
contentChanged.set(false)
Expand Down Expand Up @@ -294,6 +295,24 @@ public data class ScreenshotRecorderConfig(
}
}

interface ScreenshotRecorderCallback {
/**
* A callback to be invoked when a new screenshot available. Normally, only one of the
* [onScreenshotRecorded] method overloads should be called by a single recorder, however, it will
* still work of both are used at the same time.
*/
public interface ScreenshotRecorderCallback {
/**
* Called whenever a new frame screenshot is available.
*
* @param bitmap a screenshot taken in the form of [android.graphics.Bitmap]
*/
fun onScreenshotRecorded(bitmap: Bitmap)

/**
* Called whenever a new frame screenshot is available.
*
* @param screenshot file containing the frame screenshot
* @param frameTimestamp the timestamp when the frame screenshot was taken
*/
fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.view.View
import io.sentry.SentryOptions
import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.scheduleAtFixedRateSafely
import java.io.Closeable
import java.lang.ref.WeakReference
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
Expand All @@ -17,8 +16,8 @@ import kotlin.LazyThreadSafetyMode.NONE
@TargetApi(26)
internal class WindowRecorder(
private val options: SentryOptions,
private val screenshotRecorderCallback: ScreenshotRecorderCallback
) : Closeable {
private val screenshotRecorderCallback: ScreenshotRecorderCallback? = null
) : Recorder {

internal companion object {
private const val TAG = "WindowRecorder"
Expand Down Expand Up @@ -51,7 +50,7 @@ internal class WindowRecorder(
}
}

fun startRecording(recorderConfig: ScreenshotRecorderConfig) {
override fun start(recorderConfig: ScreenshotRecorderConfig) {
if (isRecording.getAndSet(true)) {
return
}
Expand All @@ -69,10 +68,14 @@ internal class WindowRecorder(
}
}

fun resume() = recorder?.resume()
fun pause() = recorder?.pause()
override fun resume() {
recorder?.resume()
}
override fun pause() {
recorder?.pause()
}

fun stopRecording() {
override fun stop() {
rootViewsSpy.listeners -= onRootViewsChangedListener
rootViews.forEach { recorder?.unbind(it.get()) }
recorder?.close()
Expand All @@ -93,7 +96,7 @@ internal class WindowRecorder(
}

override fun close() {
stopRecording()
stop()
capturer.gracefullyShutdown(options)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ internal abstract class BaseCaptureStrategy(
private val hub: IHub?,
private val dateProvider: ICurrentDateProvider,
protected var recorderConfig: ScreenshotRecorderConfig,
executor: ScheduledExecutorService? = null
executor: ScheduledExecutorService? = null,
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
) : CaptureStrategy {

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

protected val replayExecutor: ScheduledExecutorService by lazy {
executor ?: Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
Expand Down Expand Up @@ -72,7 +74,7 @@ internal abstract class BaseCaptureStrategy(
}
}

cache = ReplayCache(options, currentReplayId.get(), recorderConfig)
cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId, recorderConfig)

// TODO: replace it with dateProvider.currentTimeMillis to also test it
segmentTimestamp.set(DateUtils.getCurrentDateTime())
Expand Down
Loading
Loading