[clank-q4-fixit] ContextMenuManager use ListMenu

Use ListMenu to show the context menu for suggestion tiles. After this
change:

* The background of the menu can pickup dynamic colors expectedly.
* The anchor of the menu will always be the center of the tile view. Previously, the menu anchor will be the touch point of the long press.

Before: https://screenshot.googleplex.com/45whEEZzncZvjKh
After: https://screenshot.googleplex.com/7csmBSr4H5NUGLb

Bug: 339864862
Change-Id: I84e8e886598ad1f11d127713a0c1bc1b88f913ba
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6120095
Auto-Submit: Wenyu Fu <wenyufu@chromium.org>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Lijin Shen <lazzzis@google.com>
Commit-Queue: Lijin Shen <lazzzis@google.com>
Cr-Commit-Position: refs/heads/main@{#1400586}
diff --git a/chrome/android/chrome_junit_test_java_sources.gni b/chrome/android/chrome_junit_test_java_sources.gni
index 834eb48..aecb2ee 100644
--- a/chrome/android/chrome_junit_test_java_sources.gni
+++ b/chrome/android/chrome_junit_test_java_sources.gni
@@ -281,6 +281,7 @@
   "junit/src/org/chromium/chrome/browser/multiwindow/MultiInstanceManagerApi31UnitTest.java",
   "junit/src/org/chromium/chrome/browser/multiwindow/MultiWindowTestUtils.java",
   "junit/src/org/chromium/chrome/browser/multiwindow/MultiWindowUtilsUnitTest.java",
+  "junit/src/org/chromium/chrome/browser/native_page/ContextMenuManagerUnitTest.java",
   "junit/src/org/chromium/chrome/browser/native_page/NativePageFactoryTest.java",
   "junit/src/org/chromium/chrome/browser/new_tab_url/DseNewTabUrlManagerUnitTest.java",
   "junit/src/org/chromium/chrome/browser/night_mode/GlobalNightModeStateControllerTest.java",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java b/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java
index de6eebf..a016e99d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/native_page/ContextMenuManager.java
@@ -4,7 +4,6 @@
 
 package org.chromium.chrome.browser.native_page;
 
-import android.content.Context;
 import android.view.ContextMenu;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -12,14 +11,23 @@
 import android.view.View;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 
 import org.chromium.base.metrics.RecordUserAction;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
 import org.chromium.chrome.browser.ui.native_page.TouchEnabledDelegate;
+import org.chromium.components.browser_ui.widget.BrowserUiListMenuUtils;
 import org.chromium.ui.base.WindowAndroid.OnCloseContextMenuListener;
+import org.chromium.ui.listmenu.ListMenu;
+import org.chromium.ui.listmenu.ListMenuButtonDelegate;
+import org.chromium.ui.listmenu.ListMenuHost;
+import org.chromium.ui.listmenu.ListMenuItemProperties;
+import org.chromium.ui.modelutil.MVCListAdapter;
 import org.chromium.ui.mojom.WindowOpenDisposition;
+import org.chromium.ui.widget.RectProvider;
+import org.chromium.ui.widget.ViewRectProvider;
 import org.chromium.url.GURL;
 
 import java.lang.annotation.Retention;
@@ -66,6 +74,9 @@
     private final String mUserActionPrefix;
     private View mAnchorView;
 
+    // Not null after showListContextMenu.
+    private @Nullable ListMenuHost mListContextMenu;
+
     /** Defines callback to configure the context menu and respond to user interaction. */
     public interface Delegate {
         /** Opens the current item the way specified by {@code windowDisposition}. */
@@ -163,11 +174,7 @@
         for (@ContextMenuItemId int itemId = 0; itemId < ContextMenuItemId.NUM_ENTRIES; itemId++) {
             if (!shouldShowItem(itemId, delegate)) continue;
 
-            menu.add(
-                            Menu.NONE,
-                            itemId,
-                            Menu.NONE,
-                            getResourceIdForMenuItem(associatedView.getContext(), itemId))
+            menu.add(Menu.NONE, itemId, Menu.NONE, getResourceIdForMenuItem(itemId))
                     .setOnMenuItemClickListener(listener);
             hasItems = true;
         }
@@ -198,6 +205,79 @@
         notifyContextMenuShown(delegate);
     }
 
+    /**
+     * Show the context menu using a {@link ListMenu}.
+     *
+     * @param associatedView The tile view associated with context menu.
+     * @param delegate Delegate that defines the configuration of the menu and what to do when items
+     *     are tapped.
+     * @return Whether menu is shown.
+     */
+    public boolean showListContextMenu(View associatedView, Delegate delegate) {
+        MVCListAdapter.ModelList menuModel = new MVCListAdapter.ModelList();
+        for (@ContextMenuItemId int itemId = 0; itemId < ContextMenuItemId.NUM_ENTRIES; itemId++) {
+            if (!shouldShowItem(itemId, delegate)) continue;
+
+            menuModel.add(
+                    BrowserUiListMenuUtils.buildMenuListItem(
+                            /* titleId= */ getResourceIdForMenuItem(itemId),
+                            /* menuId= */ itemId,
+                            /* startIconId= */ 0));
+        }
+
+        if (menuModel.isEmpty()) {
+            return false;
+        }
+
+        // Now show the anchored popup window representing the list menu.
+        mTouchEnabledDelegate.setTouchEnabled(false);
+        mAnchorView = associatedView;
+
+        ListMenu menu =
+                BrowserUiListMenuUtils.getBasicListMenu(
+                        mAnchorView.getContext(),
+                        menuModel,
+                        model ->
+                                handleMenuItemClick(
+                                        model.get(ListMenuItemProperties.MENU_ITEM_ID), delegate));
+        mListContextMenu = new ListMenuHost(mAnchorView, null);
+        mListContextMenu.setMenuMaxWidth(
+                mAnchorView.getResources().getDimensionPixelSize(R.dimen.menu_width));
+        mListContextMenu.tryToFitLargestItem(true);
+        mListContextMenu.setDelegate(
+                new ListMenuButtonDelegate() {
+                    @Override
+                    public ListMenu getListMenu() {
+                        return menu;
+                    }
+
+                    @Override
+                    public RectProvider getRectProvider(View view) {
+                        ViewRectProvider rectProvider = new ViewRectProvider(view);
+                        rectProvider.setUseCenter(true);
+                        return rectProvider;
+                    }
+                },
+                /* overrideOnClickListener= */ false);
+        mListContextMenu.addPopupListener(
+                new ListMenuHost.PopupMenuShownListener() {
+                    @Override
+                    public void onPopupMenuShown() {}
+
+                    @Override
+                    public void onPopupMenuDismissed() {
+                        mAnchorView = null;
+                        mListContextMenu = null;
+                        mTouchEnabledDelegate.setTouchEnabled(true);
+                        mCloseContextMenuCallback.run();
+                    }
+                });
+        mListContextMenu.showMenu();
+        notifyContextMenuShown(delegate);
+
+        return true;
+    }
+
     @Override
     public void onContextMenuClosed() {
         if (mAnchorView == null) return;
@@ -257,9 +337,8 @@
 
     /**
      * Returns resource id of a string that should be displayed for menu item with given item id.
-     * @param context The activity context.
      */
-    protected @StringRes int getResourceIdForMenuItem(Context context, @ContextMenuItemId int id) {
+    protected @StringRes int getResourceIdForMenuItem(@ContextMenuItemId int id) {
         switch (id) {
             case ContextMenuItemId.OPEN_IN_NEW_TAB:
                 return R.string.contextmenu_open_in_new_tab;
@@ -315,6 +394,10 @@
         }
     }
 
+    public ListMenuHost getListMenuForTesting() {
+        return mListContextMenu;
+    }
+
     private class ItemClickListener implements OnMenuItemClickListener {
         private final Delegate mDelegate;
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java
index cb3b216f7..bb02dfb 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileGroup.java
@@ -128,10 +128,12 @@
     }
 
     /** Delegate for handling interactions with tiles. */
-    public interface TileInteractionDelegate extends OnClickListener, OnCreateContextMenuListener {
+    public interface TileInteractionDelegate
+            extends OnClickListener, OnCreateContextMenuListener, View.OnLongClickListener {
         /**
          * Set a runnable for click events on the tile. This is primarily used to track interaction
          * with the tile used by feature engagement purposes.
+         *
          * @param clickRunnable The {@link Runnable} to be executed when tile is clicked.
          */
         void setOnClickRunnable(Runnable clickRunnable);
@@ -700,10 +702,20 @@
         @Override
         public void onCreateContextMenu(
                 ContextMenu contextMenu, View view, ContextMenuInfo contextMenuInfo) {
+            if (ChromeFeatureList.isEnabled(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)) return;
+
             mContextMenuManager.createContextMenu(contextMenu, view, this);
         }
 
         @Override
+        public boolean onLongClick(View view) {
+            if (!ChromeFeatureList.isEnabled(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)) {
+                return false;
+            }
+            return mContextMenuManager.showListContextMenu(view, this);
+        }
+
+        @Override
         public void setOnClickRunnable(Runnable clickRunnable) {
             mOnClickRunnable = clickRunnable;
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileRenderer.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileRenderer.java
index 106ae0a..af879c1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileRenderer.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/tile/TileRenderer.java
@@ -24,6 +24,7 @@
 import org.chromium.base.task.TaskTraits;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.omnibox.suggestions.mostvisited.SuggestTileType;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.suggestions.ImageFetcher;
@@ -241,7 +242,11 @@
         }
 
         tileView.setOnClickListener(delegate);
-        tileView.setOnCreateContextMenuListener(delegate);
+        if (ChromeFeatureList.isEnabled(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)) {
+            tileView.setOnLongClickListener(delegate);
+        } else {
+            tileView.setOnCreateContextMenuListener(delegate);
+        }
 
         return tileView;
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/NewTabPageTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/NewTabPageTest.java
index ab7749e..2a23ca19 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/NewTabPageTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/NewTabPageTest.java
@@ -329,6 +329,7 @@
     @Test
     @SmallTest
     @Feature({"NewTabPage", "FeedNewTabPage"})
+    @DisableFeatures(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)
     public void testOpenMostVisitedItemInIncognitoTab() throws ExecutionException {
         Assert.assertNotNull(mMvTilesLayout);
         HistogramWatcher histogramWatcher = expectMostVisitedTilesRecordForNtpModuleClick();
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGroupTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGroupTest.java
index 81dcc7537..e30b968 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGroupTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGroupTest.java
@@ -28,8 +28,10 @@
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.Features;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.browser.app.ChromeActivity;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.native_page.ContextMenuManager;
 import org.chromium.chrome.browser.ntp.NewTabPage;
@@ -109,6 +111,8 @@
     @MediumTest
     @Feature({"NewTabPage"})
     @Restriction({DeviceFormFactor.PHONE})
+    // Disable the feature due to lack of good list menu testing support.
+    @Features.DisableFeatures(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)
     public void testDismissTileWithContextMenu_Phones() throws Exception {
         testDismissTileWithContextMenuImpl();
     }
@@ -117,6 +121,8 @@
     @MediumTest
     @Feature({"NewTabPage"})
     @Restriction({DeviceFormFactor.TABLET})
+    // Disable the feature due to lack of good list menu testing support.
+    @Features.DisableFeatures(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)
     public void testDismissTileWithContextMenu_Tablets() throws Exception {
         testDismissTileWithContextMenuImpl();
     }
@@ -145,6 +151,8 @@
     @MediumTest
     @Feature({"NewTabPage"})
     @Restriction({DeviceFormFactor.PHONE})
+    // Disable the feature due to lack of good list menu testing support.
+    @Features.DisableFeatures(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)
     public void testDismissTileUndo_Phones() throws Exception {
         testDismissTileUndoImpl();
     }
@@ -153,6 +161,8 @@
     @MediumTest
     @Feature({"NewTabPage"})
     @Restriction({DeviceFormFactor.TABLET})
+    // Disable the feature due to lack of good list menu testing support.
+    @Features.DisableFeatures(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)
     public void testDismissTileUndo_Tablets() throws Exception {
         testDismissTileUndoImpl();
     }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/native_page/ContextMenuManagerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/native_page/ContextMenuManagerUnitTest.java
new file mode 100644
index 0000000..e0ffb56
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/native_page/ContextMenuManagerUnitTest.java
@@ -0,0 +1,81 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.native_page;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.view.View;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowPopupWindow;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.ui.native_page.TouchEnabledDelegate;
+import org.chromium.ui.base.TestActivity;
+
+/** Unit test for {@link ContextMenuManager} */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(shadows = ShadowPopupWindow.class)
+public class ContextMenuManagerUnitTest {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Rule
+    public ActivityScenarioRule<TestActivity> mActivityScenario =
+            new ActivityScenarioRule<>(TestActivity.class);
+
+    private TestActivity mActivity;
+    private ContextMenuManager mManager;
+    private View mAnchorView;
+
+    @Mock NativePageNavigationDelegate mNavigationDelegate;
+    @Mock TouchEnabledDelegate mTouchEnabledDelegate;
+    @Mock ContextMenuManager.Delegate mDelegate;
+
+    @Before
+    public void setup() {
+        mActivityScenario.getScenario().onActivity(activity -> mActivity = activity);
+        mAnchorView = spy(new View(mActivity, null));
+        mManager = new ContextMenuManager(mNavigationDelegate, mTouchEnabledDelegate, () -> {}, "");
+    }
+
+    @Test
+    public void emptyListContextMenu() {
+        assertFalse(
+                "showContextMenu failed since list is empty.",
+                mManager.showListContextMenu(mAnchorView, mDelegate));
+    }
+
+    @Test
+    public void showListContextMenu() {
+        doReturn(true).when(mDelegate).isItemSupported(anyInt());
+        doReturn(false).when(mNavigationDelegate).isOpenInNewTabInGroupEnabled();
+        doReturn(false).when(mNavigationDelegate).isOpenInNewWindowEnabled();
+        doReturn(false).when(mNavigationDelegate).isOpenInIncognitoEnabled();
+        doReturn(null).when(mDelegate).getUrl();
+        doReturn(true).when(mAnchorView).isAttachedToWindow();
+
+        assertTrue(
+                "showContextMenu failed since list is empty.",
+                mManager.showListContextMenu(mAnchorView, mDelegate));
+        assertNotNull("List context menu is null.", mManager.getListMenuForTesting());
+        verify(mDelegate).onContextMenuCreated();
+    }
+}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/tile/TileRendererTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/tile/TileRendererTest.java
index 9329ac3..d42df44 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/tile/TileRendererTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/tile/TileRendererTest.java
@@ -34,7 +34,9 @@
 import org.chromium.base.task.TaskTraits;
 import org.chromium.base.task.test.ShadowPostTask;
 import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.Features;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
 import org.chromium.chrome.browser.suggestions.ImageFetcher;
@@ -56,6 +58,7 @@
 @Config(
         manifest = Config.NONE,
         shadows = {ShadowPostTask.class})
+@Features.EnableFeatures(ChromeFeatureList.TILE_CONTEXT_MENU_REFACTOR)
 public class TileRendererTest {
     /**
      * Backend that substitutes normal PostTask operations. Allow us to coordinate task execution
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index 4be2c87..d6253f3 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -343,6 +343,7 @@
     &kTabWindowManagerReportIndicesMismatch,
     &kTestDefaultDisabled,
     &kTestDefaultEnabled,
+    &kTileContextMenuRefactor,
     &kTraceBinderIpc,
     &kStartSurfaceReturnTime,
     &kUmaBackgroundSessions,
@@ -1076,6 +1077,10 @@
              "TestDefaultEnabled",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kTileContextMenuRefactor,
+             "TileContextMenuRefactor",
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 BASE_FEATURE(kTraceBinderIpc,
              "TraceBinderIpc",
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
index 0a2cf7b..ada91151a 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -188,6 +188,7 @@
 BASE_DECLARE_FEATURE(kHideTabletToolbarDownloadButton);
 BASE_DECLARE_FEATURE(kTestDefaultDisabled);
 BASE_DECLARE_FEATURE(kTestDefaultEnabled);
+BASE_DECLARE_FEATURE(kTileContextMenuRefactor);
 BASE_DECLARE_FEATURE(kTraceBinderIpc);
 BASE_DECLARE_FEATURE(kStartSurfaceReturnTime);
 BASE_DECLARE_FEATURE(kTabResumptionModuleAndroid);
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
index 58a30dd2..12e0376 100644
--- 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
@@ -548,6 +548,7 @@
     public static final String TASK_MANAGER_CLANK = "TaskManagerClank";
     public static final String TEST_DEFAULT_DISABLED = "TestDefaultDisabled";
     public static final String TEST_DEFAULT_ENABLED = "TestDefaultEnabled";
+    public static final String TILE_CONTEXT_MENU_REFACTOR = "TileContextMenuRefactor";
     public static final String TINKER_TANK_BOTTOM_SHEET = "TinkerTankBottomSheet";
     public static final String TOOLBAR_PHONE_CLEANUP = "ToolbarPhoneCleanup";
     public static final String TOOLBAR_SCROLL_ABLATION = "AndroidToolbarScrollAblation";
diff --git a/ui/android/java/src/org/chromium/ui/widget/ViewRectProvider.java b/ui/android/java/src/org/chromium/ui/widget/ViewRectProvider.java
index 3ae4a0e..e73d415 100644
--- a/ui/android/java/src/org/chromium/ui/widget/ViewRectProvider.java
+++ b/ui/android/java/src/org/chromium/ui/widget/ViewRectProvider.java
@@ -35,9 +35,11 @@
     private @Nullable ViewTreeObserver mViewTreeObserver;
 
     private boolean mIncludePadding;
+    private boolean mUseCenterPoint;
 
     /**
      * Creates an instance of a {@link ViewRectProvider}.
+     *
      * @param view The {@link View} used to generate a {@link Rect}.
      */
     public ViewRectProvider(View view) {
@@ -98,6 +100,16 @@
         refreshRectBounds(/* forceRefresh= */ true);
     }
 
+    /**
+     * Whether use the center of the view after all the adjustment applied (insets, margins). The
+     * Rect being provided will be a single point.
+     *
+     * @param useCenterPoint Whether the rect represents the center of the view after adjustments.
+     */
+    public void setUseCenter(boolean useCenterPoint) {
+        mUseCenterPoint = useCenterPoint;
+    }
+
     @Override
     public void startObserving(Observer observer) {
         mView.addOnAttachStateChangeListener(this);
@@ -208,6 +220,12 @@
         mRect.right = Math.min(mRect.right, mView.getRootView().getWidth());
         mRect.bottom = Math.min(mRect.bottom, mView.getRootView().getHeight());
 
+        if (mUseCenterPoint) {
+            int centerX = mRect.left + mRect.width() / 2;
+            int centerY = mRect.top + mRect.height() / 2;
+            mRect.set(centerX, centerY, centerX, centerY);
+        }
+
         notifyRectChanged();
     }
 
diff --git a/ui/android/junit/src/org/chromium/ui/widget/ViewRectProviderTest.java b/ui/android/junit/src/org/chromium/ui/widget/ViewRectProviderTest.java
index 2fe94dc..de7b421 100644
--- a/ui/android/junit/src/org/chromium/ui/widget/ViewRectProviderTest.java
+++ b/ui/android/junit/src/org/chromium/ui/widget/ViewRectProviderTest.java
@@ -213,6 +213,33 @@
         assertRectMatch(9, 18, 103, 204);
     }
 
+    @Test
+    public void testUseCenterPoint() {
+        mViewRectProvider.setUseCenter(true);
+
+        int expectedCounts = 0;
+        mView.layout(10, 20, 100, 200);
+        mView.getViewTreeObserver().dispatchOnPreDraw();
+        Assert.assertEquals(
+                "View changing its position on screen should trigger #onRectChanged.",
+                ++expectedCounts,
+                mOnRectChangeCallback.getCallCount());
+        // The rect represents the center of the rect.
+        assertRectMatch(55, 110, 55, 110);
+
+        // Use center works with margin.
+        mViewRectProvider.setMarginPx(1, 2, 3, 4);
+        mView.getViewTreeObserver().dispatchOnPreDraw();
+
+        Assert.assertEquals(
+                "View changing its position on screen should trigger #onRectChanged.",
+                ++expectedCounts,
+                mOnRectChangeCallback.getCallCount());
+        // New left: 9, new right: 103 => centerX = 56
+        // New top: 18, new bottom: 204 => centerY = 111
+        assertRectMatch(56, 111, 56, 111);
+    }
+
     private void assertRectMatch(int left, int top, int right, int bottom) {
         final Rect expectedRect = new Rect(left, top, right, bottom);
         Assert.assertEquals("Rect does not match.", expectedRect, mViewRectProvider.getRect());