Skip to content

fix(breadcrumbs): Unregister SystemEventsBroadcastReceiver when entering background #4338

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 14 commits into from
Apr 17, 2025
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
### Fixes

- Fix TTFD measurement when API called too early ([#4297](https://github.com/getsentry/sentry-java/pull/4297))
- Fix unregister `SystemEventsBroadcastReceiver` when entering background ([#4338](https://github.com/getsentry/sentry-java/pull/4338))
- This should reduce ANRs seen with this class in the stack trace for Android 14 and above

## 8.8.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
options
.getLogger()
.log(
SentryLevel.INFO,
"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed",
e);
SentryLevel.WARNING,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but I just noticed while testing no androidx.lifecycle case that it's noisy and decided to change that

"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed");
} catch (IllegalStateException e) {
options
.getLogger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import io.sentry.Breadcrumb;
import io.sentry.Hint;
import io.sentry.IScopes;
Expand All @@ -33,6 +37,7 @@
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.android.core.internal.util.Debouncer;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.Objects;
Expand All @@ -51,29 +56,46 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl

private final @NotNull Context context;

@TestOnly @Nullable SystemEventsBroadcastReceiver receiver;
@TestOnly @Nullable volatile SystemEventsBroadcastReceiver receiver;

@TestOnly @Nullable volatile ReceiverLifecycleHandler lifecycleHandler;

private final @NotNull MainLooperHandler handler;

private @Nullable SentryAndroidOptions options;

private @Nullable IScopes scopes;

private final @NotNull String[] actions;
private boolean isClosed = false;
private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock();
private volatile boolean isClosed = false;
private volatile boolean isStopped = false;
private volatile IntentFilter filter = null;
private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock();

public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
this(context, getDefaultActionsInternal());
}

private SystemEventsBreadcrumbsIntegration(
final @NotNull Context context, final @NotNull String[] actions) {
this(context, actions, new MainLooperHandler());
}

SystemEventsBreadcrumbsIntegration(
final @NotNull Context context,
final @NotNull String[] actions,
final @NotNull MainLooperHandler handler) {
this.context = ContextUtils.getApplicationContext(context);
this.actions = actions;
this.handler = handler;
}

public SystemEventsBreadcrumbsIntegration(
final @NotNull Context context, final @NotNull List<String> actions) {
this.context = ContextUtils.getApplicationContext(context);
this.actions = new String[actions.size()];
actions.toArray(this.actions);
this.handler = new MainLooperHandler();
}

@Override
Expand All @@ -83,6 +105,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
Objects.requireNonNull(
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
"SentryAndroidOptions is required");
this.scopes = scopes;

this.options
.getLogger()
Expand All @@ -92,46 +115,170 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
this.options.isEnableSystemEventBreadcrumbs());

if (this.options.isEnableSystemEventBreadcrumbs()) {
addLifecycleObserver(this.options);
registerReceiver(this.scopes, this.options, /* reportAsNewIntegration = */ true);
}
}

try {
options
.getExecutorService()
.submit(
() -> {
try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
if (!isClosed) {
startSystemEventsReceiver(scopes, (SentryAndroidOptions) options);
private void registerReceiver(
final @NotNull IScopes scopes,
final @NotNull SentryAndroidOptions options,
final boolean reportAsNewIntegration) {

if (!options.isEnableSystemEventBreadcrumbs()) {
return;
}

try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
if (isClosed || isStopped || receiver != null) {
return;
}
}

try {
options
.getExecutorService()
.submit(
() -> {
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
if (isClosed || isStopped || receiver != null) {
return;
}

receiver = new SystemEventsBroadcastReceiver(scopes, options);
if (filter == null) {
filter = new IntentFilter();
for (String item : actions) {
filter.addAction(item);
}
}
});
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Failed to start SystemEventsBreadcrumbsIntegration on executor thread.",
e);
}
try {
// registerReceiver can throw SecurityException but it's not documented in the
// official docs
ContextUtils.registerReceiver(context, options, receiver, filter);
if (reportAsNewIntegration) {
options
.getLogger()
.log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed.");
addIntegrationToSdkVersion("SystemEventsBreadcrumbs");
}
} catch (Throwable e) {
options.setEnableSystemEventBreadcrumbs(false);
options
.getLogger()
.log(
SentryLevel.ERROR,
"Failed to initialize SystemEventsBreadcrumbsIntegration.",
e);
}
}
});
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"Failed to start SystemEventsBreadcrumbsIntegration on executor thread.");
}
}

