Skip to content

Commit 6fd675a

Browse files
authored
Merge d2263b8 into d4d7816
2 parents d4d7816 + d2263b8 commit 6fd675a

File tree

10 files changed

+591
-309
lines changed

10 files changed

+591
-309
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Internal
6+
7+
- Use single `LifecycleObserver` and multi-cast it to the integrations interested in lifecycle states ([#4567](https://github.com/getsentry/sentry-java/pull/4567))
8+
59
### Fixes
610

711
- Allow multiple UncaughtExceptionHandlerIntegrations to be active at the same time ([#4462](https://github.com/getsentry/sentry-java/pull/4462))

sentry-android-core/api/sentry-android-core.api

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,17 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In
166166
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
167167
}
168168

169-
public final class io/sentry/android/core/AppState {
169+
public final class io/sentry/android/core/AppState : java/io/Closeable {
170+
public fun close ()V
170171
public static fun getInstance ()Lio/sentry/android/core/AppState;
171172
public fun isInBackground ()Ljava/lang/Boolean;
172173
}
173174

175+
public abstract interface class io/sentry/android/core/AppState$AppStateListener {
176+
public abstract fun onBackground ()V
177+
public abstract fun onForeground ()V
178+
}
179+
174180
public final class io/sentry/android/core/BuildConfig {
175181
public static final field BUILD_TYPE Ljava/lang/String;
176182
public static final field DEBUG Z
@@ -420,11 +426,13 @@ public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerfo
420426
public fun onSpanStarted (Lio/sentry/ISpan;)V
421427
}
422428

423-
public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, java/io/Closeable {
429+
public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable {
424430
public fun <init> (Landroid/content/Context;)V
425431
public fun <init> (Landroid/content/Context;Ljava/util/List;)V
426432
public fun close ()V
427433
public static fun getDefaultActions ()Ljava/util/List;
434+
public fun onBackground ()V
435+
public fun onForeground ()V
428436
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
429437
}
430438

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ static void loadDefaultAndMetadataOptions(
128128
options.setCacheDirPath(getCacheDir(context).getAbsolutePath());
129129

130130
readDefaultOptionValues(options, context, buildInfoProvider);
131+
AppState.getInstance().registerLifecycleObserver(options);
131132
}
132133

133134
@TestOnly

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

Lines changed: 26 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
44

5-
import androidx.lifecycle.ProcessLifecycleOwner;
65
import io.sentry.IScopes;
6+
import io.sentry.ISentryLifecycleToken;
77
import io.sentry.Integration;
88
import io.sentry.SentryLevel;
99
import io.sentry.SentryOptions;
10-
import io.sentry.android.core.internal.util.AndroidThreadChecker;
10+
import io.sentry.util.AutoClosableReentrantLock;
1111
import io.sentry.util.Objects;
1212
import java.io.Closeable;
1313
import java.io.IOException;
@@ -17,20 +17,11 @@
1717

1818
public final class AppLifecycleIntegration implements Integration, Closeable {
1919

20+
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
2021
@TestOnly @Nullable volatile LifecycleWatcher watcher;
2122

2223
private @Nullable SentryAndroidOptions options;
2324

24-
private final @NotNull MainLooperHandler handler;
25-
26-
public AppLifecycleIntegration() {
27-
this(new MainLooperHandler());
28-
}
29-
30-
AppLifecycleIntegration(final @NotNull MainLooperHandler handler) {
31-
this.handler = handler;
32-
}
33-
3425
@Override
3526
public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
3627
Objects.requireNonNull(scopes, "Scopes are required");
@@ -55,85 +46,47 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
5546

5647
if (this.options.isEnableAutoSessionTracking()
5748
|| this.options.isEnableAppLifecycleBreadcrumbs()) {
58-
try {
59-
Class.forName("androidx.lifecycle.DefaultLifecycleObserver");
60-
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
61-
if (AndroidThreadChecker.getInstance().isMainThread()) {
62-
addObserver(scopes);
63-
} else {
64-
// some versions of the androidx lifecycle-process require this to be executed on the main
65-
// thread.
66-
handler.post(() -> addObserver(scopes));
49+
try (final ISentryLifecycleToken ignored = lock.acquire()) {
50+
if (watcher != null) {
51+
return;
6752
}
68-
} catch (ClassNotFoundException e) {
69-
options
70-
.getLogger()
71-
.log(
72-
SentryLevel.WARNING,
73-
"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed");
74-
} catch (IllegalStateException e) {
75-
options
76-
.getLogger()
77-
.log(SentryLevel.ERROR, "AppLifecycleIntegration could not be installed", e);
78-
}
79-
}
80-
}
8153

82-
private void addObserver(final @NotNull IScopes scopes) {
83-
// this should never happen, check added to avoid warnings from NullAway
84-
if (this.options == null) {
85-
return;
86-
}
54+
watcher =
55+
new LifecycleWatcher(
56+
scopes,
57+
this.options.getSessionTrackingIntervalMillis(),
58+
this.options.isEnableAutoSessionTracking(),
59+
this.options.isEnableAppLifecycleBreadcrumbs());
8760

88-
watcher =
89-
new LifecycleWatcher(
90-
scopes,
91-
this.options.getSessionTrackingIntervalMillis(),
92-
this.options.isEnableAutoSessionTracking(),
93-
this.options.isEnableAppLifecycleBreadcrumbs());
61+
AppState.getInstance().addAppStateListener(watcher);
62+
}
9463

95-
try {
96-
ProcessLifecycleOwner.get().getLifecycle().addObserver(watcher);
9764
options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed.");
9865
addIntegrationToSdkVersion("AppLifecycle");
99-
} catch (Throwable e) {
100-
// This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
101-
// connection with conflicting dependencies of the androidx.lifecycle.
102-
// //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
103-
watcher = null;
104-
options
105-
.getLogger()
106-
.log(
107-
SentryLevel.ERROR,
108-
"AppLifecycleIntegration failed to get Lifecycle and could not be installed.",
109-
e);
11066
}
11167
}
11268

11369
private void removeObserver() {
114-
final @Nullable LifecycleWatcher watcherRef = watcher;
70+
final @Nullable LifecycleWatcher watcherRef;
71+
try (final ISentryLifecycleToken ignored = lock.acquire()) {
72+
watcherRef = watcher;
73+
watcher = null;
74+
}
75+
11576
if (watcherRef != null) {
116-
ProcessLifecycleOwner.get().getLifecycle().removeObserver(watcherRef);
77+
AppState.getInstance().removeAppStateListener(watcherRef);
11778
if (options != null) {
11879
options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration removed.");
11980
}
12081
}
121-
watcher = null;
12282
}
12383

12484
@Override
12585
public void close() throws IOException {
126-
if (watcher == null) {
127-
return;
128-
}
129-
if (AndroidThreadChecker.getInstance().isMainThread()) {
130-
removeObserver();
131-
} else {
132-
// some versions of the androidx lifecycle-process require this to be executed on the main
133-
// thread.
134-
// avoid method refs on Android due to some issues with older AGP setups
135-
// noinspection Convert2MethodRef
136-
handler.post(() -> removeObserver());
137-
}
86+
removeObserver();
87+
// TODO: probably should move it to Scopes.close(), but that'd require a new interface and
88+
// different implementations for Java and Android. This is probably fine like this too, because
89+
// integrations are closed in the same place
90+
AppState.getInstance().unregisterLifecycleObserver();
13891
}
13992
}

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

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
package io.sentry.android.core;
22

3+
import androidx.annotation.NonNull;
4+
import androidx.lifecycle.DefaultLifecycleObserver;
5+
import androidx.lifecycle.LifecycleOwner;
6+
import androidx.lifecycle.ProcessLifecycleOwner;
7+
import io.sentry.ILogger;
38
import io.sentry.ISentryLifecycleToken;
9+
import io.sentry.NoOpLogger;
10+
import io.sentry.SentryLevel;
11+
import io.sentry.android.core.internal.util.AndroidThreadChecker;
412
import io.sentry.util.AutoClosableReentrantLock;
13+
import java.io.Closeable;
14+
import java.io.IOException;
15+
import java.util.List;
16+
import java.util.concurrent.CopyOnWriteArrayList;
517
import org.jetbrains.annotations.ApiStatus;
618
import org.jetbrains.annotations.NotNull;
719
import org.jetbrains.annotations.Nullable;
820
import org.jetbrains.annotations.TestOnly;
921

1022
/** AppState holds the state of the App, e.g. whether the app is in background/foreground, etc. */
1123
@ApiStatus.Internal
12-
public final class AppState {
24+
public final class AppState implements Closeable {
1325
private static @NotNull AppState instance = new AppState();
1426
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
27+
volatile LifecycleObserver lifecycleObserver;
28+
MainLooperHandler handler = new MainLooperHandler();
1529

1630
private AppState() {}
1731

1832
public static @NotNull AppState getInstance() {
1933
return instance;
2034
}
2135

22-
private @Nullable Boolean inBackground = null;
36+
private volatile @Nullable Boolean inBackground = null;
2337

2438
@TestOnly
2539
void resetInstance() {
@@ -31,8 +45,152 @@ void resetInstance() {
3145
}
3246

3347
void setInBackground(final boolean inBackground) {
48+
this.inBackground = inBackground;
49+
}
50+
51+
void addAppStateListener(final @NotNull AppStateListener listener) {
52+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
53+
ensureLifecycleObserver(NoOpLogger.getInstance());
54+
55+
lifecycleObserver.listeners.add(listener);
56+
}
57+
}
58+
59+
void removeAppStateListener(final @NotNull AppStateListener listener) {
60+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
61+
if (lifecycleObserver != null) {
62+
lifecycleObserver.listeners.remove(listener);
63+
}
64+
}
65+
}
66+
67+
void registerLifecycleObserver(final @Nullable SentryAndroidOptions options) {
68+
if (lifecycleObserver != null) {
69+
return;
70+
}
71+
3472
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
35-
this.inBackground = inBackground;
73+
ensureLifecycleObserver(options != null ? options.getLogger() : NoOpLogger.getInstance());
74+
}
75+
}
76+
77+
private void ensureLifecycleObserver(final @NotNull ILogger logger) {
78+
if (lifecycleObserver != null) {
79+
return;
3680
}
81+
try {
82+
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
83+
// create it right away, so it's available in addAppStateListener in case it's posted to main
84+
// thread
85+
lifecycleObserver = new LifecycleObserver();
86+
87+
if (AndroidThreadChecker.getInstance().isMainThread()) {
88+
addObserverInternal(logger);
89+
} else {
90+
// some versions of the androidx lifecycle-process require this to be executed on the main
91+
// thread.
92+
handler.post(() -> addObserverInternal(logger));
93+
}
94+
} catch (ClassNotFoundException e) {
95+
logger.log(
96+
SentryLevel.WARNING,
97+
"androidx.lifecycle is not available, some features might not be properly working,"
98+
+ "e.g. Session Tracking, Network and System Events breadcrumbs, etc.");
99+
} catch (Throwable e) {
100+
logger.log(SentryLevel.ERROR, "AppState could not register lifecycle observer", e);
101+
}
102+
}
103+
104+
private void addObserverInternal(final @NotNull ILogger logger) {
105+
final @Nullable LifecycleObserver observerRef = lifecycleObserver;
106+
try {
107+
// might already be unregistered/removed so we have to check for nullability
108+
if (observerRef != null) {
109+
ProcessLifecycleOwner.get().getLifecycle().addObserver(observerRef);
110+
}
111+
} catch (Throwable e) {
112+
// This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
113+
// connection with conflicting dependencies of the androidx.lifecycle.
114+
// //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
115+
lifecycleObserver = null;
116+
logger.log(
117+
SentryLevel.ERROR,
118+
"AppState failed to get Lifecycle and could not install lifecycle observer.",
119+
e);
120+
}
121+
}
122+
123+
void unregisterLifecycleObserver() {
124+
if (lifecycleObserver == null) {
125+
return;
126+
}
127+
128+
final @Nullable LifecycleObserver ref;
129+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
130+
ref = lifecycleObserver;
131+
lifecycleObserver.listeners.clear();
132+
lifecycleObserver = null;
133+
}
134+
135+
if (AndroidThreadChecker.getInstance().isMainThread()) {
136+
removeObserverInternal(ref);
137+
} else {
138+
// some versions of the androidx lifecycle-process require this to be executed on the main
139+
// thread.
140+
// avoid method refs on Android due to some issues with older AGP setups
141+
// noinspection Convert2MethodRef
142+
handler.post(() -> removeObserverInternal(ref));
143+
}
144+
}
145+
146+
private void removeObserverInternal(final @Nullable LifecycleObserver ref) {
147+
if (ref != null) {
148+
ProcessLifecycleOwner.get().getLifecycle().removeObserver(ref);
149+
}
150+
}
151+
152+
@Override
153+
public void close() throws IOException {
154+
unregisterLifecycleObserver();
155+
}
156+
157+
final class LifecycleObserver implements DefaultLifecycleObserver {
158+
final List<AppStateListener> listeners =
159+
new CopyOnWriteArrayList<AppStateListener>() {
160+
@Override
161+
public boolean add(AppStateListener appStateListener) {
162+
// notify the listeners immediately to let them "catch up" with the current state
163+
// (mimics the behavior of androidx.lifecycle)
164+
if (Boolean.FALSE.equals(inBackground)) {
165+
appStateListener.onForeground();
166+
} else if (Boolean.TRUE.equals(inBackground)) {
167+
appStateListener.onBackground();
168+
}
169+
return super.add(appStateListener);
170+
}
171+
};
172+
173+
@Override
174+
public void onStart(@NonNull LifecycleOwner owner) {
175+
for (AppStateListener listener : listeners) {
176+
listener.onForeground();
177+
}
178+
setInBackground(false);
179+
}
180+
181+
@Override
182+
public void onStop(@NonNull LifecycleOwner owner) {
183+
for (AppStateListener listener : listeners) {
184+
listener.onBackground();
185+
}
186+
setInBackground(true);
187+
}
188+
}
189+
190+
// If necessary, we can adjust this and add other callbacks in the future
191+
public interface AppStateListener {
192+
void onForeground();
193+
194+
void onBackground();
37195
}
38196
}

0 commit comments

Comments
 (0)