Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

## Unreleased

### Features

- Session Replay: Add `Sentry.replay().pauseTouchTracking()` and `resumeTouchTracking()` to stop recording touches on sensitive screens, such as a custom PIN pad, while the screen recording continues. Touch tracking stays paused until `resumeTouchTracking()` is called, also for new replays and after `resume()` ([#6546](https://github.com/getsentry/sentry-java/pull/6546))

### Performance

- Most SDK log calls no longer allocate when `debug` is off ([#6600](https://github.com/getsentry/sentry-java/pull/6600))
Expand Down
2 changes: 2 additions & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ public final class io/sentry/android/replay/ReplayIntegration : io/sentry/IConne
public fun onTouchEvent (Landroid/view/MotionEvent;)V
public fun onWindowSizeChanged (II)V
public fun pause ()V
public fun pauseTouchTracking ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
public fun registerSegmentName (Ljava/lang/String;)V
public fun registerTraceId (Lio/sentry/protocol/SentryId;)V
public fun resume ()V
public fun resumeTouchTracking ()V
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
public fun start ()V
public fun startBuffering ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public class ReplayIntegration(
}

private var lastKnownConnectionStatus: ConnectionStatus = ConnectionStatus.UNKNOWN
private var debugMaskingEnabled: Boolean = false
@Volatile private var debugMaskingEnabled: Boolean = false
private lateinit var options: SentryOptions
private var scopes: IScopes? = null
private var recorder: Recorder? = null
Expand All @@ -135,6 +135,11 @@ public class ReplayIntegration(

internal val isEnabled = AtomicBoolean(false)
internal var isManualPause = false
// Not reset by start()/stop(), only resumeTouchTracking() clears it, so touches cannot leak
// by accident. Set directly instead of enqueueOnMainThread like lifecycle commands: it does not
// touch ReplayState, so ordering does not matter, and a queued set could let already-pending
// touch events through.
@Volatile private var isTouchTrackingPaused = false
public val replayCacheDir: File?
get() = state.get().captureStrategy?.replayCacheDir

Expand Down Expand Up @@ -397,6 +402,14 @@ public class ReplayIntegration(
}
}

override fun pauseTouchTracking() {
isTouchTrackingPaused = true
}

override fun resumeTouchTracking() {
isTouchTrackingPaused = false
}

override fun enableDebugMaskingOverlay() {
debugMaskingEnabled = true
}
Expand Down Expand Up @@ -606,7 +619,7 @@ public class ReplayIntegration(

override fun onTouchEvent(event: MotionEvent) {
val current = state.get()
if (!isEnabled.get() || !current.isTouchRecordingAllowed) {
if (!isEnabled.get() || isTouchTrackingPaused || !current.isTouchRecordingAllowed) {
return
}
current.captureStrategy?.onTouchEvent(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,93 @@ class ReplayIntegrationTest {
verify(captureStrategy, never()).onTouchEvent(any())
}

@Test
fun `pauseTouchTracking drops touches but keeps recording screenshots`() {
val captureStrategy = mock<CaptureStrategy>()
val recorder = mock<Recorder>()
val replay =
fixture.getSut(
context,
recorderProvider = { recorder },
replayCaptureStrategyProvider = { captureStrategy },
)

replay.register(fixture.scopes, fixture.options)
replay.start()
replay.pauseTouchTracking()
replay.onTouchEvent(mock())

verify(captureStrategy, never()).onTouchEvent(any())
verify(recorder, never()).pause()
verify(captureStrategy, never()).pause()
assertTrue(replay.isRecording)

replay.resumeTouchTracking()
replay.onTouchEvent(mock())

verify(captureStrategy).onTouchEvent(any())
}

@Test
fun `resume does not resume paused touch tracking`() {
val captureStrategy = mock<CaptureStrategy>()
val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy })

replay.register(fixture.scopes, fixture.options)
replay.start()
replay.pauseTouchTracking()
replay.pause()
replay.resume()
replay.onTouchEvent(mock())

verify(captureStrategy, never()).onTouchEvent(any())
}

@Test
fun `resumeTouchTracking does not record touches while replay is paused`() {
val captureStrategy = mock<CaptureStrategy>()
val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy })

replay.register(fixture.scopes, fixture.options)
replay.start()
replay.pauseTouchTracking()
replay.pause()
replay.resumeTouchTracking()
replay.onTouchEvent(mock())

verify(captureStrategy, never()).onTouchEvent(any())

replay.resume()
replay.onTouchEvent(mock())

verify(captureStrategy).onTouchEvent(any())
}

@Test
fun `paused touch tracking stays paused for new replays`() {
val firstStrategy = mock<CaptureStrategy>()
val secondStrategy = mock<CaptureStrategy>()
val thirdStrategy = mock<CaptureStrategy>()
val strategies = ArrayDeque(listOf(firstStrategy, secondStrategy, thirdStrategy))
val replay =
fixture.getSut(context, replayCaptureStrategyProvider = { strategies.removeFirst() })

replay.register(fixture.scopes, fixture.options)
// called before any replay is started
replay.pauseTouchTracking()
replay.onAppForegrounded(true)
replay.onTouchEvent(mock())
replay.onAppForegrounded(true)
replay.onTouchEvent(mock())
replay.stop()
replay.start()
replay.onTouchEvent(mock())

verify(firstStrategy, never()).onTouchEvent(any())
verify(secondStrategy, never()).onTouchEvent(any())
verify(thirdStrategy, never()).onTouchEvent(any())
}

@Test
fun `when paused manually onConnectionStatusChanged does not resume`() {
val captureStrategy = getSessionCaptureStrategy(fixture.options)
Expand Down
4 changes: 4 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,9 @@ public abstract interface class io/sentry/IReplayApi {
public abstract fun enableDebugMaskingOverlay ()V
public abstract fun flush ()V
public abstract fun pause ()V
public abstract fun pauseTouchTracking ()V
public abstract fun resume ()V
public abstract fun resumeTouchTracking ()V
public abstract fun start ()V
public abstract fun startBuffering ()V
public abstract fun stop ()V
Expand Down Expand Up @@ -1842,9 +1844,11 @@ public final class io/sentry/NoOpReplayController : io/sentry/ReplayController {
public fun onAppForegrounded (Z)V
public fun onAppSessionEnded ()V
public fun pause ()V
public fun pauseTouchTracking ()V
public fun registerSegmentName (Ljava/lang/String;)V
public fun registerTraceId (Lio/sentry/protocol/SentryId;)V
public fun resume ()V
public fun resumeTouchTracking ()V
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
public fun start ()V
public fun startBuffering ()V
Expand Down
17 changes: 17 additions & 0 deletions sentry/src/main/java/io/sentry/IReplayApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ public interface IReplayApi {
/** Resumes a replay paused with {@link #pause()}. */
void resume();

/**
* Stops recording touch events until {@link #resumeTouchTracking()} is called. Screenshots are
* still recorded. This can be used on sensitive screens, such as a custom PIN pad, where the
* screen is masked but the touch positions could reveal the input.
*
* <p>Touch tracking stays paused for all replays in the same process, also if the replay is
* stopped and started again. {@link #resume()} does not resume touch tracking. Has effect
* immediately, also before a replay is started.
*/
void pauseTouchTracking();

/**
* Resumes touch tracking paused with {@link #pauseTouchTracking()}. Touch events are recorded
* again only while the replay is not paused with {@link #pause()}.
*/
void resumeTouchTracking();

/**
* Immediately sends the current replay data to Sentry in either session or buffer mode. A
* buffering replay continues in session mode after the buffer is sent. If replay is not
Expand Down
6 changes: 6 additions & 0 deletions sentry/src/main/java/io/sentry/NoOpReplayController.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public void pause() {}
@Override
public void resume() {}

@Override
public void pauseTouchTracking() {}

@Override
public void resumeTouchTracking() {}

@Override
public void flush() {}

Expand Down
Loading