Skip to content

refactor(lifecycle): Use single lifecycle observer #4567

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Improvements

- Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542))
- Use single `LifecycleObserver` and multi-cast it to the integrations interested in lifecycle states ([#4567](https://github.com/getsentry/sentry-java/pull/4567))

### Fixes

Expand Down
12 changes: 10 additions & 2 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,17 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/AppState {
public final class io/sentry/android/core/AppState : java/io/Closeable {
public fun close ()V
public static fun getInstance ()Lio/sentry/android/core/AppState;
public fun isInBackground ()Ljava/lang/Boolean;
}

public abstract interface class io/sentry/android/core/AppState$AppStateListener {
public abstract fun onBackground ()V
public abstract fun onForeground ()V
}

public final class io/sentry/android/core/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
Expand Down Expand Up @@ -422,11 +428,13 @@ public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerfo
public fun onSpanStarted (Lio/sentry/ISpan;)V
}

public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, java/io/Closeable {
public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Ljava/util/List;)V
public fun close ()V
public static fun getDefaultActions ()Ljava/util/List;
public fun onBackground ()V
public fun onForeground ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ static void loadDefaultAndMetadataOptions(
options.setCacheDirPath(getCacheDir(context).getAbsolutePath());

readDefaultOptionValues(options, context, buildInfoProvider);
AppState.getInstance().registerLifecycleObserver(options);
}

@TestOnly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

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

import androidx.lifecycle.ProcessLifecycleOwner;
import io.sentry.IScopes;
import io.sentry.ISentryLifecycleToken;
import io.sentry.Integration;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.Objects;
import java.io.Closeable;
import java.io.IOException;
Expand All @@ -17,20 +17,11 @@

public final class AppLifecycleIntegration implements Integration, Closeable {

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

private @Nullable SentryAndroidOptions options;

private final @NotNull MainLooperHandler handler;

public AppLifecycleIntegration() {
this(new MainLooperHandler());
}

AppLifecycleIntegration(final @NotNull MainLooperHandler handler) {
this.handler = handler;
}

@Override
public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
Objects.requireNonNull(scopes, "Scopes are required");
Expand All @@ -55,85 +46,47 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions

if (this.options.isEnableAutoSessionTracking()
|| this.options.isEnableAppLifecycleBreadcrumbs()) {
try {
Class.forName("androidx.lifecycle.DefaultLifecycleObserver");
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
if (AndroidThreadChecker.getInstance().isMainThread()) {
addObserver(scopes);
} else {
// some versions of the androidx lifecycle-process require this to be executed on the main
// thread.
handler.post(() -> addObserver(scopes));
try (final ISentryLifecycleToken ignored = lock.acquire()) {
if (watcher != null) {
return;
}
} catch (ClassNotFoundException e) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed");
} catch (IllegalStateException e) {
options
.getLogger()
.log(SentryLevel.ERROR, "AppLifecycleIntegration could not be installed", e);
}
}
}

private void addObserver(final @NotNull IScopes scopes) {
// this should never happen, check added to avoid warnings from NullAway
if (this.options == null) {
return;
}
watcher =
new LifecycleWatcher(
scopes,
this.options.getSessionTrackingIntervalMillis(),
this.options.isEnableAutoSessionTracking(),
this.options.isEnableAppLifecycleBreadcrumbs());

watcher =
new LifecycleWatcher(
scopes,
this.options.getSessionTrackingIntervalMillis(),
this.options.isEnableAutoSessionTracking(),
this.options.isEnableAppLifecycleBreadcrumbs());
AppState.getInstance().addAppStateListener(watcher);
}

try {
ProcessLifecycleOwner.get().getLifecycle().addObserver(watcher);
options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed.");
addIntegrationToSdkVersion("AppLifecycle");
} catch (Throwable e) {
// 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
watcher = null;
options
.getLogger()
.log(
SentryLevel.ERROR,
"AppLifecycleIntegration failed to get Lifecycle and could not be installed.",
e);
}
}

private void removeObserver() {
final @Nullable LifecycleWatcher watcherRef = watcher;
final @Nullable LifecycleWatcher watcherRef;
try (final ISentryLifecycleToken ignored = lock.acquire()) {
watcherRef = watcher;
watcher = null;
}

if (watcherRef != null) {
ProcessLifecycleOwner.get().getLifecycle().removeObserver(watcherRef);
AppState.getInstance().removeAppStateListener(watcherRef);
if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration removed.");
}
}
watcher = null;
}

@Override
public void close() throws IOException {
if (watcher == null) {
return;
}
if (AndroidThreadChecker.getInstance().isMainThread()) {
removeObserver();
} 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(() -> removeObserver());
}
removeObserver();
// TODO: probably should move it to Scopes.close(), but that'd require a new interface and
// different implementations for Java and Android. This is probably fine like this too, because
// integrations are closed in the same place
AppState.getInstance().unregisterLifecycleObserver();
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
package io.sentry.android.core;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import io.sentry.ILogger;
import io.sentry.ISentryLifecycleToken;
import io.sentry.NoOpLogger;
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.util.AutoClosableReentrantLock;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

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

private AppState() {}

public static @NotNull AppState getInstance() {
return instance;
}

private @Nullable Boolean inBackground = null;
private volatile @Nullable Boolean inBackground = null;

@TestOnly
void resetInstance() {
Expand All @@ -31,8 +45,152 @@ void resetInstance() {
}

void setInBackground(final boolean inBackground) {
this.inBackground = inBackground;
}

void addAppStateListener(final @NotNull AppStateListener listener) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
ensureLifecycleObserver(NoOpLogger.getInstance());

lifecycleObserver.listeners.add(listener);
}
}

