Skip to content

Commit d9ab996

Browse files
authored
fix(breadcrumbs): Unregister SystemEventsBroadcastReceiver when entering background (#4338)
1 parent 084562d commit d9ab996

File tree

5 files changed

+388
-50
lines changed

5 files changed

+388
-50
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
### Fixes
1010

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

1315
## 8.8.0
1416

sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,8 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
6969
options
7070
.getLogger()
7171
.log(
72-
SentryLevel.INFO,
73-
"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed",
74-
e);
72+
SentryLevel.WARNING,
73+
"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed");
7574
} catch (IllegalStateException e) {
7675
options
7776
.getLogger()

sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java

Lines changed: 194 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import android.content.Intent;
2626
import android.content.IntentFilter;
2727
import android.os.Bundle;
28+
import androidx.annotation.NonNull;
29+
import androidx.lifecycle.DefaultLifecycleObserver;
30+
import androidx.lifecycle.LifecycleOwner;
31+
import androidx.lifecycle.ProcessLifecycleOwner;
2832
import io.sentry.Breadcrumb;
2933
import io.sentry.Hint;
3034
import io.sentry.IScopes;
@@ -33,6 +37,7 @@
3337
import io.sentry.SentryLevel;
3438
import io.sentry.SentryOptions;
3539
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
40+
import io.sentry.android.core.internal.util.AndroidThreadChecker;
3641
import io.sentry.android.core.internal.util.Debouncer;
3742
import io.sentry.util.AutoClosableReentrantLock;
3843
import io.sentry.util.Objects;
@@ -51,29 +56,46 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl
5156

5257
private final @NotNull Context context;
5358

54-
@TestOnly @Nullable SystemEventsBroadcastReceiver receiver;
59+
@TestOnly @Nullable volatile SystemEventsBroadcastReceiver receiver;
60+
61+
@TestOnly @Nullable volatile ReceiverLifecycleHandler lifecycleHandler;
62+
63+
private final @NotNull MainLooperHandler handler;
5564

5665
private @Nullable SentryAndroidOptions options;
5766

67+
private @Nullable IScopes scopes;
68+
5869
private final @NotNull String[] actions;
59-
private boolean isClosed = false;
60-
private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock();
70+
private volatile boolean isClosed = false;
71+
private volatile boolean isStopped = false;
72+
private volatile IntentFilter filter = null;
73+
private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock();
6174

6275
public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
6376
this(context, getDefaultActionsInternal());
6477
}
6578

6679
private SystemEventsBreadcrumbsIntegration(
6780
final @NotNull Context context, final @NotNull String[] actions) {
81+
this(context, actions, new MainLooperHandler());
82+
}
83+
84+
SystemEventsBreadcrumbsIntegration(
85+
final @NotNull Context context,
86+
final @NotNull String[] actions,
87+
final @NotNull MainLooperHandler handler) {
6888
this.context = ContextUtils.getApplicationContext(context);
6989
this.actions = actions;
90+
this.handler = handler;
7091
}
7192

7293
public SystemEventsBreadcrumbsIntegration(
7394
final @NotNull Context context, final @NotNull List<String> actions) {
7495
this.context = ContextUtils.getApplicationContext(context);
7596
this.actions = new String[actions.size()];
7697
actions.toArray(this.actions);
98+
this.handler = new MainLooperHandler();
7799
}
78100

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

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

94117
if (this.options.isEnableSystemEventBreadcrumbs()) {
118+
addLifecycleObserver(this.options);
119+
registerReceiver(this.scopes, this.options, /* reportAsNewIntegration = */ true);
120+
}
121+
}
95122

