[context menu] add page save option to empty space CM

Screen record:
https://screencast.googleplex.com/cast/NTc4MTc5OTAwMjE3NzUzNnwwNDE5MmJkMC00ZA

Note: Only IDS_CONTEXTMENU_SAVE_PAGE is an UI string and the page group
title is not used anywhere, hence skipping the translation screenshots
check.

Bug: 391719844
Skip-Translation-Screenshots-Check: True
Test: tools/autotest.py -C out/Default ChromeContextMenuPopulatorTest
Change-Id: If047f6250f90092fe5bae8e25850adf020c92ba3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6333204
Reviewed-by: David Trainor <dtrainor@chromium.org>
Auto-Submit: Grace Cham <hscham@chromium.org>
Reviewed-by: Ryo Hashimoto <hashimoto@chromium.org>
Commit-Queue: Grace Cham <hscham@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1437290}
diff --git a/chrome/android/java/res/values/ids.xml b/chrome/android/java/res/values/ids.xml
index 05457c5..65d44f7b 100644
--- a/chrome/android/java/res/values/ids.xml
+++ b/chrome/android/java/res/values/ids.xml
@@ -42,6 +42,9 @@
     <item type="id" name="password_infobar_accounts_spinner" />
 
     <!-- Context Menu Ids -->
+    <!-- Page Group -->
+    <item type="id" name="contextmenu_save_page" />
+
     <!-- Custom Tab Group -->
     <item type="id" name="contextmenu_open_in_new_chrome_tab" />
     <item type="id" name="contextmenu_open_in_chrome_incognito_tab" />
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuItem.java b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuItem.java
index 8051698..1d672fd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuItem.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuItem.java
@@ -63,7 +63,8 @@
         Item.OPEN_IN_NEW_TAB_IN_GROUP,
         Item.SHARE_HIGHLIGHT,
         Item.REMOVE_HIGHLIGHT,
-        Item.LEARN_MORE
+        Item.LEARN_MORE,
+        Item.SAVE_PAGE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Item {
@@ -111,8 +112,10 @@
         int SHARE_HIGHLIGHT = 32;
         int REMOVE_HIGHLIGHT = 33;
         int LEARN_MORE = 34;
+        // Page Group
+        int SAVE_PAGE = 35;
         // ALWAYS UPDATE!
-        int NUM_ENTRIES = 35;
+        int NUM_ENTRIES = 36;
     }
 
     /** Mapping from {@link Item} to the ID found in the ids.xml. */
@@ -152,6 +155,7 @@
         R.id.contextmenu_share_highlight, // Item.SHARE_HIGHLIGHT
         R.id.contextmenu_remove_highlight, // Item.REMOVE_HIGHLIGHT
         R.id.contextmenu_learn_more, // Item.LEARN_MORE
+        R.id.contextmenu_save_page, // Item.SAVE_PAGE
     };
 
     /** Mapping from {@link Item} to the ID of the string that describes the action of the item. */
@@ -191,6 +195,7 @@
         R.string.contextmenu_share_highlight, // Item.SHARE_HIGHLIGHT
         R.string.contextmenu_remove_highlight, // Item.REMOVE_HIGHLIGHT
         R.string.contextmenu_learn_more, // Item.LEARN_MORE
+        R.string.contextmenu_save_page, // Item.SAVE_PAGE
     };
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
index 4168ffd..69c111f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
@@ -173,7 +173,8 @@
             Action.SHARE_HIGHLIGHT,
             Action.REMOVE_HIGHLIGHT,
             Action.LEARN_MORE,
-            Action.OPEN_IN_NEW_TAB_IN_GROUP
+            Action.OPEN_IN_NEW_TAB_IN_GROUP,
+            Action.SAVE_PAGE,
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface Action {
@@ -218,7 +219,8 @@
             int LEARN_MORE = 38;
             int OPEN_IN_NEW_TAB_IN_GROUP = 39;
             int OPEN_IN_NEW_WINDOW = 40;
-            int NUM_ENTRIES = 41;
+            int SAVE_PAGE = 41;
+            int NUM_ENTRIES = 42;
         }
     }
 
