From 7085986549088cab922e90259a5a019844dcfa6c Mon Sep 17 00:00:00 2001 From: Carmelo Messina Date: Fri, 24 Jul 2026 07:03:22 +0200 Subject: [PATCH] Protect keyboard geometry on Android Prevent websites from inferring on-screen keyboard dimensions through viewport changes, dynamic CSS viewport units, browser-controls geometry, and the Virtual Keyboard API. Keep Chromium content at its neutral pre-IME height inside a native ProtectedViewportScrollView. Force overlays-content while protection is active, expose an empty virtual-keyboard rectangle, and retain stable browser-controls geometry throughout the IME transition. Use the focused editable or caret bounds to pan the physical viewport without resizing Blink. Preserve continuous scrolling by handing unconsumed motion between the native ScrollView and cc while retaining normal compositor hit testing for inner and root scrollers. Add an enabled-by-default feature flag and a per-site content setting. WebUI and extension pages retain Chromium's standard keyboard behavior. --- build/cromite_patches_list.txt | 1 + ...Protect-keyboard-geometry-on-Android.patch | 3351 +++++++++++++++++ 2 files changed, 3352 insertions(+) create mode 100644 build/patches/Protect-keyboard-geometry-on-Android.patch diff --git a/build/cromite_patches_list.txt b/build/cromite_patches_list.txt index df381ef7..7320c0a4 100644 --- a/build/cromite_patches_list.txt +++ b/build/cromite_patches_list.txt @@ -310,6 +310,7 @@ Disable-AppRating.patch Enable-Android-DevTools-Frontend.patch Reduce-input-capability-fingerprinting-through-normalization.patch Disable-Android-HDR.patch +Protect-keyboard-geometry-on-Android.patch # temporary or wip patches Temp-disable-predictive-back-gesture.patch diff --git a/build/patches/Protect-keyboard-geometry-on-Android.patch b/build/patches/Protect-keyboard-geometry-on-Android.patch new file mode 100644 index 00000000..955599b5 --- /dev/null +++ b/build/patches/Protect-keyboard-geometry-on-Android.patch @@ -0,0 +1,3351 @@ +From: uazo +Date: Fri, 24 Jul 2026 04:34:57 +0000 +Subject: Protect keyboard geometry on Android + +Prevent websites from inferring on-screen keyboard dimensions through +viewport changes, dynamic CSS viewport units, browser-controls geometry, +and the Virtual Keyboard API. + +Keep Chromium content at its neutral pre-IME height inside a native +ProtectedViewportScrollView. Force overlays-content while protection is +active, expose an empty virtual-keyboard rectangle, and retain stable +browser-controls geometry throughout the IME transition. + +Use the focused editable or caret bounds to pan the physical viewport +without resizing Blink. Preserve continuous scrolling by handing +unconsumed motion between the native ScrollView and cc while retaining +normal compositor hit testing for inner and root scrollers. + +Add an enabled-by-default feature flag and a per-site content setting. +WebUI and extension pages retain Chromium's standard keyboard behavior. + +License: GPL-2.0-or-later - https://spdx.org/licenses/GPL-2.0-or-later.html +--- + chrome/android/chrome_java_sources.gni | 1 + + .../chrome/browser/SwipeRefreshHandler.java | 42 + + ...ActivityTabWebContentsDelegateAndroid.java | 38 +- + .../compositor/CompositorViewHolder.java | 275 +++- + .../ProtectedViewportScrollView.java | 1446 +++++++++++++++++ + .../browser/tab/TabWebContentsObserver.java | 17 + + .../browser/flags/ChromeFeatureList.java | 2 +- + .../sAndroidProtectedKeyboardGeometry.java | 35 + + ...otectedKeyboardGeometryContentSetting.java | 94 ++ + .../protected_keyboard_geometry.grdp | 27 + + .../protected_keyboard_geometry.inc | 21 + + .../protected_keyboard_geometry.inc | 2 + + .../android/overscroll_controller_android.cc | 40 + + .../android/overscroll_controller_android.h | 11 + + .../web_contents/web_contents_android.cc | 6 + + .../web_contents/web_contents_android.h | 1 + + .../content/browser/input/ImeAdapterImpl.java | 90 + + .../browser/webcontents/WebContentsImpl.java | 8 + + .../content_public/browser/WebContents.java | 3 + + .../browser/test/mock/MockWebContents.java | 3 + + .../Android-Protected-Keyboard-Geometry.inc | 10 + + .../protected_keyboard_geometry.inc | 5 + + .../Android-Protected-Keyboard-Geometry.inc | 3 + + .../Android-Protected-Keyboard-Geometry.inc | 2 + + .../public/mojom/input/input_handler.mojom | 4 + + .../core/frame/local_frame_mojo_handler.cc | 7 +- + .../renderer/core/frame/viewport_data.cc | 13 +- + .../blink/renderer/core/frame/viewport_data.h | 4 +- + .../core/frame/web_frame_widget_impl.cc | 20 + + .../core/frame/web_frame_widget_impl.h | 1 + + third_party/blink/renderer/core/page/page.cc | 25 + + third_party/blink/renderer/core/page/page.h | 2 + + .../input/frame_widget_input_handler_impl.cc | 9 + + .../input/frame_widget_input_handler_impl.h | 1 + + .../widget/input/input_handler_proxy.cc | 28 +- + .../widget/input/input_handler_proxy.h | 8 + + ui/android/event_forwarder.cc | 63 + + ui/android/event_forwarder.h | 18 + + .../chromium/ui/OverscrollRefreshHandler.java | 16 + + .../org/chromium/ui/base/EventForwarder.java | 48 + + ui/android/overscroll_refresh.cc | 15 + + ui/android/overscroll_refresh.h | 5 + + ui/android/overscroll_refresh_handler.cc | 24 + + ui/android/overscroll_refresh_handler.h | 5 + + 44 files changed, 2468 insertions(+), 30 deletions(-) + create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/compositor/ProtectedViewportScrollView.java + create mode 100644 chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sAndroidProtectedKeyboardGeometry.java + create mode 100644 components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/impl/CromiteProtectedKeyboardGeometryContentSetting.java + create mode 100644 components/browser_ui/strings/bromite_content_settings/protected_keyboard_geometry.grdp + create mode 100644 components/content_settings/core/browser/bromite_content_settings/protected_keyboard_geometry.inc + create mode 100644 components/content_settings/core/common/bromite_content_settings/protected_keyboard_geometry.inc + create mode 100644 cromite_flags/chrome/browser/about_flags_cc/Android-Protected-Keyboard-Geometry.inc + create mode 100644 cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/protected_keyboard_geometry.inc + create mode 100644 cromite_flags/third_party/blink/common/features_cc/Android-Protected-Keyboard-Geometry.inc + create mode 100644 cromite_flags/third_party/blink/common/features_h/Android-Protected-Keyboard-Geometry.inc + +diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni +--- a/chrome/android/chrome_java_sources.gni ++++ b/chrome/android/chrome_java_sources.gni +@@ -313,6 +313,7 @@ chrome_java_sources = [ + "java/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerImpl.java", + "java/src/org/chromium/chrome/browser/compositor/CompositorView.java", + "java/src/org/chromium/chrome/browser/compositor/CompositorViewHolder.java", ++ "java/src/org/chromium/chrome/browser/compositor/ProtectedViewportScrollView.java", + "java/src/org/chromium/chrome/browser/compositor/LayerTitleCache.java", + "java/src/org/chromium/chrome/browser/compositor/layouts/Layout.java", + "java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerChrome.java", +diff --git a/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java +--- a/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java ++++ b/chrome/android/java/src/org/chromium/chrome/browser/SwipeRefreshHandler.java +@@ -14,6 +14,7 @@ import android.view.HapticFeedbackConstants; + import android.view.View; + import android.view.ViewGroup; + import android.view.ViewGroup.LayoutParams; ++import android.view.ViewParent; + + import androidx.annotation.ColorInt; + +@@ -26,6 +27,7 @@ import org.chromium.build.annotations.NullMarked; + import org.chromium.build.annotations.Nullable; + import org.chromium.chrome.R; + import org.chromium.chrome.browser.browser_controls.BottomOverscrollHandler; ++import org.chromium.chrome.browser.compositor.ProtectedViewportScrollView; + import org.chromium.chrome.browser.gesturenav.HistoryNavigationCoordinator; + import org.chromium.chrome.browser.tab.EmptyTabObserver; + import org.chromium.chrome.browser.tab.Tab; +@@ -109,6 +111,7 @@ public class SwipeRefreshHandler extends TabWebContentsUserData + // Handles overscroll PULL_FROM_BOTTOM_EDGE. This is used to track the browser controls + // state. + private @Nullable BottomOverscrollHandler mBottomOverscrollHandler; ++ private @Nullable ProtectedViewportScrollView mProtectedViewportOverscroll; + + public static SwipeRefreshHandler from(Tab tab) { + return SwipeRefreshHandler.from(tab, DEFAULT_SWIPE_REFRESH_LAYOUT_CREATOR); +@@ -222,6 +225,7 @@ public class SwipeRefreshHandler extends TabWebContentsUserData + mContainerView = null; + mNavigationCoordinator = null; + mBottomOverscrollHandler = null; ++ mProtectedViewportOverscroll = null; + setEnabled(false); + } + +@@ -272,6 +276,43 @@ public class SwipeRefreshHandler extends TabWebContentsUserData + return false; + } + ++ private @Nullable ProtectedViewportScrollView findProtectedViewport() { ++ View current = mTab.getContentView(); ++ while (current != null) { ++ if (current instanceof ProtectedViewportScrollView protectedViewport) { ++ return protectedViewport; ++ } ++ ViewParent parent = current.getParent(); ++ current = parent instanceof View ? (View) parent : null; ++ } ++ return null; ++ } ++ ++ @Override ++ public boolean startProtectedViewportHandoff() { ++ endProtectedViewportHandoff(); ++ ProtectedViewportScrollView protectedViewport = findProtectedViewport(); ++ if (protectedViewport == null ++ || !protectedViewport.startCompositorOverscrollHandoff()) { ++ return false; ++ } ++ mProtectedViewportOverscroll = protectedViewport; ++ return true; ++ } ++ ++ @Override ++ public void updateProtectedViewportHandoff(float accumulatedDeltaY) { ++ if (mProtectedViewportOverscroll != null) { ++ mProtectedViewportOverscroll.pullCompositorOverscroll(accumulatedDeltaY); ++ } ++ } ++ ++ @Override ++ public void endProtectedViewportHandoff() { ++ if (mProtectedViewportOverscroll == null) return; ++ mProtectedViewportOverscroll = null; ++ } ++ + /** Sets {@link HistoryNavigationCoordinator} object. */ + public void setNavigationCoordinator(HistoryNavigationCoordinator navigationHandler) { + mNavigationCoordinator = navigationHandler; +@@ -315,6 +356,7 @@ public class SwipeRefreshHandler extends TabWebContentsUserData + @Override + public void reset() { + cancelStopRefreshingRunnable(); ++ endProtectedViewportHandoff(); + if (mSwipeRefreshLayout != null) mSwipeRefreshLayout.reset(); + if (mNavigationCoordinator != null) mNavigationCoordinator.reset(); + if (mBottomOverscrollHandler != null) mBottomOverscrollHandler.reset(); +diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/ActivityTabWebContentsDelegateAndroid.java b/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/ActivityTabWebContentsDelegateAndroid.java +--- a/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/ActivityTabWebContentsDelegateAndroid.java ++++ b/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/ActivityTabWebContentsDelegateAndroid.java +@@ -530,30 +530,44 @@ public class ActivityTabWebContentsDelegateAndroid extends TabWebContentsDelegat + + @Override + public int getTopControlsHeight() { +- return mBrowserControlsStateProvider != null +- ? mBrowserControlsStateProvider.getTopControlsHeight() +- : 0; ++ int value = ++ mBrowserControlsStateProvider != null ++ ? mBrowserControlsStateProvider.getTopControlsHeight() ++ : 0; ++ CompositorViewHolder viewHolder = mCompositorViewHolderSupplier.get(); ++ return viewHolder != null ? viewHolder.getProtectedTopControlsHeight(value) : value; + } + + @Override + public int getTopControlsMinHeight() { +- return mBrowserControlsStateProvider != null +- ? mBrowserControlsStateProvider.getTopControlsMinHeight() +- : 0; ++ int value = ++ mBrowserControlsStateProvider != null ++ ? mBrowserControlsStateProvider.getTopControlsMinHeight() ++ : 0; ++ CompositorViewHolder viewHolder = mCompositorViewHolderSupplier.get(); ++ return viewHolder != null ? viewHolder.getProtectedTopControlsMinHeight(value) : value; + } + + @Override + public int getBottomControlsHeight() { +- return mBrowserControlsStateProvider != null +- ? mBrowserControlsStateProvider.getBottomControlsHeight() +- : 0; ++ int value = ++ mBrowserControlsStateProvider != null ++ ? mBrowserControlsStateProvider.getBottomControlsHeight() ++ : 0; ++ CompositorViewHolder viewHolder = mCompositorViewHolderSupplier.get(); ++ return viewHolder != null ? viewHolder.getProtectedBottomControlsHeight(value) : value; + } + + @Override + public int getBottomControlsMinHeight() { +- return mBrowserControlsStateProvider != null +- ? mBrowserControlsStateProvider.getBottomControlsMinHeight() +- : 0; ++ int value = ++ mBrowserControlsStateProvider != null ++ ? mBrowserControlsStateProvider.getBottomControlsMinHeight() ++ : 0; ++ CompositorViewHolder viewHolder = mCompositorViewHolderSupplier.get(); ++ return viewHolder != null ++ ? viewHolder.getProtectedBottomControlsMinHeight(value) ++ : value; + } + + @Override +diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/CompositorViewHolder.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/CompositorViewHolder.java +--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/CompositorViewHolder.java ++++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/CompositorViewHolder.java +@@ -46,6 +46,7 @@ import androidx.customview.widget.ExploreByTouchHelper; + import org.chromium.base.Callback; + import org.chromium.base.DeviceInfo; + import org.chromium.base.InputHintChecker; ++import org.chromium.base.Log; + import org.chromium.base.ObserverList; + import org.chromium.base.TraceEvent; + import org.chromium.base.metrics.RecordHistogram; +@@ -63,6 +64,7 @@ import org.chromium.chrome.browser.compositor.layouts.LayoutManagerHost; + import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl; + import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost; + import org.chromium.chrome.browser.flags.ChromeFeatureList; ++import org.chromium.chrome.browser.flags.cromite.sAndroidProtectedKeyboardGeometry; + import org.chromium.chrome.browser.fullscreen.BrowserControlsManager; + import org.chromium.chrome.browser.fullscreen.FullscreenManager; + import org.chromium.chrome.browser.layouts.EventFilter.EventType; +@@ -75,6 +77,7 @@ import org.chromium.chrome.browser.tab.TabCreationState; + import org.chromium.chrome.browser.tab.TabLoadIfNeededCaller; + import org.chromium.chrome.browser.tab.TabObscuringHandler; + import org.chromium.chrome.browser.tab.TabObserver; ++import org.chromium.chrome.browser.tab.TabWebContentsObserver; + import org.chromium.chrome.browser.tab_ui.TabContentManager; + import org.chromium.chrome.browser.tabmodel.TabCreatorManager; + import org.chromium.chrome.browser.tabmodel.TabModel; +@@ -88,8 +91,13 @@ import org.chromium.chrome.browser.ui.side_ui.SideUiCoordinator.SideUiSpecs; + import org.chromium.chrome.browser.ui.side_ui.SideUiObserver; + import org.chromium.chrome.browser.ui.side_ui.SideUiStateProvider; + import org.chromium.components.browser_ui.widget.TouchEventObserver; ++import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge; + import org.chromium.components.browser_ui.widget.TouchEventProvider; + import org.chromium.components.content_capture.OnscreenContentProvider; ++import org.chromium.components.content_settings.ContentSetting; ++import org.chromium.components.content_settings.ContentSettingsType; ++import org.chromium.components.embedder_support.util.UrlUtilities; ++import org.chromium.components.embedder_support.util.UrlConstants; + import org.chromium.components.embedder_support.view.ContentView; + import org.chromium.components.prefs.PrefService; + import org.chromium.content_public.browser.NavigationHandle; +@@ -177,6 +185,8 @@ public class CompositorViewHolder extends FrameLayout + private LayoutManagerImpl mLayoutManager; + private Activity mActivity; + private CompositorView mCompositorView; ++ private @Nullable ProtectedViewportScrollView mProtectedViewport; ++ private FrameLayout mProtectedViewportContent; + + private boolean mContentOverlayVisiblity = true; + private boolean mCanBeFocusable; +@@ -246,7 +256,7 @@ public class CompositorViewHolder extends FrameLayout + * Web content can use APIs that can change this to cause the WebContents to be resized. + */ + @VirtualKeyboardMode.EnumType +- private int mVirtualKeyboardMode = VirtualKeyboardMode.RESIZES_VISUAL; ++ private int mVirtualKeyboardMode = VirtualKeyboardMode.OVERLAYS_CONTENT; + + private @Nullable OnscreenContentProvider mOnscreenContentProvider; + +@@ -357,10 +367,11 @@ public class CompositorViewHolder extends FrameLayout + @Override + public void onDidFinishNavigationInPrimaryMainFrame( + Tab tab, NavigationHandle navigation) { +- if (!navigation.isSameDocument() && navigation.hasCommitted()) { ++ if (navigation.hasCommitted()) { + assert getWebContents() == tab.getWebContents(); + assert getWebContents() != null; +- updateVirtualKeyboardMode(getWebContents().getVirtualKeyboardMode()); ++ updateProtectedViewportAndVirtualKeyboardMode( ++ getWebContents().getVirtualKeyboardMode()); + } + } + +@@ -437,6 +448,8 @@ public class CompositorViewHolder extends FrameLayout + + @org.chromium.build.annotations.Initializer + private void internalInit() { ++ createProtectedViewport(); ++ + addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + Tab tab = getCurrentTab(); +@@ -470,8 +483,8 @@ public class CompositorViewHolder extends FrameLayout + }); + + mCompositorView = new CompositorView(getContext(), this); +- // mCompositorView should always be the first child. +- addView( ++ // mCompositorView should always be the first child in the protected viewport. ++ mProtectedViewportContent.addView( + mCompositorView, + 0, + new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); +@@ -740,6 +753,8 @@ public class CompositorViewHolder extends FrameLayout + PrefService prefService) { + mActivity = assumeNonNull(windowAndroid.getActivity().get()); + mCompositorView.initNativeCompositor(windowAndroid, tabContentManager); ++ if (mProtectedViewport != null) ++ mProtectedViewport.setInsetObserver(windowAndroid.getInsetObserver()); + + if (mControlContainer != null) { + mCompositorView +@@ -812,6 +827,11 @@ public class CompositorViewHolder extends FrameLayout + if (o.onInterceptTouchEvent(e)) return true; + } + ++ if (mProtectedViewport != null ++ && mProtectedViewport.shouldDeferParentInterception(e)) { ++ return false; ++ } ++ + if (mLayoutManager == null) return false; + + int actionMasked = SPenSupport.convertSPenEventAction(e.getActionMasked()); +@@ -997,6 +1017,166 @@ public class CompositorViewHolder extends FrameLayout + } + } + ++ private boolean isProtectedKeyboardGeometryEnabled() { ++ if (!sAndroidProtectedKeyboardGeometry.getInstance().isEnabled()) { ++ return false; ++ } ++ WebContents webContents = getWebContents(); ++ if (mTabVisible == null || webContents == null || webContents.isDestroyed()) { ++ return true; ++ } ++ ++ TabWebContentsObserver observer = TabWebContentsObserver.from(mTabVisible); ++ Boolean enabled = ++ observer.getProtectedKeyboardGeometryEnabledForCurrentDocument(); ++ if (enabled != null) return enabled; ++ ++ GURL url = webContents.getLastCommittedUrl(); ++ enabled = true; ++ if (UrlUtilities.isInternalScheme(url) ++ || UrlConstants.CHROME_EXTENSION_SCHEME.equals(url.getScheme())) { ++ enabled = false; ++ } else if (!url.isEmpty()) { ++ @ContentSetting ++ int setting = ++ WebsitePreferenceBridge.getContentSetting( ++ mTabVisible.getProfile(), ++ ContentSettingsType.PROTECTED_KEYBOARD_GEOMETRY, ++ url, ++ url); ++ enabled = setting == ContentSetting.ALLOW; ++ } ++ observer.setProtectedKeyboardGeometryEnabledForCurrentDocument(enabled); ++ return enabled; ++ } ++ ++ private void updateProtectedViewportAndVirtualKeyboardMode( ++ @VirtualKeyboardMode.EnumType int newMode) { ++ boolean protectedGeometryEnabled = isProtectedKeyboardGeometryEnabled(); ++ if (protectedGeometryEnabled) { ++ updateVirtualKeyboardMode(newMode); ++ } ++ if (mProtectedViewport != null) { ++ mProtectedViewport.resetCurrentDocumentState(); ++ mProtectedViewport.refreshEnabledState(); ++ } ++ if (!protectedGeometryEnabled) { ++ updateVirtualKeyboardMode(newMode); ++ } ++ } ++ ++ private void createProtectedViewport() { ++ if (!sAndroidProtectedKeyboardGeometry.getInstance().isEnabled()) { ++ mProtectedViewportContent = this; ++ return; ++ } ++ ++ mProtectedViewport = ++ new ProtectedViewportScrollView( ++ getContext(), ++ new ProtectedViewportScrollView.CompositorScrollDelegate() { ++ private @Nullable EventForwarder getEventForwarder() { ++ if (mTabVisible == null) return null; ++ WebContents webContents = mTabVisible.getWebContents(); ++ return webContents != null ? webContents.getEventForwarder() : null; ++ } ++ ++ @Override ++ public void beginCompositorScroll( ++ long timeMs, float x, float y, float deltaY) { ++ EventForwarder forwarder = getEventForwarder(); ++ if (forwarder != null) { ++ forwarder.beginCompositorScroll(timeMs, x, y, deltaY); ++ } ++ } ++ ++ @Override ++ public void updateCompositorScroll( ++ long timeMs, float x, float y, float deltaY) { ++ EventForwarder forwarder = getEventForwarder(); ++ if (forwarder != null) { ++ forwarder.updateCompositorScroll(timeMs, x, y, deltaY); ++ } ++ } ++ ++ @Override ++ public void endCompositorScroll( ++ long timeMs, float x, float y, float velocityY) { ++ EventForwarder forwarder = getEventForwarder(); ++ if (forwarder == null) return; ++ forwarder.endCompositorScroll(timeMs, x, y, velocityY); ++ } ++ }); ++ mProtectedViewport.setProtectionDelegate( ++ new ProtectedViewportScrollView.ProtectionDelegate() { ++ @Override ++ public boolean isProtectedKeyboardGeometryEnabled() { ++ return CompositorViewHolder.this ++ .isProtectedKeyboardGeometryEnabled(); ++ } ++ ++ @Override ++ public int getKeyboardInset() { ++ return mApplicationBottomInsetSupplier != null ++ ? mApplicationBottomInsetSupplier.getInsets().webContentsHeightInset ++ : 0; ++ } ++ ++ @Override ++ public boolean isKeyboardShowing() { ++ return mIsKeyboardShowing; ++ } ++ ++ @Override ++ public boolean isFocusedElementEditable() { ++ WebContents webContents = getWebContents(); ++ return webContents != null ++ && !webContents.isDestroyed() ++ && webContents.isFocusedElementEditable(); ++ } ++ ++ @Override ++ public int getViewportWidth() { ++ return getViewportSize().x; ++ } ++ ++ @Override ++ public boolean controlsResizeView() { ++ return mControlsResizeView; ++ } ++ }); ++ addView( ++ mProtectedViewport, ++ 0, ++ new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); ++ ++ mProtectedViewportContent = mProtectedViewport.getContentContainer(); ++ } ++ ++ /** Returns the top controls height exposed to Blink during protected IME resize. */ ++ public int getProtectedTopControlsHeight(int currentValue) { ++ if (mProtectedViewport == null) return currentValue; ++ return mProtectedViewport.getProtectedTopControlsHeight(currentValue); ++ } ++ ++ /** Returns the top controls minimum height exposed to Blink during protected IME resize. */ ++ public int getProtectedTopControlsMinHeight(int currentValue) { ++ if (mProtectedViewport == null) return currentValue; ++ return mProtectedViewport.getProtectedTopControlsMinHeight(currentValue); ++ } ++ ++ /** Returns the bottom controls height exposed to Blink during protected IME resize. */ ++ public int getProtectedBottomControlsHeight(int currentValue) { ++ if (mProtectedViewport == null) return currentValue; ++ return mProtectedViewport.getProtectedBottomControlsHeight(currentValue); ++ } ++ ++ /** Returns the bottom controls minimum height exposed to Blink during protected IME resize. */ ++ public int getProtectedBottomControlsMinHeight(int currentValue) { ++ if (mProtectedViewport == null) return currentValue; ++ return mProtectedViewport.getProtectedBottomControlsMinHeight(currentValue); ++ } ++ + /** + * Ensures the tab-backed webContents' size is up to date. + * +@@ -1052,8 +1232,48 @@ public class CompositorViewHolder extends FrameLayout + keyboardInset = 0; + } + ++ if (mProtectedViewport != null) { ++ controlsInsets = ++ mProtectedViewport.protectControlsInsets( ++ controlsInsets, width, keyboardInset, mControlsResizeView, mVirtualKeyboardMode); ++ } ++ + int verticalViewportInsets = controlsInsets + keyboardInset; + ++ if (mProtectedViewport != null) { ++ StackTraceElement[] sizingStack = Thread.currentThread().getStackTrace(); ++ String sizingCaller = sizingStack.length > 3 ? sizingStack[3].getMethodName() : "unknown"; ++ if (ProtectedViewportScrollView.DEBUG_DIAGNOSTICS) { ++ Log.i( ++ "ProtectedViewport", ++ "--updateWebContentsSize caller=%s viewport=%dx%d view=%dx%d" ++ + " controlsResize=%b controlsHeight=%d controlsMinHeight=%d" ++ + " controlsInset=%d keyboardInset=%d verticalInset=%d" ++ + " topOffset=%d contentOffset=%d target=%dx%d", ++ sizingCaller, ++ width, ++ height, ++ view.getWidth(), ++ view.getHeight(), ++ mControlsResizeView, ++ mBrowserControlsManager != null ++ ? mBrowserControlsManager.getTopControlsHeight() ++ + mBrowserControlsManager.getBottomControlsHeight() ++ : 0, ++ mBrowserControlsManager != null ++ ? mBrowserControlsManager.getTopControlsMinHeight() ++ + mBrowserControlsManager.getBottomControlsMinHeight() ++ : 0, ++ controlsInsets, ++ keyboardInset, ++ verticalViewportInsets, ++ mBrowserControlsManager != null ? mBrowserControlsManager.getTopControlOffset() : 0, ++ mBrowserControlsManager != null ? mBrowserControlsManager.getContentOffset() : 0, ++ width - horizontalViewportInsets, ++ height - verticalViewportInsets); ++ } ++ } ++ + if (isAttachedToWindow(view)) { + webContents.setSize(width - horizontalViewportInsets, height - verticalViewportInsets, verticalViewportInsets); + +@@ -1088,6 +1308,9 @@ public class CompositorViewHolder extends FrameLayout + + @VirtualKeyboardMode.EnumType + private int defaultVirtualKeyboardMode() { ++ if (isProtectedKeyboardGeometryEnabled()) { ++ return VirtualKeyboardMode.OVERLAYS_CONTENT; ++ } + if (mPrefService.getBoolean(Pref.VIRTUAL_KEYBOARD_RESIZES_LAYOUT_BY_DEFAULT)) { + return VirtualKeyboardMode.RESIZES_CONTENT; + } +@@ -1187,6 +1410,9 @@ public class CompositorViewHolder extends FrameLayout + boolean bottomControlsMinHeightChanged, + boolean requestNewFrame, + boolean isVisibilityForced) { ++ if (mProtectedViewport != null) ++ mProtectedViewport.onControlsOffsetChangedBeforeLayout(topOffset, isVisibilityForced); ++ + if (!isVisibilityForced && getResourceManager() != null) { + getResourceManager() + .assertResourceExists(AndroidResourceType.DYNAMIC, R.id.control_container); +@@ -1203,6 +1429,10 @@ public class CompositorViewHolder extends FrameLayout + } + + updateContentViewChildrenDimension(); ++ if (mProtectedViewport != null) { ++ mProtectedViewport.onControlsOffsetChangedAfterLayout( ++ topOffset, bottomOffset, isVisibilityForced); ++ } + } + + @Override +@@ -1211,7 +1441,13 @@ public class CompositorViewHolder extends FrameLayout + if (mTabVisible == null) return; + onBrowserControlsHeightChanged(); + updateWebContentsSize(getCurrentTab()); ++ if (mProtectedViewport != null) { ++ WebContents webContents = getWebContents(); ++ if (webContents != null) webContents.updateFocusedElementBounds(); ++ } + onViewportChanged(); ++ if (mProtectedViewport != null) ++ mProtectedViewport.onBottomControlsHeightChanged(); + } + + @Override +@@ -1219,6 +1455,10 @@ public class CompositorViewHolder extends FrameLayout + if (mTabVisible == null) return; + onBrowserControlsHeightChanged(); + updateWebContentsSize(getCurrentTab()); ++ if (mProtectedViewport != null) { ++ WebContents webContents = getWebContents(); ++ if (webContents != null) webContents.updateFocusedElementBounds(); ++ } + onViewportChanged(); + } + +@@ -1484,8 +1724,12 @@ public class CompositorViewHolder extends FrameLayout + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ++ boolean wasKeyboardShowing = mIsKeyboardShowing; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mIsKeyboardShowing = KeyboardVisibilityDelegate.getInstance().isKeyboardShowing(this); ++ if (mProtectedViewport != null && wasKeyboardShowing && !mIsKeyboardShowing) { ++ mProtectedViewport.resetViewportPan(); ++ } + } + + @Override +@@ -1520,6 +1764,8 @@ public class CompositorViewHolder extends FrameLayout + */ + public void setBrowserControlsManager(BrowserControlsManager manager) { + mBrowserControlsManager = manager; ++ if (mProtectedViewport != null) ++ mProtectedViewport.setBrowserControlsStateProvider(manager); + mBrowserControlsManager.addObserver(this); + onViewportChanged(); + } +@@ -1651,7 +1897,8 @@ public class CompositorViewHolder extends FrameLayout + if (mView == null) return; + WebContents webContents = getWebContents(); + if (show) { +- if (mView != assumeNonNull(getCurrentTab()).getView() || mView.getParent() == this) { ++ if (mView != assumeNonNull(getCurrentTab()).getView() ++ || mView.getParent() == mProtectedViewportContent) { + return; + } + // During tab creation, we temporarily add the new tab's view to a FrameLayout to +@@ -1667,20 +1914,20 @@ public class CompositorViewHolder extends FrameLayout + + // CompositorView always has index of 0. + // TODO(crbug.com/40770763): Look into enforcing the z-order of the views. +- addView(mView, 1); ++ mProtectedViewportContent.addView(mView, 1); + repositionTabViewForSideUi(); + updateFocusability(false, /* blockDescendants= */ false); + + // Claim focus for the new view unless the user is currently using the URL bar. + if (mUrlBar == null || !mUrlBar.hasFocus()) mView.requestFocus(); + } else { +- if (mView.getParent() == this) { ++ if (mView.getParent() == mProtectedViewportContent) { + updateFocusability(mCanBeFocusable, /* blockDescendants= */ false); + + if (webContents != null && !webContents.isDestroyed()) { + assumeNonNull(getContentView()).setVisibility(View.INVISIBLE); + } +- removeView(mView); ++ mProtectedViewportContent.removeView(mView); + } + } + } +@@ -1698,6 +1945,11 @@ public class CompositorViewHolder extends FrameLayout + + @VisibleForTesting + void onWillShowBrowserControls(boolean viewTransitionOptIn) { ++ if (mProtectedViewport != null) { ++ mProtectedViewport.prepareForKeyboardShow(); ++ assert ChromeFeatureList.sBrowserControlsEarlyResize.isEnabled() ++ : "Protected viewport requires BrowserControlsEarlyResize"; ++ } + // TODO(bokan): Flag guarding potential new behavior + // https://crbug.com/332331777. + if (!viewTransitionOptIn && !ChromeFeatureList.sBrowserControlsEarlyResize.isEnabled()) { +@@ -1740,6 +1992,11 @@ public class CompositorViewHolder extends FrameLayout + + mTabVisible = tab; + mView = newView; ++ WebContents webContents = getWebContents(); ++ updateProtectedViewportAndVirtualKeyboardMode( ++ webContents != null ++ ? webContents.getVirtualKeyboardMode() ++ : VirtualKeyboardMode.UNSET); + + updateContentOverlayVisibility(mContentOverlayVisiblity); + +diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/ProtectedViewportScrollView.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/ProtectedViewportScrollView.java +new file mode 100644 +--- /dev/null ++++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/ProtectedViewportScrollView.java +@@ -0,0 +1,1446 @@ ++/* ++ This file is part of Cromite. ++ ++ Cromite is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Cromite is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Cromite. If not, see . ++*/ ++ ++package org.chromium.chrome.browser.compositor; ++ ++import android.content.Context; ++import android.graphics.Rect; ++import android.view.MotionEvent; ++import android.view.VelocityTracker; ++import android.view.View; ++import android.view.ViewConfiguration; ++import android.widget.FrameLayout; ++import android.widget.ScrollView; ++ ++import androidx.core.view.WindowInsetsAnimationCompat; ++import androidx.core.view.WindowInsetsCompat; ++ ++import org.chromium.base.Log; ++import org.chromium.build.BuildConfig; ++import org.chromium.build.annotations.NullMarked; ++import org.chromium.build.annotations.Nullable; ++import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider; ++import org.chromium.chrome.browser.browser_controls.BrowserControlsUtils; ++import org.chromium.ui.insets.InsetObserver; ++import org.chromium.ui.insets.InsetObserver.WindowInsetsAnimationListener; ++import org.chromium.ui.mojom.VirtualKeyboardMode; ++ ++import java.util.List; ++ ++/** ++ * A scrollable physical viewport around Chromium's rendered content. ++ * ++ *

