Skip to content

Commit 31b2243

Browse files
runningcodeclaude
andauthored
fix(android): Always remove FirstDrawDoneListener after the first draw (#6152)
* fix(android): Always remove FirstDrawDoneListener after the first draw The listener was removed from an OnGlobalLayoutListener, which only runs if a layout pass follows the draw. On a static screen it stayed registered and ran on every frame, retaining its callback. Post the removal to the main thread instead so it always happens. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * changelog --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 1295a3d commit 31b2243

3 files changed

Lines changed: 17 additions & 48 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Always remove the first-draw listener after the first frame, even if no layout pass follows it ([#6152](https://github.com/getsentry/sentry-java/pull/6152))
8+
39
## 8.61.1
410

511
### Fixes

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,15 @@ public void onDraw() {
109109
if (view == null) {
110110
return;
111111
}
112-
// OnDrawListeners cannot be removed within onDraw, so we remove it with a
113-
// GlobalLayoutListener
114-
view.getViewTreeObserver()
115-
.addOnGlobalLayoutListener(
116-
new ViewTreeObserver.OnGlobalLayoutListener() {
117-
@Override
118-
public void onGlobalLayout() {
119-
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
120-
view.getViewTreeObserver().removeOnDrawListener(FirstDrawDoneListener.this);
121-
}
122-
});
112+
// OnDrawListeners cannot be removed within onDraw. Posting the removal instead of waiting for
113+
// the next global layout guarantees it happens, even if no layout pass follows the draw.
114+
mainThreadHandler.post(
115+
() -> {
116+
final ViewTreeObserver observer = view.getViewTreeObserver();
117+
if (observer.isAlive()) {
118+
observer.removeOnDrawListener(this);
119+
}
120+
});
123121
mainThreadHandler.postAtFrontOfQueue(callback);
124122
}
125123

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

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -113,49 +113,14 @@ class FirstDrawDoneListenerTest {
113113
}
114114

115115
@Test
116-
fun `registerForNextDraw unregister itself after onDraw`() {
116+
fun `registerForNextDraw unregisters itself after onDraw without a layout pass`() {
117117
val view = fixture.getSut()
118118
FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo)
119119
assertFalse(fixture.onDrawListeners.isEmpty())
120120

121-
// Does not remove OnDrawListener before onDraw, even if OnGlobalLayout is triggered
122-
view.viewTreeObserver.dispatchOnGlobalLayout()
123-
assertFalse(fixture.onDrawListeners.isEmpty())
124-
125-
// Removes OnDrawListener in the next OnGlobalLayout after onDraw
126-
view.viewTreeObserver.dispatchOnDraw()
127-
view.viewTreeObserver.dispatchOnGlobalLayout()
128-
assertTrue(fixture.onDrawListeners.isEmpty())
129-
}
130-
131-
@Test
132-
fun `OnGlobalLayoutListener is removed after cleanup`() {
133-
val view = fixture.getSut()
134-
135-
// Initialize mOnGlobalLayoutListeners via a dummy add/remove
136-
val dummyGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {}
137-
view.viewTreeObserver.addOnGlobalLayoutListener(dummyGlobalListener)
138-
view.viewTreeObserver.removeOnGlobalLayoutListener(dummyGlobalListener)
139-
140-
// CopyOnWriteArray wraps an internal ArrayList called mData
141-
val copyOnWriteArray: Any = view.viewTreeObserver.getProperty("mOnGlobalLayoutListeners")
142-
val mDataField = copyOnWriteArray.javaClass.getDeclaredField("mData")
143-
mDataField.isAccessible = true
144-
145-
@Suppress("UNCHECKED_CAST")
146-
fun globalLayoutListeners(): ArrayList<*> = mDataField.get(copyOnWriteArray) as ArrayList<*>
147-
148-
assertTrue(globalLayoutListeners().isEmpty())
149-
150-
FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo)
151-
152-
// onDraw registers a cleanup OnGlobalLayoutListener
153121
view.viewTreeObserver.dispatchOnDraw()
154-
assertFalse(globalLayoutListeners().isEmpty())
122+
Shadows.shadowOf(Looper.getMainLooper()).idle()
155123

156-
// onGlobalLayout fires the cleanup, which removes both the draw and layout listeners
157-
view.viewTreeObserver.dispatchOnGlobalLayout()
158-
assertTrue(globalLayoutListeners().isEmpty())
159124
assertTrue(fixture.onDrawListeners.isEmpty())
160125
}
161126

0 commit comments

Comments
 (0)