Skip to content

Commit 48b60cb

Browse files
authored
Merge c4a46ad into b779765
2 parents b779765 + c4a46ad commit 48b60cb

File tree

11 files changed

+208
-43
lines changed

11 files changed

+208
-43
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Correctly capture Dialogs and non full-sized windows ([#4354](https://github.com/getsentry/sentry-java/pull/4354))
8+
39
## 8.9.0
410

511
### Features

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
5050
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
5151
}
5252

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 {
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/WindowCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable {
5454
public static final field $stable I
5555
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
5656
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
@@ -68,6 +68,7 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
6868
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
6969
public fun onScreenshotRecorded (Ljava/io/File;J)V
7070
public fun onTouchEvent (Landroid/view/MotionEvent;)V
71+
public fun onWindowSizeChanged (II)V
7172
public fun pause ()V
7273
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
7374
public fun resume ()V
@@ -121,6 +122,10 @@ public final class io/sentry/android/replay/ViewExtensionsKt {
121122
public static final fun sentryReplayUnmask (Landroid/view/View;)V
122123
}
123124

125+
public abstract interface class io/sentry/android/replay/WindowCallback {
126+
public abstract fun onWindowSizeChanged (II)V
127+
}
128+
124129
public abstract interface class io/sentry/android/replay/gestures/TouchRecorderCallback {
125130
public abstract fun onTouchEvent (Landroid/view/MotionEvent;)V
126131
}

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public class ReplayIntegration(
6969
ReplayController,
7070
ComponentCallbacks,
7171
IConnectionStatusObserver,
72-
IRateLimitObserver {
72+
IRateLimitObserver,
73+
WindowCallback {
7374

7475
private companion object {
7576
init {
@@ -139,7 +140,7 @@ public class ReplayIntegration(
139140
}
140141

141142
this.scopes = scopes
142-
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler, replayExecutor)
143+
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, this, mainLooperHandler, replayExecutor)
143144
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
144145
isEnabled.set(true)
145146

@@ -183,15 +184,12 @@ public class ReplayIntegration(
183184
return
184185
}
185186

186-
val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
187187
captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) {
188188
SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, replayCacheProvider)
189189
} else {
190190
BufferCaptureStrategy(options, scopes, dateProvider, random, replayExecutor, replayCacheProvider)
191191
}
192192

193-
captureStrategy?.start(recorderConfig)
194-
recorder?.start(recorderConfig)
195193
registerRootViewListeners()
196194
lifecycle.currentState = STARTED
197195
}
@@ -322,17 +320,16 @@ public class ReplayIntegration(
322320
return
323321
}
324322

325-
recorder?.stop()
326-
327-
// refresh config based on new device configuration
328-
val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
329-
captureStrategy?.onConfigurationChanged(recorderConfig)
330-
331-
recorder?.start(recorderConfig)
332-
// we have to restart recorder with a new config and pause immediately if the replay is paused
333-
if (lifecycle.currentState == PAUSED) {
334-
recorder?.pause()
323+
captureStrategy?.stop()
324+
recorder?.let {
325+
it.stop()
326+
if (it is ConfigurationChangedListener) {
327+
it.onConfigurationChanged()
328+
}
335329
}
330+
331+
// once the window size is determined
332+
// onWindowSizeChanged is triggered and we'll start the actual capturing
336333
}
337334

338335
override fun onConnectionStatusChanged(status: ConnectionStatus) {
@@ -464,6 +461,31 @@ public class ReplayIntegration(
464461
}
465462
}
466463

464+
override fun onWindowSizeChanged(width: Int, height: Int) {
465+
if (!isEnabled.get() || !isRecording()) {
466+
return
467+
}
468+
469+
recorder?.stop()
470+
471+
val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.fromSize(context, options.sessionReplay, width, height)
472+
473+
captureStrategy?.let { capture ->
474+
if (capture.currentReplayId == SentryId.EMPTY_ID) {
475+
capture.start(recorderConfig)
476+
} else {
477+
capture.onConfigurationChanged(recorderConfig)
478+
}
479+
}
480+
recorder?.start(recorderConfig)
481+
482+
// we have to restart recorder with a new config and pause immediately if the replay is paused
483+
if (lifecycle.currentState == PAUSED) {
484+
recorder?.pause()
485+
captureStrategy?.pause()
486+
}
487+
}
488+
467489
private class PreviousReplayHint : Backfillable {
468490
override fun shouldEnrich(): Boolean = false
469491
}

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
@@ -7,15 +7,11 @@ import android.graphics.Canvas
77
import android.graphics.Color
88
import android.graphics.Matrix
99
import android.graphics.Paint
10-
import android.graphics.Point
1110
import android.graphics.Rect
1211
import android.graphics.RectF
13-
import android.os.Build.VERSION
14-
import android.os.Build.VERSION_CODES
1512
import android.view.PixelCopy
1613
import android.view.View
1714
import android.view.ViewTreeObserver
18-
import android.view.WindowManager
1915
import io.sentry.SentryLevel.DEBUG
2016
import io.sentry.SentryLevel.INFO
2117
import io.sentry.SentryLevel.WARNING
@@ -177,6 +173,9 @@ internal class ScreenshotRecorder(
177173
}
178174

179175
override fun onDraw() {
176+
if (!isCapturing.get()) {
177+
return
178+
}
180179
val root = rootView?.get()
181180
if (root == null || root.width <= 0 || root.height <= 0 || !root.isShown) {
182181
options.logger.log(DEBUG, "Root view is invalid, not capturing screenshot")
@@ -280,35 +279,26 @@ public data class ScreenshotRecorderConfig(
280279
}
281280
}
282281

283-
fun from(
282+
fun fromSize(
284283
context: Context,
285-
sessionReplay: SentryReplayOptions
284+
sessionReplay: SentryReplayOptions,
285+
windowWidth: Int,
286+
windowHeight: Int
286287
): ScreenshotRecorderConfig {
287-
// PixelCopy takes screenshots including system bars, so we have to get the real size here
288-
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
289-
val screenBounds = if (VERSION.SDK_INT >= VERSION_CODES.R) {
290-
wm.currentWindowMetrics.bounds
291-
} else {
292-
val screenBounds = Point()
293-
@Suppress("DEPRECATION")
294-
wm.defaultDisplay.getRealSize(screenBounds)
295-
Rect(0, 0, screenBounds.x, screenBounds.y)
296-
}
297-
298288
// use the baseline density of 1x (mdpi)
299289
val (height, width) =
300-
((screenBounds.height() / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
290+
((windowHeight / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
301291
.roundToInt()
302292
.adjustToBlockSize() to
303-
((screenBounds.width() / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
293+
((windowWidth / context.resources.displayMetrics.density) * sessionReplay.quality.sizeScale)
304294
.roundToInt()
305295
.adjustToBlockSize()
306296

307297
return ScreenshotRecorderConfig(
308298
recordingWidth = width,
309299
recordingHeight = height,
310-
scaleFactorX = width.toFloat() / screenBounds.width(),
311-
scaleFactorY = height.toFloat() / screenBounds.height(),
300+
scaleFactorX = width.toFloat() / windowWidth,
301+
scaleFactorY = height.toFloat() / windowHeight,
312302
frameRate = sessionReplay.frameRate,
313303
bitRate = sessionReplay.quality.bitRate
314304
)
@@ -337,3 +327,10 @@ public interface ScreenshotRecorderCallback {
337327
*/
338328
public fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long)
339329
}
330+
331+
/**
332+
* A callback to be invoked when once current window size is determined or changes
333+
*/
334+
public interface WindowCallback {
335+
public fun onWindowSizeChanged(width: Int, height: Int)
336+
}

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

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

33
import android.annotation.TargetApi
4+
import android.graphics.Point
45
import android.view.View
6+
import android.view.ViewTreeObserver
57
import io.sentry.SentryOptions
68
import io.sentry.android.replay.util.MainLooperHandler
9+
import io.sentry.android.replay.util.addOnDrawListenerSafe
710
import io.sentry.android.replay.util.gracefullyShutdown
11+
import io.sentry.android.replay.util.hasSize
12+
import io.sentry.android.replay.util.removeOnDrawListenerSafe
813
import io.sentry.android.replay.util.scheduleAtFixedRateSafely
914
import io.sentry.util.AutoClosableReentrantLock
1015
import java.lang.ref.WeakReference
@@ -19,16 +24,18 @@ import java.util.concurrent.atomic.AtomicBoolean
1924
internal class WindowRecorder(
2025
private val options: SentryOptions,
2126
private val screenshotRecorderCallback: ScreenshotRecorderCallback? = null,
27+
private val windowCallback: WindowCallback,
2228
private val mainLooperHandler: MainLooperHandler,
2329
private val replayExecutor: ScheduledExecutorService
24-
) : Recorder, OnRootViewsChangedListener {
30+
) : Recorder, OnRootViewsChangedListener, ConfigurationChangedListener {
2531

2632
internal companion object {
2733
private const val TAG = "WindowRecorder"
2834
}
2935

3036
private val isRecording = AtomicBoolean(false)
3137
private val rootViews = ArrayList<WeakReference<View>>()
38+
private var lastKnownWindowSize: Point = Point()
3239
private val rootViewsLock = AutoClosableReentrantLock()
3340
private var recorder: ScreenshotRecorder? = null
3441
private var capturingTask: ScheduledFuture<*>? = null
@@ -41,26 +48,64 @@ internal class WindowRecorder(
4148
if (added) {
4249
rootViews.add(WeakReference(root))
4350
recorder?.bind(root)
51+
determineWindowSize(root)
4452
} else {
4553
recorder?.unbind(root)
4654
rootViews.removeAll { it.get() == root }
4755

4856
val newRoot = rootViews.lastOrNull()?.get()
4957
if (newRoot != null && root != newRoot) {
5058
recorder?.bind(newRoot)
59+
determineWindowSize(newRoot)
5160
} else {
5261
Unit // synchronized block wants us to return something lol
5362
}
5463
}
5564
}
5665
}
5766

67+
fun determineWindowSize(root: View) {
68+
if (root.hasSize()) {
69+
if (root.width != lastKnownWindowSize.x && root.height != lastKnownWindowSize.y) {
70+
lastKnownWindowSize.set(root.width, root.height)
71+
windowCallback.onWindowSizeChanged(root.width, root.height)
72+
}
73+
} else {
74+
root.addOnDrawListenerSafe(object : ViewTreeObserver.OnDrawListener {
75+
override fun onDraw() {
76+
val currentRoot = rootViews.lastOrNull()?.get()
77+
if (root != currentRoot) {
78+
return
79+
}
80+
if (root.hasSize()) {
81+
if (root.width != lastKnownWindowSize.x && root.height != lastKnownWindowSize.y) {
82+
lastKnownWindowSize.set(root.width, root.height)
83+
windowCallback.onWindowSizeChanged(root.width, root.height)
84+
}
85+
root.removeOnDrawListenerSafe(this)
86+
}
87+
}
88+
})
89+
}
90+
}
91+
5892
override fun start(recorderConfig: ScreenshotRecorderConfig) {
5993
if (isRecording.getAndSet(true)) {
6094
return
6195
}
6296

63-
recorder = ScreenshotRecorder(recorderConfig, options, mainLooperHandler, replayExecutor, screenshotRecorderCallback)
97+
recorder = ScreenshotRecorder(
98+
recorderConfig,
99+
options,
100+
mainLooperHandler,
101+
replayExecutor,
102+
screenshotRecorderCallback
103+
)
104+
105+
val newRoot = rootViews.lastOrNull()?.get()
106+
if (newRoot != null) {
107+
recorder?.bind(newRoot)
108+
}
64109
// TODO: change this to use MainThreadHandler and just post on the main thread with delay
65110
// to avoid thread context switch every time
66111
capturingTask = capturer.scheduleAtFixedRateSafely(
@@ -77,15 +122,12 @@ internal class WindowRecorder(
77122
override fun resume() {
78123
recorder?.resume()
79124
}
125+
80126
override fun pause() {
81127
recorder?.pause()
82128
}
83129

84130
override fun stop() {
85-
rootViewsLock.acquire().use {
86-
rootViews.forEach { recorder?.unbind(it.get()) }
87-
rootViews.clear()
88-
}
89131
recorder?.close()
90132
recorder = null
91133
capturingTask?.cancel(false)
@@ -94,10 +136,19 @@ internal class WindowRecorder(
94136
}
95137

96138
override fun close() {
139+
onConfigurationChanged()
97140
stop()
98141
capturer.gracefullyShutdown(options)
99142
}
100143

144+
override fun onConfigurationChanged() {
145+
lastKnownWindowSize.set(0, 0)
146+
rootViewsLock.acquire().use {
147+
rootViews.forEach { recorder?.unbind(it.get()) }
148+
rootViews.clear()
149+
}
150+
}
151+
101152
private class RecorderExecutorServiceThreadFactory : ThreadFactory {
102153
private var cnt = 0
103154
override fun newThread(r: Runnable): Thread {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ internal fun interface OnRootViewsChangedListener {
118118
)
119119
}
120120

121+
internal fun interface ConfigurationChangedListener {
122+
/**
123+
* Called whenever the device configuration changes
124+
*/
125+
fun onConfigurationChanged()
126+
}
127+
121128
/**
122129
* A utility that holds the list of root views that WindowManager updates.
123130
*/

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,7 @@ internal fun View?.removeOnDrawListenerSafe(listener: ViewTreeObserver.OnDrawLis
201201
// viewTreeObserver is already dead
202202
}
203203
}
204+
205+
internal fun View.hasSize(): Boolean {
206+
return width != 0 && height != 0
207+
}

0 commit comments

Comments
 (0)