The scroll position is outside Blink's logical viewport. A focused editable can request that ++ * its physical rectangle be made visible after Android resizes the application for the keyboard, ++ * while the user can subsequently adjust that pan with normal touch scrolling. ++ * ++ *

Terminology used by this class: ++ * ++ *

    ++ *
  • Neutral is the content geometry retained as if the IME had not reduced the native ++ * viewport, at this ScrollView's scroll origin and the largest height observed for the ++ * current width. ++ *
  • Unforced baseline is the latest browser-controls state observed while visibility is ++ * not constrained to SHOWN or HIDDEN and can follow normal page scrolling. ++ *
  • Early capture is the synchronous snapshot taken before Android reports the IME ++ * animation, closing the interval in which controls geometry could otherwise change first. ++ *
  • Protected keyboard resize is the logical interval during which keyboard-related ++ * native changes must not alter the content or controls geometry exposed to Blink. It can ++ * begin before the native viewport actually changes size. ++ *
  • Clearance is the neutral vertical space below the focused editable rectangle. It is ++ * preserved above the IME, after subtracting browser controls that physically disappeared; ++ * it is not the keyboard inset. ++ *
  • Physical pan is this native ScrollView's offset. It moves rendered content without ++ * changing Blink's logical page scroll or viewport geometry. ++ *
  • Unlocked physical pan is the portion of the physical scroll range currently made ++ * available by the focus request or by unused cc scrolling. ++ *
  • Handoff is the transfer of unconsumed vertical motion at the boundary between this ++ * physical ScrollView and cc's scroll tree. Residual touch motion can move to cc, while cc ++ * overscroll can unlock and consume remaining physical pan. ++ *
