Skip to content

Commit da939a8

Browse files
authored
Improve callback handling and test visibility (#4593)
1 parent a5887e9 commit da939a8

File tree

2 files changed

+79
-6
lines changed

2 files changed

+79
-6
lines changed

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import androidx.annotation.NonNull;
44
import androidx.lifecycle.DefaultLifecycleObserver;
5+
import androidx.lifecycle.LifecycleObserver;
56
import androidx.lifecycle.LifecycleOwner;
67
import androidx.lifecycle.ProcessLifecycleOwner;
78
import io.sentry.ILogger;
@@ -24,8 +25,8 @@
2425
public final class AppState implements Closeable {
2526
private static @NotNull AppState instance = new AppState();
2627
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
27-
volatile LifecycleObserver lifecycleObserver;
28-
MainLooperHandler handler = new MainLooperHandler();
28+
private volatile LifecycleObserver lifecycleObserver;
29+
private MainLooperHandler handler = new MainLooperHandler();
2930

3031
private AppState() {}
3132

@@ -35,6 +36,16 @@ private AppState() {}
3536

3637
private volatile @Nullable Boolean inBackground = null;
3738

39+
@TestOnly
40+
LifecycleObserver getLifecycleObserver() {
41+
return lifecycleObserver;
42+
}
43+
44+
@TestOnly
45+
void setHandler(final @NotNull MainLooperHandler handler) {
46+
this.handler = handler;
47+
}
48+
3849
@TestOnly
3950
void resetInstance() {
4051
instance = new AppState();
@@ -159,31 +170,32 @@ final class LifecycleObserver implements DefaultLifecycleObserver {
159170
new CopyOnWriteArrayList<AppStateListener>() {
160171
@Override
161172
public boolean add(AppStateListener appStateListener) {
173+
final boolean addResult = super.add(appStateListener);
162174
// notify the listeners immediately to let them "catch up" with the current state
163175
// (mimics the behavior of androidx.lifecycle)
164176
if (Boolean.FALSE.equals(inBackground)) {
165177
appStateListener.onForeground();
166178
} else if (Boolean.TRUE.equals(inBackground)) {
167179
appStateListener.onBackground();
168180
}
169-
return super.add(appStateListener);
181+
return addResult;
170182
}
171183
};
172184

173185
@Override
174186
public void onStart(@NonNull LifecycleOwner owner) {
187+
setInBackground(false);
175188
for (AppStateListener listener : listeners) {
176189
listener.onForeground();
177190
}
178-
setInBackground(false);
179191
}
180192

181193
@Override
182194
public void onStop(@NonNull LifecycleOwner owner) {
195+
setInBackground(true);
183196
for (AppStateListener listener : listeners) {
184197
listener.onBackground();
185198
}
186-
setInBackground(true);
187199
}
188200
}
189201

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.sentry.android.core.internal.util.AndroidThreadChecker
66
import java.util.concurrent.CountDownLatch
77
import kotlin.test.AfterTest
88
import kotlin.test.BeforeTest
9+
import kotlin.test.assertEquals
910
import kotlin.test.assertFalse
1011
import kotlin.test.assertNotNull
1112
import kotlin.test.assertNull
@@ -35,7 +36,7 @@ class AppStateTest {
3536
fun getSut(isMainThread: Boolean = true): AppState {
3637
val appState = AppState.getInstance()
3738
whenever(mockThreadChecker.isMainThread).thenReturn(isMainThread)
38-
appState.handler = mockHandler
39+
appState.setHandler(mockHandler)
3940
return appState
4041
}
4142

@@ -261,6 +262,66 @@ class AppStateTest {
261262
assertTrue(sut.isInBackground()!!)
262263
}
263264

265+
@Test
266+
fun `a listener can be unregistered within a callback`() {
267+
val sut = fixture.getSut()
268+
269+
var onForegroundCalled = false
270+
val listener =
271+
object : AppStateListener {
272+
override fun onForeground() {
273+
sut.removeAppStateListener(this)
274+
onForegroundCalled = true
275+
}
276+
277+
override fun onBackground() {
278+
// ignored
279+
}
280+
}
281+
282+
sut.registerLifecycleObserver(fixture.options)
283+
val observer = sut.lifecycleObserver!!
284+
observer.onStart(mock())
285+
286+
// if an observer is added
287+
sut.addAppStateListener(listener)
288+
289+
// it should be notified
290+
assertTrue(onForegroundCalled)
291+
292+
// and removed from the list of listeners if it unregisters itself within the callback
293+
assertEquals(sut.lifecycleObserver?.listeners?.size, 0)
294+
}
295+
296+
@Test
297+
fun `state is correct within onStart and onStop callbacks`() {
298+
val sut = fixture.getSut()
299+
300+
var onForegroundCalled = false
301+
var onBackgroundCalled = false
302+
val listener =
303+
object : AppStateListener {
304+
override fun onForeground() {
305+
assertFalse(sut.isInBackground!!)
306+
onForegroundCalled = true
307+
}
308+
309+
override fun onBackground() {
310+
assertTrue(sut.isInBackground!!)
311+
onBackgroundCalled = true
312+
}
313+
}
314+
315+
sut.addAppStateListener(listener)
316+
317+
val observer = sut.lifecycleObserver!!
318+
observer.onStart(mock())
319+
observer.onStop(mock())
320+
321+
assertTrue(onForegroundCalled)
322+
assertTrue(onBackgroundCalled)
323+
}
324+
264325
@Test
265326
fun `thread safety - concurrent access is handled`() {
266327
val listeners = (1..5).map { fixture.createListener() }

0 commit comments

Comments
 (0)