11package io .sentry .android .core ;
22
3+ import androidx .annotation .NonNull ;
4+ import androidx .lifecycle .DefaultLifecycleObserver ;
5+ import androidx .lifecycle .Lifecycle ;
6+ import androidx .lifecycle .LifecycleOwner ;
7+ import androidx .lifecycle .ProcessLifecycleOwner ;
8+ import io .sentry .ILogger ;
39import io .sentry .ISentryLifecycleToken ;
10+ import io .sentry .NoOpLogger ;
11+ import io .sentry .SentryLevel ;
12+ import io .sentry .android .core .internal .util .AndroidThreadChecker ;
413import io .sentry .util .AutoClosableReentrantLock ;
14+ import java .io .Closeable ;
15+ import java .io .IOException ;
16+ import java .util .List ;
17+ import java .util .concurrent .CopyOnWriteArrayList ;
518import org .jetbrains .annotations .ApiStatus ;
619import org .jetbrains .annotations .NotNull ;
720import org .jetbrains .annotations .Nullable ;
821import org .jetbrains .annotations .TestOnly ;
922
1023/** AppState holds the state of the App, e.g. whether the app is in background/foreground, etc. */
1124@ ApiStatus .Internal
12- public final class AppState {
25+ public final class AppState implements Closeable {
1326 private static @ NotNull AppState instance = new AppState ();
1427 private final @ NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
28+ volatile LifecycleObserver lifecycleObserver ;
29+ MainLooperHandler handler = new MainLooperHandler ();
1530
1631 private AppState () {}
1732
1833 public static @ NotNull AppState getInstance () {
1934 return instance ;
2035 }
2136
22- private @ Nullable Boolean inBackground = null ;
37+ private volatile @ Nullable Boolean inBackground = null ;
2338
2439 @ TestOnly
2540 void resetInstance () {
@@ -31,8 +46,156 @@ void resetInstance() {
3146 }
3247
3348 void setInBackground (final boolean inBackground ) {
49+ this .inBackground = inBackground ;
50+ }
51+
52+ void addAppStateListener (final @ NotNull AppStateListener listener ) {
53+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
54+ ensureLifecycleObserver (NoOpLogger .getInstance ());
55+
56+ lifecycleObserver .listeners .add (listener );
57+ }
58+ }
59+
60+ void removeAppStateListener (final @ NotNull AppStateListener listener ) {
61+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
62+ if (lifecycleObserver != null ) {
63+ lifecycleObserver .listeners .remove (listener );
64+ }
65+ }
66+ }
67+
68+ void addLifecycleObserver (final @ Nullable SentryAndroidOptions options ) {
69+ if (lifecycleObserver != null ) {
70+ return ;
71+ }
72+
3473 try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
35- this .inBackground = inBackground ;
74+ ensureLifecycleObserver (options != null ? options .getLogger () : NoOpLogger .getInstance ());
75+ }
76+ }
77+
78+ private void ensureLifecycleObserver (final @ NotNull ILogger logger ) {
79+ if (lifecycleObserver != null ) {
80+ return ;
3681 }
82+ try {
83+ Class .forName ("androidx.lifecycle.ProcessLifecycleOwner" );
84+ // create it right away, so it's available in addAppStateListener in case it's posted to main 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
96+ .log (
97+ SentryLevel .WARNING ,
98+ "androidx.lifecycle is not available, some features might not be properly working,"
99+ + "e.g. Session Tracking, Network and System Events breadcrumbs, etc." );
100+ } catch (Throwable e ) {
101+ logger
102+ .log (
103+ SentryLevel .ERROR ,
104+ "AppState could not register lifecycle observer" ,
105+ e );
106+ }
107+ }
108+
109+ private void addObserverInternal (final @ NotNull ILogger logger ) {
110+ final @ Nullable LifecycleObserver observerRef = lifecycleObserver ;
111+ try {
112+ // might already be unregistered/removed so we have to check for nullability
113+ if (observerRef != null ) {
114+ ProcessLifecycleOwner .get ().getLifecycle ().addObserver (observerRef );
115+ }
116+ } catch (Throwable e ) {
117+ // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
118+ // connection with conflicting dependencies of the androidx.lifecycle.
119+ // //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
120+ lifecycleObserver = null ;
121+ logger
122+ .log (
123+ SentryLevel .ERROR ,
124+ "AppState failed to get Lifecycle and could not install lifecycle observer." ,
125+ e );
126+ }
127+ }
128+
129+ void removeLifecycleObserver () {
130+ if (lifecycleObserver == null ) {
131+ return ;
132+ }
133+
134+ final @ Nullable LifecycleObserver ref ;
135+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
136+ ref = lifecycleObserver ;
137+ lifecycleObserver .listeners .clear ();
138+ lifecycleObserver = null ;
139+ }
140+
141+ if (AndroidThreadChecker .getInstance ().isMainThread ()) {
142+ removeObserverInternal (ref );
143+ } else {
144+ // some versions of the androidx lifecycle-process require this to be executed on the main
145+ // thread.
146+ // avoid method refs on Android due to some issues with older AGP setups
147+ // noinspection Convert2MethodRef
148+ handler .post (() -> removeObserverInternal (ref ));
149+ }
150+ }
151+
152+ private void removeObserverInternal (final @ Nullable LifecycleObserver ref ) {
153+ if (ref != null ) {
154+ ProcessLifecycleOwner .get ().getLifecycle ().removeObserver (ref );
155+ }
156+ }
157+
158+ @ Override
159+ public void close () throws IOException {
160+ removeLifecycleObserver ();
161+ }
162+
163+ static final class LifecycleObserver implements DefaultLifecycleObserver {
164+ final List <AppStateListener > listeners = new CopyOnWriteArrayList <AppStateListener >() {
165+ @ Override
166+ public boolean add (AppStateListener appStateListener ) {
167+ // notify the listeners immediately to let them "catch up" with the current state (mimics the behavior of androidx.lifecycle)
168+ Lifecycle .State currentState = ProcessLifecycleOwner .get ().getLifecycle ().getCurrentState ();
169+ if (currentState .isAtLeast (Lifecycle .State .STARTED )) {
170+ appStateListener .onForeground ();
171+ } else {
172+ appStateListener .onBackground ();
173+ }
174+ return super .add (appStateListener );
175+ }
176+ };
177+
178+ @ Override
179+ public void onStart (@ NonNull LifecycleOwner owner ) {
180+ for (AppStateListener listener : listeners ) {
181+ listener .onForeground ();
182+ }
183+ AppState .getInstance ().setInBackground (false );
184+ }
185+
186+ @ Override
187+ public void onStop (@ NonNull LifecycleOwner owner ) {
188+ for (AppStateListener listener : listeners ) {
189+ listener .onBackground ();
190+ }
191+ AppState .getInstance ().setInBackground (true );
192+ }
193+ }
194+
195+ // If necessary, we can adjust this and add other callbacks in the future
196+ public interface AppStateListener {
197+ void onForeground ();
198+
199+ void onBackground ();
37200 }
38201}
0 commit comments