void removeAppStateListener(final @NotNull AppStateListener listener) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
if (lifecycleObserver != null) {
lifecycleObserver.listeners.remove(listener);
}
}
}

void registerLifecycleObserver(final @Nullable SentryAndroidOptions options) {
if (lifecycleObserver != null) {
return;
}

try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
this.inBackground = inBackground;
ensureLifecycleObserver(options != null ? options.getLogger() : NoOpLogger.getInstance());
}
}

private void ensureLifecycleObserver(final @NotNull ILogger logger) {
if (lifecycleObserver != null) {
return;
}
try {
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
// create it right away, so it's available in addAppStateListener in case it's posted to main
// thread
lifecycleObserver = new LifecycleObserver();

if (AndroidThreadChecker.getInstance().isMainThread()) {
addObserverInternal(logger);
} else {
// some versions of the androidx lifecycle-process require this to be executed on the main
// thread.
handler.post(() -> addObserverInternal(logger));
}
} catch (ClassNotFoundException e) {
logger.log(
SentryLevel.WARNING,
"androidx.lifecycle is not available, some features might not be properly working,"
+ "e.g. Session Tracking, Network and System Events breadcrumbs, etc.");
} catch (Throwable e) {
logger.log(SentryLevel.ERROR, "AppState could not register lifecycle observer", e);
}
}

private void addObserverInternal(final @NotNull ILogger logger) {
final @Nullable LifecycleObserver observerRef = lifecycleObserver;
try {
// might already be unregistered/removed so we have to check for nullability
if (observerRef != null) {
ProcessLifecycleOwner.get().getLifecycle().addObserver(observerRef);
}
} catch (Throwable e) {
// 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
lifecycleObserver = null;
logger.log(
SentryLevel.ERROR,
"AppState failed to get Lifecycle and could not install lifecycle observer.",
e);
}
}

void unregisterLifecycleObserver() {
if (lifecycleObserver == null) {
return;
}

final @Nullable LifecycleObserver ref;
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
ref = lifecycleObserver;
lifecycleObserver.listeners.clear();
lifecycleObserver = null;
}

if (AndroidThreadChecker.getInstance().isMainThread()) {
removeObserverInternal(ref);
} 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(ref));
}
}

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

@Override
public void close() throws IOException {
unregisterLifecycleObserver();
}

final class LifecycleObserver implements DefaultLifecycleObserver {
final List<AppStateListener> listeners =
new CopyOnWriteArrayList<AppStateListener>() {
@Override
public boolean add(AppStateListener appStateListener) {
// notify the listeners immediately to let them "catch up" with the current state
// (mimics the behavior of androidx.lifecycle)
if (Boolean.FALSE.equals(inBackground)) {
appStateListener.onForeground();
} else if (Boolean.TRUE.equals(inBackground)) {
appStateListener.onBackground();
}
return super.add(appStateListener);
}
};

@Override
public void onStart(@NonNull LifecycleOwner owner) {
for (AppStateListener listener : listeners) {
listener.onForeground();
}
setInBackground(false);
}

@Override
public void onStop(@NonNull LifecycleOwner owner) {
for (AppStateListener listener : listeners) {
listener.onBackground();
}
setInBackground(true);
}
}

// If necessary, we can adjust this and add other callbacks in the future
public interface AppStateListener {
void onForeground();

void onBackground();
}
}
Loading
Loading