@@ -275,7 +277,8 @@
         return DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext);
     }
 
-    public static boolean shouldShowEmptySpaceContextMenu() {
+    @VisibleForTesting
+    boolean shouldShowEmptySpaceContextMenu() {
         return DeviceFormFactor.isDesktop()
                 && DeviceInput.supportsAlphabeticKeyboard()
                 && DeviceInput.supportsPrecisionPointer();
@@ -288,7 +291,14 @@
         List<Pair<Integer, ModelList>> groupedItems = new ArrayList<>();
 
         if (mParams.isPage() && shouldShowEmptySpaceContextMenu()) {
-            // TODO (crbug.com/391719844): add new groups and items for page actions.
+            ModelList pageGroup = new ModelList();
+            // TODO(crbug.com/405842034): investigate supporting downloads in incognito mode.
+            if (!mItemDelegate.isIncognito()
+                    && UrlUtilities.isDownloadableScheme(mParams.getPageUrl())) {
+                pageGroup.add(
+                        createListItem(Item.SAVE_PAGE, false, !mIsDownloadRestrictedByPolicy));
+            }
+            groupedItems.add(new Pair<>(R.string.contextmenu_page_title, pageGroup));
         }
 
         if (mParams.isAnchor()) {
@@ -616,6 +626,14 @@
             } else if (mItemDelegate.startDownload(url, true)) {
                 mNativeDelegate.startDownload(url, false);
             }
+        } else if (itemId == R.id.contextmenu_save_page) {
+            recordContextMenuSelection(ContextMenuUma.Action.SAVE_PAGE);
+            GURL url = mItemDelegate.getPageUrl();
+            if (mIsDownloadRestrictedByPolicy) {
+                showDownloadRestrictedToast();
+            } else if (mItemDelegate.startDownload(url, true)) {
+                mNativeDelegate.startDownload(url, false);
+            }
         } else if (itemId == R.id.contextmenu_share_link) {
             recordContextMenuSelection(ContextMenuUma.Action.SHARE_LINK);
             // TODO(crbug.com/40549331): Migrate ShareParams to GURL.
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulatorTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulatorTest.java
index 0f03a5a..b91246e18 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulatorTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulatorTest.java
@@ -5,7 +5,6 @@
 package org.chromium.chrome.browser.contextmenu;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.eq;
@@ -146,6 +145,12 @@
         doReturn(false).when(mPopulator).shouldTriggerEphemeralTabHelpUi();
         doReturn(false).when(mPopulator).shouldTriggerReadLaterHelpUi();
         doReturn(true).when(mExternalAuthUtils).isGoogleSigned(IntentHandler.PACKAGE_GSA);
+        doReturn(false).when(mPopulator).shouldShowEmptySpaceContextMenu();
+    }
+
+    private void initializePopulatorOnDesktop(@ContextMenuMode int mode, ContextMenuParams params) {
+        initializePopulator(mode, params);
+        doReturn(true).when(mPopulator).shouldShowEmptySpaceContextMenu();
     }
 
     private void checkMenuOptions(List<Integer> disabled, int[]... groups) {
@@ -160,11 +165,10 @@
             int[] availableInTab = new int[contextMenuState.get(i).second.size()];
             for (int j = 0; j < contextMenuState.get(i).second.size(); j++) {
                 PropertyModel model = contextMenuState.get(i).second.get(j).model;
-                if (disabled.contains(model.get(MENU_ID))) {
-                    assertFalse(model.get(ENABLED));
-                } else {
-                    assertTrue(model.get(ENABLED));
-                }
+                assertEquals(
+                        "'" + model.get(TEXT) + "' has different enablement setting than expected",
+                        !disabled.contains(model.get(MENU_ID)),
+                        model.get(ENABLED));
                 availableInTab[j] = model.get(MENU_ID);
             }
 
@@ -187,13 +191,25 @@
             }
 
             if (!Arrays.equals(expectedItemsInGroup, availableInTab)) {
-                StringBuilder info = new StringBuilder();
+                StringBuilder generated_info = new StringBuilder();
                 for (int j = 0; j < contextMenuState.get(i).second.size(); j++) {
-                    info.append("'");
-                    info.append(contextMenuState.get(i).second.get(j).model.get(TEXT));
-                    info.append("' ");
+                    generated_info.append("'");
+                    generated_info.append(contextMenuState.get(i).second.get(j).model.get(TEXT));
+                    generated_info.append("' ");
                 }
-                fail("Items in group " + i + " don't match, generated: " + info.toString());
+                StringBuilder expected_info = new StringBuilder();
+                for (int j = 0; j < expectedItemsInGroup.length; j++) {
+                    expected_info.append("'");
+                    expected_info.append(expectedItemsInGroup[j]);
+                    expected_info.append("' ");
+                }
+                fail(
+                        "Items in group "
+                                + i
+                                + " don't match, expecting: "
+                                + expected_info.toString()
+                                + ", generated: "
+                                + generated_info.toString());
             }
         }
     }