++ */ ++@NullMarked ++public final class ProtectedViewportScrollView extends ScrollView { ++ private static final String TAG = "ProtectedViewport"; ++ public static final boolean DEBUG_DIAGNOSTICS = false; ++ private static final int INVALID_POINTER_ID = -1; ++ ++ // Gesture routing transitions: ++ // DOWN -> UNDECIDED ++ // MOVE beyond touch slop -> HORIZONTAL or VERTICAL ++ // VERTICAL with physical range -> VERTICAL_PARENT_DEFERRED ++ // UP or CANCEL -> UNDECIDED ++ private enum GestureRoutingState { ++ UNDECIDED, ++ HORIZONTAL, ++ VERTICAL, ++ VERTICAL_PARENT_DEFERRED ++ } ++ ++ /** Transfers unconsumed physical scrolling to Chromium's compositor scroll tree. */ ++ public interface CompositorScrollDelegate { ++ void beginCompositorScroll(long timeMs, float x, float y, float deltaY); ++ ++ void updateCompositorScroll(long timeMs, float x, float y, float deltaY); ++ ++ void endCompositorScroll(long timeMs, float x, float y, float velocityY); ++ } ++ ++ /** Supplies state owned by Chromium while this view owns protected viewport policy. */ ++ public interface ProtectionDelegate { ++ boolean isProtectedKeyboardGeometryEnabled(); ++ ++ int getKeyboardInset(); ++ ++ boolean isKeyboardShowing(); ++ ++ boolean isFocusedElementEditable(); ++ ++ int getViewportWidth(); ++ ++ boolean controlsResizeView(); ++ } ++ ++ private enum BrowserControlsTrackingMode { ++ FIXED_INSETS, ++ VISIBLE_CONTROLS ++ } ++ ++ private static final class BrowserControlsSnapshot { ++ // Viewport width for which this browser-controls geometry was captured. ++ final int mWidth; ++ ++ // Insets returned to WebContents. Mutable only while following visible controls. ++ int mInsets; ++ ++ // Full and minimum controls geometry exposed to Blink during protection. ++ final int mTopHeight; ++ final int mTopMinHeight; ++ final int mBottomHeight; ++ final int mBottomMinHeight; ++ ++ // Physical bottom-controls visibility and the policy used to update the inset. ++ final int mVisibleBottomHeight; ++ final BrowserControlsTrackingMode mTrackingMode; ++ ++ BrowserControlsSnapshot( ++ int width, ++ int insets, ++ int topHeight, ++ int topMinHeight, ++ int bottomHeight, ++ int bottomMinHeight, ++ int visibleBottomHeight, ++ BrowserControlsTrackingMode trackingMode) { ++ mWidth = width; ++ mInsets = insets; ++ mTopHeight = topHeight; ++ mTopMinHeight = topMinHeight; ++ mBottomHeight = bottomHeight; ++ mBottomMinHeight = bottomMinHeight; ++ mVisibleBottomHeight = visibleBottomHeight; ++ mTrackingMode = trackingMode; ++ } ++ } ++ ++ private static final class BrowserControlsProtectionState { ++ // Most recent controls state observed without forced visibility. ++ boolean mLastUnforcedResizeView; ++ int mLastUnforcedTopOffset; ++ ++ // Baseline used to compensate top controls shown for the focused editable. ++ int mEditableTopOffsetBaseline; ++ ++ // Independent lifecycle latches; both may be true at the same time. ++ boolean mEarlyShowCaptured; ++ boolean mEditableTopControlsActive; ++ ++ // Controls geometry currently returned while protection is active. ++ @Nullable BrowserControlsSnapshot mSnapshot; ++ ++ void reset() { ++ mLastUnforcedResizeView = false; ++ mLastUnforcedTopOffset = 0; ++ mEditableTopOffsetBaseline = 0; ++ mEarlyShowCaptured = false; ++ mEditableTopControlsActive = false; ++ mSnapshot = null; ++ } ++ } ++ ++ private static final class ViewportPanState { ++ // First focused editable rectangle retained for the keyboard interaction. ++ final Rect mRequestedRect = new Rect(); ++ ++ // Neutral space below the rectangle, preserved when positioning it above the IME. ++ int mRequestedBottomClearance; ++ ++ // Physically hidden bottom controls removed from the retained neutral clearance. ++ int mHiddenBottomControlsHeight; ++ ++ // Physical pan range currently available to touch and compositor overscroll. ++ int mUnlockedPhysicalPanY; ++ ++ boolean hasRequest() { ++ return !mRequestedRect.isEmpty(); ++ } ++ ++ void reset() { ++ mRequestedRect.setEmpty(); ++ mRequestedBottomClearance = 0; ++ mHiddenBottomControlsHeight = 0; ++ mUnlockedPhysicalPanY = 0; ++ } ++ } ++ ++ private static final class CompositorHandoffState { ++ // Synthetic compositor scroll sequence opened for the current touch stream. ++ boolean mScrollInProgress; ++ ++ // Whether the current Android gesture has forwarded real motion. ++ boolean mCurrentGestureForwardedDelta; ++ ++ // Physical scroll origin used when cc returns cumulative overscroll. ++ int mPullStartY; ++ } ++ ++ // Native hierarchy and immutable Android collaborators. ++ ++ // ScrollView's only direct child. Its overflow supplies physical scroll range. ++ private final FrameLayout mScrollContent; ++ ++ // Owns Chromium's CompositorView and ContentView at the protected neutral height. ++ private final FrameLayout mContent; ++ ++ // Transfers motion left at a physical edge to Chromium's compositor scroll tree. ++ private final CompositorScrollDelegate mCompositorScrollDelegate; ++ ++ // Supplies device-specific touch and fling thresholds. ++ private final ViewConfiguration mViewConfiguration; ++ ++ // Current touch sequence and routing decision. ++ ++ // Tracks the active pointer velocity; null outside a tracked touch sequence. ++ private @Nullable VelocityTracker mGestureVelocityTracker; ++ ++ // Pointer whose terminal Y velocity can be handed to the compositor. ++ private int mGesturePointerId = INVALID_POINTER_ID; ++ ++ // ACTION_DOWN coordinates used to choose and latch the gesture axis. ++ private float mGestureStartX; ++ private float mGestureStartY; ++ private GestureRoutingState mGestureRoutingState = GestureRoutingState.UNDECIDED; ++ ++ // Latest touch sample, retained because overScrollBy() does not receive MotionEvent. ++ private long mLastGestureEventTimeMs; ++ private float mLastGestureX; ++ private float mLastGestureY; ++ ++ // Bidirectional state exchanged with Chromium's compositor scroll path. ++ private final CompositorHandoffState mCompositorHandoff = new CompositorHandoffState(); ++ ++ // Focus request and physical pan geometry for the current keyboard interaction. ++ private final ViewportPanState mPanState = new ViewportPanState(); ++ ++ // Top-controls correction applied when capturing a focused editable rectangle. ++ private int mShownTopControlsHeight; ++ ++ // Largest neutral content size observed for the current width; -1 invalidates the cache. ++ private int mNeutralContentWidth = -1; ++ private int mNeutralContentHeight; ++ ++ // Policy and browser-controls sources installed by CompositorViewHolder. ++ private @Nullable ProtectionDelegate mProtectionDelegate; ++ private @Nullable BrowserControlsStateProvider mBrowserControlsStateProvider; ++ ++ // Browser-controls baseline, lifecycle latches, and currently protected geometry. ++ private final BrowserControlsProtectionState mControlsState = ++ new BrowserControlsProtectionState(); ++ ++ // IME animation lifecycle and observer registration. ++ ++ // True only between onPrepare() and onEnd() for the current IME animation. ++ private boolean mImeAnimationRunning; ++ ++ // Direction latched at onPrepare(); retained after onEnd() until the next keyboard show. ++ private boolean mImeClosing; ++ ++ // Optional source of IME animation events. ++ private @Nullable InsetObserver mInsetObserver; ++ ++ // Effective per-document feature state currently applied to this native view. ++ private boolean mEnabled; ++ ++ // The browser-controls latches and IME flags are independent, not mutually exclusive states. ++ // ++ // prepareForKeyboardShow(), after accepting an editable focus: ++ // - captures mControlsState.mSnapshot before the IME transition; ++ // - sets mControlsState.mEarlyShowCaptured = true; ++ // - sets mImeClosing = false. ++ // ++ // updateEditableTopControlsState(isVisibilityForced): ++ // - if isVisibilityForced && isFocusedElementEditable(): ++ // mControlsState.mEditableTopControlsActive = true; ++ // - otherwise: ++ // mControlsState.mEarlyShowCaptured = false; ++ // mControlsState.mEditableTopControlsActive = false. ++ // isVisibilityForced is the value received from BrowserControlsManager. It means ++ // that controls visibility is constrained to SHOWN or HIDDEN; it does not mean ++ // that the controls are necessarily visible. ++ // ++ // IME animation listener: ++ // - onPrepare(): ++ // mImeAnimationRunning = true; ++ // mImeClosing = keyboardWasVisible; ++ // - onEnd(): ++ // mImeAnimationRunning = false; ++ // mImeClosing is intentionally retained. ++ // ++ ++ // Receives IME animation lifecycle events and keeps the protected transition state aligned. ++ private final WindowInsetsAnimationListener mInsetsAnimationListener = ++ new WindowInsetsAnimationListener() { ++ @Override ++ public void onPrepare(WindowInsetsAnimationCompat animation) { ++ if (!isImeAnimation(animation)) return; ++ boolean keyboardWasVisible = ++ isProtectedKeyboardResizeWithoutEditableTopControls(getKeyboardInset()); ++ mImeClosing = keyboardWasVisible; ++ if (!keyboardWasVisible ++ && !mControlsState.mEditableTopControlsActive ++ && !mControlsState.mEarlyShowCaptured) { ++ captureCurrentBrowserControlsState(getViewportWidth()); ++ } ++ mImeAnimationRunning = true; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--imeAnimation prepare direction=%s", ++ keyboardWasVisible ? "closing" : "opening"); ++ } ++ } ++ ++ @Override ++ public void onStart( ++ WindowInsetsAnimationCompat animation, ++ WindowInsetsAnimationCompat.BoundsCompat bounds) {} ++ ++ @Override ++ public void onProgress( ++ WindowInsetsCompat insets, ++ List runningAnimations) {} ++ ++ @Override ++ public void onEnd(WindowInsetsAnimationCompat animation) { ++ if (!isImeAnimation(animation)) return; ++ mImeAnimationRunning = false; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, "--imeAnimation end"); ++ } ++ } ++ }; ++ ++ /** ++ * Creates the physical viewport that can pan Chromium content independently of Blink. ++ * ++ * @param compositorScrollDelegate forwards motion that this physical viewport cannot consume ++ */ ++ public ProtectedViewportScrollView( ++ Context context, CompositorScrollDelegate compositorScrollDelegate) { ++ super(context); ++ ++ if (BuildConfig.ENABLE_ASSERTS) { ++ // Diagnostic color: exposed pixels belong to the native protected viewport rather ++ // than Blink, the compositor surface, or browser-controls UI. ++ setBackgroundColor(0xFFFF00FF); ++ } ++ ++ mCompositorScrollDelegate = compositorScrollDelegate; ++ ++ mViewConfiguration = ViewConfiguration.get(context); ++ setFillViewport(true); ++ setClipToPadding(true); ++ setOverScrollMode(OVER_SCROLL_NEVER); ++ setVerticalScrollBarEnabled(false); ++ ++ mScrollContent = new FrameLayout(context); ++ super.addView( ++ mScrollContent, ++ new ScrollView.LayoutParams( ++ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); ++ ++ mContent = new FrameLayout(context); ++ mScrollContent.addView( ++ mContent, ++ new FrameLayout.LayoutParams( ++ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); ++ } ++ ++ /** ++ * Returns the container that must own CompositorView and ContentView. ++ * ++ *

Keeping Chromium content below {@link #mScrollContent} lets this view add physical scroll ++ * range without changing Blink's logical viewport dimensions. ++ */ ++ public FrameLayout getContentContainer() { ++ return mContent; ++ } ++ ++ /** Installs the delegate used to resolve per-document policy and Chromium-owned geometry. */ ++ public void setProtectionDelegate(ProtectionDelegate protectionDelegate) { ++ mProtectionDelegate = protectionDelegate; ++ } ++ ++ /** Discards transient geometry and lifecycle state owned by the previous document. */ ++ public void resetCurrentDocumentState() { ++ mImeAnimationRunning = false; ++ mImeClosing = false; ++ mControlsState.reset(); ++ mShownTopControlsHeight = 0; ++ resetViewportPan(); ++ } ++ ++ /** ++ * Re-evaluates the effective per-document policy and applies its view-level lifecycle. ++ * ++ *

Document-specific state is reset separately by {@link #resetCurrentDocumentState()}. ++ */ ++ public void refreshEnabledState() { ++ boolean enabled = ++ mProtectionDelegate != null ++ && mProtectionDelegate.isProtectedKeyboardGeometryEnabled(); ++ if (mEnabled == enabled) return; ++ mEnabled = enabled; ++ mNeutralContentWidth = -1; ++ mNeutralContentHeight = 0; ++ if (enabled) { ++ registerInsetObserver(); ++ } else { ++ unregisterInsetObserver(); ++ } ++ requestLayout(); ++ } ++ ++ /** Supplies the live browser-controls geometry captured around an IME transition. */ ++ public void setBrowserControlsStateProvider( ++ BrowserControlsStateProvider browserControlsStateProvider) { ++ mBrowserControlsStateProvider = browserControlsStateProvider; ++ } ++ ++ /** ++ * Replaces the source of IME animation events. ++ * ++ *

Registration is deferred until protection is enabled and this view is attached. ++ */ ++ public void setInsetObserver(@Nullable InsetObserver insetObserver) { ++ if (insetObserver == mInsetObserver) return; ++ unregisterInsetObserver(); ++ mInsetObserver = insetObserver; ++ registerInsetObserver(); ++ } ++ ++ private void registerInsetObserver() { ++ if (!mEnabled ++ || mInsetObserver == null ++ || !isAttachedToWindow()) { ++ return; ++ } ++ mInsetObserver.addWindowInsetsAnimationListener( ++ mInsetsAnimationListener); ++ } ++ ++ private void unregisterInsetObserver() { ++ if (mInsetObserver == null) return; ++ mInsetObserver.removeWindowInsetsAnimationListener( ++ mInsetsAnimationListener); ++ } ++ ++ private static boolean isImeAnimation(WindowInsetsAnimationCompat animation) { ++ return (animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0; ++ } ++ ++ private int getKeyboardInset() { ++ return mProtectionDelegate != null ? mProtectionDelegate.getKeyboardInset() : 0; ++ } ++ ++ private int getViewportWidth() { ++ return mProtectionDelegate != null ? mProtectionDelegate.getViewportWidth() : 0; ++ } ++ ++ private int getVisibleBottomControlsHeight(int bottomControlsOffset) { ++ if (mBrowserControlsStateProvider == null) return 0; ++ return Math.max( ++ 0, mBrowserControlsStateProvider.getBottomControlsHeight() - bottomControlsOffset); ++ } ++ ++ /** ++ * Recomputes hidden bottom controls after a height-only notification. ++ * ++ *

That callback does not carry an offset, so use the provider's current value. ++ */ ++ private void updateHiddenBottomControlsHeightFromProvider() { ++ if (mBrowserControlsStateProvider == null) return; ++ updateHiddenBottomControlsHeight( ++ Math.round(mBrowserControlsStateProvider.getBottomControlOffset())); ++ } ++ ++ /** ++ * Recomputes hidden bottom controls from the offset carried by an applied offset update. ++ * ++ *

Using the callback value keeps this calculation in the same browser-controls update even ++ * if the provider's observable state is updated at a different point in that lifecycle. ++ */ ++ private void updateHiddenBottomControlsHeight(int bottomControlsOffset) { ++ int visibleBottomControlsHeight = getVisibleBottomControlsHeight(bottomControlsOffset); ++ BrowserControlsSnapshot snapshot = mControlsState.mSnapshot; ++ int protectedVisibleBottomControlsHeight = ++ snapshot != null ? snapshot.mVisibleBottomHeight : 0; ++ int hiddenBottomControlsHeight = ++ snapshot != null ++ && snapshot.mTrackingMode ++ == BrowserControlsTrackingMode.VISIBLE_CONTROLS ++ ? 0 ++ : Math.max( ++ 0, ++ protectedVisibleBottomControlsHeight ++ - visibleBottomControlsHeight); ++ setHiddenBottomControlsHeight(hiddenBottomControlsHeight); ++ } ++ ++ /** Captures current controls geometry using the resize policy reported by the delegate. */ ++ private void captureCurrentBrowserControlsState(int width) { ++ boolean controlsResizeView = ++ mProtectionDelegate != null && mProtectionDelegate.controlsResizeView(); ++ captureBrowserControlsSnapshot( ++ width, controlsResizeView, /* preserveVisibleBottomControls= */ false); ++ } ++ ++ /** ++ * Captures controls geometry using transition state already selected by the caller. ++ * ++ *

preserveVisibleBottomControls keeps the initially visible bottom portion ++ * instead of reserving the full controls height, then tracks later visibility changes. ++ */ ++ private void captureBrowserControlsSnapshot( ++ int width, boolean controlsResizeView, boolean preserveVisibleBottomControls) { ++ if (mBrowserControlsStateProvider == null || width <= 0) return; ++ int topControlsHeight = mBrowserControlsStateProvider.getTopControlsHeight(); ++ int topControlsMinHeight = mBrowserControlsStateProvider.getTopControlsMinHeight(); ++ int bottomControlsHeight = mBrowserControlsStateProvider.getBottomControlsHeight(); ++ int bottomControlsMinHeight = mBrowserControlsStateProvider.getBottomControlsMinHeight(); ++ int visibleBottomControlsHeight = ++ getVisibleBottomControlsHeight( ++ Math.round(mBrowserControlsStateProvider.getBottomControlOffset())); ++ int controlsInsets = ++ controlsResizeView ++ ? topControlsHeight ++ + (preserveVisibleBottomControls ++ ? visibleBottomControlsHeight ++ : bottomControlsHeight) ++ : topControlsMinHeight + bottomControlsMinHeight; ++ mControlsState.mSnapshot = ++ new BrowserControlsSnapshot( ++ width, ++ controlsInsets, ++ topControlsHeight, ++ topControlsMinHeight, ++ bottomControlsHeight, ++ bottomControlsMinHeight, ++ visibleBottomControlsHeight, ++ preserveVisibleBottomControls ++ ? BrowserControlsTrackingMode.VISIBLE_CONTROLS ++ : BrowserControlsTrackingMode.FIXED_INSETS); ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--captureBrowserControls top=%d/%d bottom=%d/%d visibleBottom=%d" ++ + " preserveVisibleBottom=%b inset=%d", ++ topControlsHeight, ++ topControlsMinHeight, ++ bottomControlsHeight, ++ bottomControlsMinHeight, ++ visibleBottomControlsHeight, ++ preserveVisibleBottomControls, ++ controlsInsets); ++ } ++ } ++ ++ /** Evaluates protected resize using the keyboard inset supplied by the caller. */ ++ private boolean isProtectedKeyboardResizeWithoutEditableTopControls(int keyboardInset) { ++ return mEnabled ++ && (mImeAnimationRunning ++ || (mProtectionDelegate != null && mProtectionDelegate.isKeyboardShowing()) ++ || keyboardInset < 0 ++ || getHeight() < mContent.getHeight()); ++ } ++ ++ private boolean isProtectedKeyboardResize(int keyboardInset) { ++ return mControlsState.mEarlyShowCaptured ++ || mControlsState.mEditableTopControlsActive ++ || isProtectedKeyboardResizeWithoutEditableTopControls(keyboardInset); ++ } ++ ++ private boolean isFocusedElementEditable() { ++ return mProtectionDelegate != null && mProtectionDelegate.isFocusedElementEditable(); ++ } ++ ++ /** ++ * Tracks the browser-controlled top-controls phase around a focused editable. ++ * ++ *

isVisibilityForced comes from BrowserControlsManager. It means controls ++ * visibility is constrained to SHOWN or HIDDEN rather than driven by page scrolling; it does ++ * not by itself mean that the top controls are currently visible. ++ * ++ *

Protection treats that phase as relevant only while an editable is focused. Normal ++ * scroll-driven controls changes continue updating the unforced baseline. When protection ++ * starts, reuse the synchronous early snapshot if present; otherwise capture the last unforced ++ * state before layout. This keeps browser-controls motion out of Blink's keyboard geometry ++ * and compensates focused-element coordinates by the amount of top controls actually shown. ++ * ++ * @param isVisibilityForced whether Chromium has constrained controls visibility ++ */ ++ private void updateEditableTopControlsState(boolean isVisibilityForced) { ++ boolean focusedEditable = isFocusedElementEditable(); ++ boolean shouldProtect = isVisibilityForced && focusedEditable; ++ if (!shouldProtect) mControlsState.mEarlyShowCaptured = false; ++ if (mControlsState.mEditableTopControlsActive == shouldProtect) return; ++ ++ if (shouldProtect ++ && !isProtectedKeyboardResizeWithoutEditableTopControls(getKeyboardInset())) { ++ if (!mControlsState.mEarlyShowCaptured) { ++ mControlsState.mEditableTopOffsetBaseline = ++ mControlsState.mLastUnforcedTopOffset; ++ captureBrowserControlsSnapshot( ++ getViewportWidth(), ++ mControlsState.mLastUnforcedResizeView, ++ /* preserveVisibleBottomControls= */ false); ++ } ++ } ++ mControlsState.mEditableTopControlsActive = shouldProtect; ++ updateShownTopControlsHeight(); ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--editableTopControls active=%b forced=%b focusedEditable=%b earlyCapture=%b", ++ mControlsState.mEditableTopControlsActive, ++ isVisibilityForced, ++ focusedEditable, ++ mControlsState.mEarlyShowCaptured); ++ } ++ } ++ ++ private void updateShownTopControlsHeight() { ++ if (mBrowserControlsStateProvider == null) return; ++ int currentTopVisibleContentOffset = ++ Math.round(mBrowserControlsStateProvider.getTopVisibleContentOffset()); ++ int shownTopControlsHeight = ++ mControlsState.mEditableTopControlsActive ++ ? Math.max( ++ 0, ++ currentTopVisibleContentOffset ++ - mControlsState.mEditableTopOffsetBaseline) ++ : 0; ++ setShownTopControlsHeight(shownTopControlsHeight); ++ } ++ ++ /** ++ * Captures the last unforced top-controls state before Chromium applies a controls layout. ++ * ++ *

A forced show for an editable element must not change the geometry exposed to Blink. ++ */ ++ public void onControlsOffsetChangedBeforeLayout(int topOffset, boolean isVisibilityForced) { ++ if (!mEnabled) return; ++ if (!isVisibilityForced && mBrowserControlsStateProvider != null) { ++ mControlsState.mLastUnforcedResizeView = ++ BrowserControlsUtils.controlsResizeView(mBrowserControlsStateProvider); ++ mControlsState.mLastUnforcedTopOffset = ++ Math.round(mBrowserControlsStateProvider.getTopVisibleContentOffset()); ++ } ++ updateEditableTopControlsState(isVisibilityForced); ++ updateShownTopControlsHeight(); ++ if (DEBUG_DIAGNOSTICS) { ++ logBrowserControlsState("before-layout", topOffset, isVisibilityForced); ++ } ++ } ++ ++ /** ++ * Updates physical top/bottom controls compensation after their offsets have been applied. ++ */ ++ public void onControlsOffsetChangedAfterLayout( ++ int topOffset, int bottomOffset, boolean isVisibilityForced) { ++ if (!mEnabled) return; ++ updateHiddenBottomControlsHeight(bottomOffset); ++ if (DEBUG_DIAGNOSTICS) { ++ logBrowserControlsState("after-update", topOffset, isVisibilityForced); ++ post(() -> logBrowserControlsState("after-layout", topOffset, isVisibilityForced)); ++ } ++ } ++ ++ private void logBrowserControlsState( ++ String phase, int callbackTopOffset, boolean isVisibilityForced) { ++ if (mBrowserControlsStateProvider == null) return; ++ logBrowserControlsState( ++ phase, ++ callbackTopOffset, ++ mBrowserControlsStateProvider.getTopControlOffset(), ++ mBrowserControlsStateProvider.getContentOffset(), ++ mBrowserControlsStateProvider.getTopVisibleContentOffset(), ++ isVisibilityForced); ++ } ++ ++ /** Recomputes compensation when the physical bottom-controls height changes. */ ++ public void onBottomControlsHeightChanged() { ++ if (!mEnabled) return; ++ updateHiddenBottomControlsHeightFromProvider(); ++ } ++ ++ /** ++ * Returns the browser-controls inset used to size WebContents. ++ * ++ *

Outside an IME transition the current value is observed and captured. While protection is ++ * active, the captured or deliberately tracked controls geometry is returned so Blink cannot ++ * infer the keyboard from a transient controls configuration. ++ */ ++ public int protectControlsInsets( ++ int currentControlsInsets, int width, int keyboardInset, boolean controlsResizeView, ++ @VirtualKeyboardMode.EnumType int virtualKeyboardMode) { ++ if (!mEnabled) return currentControlsInsets; ++ ++ assert virtualKeyboardMode == VirtualKeyboardMode.OVERLAYS_CONTENT; ++ boolean isProtectedKeyboardResize = isProtectedKeyboardResize(keyboardInset); ++ ++ if (!isProtectedKeyboardResize) { ++ mControlsState.mLastUnforcedResizeView = controlsResizeView; ++ if (mBrowserControlsStateProvider != null) { ++ mControlsState.mLastUnforcedTopOffset = ++ Math.round(mBrowserControlsStateProvider.getTopVisibleContentOffset()); ++ } ++ } ++ ++ BrowserControlsSnapshot snapshot = mControlsState.mSnapshot; ++ if (snapshot == null || width != snapshot.mWidth || !isProtectedKeyboardResize) { ++ captureCurrentBrowserControlsState(width); ++ snapshot = mControlsState.mSnapshot; ++ } ++ if (!isProtectedKeyboardResize || snapshot == null) { ++ return currentControlsInsets; ++ } ++ ++ if (snapshot.mTrackingMode == BrowserControlsTrackingMode.VISIBLE_CONTROLS ++ && mBrowserControlsStateProvider != null) { ++ int visibleTopControlsHeight = ++ Math.max( ++ 0, ++ Math.round(mBrowserControlsStateProvider.getTopVisibleContentOffset())); ++ int reservedTopControlsHeight = ++ mControlsState.mEarlyShowCaptured ++ || mControlsState.mEditableTopControlsActive ++ ? snapshot.mTopHeight ++ : visibleTopControlsHeight; ++ int visibleBottomControlsHeight = ++ getVisibleBottomControlsHeight( ++ Math.round(mBrowserControlsStateProvider.getBottomControlOffset())); ++ int reservedBottomControlsHeight = ++ !mImeClosing && mControlsState.mEarlyShowCaptured ++ ? Math.min( ++ visibleBottomControlsHeight, ++ snapshot.mVisibleBottomHeight) ++ : visibleBottomControlsHeight; ++ int trackedControlsInsets = reservedTopControlsHeight + reservedBottomControlsHeight; ++ if (trackedControlsInsets != snapshot.mInsets) { ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--trackVisibleControls top=%d reservedTop=%d" ++ + " bottom=%d reservedBottom=%d closing=%b inset=%d->%d", ++ visibleTopControlsHeight, ++ reservedTopControlsHeight, ++ visibleBottomControlsHeight, ++ reservedBottomControlsHeight, ++ mImeClosing, ++ snapshot.mInsets, ++ trackedControlsInsets); ++ } ++ snapshot.mInsets = trackedControlsInsets; ++ } ++ } ++ return snapshot.mInsets; ++ } ++ ++ /** Returns the top-controls height that Blink should observe during protected IME resize. */ ++ public int getProtectedTopControlsHeight(int currentValue) { ++ BrowserControlsSnapshot snapshot = mControlsState.mSnapshot; ++ return isProtectedKeyboardResize(getKeyboardInset()) && snapshot != null ++ ? snapshot.mTopHeight ++ : currentValue; ++ } ++ ++ /** Returns the top-controls minimum height preserved for protected IME resize. */ ++ public int getProtectedTopControlsMinHeight(int currentValue) { ++ BrowserControlsSnapshot snapshot = mControlsState.mSnapshot; ++ return isProtectedKeyboardResize(getKeyboardInset()) && snapshot != null ++ ? snapshot.mTopMinHeight ++ : currentValue; ++ } ++ ++ /** Returns the bottom-controls height preserved for protected IME resize. */ ++ public int getProtectedBottomControlsHeight(int currentValue) { ++ BrowserControlsSnapshot snapshot = mControlsState.mSnapshot; ++ return isProtectedKeyboardResize(getKeyboardInset()) && snapshot != null ++ ? snapshot.mBottomHeight ++ : currentValue; ++ } ++ ++ /** Returns the bottom-controls minimum height preserved for protected IME resize. */ ++ public int getProtectedBottomControlsMinHeight(int currentValue) { ++ BrowserControlsSnapshot snapshot = mControlsState.mSnapshot; ++ return isProtectedKeyboardResize(getKeyboardInset()) && snapshot != null ++ ? snapshot.mBottomMinHeight ++ : currentValue; ++ } ++ ++ /** ++ * Captures browser-controls state synchronously before an editable forces their visibility. ++ * ++ *

This early snapshot closes the interval before Android reports the IME animation. ++ */ ++ public void prepareForKeyboardShow() { ++ refreshEnabledState(); ++ if (!isFocusedElementEditable() ++ || isProtectedKeyboardResizeWithoutEditableTopControls(getKeyboardInset())) { ++ return; ++ } ++ mImeClosing = false; ++ mControlsState.mEditableTopOffsetBaseline = ++ mBrowserControlsStateProvider != null ++ ? Math.round(mBrowserControlsStateProvider.getTopVisibleContentOffset()) ++ : 0; ++ captureBrowserControlsSnapshot( ++ getViewportWidth(), ++ /* controlsResizeView= */ true, ++ /* preserveVisibleBottomControls= */ true); ++ mControlsState.mEarlyShowCaptured = true; ++ } ++ ++ /** ++ * Keeps Chromium content at the largest neutral height observed for the current width. ++ * ++ *

An IME resize therefore creates physical scroll range around unchanged Blink content. ++ */ ++ @Override ++ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ++ int viewportWidth = ++ Math.max( ++ 0, ++ MeasureSpec.getSize(widthMeasureSpec) ++ - getPaddingLeft() ++ - getPaddingRight()); ++ int viewportHeight = ++ Math.max( ++ 0, ++ MeasureSpec.getSize(heightMeasureSpec) ++ - getPaddingTop() ++ - getPaddingBottom()); ++ ++ if (!mEnabled) { ++ mNeutralContentWidth = viewportWidth; ++ mNeutralContentHeight = viewportHeight; ++ FrameLayout.LayoutParams contentLayoutParams = ++ (FrameLayout.LayoutParams) mContent.getLayoutParams(); ++ contentLayoutParams.height = viewportHeight; ++ super.onMeasure(widthMeasureSpec, heightMeasureSpec); ++ return; ++ } ++ ++ // Keep Chromium at the largest (neutral) viewport height observed for this width. The IME ++ // may shrink this ScrollView, but it must only expose more physical scroll range; it must ++ // not resize mContent. A width change denotes a new configuration (for example rotation) ++ // and establishes a new neutral height. ++ if (viewportWidth != mNeutralContentWidth) { ++ mNeutralContentWidth = viewportWidth; ++ mNeutralContentHeight = viewportHeight; ++ } else { ++ mNeutralContentHeight = Math.max(mNeutralContentHeight, viewportHeight); ++ } ++ ++ FrameLayout.LayoutParams contentLayoutParams = ++ (FrameLayout.LayoutParams) mContent.getLayoutParams(); ++ contentLayoutParams.height = mNeutralContentHeight; ++ ++ // Once the focus request has been cleared, no synthetic scroll range may survive the IME ++ // transition. Enforce this during measurement as well as in resetViewportPan(). ++ if (!mPanState.hasRequest()) { ++ mPanState.mRequestedBottomClearance = 0; ++ mPanState.mUnlockedPhysicalPanY = 0; ++ } ++ ++ super.onMeasure(widthMeasureSpec, heightMeasureSpec); ++ } ++ ++ void logBrowserControlsState( ++ String phase, ++ int callbackTopOffset, ++ int currentTopOffset, ++ int contentOffset, ++ float topVisibleContentOffset, ++ boolean isVisibilityForced) { ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--browserControls phase=%s callbackTopOffset=%d currentTopOffset=%d" ++ + " contentOffset=%d topVisibleContentOffset=%.2f forced=%b" ++ + " top=%d translationY=%.2f height=%d contentHeight=%d" ++ + " scrollY=%d requestedRect=%s requestedBottomOnScreen=%d", ++ phase, ++ callbackTopOffset, ++ currentTopOffset, ++ contentOffset, ++ topVisibleContentOffset, ++ isVisibilityForced, ++ getTop(), ++ getTranslationY(), ++ getHeight(), ++ mContent.getHeight(), ++ getScrollY(), ++ mPanState.mRequestedRect, ++ !mPanState.hasRequest() ? -1 : mPanState.mRequestedRect.bottom - getScrollY()); ++ } ++ } ++ ++ /** Claims an unused compositor sequence only while more physical pan exists. */ ++ public boolean startCompositorOverscrollHandoff() { ++ if (!mEnabled) return false; ++ mCompositorHandoff.mPullStartY = getScrollY(); ++ boolean canConsume = ++ mPanState.hasRequest() && getScrollY() < getMaximumScrollY(); ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, "--startCompositorOverscrollHandoff canConsume=%b startY=%d max=%d", ++ canConsume, mCompositorHandoff.mPullStartY, getMaximumScrollY()); ++ } ++ return canConsume; ++ } ++ ++ /** Applies cc's cumulative unused vertical delta to the remaining physical range. */ ++ public void pullCompositorOverscroll(float accumulatedDeltaY) { ++ int oldScrollY = getScrollY(); ++ int targetScrollY = Math.max(0, Math.min( ++ mCompositorHandoff.mPullStartY + Math.round(accumulatedDeltaY), ++ getMaximumScrollY())); ++ if (targetScrollY != oldScrollY) { ++ mPanState.mUnlockedPhysicalPanY = ++ Math.max(mPanState.mUnlockedPhysicalPanY, targetScrollY); ++ scrollTo(0, targetScrollY); ++ } ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, ++ "--pullCompositorOverscroll accumulatedDeltaY=%.2f start=%d old=%d" ++ + " target=%d actual=%d max=%d", ++ accumulatedDeltaY, mCompositorHandoff.mPullStartY, oldScrollY, ++ targetScrollY, getScrollY(), getMaximumScrollY()); ++ } ++ } ++ ++ /** Clears the retained focus request and returns the physical viewport to its neutral origin. */ ++ public void resetViewportPan() { ++ mPanState.reset(); ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, "--resetViewportPan"); ++ } ++ scrollTo(0, 0); ++ } ++ ++ /** ++ * Updates the part of the bottom browser controls that has physically disappeared for the IME. ++ * ++ *

Blink retains the unobstructed controls geometry while the protected viewport is active, ++ * so the focused rectangle's neutral bottom clearance still contains this hidden UI. Remove ++ * only the physically hidden portion when positioning that rectangle above the keyboard. ++ */ ++ private void setHiddenBottomControlsHeight(int hiddenBottomControlsHeight) { ++ int clampedHeight = Math.max(0, hiddenBottomControlsHeight); ++ if (mPanState.mHiddenBottomControlsHeight == clampedHeight) return; ++ mPanState.mHiddenBottomControlsHeight = clampedHeight; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, "--hiddenBottomControls height=%d", mPanState.mHiddenBottomControlsHeight); ++ } ++ if (mPanState.hasRequest()) { ++ applyRequestedPan(); ++ } ++ } ++ ++ /** Updates the top-controls offset that Chromium has added to focused-element rectangles. */ ++ private void setShownTopControlsHeight(int shownTopControlsHeight) { ++ int clampedHeight = Math.max(0, shownTopControlsHeight); ++ if (mShownTopControlsHeight == clampedHeight) return; ++ mShownTopControlsHeight = clampedHeight; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, "--shownTopControls height=%d", mShownTopControlsHeight); ++ } ++ } ++ ++ private void updateGestureRoutingState(MotionEvent event) { ++ int action = event.getActionMasked(); ++ if (action == MotionEvent.ACTION_DOWN) { ++ mGestureStartX = event.getX(); ++ mGestureStartY = event.getY(); ++ mGestureRoutingState = GestureRoutingState.UNDECIDED; ++ return; ++ } ++ if (action != MotionEvent.ACTION_MOVE ++ || mGestureRoutingState != GestureRoutingState.UNDECIDED) { ++ return; ++ } ++ ++ float fingerDeltaX = event.getX() - mGestureStartX; ++ float fingerDeltaY = event.getY() - mGestureStartY; ++ float absoluteDeltaX = Math.abs(fingerDeltaX); ++ float absoluteDeltaY = Math.abs(fingerDeltaY); ++ if (Math.max(absoluteDeltaX, absoluteDeltaY) ++ <= mViewConfiguration.getScaledTouchSlop()) { ++ return; ++ } ++ ++ mGestureRoutingState = ++ absoluteDeltaY > absoluteDeltaX ++ ? GestureRoutingState.VERTICAL ++ : GestureRoutingState.HORIZONTAL; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--gestureAxis axis=%s fingerDeltaX=%f fingerDeltaY=%f", ++ mGestureRoutingState == GestureRoutingState.VERTICAL ++ ? "vertical" ++ : "horizontal", ++ fingerDeltaX, ++ fingerDeltaY); ++ } ++ } ++ ++ /** ++ * Returns whether CompositorViewHolder must leave the current sequence to this ScrollView. ++ * ++ *

The decision is latched for the rest of the gesture. Otherwise the parent layout could ++ * intercept immediately after the physical viewport reaches its edge, before overScrollBy() ++ * can forward the remaining motion to Blink. ++ */ ++ public boolean shouldDeferParentInterception(MotionEvent event) { ++ if (!mEnabled) return false; ++ updateGestureRoutingState(event); ++ int action = event.getActionMasked(); ++ if (action == MotionEvent.ACTION_MOVE ++ && mGestureRoutingState == GestureRoutingState.VERTICAL ++ && getMaximumScrollY() > 0) { ++ mGestureRoutingState = GestureRoutingState.VERTICAL_PARENT_DEFERRED; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--deferParentInterception scrollY=%d max=%d", ++ getScrollY(), ++ getMaximumScrollY()); ++ } ++ } ++ ++ boolean defer = ++ mGestureRoutingState == GestureRoutingState.VERTICAL_PARENT_DEFERRED; ++ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { ++ mGestureRoutingState = GestureRoutingState.UNDECIDED; ++ } ++ return defer; ++ } ++ ++ /** Intercepts only gestures assigned to the vertical protected-viewport axis. */ ++ @Override ++ public boolean onInterceptTouchEvent(MotionEvent event) { ++ if (!mEnabled) return false; ++ updateGestureRoutingState(event); ++ if (mGestureRoutingState == GestureRoutingState.HORIZONTAL) { ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i(TAG, "--skipProtectedIntercept horizontal gesture"); ++ } ++ return false; ++ } ++ return super.onInterceptTouchEvent(event); ++ } ++ ++ /** ++ * Tracks velocity and completes any compositor scroll sequence attached to this touch gesture. ++ */ ++ @Override ++ public boolean dispatchTouchEvent(MotionEvent event) { ++ if (!mEnabled) return super.dispatchTouchEvent(event); ++ int action = event.getActionMasked(); ++ if (action == MotionEvent.ACTION_DOWN) { ++ beginTrackingGesture(event); ++ } else if (mGestureVelocityTracker != null) { ++ mGestureVelocityTracker.addMovement(event); ++ } ++ mLastGestureEventTimeMs = event.getEventTime(); ++ mLastGestureX = event.getX(); ++ mLastGestureY = event.getY(); ++ ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--dispatchTouch before action=%s time=%d x=%.1f y=%.1f" ++ + " blinkScroll=%b forwarded=%b scrollY=%d physicalLimit=%d", ++ MotionEvent.actionToString(action), ++ event.getEventTime(), ++ event.getX(), ++ event.getY(), ++ mCompositorHandoff.mScrollInProgress, ++ mCompositorHandoff.mCurrentGestureForwardedDelta, ++ getScrollY(), ++ getUnlockedPhysicalPanY()); ++ } ++ ++ boolean handOffAtNeutralEdge = false; ++ float fingerVelocityY = 0; ++ int unlockedPhysicalPanY = getUnlockedPhysicalPanY(); ++ if (action == MotionEvent.ACTION_UP && mGestureVelocityTracker != null) { ++ mGestureVelocityTracker.computeCurrentVelocity( ++ 1000, mViewConfiguration.getScaledMaximumFlingVelocity()); ++ fingerVelocityY = mGestureVelocityTracker.getYVelocity(mGesturePointerId); ++ int minimumFlingVelocity = mViewConfiguration.getScaledMinimumFlingVelocity(); ++ handOffAtNeutralEdge = ++ mCompositorHandoff.mCurrentGestureForwardedDelta ++ && ((getScrollY() == 0 ++ && fingerVelocityY > minimumFlingVelocity) ++ || (getScrollY() == unlockedPhysicalPanY ++ && fingerVelocityY < -minimumFlingVelocity)); ++ } ++ ++ boolean handled = super.dispatchTouchEvent(event); ++ ++ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { ++ if (mCompositorHandoff.mScrollInProgress) { ++ float handoffVelocityY = ++ action == MotionEvent.ACTION_UP && handOffAtNeutralEdge ++ ? fingerVelocityY ++ : 0; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--ccScrollEnd scrollY=%d fingerVelocityY=%f handoffVelocityY=%f", ++ getScrollY(), ++ fingerVelocityY, ++ handoffVelocityY); ++ } ++ mCompositorScrollDelegate.endCompositorScroll( ++ event.getEventTime(), ++ event.getX(), ++ event.getY(), ++ handoffVelocityY); ++ mCompositorHandoff.mScrollInProgress = false; ++ } ++ endTrackingGesture(); ++ } ++ return handled; ++ } ++ ++ /** ++ * Splits vertical motion between the unlocked physical pan and Chromium's compositor. ++ * ++ *

Once the physical range reaches an edge, the unconsumed delta continues in cc rather than ++ * terminating the user's gesture at the ScrollView boundary. ++ */ ++ @Override ++ protected boolean overScrollBy( ++ int deltaX, ++ int deltaY, ++ int scrollX, ++ int scrollY, ++ int scrollRangeX, ++ int scrollRangeY, ++ int maxOverScrollX, ++ int maxOverScrollY, ++ boolean isTouchEvent) { ++ if (!mEnabled) { ++ return super.overScrollBy( ++ deltaX, ++ deltaY, ++ scrollX, ++ scrollY, ++ scrollRangeX, ++ scrollRangeY, ++ maxOverScrollX, ++ maxOverScrollY, ++ isTouchEvent); ++ } ++ int physicalPanLimitY = getUnlockedPhysicalPanY(); ++ int targetY = scrollY + deltaY; ++ int clampedTargetY = Math.max(0, Math.min(targetY, physicalPanLimitY)); ++ int consumedY = ++ mCompositorHandoff.mScrollInProgress ? 0 : clampedTargetY - scrollY; ++ int unconsumedY = deltaY - consumedY; ++ ++ boolean clamped = ++ super.overScrollBy( ++ deltaX, ++ consumedY, ++ scrollX, ++ scrollY, ++ scrollRangeX, ++ scrollRangeY, ++ maxOverScrollX, ++ maxOverScrollY, ++ isTouchEvent); ++ ++ if (isTouchEvent && getMaximumScrollY() > 0 && unconsumedY != 0) { ++ mCompositorHandoff.mCurrentGestureForwardedDelta = true; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--overscrollHandoff scrollY=%d range=%d physicalLimit=%d" ++ + " deltaY=%d consumedY=%d unconsumedY=%d", ++ scrollY, ++ scrollRangeY, ++ physicalPanLimitY, ++ deltaY, ++ consumedY, ++ unconsumedY); ++ } ++ ++ // EventForwarder expects widget coordinates. Adding the physical ScrollView offset ++ // can put cc's hit-test point outside the widget. ++ float contentY = mLastGestureY; ++ if (!mCompositorHandoff.mScrollInProgress) { ++ mCompositorScrollDelegate.beginCompositorScroll( ++ mLastGestureEventTimeMs, ++ mLastGestureX, ++ contentY, ++ unconsumedY); ++ mCompositorHandoff.mScrollInProgress = true; ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--ccScrollBegin time=%d point=%.1f,%.1f deltaY=%d", ++ mLastGestureEventTimeMs, ++ mLastGestureX, ++ contentY, ++ unconsumedY); ++ } ++ } ++ mCompositorScrollDelegate.updateCompositorScroll( ++ mLastGestureEventTimeMs, mLastGestureX, contentY, unconsumedY); ++ ++ // ScrollView clears its VelocityTracker when this method reports a clamp. For this ++ // container the edge is not a barrier: the remaining motion continues in cc. ++ return false; ++ } ++ return clamped; ++ } ++ ++ /** ++ * Converts the first focused-editable visibility request into a retained physical pan target. ++ * ++ *

Later requests are acknowledged without replacing the target while the same IME session is ++ * active, so page-side focus changes cannot repeatedly reposition the protected viewport. ++ */ ++ @Override ++ public boolean requestChildRectangleOnScreen( ++ View child, Rect rectangle, boolean immediate) { ++ if (!mEnabled) { ++ return super.requestChildRectangleOnScreen(child, rectangle, immediate); ++ } ++ if (mPanState.hasRequest()) { ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--ignoreChildRectangleRequest child=%s sourceRect=%s retainedRect=%s scrollY=%d", ++ child.getClass().getSimpleName(), ++ rectangle, ++ mPanState.mRequestedRect, ++ getScrollY()); ++ } ++ return true; ++ } ++ Rect target = new Rect(rectangle); ++ offsetDescendantRectToMyCoords(child, target); ++ target.offset(0, -mShownTopControlsHeight); ++ ++ // ImeAdapterImpl includes Chromium's content Y offset in the focused editable rectangle. ++ // With a protected, full-height mContent this can place the requested rectangle below the ++ // physical content boundary. Preserve its size while bringing it back inside that boundary. ++ int bottomOverflow = ++ mContent.getHeight() > 0 ++ ? Math.max(0, target.bottom - mContent.getHeight()) ++ : 0; ++ if (bottomOverflow > 0) target.offset(0, -bottomOverflow); ++ ++ // offsetDescendantRectToMyCoords() already returns coordinates in this ScrollView's ++ // content space. Adding scrollY again would double-count the physical pan. ++ mPanState.mRequestedRect.set(target); ++ int neutralVisibleBottom = ++ Math.max(0, mContent.getHeight() - getPaddingBottom()); ++ mPanState.mRequestedBottomClearance = ++ Math.max(0, neutralVisibleBottom - mPanState.mRequestedRect.bottom); ++ ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--requestChildRectangleOnScreen child=%s sourceRect=%s targetRect=%s" ++ + " immediate=%b top=%d height=%d contentHeight=%d" ++ + " scrollContentHeight=%d shownTopControls=%d" ++ + " bottomClearance=%d scrollY=%d", ++ child.getClass().getSimpleName(), ++ rectangle, ++ mPanState.mRequestedRect, ++ immediate, ++ getTop(), ++ getHeight(), ++ mContent.getHeight(), ++ mScrollContent.getHeight(), ++ mShownTopControlsHeight, ++ mPanState.mRequestedBottomClearance, ++ getScrollY()); ++ } ++ ++ // The focus request can arrive before the keyboard animation has updated this view. ++ post(() -> applyRequestedPan()); ++ return true; ++ } ++ ++ /** Reapplies a retained pan after Android has delivered the latest IME-driven view size. */ ++ @Override ++ protected void onSizeChanged(int w, int h, int oldw, int oldh) { ++ super.onSizeChanged(w, h, oldw, oldh); ++ ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--onSizeChanged size=%dx%d oldSize=%dx%d top=%d contentHeight=%d" ++ + " scrollY=%d requestedRect=%s", ++ w, ++ h, ++ oldw, ++ oldh, ++ getTop(), ++ mContent.getHeight(), ++ getScrollY(), ++ mPanState.mRequestedRect); ++ } ++ ++ // The focus request normally arrives before Android finishes resizing for the keyboard. ++ if (mPanState.hasRequest()) { ++ post(() -> applyRequestedPan()); ++ } ++ } ++ ++ /** ++ * Positions the retained rectangle above the keyboard while preserving its neutral clearance. ++ * ++ *

Browser-controls UI that physically disappeared is removed from that clearance, and the ++ * resulting pan becomes the range that direct touch scrolling may consume. ++ */ ++ private void applyRequestedPan() { ++ if (!mPanState.hasRequest() || getHeight() == 0) { ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--applyRequestedPan (exit1) rect=%s height=%d contentHeight=%d", ++ mPanState.mRequestedRect, ++ getHeight(), ++ mContent.getHeight()); ++ } ++ return; ++ } ++ ++ int visibleBottom = getHeight() - getPaddingBottom(); ++ int minimumScrollY = Math.max(0, mPanState.mRequestedRect.bottom - visibleBottom); ++ int effectiveBottomClearance = ++ Math.max( ++ 0, ++ mPanState.mRequestedBottomClearance ++ - mPanState.mHiddenBottomControlsHeight); ++ int requestedScrollY = minimumScrollY; ++ if (minimumScrollY > 0) { ++ requestedScrollY += effectiveBottomClearance; ++ } ++ int maximumScrollY = getMaximumScrollY(); ++ requestedScrollY = Math.min(requestedScrollY, maximumScrollY); ++ mPanState.mUnlockedPhysicalPanY = requestedScrollY; ++ ++ scrollTo(0, requestedScrollY); ++ ++ if (DEBUG_DIAGNOSTICS) { ++ Log.i( ++ TAG, ++ "--applyRequestedPan rect=%s top=%d height=%d visibleBottom=%d" ++ + " contentHeight=%d minimumScrollY=%d bottomClearance=%d" ++ + " hiddenBottomControls=%d effectiveBottomClearance=%d" ++ + " requestedScrollY=%d actualScrollY=%d max=%d" ++ + " requestedBottomOnScreen=%d", ++ mPanState.mRequestedRect, ++ getTop(), ++ getHeight(), ++ visibleBottom, ++ mContent.getHeight(), ++ minimumScrollY, ++ mPanState.mRequestedBottomClearance, ++ mPanState.mHiddenBottomControlsHeight, ++ effectiveBottomClearance, ++ requestedScrollY, ++ getScrollY(), ++ maximumScrollY, ++ mPanState.mRequestedRect.bottom - getScrollY()); ++ } ++ } ++ ++ /** Starts IME animation observation when policy and attachment state both allow it. */ ++ @Override ++ protected void onAttachedToWindow() { ++ super.onAttachedToWindow(); ++ registerInsetObserver(); ++ } ++ ++ /** Stops IME animation observation before the protected viewport leaves the hierarchy. */ ++ @Override ++ protected void onDetachedFromWindow() { ++ unregisterInsetObserver(); ++ mImeAnimationRunning = false; ++ super.onDetachedFromWindow(); ++ } ++ ++ private int getMaximumScrollY() { ++ return Math.max( ++ 0, mScrollContent.getHeight() - getHeight() + getPaddingTop() + getPaddingBottom()); ++ } ++ ++ private int getUnlockedPhysicalPanY() { ++ return Math.min(mPanState.mUnlockedPhysicalPanY, getMaximumScrollY()); ++ } ++ ++ private void beginTrackingGesture(MotionEvent event) { ++ if (mGestureVelocityTracker == null) { ++ mGestureVelocityTracker = VelocityTracker.obtain(); ++ } else { ++ mGestureVelocityTracker.clear(); ++ } ++ mGestureVelocityTracker.addMovement(event); ++ mGesturePointerId = event.getPointerId(0); ++ mCompositorHandoff.mCurrentGestureForwardedDelta = false; ++ } ++ ++ private void endTrackingGesture() { ++ if (mGestureVelocityTracker != null) { ++ mGestureVelocityTracker.recycle(); ++ mGestureVelocityTracker = null; ++ } ++ mGesturePointerId = INVALID_POINTER_ID; ++ mCompositorHandoff.mCurrentGestureForwardedDelta = false; ++ } ++} +diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java +--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java ++++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java +@@ -64,6 +64,7 @@ public class TabWebContentsObserver extends TabWebContentsUserData { + private final ObserverList> mInitObservers = new ObserverList<>(); + private @Nullable Observer mObserver; + private @Nullable GURL mLastUrl; ++ private @Nullable Boolean mProtectedKeyboardGeometryEnabled; + + public static TabWebContentsObserver from(Tab tab) { + TabWebContentsObserver observer = get(tab); +@@ -109,6 +110,7 @@ public class TabWebContentsObserver extends TabWebContentsUserData { + + @Override + public void initWebContents(WebContents webContents) { ++ mProtectedKeyboardGeometryEnabled = null; + mObserver = new Observer(webContents); + + for (Callback callback : mInitObservers) callback.onResult(webContents); +@@ -123,6 +125,17 @@ public class TabWebContentsObserver extends TabWebContentsUserData { + } + } + ++ /** Returns the protected keyboard geometry decision cached for the current document. */ ++ public @Nullable Boolean getProtectedKeyboardGeometryEnabledForCurrentDocument() { ++ return mProtectedKeyboardGeometryEnabled; ++ } ++ ++ /** Caches the protected keyboard geometry decision for the current document. */ ++ public void setProtectedKeyboardGeometryEnabledForCurrentDocument( ++ @Nullable Boolean enabled) { ++ mProtectedKeyboardGeometryEnabled = enabled; ++ } ++ + public void simulateRendererKilledForTesting() { + if (mObserver != null) { + mObserver.primaryMainFrameRenderProcessGone(TerminationStatus.PROCESS_WAS_KILLED); +@@ -304,6 +317,10 @@ public class TabWebContentsObserver extends TabWebContentsUserData { + + @Override + public void didFinishNavigationInPrimaryMainFrame(NavigationHandle navigation) { ++ if (navigation.hasCommitted()) { ++ mProtectedKeyboardGeometryEnabled = null; ++ } ++ + RewindableIterator observers = mTab.getTabObservers(); + while (observers.hasNext()) { + observers.next().onDidFinishNavigationInPrimaryMainFrame(mTab, navigation); +diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java +--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java ++++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java +@@ -1453,7 +1453,7 @@ public abstract class ChromeFeatureList { + public static final MutableFlagWithSafeDefault sAndroidZoomImmersive = + newMutableFlagWithSafeDefault(ANDROID_ZOOM_IMMERSIVE, false); + public static final MutableFlagWithSafeDefault sBrowserControlsEarlyResize = +- newMutableFlagWithSafeDefault(BROWSER_CONTROLS_EARLY_RESIZE, false); ++ newMutableFlagWithSafeDefault(BROWSER_CONTROLS_EARLY_RESIZE, true); + public static final MutableFlagWithSafeDefault sBrowserControlsPersistsOnCvh = + newMutableFlagWithSafeDefault(BROWSER_CONTROLS_PERSISTS_ON_CVH, true); + // Default to false. The logic behind the flag is not relevant when native is not initialized. +diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sAndroidProtectedKeyboardGeometry.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sAndroidProtectedKeyboardGeometry.java +new file mode 100644 +--- /dev/null ++++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sAndroidProtectedKeyboardGeometry.java +@@ -0,0 +1,35 @@ ++/* ++ This file is part of Cromite. ++ ++ Cromite is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Cromite is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Cromite. If not, see . ++*/ ++ ++package org.chromium.chrome.browser.flags.cromite; ++ ++import org.chromium.chrome.browser.flags.ChromeFeatureMap; ++import org.chromium.components.cached_flags.CachedFlag; ++ ++public class sAndroidProtectedKeyboardGeometry { ++ private static final CachedFlag sInstance = ++ new CachedFlag( ++ ChromeFeatureMap.getInstance(), ++ "AndroidProtectedKeyboardGeometry", ++ true); ++ ++ private sAndroidProtectedKeyboardGeometry() {} ++ ++ public static CachedFlag getInstance() { ++ return sInstance; ++ } ++} +diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/impl/CromiteProtectedKeyboardGeometryContentSetting.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/impl/CromiteProtectedKeyboardGeometryContentSetting.java +new file mode 100644 +--- /dev/null ++++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/impl/CromiteProtectedKeyboardGeometryContentSetting.java +@@ -0,0 +1,94 @@ ++/* ++ This file is part of Bromite. ++ ++ Bromite is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Bromite is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Bromite. If not, see . ++*/ ++ ++package org.chromium.components.browser_ui.site_settings.impl; ++ ++import org.chromium.components.browser_ui.site_settings.R; ++ ++import org.chromium.components.browser_ui.site_settings.BromiteCustomContentSetting; ++import org.chromium.components.browser_ui.site_settings.ContentSettingsResources; ++import org.chromium.components.browser_ui.site_settings.SiteSettingsCategory; ++import org.chromium.components.content_settings.ContentSetting; ++import org.chromium.components.content_settings.ContentSettingsType; ++import org.chromium.content_public.browser.BrowserContextHandle; ++ ++import androidx.annotation.Nullable; ++import androidx.preference.Preference; ++import androidx.preference.PreferenceScreen; ++ ++import java.util.ArrayList; ++ ++public class CromiteProtectedKeyboardGeometryContentSetting extends BromiteCustomContentSetting { ++ public CromiteProtectedKeyboardGeometryContentSetting() { ++ super(/*contentSettingsType*/ ContentSettingsType.PROTECTED_KEYBOARD_GEOMETRY, ++ /*defaultEnabledValue*/ ContentSetting.ALLOW, ++ /*defaultDisabledValue*/ ContentSetting.BLOCK, ++ /*allowException*/ true, ++ /*preferenceKey*/ "keyboard_size_protection", ++ /*profilePrefKey*/ "keyboard_size_protection"); ++ } ++ ++ @Override ++ public ContentSettingsResources.ResourceItem getResourceItem() { ++ return new ContentSettingsResources.ResourceItem( ++ /*icon*/ R.drawable.web_asset, ++ /*title*/ R.string.protected_keyboard_geometry_permission_title, ++ /*defaultEnabledValue*/ getDefaultEnabledValue(), ++ /*defaultDisabledValue*/ getDefaultDisabledValue(), ++ /*enabledSummary*/ R.string.website_settings_category_protected_keyboard_geometry_enabled, ++ /*disabledSummary*/ R.string.website_settings_category_protected_keyboard_geometry_disabled, ++ /*summaryOverrideForScreenReader*/ 0); ++ } ++ ++ @Override ++ public int getCategorySummary(@Nullable @ContentSetting int value) { ++ switch (value) { ++ case ContentSetting.ALLOW: ++ return R.string.website_settings_category_protected_keyboard_geometry_enabled; ++ case ContentSetting.BLOCK: ++ return R.string.website_settings_category_protected_keyboard_geometry_disabled; ++ default: ++ // this will cause a runtime exception ++ return 0; ++ } ++ } ++ ++ @Override ++ public int getCategoryDescription() { ++ return R.string.settings_site_settings_protected_keyboard_geometry_description; ++ } ++ ++ @Override ++ public boolean requiresTriStateContentSetting() { ++ return false; ++ } ++ ++ @Override ++ public boolean showOnlyDescriptions() { ++ return true; ++ } ++ ++ @Override ++ public int getAddExceptionDialogMessage() { ++ return R.string.website_settings_category_protected_keyboard_geometry_enabled; ++ } ++ ++ @Override ++ public @Nullable Boolean considerException(SiteSettingsCategory category, @ContentSetting int value) { ++ return value != ContentSetting.BLOCK; ++ } ++} +diff --git a/components/browser_ui/strings/bromite_content_settings/protected_keyboard_geometry.grdp b/components/browser_ui/strings/bromite_content_settings/protected_keyboard_geometry.grdp +new file mode 100644 +--- /dev/null ++++ b/components/browser_ui/strings/bromite_content_settings/protected_keyboard_geometry.grdp +@@ -0,0 +1,27 @@ ++ ++ ++ ++ Keyboard Size Protection ++ ++ ++ Keyboard Size Protection ++ ++ ++ Keyboard Size Protection ++ ++ ++ Hides the keyboard size ++ ++ ++ Enabled ++ ++ ++ Disabled ++ ++ ++ Keyboard size is protected ++ ++ ++ Keyboard size is not protected ++ ++ +diff --git a/components/content_settings/core/browser/bromite_content_settings/protected_keyboard_geometry.inc b/components/content_settings/core/browser/bromite_content_settings/protected_keyboard_geometry.inc +new file mode 100644 +--- /dev/null ++++ b/components/content_settings/core/browser/bromite_content_settings/protected_keyboard_geometry.inc +@@ -0,0 +1,21 @@ ++ Register(ContentSettingsType::PROTECTED_KEYBOARD_GEOMETRY, "keyboard_size_protection", CONTENT_SETTING_ALLOW, ++ WebsiteSettingsInfo::SYNCABLE, ++ /*allowlisted_schemes=*/{}, ++ /*valid_settings=*/{CONTENT_SETTING_ALLOW, ++ CONTENT_SETTING_BLOCK}, ++ WebsiteSettingsInfo::TOP_ORIGIN_ONLY_SCOPE, ++ WebsiteSettingsRegistry::PLATFORM_ANDROID, ++ ContentSettingsInfo::INHERIT_IN_INCOGNITO, ++ PermissionSettingsInfo::EXCEPTIONS_ON_SECURE_AND_INSECURE_ORIGINS); ++ ++ content_settings::WebsiteSettingsRegistry::GetInstance() ++ ->GetMutable(ContentSettingsType::PROTECTED_KEYBOARD_GEOMETRY) ++ ->set_show_into_info_page() ++ .set_is_renderer_content_setting() ++ .set_title_ui(IDS_SITE_SETTINGS_TYPE_PROTECTED_KEYBOARD_GEOMETRY) ++ .set_description_ui(IDS_SETTINGS_SITE_SETTINGS_PROTECTED_KEYBOARD_GEOMETRY_DESCRIPTION) ++ .set_allowed_ui(IDS_WEBSITE_SETTINGS_CATEGORY_PROTECTED_KEYBOARD_GEOMETRY_ENABLED) ++ .set_blocked_ui(IDS_WEBSITE_SETTINGS_CATEGORY_PROTECTED_KEYBOARD_GEOMETRY_DISABLED) ++ .set_allowed_exceptions_ui(IDS_SETTINGS_SITE_SETTINGS_PROTECTED_KEYBOARD_GEOMETRY_ALLOWED_EXCEPTIONS) ++ .set_blocked_exceptions_ui(IDS_SETTINGS_SITE_SETTINGS_PROTECTED_KEYBOARD_GEOMETRY_BLOCKED_EXCEPTIONS) ++ .set_mid_sentence_ui(IDS_SITE_SETTINGS_TYPE_PROTECTED_KEYBOARD_GEOMETRY_MID_SENTENCE); +diff --git a/components/content_settings/core/common/bromite_content_settings/protected_keyboard_geometry.inc b/components/content_settings/core/common/bromite_content_settings/protected_keyboard_geometry.inc +new file mode 100644 +--- /dev/null ++++ b/components/content_settings/core/common/bromite_content_settings/protected_keyboard_geometry.inc +@@ -0,0 +1,2 @@ ++PROTECTED_KEYBOARD_GEOMETRY, ++ +diff --git a/content/browser/android/overscroll_controller_android.cc b/content/browser/android/overscroll_controller_android.cc +--- a/content/browser/android/overscroll_controller_android.cc ++++ b/content/browser/android/overscroll_controller_android.cc +@@ -124,6 +124,15 @@ OverscrollControllerAndroid::OverscrollControllerAndroid( + OverscrollControllerAndroid::~OverscrollControllerAndroid() { + } + ++void OverscrollControllerAndroid::EndProtectedViewportHandoff() { ++ if (protected_viewport_handoff_active_) { ++ refresh_effect_->EndProtectedViewportHandoff(); ++ } ++ ++ protected_viewport_scroll_sequence_ = false; ++ protected_viewport_handoff_active_ = false; ++} ++ + void OverscrollControllerAndroid::OnGestureEvent( + const blink::WebGestureEvent& event) { + if (!ShouldHandleInputEvents()) { +@@ -132,8 +141,15 @@ void OverscrollControllerAndroid::OnGestureEvent( + + switch (event.GetType()) { + case blink::WebInputEvent::Type::kGestureScrollBegin: ++ EndProtectedViewportHandoff(); + refresh_effect_->OnScrollBegin( + gfx::ScalePoint(event.PositionInWidget(), dpi_scale_)); ++ // Do not call Java reentrantly while the synthetic GestureScrollBegin ++ // is still being routed. Remember only that this is a candidate ++ // sequence; the first DidOverscroll asks Java whether it can consume. ++ protected_viewport_scroll_sequence_ = ++ !event.data.scroll_begin.target_viewport && ++ event.data.scroll_begin.pointer_count == 0; + break; + case blink::WebInputEvent::Type::kGestureScrollUpdate: { + if (event.SourceDevice() == blink::WebGestureDevice::kTouchpad) { +@@ -143,6 +159,7 @@ void OverscrollControllerAndroid::OnGestureEvent( + } + } break; + case blink::WebInputEvent::Type::kGestureScrollEnd: { ++ EndProtectedViewportHandoff(); + if (event.SourceDevice() == blink::WebGestureDevice::kTouchpad) { + refresh_effect_->OnScrollEnd(gfx::Vector2dF()); + } +@@ -150,6 +167,7 @@ void OverscrollControllerAndroid::OnGestureEvent( + } + + case blink::WebInputEvent::Type::kGestureFlingStart: { ++ EndProtectedViewportHandoff(); + if (refresh_effect_->IsActive()) { + gfx::Vector2dF scroll_velocity(event.data.fling_start.velocity_x, + event.data.fling_start.velocity_y); +@@ -202,6 +220,26 @@ void OverscrollControllerAndroid::OnOverscrolled( + if (!enabled_) + return; + ++ if (protected_viewport_scroll_sequence_ && ++ params.latest_overscroll_delta.y() != 0.0f) { ++ if (!protected_viewport_handoff_active_) { ++ protected_viewport_handoff_active_ = ++ refresh_effect_->StartProtectedViewportHandoff(); ++ if (!protected_viewport_handoff_active_) { ++ protected_viewport_scroll_sequence_ = false; ++ } ++ } ++ if (protected_viewport_handoff_active_) { ++ // The protected sequence uses an absolute accumulated value. This makes ++ // delivery idempotent if DidOverscroll is observed more than once. ++ refresh_effect_->UpdateProtectedViewportHandoff( ++ params.accumulated_overscroll.y()); ++ // Do not set is_handling_sequence_: the physical touch remains owned by ++ // ProtectedViewportScrollView and must not be transferred back from Viz. ++ return; ++ } ++ } ++ + if (refresh_effect_) { + refresh_effect_->OnOverscrolled(params.overscroll_behavior, + params.accumulated_overscroll, +@@ -290,6 +328,8 @@ void OverscrollControllerAndroid::Enable() { + void OverscrollControllerAndroid::Disable() { + if (!enabled_) + return; ++ protected_viewport_scroll_sequence_ = false; ++ protected_viewport_handoff_active_ = false; + enabled_ = false; + if (!enabled_) { + if (refresh_effect_) +diff --git a/content/browser/android/overscroll_controller_android.h b/content/browser/android/overscroll_controller_android.h +--- a/content/browser/android/overscroll_controller_android.h ++++ b/content/browser/android/overscroll_controller_android.h +@@ -107,6 +107,7 @@ class CONTENT_EXPORT OverscrollControllerAndroid + ConsumedBeginDoesNotResetEnabledRefresh); + + bool ShouldHandleInputEvents(); ++ void EndProtectedViewportHandoff(); + void OnGestureEvent(const blink::WebGestureEvent& event); + + // To be called upon receipt of a gesture event ack. +@@ -135,6 +136,16 @@ class CONTENT_EXPORT OverscrollControllerAndroid + // action (ACTION_UP/ACTION_CANCEL) occurs. + bool is_handling_sequence_ = false; + ++ // State for returning unused compositor scrolling to the protected viewport: ++ // false, false: no eligible sequence; ++ // true, false: eligible sequence, waiting for Java to accept the handoff; ++ // true, true: Java accepted; forward unused vertical scroll deltas. ++ // `protected_viewport_handoff_active_` therefore always implies ++ // `protected_viewport_scroll_sequence_`. A terminal gesture, a replacement ++ // scroll begin, or Disable() resets both values to false. ++ bool protected_viewport_scroll_sequence_ = false; ++ bool protected_viewport_handoff_active_ = false; ++ + // Stores the last seen position of a touch input event (in pix) to correctly + // calculate scroll deltas for `refresh_effect_`. + gfx::Vector2dF last_pos_; +diff --git a/content/browser/web_contents/web_contents_android.cc b/content/browser/web_contents/web_contents_android.cc +--- a/content/browser/web_contents/web_contents_android.cc ++++ b/content/browser/web_contents/web_contents_android.cc +@@ -547,6 +547,12 @@ void WebContentsAndroid::ScrollFocusedEditableNodeIntoView(JNIEnv* env) { + input_handler->ScrollFocusedEditableNodeIntoView(); + } + ++void WebContentsAndroid::UpdateFocusedElementBounds(JNIEnv* env) { ++ auto* input_handler = web_contents_->GetFocusedFrameWidgetInputHandler(); ++ if (input_handler) ++ input_handler->UpdateFocusedElementBounds(); ++} ++ + void WebContentsAndroid::SelectAroundCaretAck( + int startOffset, + int endOffset, +diff --git a/content/browser/web_contents/web_contents_android.h b/content/browser/web_contents/web_contents_android.h +--- a/content/browser/web_contents/web_contents_android.h ++++ b/content/browser/web_contents/web_contents_android.h +@@ -111,6 +111,7 @@ class CONTENT_EXPORT WebContentsAndroid { + bool IsFullscreenForCurrentTab(JNIEnv* env); + void ExitFullscreen(JNIEnv* env); + void ScrollFocusedEditableNodeIntoView(JNIEnv* env); ++ void UpdateFocusedElementBounds(JNIEnv* env); + void SelectAroundCaret(JNIEnv* env, + int32_t granularity, + bool should_show_handle, +diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java +--- a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java ++++ b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java +@@ -183,6 +183,13 @@ public class ImeAdapterImpl + // Used to tell View#onSizeChanged to focus a form element. + private final Rect mFocusPreOSKViewportRect = new Rect(); + ++ // Bounds of the focused Blink editable in container-view pixels. Used as a fallback pan ++ // target when caret geometry is unavailable. ++ private final Rect mFocusedEditableRectPix = new Rect(); ++ ++ // Last caret bounds received through CursorAnchorInfo, in container-view pixels. ++ private final Rect mFocusedEditableCaretRectPix = new Rect(); ++ + // Keep the current configuration to detect the change when onConfigurationChanged() is called. + private Configuration mCurrentConfig; + +@@ -922,6 +929,11 @@ public class ImeAdapterImpl + // doesn't affect the size of the web contents. + mWebContents.scrollFocusedEditableNodeIntoView(); + } ++ ++ if (resultCode == InputMethodManager.RESULT_SHOWN ++ || resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN) { ++ containerView.post(this::requestFocusedTextInputRectangleOnScreen); ++ } + } + + @CalledByNative +@@ -933,6 +945,29 @@ public class ImeAdapterImpl + mFocusPreOSKViewportRect.setEmpty(); + } + ++ private void requestFocusedTextInputRectangleOnScreen() { ++ if (!isValid()) return; ++ ++ boolean usingCaret = !mFocusedEditableCaretRectPix.isEmpty(); ++ Rect requestedRect = usingCaret ? mFocusedEditableCaretRectPix : mFocusedEditableRectPix; ++ if (requestedRect.isEmpty()) return; ++ ++ boolean handled = ++ getContainerView() ++ .requestRectangleOnScreen( ++ new Rect(requestedRect), ++ /* immediate= */ false); ++ ++ if (DEBUG_LOGS) { ++ Log.i( ++ TAG, ++ "--requestRectangleOnScreen source=%s rect=%s handled=%b", ++ usingCaret ? "caret" : "editable", ++ requestedRect, ++ handled); ++ } ++ } ++ + @Override + public ResultReceiver getNewShowKeyboardReceiver() { + if (mShowKeyboardResultReceiver == null) { +@@ -1526,6 +1561,31 @@ public class ImeAdapterImpl + + View containerView = getContainerView(); + ++ mFocusedEditableCaretRectPix.setEmpty(); ++ if (isEditable) { ++ mFocusedEditableRectPix.set( ++ fromCssToDevicePix(nodeLeftDip, nodeTopDip, nodeRightDip, nodeBottomDip)); ++ if (DEBUG_LOGS) { ++ Log.i( ++ TAG, ++ "--focusedEditableGeometry css=[%d,%d - %d,%d] pix=%s", ++ nodeLeftDip, ++ nodeTopDip, ++ nodeRightDip, ++ nodeBottomDip, ++ mFocusedEditableRectPix); ++ } ++ if (isValid()) { ++ ImeAdapterImplJni.get() ++ .requestCursorUpdate( ++ mNativeImeAdapterAndroid, ++ true /* immediate request */, ++ false /* disable monitoring */); ++ } ++ } else { ++ mFocusedEditableRectPix.setEmpty(); ++ } ++ + // Update edit bounds to stylus writing service. + if (mWebContents.getStylusWritingHandler() != null) { + RenderCoordinatesImpl coords = mWebContents.getRenderCoordinates(); +@@ -1783,6 +1843,35 @@ public class ImeAdapterImpl + mCursorAnchorInfoController.updateCursorAnchorInfoData( + cursorAnchorInfo, containerView); + ++ if (cursorAnchorInfo.insertionMarker != null) { ++ var caretCss = cursorAnchorInfo.insertionMarker; ++ Rect cachedCaretPix = ++ fromCssToDevicePix( ++ caretCss.x, ++ caretCss.y, ++ caretCss.x + caretCss.width, ++ caretCss.y + caretCss.height); ++ // A collapsed caret is a vertical line, but Rect.isEmpty() also treats a ++ // zero-width rectangle as empty. Preserve it as a one-pixel pan target. ++ cachedCaretPix.right = Math.max(cachedCaretPix.left + 1, cachedCaretPix.right); ++ mFocusedEditableCaretRectPix.set(cachedCaretPix); ++ if (DEBUG_LOGS) { ++ Log.i( ++ TAG, ++ "--cursorAnchorGeometry selectionMove=%b css=[%d,%d %dx%d]" ++ + " focusedPix=%s caretPix=%s", ++ isSelectionMove, ++ caretCss.x, ++ caretCss.y, ++ caretCss.width, ++ caretCss.height, ++ mFocusedEditableRectPix, ++ mFocusedEditableCaretRectPix); ++ } ++ } else { ++ mFocusedEditableCaretRectPix.setEmpty(); ++ } ++ + // Request view system keep caret on screen when moved. + if (isSelectionMove + && cursorAnchorInfo.insertionMarker != null +@@ -1795,6 +1884,7 @@ public class ImeAdapterImpl + caretCss.y, + caretCss.x + caretCss.width, + caretCss.y + caretCss.height); ++ if (DEBUG_LOGS) Log.i(TAG, "--cursorRectangleOnScreen pix=%s", caretPix); + + // Note: `SDK_INT_FULL` added in `BAKLAVA`, hence two checks. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA +diff --git a/content/public/android/java/src/org/chromium/content/browser/webcontents/WebContentsImpl.java b/content/public/android/java/src/org/chromium/content/browser/webcontents/WebContentsImpl.java +--- a/content/public/android/java/src/org/chromium/content/browser/webcontents/WebContentsImpl.java ++++ b/content/public/android/java/src/org/chromium/content/browser/webcontents/WebContentsImpl.java +@@ -689,6 +689,12 @@ public class WebContentsImpl + WebContentsImplJni.get().scrollFocusedEditableNodeIntoView(mNativeWebContentsAndroid); + } + ++ @Override ++ public void updateFocusedElementBounds() { ++ checkNotDestroyed(); ++ WebContentsImplJni.get().updateFocusedElementBounds(mNativeWebContentsAndroid); ++ } ++ + @Override + public void selectAroundCaret( + @SelectionGranularity int granularity, +@@ -1420,6 +1426,8 @@ public class WebContentsImpl + + void scrollFocusedEditableNodeIntoView(long nativeWebContentsAndroid); + ++ void updateFocusedElementBounds(long nativeWebContentsAndroid); ++ + void selectAroundCaret( + long nativeWebContentsAndroid, + int granularity, +diff --git a/content/public/android/java/src/org/chromium/content_public/browser/WebContents.java b/content/public/android/java/src/org/chromium/content_public/browser/WebContents.java +--- a/content/public/android/java/src/org/chromium/content_public/browser/WebContents.java ++++ b/content/public/android/java/src/org/chromium/content_public/browser/WebContents.java +@@ -310,6 +310,9 @@ public interface WebContents extends Parcelable { + /** Brings the Editable to the visible area while IME is up to make easier for inputing text. */ + void scrollFocusedEditableNodeIntoView(); + ++ /** Recomputes the focused element bounds after a renderer viewport resize. */ ++ void updateFocusedElementBounds(); ++ + /** + * Selects at the specified granularity around the caret and potentially shows the selection + * handles and context menu. The caller can check if selection actually occurred by listening to +diff --git a/content/public/test/android/javatests/src/org/chromium/content_public/browser/test/mock/MockWebContents.java b/content/public/test/android/javatests/src/org/chromium/content_public/browser/test/mock/MockWebContents.java +--- a/content/public/test/android/javatests/src/org/chromium/content_public/browser/test/mock/MockWebContents.java ++++ b/content/public/test/android/javatests/src/org/chromium/content_public/browser/test/mock/MockWebContents.java +@@ -205,6 +205,9 @@ public class MockWebContents implements WebContents, WebContentsObserver.Observa + @Override + public void scrollFocusedEditableNodeIntoView() {} + ++ @Override ++ public void updateFocusedElementBounds() {} ++ + @Override + public void selectAroundCaret( + @SelectionGranularity int granularity, +diff --git a/cromite_flags/chrome/browser/about_flags_cc/Android-Protected-Keyboard-Geometry.inc b/cromite_flags/chrome/browser/about_flags_cc/Android-Protected-Keyboard-Geometry.inc +new file mode 100644 +--- /dev/null ++++ b/cromite_flags/chrome/browser/about_flags_cc/Android-Protected-Keyboard-Geometry.inc +@@ -0,0 +1,10 @@ ++#ifdef FLAG_SECTION ++ ++ {"android-protected-keyboard-geometry", ++ "Android Protected Keyboard Geometry", ++ "Protects Android on-screen keyboard geometry from being exposed to web " ++ "content. The per-site Viewport Protection setting is also respected.", ++ kOsAndroid, ++ FEATURE_VALUE_TYPE(blink::features::kAndroidProtectedKeyboardGeometry)}, ++ ++#endif +diff --git a/cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/protected_keyboard_geometry.inc b/cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/protected_keyboard_geometry.inc +new file mode 100644 +--- /dev/null ++++ b/cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/protected_keyboard_geometry.inc +@@ -0,0 +1,5 @@ ++#if BUILDFLAG(IS_ANDROID) ++ ++SET_CROMITE_FEATURE_ENABLED(kBrowserControlsEarlyResize); ++ ++#endif +diff --git a/cromite_flags/third_party/blink/common/features_cc/Android-Protected-Keyboard-Geometry.inc b/cromite_flags/third_party/blink/common/features_cc/Android-Protected-Keyboard-Geometry.inc +new file mode 100644 +--- /dev/null ++++ b/cromite_flags/third_party/blink/common/features_cc/Android-Protected-Keyboard-Geometry.inc +@@ -0,0 +1,3 @@ ++CROMITE_FEATURE(kAndroidProtectedKeyboardGeometry, ++ "AndroidProtectedKeyboardGeometry", ++ base::FEATURE_ENABLED_BY_DEFAULT); +diff --git a/cromite_flags/third_party/blink/common/features_h/Android-Protected-Keyboard-Geometry.inc b/cromite_flags/third_party/blink/common/features_h/Android-Protected-Keyboard-Geometry.inc +new file mode 100644 +--- /dev/null ++++ b/cromite_flags/third_party/blink/common/features_h/Android-Protected-Keyboard-Geometry.inc +@@ -0,0 +1,2 @@ ++// Conceals Android on-screen keyboard geometry from web content. ++BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kAndroidProtectedKeyboardGeometry); +diff --git a/third_party/blink/public/mojom/input/input_handler.mojom b/third_party/blink/public/mojom/input/input_handler.mojom +--- a/third_party/blink/public/mojom/input/input_handler.mojom ++++ b/third_party/blink/public/mojom/input/input_handler.mojom +@@ -422,6 +422,10 @@ interface FrameWidgetInputHandler { + // editable divs). + ScrollFocusedEditableNodeIntoView(); + ++ // Recomputes and sends the current focused element bounds to the browser. ++ // Used when browser UI resizes the renderer viewport without changing focus. ++ UpdateFocusedElementBounds(); ++ + // Replies when the next PageScaleAnimation has completed. Can only be called + // on the outermost main frame. Can be used to reliably wait for a + // ScrollFocusedEditableNodeIntoView to complete since that may create a +diff --git a/third_party/blink/renderer/core/frame/local_frame_mojo_handler.cc b/third_party/blink/renderer/core/frame/local_frame_mojo_handler.cc +--- a/third_party/blink/renderer/core/frame/local_frame_mojo_handler.cc ++++ b/third_party/blink/renderer/core/frame/local_frame_mojo_handler.cc +@@ -55,6 +55,7 @@ + #include "third_party/blink/renderer/core/frame/remote_frame_owner.h" + #include "third_party/blink/renderer/core/frame/reporting_context.h" + #include "third_party/blink/renderer/core/frame/savable_resources.h" ++#include "third_party/blink/renderer/core/frame/web_frame_widget_impl.h" + #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" + #include "third_party/blink/renderer/core/fullscreen/fullscreen.h" + #include "third_party/blink/renderer/core/html/html_element.h" +@@ -597,7 +598,11 @@ void LocalFrameMojoHandler::NotifyVirtualKeyboardOverlayRect( + keyboard_rect.width() / scale_factor, + keyboard_rect.height() / scale_factor); + +- frame_->NotifyVirtualKeyboardOverlayRectObservers(scaled_rect); ++ if (page->IsAndroidProtectedKeyboardGeometryEnabled(frame_)) { ++ frame_->NotifyVirtualKeyboardOverlayRectObservers(gfx::Rect()); ++ } else { ++ frame_->NotifyVirtualKeyboardOverlayRectObservers(scaled_rect); ++ } + } + + void LocalFrameMojoHandler::ShowInterestInElement(int nodeID) { +diff --git a/third_party/blink/renderer/core/frame/viewport_data.cc b/third_party/blink/renderer/core/frame/viewport_data.cc +--- a/third_party/blink/renderer/core/frame/viewport_data.cc ++++ b/third_party/blink/renderer/core/frame/viewport_data.cc +@@ -76,7 +76,7 @@ ViewportDescription ViewportData::GetViewportDescription() const { + + // Setting `navigator.virtualKeyboard.overlaysContent` should override the + // virtual-keyboard mode set from the viewport meta tag. +- if (virtual_keyboard_overlays_content_) { ++ if (GetVirtualKeyboardOverlaysContent()) { + applied_viewport_description.virtual_keyboard_mode = + ui::mojom::blink::VirtualKeyboardMode::kOverlaysContent; + } +@@ -84,6 +84,17 @@ ViewportDescription ViewportData::GetViewportDescription() const { + return applied_viewport_description; + } + ++bool ViewportData::GetVirtualKeyboardOverlaysContent() const { ++ // The bottom-controls policy keeps the author-controlled mode disabled. The ++ // protected keyboard policy is layered by GetVirtualKeyboardOverlaysContent(). ++ if (document_->GetFrame() && document_->GetPage() ++ && document_->GetPage()->IsAndroidProtectedKeyboardGeometryEnabled( ++ document_->GetFrame())) { ++ return true; ++ } ++ return virtual_keyboard_overlays_content_; ++} ++ + void ViewportData::UpdateViewportDescription() { + if (!document_->GetFrame()) + return; +diff --git a/third_party/blink/renderer/core/frame/viewport_data.h b/third_party/blink/renderer/core/frame/viewport_data.h +--- a/third_party/blink/renderer/core/frame/viewport_data.h ++++ b/third_party/blink/renderer/core/frame/viewport_data.h +@@ -47,9 +47,7 @@ class ViewportData final : public GarbageCollected { + } + + CORE_EXPORT void SetVirtualKeyboardOverlaysContent(bool overlays_content); +- CORE_EXPORT bool GetVirtualKeyboardOverlaysContent() const { +- return virtual_keyboard_overlays_content_; +- } ++ CORE_EXPORT bool GetVirtualKeyboardOverlaysContent() const; + + private: + Member document_; +diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc b/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc +--- a/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc ++++ b/third_party/blink/renderer/core/frame/web_frame_widget_impl.cc +@@ -1863,6 +1863,8 @@ void WebFrameWidgetImpl::UpdateVisualProperties( + widget_base_->VisibleViewportSize()](RemoteFrame* remote_frame) { + remote_frame->DidChangeVisibleViewportSize(visible_viewport_size); + }); ++ ++ UpdateFocusedElementBounds(); + } + + #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +@@ -4635,6 +4637,24 @@ void WebFrameWidgetImpl::ScrollFocusedEditableNodeIntoView() { + local_frame->ScrollFocusedEditableElementIntoView(); + } + ++void WebFrameWidgetImpl::UpdateFocusedElementBounds() { ++ Element* focused_element = FocusedElement(); ++ if (!focused_element) ++ return; ++ ++ EditContext* edit_context = ++ focused_element->GetDocument() ++ .GetFrame() ++ ->GetInputMethodController() ++ .GetActiveEditContext(); ++ ++ if (!WebElement(focused_element).IsEditable() && !edit_context) ++ return; ++ ++ focused_element->GetDocument().SendFocusNotification( ++ focused_element, mojom::blink::FocusType::kNone); ++} ++ + void WebFrameWidgetImpl::WaitForPageScaleAnimationForTesting( + WaitForPageScaleAnimationForTestingCallback callback) { + DCHECK(ForMainFrame()); +diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_impl.h b/third_party/blink/renderer/core/frame/web_frame_widget_impl.h +--- a/third_party/blink/renderer/core/frame/web_frame_widget_impl.h ++++ b/third_party/blink/renderer/core/frame/web_frame_widget_impl.h +@@ -934,6 +934,7 @@ class CORE_EXPORT WebFrameWidgetImpl + mojom::blink::SelectionMenuBehavior behavior) override; + void MoveRangeSelectionExtent(const gfx::Point& extent_in_dips) override; + void ScrollFocusedEditableNodeIntoView() override; ++ void UpdateFocusedElementBounds() override; + void WaitForPageScaleAnimationForTesting( + WaitForPageScaleAnimationForTestingCallback callback) override; + void MoveCaret(const gfx::Point& point_in_dips) override; +diff --git a/third_party/blink/renderer/core/page/page.cc b/third_party/blink/renderer/core/page/page.cc +--- a/third_party/blink/renderer/core/page/page.cc ++++ b/third_party/blink/renderer/core/page/page.cc +@@ -1289,6 +1289,31 @@ void Page::UpdateAcceleratedCompositingSettings() { + } + } + ++bool Page::IsAndroidProtectedKeyboardGeometryEnabled(LocalFrame* frame) const { ++#if !BUILDFLAG(IS_ANDROID) ++ return false; ++#else ++ if (!frame) return true; ++ if (!base::FeatureList::IsEnabled( ++ features::kAndroidProtectedKeyboardGeometry)) { ++ return false; ++ } ++ ++ WebContentSettingsClient* settings = frame->GetContentSettingsClient(); ++ if (!settings) return true; ++ if (!settings->AllowContentSetting( ++ ContentSettingsType::PROTECTED_KEYBOARD_GEOMETRY, ++ /*default_value=*/false)) { ++ return false; ++ } ++ ++ const String protocol = ++ frame->GetSecurityContext()->GetSecurityOrigin()->Protocol(); ++ return !SchemeRegistry::IsWebUIScheme(protocol) && ++ !CommonSchemeRegistry::IsExtensionScheme(protocol.Ascii()); ++#endif ++} ++ + void Page::CalculateEmulatedScreenSetting(LocalFrame* frame, bool force) { + bool isEnabled = base::FeatureList::IsEnabled(features::kViewportProtection); + blink::WebContentSettingsClient* settings = frame->GetContentSettingsClient(); +diff --git a/third_party/blink/renderer/core/page/page.h b/third_party/blink/renderer/core/page/page.h +--- a/third_party/blink/renderer/core/page/page.h ++++ b/third_party/blink/renderer/core/page/page.h +@@ -511,6 +511,8 @@ class CORE_EXPORT Page final : public GarbageCollected, + return *v8_compile_hints_consumer_; + } + ++ bool IsAndroidProtectedKeyboardGeometryEnabled(LocalFrame* frame) const; ++ + // Returns the token uniquely identifying the browsing context group this page + // lives in. + const base::UnguessableToken& BrowsingContextGroupToken(); +diff --git a/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.cc b/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.cc +--- a/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.cc ++++ b/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.cc +@@ -447,6 +447,15 @@ void FrameWidgetInputHandlerImpl::ScrollFocusedEditableNodeIntoView() { + main_thread_frame_widget_input_handler_)); + } + ++void FrameWidgetInputHandlerImpl::UpdateFocusedElementBounds() { ++ RunOnMainThread(base::BindOnce( ++ [](base::WeakPtr handler) { ++ if (handler) ++ handler->UpdateFocusedElementBounds(); ++ }, ++ main_thread_frame_widget_input_handler_)); ++} ++ + void FrameWidgetInputHandlerImpl::WaitForPageScaleAnimationForTesting( + WaitForPageScaleAnimationForTestingCallback callback) { + // Ensure the Mojo callback is invoked from the thread on which the message +diff --git a/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.h b/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.h +--- a/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.h ++++ b/third_party/blink/renderer/platform/widget/input/frame_widget_input_handler_impl.h +@@ -101,6 +101,7 @@ class PLATFORM_EXPORT FrameWidgetInputHandlerImpl + blink::mojom::SelectionMenuBehavior selection_menu_behavior) override; + void MoveRangeSelectionExtent(const gfx::Point& extent) override; + void ScrollFocusedEditableNodeIntoView() override; ++ void UpdateFocusedElementBounds() override; + void WaitForPageScaleAnimationForTesting( + WaitForPageScaleAnimationForTestingCallback callback) override; + void MoveCaret(const gfx::Point& point) override; +diff --git a/third_party/blink/renderer/platform/widget/input/input_handler_proxy.cc b/third_party/blink/renderer/platform/widget/input/input_handler_proxy.cc +--- a/third_party/blink/renderer/platform/widget/input/input_handler_proxy.cc ++++ b/third_party/blink/renderer/platform/widget/input/input_handler_proxy.cc +@@ -1199,6 +1199,13 @@ InputHandlerProxy::EventDisposition InputHandlerProxy::HandleGestureScrollBegin( + InputHandlerScrollEnd(std::nullopt); + } + ++ // These synthetic events have no physical pointers. Viewport protection is ++ // selected through content settings rather than kViewportProtection. ++ protected_viewport_scroll_sequence_ = ++ !gesture_event.data.scroll_begin.target_viewport && ++ gesture_event.data.scroll_begin.pointer_count == 0; ++ protected_viewport_accumulated_overscroll_ = gfx::Vector2dF(); ++ + cc::ScrollState scroll_state(CreateScrollStateDataForGesture(gesture_event)); + cc::InputHandler::ScrollStatus scroll_status; + if (gesture_event.data.scroll_begin.target_viewport) { +@@ -1359,6 +1366,8 @@ InputHandlerProxy::HandleGestureScrollUpdate( + InputHandlerProxy::EventDisposition InputHandlerProxy::HandleGestureScrollEnd( + const WebGestureEvent& gesture_event) { + TRACE_EVENT0("input", "InputHandlerProxy::HandleGestureScrollEnd"); ++ protected_viewport_scroll_sequence_ = false; ++ protected_viewport_accumulated_overscroll_ = gfx::Vector2dF(); + + const cc::ElementId latched_element_id = + input_handler_->LatchedScrollerElementId(); +@@ -1981,15 +1990,28 @@ void InputHandlerProxy::HandleOverscroll( + const cc::InputHandlerScrollResult& scroll_result, + const blink::WebGestureDevice source_device) { + DCHECK(client_); +- if (!scroll_result.did_overscroll_root) ++ const bool forward_protected_unused_delta = ++ protected_viewport_scroll_sequence_ && ++ !scroll_result.unused_scroll_delta.IsZero(); ++ if (!scroll_result.did_overscroll_root && ++ !forward_protected_unused_delta) { + return; ++ } + + TRACE_EVENT("input", "InputHandlerProxy::DidOverscroll"); + + // Bundle overscroll message with triggering event response, saving an IPC. + current_overscroll_params_ = std::make_unique(); +- current_overscroll_params_->accumulated_overscroll = +- scroll_result.accumulated_root_overscroll; ++ if (forward_protected_unused_delta && ++ !scroll_result.did_overscroll_root) { ++ protected_viewport_accumulated_overscroll_ += ++ scroll_result.unused_scroll_delta; ++ current_overscroll_params_->accumulated_overscroll = ++ protected_viewport_accumulated_overscroll_; ++ } else { ++ current_overscroll_params_->accumulated_overscroll = ++ scroll_result.accumulated_root_overscroll; ++ } + current_overscroll_params_->latest_overscroll_delta = + scroll_result.unused_scroll_delta; + current_overscroll_params_->causal_event_viewport_point = +diff --git a/third_party/blink/renderer/platform/widget/input/input_handler_proxy.h b/third_party/blink/renderer/platform/widget/input/input_handler_proxy.h +--- a/third_party/blink/renderer/platform/widget/input/input_handler_proxy.h ++++ b/third_party/blink/renderer/platform/widget/input/input_handler_proxy.h +@@ -420,6 +420,14 @@ class PLATFORM_EXPORT InputHandlerProxy : public cc::InputHandlerClient, + // bundled in the event ack, saving an IPC. + std::unique_ptr current_overscroll_params_; + ++ // True only for the compositor scroll sequence synthesized by the protected ++ // Android viewport after its automatic physical pan reaches its limit. ++ bool protected_viewport_scroll_sequence_ = false; ++ ++ // Cumulative unused delta for the protected sequence. Unlike root ++ // overscroll, cc does not populate accumulated_root_overscroll here. ++ gfx::Vector2dF protected_viewport_accumulated_overscroll_; ++ + std::unique_ptr compositor_event_queue_; + + // Set only when the compositor input handler is handling a gesture. Tells +diff --git a/ui/android/event_forwarder.cc b/ui/android/event_forwarder.cc +--- a/ui/android/event_forwarder.cc ++++ b/ui/android/event_forwarder.cc +@@ -352,6 +352,69 @@ void EventForwarder::ScrollBy(JNIEnv* env, float delta_x, float delta_y) { + view_->ScrollBy(delta_x, delta_y); + } + ++void EventForwarder::BeginCompositorScroll(JNIEnv* env, ++ int64_t time_ms, ++ float x, ++ float y, ++ float initial_delta_y) { ++ CancelFling(env, time_ms, /*prevent_boosting=*/true, ++ /*is_touchpad_event=*/false); ++ const float dip_scale = view_->GetDipScaleFactor(); ++ const gfx::PointF location(x / dip_scale, y / dip_scale); ++ ++ // Do not target the root viewport: cc must hit-test the gesture location and ++ // select the scroll node under it (for example a WebUI's inner scroller). ++ view_->OnGestureEvent(GestureEventAndroid( ++ GESTURE_EVENT_TYPE_SCROLL_START, location, location, time_ms, ++ ui::GestureDeviceType::DEVICE_TOUCHSCREEN, 0, 0, ++ -initial_delta_y / dip_scale, 0, 0, ++ /*target_viewport=*/false, ++ /*synthetic_scroll=*/false, ++ /*prevent_boosting=*/true)); ++} ++ ++void EventForwarder::UpdateCompositorScroll(JNIEnv* env, ++ int64_t time_ms, ++ float x, ++ float y, ++ float delta_y) { ++ const float dip_scale = view_->GetDipScaleFactor(); ++ const gfx::PointF location(x / dip_scale, y / dip_scale); ++ view_->OnGestureEvent(GestureEventAndroid( ++ GESTURE_EVENT_TYPE_SCROLL_BY, location, location, time_ms, ++ ui::GestureDeviceType::DEVICE_TOUCHSCREEN, 0, 0, -delta_y / dip_scale, ++ 0, 0, ++ /*target_viewport=*/false, ++ /*synthetic_scroll=*/false, ++ /*prevent_boosting=*/true)); ++} ++ ++void EventForwarder::EndCompositorScroll(JNIEnv* env, ++ int64_t time_ms, ++ float x, ++ float y, ++ float velocity_y) { ++ const float dip_scale = view_->GetDipScaleFactor(); ++ const gfx::PointF location(x / dip_scale, y / dip_scale); ++ if (velocity_y != 0) { ++ view_->OnGestureEvent(GestureEventAndroid( ++ GESTURE_EVENT_TYPE_FLING_START, location, location, time_ms, ++ ui::GestureDeviceType::DEVICE_TOUCHSCREEN, 0, 0, 0, 0, ++ velocity_y / dip_scale, ++ /*target_viewport=*/false, ++ /*synthetic_scroll=*/false, ++ /*prevent_boosting=*/true)); ++ return; ++ } ++ ++ view_->OnGestureEvent(GestureEventAndroid( ++ GESTURE_EVENT_TYPE_SCROLL_END, location, location, time_ms, ++ ui::GestureDeviceType::DEVICE_TOUCHSCREEN, 0, 0, 0, 0, 0, ++ /*target_viewport=*/false, ++ /*synthetic_scroll=*/false, ++ /*prevent_boosting=*/true)); ++} ++ + void EventForwarder::ScrollTo(JNIEnv* env, float x, float y) { + view_->ScrollTo(x, y); + } +diff --git a/ui/android/event_forwarder.h b/ui/android/event_forwarder.h +--- a/ui/android/event_forwarder.h ++++ b/ui/android/event_forwarder.h +@@ -101,6 +101,24 @@ class UI_ANDROID_EXPORT EventForwarder { + + void ScrollBy(JNIEnv* env, float delta_x, float delta_y); + ++ void BeginCompositorScroll(JNIEnv* env, ++ int64_t time_ms, ++ float x, ++ float y, ++ float initial_delta_y); ++ ++ void UpdateCompositorScroll(JNIEnv* env, ++ int64_t time_ms, ++ float x, ++ float y, ++ float delta_y); ++ ++ void EndCompositorScroll(JNIEnv* env, ++ int64_t time_ms, ++ float x, ++ float y, ++ float velocity_y); ++ + void ScrollTo(JNIEnv* env, float x, float y); + + void DoubleTap(JNIEnv* env, int64_t time_ms, int32_t x, int32_t y); +diff --git a/ui/android/java/src/org/chromium/ui/OverscrollRefreshHandler.java b/ui/android/java/src/org/chromium/ui/OverscrollRefreshHandler.java +--- a/ui/android/java/src/org/chromium/ui/OverscrollRefreshHandler.java ++++ b/ui/android/java/src/org/chromium/ui/OverscrollRefreshHandler.java +@@ -60,6 +60,22 @@ public interface OverscrollRefreshHandler { + @CalledByNative + void reset(); + ++ /** Starts returning unused compositor scrolling to the protected Android viewport. */ ++ @CalledByNative ++ boolean startProtectedViewportHandoff(); ++ ++ /** ++ * Updates the protected viewport using the cumulative unused vertical compositor delta. ++ * ++ * @param accumulatedDeltaY Cumulative unused vertical delta in device pixels. ++ */ ++ @CalledByNative ++ void updateProtectedViewportHandoff(float accumulatedDeltaY); ++ ++ /** Ends an active protected viewport handoff. */ ++ @CalledByNative ++ void endProtectedViewportHandoff(); ++ + /** + * Toggle whether the effect is active. + * +diff --git a/ui/android/java/src/org/chromium/ui/base/EventForwarder.java b/ui/android/java/src/org/chromium/ui/base/EventForwarder.java +--- a/ui/android/java/src/org/chromium/ui/base/EventForwarder.java ++++ b/ui/android/java/src/org/chromium/ui/base/EventForwarder.java +@@ -879,6 +879,37 @@ public class EventForwarder { + EventForwarderJni.get().scrollBy(mNativeEventForwarder, dxPix, dyPix); + } + ++ /** ++ * Starts a compositor scroll at the supplied point. Unlike {@link #scrollBy}, this performs ++ * normal compositor hit testing instead of forcing the root viewport. ++ */ ++ public void beginCompositorScroll( ++ long timeMs, float xPix, float yPix, float initialDeltaYPix) { ++ if (mNativeEventForwarder == 0) return; ++ EventForwarderJni.get() ++ .beginCompositorScroll( ++ mNativeEventForwarder, timeMs, xPix, yPix, initialDeltaYPix); ++ } ++ ++ /** Updates the compositor scroll sequence started by {@link #beginCompositorScroll}. */ ++ public void updateCompositorScroll( ++ long timeMs, float xPix, float yPix, float deltaYPix) { ++ if (mNativeEventForwarder == 0) return; ++ EventForwarderJni.get() ++ .updateCompositorScroll(mNativeEventForwarder, timeMs, xPix, yPix, deltaYPix); ++ } ++ ++ /** ++ * Finishes a compositor scroll sequence, optionally continuing it as a fling when velocity is ++ * non-zero. ++ */ ++ public void endCompositorScroll( ++ long timeMs, float xPix, float yPix, float velocityYPix) { ++ if (mNativeEventForwarder == 0) return; ++ EventForwarderJni.get() ++ .endCompositorScroll(mNativeEventForwarder, timeMs, xPix, yPix, velocityYPix); ++ } ++ + /** + * @see View#scrollTo() + */ +@@ -1009,6 +1040,23 @@ public class EventForwarder { + + void scrollBy(long nativeEventForwarder, float deltaX, float deltaY); + ++ void beginCompositorScroll( ++ long nativeEventForwarder, ++ long timeMs, ++ float x, ++ float y, ++ float initialDeltaY); ++ ++ void updateCompositorScroll( ++ long nativeEventForwarder, long timeMs, float x, float y, float deltaY); ++ ++ void endCompositorScroll( ++ long nativeEventForwarder, ++ long timeMs, ++ float x, ++ float y, ++ float velocityY); ++ + void scrollTo(long nativeEventForwarder, float x, float y); + + void doubleTap(long nativeEventForwarder, long timeMs, int x, int y); +diff --git a/ui/android/overscroll_refresh.cc b/ui/android/overscroll_refresh.cc +--- a/ui/android/overscroll_refresh.cc ++++ b/ui/android/overscroll_refresh.cc +@@ -207,6 +207,21 @@ void OverscrollRefresh::SetTouchpadOverscrollHistoryNavigation(bool enabled) { + touchpad_overscroll_history_navigation_enabled_ = enabled; + } + ++bool OverscrollRefresh::StartProtectedViewportHandoff() { ++ return handler_ && handler_->StartProtectedViewportHandoff(); ++} ++ ++void OverscrollRefresh::UpdateProtectedViewportHandoff( ++ float accumulated_delta_y) { ++ CHECK(handler_); ++ handler_->UpdateProtectedViewportHandoff(accumulated_delta_y); ++} ++ ++void OverscrollRefresh::EndProtectedViewportHandoff() { ++ CHECK(handler_); ++ handler_->EndProtectedViewportHandoff(); ++} ++ + void OverscrollRefresh::Release(bool allow_refresh) { + if (scroll_consumption_state_ == ScrollConsumptionState::kEnabled) + handler_->PullRelease(allow_refresh); +diff --git a/ui/android/overscroll_refresh.h b/ui/android/overscroll_refresh.h +--- a/ui/android/overscroll_refresh.h ++++ b/ui/android/overscroll_refresh.h +@@ -100,6 +100,11 @@ class UI_ANDROID_EXPORT OverscrollRefresh { + + void SetTouchpadOverscrollHistoryNavigation(bool enabled); + ++ // Returns unused compositor scrolling to the protected Android viewport. ++ virtual bool StartProtectedViewportHandoff(); ++ virtual void UpdateProtectedViewportHandoff(float accumulated_delta_y); ++ virtual void EndProtectedViewportHandoff(); ++ + protected: + // This constructor is for mocking only. + OverscrollRefresh(); +diff --git a/ui/android/overscroll_refresh_handler.cc b/ui/android/overscroll_refresh_handler.cc +--- a/ui/android/overscroll_refresh_handler.cc ++++ b/ui/android/overscroll_refresh_handler.cc +@@ -66,6 +66,30 @@ void OverscrollRefreshHandler::PullReset() { + Java_OverscrollRefreshHandler_reset(env, GetRefreshHandlerChecked(env)); + } + ++bool OverscrollRefreshHandler::StartProtectedViewportHandoff() { ++ if (!has_handler_) { ++ return false; ++ } ++ auto* env = AttachCurrentThread(); ++ return Java_OverscrollRefreshHandler_startProtectedViewportHandoff( ++ env, GetRefreshHandlerChecked(env)); ++} ++ ++void OverscrollRefreshHandler::UpdateProtectedViewportHandoff( ++ float accumulated_delta_y) { ++ CHECK(has_handler_); ++ auto* env = AttachCurrentThread(); ++ Java_OverscrollRefreshHandler_updateProtectedViewportHandoff( ++ env, GetRefreshHandlerChecked(env), accumulated_delta_y); ++} ++ ++void OverscrollRefreshHandler::EndProtectedViewportHandoff() { ++ CHECK(has_handler_); ++ auto* env = AttachCurrentThread(); ++ Java_OverscrollRefreshHandler_endProtectedViewportHandoff( ++ env, GetRefreshHandlerChecked(env)); ++} ++ + ScopedJavaLocalRef OverscrollRefreshHandler::GetRefreshHandlerChecked( + JNIEnv* env) const { + auto refresh_handler = Java_OverscrollRefreshHandler_getRef( +diff --git a/ui/android/overscroll_refresh_handler.h b/ui/android/overscroll_refresh_handler.h +--- a/ui/android/overscroll_refresh_handler.h ++++ b/ui/android/overscroll_refresh_handler.h +@@ -43,6 +43,11 @@ class UI_ANDROID_EXPORT OverscrollRefreshHandler { + // Reset the active pull state. + virtual void PullReset(); + ++ // Returns unused compositor scrolling to the protected Android viewport. ++ virtual bool StartProtectedViewportHandoff(); ++ virtual void UpdateProtectedViewportHandoff(float accumulated_delta_y); ++ virtual void EndProtectedViewportHandoff(); ++ + private: + base::android::ScopedJavaLocalRef GetRefreshHandlerChecked( + JNIEnv* env) const; +--