diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e8119a42..2e43575525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- Fix potential ANR/deadlock in Session Replay when `checkCanRecord` runs on the replay executor thread ([#5837](https://github.com/getsentry/sentry-java/pull/5837)) - Prevent concurrent PixelCopy access during Session Replay masking and bitmap cleanup ([#5808](https://github.com/getsentry/sentry-java/pull/5808)) - Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607)) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index d9cd15d891..5134ddc03b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.os.Build +import android.os.Looper import android.view.MotionEvent import io.sentry.Breadcrumb import io.sentry.DataCategory.All @@ -352,7 +353,7 @@ public class ReplayIntegration( } addFrame(bitmap, frameTimeStamp, screen) } - checkCanRecord() + postOnMainThread { checkCanRecord() } } override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) { @@ -375,7 +376,7 @@ public class ReplayIntegration( } addFrame(screenshot, frameTimestamp, screen) } - checkCanRecord() + postOnMainThread { checkCanRecord() } } override fun close() { @@ -444,6 +445,17 @@ public class ReplayIntegration( captureStrategy?.onTouchEvent(event) } + // Runs [block] on the main thread. If already there, executes inline; otherwise posts via + // the main looper handler. Prevents deadlocks when lifecycle-lock-acquiring code (e.g. + // checkCanRecord -> pauseInternal) is called from the replay executor thread. + private inline fun postOnMainThread(crossinline block: () -> Unit) { + if (Looper.myLooper() == Looper.getMainLooper()) { + block() + } else { + mainLooperHandler.post { block() } + } + } + /** * Check if we're offline or rate-limited and pause for session mode to not overflow the envelope * cache. diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt index c26e6be9c4..b84b1b5334 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt @@ -23,6 +23,7 @@ import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider +import io.sentry.transport.RateLimiter import java.time.Duration import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -41,6 +42,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -61,11 +63,17 @@ class ReplaySmokeTest { internal class Fixture { val options = SentryOptions() val scope = Scope(options) + val rateLimiter = + mock { + on { isActiveForCategory(any()) }.thenReturn(false) + } val scopes = mock { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + + on { rateLimiter }.doReturn(rateLimiter) } private class ImmediateHandler : @@ -91,7 +99,10 @@ class ReplaySmokeTest { mainLooperHandler = mock { whenever(mock.handler).thenReturn(ImmediateHandler()) - whenever(mock.post(any())).then { (it.arguments[0] as Runnable).run() } + whenever(mock.post(any())).then { + (it.arguments[0] as Runnable).run() + true + } whenever(mock.postDelayed(any(), anyLong())).then { // have to use another thread here otherwise it will block the test thread recordingThread.schedule(