@@ -1448,4 +1464,125 @@
         };
         checkMenuOptions(image2Expected);
     }
+
+    @Test
+    @SmallTest
+    @UiThreadTest
+    public void testPage() {
+        FirstRunStatus.setFirstRunFlowComplete(true);
+        ContextMenuParams params =
+                new ContextMenuParams(
+                        0,
+                        ContextMenuDataMediaType.NONE,
+                        new GURL(PAGE_URL),
+                        GURL.emptyGURL(),
+                        "",
+                        GURL.emptyGURL(),
+                        GURL.emptyGURL(),
+                        "",
+                        null,
+                        false,
+                        0,
+                        0,
+                        MenuSourceType.TOUCH,
+                        false,
+                        false,
+                        /* additionalNavigationParams= */ null);
+
+        int[] expected = {R.id.contextmenu_save_page};
+
+        initializePopulatorOnDesktop(ChromeContextMenuPopulator.ContextMenuMode.NORMAL, params);
+        checkMenuOptions(expected);
+
+        initializePopulatorOnDesktop(ChromeContextMenuPopulator.ContextMenuMode.CUSTOM_TAB, params);
+        checkMenuOptions(expected);
+
+        initializePopulatorOnDesktop(ChromeContextMenuPopulator.ContextMenuMode.WEB_APP, params);
+        checkMenuOptions(expected);
+
+        initializePopulatorOnDesktop(
+                ChromeContextMenuPopulator.ContextMenuMode.NETWORK_BOUND_TAB, params);
+        checkMenuOptions(expected);
+    }
+
+    @Test
+    @SmallTest
+    @UiThreadTest
+    public void testPageNotOnDesktop() {
+        FirstRunStatus.setFirstRunFlowComplete(true);
+        ContextMenuParams params =
+                new ContextMenuParams(
+                        0,
+                        ContextMenuDataMediaType.NONE,
+                        new GURL(PAGE_URL),
+                        GURL.emptyGURL(),
+                        "",
+                        GURL.emptyGURL(),
+                        GURL.emptyGURL(),
+                        "",
+                        null,
+                        false,
+                        0,
+                        0,
+                        MenuSourceType.TOUCH,
+                        false,
+                        false,
+                        /* additionalNavigationParams= */ null);
+
+        int[] expected = null;
+
+        initializePopulator(ChromeContextMenuPopulator.ContextMenuMode.NORMAL, params);
+        checkMenuOptions(expected);
+
+        initializePopulator(ChromeContextMenuPopulator.ContextMenuMode.CUSTOM_TAB, params);
+        checkMenuOptions(expected);
+
+        initializePopulator(ChromeContextMenuPopulator.ContextMenuMode.WEB_APP, params);
+        checkMenuOptions(expected);
+
+        initializePopulator(ChromeContextMenuPopulator.ContextMenuMode.NETWORK_BOUND_TAB, params);
+        checkMenuOptions(expected);
+    }
+
+    @Test
+    @SmallTest
+    @UiThreadTest
+    public void testPageDownloadRestricted() {
+        FirstRunStatus.setFirstRunFlowComplete(true);
+        ContextMenuParams params =
+                new ContextMenuParams(
+                        0,
+                        ContextMenuDataMediaType.NONE,
+                        new GURL(PAGE_URL),
+                        GURL.emptyGURL(),
+                        "",
+                        GURL.emptyGURL(),
+                        GURL.emptyGURL(),
+                        "",
+                        null,
+                        false,
+                        0,
+                        0,
+                        MenuSourceType.TOUCH,
+                        false,
+                        false,
+                        /* additionalNavigationParams= */ null);
+        DownloadUtils.setIsDownloadRestrictedByPolicyForTesting(true);
+
+        int[] expected = {R.id.contextmenu_save_page};
+        List<Integer> expected_disabled = Arrays.asList(R.id.contextmenu_save_page);
+
+        initializePopulatorOnDesktop(ChromeContextMenuPopulator.ContextMenuMode.NORMAL, params);
+        checkMenuOptions(expected_disabled, expected);
+
+        initializePopulatorOnDesktop(ChromeContextMenuPopulator.ContextMenuMode.CUSTOM_TAB, params);
+        checkMenuOptions(expected_disabled, expected);
+
+        initializePopulatorOnDesktop(ChromeContextMenuPopulator.ContextMenuMode.WEB_APP, params);
+        checkMenuOptions(expected_disabled, expected);
+
+        initializePopulatorOnDesktop(
+                ChromeContextMenuPopulator.ContextMenuMode.NETWORK_BOUND_TAB, params);
+        checkMenuOptions(expected_disabled, expected);
+    }
 }
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index b303ffb..16d9c328 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -3095,6 +3095,9 @@
       </message>
 
       <!-- ContextMenu -->