96-
try {
97-
options
98-
.getExecutorService()
99-
.submit(
100-
() -> {
101-
try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
102-
if (!isClosed) {
103-
startSystemEventsReceiver(scopes, (SentryAndroidOptions) options);
123+
private void registerReceiver(
124+
final @NotNull IScopes scopes,
125+
final @NotNull SentryAndroidOptions options,
126+
final boolean reportAsNewIntegration) {
127+
128+
if (!options.isEnableSystemEventBreadcrumbs()) {
129+
return;
130+
}
131+
132+
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
133+
if (isClosed || isStopped || receiver != null) {
134+
return;
135+
}
136+
}
137+
138+
try {
139+
options
140+
.getExecutorService()
141+
.submit(
142+
() -> {
143+
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
144+
if (isClosed || isStopped || receiver != null) {
145+
return;
146+
}
147+
148+
receiver = new SystemEventsBroadcastReceiver(scopes, options);
149+
if (filter == null) {
150+
filter = new IntentFilter();
151+
for (String item : actions) {
152+
filter.addAction(item);
104153
}
105154
}
106-
});
107-
} catch (Throwable e) {
108-
options
109-
.getLogger()
110-
.log(
111-
SentryLevel.DEBUG,
112-
"Failed to start SystemEventsBreadcrumbsIntegration on executor thread.",
113-
e);
114-
}
155+
try {
156+
// registerReceiver can throw SecurityException but it's not documented in the
157+
// official docs
158+
ContextUtils.registerReceiver(context, options, receiver, filter);
159+
if (reportAsNewIntegration) {
160+
options
161+
.getLogger()
162+
.log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed.");
163+
addIntegrationToSdkVersion("SystemEventsBreadcrumbs");
164+
}
165+
} catch (Throwable e) {
166+
options.setEnableSystemEventBreadcrumbs(false);
167+
options
168+
.getLogger()
169+
.log(
170+
SentryLevel.ERROR,
171+
"Failed to initialize SystemEventsBreadcrumbsIntegration.",
172+
e);
173+
}
174+
}
175+
});
176+
} catch (Throwable e) {
177+
options
178+
.getLogger()
179+
.log(
180+
SentryLevel.WARNING,
181+
"Failed to start SystemEventsBreadcrumbsIntegration on executor thread.");
115182
}
116183
}
117184

118-
private void startSystemEventsReceiver(
119-
final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
120-
receiver = new SystemEventsBroadcastReceiver(scopes, options);
121-
final IntentFilter filter = new IntentFilter();
122-
for (String item : actions) {
123-
filter.addAction(item);
185+
private void unregisterReceiver() {
186+
final @Nullable SystemEventsBroadcastReceiver receiverRef;
187+
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
188+
isStopped = true;
189+
receiverRef = receiver;
190+
receiver = null;
124191
}
192+
193+
if (receiverRef != null) {
194+
context.unregisterReceiver(receiverRef);
195+
}
196+
}
197+
198+
// TODO: this duplicates a lot of AppLifecycleIntegration. We should register once on init
199+
// and multiplex to different listeners rather.
200+
private void addLifecycleObserver(final @NotNull SentryAndroidOptions options) {
125201
try {
126-
// registerReceiver can throw SecurityException but it's not documented in the official docs
127-
ContextUtils.registerReceiver(context, options, receiver, filter);
128-
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed.");
129-
addIntegrationToSdkVersion("SystemEventsBreadcrumbs");
202+
Class.forName("androidx.lifecycle.DefaultLifecycleObserver");
203+
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
204+
if (AndroidThreadChecker.getInstance().isMainThread()) {
205+
addObserverInternal(options);
206+
} else {
207+
// some versions of the androidx lifecycle-process require this to be executed on the main
208+
// thread.
209+
handler.post(() -> addObserverInternal(options));
210+
}
211+
} catch (ClassNotFoundException e) {
212+
options
213+
.getLogger()
214+
.log(
215+
SentryLevel.WARNING,
216+
"androidx.lifecycle is not available, SystemEventsBreadcrumbsIntegration won't be able"
217+
+ " to register/unregister an internal BroadcastReceiver. This may result in an"
218+
+ " increased ANR rate on Android 14 and above.");
219+
} catch (Throwable e) {
220+
options
221+
.getLogger()
222+
.log(
223+
SentryLevel.ERROR,
224+
"SystemEventsBreadcrumbsIntegration could not register lifecycle observer",
225+
e);
226+
}
227+
}
228+
229+
private void addObserverInternal(final @NotNull SentryAndroidOptions options) {
230+
lifecycleHandler = new ReceiverLifecycleHandler();
231+
232+
try {
233+
ProcessLifecycleOwner.get().getLifecycle().addObserver(lifecycleHandler);
130234
} catch (Throwable e) {
131-
options.setEnableSystemEventBreadcrumbs(false);
235+
// This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
236+
// connection with conflicting dependencies of the androidx.lifecycle.
237+
// //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
238+
lifecycleHandler = null;
132239
options
133240
.getLogger()
134-
.log(SentryLevel.ERROR, "Failed to initialize SystemEventsBreadcrumbsIntegration.", e);
241+
.log(
242+
SentryLevel.ERROR,
243+
"SystemEventsBreadcrumbsIntegration failed to get Lifecycle and could not install lifecycle observer.",
244+
e);
245+
}
246+
}
247+
248+
private void removeLifecycleObserver() {
249+
if (lifecycleHandler != null) {
250+
if (AndroidThreadChecker.getInstance().isMainThread()) {
251+
removeObserverInternal();
252+
} else {
253+
// some versions of the androidx lifecycle-process require this to be executed on the main
254+
// thread.
255+
// avoid method refs on Android due to some issues with older AGP setups
256+
// noinspection Convert2MethodRef
257+
handler.post(() -> removeObserverInternal());
258+
}
259+
}
260+
}
261+
262+
private void removeObserverInternal() {
263+
final @Nullable ReceiverLifecycleHandler watcherRef = lifecycleHandler;
264+
if (watcherRef != null) {
265+
ProcessLifecycleOwner.get().getLifecycle().removeObserver(watcherRef);
266+
}
267+
lifecycleHandler = null;
268+
}
269+
270+
@Override
271+
public void close() throws IOException {
272+
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
273+
isClosed = true;
274+
filter = null;
275+
}
276+
277+
removeLifecycleObserver();
278+
unregisterReceiver();
279+
280+
if (options != null) {
281+
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration remove.");
135282
}
136283
}
137284

