Skip to content

Commit 46c831a

Browse files
committed
fix(breadcrumbs): Unregister SystemEventsBroadcastReceiver when entering background (#4338)
1 parent 2ed8422 commit 46c831a

File tree

5 files changed

+399
-58
lines changed

5 files changed

+399
-58
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
- Compress Screenshots on a background thread ([#4295](https://github.com/getsentry/sentry-java/pull/4295))
88
- Improve low memory breadcrumb capturing ([#4325](https://github.com/getsentry/sentry-java/pull/4325))
99
- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))
10+
- Fix unregister `SystemEventsBroadcastReceiver` when entering background ([#4338](https://github.com/getsentry/sentry-java/pull/4338))
11+
- This should reduce ANRs seen with this class in the stack trace for Android 14 and above
1012

1113
## 7.22.5
1214

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 IHub hub, final @NotNull SentryOptions optio
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,13 +25,18 @@
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.IHub;
3135
import io.sentry.Integration;
3236
import io.sentry.SentryLevel;
3337
import io.sentry.SentryOptions;
3438
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
39+
import io.sentry.android.core.internal.util.AndroidMainThreadChecker;
3540
import io.sentry.android.core.internal.util.Debouncer;
3641
import io.sentry.util.Objects;
3742
import io.sentry.util.StringUtils;
@@ -49,29 +54,46 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl
4954

5055
private final @NotNull Context context;
5156

52-
@TestOnly @Nullable SystemEventsBroadcastReceiver receiver;
57+
@TestOnly @Nullable volatile SystemEventsBroadcastReceiver receiver;
58+
59+
@TestOnly @Nullable volatile ReceiverLifecycleHandler lifecycleHandler;
60+
61+
private final @NotNull MainLooperHandler handler;
5362

5463
private @Nullable SentryAndroidOptions options;
5564

65+
private @Nullable IHub hub;
66+
5667
private final @NotNull String[] actions;
57-
private boolean isClosed = false;
58-
private final @NotNull Object startLock = new Object();
68+
private volatile boolean isClosed = false;
69+
private volatile boolean isStopped = false;
70+
private volatile IntentFilter filter = null;
71+
private final @NotNull Object receiverLock = new Object();
5972

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

6477
private SystemEventsBreadcrumbsIntegration(
6578
final @NotNull Context context, final @NotNull String[] actions) {
79+
this(context, actions, new MainLooperHandler());
80+
}
81+
82+
SystemEventsBreadcrumbsIntegration(
83+
final @NotNull Context context,
84+
final @NotNull String[] actions,
85+
final @NotNull MainLooperHandler handler) {
6686
this.context = ContextUtils.getApplicationContext(context);
6787
this.actions = actions;
88+
this.handler = handler;
6889
}
6990

7091
public SystemEventsBreadcrumbsIntegration(
7192
final @NotNull Context context, final @NotNull List<String> actions) {
7293
this.context = ContextUtils.getApplicationContext(context);
7394
this.actions = new String[actions.size()];
7495
actions.toArray(this.actions);
96+
this.handler = new MainLooperHandler();
7597
}
7698

7799
@Override
@@ -81,6 +103,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio
81103
Objects.requireNonNull(
82104
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
83105
"SentryAndroidOptions is required");
106+
this.hub = hub;
84107

85108
this.options
86109
.getLogger()
@@ -90,46 +113,170 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio
90113
this.options.isEnableSystemEventBreadcrumbs());
91114

92115
if (this.options.isEnableSystemEventBreadcrumbs()) {
116+
addLifecycleObserver(this.options);
117+
registerReceiver(this.hub, this.options, /* reportAsNewIntegration = */ true);
118+
}
119+
}
93120

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

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

@@ -162,18 +309,23 @@ private void startSystemEventsReceiver(
162309
return actions;
163310
}
164311

165-
@Override
166-
public void close() throws IOException {
167-
synchronized (startLock) {
168-
isClosed = true;
169-
}
170-
if (receiver != null) {
171-
context.unregisterReceiver(receiver);
172-
receiver = null;
312+
final class ReceiverLifecycleHandler implements DefaultLifecycleObserver {
313+
@Override
314+
public void onStart(@NonNull LifecycleOwner owner) {
315+
if (hub == null || options == null) {
316+
return;
317+
}
173318

174-
if (options != null) {
175-
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration remove.");
319+
synchronized (receiverLock) {
320+
isStopped = false;
176321
}
322+
323+
registerReceiver(hub, options, /* reportAsNewIntegration = */ false);
324+
}
325+
326+
@Override
327+
public void onStop(@NonNull LifecycleOwner owner) {
328+
unregisterReceiver();
177329
}
178330
}
179331

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)