+      <message name="IDS_CONTEXTMENU_SAVE_PAGE" desc="This string is shown in the context menu for saving the current page.">
+        Download
+      </message>
       <message name="IDS_CONTEXTMENU_OPEN_IN_OTHER_WINDOW" desc="Context sensitive menu item to open the selected link in the other window. [CHAR_LIMIT=30]">
         Open in other window
       </message>
@@ -5538,6 +5541,9 @@
       </message>
 
       <!-- Custom Context Menu Informations -->
+      <message name="IDS_CONTEXTMENU_PAGE_TITLE" desc="The title of a context menu tab when the item pressed contains more than one type. This indicates that all the actions are related to the page.">
+        PAGE
+      </message>
       <message name="IDS_CONTEXTMENU_IMAGE_TITLE" desc="The title of a context menu tab when the item pressed contains more than one type. This indicates that all the actions are related to the image.">
         IMAGE
       </message>
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_SAVE_PAGE.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_SAVE_PAGE.png.sha1
new file mode 100644
index 0000000..9ff3d4f
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTEXTMENU_SAVE_PAGE.png.sha1
@@ -0,0 +1 @@
+b3564117e33bd2b1be95fde0eb074d1caf7597e5
\ No newline at end of file
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 216f158..9a204cb8 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -2742,6 +2742,7 @@
   <int value="38" label="Learn more about the shared highlighting feature"/>
   <int value="39" label="Open in new tab in group"/>
   <int value="40" label="Open in new window"/>
+  <int value="41" label="Save page"/>
 </enum>
 
 <!-- LINT.IfChange(ConversionCreateAggregatableReportStatus) -->