commit 4efdb817bc4d250594d88c6d602cfcf4b86ae67e
parent 19bc5ed076c9112ce7624c00267fab3b5e47f9e6
Author: Botond Ballo <botond@mozilla.com>
Date: Tue, 2 Dec 2025 01:44:26 +0000
Bug 1990617 - Resend the most recent compositor scroll update when a new CompositorScrollDelegate is registered. r=hiro,geckoview-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D271858
Diffstat:
2 files changed, 80 insertions(+), 0 deletions(-)
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PanZoomControllerTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PanZoomControllerTest.kt
@@ -1,6 +1,7 @@
package org.mozilla.geckoview.test
import android.os.SystemClock
+import android.util.Log
import android.view.InputDevice
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -20,6 +21,7 @@ import org.mozilla.geckoview.GeckoSession.ScrollPositionUpdate
import org.mozilla.geckoview.PanZoomController
import org.mozilla.geckoview.ScreenLength
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
+import org.mozilla.geckoview.test.util.UiThreadUtils
import kotlin.math.roundToInt
@RunWith(AndroidJUnit4::class)
@@ -27,6 +29,7 @@ import kotlin.math.roundToInt
class PanZoomControllerTest : BaseSessionTest() {
private val errorEpsilon = 3.0
private val scrollWaitTimeout = 10000.0 // 10 seconds
+ private val LOGTAG = "PanZoomControllerTest"
private fun setupDocument(documentPath: String) {
mainSession.loadTestPath(documentPath)
@@ -796,6 +799,71 @@ class PanZoomControllerTest : BaseSessionTest() {
@WithDisplay(width = 100, height = 100)
@Test
+ fun compositorScrollDelegateNotifiedOnRegistration() {
+ // Load a simple vertically scrollable page
+ setupDocument(SIMPLE_SCROLL_TEST_PATH)
+
+ // Without a CompositorScrollDelegate registered yet,
+ // scroll down by 50 pixels, and wait for the change
+ // to be propagated to the compositor.
+ mainSession.evaluateJS("window.scrollTo(0, 50)")
+ mainSession.promiseAllPaintsDone()
+ mainSession.flushApzRepaints()
+
+ // The compositor reports a scroll position updates
+ // delayed by one frame, so wait an additional frame
+ // to ensure the y=50 gets reported.
+ val promise = mainSession.evaluatePromiseJS(
+ """
+ new Promise(resolve => {
+ window.requestAnimationFrame(() => resolve(true));
+ });
+ """,
+ )
+ assertThat(
+ "we waited a frame",
+ promise.value as Boolean, equalTo(true),
+ )
+ mainSession.promiseAllPaintsDone()
+ mainSession.flushApzRepaints()
+
+ // Register a CompositorScrollDelegate, and check that it
+ // immediately gets notified about the scrollY=50, even
+ // though that scroll offset was reached before the delegate
+ // was registered.
+ var wasNotified = false
+ sessionRule.addExternalDelegateUntilTestEnd(
+ GeckoSession.CompositorScrollDelegate::class,
+ mainSession::setCompositorScrollDelegate,
+ { mainSession.setCompositorScrollDelegate(null) },
+ object : GeckoSession.CompositorScrollDelegate {
+ override fun onScrollChanged(session: GeckoSession, update: ScrollPositionUpdate) {
+ var scrollY = update.scrollY
+ Log.d(LOGTAG, "test scroll delegate onScrollChanged, scrollY = " + scrollY)
+ wasNotified = true
+ assertThat(
+ "notified scrollY is correct",
+ scrollY, equalTo(50.0f),
+ )
+ }
+ },
+ )
+
+ // setCompositorScrollDelegate() runs on the UI thread,
+ // so the delegate callback may not be called synchronously.
+ UiThreadUtils.loopUntilIdle(sessionRule.env.defaultTimeoutMillis)
+
+ assertThat(
+ "delegate was notified on registration",
+ wasNotified, equalTo(true),
+ )
+
+ // Clean up
+ mainSession.setCompositorScrollDelegate(null)
+ }
+
+ @WithDisplay(width = 100, height = 100)
+ @Test
fun stylusTilt() {
setupDocument(TOUCH_HTML_PATH)
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -291,6 +291,7 @@ public class GeckoSession {
private float mViewportLeft;
private float mViewportTop;
private float mViewportZoom = 1.0f;
+ private ScrollPositionUpdate mLastScrollPositionUpdate = null;
private int mKeyboardHeight = 0; // The software keyboard height, 0 if it's hidden.
//
@@ -3427,7 +3428,17 @@ public class GeckoSession {
@UiThread
public void setCompositorScrollDelegate(final @Nullable CompositorScrollDelegate delegate) {
ThreadUtils.assertOnUiThread();
+ if (mCompositorScrollDelegate == delegate) {
+ return;
+ }
+
mCompositorScrollDelegate = delegate;
+
+ // Notify the newly registered delegate immediately about the
+ // most recent update, if there is one.
+ if (mCompositorScrollDelegate != null && mLastScrollPositionUpdate != null) {
+ mCompositorScrollDelegate.onScrollChanged(this, mLastScrollPositionUpdate);
+ }
}
/**
@@ -7835,6 +7846,7 @@ public class GeckoSession {
update.scrollY = scrollY;
update.zoom = zoom;
update.source = source;
+ mLastScrollPositionUpdate = update;
if (mCompositorScrollDelegate != null) {
mCompositorScrollDelegate.onScrollChanged(this, update);
}