Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- 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.61.1

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,15 @@ public void onDraw() {
if (view == null) {
return;
}
// OnDrawListeners cannot be removed within onDraw, so we remove it with a
// GlobalLayoutListener
view.getViewTreeObserver()
.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
view.getViewTreeObserver().removeOnDrawListener(FirstDrawDoneListener.this);
}
});
// OnDrawListeners cannot be removed within onDraw. Posting the removal instead of waiting for
// the next global layout guarantees it happens, even if no layout pass follows the draw.
mainThreadHandler.post(
() -> {
final ViewTreeObserver observer = view.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnDrawListener(this);
}
});
mainThreadHandler.postAtFrontOfQueue(callback);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,49 +113,14 @@ class FirstDrawDoneListenerTest {
}

@Test
fun `registerForNextDraw unregister itself after onDraw`() {
fun `registerForNextDraw unregisters itself after onDraw without a layout pass`() {
val view = fixture.getSut()
FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo)
assertFalse(fixture.onDrawListeners.isEmpty())

// Does not remove OnDrawListener before onDraw, even if OnGlobalLayout is triggered
view.viewTreeObserver.dispatchOnGlobalLayout()
assertFalse(fixture.onDrawListeners.isEmpty())

// Removes OnDrawListener in the next OnGlobalLayout after onDraw
view.viewTreeObserver.dispatchOnDraw()
view.viewTreeObserver.dispatchOnGlobalLayout()
assertTrue(fixture.onDrawListeners.isEmpty())
}

@Test
fun `OnGlobalLayoutListener is removed after cleanup`() {
val view = fixture.getSut()

// Initialize mOnGlobalLayoutListeners via a dummy add/remove
val dummyGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {}
view.viewTreeObserver.addOnGlobalLayoutListener(dummyGlobalListener)
view.viewTreeObserver.removeOnGlobalLayoutListener(dummyGlobalListener)

// CopyOnWriteArray wraps an internal ArrayList called mData
val copyOnWriteArray: Any = view.viewTreeObserver.getProperty("mOnGlobalLayoutListeners")
val mDataField = copyOnWriteArray.javaClass.getDeclaredField("mData")
mDataField.isAccessible = true

@Suppress("UNCHECKED_CAST")
fun globalLayoutListeners(): ArrayList<*> = mDataField.get(copyOnWriteArray) as ArrayList<*>

assertTrue(globalLayoutListeners().isEmpty())

FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo)

// onDraw registers a cleanup OnGlobalLayoutListener
view.viewTreeObserver.dispatchOnDraw()
assertFalse(globalLayoutListeners().isEmpty())
Shadows.shadowOf(Looper.getMainLooper()).idle()

// onGlobalLayout fires the cleanup, which removes both the draw and layout listeners
view.viewTreeObserver.dispatchOnGlobalLayout()
assertTrue(globalLayoutListeners().isEmpty())
assertTrue(fixture.onDrawListeners.isEmpty())
}

Expand Down
Loading