Skip to content

Commit

Permalink
Merge 82680fb into b3b7813
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Apr 18, 2024
2 parents b3b7813 + 82680fb commit a82aaa3
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 43 deletions.
12 changes: 12 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,15 @@ 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 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 +58,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,17 +22,30 @@ 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
Expand All @@ -58,7 +71,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 +107,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 +146,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 +162,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 +192,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 Down Expand Up @@ -72,7 +73,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
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.sentry.android.replay.capture

import android.graphics.Bitmap
import io.sentry.DateUtils
import io.sentry.Hint
import io.sentry.IHub
Expand All @@ -9,9 +8,11 @@ import io.sentry.SentryLevel.ERROR
import io.sentry.SentryLevel.INFO
import io.sentry.SentryOptions
import io.sentry.SentryReplayEvent.ReplayType.BUFFER
import io.sentry.android.replay.ReplayCache
import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.util.sample
import io.sentry.android.replay.util.submitSafely
import io.sentry.protocol.SentryId
import io.sentry.transport.ICurrentDateProvider
import io.sentry.util.FileUtils
import java.io.File
Expand All @@ -22,8 +23,9 @@ internal class BufferCaptureStrategy(
private val hub: IHub?,
private val dateProvider: ICurrentDateProvider,
recorderConfig: ScreenshotRecorderConfig,
private val random: SecureRandom
) : BaseCaptureStrategy(options, hub, dateProvider, recorderConfig) {
private val random: SecureRandom,
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
) : BaseCaptureStrategy(options, hub, dateProvider, recorderConfig, replayCacheProvider = replayCacheProvider) {

private val bufferedSegments = mutableListOf<ReplaySegment.Created>()

Expand Down Expand Up @@ -91,12 +93,12 @@ internal class BufferCaptureStrategy(
}
}

override fun onScreenshotRecorded(bitmap: Bitmap) {
override fun onScreenshotRecorded(store: ReplayCache.(frameTimestamp: Long) -> Unit) {
// have to do it before submitting, otherwise if the queue is busy, the timestamp won't be
// reflecting the exact time of when it was captured
val frameTimestamp = dateProvider.currentTimeMillis
replayExecutor.submitSafely(options, "$TAG.add_frame") {
cache?.addFrame(bitmap, frameTimestamp)
cache?.store(frameTimestamp)

val now = dateProvider.currentTimeMillis
val bufferLimit = now - options.experimental.sessionReplay.errorReplayDuration
Expand Down
Loading

0 comments on commit a82aaa3

Please sign in to comment.