@@ -164,18 +311,23 @@ private void startSystemEventsReceiver(
164311
return actions;
165312
}
166313

167-
@Override
168-
public void close() throws IOException {
169-
try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
170-
isClosed = true;
171-
}
172-
if (receiver != null) {
173-
context.unregisterReceiver(receiver);
174-
receiver = null;
314+
final class ReceiverLifecycleHandler implements DefaultLifecycleObserver {
315+
@Override
316+
public void onStart(@NonNull LifecycleOwner owner) {
317+
if (scopes == null || options == null) {
318+
return;
319+
}
175320

176-
if (options != null) {
177-
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration remove.");
321+
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
322+
isStopped = false;
178323
}
324+
325+
registerReceiver(scopes, options, /* reportAsNewIntegration = */ false);
326+
}
327+
328+
@Override
329+
public void onStop(@NonNull LifecycleOwner owner) {
330+
unregisterReceiver();
179331
}
180332
}
181333

sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.lifecycle.Lifecycle.Event.ON_START
55
import androidx.lifecycle.Lifecycle.Event.ON_STOP
66
import androidx.lifecycle.LifecycleRegistry
7+
import androidx.lifecycle.ProcessLifecycleOwner
78
import androidx.test.core.app.ApplicationProvider
89
import androidx.test.ext.junit.runners.AndroidJUnit4
910
import io.sentry.CheckIn
@@ -24,7 +25,6 @@ import io.sentry.protocol.SentryId
2425
import io.sentry.protocol.SentryTransaction
2526
import io.sentry.transport.RateLimiter
2627
import org.junit.runner.RunWith
27-
import org.mockito.kotlin.mock
2828
import org.robolectric.annotation.Config
2929
import java.util.LinkedList
3030
import kotlin.test.BeforeTest
@@ -116,7 +116,7 @@ class SessionTrackingIntegrationTest {
116116
}
117117

118118
private fun setupLifecycle(options: SentryOptions): LifecycleRegistry {
119-
val lifecycle = LifecycleRegistry(mock())
119+
val lifecycle = LifecycleRegistry(ProcessLifecycleOwner.get())
120120
val lifecycleWatcher = (
121121
options.integrations.find {
122122
it is AppLifecycleIntegration

0 commit comments

Comments
 (0)