Skip to content

Determine recording size based on active window #4354

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

Merged
merged 13 commits into from
Jun 5, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Send UI Profiling app start chunk when it finishes ([#4423](https://github.com/getsentry/sentry-java/pull/4423))
- Republish Javadoc [#4457](https://github.com/getsentry/sentry-java/pull/4457)
- Finalize `OkHttpEvent` even if no active span in `SentryOkHttpInterceptor` [#4469](https://github.com/getsentry/sentry-java/pull/4469)
- Correctly capture Dialogs and non full-sized windows ([#4354](https://github.com/getsentry/sentry-java/pull/4354))

## 8.13.2

Expand Down
18 changes: 12 additions & 6 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ public final class io/sentry/android/replay/ModifierExtensionsKt {
}

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

Expand All @@ -50,11 +52,11 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
}

public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable {
public final class io/sentry/android/replay/ReplayIntegration : io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/WindowCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable {
public static final field $stable I
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 <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun captureReplay (Ljava/lang/Boolean;)V
public fun close ()V
public fun disableDebugMaskingOverlay ()V
Expand All @@ -64,13 +66,13 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
public fun getReplayId ()Lio/sentry/protocol/SentryId;
public fun isDebugMaskingOverlayEnabled ()Z
public fun isRecording ()Z
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
public final fun onConfigurationChanged (Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V
public fun onLowMemory ()V
public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
public fun onScreenshotRecorded (Ljava/io/File;J)V
public fun onTouchEvent (Landroid/view/MotionEvent;)V
public fun onWindowSizeChanged (II)V
public fun pause ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
public fun resume ()V
Expand Down Expand Up @@ -124,6 +126,10 @@ public final class io/sentry/android/replay/ViewExtensionsKt {
public static final fun sentryReplayUnmask (Landroid/view/View;)V
}

public abstract interface class io/sentry/android/replay/WindowCallback {
public abstract fun onWindowSizeChanged (II)V
}

public abstract interface class io/sentry/android/replay/gestures/TouchRecorderCallback {
public abstract fun onTouchEvent (Landroid/view/MotionEvent;)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ public interface Recorder : Closeable {
* 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
*/
public fun start(recorderConfig: ScreenshotRecorderConfig)
public fun start()

public fun onConfigurationChanged(config: ScreenshotRecorderConfig)

public fun resume()

public fun pause()

public fun reset()

public fun stop()
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package io.sentry.android.replay

import android.content.ComponentCallbacks
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.os.Build
import android.view.MotionEvent
Expand Down Expand Up @@ -60,16 +58,15 @@ public class ReplayIntegration(
private val context: Context,
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,
TouchRecorderCallback,
ReplayController,
ComponentCallbacks,
IConnectionStatusObserver,
IRateLimitObserver {
IRateLimitObserver,
WindowCallback {

private companion object {
init {
Expand All @@ -83,20 +80,18 @@ public class ReplayIntegration(
context.appContext(),
dateProvider,
null,
null,
null
)

internal constructor(
context: Context,
dateProvider: ICurrentDateProvider,
recorderProvider: (() -> Recorder)?,
recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)?,
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)?,
replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null,
mainLooperHandler: MainLooperHandler? = null,
gestureRecorderProvider: (() -> GestureRecorder)? = null
) : this(context.appContext(), dateProvider, recorderProvider, recorderConfigProvider, replayCacheProvider) {
) : this(context.appContext(), dateProvider, recorderProvider, replayCacheProvider) {
this.replayCaptureStrategyProvider = replayCaptureStrategyProvider
this.mainLooperHandler = mainLooperHandler ?: MainLooperHandler()
this.gestureRecorderProvider = gestureRecorderProvider
Expand Down Expand Up @@ -139,22 +134,12 @@ public class ReplayIntegration(
}

this.scopes = scopes
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler, replayExecutor)
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, this, mainLooperHandler, replayExecutor)
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
isEnabled.set(true)

options.connectionStatusProvider.addConnectionStatusObserver(this)
scopes.rateLimiter?.addRateLimitObserver(this)
if (options.sessionReplay.isTrackOrientationChange) {
try {
context.registerComponentCallbacks(this)
} catch (e: Throwable) {
options.logger.log(
INFO,
"ComponentCallbacks is not available, orientation changes won't be handled by Session replay"
)
}
}

addIntegrationToSdkVersion("Replay")

Expand Down Expand Up @@ -183,17 +168,16 @@ public class ReplayIntegration(
return
}

val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
lifecycle.currentState = STARTED
captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) {
SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, replayCacheProvider)
} else {
BufferCaptureStrategy(options, scopes, dateProvider, random, replayExecutor, replayCacheProvider)
}
recorder?.start()
captureStrategy?.start()

captureStrategy?.start(recorderConfig)
recorder?.start(recorderConfig)
registerRootViewListeners()
lifecycle.currentState = STARTED
}
}

Expand All @@ -215,9 +199,9 @@ public class ReplayIntegration(
return
}

lifecycle.currentState = RESUMED
captureStrategy?.resume()
recorder?.resume()
lifecycle.currentState = RESUMED
}
}

Expand Down Expand Up @@ -280,6 +264,7 @@ public class ReplayIntegration(
}

unregisterRootViewListeners()
recorder?.reset()
recorder?.stop()
gestureRecorder?.stop()
captureStrategy?.stop()
Expand Down Expand Up @@ -312,12 +297,6 @@ public class ReplayIntegration(

options.connectionStatusProvider.removeConnectionStatusObserver(this)
scopes?.rateLimiter?.removeRateLimitObserver(this)
if (options.sessionReplay.isTrackOrientationChange) {
try {
context.unregisterComponentCallbacks(this)
} catch (ignored: Throwable) {
}
}
stop()
recorder?.close()
recorder = null
Expand All @@ -327,24 +306,6 @@ public class ReplayIntegration(
}
}

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

recorder?.stop()

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

recorder?.start(recorderConfig)
// we have to restart recorder with a new config and pause immediately if the replay is paused
if (lifecycle.currentState == PAUSED) {
recorder?.pause()
}
}

override fun onConnectionStatusChanged(status: ConnectionStatus) {
if (captureStrategy !is SessionCaptureStrategy) {
// we only want to stop recording when offline for session mode
Expand Down Expand Up @@ -372,8 +333,6 @@ public class ReplayIntegration(
}
}

override fun onLowMemory(): Unit = Unit

override fun onTouchEvent(event: MotionEvent) {
if (!isEnabled.get() || !lifecycle.isTouchRecordingAllowed()) {
return
Expand Down Expand Up @@ -474,6 +433,30 @@ public class ReplayIntegration(
}
}

override fun onWindowSizeChanged(width: Int, height: Int) {
if (!isEnabled.get() || !isRecording()) {
return
}
if (options.sessionReplay.isTrackConfiguration) {
val recorderConfig =
ScreenshotRecorderConfig.fromSize(context, options.sessionReplay, width, height)
onConfigurationChanged(recorderConfig)
}
}

public fun onConfigurationChanged(config: ScreenshotRecorderConfig) {
if (!isEnabled.get() || !isRecording()) {
return
}
captureStrategy?.onConfigurationChanged(config)
recorder?.onConfigurationChanged(config)

// we have to restart recorder with a new config and pause immediately if the replay is paused
if (lifecycle.currentState == PAUSED) {
recorder?.pause()
}
}

private class PreviousReplayHint : Backfillable {
override fun shouldEnrich(): Boolean = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.view.PixelCopy
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import io.sentry.SentryLevel.DEBUG
import io.sentry.SentryLevel.INFO
import io.sentry.SentryLevel.WARNING
Expand Down Expand Up @@ -196,6 +192,9 @@ internal class ScreenshotRecorder(
}

override fun onDraw() {
if (!isCapturing.get()) {
return
}
val root = rootView?.get()
if (root == null || root.width <= 0 || root.height <= 0 || !root.isShown) {
options.logger.log(DEBUG, "Root view is invalid, not capturing screenshot")
Expand Down Expand Up @@ -303,35 +302,26 @@ public data class ScreenshotRecorderConfig(
}
}

fun from(
fun fromSize(
context: Context,
sessionReplay: SentryReplayOptions
sessionReplay: SentryReplayOptions,
windowWidth: Int,
windowHeight: Int
): ScreenshotRecorderConfig {
// PixelCopy takes screenshots including system bars, so we have to get the real size here
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val screenBounds = if (VERSION.SDK_INT >= VERSION_CODES.R) {
wm.currentWindowMetrics.bounds
} else {
val screenBounds = Point()
@Suppress("DEPRECATION")
wm.defaultDisplay.getRealSize(screenBounds)
Rect(0, 0, screenBounds.x, screenBounds.y)
}

// use the baseline density of 1x (mdpi)
val (height, width) =
((screenBounds.height() / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
((windowHeight / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
.roundToInt()
.adjustToBlockSize() to
((screenBounds.width() / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
((windowWidth / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
.roundToInt()
.adjustToBlockSize()

return ScreenshotRecorderConfig(
recordingWidth = width,
recordingHeight = height,
scaleFactorX = width.toFloat() / screenBounds.width(),
scaleFactorY = height.toFloat() / screenBounds.height(),
scaleFactorX = width.toFloat() / windowWidth,
scaleFactorY = height.toFloat() / windowHeight,
frameRate = sessionReplay.frameRate,
bitRate = sessionReplay.quality.bitRate
)
Expand Down Expand Up @@ -360,3 +350,10 @@ public interface ScreenshotRecorderCallback {
*/
public fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long)
}

/**
* A callback to be invoked when once current window size is determined or changes
*/
public interface WindowCallback {
public fun onWindowSizeChanged(width: Int, height: Int)
}
Loading
Loading