private void startSystemEventsReceiver(
final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
receiver = new SystemEventsBroadcastReceiver(scopes, options);
final IntentFilter filter = new IntentFilter();
for (String item : actions) {
filter.addAction(item);
private void unregisterReceiver() {
final @Nullable SystemEventsBroadcastReceiver receiverRef;
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
isStopped = true;
receiverRef = receiver;
receiver = null;
}

if (receiverRef != null) {
context.unregisterReceiver(receiverRef);
}
}

// TODO: this duplicates a lot of AppLifecycleIntegration. We should register once on init
// and multiplex to different listeners rather.
private void addLifecycleObserver(final @NotNull SentryAndroidOptions options) {
try {
// registerReceiver can throw SecurityException but it's not documented in the official docs
ContextUtils.registerReceiver(context, options, receiver, filter);
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed.");
addIntegrationToSdkVersion("SystemEventsBreadcrumbs");
Class.forName("androidx.lifecycle.DefaultLifecycleObserver");
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
if (AndroidThreadChecker.getInstance().isMainThread()) {
addObserverInternal(options);
} else {
// some versions of the androidx lifecycle-process require this to be executed on the main
// thread.
handler.post(() -> addObserverInternal(options));
}
} catch (ClassNotFoundException e) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"androidx.lifecycle is not available, SystemEventsBreadcrumbsIntegration won't be able"
+ " to register/unregister an internal BroadcastReceiver. This may result in an"
+ " increased ANR rate on Android 14 and above.");
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"SystemEventsBreadcrumbsIntegration could not register lifecycle observer",
e);
}
}

private void addObserverInternal(final @NotNull SentryAndroidOptions options) {
lifecycleHandler = new ReceiverLifecycleHandler();

try {
ProcessLifecycleOwner.get().getLifecycle().addObserver(lifecycleHandler);
} catch (Throwable e) {
options.setEnableSystemEventBreadcrumbs(false);
// This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
// connection with conflicting dependencies of the androidx.lifecycle.
// //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
lifecycleHandler = null;
options
.getLogger()
.log(SentryLevel.ERROR, "Failed to initialize SystemEventsBreadcrumbsIntegration.", e);
.log(
SentryLevel.ERROR,
"SystemEventsBreadcrumbsIntegration failed to get Lifecycle and could not install lifecycle observer.",
e);
}
}

private void removeLifecycleObserver() {
if (lifecycleHandler != null) {
if (AndroidThreadChecker.getInstance().isMainThread()) {
removeObserverInternal();
} else {
// some versions of the androidx lifecycle-process require this to be executed on the main
// thread.
// avoid method refs on Android due to some issues with older AGP setups
// noinspection Convert2MethodRef
handler.post(() -> removeObserverInternal());
}
}
}

private void removeObserverInternal() {
final @Nullable ReceiverLifecycleHandler watcherRef = lifecycleHandler;
if (watcherRef != null) {
ProcessLifecycleOwner.get().getLifecycle().removeObserver(watcherRef);
}
lifecycleHandler = null;
}

@Override
public void close() throws IOException {
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
isClosed = true;
filter = null;
}

removeLifecycleObserver();
unregisterReceiver();

if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration remove.");
}
}

Expand Down Expand Up @@ -164,18 +311,23 @@ private void startSystemEventsReceiver(
return actions;
}

@Override
public void close() throws IOException {
try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
isClosed = true;
}
if (receiver != null) {
context.unregisterReceiver(receiver);
receiver = null;
final class ReceiverLifecycleHandler implements DefaultLifecycleObserver {
@Override
public void onStart(@NonNull LifecycleOwner owner) {
if (scopes == null || options == null) {
return;
}

if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration remove.");
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
isStopped = false;
}

registerReceiver(scopes, options, /* reportAsNewIntegration = */ false);
}

@Override
public void onStop(@NonNull LifecycleOwner owner) {
unregisterReceiver();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import androidx.lifecycle.Lifecycle.Event.ON_START
import androidx.lifecycle.Lifecycle.Event.ON_STOP
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.CheckIn
Expand All @@ -24,7 +25,6 @@ import io.sentry.protocol.SentryId
import io.sentry.protocol.SentryTransaction
import io.sentry.transport.RateLimiter
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.robolectric.annotation.Config
import java.util.LinkedList
import kotlin.test.BeforeTest
Expand Down Expand Up @@ -116,7 +116,7 @@ class SessionTrackingIntegrationTest {
}

private fun setupLifecycle(options: SentryOptions): LifecycleRegistry {
val lifecycle = LifecycleRegistry(mock())
val lifecycle = LifecycleRegistry(ProcessLifecycleOwner.get())
val lifecycleWatcher = (
options.integrations.find {
it is AppLifecycleIntegration
Expand Down
Loading
Loading