Skip to content

Commit c75be29

Browse files
committed
fix(android): Ensure frame metrics listeners are registered/unregistered on the main thread
1 parent dea75aa commit c75be29

File tree

2 files changed

+110
-17
lines changed

2 files changed

+110
-17
lines changed

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -282,16 +282,22 @@ public void stopCollection(final @Nullable String listenerId) {
282282

283283
@SuppressLint("NewApi")
284284
private void stopTrackingWindow(final @NotNull Window window) {
285-
if (trackedWindows.contains(window)) {
286-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) {
287-
try {
288-
windowFrameMetricsManager.removeOnFrameMetricsAvailableListener(
289-
window, frameMetricsAvailableListener);
290-
} catch (Exception e) {
291-
logger.log(SentryLevel.ERROR, "Failed to remove frameMetricsAvailableListener", e);
292-
}
293-
}
294-
trackedWindows.remove(window);
285+
final boolean wasTracked = trackedWindows.remove(window);
286+
if (wasTracked) {
287+
new Handler(Looper.getMainLooper())
288+
.post(
289+
() -> {
290+
try {
291+
// Re-check if we should still stop tracking this window
292+
if (!trackedWindows.contains(window)) {
293+
windowFrameMetricsManager.removeOnFrameMetricsAvailableListener(
294+
window, frameMetricsAvailableListener);
295+
}
296+
} catch (Throwable e) {
297+
logger.log(
298+
SentryLevel.ERROR, "Failed to remove frameMetricsAvailableListener", e);
299+
}
300+
});
295301
}
296302
}
297303

@@ -305,18 +311,36 @@ private void setCurrentWindow(final @NotNull Window window) {
305311

306312
@SuppressLint("NewApi")
307313
private void trackCurrentWindow() {
308-
Window window = currentWindow != null ? currentWindow.get() : null;
314+
@Nullable Window window = currentWindow != null ? currentWindow.get() : null;
309315
if (window == null || !isAvailable) {
310316
return;
311317
}
312318

313-
if (!trackedWindows.contains(window) && !listenerMap.isEmpty()) {
319+
if (trackedWindows.contains(window)) {
320+
return;
321+
}
322+
323+
if (listenerMap.isEmpty()) {
324+
return;
325+
}
314326

315-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N && handler != null) {
316-
trackedWindows.add(window);
317-
windowFrameMetricsManager.addOnFrameMetricsAvailableListener(
318-
window, frameMetricsAvailableListener, handler);
319-
}
327+
if (handler != null) {
328+
trackedWindows.add(window);
329+
// Ensure the addOnFrameMetricsAvailableListener is called on the main thread
330+
new Handler(Looper.getMainLooper())
331+
.post(
332+
() -> {
333+
// Re-check if we should still track this window
334+
// in case stopTrackingWindow was called in the meantime
335+
if (trackedWindows.contains(window)) {
336+
try {
337+
windowFrameMetricsManager.addOnFrameMetricsAvailableListener(
338+
window, frameMetricsAvailableListener, handler);
339+
} catch (Throwable e) {
340+
logger.log(SentryLevel.ERROR, "Failed to add frameMetricsAvailableListener", e);
341+
}
342+
}
343+
});
320344
}
321345
}
322346

@@ -373,6 +397,9 @@ default void addOnFrameMetricsAvailableListener(
373397
final @NotNull Window window,
374398
final @Nullable Window.OnFrameMetricsAvailableListener frameMetricsAvailableListener,
375399
final @Nullable Handler handler) {
400+
if (frameMetricsAvailableListener == null) {
401+
return;
402+
}
376403
window.addOnFrameMetricsAvailableListener(frameMetricsAvailableListener, handler);
377404
}
378405

sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ class SentryFrameMetricsCollectorTest {
148148
collector.startCollection(mock())
149149
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
150150
collector.onActivityStarted(fixture.activity)
151+
// Execute pending main looper tasks since addOnFrameMetricsAvailableListener is posted to main
152+
// thread
153+
Shadows.shadowOf(Looper.getMainLooper()).idle()
151154
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
152155
}
153156

@@ -157,8 +160,12 @@ class SentryFrameMetricsCollectorTest {
157160

158161
collector.startCollection(mock())
159162
collector.onActivityStarted(fixture.activity)
163+
// Execute pending add operations
164+
Shadows.shadowOf(Looper.getMainLooper()).idle()
160165
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
161166
collector.onActivityStopped(fixture.activity)
167+
// Execute pending remove operations
168+
Shadows.shadowOf(Looper.getMainLooper()).idle()
162169
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
163170
}
164171

@@ -181,6 +188,8 @@ class SentryFrameMetricsCollectorTest {
181188
collector.onActivityStarted(fixture.activity)
182189
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
183190
collector.startCollection(mock())
191+
// Execute pending main looper tasks
192+
Shadows.shadowOf(Looper.getMainLooper()).idle()
184193
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
185194
}
186195

@@ -189,9 +198,13 @@ class SentryFrameMetricsCollectorTest {
189198
val collector = fixture.getSut(context)
190199
val id = collector.startCollection(mock())
191200
collector.onActivityStarted(fixture.activity)
201+
// Execute pending add operations
202+
Shadows.shadowOf(Looper.getMainLooper()).idle()
192203

193204
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
194205
collector.stopCollection(id)
206+
// Execute pending remove operations
207+
Shadows.shadowOf(Looper.getMainLooper()).idle()
195208
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
196209
}
197210

@@ -205,9 +218,13 @@ class SentryFrameMetricsCollectorTest {
205218

206219
collector.onActivityStarted(fixture.activity)
207220
collector.onActivityStarted(fixture.activity)
221+
// Execute pending add operations
222+
Shadows.shadowOf(Looper.getMainLooper()).idle()
208223

209224
collector.onActivityStopped(fixture.activity)
210225
collector.onActivityStopped(fixture.activity)
226+
// Execute pending remove operations
227+
Shadows.shadowOf(Looper.getMainLooper()).idle()
211228

212229
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
213230
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
@@ -228,9 +245,13 @@ class SentryFrameMetricsCollectorTest {
228245
collector.startCollection(mock())
229246
collector.onActivityStarted(fixture.activity)
230247
collector.onActivityStarted(fixture.activity2)
248+
// Execute pending add operations
249+
Shadows.shadowOf(Looper.getMainLooper()).idle()
231250
assertEquals(2, fixture.addOnFrameMetricsAvailableListenerCounter)
232251
collector.onActivityStopped(fixture.activity)
233252
collector.onActivityStopped(fixture.activity2)
253+
// Execute pending remove operations
254+
Shadows.shadowOf(Looper.getMainLooper()).idle()
234255
assertEquals(2, fixture.removeOnFrameMetricsAvailableListenerCounter)
235256
}
236257

@@ -240,10 +261,13 @@ class SentryFrameMetricsCollectorTest {
240261
val id1 = collector.startCollection(mock())
241262
val id2 = collector.startCollection(mock())
242263
collector.onActivityStarted(fixture.activity)
264+
Shadows.shadowOf(Looper.getMainLooper()).idle()
243265
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
244266
collector.stopCollection(id1)
267+
Shadows.shadowOf(Looper.getMainLooper()).idle()
245268
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
246269
collector.stopCollection(id2)
270+
Shadows.shadowOf(Looper.getMainLooper()).idle()
247271
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
248272
}
249273

@@ -511,6 +535,48 @@ class SentryFrameMetricsCollectorTest {
511535
)
512536
}
513537

538+
@Test
539+
fun `collector calls addOnFrameMetricsAvailableListener on main thread`() {
540+
val collector = fixture.getSut(context)
541+
542+
collector.startCollection(mock())
543+
collector.onActivityStarted(fixture.activity)
544+
545+
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
546+
Shadows.shadowOf(Looper.getMainLooper()).idle()
547+
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
548+
}
549+
550+
@Test
551+
fun `collector calls removeOnFrameMetricsAvailableListener on main thread`() {
552+
val collector = fixture.getSut(context)
553+
collector.startCollection(mock())
554+
collector.onActivityStarted(fixture.activity)
555+
Shadows.shadowOf(Looper.getMainLooper()).idle()
556+
557+
collector.onActivityStopped(fixture.activity)
558+
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
559+
Shadows.shadowOf(Looper.getMainLooper()).idle()
560+
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
561+
}
562+
563+
@Test
564+
fun `collector prevents race condition when stop is called immediately after start`() {
565+
val collector = fixture.getSut(context)
566+
567+
collector.startCollection(mock())
568+
collector.onActivityStarted(fixture.activity)
569+
collector.onActivityStopped(fixture.activity)
570+
571+
// Now execute all pending operations
572+
Shadows.shadowOf(Looper.getMainLooper()).idle()
573+
574+
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
575+
// remove will still execute as it has no clue that add bailed on the main thread
576+
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
577+
assertEquals(0, collector.getProperty<Set<Window>>("trackedWindows").size)
578+
}
579+
514580
private fun createMockWindow(refreshRate: Float = 60F): Window {
515581
val mockWindow = mock<Window>()
516582
val mockDisplay = mock<Display>()

0 commit comments

Comments
 (0)