Skip to content

Commit e53e3d1

Browse files
markushiromtsngetsentry-bot
authored
Determine recording size based on active window (#4354)
* Determine recording size based on active window * Extend sample app with Dialog * Update Changelog * Use onPreDrawListener instead of onDrawListener for determining window size * fix(replay): Fix window tracking (#4419) * fix(replay): Fix window tracking * api dump * Fix Changelog * Fix tests * [SR] Remove configuration from start() method (#4454) * Remove configuration from start() method * Open up onConfigurationChanged for Hybrid SDKs * Address logging concerns * Format code * Cache last known config * Update sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com> * Fix order * Fix compile issue --------- Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io> Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com> --------- Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent 0e20a03 commit e53e3d1

File tree

22 files changed

+393
-190
lines changed

22 files changed

+393
-190
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Send UI Profiling app start chunk when it finishes ([#4423](https://github.com/getsentry/sentry-java/pull/4423))
88
- Republish Javadoc [#4457](https://github.com/getsentry/sentry-java/pull/4457)
99
- Finalize `OkHttpEvent` even if no active span in `SentryOkHttpInterceptor` [#4469](https://github.com/getsentry/sentry-java/pull/4469)
10+
- Correctly capture Dialogs and non full-sized windows ([#4354](https://github.com/getsentry/sentry-java/pull/4354))
1011

1112
## 8.13.2
1213

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ public final class io/sentry/android/replay/ModifierExtensionsKt {
3434
}
3535

3636
public abstract interface class io/sentry/android/replay/Recorder : java/io/Closeable {
37+
public abstract fun onConfigurationChanged (Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
3738
public abstract fun pause ()V
39+
public abstract fun reset ()V
3840
public abstract fun resume ()V
39-
public abstract fun start (Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
41+
public abstract fun start ()V
4042
public abstract fun stop ()V
4143
}
4244

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

53-
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 {
55+
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 {
5456
public static final field $stable I
5557
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
56-
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
57-
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
58+
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V
59+
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
5860
public fun captureReplay (Ljava/lang/Boolean;)V
5961
public fun close ()V
6062
public fun disableDebugMaskingOverlay ()V
@@ -64,13 +66,13 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
6466
public fun getReplayId ()Lio/sentry/protocol/SentryId;
6567
public fun isDebugMaskingOverlayEnabled ()Z
6668
public fun isRecording ()Z
67-
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
69+
public final fun onConfigurationChanged (Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
6870
public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V
69-
public fun onLowMemory ()V
7071
public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V
7172
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
7273
public fun onScreenshotRecorded (Ljava/io/File;J)V
7374
public fun onTouchEvent (Landroid/view/MotionEvent;)V
75+
public fun onWindowSizeChanged (II)V
7476
public fun pause ()V
7577
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
7678
public fun resume ()V
@@ -124,6 +126,10 @@ public final class io/sentry/android/replay/ViewExtensionsKt {
124126
public static final fun sentryReplayUnmask (Landroid/view/View;)V
125127
}
126128

129+
public abstract interface class io/sentry/android/replay/WindowCallback {
130+
public abstract fun onWindowSizeChanged (II)V
131+
}
132+
127133
public abstract interface class io/sentry/android/replay/gestures/TouchRecorderCallback {
128134
public abstract fun onTouchEvent (Landroid/view/MotionEvent;)V
129135
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ public interface Recorder : Closeable {
88
* at which the screenshots should be taken, and the screenshots size/resolution, which can
99
* change e.g. in the case of orientation change or window size change
1010
*/
11-
public fun start(recorderConfig: ScreenshotRecorderConfig)
11+
public fun start()
12+
13+
public fun onConfigurationChanged(config: ScreenshotRecorderConfig)
1214

1315
public fun resume()
1416

1517
public fun pause()
1618

19+
public fun reset()
20+
1721
public fun stop()
1822
}

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

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package io.sentry.android.replay
22

3-
import android.content.ComponentCallbacks
43
import android.content.Context
5-
import android.content.res.Configuration
64
import android.graphics.Bitmap
75
import android.os.Build
86
import android.view.MotionEvent
@@ -60,16 +58,15 @@ public class ReplayIntegration(
6058
private val context: Context,
6159
private val dateProvider: ICurrentDateProvider,
6260
private val recorderProvider: (() -> Recorder)? = null,
63-
private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null,
6461
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
6562
) : Integration,
6663
Closeable,
6764
ScreenshotRecorderCallback,
6865
TouchRecorderCallback,
6966
ReplayController,
70-
ComponentCallbacks,
7167
IConnectionStatusObserver,
72-
IRateLimitObserver {
68+
IRateLimitObserver,
69+
WindowCallback {
7370

7471
private companion object {
7572
init {
@@ -83,20 +80,18 @@ public class ReplayIntegration(
8380
context.appContext(),
8481
dateProvider,
8582
null,
86-
null,
8783
null
8884
)
8985

9086
internal constructor(
9187
context: Context,
9288
dateProvider: ICurrentDateProvider,
9389
recorderProvider: (() -> Recorder)?,
94-
recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)?,
9590
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)?,
9691
replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null,
9792
mainLooperHandler: MainLooperHandler? = null,
9893
gestureRecorderProvider: (() -> GestureRecorder)? = null
99-
) : this(context.appContext(), dateProvider, recorderProvider, recorderConfigProvider, replayCacheProvider) {
94+
) : this(context.appContext(), dateProvider, recorderProvider, replayCacheProvider) {
10095
this.replayCaptureStrategyProvider = replayCaptureStrategyProvider
10196
this.mainLooperHandler = mainLooperHandler ?: MainLooperHandler()
10297
this.gestureRecorderProvider = gestureRecorderProvider
@@ -139,22 +134,12 @@ public class ReplayIntegration(
139134
}
140135

141136
this.scopes = scopes
142-
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler, replayExecutor)
137+
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, this, mainLooperHandler, replayExecutor)
143138
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
144139
isEnabled.set(true)
145140

146141
options.connectionStatusProvider.addConnectionStatusObserver(this)
147142
scopes.rateLimiter?.addRateLimitObserver(this)
148-
if (options.sessionReplay.isTrackOrientationChange) {
149-
try {
150-
context.registerComponentCallbacks(this)
151-
} catch (e: Throwable) {
152-
options.logger.log(
153-
INFO,
154-
"ComponentCallbacks is not available, orientation changes won't be handled by Session replay"
155-
)
156-
}
157-
}
158143

159144
addIntegrationToSdkVersion("Replay")
160145

@@ -183,17 +168,16 @@ public class ReplayIntegration(
183168
return
184169
}
185170

186-
val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
171+
lifecycle.currentState = STARTED
187172
captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) {
188173
SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, replayCacheProvider)
189174
} else {
190175
BufferCaptureStrategy(options, scopes, dateProvider, random, replayExecutor, replayCacheProvider)
191176
}
177+
recorder?.start()
178+
captureStrategy?.start()
192179

193-
captureStrategy?.start(recorderConfig)
194-
recorder?.start(recorderConfig)
195180
registerRootViewListeners()
196-
lifecycle.currentState = STARTED
197181
}
198182
}
199183

@@ -215,9 +199,9 @@ public class ReplayIntegration(
215199
return
216200
}
217201

202+
lifecycle.currentState = RESUMED
218203
captureStrategy?.resume()
219204
recorder?.resume()
220-
lifecycle.currentState = RESUMED
221205
}
222206
}
223207

@@ -280,6 +264,7 @@ public class ReplayIntegration(
280264
}
281265

282266
unregisterRootViewListeners()
267+
recorder?.reset()
283268
recorder?.stop()
284269
gestureRecorder?.stop()
285270
captureStrategy?.stop()
@@ -312,12 +297,6 @@ public class ReplayIntegration(
312297

313298
options.connectionStatusProvider.removeConnectionStatusObserver(this)
314299
scopes?.rateLimiter?.removeRateLimitObserver(this)
315-
if (options.sessionReplay.isTrackOrientationChange) {
316-
try {
317-
context.unregisterComponentCallbacks(this)
318-
} catch (ignored: Throwable) {
319-
}
320-
}
321300
stop()
322301
recorder?.close()
323302
recorder = null
@@ -327,24 +306,6 @@ public class ReplayIntegration(
327306
}
328307
}
329308

330-
override fun onConfigurationChanged(newConfig: Configuration) {
331-
if (!isEnabled.get() || !isRecording()) {
332-
return
333-
}
334-
335-
recorder?.stop()
336-
337-
// refresh config based on new device configuration
338-
val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
339-
captureStrategy?.onConfigurationChanged(recorderConfig)
340-
341-
recorder?.start(recorderConfig)
342-
// we have to restart recorder with a new config and pause immediately if the replay is paused
343-
if (lifecycle.currentState == PAUSED) {
344-
recorder?.pause()
345-
}
346-
}
347-
348309
override fun onConnectionStatusChanged(status: ConnectionStatus) {
349310
if (captureStrategy !is SessionCaptureStrategy) {
350311
// we only want to stop recording when offline for session mode
@@ -372,8 +333,6 @@ public class ReplayIntegration(
372333
}
373334
}
374335

375-
override fun onLowMemory(): Unit = Unit
376-
377336
override fun onTouchEvent(event: MotionEvent) {
378337
if (!isEnabled.get() || !lifecycle.isTouchRecordingAllowed()) {
379338
return
@@ -474,6 +433,30 @@ public class ReplayIntegration(
474433
}
475434
}
476435

436+
override fun onWindowSizeChanged(width: Int, height: Int) {
437+
if (!isEnabled.get() || !isRecording()) {
438+
return
439+
}
440+
if (options.sessionReplay.isTrackConfiguration) {
441+
val recorderConfig =
442+
ScreenshotRecorderConfig.fromSize(context, options.sessionReplay, width, height)
443+
onConfigurationChanged(recorderConfig)
444+
}
445+
}
446+
447+
public fun onConfigurationChanged(config: ScreenshotRecorderConfig) {
448+
if (!isEnabled.get() || !isRecording()) {
449+
return
450+
}
451+
captureStrategy?.onConfigurationChanged(config)
452+
recorder?.onConfigurationChanged(config)
453+
454+
// we have to restart recorder with a new config and pause immediately if the replay is paused
455+
if (lifecycle.currentState == PAUSED) {
456+
recorder?.pause()
457+
}
458+
}
459+
477460
private class PreviousReplayHint : Backfillable {
478461
override fun shouldEnrich(): Boolean = false
479462
}

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

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@ import android.graphics.Canvas
88
import android.graphics.Color
99
import android.graphics.Matrix
1010
import android.graphics.Paint
11-
import android.graphics.Point
1211
import android.graphics.Rect
1312
import android.graphics.RectF
14-
import android.os.Build.VERSION
15-
import android.os.Build.VERSION_CODES
1613
import android.view.PixelCopy
1714
import android.view.View
1815
import android.view.ViewTreeObserver
19-
import android.view.WindowManager
2016
import io.sentry.SentryLevel.DEBUG
2117
import io.sentry.SentryLevel.INFO
2218
import io.sentry.SentryLevel.WARNING
@@ -196,6 +192,9 @@ internal class ScreenshotRecorder(
196192
}
197193

198194
override fun onDraw() {
195+
if (!isCapturing.get()) {
196+
return
197+
}
199198
val root = rootView?.get()
200199
if (root == null || root.width <= 0 || root.height <= 0 || !root.isShown) {
201200
options.logger.log(DEBUG, "Root view is invalid, not capturing screenshot")
@@ -303,35 +302,26 @@ public data class ScreenshotRecorderConfig(
303302
}
304303
}
305304

306-
fun from(
305+
fun fromSize(
307306
context: Context,
308-
sessionReplay: SentryReplayOptions
307+
sessionReplay: SentryReplayOptions,
308+
windowWidth: Int,
309+
windowHeight: Int
309310
): ScreenshotRecorderConfig {
310-
// PixelCopy takes screenshots including system bars, so we have to get the real size here
311-
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
312-
val screenBounds = if (VERSION.SDK_INT >= VERSION_CODES.R) {
313-
wm.currentWindowMetrics.bounds
314-
} else {
315-
val screenBounds = Point()
316-
@Suppress("DEPRECATION")
317-
wm.defaultDisplay.getRealSize(screenBounds)
318-
Rect(0, 0, screenBounds.x, screenBounds.y)
319-
}
320-
321311
// use the baseline density of 1x (mdpi)
322312
val (height, width) =
323-
((screenBounds.height() / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
313+
((windowHeight / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
324314
.roundToInt()
325315
.adjustToBlockSize() to
326-
((screenBounds.width() / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
316+
((windowWidth / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
327317
.roundToInt()
328318
.adjustToBlockSize()
329319

330320
return ScreenshotRecorderConfig(
331321
recordingWidth = width,
332322
recordingHeight = height,
333-
scaleFactorX = width.toFloat() / screenBounds.width(),
334-
scaleFactorY = height.toFloat() / screenBounds.height(),
323+
scaleFactorX = width.toFloat() / windowWidth,
324+
scaleFactorY = height.toFloat() / windowHeight,
335325
frameRate = sessionReplay.frameRate,
336326
bitRate = sessionReplay.quality.bitRate
337327
)
@@ -360,3 +350,10 @@ public interface ScreenshotRecorderCallback {
360350
*/
361351
public fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long)
362352
}
353+
354+
/**
355+
* A callback to be invoked when once current window size is determined or changes
356+
*/
357+
public interface WindowCallback {
358+
public fun onWindowSizeChanged(width: Int, height: Int)
359+
}

0 commit comments

Comments
 (0)