From 47521cfbf52dfb785d3be703cf45789373e4cda6 Mon Sep 17 00:00:00 2001 From: Serg Date: Fri, 27 Mar 2026 16:57:06 -0400 Subject: [PATCH] Android new tab at the bottom (#35051) * [Android] Move tab switcher controls to bottom when address bar is at bottom When the address bar is configured to show at the bottom, reposition the entire hub toolbar (new tab, menu, pane switcher, search, shred) from the top to the bottom of the tab switcher for one-handed accessibility. - BraveHubManagerImpl: Add maybeRepositionToolbarToBottom() that changes the toolbar wrapper's FrameLayout gravity to BOTTOM and swaps the pane host container's top margin to bottom margin. Uses EdgeToEdge pad adjuster (same pattern as upstream HubBottomToolbarCoordinator) to handle navigation bar insets. Properly cleans up the adjuster on hub hide since the coordinator and views are destroyed between show/hide cycles. - BraveHubManagerImplClassAdapter: Expose parent's mEdgeToEdgeSupplier field via deleteField/makeProtectedField bytecode pattern. - BraveToolbarManager: Remove setLayoutStateProvider() and its LayoutStateObserver that toggled menu popup direction on tab switcher transitions. No longer needed since the hub toolbar is now at the bottom when address bar is at bottom. - BraveHubToolbarView: Change INVISIBLE to GONE for hidden action/menu buttons when bottom navigation controls are enabled (address bar on top). INVISIBLE left the buttons clickable despite being hidden. Resolves: https://github.com/brave/brave-browser/issues/53326 --- android/java/apk_for_test.flags | 1 + .../browser/toolbar/BraveToolbarManager.java | 34 +----- .../chromium/chrome/browser/BytecodeTest.java | 3 + .../internal/android/java/java_sources.gni | 7 +- .../browser/hub/BraveHubManagerImpl.java | 109 +++++++++++++++++- .../browser/hub/BraveHubToolbarView.java | 10 +- .../BraveHubManagerImplClassAdapter.java | 3 + 7 files changed, 124 insertions(+), 43 deletions(-) diff --git a/android/java/apk_for_test.flags b/android/java/apk_for_test.flags index f633b59aed3..e919e3b1905 100644 --- a/android/java/apk_for_test.flags +++ b/android/java/apk_for_test.flags @@ -908,6 +908,7 @@ -keep class org.chromium.chrome.browser.hub.HubManagerImpl { *** ensureHubCoordinatorIsInitialized(...); + *** mEdgeToEdgeSupplier; } -keep class org.chromium.chrome.browser.ui.system.StatusBarColorController { diff --git a/android/java/org/chromium/chrome/browser/toolbar/BraveToolbarManager.java b/android/java/org/chromium/chrome/browser/toolbar/BraveToolbarManager.java index ff10835dcf5..3b74feb262a 100644 --- a/android/java/org/chromium/chrome/browser/toolbar/BraveToolbarManager.java +++ b/android/java/org/chromium/chrome/browser/toolbar/BraveToolbarManager.java @@ -47,7 +47,6 @@ import org.chromium.chrome.browser.findinpage.FindToolbarManager; import org.chromium.chrome.browser.fullscreen.FullscreenManager; import org.chromium.chrome.browser.homepage.HomepageManager; import org.chromium.chrome.browser.layouts.LayoutStateProvider; -import org.chromium.chrome.browser.layouts.LayoutType; import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher; import org.chromium.chrome.browser.merchant_viewer.MerchantTrustSignalsCoordinator; import org.chromium.chrome.browser.multiwindow.MultiInstanceManager; @@ -140,7 +139,7 @@ public class BraveToolbarManager extends ToolbarManager private TabCreatorManager mTabCreatorManager; private NonNullObservableSupplier mModalDialogManagerSupplier; private TabObscuringHandler mTabObscuringHandler; - private LayoutStateProvider mLayoutStateProvider; + private MonotonicObservableSupplier mReadAloudControllerSupplier; private TopUiThemeColorProvider mTopUiThemeColorProvider; private int mCurrentOrientation; @@ -164,7 +163,6 @@ public class BraveToolbarManager extends ToolbarManager private final BrowserControlsSizer mBrowserControlsSizer; private final DataSharingTabManager mDataSharingTabManager; private final MonotonicObservableSupplier mTabModelSelectorSupplier; - private LayoutStateProvider.LayoutStateObserver mLayoutStateObserver; private Runnable mOpenGridTabSwitcherHandler; private final MonotonicObservableSupplier mTabBookmarkerSupplier; private final Supplier mShareDelegateSupplier; @@ -293,8 +291,6 @@ public class BraveToolbarManager extends ToolbarManager if (isToolbarPhone()) { updateBraveBottomControlsVisibility(); - mLayoutStateProviderSupplier.onAvailable( - mCallbackController.makeCancelable(this::setLayoutStateProvider)); } mBraveHomepageStateListener = @@ -506,10 +502,6 @@ public class BraveToolbarManager extends ToolbarManager @Override public void destroy() { - if (mLayoutStateProvider != null && mLayoutStateObserver != null) { - mLayoutStateProvider.removeObserver(mLayoutStateObserver); - mLayoutStateObserver = null; - } super.destroy(); HomepageManager.getInstance().removeListener(mBraveHomepageStateListener); } @@ -628,30 +620,6 @@ public class BraveToolbarManager extends ToolbarManager } } - private void setLayoutStateProvider(LayoutStateProvider layoutStateProvider) { - assert mLayoutStateObserver == null : "mLayoutStateObserver should be set only once"; - - mLayoutStateObserver = - new LayoutStateProvider.LayoutStateObserver() { - @Override - public void onStartedShowing(@LayoutType int layoutType) { - if (layoutType == LayoutType.TAB_SWITCHER - && BottomToolbarConfiguration.isToolbarBottomAnchored()) { - BraveMenuButtonCoordinator.setMenuFromBottom(false); - } - } - - @Override - public void onStartedHiding(@LayoutType int layoutType) { - if (layoutType == LayoutType.TAB_SWITCHER - && BottomToolbarConfiguration.isToolbarBottomAnchored()) { - BraveMenuButtonCoordinator.setMenuFromBottom(true); - } - } - }; - layoutStateProvider.addObserver(mLayoutStateObserver); - } - public void openHomepage() { if (mToolbarTabController == null) return; diff --git a/android/javatests/org/chromium/chrome/browser/BytecodeTest.java b/android/javatests/org/chromium/chrome/browser/BytecodeTest.java index c1dd6ecb232..5dbf2dfad55 100644 --- a/android/javatests/org/chromium/chrome/browser/BytecodeTest.java +++ b/android/javatests/org/chromium/chrome/browser/BytecodeTest.java @@ -2705,6 +2705,9 @@ public class BytecodeTest { fieldExists( "org/chromium/chrome/browser/infobar/InfoBarContainerView", "mEdgeToEdgeSupplier")); + Assert.assertTrue( + fieldExists( + "org/chromium/chrome/browser/hub/HubManagerImpl", "mEdgeToEdgeSupplier")); Assert.assertTrue( fieldExists( "org/chromium/chrome/browser/password_manager/PasswordManagerHelper", diff --git a/browser/hub/internal/android/java/java_sources.gni b/browser/hub/internal/android/java/java_sources.gni index d9fd3e4fa46..ee8e8722c8d 100644 --- a/browser/hub/internal/android/java/java_sources.gni +++ b/browser/hub/internal/android/java/java_sources.gni @@ -10,5 +10,8 @@ brave_browser_hub_internal_java_sources = [ "//brave/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubToolbarView.java", ] -brave_browser_hub_internal_java_deps = - [ "//brave/browser/brave_shields/android:java" ] +brave_browser_hub_internal_java_deps = [ + "//base:base_shared_preferences_java", + "//brave/browser/brave_shields/android:java", + "//chrome/browser/preferences:java", +] diff --git a/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubManagerImpl.java b/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubManagerImpl.java index 08e4cd340e8..7356934ee46 100644 --- a/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubManagerImpl.java +++ b/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubManagerImpl.java @@ -8,24 +8,29 @@ package org.chromium.chrome.browser.hub; import android.app.Activity; import android.content.ComponentCallbacks; import android.content.res.Configuration; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import org.chromium.base.BravePreferenceKeys; import org.chromium.base.BraveReflectionUtil; -import org.chromium.base.ContextUtils; import org.chromium.base.supplier.MonotonicObservableSupplier; import org.chromium.base.supplier.NonNullObservableSupplier; import org.chromium.base.supplier.OneshotSupplier; import org.chromium.chrome.browser.back_press.BackPressManager; +import org.chromium.chrome.browser.preferences.ChromeSharedPreferences; import org.chromium.chrome.browser.profiles.ProfileProvider; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.toolbar.menu_button.MenuButtonCoordinator; import org.chromium.chrome.browser.toolbar.settings.AddressBarPreference; import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeController; +import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeControllerFactory; import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager; import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityClient; import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController; import org.chromium.ui.base.DeviceFormFactor; +import org.chromium.ui.edge_to_edge.EdgeToEdgePadAdjuster; /** * Brave's extension of {@link HubManagerImpl}. We need it to adjust bottom margin for the bottom @@ -36,6 +41,9 @@ public class BraveHubManagerImpl extends HubManagerImpl { private int mBottomToolbarHeight; private final boolean mIsTablet; private final ComponentCallbacks mComponentCallbacks; + // This field is deleted by BraveHubManagerImplClassAdapter so the parent's field is used. + private MonotonicObservableSupplier mEdgeToEdgeSupplier; + private EdgeToEdgePadAdjuster mEdgeToEdgePadAdjuster; public BraveHubManagerImpl( Activity activity, @@ -106,6 +114,18 @@ public class BraveHubManagerImpl extends HubManagerImpl { super.onHubLayoutShow(); maybeUpdateBottomMarginForContainerView(); + maybeRepositionToolbarToBottom(); + } + + @Override + public void onHubLayoutDoneHiding() { + // The parent destroys the HubCoordinator (and all views) here, so the adjuster's + // view reference becomes stale. Reset it so it's recreated on the next show. + if (mEdgeToEdgePadAdjuster != null) { + mEdgeToEdgePadAdjuster.destroy(); + mEdgeToEdgePadAdjuster = null; + } + super.onHubLayoutDoneHiding(); } @Override @@ -115,6 +135,10 @@ public class BraveHubManagerImpl extends HubManagerImpl { if (mActivity != null) { mActivity.unregisterComponentCallbacks(mComponentCallbacks); } + if (mEdgeToEdgePadAdjuster != null) { + mEdgeToEdgePadAdjuster.destroy(); + mEdgeToEdgePadAdjuster = null; + } } private void maybeUpdateBottomMarginForContainerView() { @@ -147,8 +171,8 @@ public class BraveHubManagerImpl extends HubManagerImpl { HubContainerView containerView = getContainerView(); LayoutParams params = (LayoutParams) containerView.getLayoutParams(); params.bottomMargin = - ContextUtils.getAppSharedPreferences() - .getBoolean( + ChromeSharedPreferences.getInstance() + .readBoolean( BravePreferenceKeys .BRAVE_IS_MENU_FROM_BOTTOM, true) @@ -158,6 +182,85 @@ public class BraveHubManagerImpl extends HubManagerImpl { }); } + /** + * Repositions the hub toolbar from top to bottom when the address bar is at the bottom. This + * moves all hub controls (new tab, menu, pane switcher, search) to the bottom of the tab + * switcher for one-handed accessibility. + */ + private void maybeRepositionToolbarToBottom() { + if (mIsTablet || !isToolbarBottomAnchored()) return; + + HubContainerView containerView = getContainerView(); + if (containerView == null) return; + + View hubToolbar = containerView.findViewById(R.id.hub_toolbar); + if (hubToolbar == null) return; + + // Move the toolbar layout wrapper to the bottom of its FrameLayout parent. + View toolbarWrapper = (View) hubToolbar.getParent(); + if (toolbarWrapper != null + && toolbarWrapper.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams params = + (FrameLayout.LayoutParams) toolbarWrapper.getLayoutParams(); + params.gravity = Gravity.BOTTOM; + toolbarWrapper.setLayoutParams(params); + } + + // Swap the pane host container margins: top margin → bottom margin so the content + // area leaves space for the toolbar at the bottom instead of the top. + View hostContainer = containerView.findViewById(R.id.hub_pane_host_container); + if (hostContainer != null + && hostContainer.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams params = + (FrameLayout.LayoutParams) hostContainer.getLayoutParams(); + if (params.topMargin > 0 && params.bottomMargin == 0) { + int toolbarHeight = params.topMargin; + params.topMargin = 0; + params.bottomMargin = toolbarHeight; + hostContainer.setLayoutParams(params); + } + } + + // Apply bottom padding for the system navigation bar so the toolbar content + // does not sit behind the gesture bar, matching the address bar behavior. + applyNavigationBarPadding(hubToolbar, hostContainer); + } + + /** + * Applies bottom padding to the toolbar for the system navigation bar inset using the + * edge-to-edge controller (same pattern as upstream HubBottomToolbarCoordinator), and adjusts + * the pane host container's bottom margin when the toolbar wrapper's layout changes. + */ + private void applyNavigationBarPadding(View hubToolbar, View hostContainer) { + // Guard against multiple calls (onHubLayoutShow fires each time hub is shown). + if (mEdgeToEdgePadAdjuster != null) return; + + View toolbarWrapper = (View) hubToolbar.getParent(); + if (toolbarWrapper == null) return; + + // Use the EdgeToEdge factory to apply bottom padding for the navigation bar inset. + mEdgeToEdgePadAdjuster = + EdgeToEdgeControllerFactory.createForViewAndObserveSupplier( + toolbarWrapper, mEdgeToEdgeSupplier); + + // When the toolbar wrapper's height changes (due to edge-to-edge padding), update + // the pane host container's bottom margin to prevent content overlap. + toolbarWrapper.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + int newHeight = bottom - top; + if (hostContainer != null + && hostContainer.getLayoutParams() + instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams hostParams = + (FrameLayout.LayoutParams) hostContainer.getLayoutParams(); + if (hostParams.bottomMargin != newHeight) { + hostParams.bottomMargin = newHeight; + hostContainer.setLayoutParams(hostParams); + } + } + }); + } + private boolean isToolbarBottomAnchored() { return !AddressBarPreference.isToolbarConfiguredToShowOnTop(); } diff --git a/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubToolbarView.java b/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubToolbarView.java index 0786a9e140d..c99163e9810 100644 --- a/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubToolbarView.java +++ b/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/BraveHubToolbarView.java @@ -15,9 +15,9 @@ import android.widget.FrameLayout; import org.chromium.base.BraveFeatureList; import org.chromium.base.BravePreferenceKeys; -import org.chromium.base.ContextUtils; import org.chromium.chrome.browser.brave_shields.FirstPartyStorageCleanerInterface; import org.chromium.chrome.browser.flags.ChromeFeatureList; +import org.chromium.chrome.browser.preferences.ChromeSharedPreferences; import org.chromium.chrome.browser.toolbar.settings.AddressBarPreference; /** @@ -82,11 +82,11 @@ public class BraveHubToolbarView extends HubToolbarView { private void updateButtonsVisibility() { boolean shouldHideButtons = AddressBarPreference.isToolbarConfiguredToShowOnTop() - && ContextUtils.getAppSharedPreferences() - .getBoolean(BravePreferenceKeys.BRAVE_IS_MENU_FROM_BOTTOM, true); + && ChromeSharedPreferences.getInstance() + .readBoolean(BravePreferenceKeys.BRAVE_IS_MENU_FROM_BOTTOM, true); - mActionButton.setVisibility(shouldHideButtons ? View.INVISIBLE : View.VISIBLE); - mMenuButton.setVisibility(shouldHideButtons ? View.INVISIBLE : View.VISIBLE); + mActionButton.setVisibility(shouldHideButtons ? View.GONE : View.VISIBLE); + mMenuButton.setVisibility(shouldHideButtons ? View.GONE : View.VISIBLE); final boolean shouldShowShredButton = ChromeFeatureList.isEnabled(BraveFeatureList.BRAVE_SHRED); diff --git a/build/android/bytecode/java/org/brave/bytecode/BraveHubManagerImplClassAdapter.java b/build/android/bytecode/java/org/brave/bytecode/BraveHubManagerImplClassAdapter.java index 5c75a4dc0af..f64c9c1f5ed 100644 --- a/build/android/bytecode/java/org/brave/bytecode/BraveHubManagerImplClassAdapter.java +++ b/build/android/bytecode/java/org/brave/bytecode/BraveHubManagerImplClassAdapter.java @@ -16,5 +16,8 @@ public class BraveHubManagerImplClassAdapter extends BraveClassVisitor { super(visitor); redirectConstructor(sHubManagerImplClassName, sBraveHubManagerImplClassName); + + deleteField(sBraveHubManagerImplClassName, "mEdgeToEdgeSupplier"); + makeProtectedField(sHubManagerImplClassName, "mEdgeToEdgeSupplier"); } }