Skip to content
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