From c69c9f08829ac10630ecbe62a981bf67f49d3fbf Mon Sep 17 00:00:00 2001 From: Makoto Kato Date: Thu, 14 Jul 2022 18:32:47 +0000 Subject: [PATCH] Bug 1763570 - Wait for APZ state to set autofill information. r=geckoview-reviewers,owlish When setting focus to input element, Gecko sets focused element to central via `zoomToFocusedInput`. So when we receives `focusin` event, content may be scrolled and zoomed. To pass correct element rectangle, we have to wait until it is completed. Fennec added `PanZoom:StateChange` event to listen APZ state. So GV should use same way. Differential Revision: https://phabricator.services.mozilla.com/D150453 --- dom/ipc/BrowserChild.cpp | 10 +++- .../android/actors/GeckoViewAutoFillChild.jsm | 19 +++++-- .../geckoview/test/AutofillDelegateTest.kt | 24 +++++++++ .../modules/geckoview/GeckoViewUtils.jsm | 51 +++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp index 8f73bd0041fe..554824afd6c0 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp @@ -1473,13 +1473,19 @@ bool BrowserChild::NotifyAPZStateChange( const layers::GeckoContentController::APZStateChange& aChange, const int& aArg) { mAPZEventState->ProcessAPZStateChange(aViewId, aChange, aArg); + nsCOMPtr observerService = + mozilla::services::GetObserverService(); if (aChange == layers::GeckoContentController::APZStateChange::eTransformEnd) { // This is used by tests to determine when the APZ is done doing whatever // it's doing. XXX generify this as needed when writing additional tests. - nsCOMPtr observerService = - mozilla::services::GetObserverService(); observerService->NotifyObservers(nullptr, "APZ:TransformEnd", nullptr); + observerService->NotifyObservers(nullptr, "PanZoom:StateChange", + u"NOTHING"); + } else if (aChange == + layers::GeckoContentController::APZStateChange::eTransformBegin) { + observerService->NotifyObservers(nullptr, "PanZoom:StateChange", + u"PANNING"); } return true; } diff --git a/mobile/android/actors/GeckoViewAutoFillChild.jsm b/mobile/android/actors/GeckoViewAutoFillChild.jsm index 18721ca57f2b..e1820e5b18bd 100644 --- a/mobile/android/actors/GeckoViewAutoFillChild.jsm +++ b/mobile/android/actors/GeckoViewAutoFillChild.jsm @@ -47,11 +47,22 @@ class GeckoViewAutoFillChild extends GeckoViewActorChild { break; } case "focusin": { - if ( - this.contentWindow.HTMLInputElement.isInstance(aEvent.composedTarget) - ) { - this.onFocus(aEvent.composedTarget); + const element = aEvent.composedTarget; + if (!this.contentWindow.HTMLInputElement.isInstance(element)) { + break; } + GeckoViewUtils.waitForPanZoomState(this.contentWindow).finally(() => { + if (Cu.isDeadWrapper(element)) { + // Focus element is removed or document is navigated to new page. + return; + } + const focusedElement = + Services.focus.focusedElement || + element.ownerDocument?.activeElement; + if (element == focusedElement) { + this.onFocus(focusedElement); + } + }); break; } case "focusout": { diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt index 892142b19942..b50d8139f4a5 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt @@ -6,6 +6,7 @@ package org.mozilla.geckoview.test import androidx.test.filters.MediumTest import android.util.SparseArray +import android.view.KeyEvent import android.view.View import org.hamcrest.Matchers.* import org.junit.Test @@ -13,6 +14,7 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.mozilla.geckoview.Autofill import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSession.TextInputDelegate import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.* @RunWith(Parameterized::class) @@ -510,4 +512,26 @@ class AutofillDelegateTest : BaseSessionTest() { assertThat("autofill hint count", checkAutofillChild(root), equalTo(6)) } + + @WithDisplay(width = 100, height = 100) + @Test fun autofillWaitForKeyboard() { + // Wait for the accessibility nodes to populate. + mainSession.loadUri(pageUrl) + mainSession.waitForPageStop() + + mainSession.pressKey(KeyEvent.KEYCODE_CTRL_LEFT) + mainSession.evaluateJS("document.querySelector('#pass2').focus()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate, TextInputDelegate { + @AssertCalled(order = [2]) + override fun onNodeFocus(session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData) { + assertThat("ID should be valid", node, notNullValue()) + } + + @AssertCalled(order = [1]) + override fun showSoftInput(session: GeckoSession) {} + }) + } } diff --git a/mobile/android/modules/geckoview/GeckoViewUtils.jsm b/mobile/android/modules/geckoview/GeckoViewUtils.jsm index 3d48694bed47..bbade69e341f 100644 --- a/mobile/android/modules/geckoview/GeckoViewUtils.jsm +++ b/mobile/android/modules/geckoview/GeckoViewUtils.jsm @@ -7,6 +7,9 @@ const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm"); +const { clearTimeout, setTimeout } = ChromeUtils.import( + "resource://gre/modules/Timer.jsm" +); const lazy = {}; @@ -325,6 +328,54 @@ var GeckoViewUtils = { return null; }, + /** + * Return promise for waiting for finishing PanZoomState. + * + * @param aWindow a DOM window. + * @return promise + */ + waitForPanZoomState(aWindow) { + return new Promise((resolve, reject) => { + if ( + !aWindow?.windowUtils.asyncPanZoomEnabled || + !Services.prefs.getBoolPref("apz.zoom-to-focused-input.enabled") + ) { + // No zoomToFocusedInput. + resolve(); + return; + } + + let timerId = 0; + + const panZoomState = (aSubject, aTopic, aData) => { + if (timerId != 0) { + // aWindow may be dead object now. + try { + clearTimeout(timerId); + } catch (e) {} + timerId = 0; + } + + if (aData === "NOTHING") { + Services.obs.removeObserver(panZoomState, "PanZoom:StateChange"); + resolve(); + } + }; + + Services.obs.addObserver(panZoomState, "PanZoom:StateChange"); + + // "GeckoView:ZoomToInput" has the timeout as 500ms when window isn't + // resized (it means on-screen-keyboard is already shown). + // So after up to 500ms, APZ event is sent. So we need to wait for more + // 500ms. + timerId = setTimeout(() => { + // PanZoom state isn't changed. zoomToFocusedInput will return error. + Services.obs.removeObserver(panZoomState, "PanZoom:StateChange"); + reject(); + }, 600); + }); + }, + /** * Add logging functions to the specified scope that forward to the given * Log.jsm logger. Currently "debug" and "warn" functions are supported. To