// Copyright 2013 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.contextmenu;
import static org.chromium.chrome.browser.contextmenu.ContextMenuItemProperties.MENU_ID;
import static org.chromium.chrome.browser.contextmenu.ContextMenuItemProperties.TEXT;
import static org.chromium.chrome.browser.contextmenu.ContextMenuItemWithIconButtonProperties.BUTTON_CONTENT_DESC;
import static org.chromium.chrome.browser.contextmenu.ContextMenuItemWithIconButtonProperties.BUTTON_IMAGE;
import static org.chromium.chrome.browser.contextmenu.ContextMenuItemWithIconButtonProperties.BUTTON_MENU_ID;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.MailTo;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;
import android.webkit.URLUtil;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.contextmenu.ChromeContextMenuItem.Item;
import org.chromium.chrome.browser.contextmenu.ContextMenuCoordinator.ListItemType;
import org.chromium.chrome.browser.ephemeraltab.EphemeralTabCoordinator;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.gsa.GSAUtils;
import org.chromium.chrome.browser.lens.LensController;
import org.chromium.chrome.browser.lens.LensEntryPoint;
import org.chromium.chrome.browser.lens.LensIntentParams;
import org.chromium.chrome.browser.lens.LensMetrics;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.read_later.ReadingListUtils;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.share.ChromeShareExtras;
import org.chromium.chrome.browser.share.LensUtils;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.share.ShareDelegate.ShareOrigin;
import org.chromium.chrome.browser.share.ShareHelper;
import org.chromium.chrome.browser.share.ShareUtils;
import org.chromium.chrome.browser.share.link_to_text.LinkToTextHelper;
import org.chromium.chrome.browser.tab.TabContextMenuItemDelegate;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.components.embedder_support.contextmenu.ChipDelegate;
import org.chromium.components.embedder_support.contextmenu.ChipRenderParams;
import org.chromium.components.embedder_support.contextmenu.ContextMenuImageFormat;
import org.chromium.components.embedder_support.contextmenu.ContextMenuNativeDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.components.embedder_support.contextmenu.ContextMenuPopulator;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.externalauth.ExternalAuthUtils;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.components.ukm.UkmRecorder;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/** A {@link ContextMenuPopulator} used for showing the default Chrome context menu. */
public class ChromeContextMenuPopulator implements ContextMenuPopulator {
private final Context mContext;
private final TabContextMenuItemDelegate mItemDelegate;
private final @ContextMenuMode int mMode;
private final Supplier<ShareDelegate> mShareDelegateSupplier;
private final ExternalAuthUtils mExternalAuthUtils;
private final ContextMenuParams mParams;
private @Nullable UkmRecorder.Bridge mUkmRecorderBridge;
private ContextMenuNativeDelegate mNativeDelegate;
private static final String LENS_SUPPORT_STATUS_HISTOGRAM_NAME =
"ContextMenu.LensSupportStatus";
// True when the tracker indicates IPH in the form of "new" label needs to be shown.
private Boolean mShowEphemeralTabNewLabel;
/** Defines the Groups of each Context Menu Item */
@IntDef({ContextMenuGroup.LINK, ContextMenuGroup.IMAGE, ContextMenuGroup.VIDEO})
@Retention(RetentionPolicy.SOURCE)
public @interface ContextMenuGroup {
int LINK = 0;
int IMAGE = 1;
int VIDEO = 2;
}
/** Defines the context menu modes */
@IntDef({ContextMenuMode.NORMAL, ContextMenuMode.CUSTOM_TAB, ContextMenuMode.WEB_APP})
@Retention(RetentionPolicy.SOURCE)
public @interface ContextMenuMode {
int NORMAL = 0; /* Default mode*/
int CUSTOM_TAB = 1; /* Custom tab mode */
int WEB_APP = 2; /* Full screen mode */
}
static class ContextMenuUma {
// Note: these values must match the ContextMenuOptionAndroid enum in enums.xml.
// Only add values to the end, right before NUM_ENTRIES!
@IntDef({
Action.OPEN_IN_NEW_TAB,
Action.OPEN_IN_INCOGNITO_TAB,
Action.COPY_LINK_ADDRESS,
Action.COPY_EMAIL_ADDRESS,
Action.COPY_LINK_TEXT,
Action.SAVE_LINK,
Action.SAVE_IMAGE,
Action.OPEN_IMAGE,
Action.OPEN_IMAGE_IN_NEW_TAB,
Action.SEARCH_BY_IMAGE,
Action.LOAD_ORIGINAL_IMAGE,
Action.SAVE_VIDEO,
Action.SHARE_IMAGE,
Action.OPEN_IN_OTHER_WINDOW,
Action.OPEN_IN_NEW_WINDOW,
Action.SEND_EMAIL,
Action.ADD_TO_CONTACTS,
Action.CALL,
Action.SEND_TEXT_MESSAGE,
Action.COPY_PHONE_NUMBER,
Action.OPEN_IN_NEW_CHROME_TAB,
Action.OPEN_IN_CHROME_INCOGNITO_TAB,
Action.OPEN_IN_BROWSER,
Action.OPEN_IN_CHROME,
Action.SHARE_LINK,
Action.OPEN_IN_EPHEMERAL_TAB,
Action.OPEN_IMAGE_IN_EPHEMERAL_TAB,
Action.DIRECT_SHARE_LINK,
Action.DIRECT_SHARE_IMAGE,
Action.SEARCH_WITH_GOOGLE_LENS,
Action.COPY_IMAGE,
Action.SHOP_IMAGE_WITH_GOOGLE_LENS,
Action.READ_LATER,
Action.SHOP_WITH_GOOGLE_LENS_CHIP,
Action.TRANSLATE_WITH_GOOGLE_LENS_CHIP,
Action.SHARE_HIGHLIGHT,
Action.REMOVE_HIGHLIGHT,
Action.LEARN_MORE,
Action.OPEN_IN_NEW_TAB_IN_GROUP
})
@Retention(RetentionPolicy.SOURCE)
public @interface Action {
int OPEN_IN_NEW_TAB = 0;
int OPEN_IN_INCOGNITO_TAB = 1;
int COPY_LINK_ADDRESS = 2;
int COPY_EMAIL_ADDRESS = 3;
int COPY_LINK_TEXT = 4;
int SAVE_LINK = 5;
int SAVE_IMAGE = 6;
int OPEN_IMAGE = 7;
int OPEN_IMAGE_IN_NEW_TAB = 8;
int SEARCH_BY_IMAGE = 9;
int LOAD_ORIGINAL_IMAGE = 10;
int SAVE_VIDEO = 11;
int SHARE_IMAGE = 12;
int OPEN_IN_OTHER_WINDOW = 13;
int SEND_EMAIL = 14;
int ADD_TO_CONTACTS = 15;
int CALL = 16;
int SEND_TEXT_MESSAGE = 17;
int COPY_PHONE_NUMBER = 18;
int OPEN_IN_NEW_CHROME_TAB = 19;
int OPEN_IN_CHROME_INCOGNITO_TAB = 20;
int OPEN_IN_BROWSER = 21;
int OPEN_IN_CHROME = 22;
int SHARE_LINK = 23;
int OPEN_IN_EPHEMERAL_TAB = 24;
int OPEN_IMAGE_IN_EPHEMERAL_TAB = 25;
int DIRECT_SHARE_LINK = 26;
int DIRECT_SHARE_IMAGE = 27;
int SEARCH_WITH_GOOGLE_LENS = 28;
int COPY_IMAGE = 29;
// int SHOP_SIMILAR_PRODUCTS = 30; Deprecated since 06/2021.
int SHOP_IMAGE_WITH_GOOGLE_LENS = 31;
// int SEARCH_SIMILAR_PRODUCTS = 32; // Deprecated since 06/2021.
int READ_LATER = 33;
int SHOP_WITH_GOOGLE_LENS_CHIP = 34;
int TRANSLATE_WITH_GOOGLE_LENS_CHIP = 35;
int SHARE_HIGHLIGHT = 36;
int REMOVE_HIGHLIGHT = 37;
int LEARN_MORE = 38;
int OPEN_IN_NEW_TAB_IN_GROUP = 39;
int OPEN_IN_NEW_WINDOW = 40;
int NUM_ENTRIES = 41;
}
}
/**
* Builds a {@link ChromeContextMenuPopulator}.
*
* @param itemDelegate The {@link TabContextMenuItemDelegate} that will be notified with actions
* to perform when menu items are selected.
* @param shareDelegate The Supplier of {@link ShareDelegate} that will be notified when a share
* action is performed.
* @param mode Defines the context menu mode
* @param externalAuthUtils {@link ExternalAuthUtils} instance.
* @param context The {@link Context} used to retrieve the strings.
* @param params The {@link ContextMenuParams} to populate the menu items.
* @param nativeDelegate The {@link ContextMenuNativeDelegate} used to interact with native.
*/
public ChromeContextMenuPopulator(
TabContextMenuItemDelegate itemDelegate,
Supplier<ShareDelegate> shareDelegate,
@ContextMenuMode int mode,
ExternalAuthUtils externalAuthUtils,
Context context,
ContextMenuParams params,
ContextMenuNativeDelegate nativeDelegate) {
mItemDelegate = itemDelegate;
mShareDelegateSupplier = shareDelegate;
mMode = mode;
mExternalAuthUtils = externalAuthUtils;
mContext = context;
mParams = params;
mNativeDelegate = nativeDelegate;
}
/**
* Gets the link of the item or empty text if the Url is empty.
* @return A string with the link or an empty string.
*/
public static String createUrlText(ContextMenuParams params) {
if (!isEmptyUrl(params.getLinkUrl())) {
return getUrlText(params);
}
return "";
}
private static String getUrlText(ContextMenuParams params) {
// ContextMenuParams can only be created after the browser has started.
assert BrowserStartupController.getInstance().isFullBrowserStarted();
return UrlFormatter.formatUrlForDisplayOmitSchemeOmitTrivialSubdomains(
params.getLinkUrl().getSpec());
}
@VisibleForTesting
boolean isTabletScreen() {
return DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext);
}
@Override
public List<Pair<Integer, ModelList>> buildContextMenu() {
mShowEphemeralTabNewLabel = null;
List<Pair<Integer, ModelList>> groupedItems = new ArrayList<>();
if (mParams.isAnchor()) {
ModelList linkGroup = new ModelList();
if (FirstRunStatus.getFirstRunFlowComplete()
&& !isEmptyUrl(mParams.getUrl())
&& UrlUtilities.isAcceptedScheme(mParams.getUrl())) {
if (mMode == ContextMenuMode.NORMAL) {
linkGroup.add(createListItem(Item.OPEN_IN_NEW_TAB_IN_GROUP));
linkGroup.add(createListItem(Item.OPEN_IN_NEW_TAB));
if (!mItemDelegate.isIncognito() && mItemDelegate.isIncognitoSupported()) {
linkGroup.add(createListItem(Item.OPEN_IN_INCOGNITO_TAB));
}
if (mItemDelegate.isOpenInOtherWindowSupported()) {
linkGroup.add(createListItem(Item.OPEN_IN_OTHER_WINDOW));
} else if (isTabletScreen() && mItemDelegate.canEnterMultiWindowMode()) {
linkGroup.add(createListItem(Item.OPEN_IN_NEW_WINDOW));
}
}
if ((mMode == ContextMenuMode.NORMAL || mMode == ContextMenuMode.CUSTOM_TAB)
&& EphemeralTabCoordinator.isSupported()) {
mShowEphemeralTabNewLabel = shouldTriggerEphemeralTabHelpUi();
linkGroup.add(
createListItem(Item.OPEN_IN_EPHEMERAL_TAB, mShowEphemeralTabNewLabel));
}
}
if (!MailTo.isMailTo(mParams.getLinkUrl().getSpec())
&& !UrlUtilities.isTelScheme(mParams.getLinkUrl())) {
linkGroup.add(createListItem(Item.COPY_LINK_ADDRESS));
if (!mParams.getLinkText().trim().isEmpty() && !mParams.isImage()) {
linkGroup.add(createListItem(Item.COPY_LINK_TEXT));
}
}
if (FirstRunStatus.getFirstRunFlowComplete()) {
if (!mItemDelegate.isIncognito()
&& UrlUtilities.isDownloadableScheme(mParams.getLinkUrl())) {
linkGroup.add(createListItem(Item.SAVE_LINK_AS));
}
if (!mParams.isImage()
&& ReadingListUtils.isReadingListSupported(mParams.getLinkUrl())) {
linkGroup.add(createListItem(Item.READ_LATER, shouldTriggerReadLaterHelpUi()));
}
if (enableShareFromContextMenu()) {
linkGroup.add(createShareListItem(Item.SHARE_LINK, Item.DIRECT_SHARE_LINK));
}
if (UrlUtilities.isTelScheme(mParams.getLinkUrl())) {
if (mItemDelegate.supportsCall()) {
linkGroup.add(createListItem(Item.CALL));
}
if (mItemDelegate.supportsSendTextMessage()) {
linkGroup.add(createListItem(Item.SEND_MESSAGE));
}
if (mItemDelegate.supportsAddToContacts()) {
linkGroup.add(createListItem(Item.ADD_TO_CONTACTS));
}
}
if (MailTo.isMailTo(mParams.getLinkUrl().getSpec())) {
if (mItemDelegate.supportsSendEmailMessage()) {
linkGroup.add(createListItem(Item.SEND_MESSAGE));
}
if (!TextUtils.isEmpty(MailTo.parse(mParams.getLinkUrl().getSpec()).getTo())
&& mItemDelegate.supportsAddToContacts()) {
linkGroup.add(createListItem(Item.ADD_TO_CONTACTS));
}
}
}
if (UrlUtilities.isTelScheme(mParams.getLinkUrl())
|| MailTo.isMailTo(mParams.getLinkUrl().getSpec())) {
linkGroup.add(createListItem(Item.COPY));
}
if (linkGroup.size() > 0) {
groupedItems.add(new Pair<>(R.string.contextmenu_link_title, linkGroup));
}
}
if (mParams.isImage() && FirstRunStatus.getFirstRunFlowComplete()) {
ModelList imageGroup = new ModelList();
boolean isSrcDownloadableScheme =
UrlUtilities.isDownloadableScheme(mParams.getSrcUrl());
// Avoid showing open image option for same image which is already opened.
if (mMode == ContextMenuMode.CUSTOM_TAB
&& !mItemDelegate.getPageUrl().equals(mParams.getSrcUrl())) {
imageGroup.add(createListItem(Item.OPEN_IMAGE));
}
if (mMode == ContextMenuMode.NORMAL) {
imageGroup.add(createListItem(Item.OPEN_IMAGE_IN_NEW_TAB));
}
if ((mMode == ContextMenuMode.NORMAL || mMode == ContextMenuMode.CUSTOM_TAB)
&& EphemeralTabCoordinator.isSupported()) {
if (mShowEphemeralTabNewLabel == null) {
mShowEphemeralTabNewLabel = shouldTriggerEphemeralTabHelpUi();
}
imageGroup.add(
createListItem(
Item.OPEN_IMAGE_IN_EPHEMERAL_TAB, mShowEphemeralTabNewLabel));
}
imageGroup.add(createListItem(Item.COPY_IMAGE));
if (isSrcDownloadableScheme) {
imageGroup.add(createListItem(Item.SAVE_IMAGE));
}
if (mMode == ContextMenuMode.CUSTOM_TAB || mMode == ContextMenuMode.NORMAL) {
if (checkSupportsGoogleSearchByImage(isSrcDownloadableScheme)) {
// Determine which image search menu item would be shown.
boolean shouldShowSearchImageWithLens =
shouldShowSearchWithLensAndRecordMetrics(
mParams.getPageUrl(), mItemDelegate.isIncognito());
if (shouldShowSearchImageWithLens) {
imageGroup.add(createListItem(Item.SEARCH_WITH_GOOGLE_LENS, true));
maybeRecordUkmLensShown();
} else {
imageGroup.add(createListItem(Item.SEARCH_BY_IMAGE));
maybeRecordUkmSearchByImageShown();
}
} else {
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME,
LensMetrics.LensSupportStatus.SEARCH_BY_IMAGE_UNAVAILABLE);
}
}
if (enableShareFromContextMenu()) {
imageGroup.add(createShareListItem(Item.SHARE_IMAGE, Item.DIRECT_SHARE_IMAGE));
}
groupedItems.add(new Pair<>(R.string.contextmenu_image_title, imageGroup));
}
if (mParams.isVideo()
&& FirstRunStatus.getFirstRunFlowComplete()
&& mParams.canSaveMedia()
&& UrlUtilities.isDownloadableScheme(mParams.getSrcUrl())) {
ModelList videoGroup = new ModelList();
videoGroup.add(createListItem(Item.SAVE_VIDEO));
groupedItems.add(new Pair<>(R.string.contextmenu_video_title, videoGroup));
}
if (mParams.getOpenedFromHighlight()) {
ModelList sharedHighlightingGroup = new ModelList();
if (mMode == ContextMenuMode.NORMAL) {
sharedHighlightingGroup.add(createListItem(Item.SHARE_HIGHLIGHT));
}
sharedHighlightingGroup.add(createListItem(Item.REMOVE_HIGHLIGHT));
if (mMode == ContextMenuMode.NORMAL) {
sharedHighlightingGroup.add(createListItem(Item.LEARN_MORE));
}
groupedItems.add(new Pair<>(null, sharedHighlightingGroup));
}
if (mMode != ContextMenuMode.NORMAL && FirstRunStatus.getFirstRunFlowComplete()) {
ModelList items =
groupedItems.isEmpty()
? new ModelList()
: groupedItems.get(
mMode == ContextMenuMode.CUSTOM_TAB
? 0
: groupedItems.size() - 1)
.second;
if (UrlUtilities.isAcceptedScheme(mParams.getUrl())) {
if (mMode == ContextMenuMode.WEB_APP) {
items.add(createListItem(Item.OPEN_IN_CHROME));
} else if (mMode == ContextMenuMode.CUSTOM_TAB && !mItemDelegate.isIncognito()) {
boolean addNewEntries =
!UrlUtilities.isInternalScheme(mParams.getUrl())
&& !isEmptyUrl(mParams.getUrl());
if (ChromeSharedPreferences.getInstance()
.readBoolean(ChromePreferenceKeys.CHROME_DEFAULT_BROWSER, false)
&& addNewEntries) {
if (mItemDelegate.isIncognitoSupported()) {
items.add(0, createListItem(Item.OPEN_IN_CHROME_INCOGNITO_TAB));
}
items.add(0, createListItem(Item.OPEN_IN_NEW_CHROME_TAB));
} else if (addNewEntries && UrlUtilities.isAcceptedScheme(mParams.getUrl())) {
items.add(0, createListItem(Item.OPEN_IN_BROWSER_ID));
}
}
}
if (groupedItems.isEmpty() && items.size() > 0) {
groupedItems.add(new Pair<>(R.string.contextmenu_link_title, items));
}
}
return groupedItems;
}
@VisibleForTesting
boolean shouldTriggerEphemeralTabHelpUi() {
Tracker tracker = TrackerFactory.getTrackerForProfile(getProfile());
return tracker.isInitialized()
&& tracker.shouldTriggerHelpUI(FeatureConstants.EPHEMERAL_TAB_FEATURE);
}
@VisibleForTesting
boolean shouldTriggerReadLaterHelpUi() {
Tracker tracker = TrackerFactory.getTrackerForProfile(getProfile());
return tracker.isInitialized()
&& tracker.shouldTriggerHelpUI(FeatureConstants.READ_LATER_CONTEXT_MENU_FEATURE);
}
@Override
public boolean isIncognito() {
return mItemDelegate.isIncognito();
}
@Override
public String getPageTitle() {
return mItemDelegate.getPageTitle();
}
@Override
public boolean onItemSelected(int itemId) {
if (itemId == R.id.contextmenu_open_in_new_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_NEW_TAB);
mItemDelegate.onOpenInNewTab(
mParams.getUrl(),
mParams.getReferrer(),
/* navigateToTab= */ false,
mParams.getAdditionalNavigationParams());
} else if (itemId == R.id.contextmenu_open_in_new_tab_in_group) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_NEW_TAB_IN_GROUP);
mItemDelegate.onOpenInNewTabInGroup(mParams.getUrl(), mParams.getReferrer());
} else if (itemId == R.id.contextmenu_open_in_incognito_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_INCOGNITO_TAB);
mItemDelegate.onOpenInNewIncognitoTab(mParams.getUrl());
} else if (itemId == R.id.contextmenu_open_in_other_window) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_OTHER_WINDOW);
mItemDelegate.onOpenInOtherWindow(mParams.getUrl(), mParams.getReferrer());
} else if (itemId == R.id.contextmenu_open_in_new_window) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_NEW_WINDOW);
// |openInOtherWindow| can handle opening in a new window as well.
mItemDelegate.onOpenInOtherWindow(mParams.getUrl(), mParams.getReferrer());
} else if (itemId == R.id.contextmenu_open_in_ephemeral_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_EPHEMERAL_TAB);
mItemDelegate.onOpenInEphemeralTab(mParams.getUrl(), mParams.getLinkText());
} else if (itemId == R.id.contextmenu_open_image) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IMAGE);
mItemDelegate.onOpenImageUrl(mParams.getSrcUrl(), mParams.getReferrer());
} else if (itemId == R.id.contextmenu_open_image_in_new_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IMAGE_IN_NEW_TAB);
mItemDelegate.onOpenImageInNewTab(mParams.getSrcUrl(), mParams.getReferrer());
} else if (itemId == R.id.contextmenu_open_image_in_ephemeral_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IMAGE_IN_EPHEMERAL_TAB);
String title = mParams.getTitleText();
if (TextUtils.isEmpty(title)) {
title = URLUtil.guessFileName(mParams.getSrcUrl().getSpec(), null, null);
}
mItemDelegate.onOpenInEphemeralTab(mParams.getSrcUrl(), title);
} else if (itemId == R.id.contextmenu_copy_image) {
recordContextMenuSelection(ContextMenuUma.Action.COPY_IMAGE);
copyImageToClipboard();
} else if (itemId == R.id.contextmenu_copy_link_address) {
recordContextMenuSelection(ContextMenuUma.Action.COPY_LINK_ADDRESS);
mItemDelegate.onSaveToClipboard(
mParams.getUnfilteredLinkUrl().getSpec(),
TabContextMenuItemDelegate.ClipboardType.LINK_URL);
} else if (itemId == R.id.contextmenu_call) {
recordContextMenuSelection(ContextMenuUma.Action.CALL);
mItemDelegate.onCall(mParams.getLinkUrl());
} else if (itemId == R.id.contextmenu_send_message) {
if (MailTo.isMailTo(mParams.getLinkUrl().getSpec())) {
recordContextMenuSelection(ContextMenuUma.Action.SEND_EMAIL);
mItemDelegate.onSendEmailMessage(mParams.getLinkUrl());
} else if (UrlUtilities.isTelScheme(mParams.getLinkUrl())) {
recordContextMenuSelection(ContextMenuUma.Action.SEND_TEXT_MESSAGE);
mItemDelegate.onSendTextMessage(mParams.getLinkUrl());
}
} else if (itemId == R.id.contextmenu_add_to_contacts) {
recordContextMenuSelection(ContextMenuUma.Action.ADD_TO_CONTACTS);
mItemDelegate.onAddToContacts(mParams.getLinkUrl());
} else if (itemId == R.id.contextmenu_copy) {
if (MailTo.isMailTo(mParams.getLinkUrl().getSpec())) {
recordContextMenuSelection(ContextMenuUma.Action.COPY_EMAIL_ADDRESS);
mItemDelegate.onSaveToClipboard(
MailTo.parse(mParams.getLinkUrl().getSpec()).getTo(),
TabContextMenuItemDelegate.ClipboardType.LINK_URL);
} else if (UrlUtilities.isTelScheme(mParams.getLinkUrl())) {
recordContextMenuSelection(ContextMenuUma.Action.COPY_PHONE_NUMBER);
mItemDelegate.onSaveToClipboard(
UrlUtilities.getTelNumber(mParams.getLinkUrl()),
TabContextMenuItemDelegate.ClipboardType.LINK_URL);
}
} else if (itemId == R.id.contextmenu_copy_link_text) {
recordContextMenuSelection(ContextMenuUma.Action.COPY_LINK_TEXT);
mItemDelegate.onSaveToClipboard(
mParams.getLinkText(), TabContextMenuItemDelegate.ClipboardType.LINK_TEXT);
} else if (itemId == R.id.contextmenu_save_image) {
recordContextMenuSelection(ContextMenuUma.Action.SAVE_IMAGE);
if (mItemDelegate.startDownload(mParams.getSrcUrl(), false)) {
mNativeDelegate.startDownload(false);
}
} else if (itemId == R.id.contextmenu_save_video) {
recordContextMenuSelection(ContextMenuUma.Action.SAVE_VIDEO);
if (mItemDelegate.startDownload(mParams.getSrcUrl(), false)) {
mNativeDelegate.startDownload(false);
}
} else if (itemId == R.id.contextmenu_save_link_as) {
recordContextMenuSelection(ContextMenuUma.Action.SAVE_LINK);
GURL url = mParams.getUnfilteredLinkUrl();
if (mItemDelegate.startDownload(url, true)) {
mNativeDelegate.startDownload(true);
}
} else if (itemId == R.id.contextmenu_share_link) {
recordContextMenuSelection(ContextMenuUma.Action.SHARE_LINK);
// TODO(crbug.com/40549331): Migrate ShareParams to GURL.
ShareParams linkShareParams =
new ShareParams.Builder(
getWindow(),
ContextMenuUtils.getTitle(mParams),
mParams.getUrl().getSpec())
.build();
mShareDelegateSupplier
.get()
.share(
linkShareParams,
new ChromeShareExtras.Builder().setSaveLastUsed(true).build(),
ShareOrigin.CONTEXT_MENU);
} else if (itemId == R.id.contextmenu_read_later) {
recordContextMenuSelection(ContextMenuUma.Action.READ_LATER);
// TODO(crbug.com/40156623): Download the page to offline page backend.
String title = mParams.getTitleText();
if (TextUtils.isEmpty(title)) {
title = mParams.getLinkText();
}
mItemDelegate.onReadLater(mParams.getUrl(), title);
} else if (itemId == R.id.contextmenu_direct_share_link) {
recordContextMenuSelection(ContextMenuUma.Action.DIRECT_SHARE_LINK);
final ShareParams shareParams =
new ShareParams.Builder(
getWindow(),
ContextMenuUtils.getTitle(mParams),
mParams.getUrl().getSpec())
.build();
mShareDelegateSupplier
.get()
.share(
shareParams,
new ChromeShareExtras.Builder().setShareDirectly(true).build(),
ShareOrigin.CONTEXT_MENU);
} else if (itemId == R.id.contextmenu_search_with_google_lens) {
recordContextMenuSelection(ContextMenuUma.Action.SEARCH_WITH_GOOGLE_LENS);
searchWithGoogleLens(LensEntryPoint.CONTEXT_MENU_SEARCH_MENU_ITEM);
SharedPreferencesManager prefManager = ChromeSharedPreferences.getInstance();
prefManager.writeBoolean(
ChromePreferenceKeys.CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS_CLICKED, true);
} else if (itemId == R.id.contextmenu_search_by_image) {
LensMetrics.recordAmbientSearchQuery(
LensMetrics.AmbientSearchEntryPoint.CONTEXT_MENU_SEARCH_IMAGE_WITH_WEB);
recordContextMenuSelection(ContextMenuUma.Action.SEARCH_BY_IMAGE);
mNativeDelegate.searchForImage();
} else if (itemId == R.id.contextmenu_share_image) {
recordContextMenuSelection(ContextMenuUma.Action.SHARE_IMAGE);
shareImage();
} else if (itemId == R.id.contextmenu_direct_share_image) {
recordContextMenuSelection(ContextMenuUma.Action.DIRECT_SHARE_IMAGE);
mNativeDelegate.retrieveImageForShare(
ContextMenuImageFormat.ORIGINAL,
(Uri uri) -> {
assert ShareHelper.getLastShareComponentName() != null;
ShareParams params =
new ShareParams.Builder(getWindow(), mParams.getTitleText(), "")
.setSingleImageUri(uri)
.build();
ShareHelper.shareDirectly(
params,
ShareHelper.getLastShareComponentName(),
getProfile(),
false);
});
} else if (itemId == R.id.contextmenu_open_in_chrome) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_CHROME);
mItemDelegate.onOpenInChrome(mParams.getUrl(), mParams.getPageUrl());
} else if (itemId == R.id.contextmenu_open_in_new_chrome_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_NEW_CHROME_TAB);
mItemDelegate.onOpenInNewChromeTabFromCCT(mParams.getUrl(), false);
} else if (itemId == R.id.contextmenu_open_in_chrome_incognito_tab) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_CHROME_INCOGNITO_TAB);
mItemDelegate.onOpenInNewChromeTabFromCCT(mParams.getUrl(), true);
} else if (itemId == R.id.contextmenu_open_in_browser_id) {
recordContextMenuSelection(ContextMenuUma.Action.OPEN_IN_BROWSER);
mItemDelegate.onOpenInDefaultBrowser(mParams.getUrl());
} else if (itemId == R.id.contextmenu_share_highlight) {
recordContextMenuSelection(ContextMenuUma.Action.SHARE_HIGHLIGHT);
shareHighlighting();
} else if (itemId == R.id.contextmenu_remove_highlight) {
recordContextMenuSelection(ContextMenuUma.Action.REMOVE_HIGHLIGHT);
LinkToTextHelper.removeHighlightsAllFrames(mItemDelegate.getWebContents());
} else if (itemId == R.id.contextmenu_learn_more) {
recordContextMenuSelection(ContextMenuUma.Action.LEARN_MORE);
mItemDelegate.onOpenInNewTab(
new GURL(LinkToTextHelper.SHARED_HIGHLIGHTING_SUPPORT_URL),
mParams.getReferrer(),
/* navigateToTab= */ true,
/* additionalNavigationParams= */ null);
} else {
assert false;
}
return true;
}
@Override
public void onMenuClosed() {
if (mShowEphemeralTabNewLabel != null && mShowEphemeralTabNewLabel) {
Tracker tracker = TrackerFactory.getTrackerForProfile(getProfile());
if (tracker.isInitialized()) tracker.dismissed(FeatureConstants.EPHEMERAL_TAB_FEATURE);
}
}
private WindowAndroid getWindow() {
return mItemDelegate.getWebContents().getTopLevelNativeWindow();
}
private Activity getActivity() {
return getWindow().getActivity().get();
}
private void shareHighlighting() {
ShareParams linkShareParams =
new ShareParams.Builder(
getWindow(), /* title= */ "", /* url= */ mParams.getUrl().getSpec())
.build();
mShareDelegateSupplier
.get()
.share(
linkShareParams,
new ChromeShareExtras.Builder()
.setSaveLastUsed(true)
.setIsReshareHighlightedText(true)
.setRenderFrameHost(mNativeDelegate.getRenderFrameHost())
.setDetailedContentType(
ChromeShareExtras.DetailedContentType.HIGHLIGHTED_TEXT)
.build(),
ShareOrigin.MOBILE_ACTION_MODE);
}
/** Copy the image, that triggered the current context menu, to system clipboard. */
private void copyImageToClipboard() {
mNativeDelegate.retrieveImageForShare(
ContextMenuImageFormat.ORIGINAL, mItemDelegate::onSaveImageToClipboard);
}
/**
* Share the image that triggered the current context menu.
* Package-private, allowing access only from the context menu item to ensure that
* it will use the right activity set when the menu was displayed.
*/
private void shareImage() {
mNativeDelegate.retrieveImageForShare(
ContextMenuImageFormat.ORIGINAL,
(Uri imageUri) -> {
ContentResolver contentResolver =
ContextUtils.getApplicationContext().getContentResolver();
String mimeType = contentResolver.getType(imageUri);
ShareParams imageShareParams =
new ShareParams.Builder(
getWindow(),
ContextMenuUtils.getTitle(mParams),
/* url= */ "")
.setSingleImageUri(imageUri)
.setFileContentType(mimeType)
.build();
int detailedContentType;
if (mimeType.equals("image/gif")) {
detailedContentType = ChromeShareExtras.DetailedContentType.GIF;
} else {
detailedContentType = ChromeShareExtras.DetailedContentType.IMAGE;
}
mShareDelegateSupplier
.get()
.share(
imageShareParams,
new ChromeShareExtras.Builder()
.setSaveLastUsed(true)
.setImageSrcUrl(mParams.getSrcUrl())
.setContentUrl(mParams.getPageUrl())
.setDetailedContentType(detailedContentType)
.build(),
ShareOrigin.CONTEXT_MENU);
});
}
/**
* @return The service that handles TemplateUrls.
*/
protected TemplateUrlService getTemplateUrlService() {
return TemplateUrlServiceFactory.getForProfile(getProfile());
}
/**
* Search for the image by intenting to the lens app with the image data attached.
* @param lensEntryPoint The entry point that launches the Lens app.
*/
protected void searchWithGoogleLens(@LensEntryPoint int lensEntryPoint) {
mNativeDelegate.retrieveImageForShare(
ContextMenuImageFormat.PNG,
(Uri imageUri) -> {
LensIntentParams intentParams = getLensIntentParams(lensEntryPoint, imageUri);
LensController.getInstance().startLens(getWindow(), intentParams);
});
}
/**
* Build the intent params for Lens Context Menu features.
* @param lensEntryPoint The entry point that launches the Lens app.
* @param imageUri The image url that the context menu was triggered on.
* @return A LensIntentParams. Will be used to launch the Lens app.
*/
@VisibleForTesting
protected LensIntentParams getLensIntentParams(
@LensEntryPoint int lensEntryPoint, Uri imageUri) {
return new LensIntentParams.Builder(lensEntryPoint, isIncognito())
.withImageUri(imageUri)
.withImageTitleOrAltText(mParams.getTitleText())
.withSrcUrl(mParams.getSrcUrl().getValidSpecOrEmpty())
.withPageUrl(mParams.getPageUrl().getValidSpecOrEmpty())
.build();
}
@Override
public @Nullable ChipDelegate getChipDelegate() {
if (LensChipDelegate.isEnabled(isIncognito(), isTabletScreen())) {
// TODO(crbug.com/40549331): Migrate LensChipDelegate to GURL.
return new LensChipDelegate(
mParams.getPageUrl().getSpec(),
mParams.getTitleText(),
mParams.getSrcUrl().getSpec(),
getPageTitle(),
isIncognito(),
isTabletScreen(),
mItemDelegate.getWebContents(),
mNativeDelegate,
getOnChipClickedCallback(),
getOnChipShownCallback());
}
return null;
}
private Callback<Integer> getOnChipShownCallback() {
return (Integer result) -> {
int chipType = result.intValue();
maybeRecordUkmLensChipShown(chipType);
};
}
private Callback<Integer> getOnChipClickedCallback() {
return (Integer result) -> {
int chipType = result.intValue();
switch (chipType) {
case ChipRenderParams.ChipType.LENS_TRANSLATE_CHIP:
recordContextMenuSelection(
ContextMenuUma.Action.TRANSLATE_WITH_GOOGLE_LENS_CHIP);
return;
default:
// Unreachable value.
throw new IllegalArgumentException("Invalid chip type provided to callback.");
}
};
}
/**
* Checks whether a url is empty or blank.
* @param url The url need to be checked.
* @return True if the url is empty or "about:blank".
*/
private static boolean isEmptyUrl(GURL url) {
return url == null
|| url.isEmpty()
|| url.getSpec().equals(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
}
/** Record a UMA ping and a UKM ping if enabled. */
private void recordContextMenuSelection(int actionId) {
if (LensUtils.shouldLogUkmForLensContextMenuFeatures()) {
maybeRecordActionUkm("ContextMenuAndroid.Selected", actionId);
}
}
/**
* Whether the lens menu items should be shown based on a set of application
* compatibility checks.
*
* @param pageUrl The Url associated with the main frame of the page that triggered the context
* menu.
* @param isIncognito Whether the user is incognito.
* @return A boolean. True if "Search image with Google Lens" should be enabled, otherwise
* False.
*/
private boolean shouldShowSearchWithLensAndRecordMetrics(GURL pageUrl, boolean isIncognito) {
// If Google Lens feature is not supported, show search by image menu item.
if (!LensUtils.isGoogleLensFeatureEnabled(isIncognito)) {
return false;
}
final TemplateUrlService templateUrlServiceInstance = getTemplateUrlService();
String versionName = LensUtils.getLensActivityVersionNameIfAvailable(mContext);
if (!templateUrlServiceInstance.isDefaultSearchEngineGoogle()) {
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME,
LensMetrics.LensSupportStatus.NON_GOOGLE_SEARCH_ENGINE);
return false;
}
if (TextUtils.isEmpty(versionName)) {
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME,
LensMetrics.LensSupportStatus.ACTIVITY_NOT_ACCESSIBLE);
return false;
}
if (GSAUtils.isAgsaVersionBelowMinimum(
versionName, LensUtils.getMinimumAgsaVersionForLensSupport())) {
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME, LensMetrics.LensSupportStatus.OUT_OF_DATE);
return false;
}
if (LensUtils.isDeviceOsBelowMinimum()) {
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME, LensMetrics.LensSupportStatus.LEGACY_OS);
return false;
}
if (!LensUtils.isValidAgsaPackage(mExternalAuthUtils)) {
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME,
LensMetrics.LensSupportStatus.INVALID_PACKAGE);
return false;
}
LensMetrics.recordLensSupportStatus(
LENS_SUPPORT_STATUS_HISTOGRAM_NAME,
LensMetrics.LensSupportStatus.LENS_SEARCH_SUPPORTED);
return true;
}
private ListItem createListItem(@Item int item) {
return createListItem(item, false);
}
private ListItem createListItem(@Item int item, boolean showInProductHelp) {
final PropertyModel model =
new PropertyModel.Builder(ContextMenuItemProperties.ALL_KEYS)
.with(MENU_ID, ChromeContextMenuItem.getMenuId(item))
.with(
TEXT,
ChromeContextMenuItem.getTitle(
mContext, getProfile(), item, showInProductHelp))
.build();
return new ListItem(ListItemType.CONTEXT_MENU_ITEM, model);
}
private ListItem createShareListItem(@Item int item, @Item int iconButtonItem) {
final boolean isLink = item == Item.SHARE_LINK;
final Pair<Drawable, CharSequence> shareInfo = createRecentShareAppInfo(isLink);
final PropertyModel model =
new PropertyModel.Builder(ContextMenuItemWithIconButtonProperties.ALL_KEYS)
.with(MENU_ID, ChromeContextMenuItem.getMenuId(item))
.with(
TEXT,
ChromeContextMenuItem.getTitle(mContext, getProfile(), item, false))
.with(BUTTON_IMAGE, shareInfo.first)
.with(BUTTON_CONTENT_DESC, shareInfo.second)
.with(BUTTON_MENU_ID, ChromeContextMenuItem.getMenuId(iconButtonItem))
.build();
return new ListItem(ListItemType.CONTEXT_MENU_ITEM_WITH_ICON_BUTTON, model);
}
/**
* Return the icon and name of the most recently shared app by certain app.
* @param isLink Whether the item is SHARE_LINK.
*/
private static Pair<Drawable, CharSequence> createRecentShareAppInfo(boolean isLink) {
return isLink
? ShareHelper.getShareableIconAndNameForText()
// Use jpeg as a generic image type to be shared. Most apps accepting jpeg
// should also accept other image types.
: ShareHelper.getShareableIconAndNameForFileContentType("image/jpeg");
}
/**
* If not disabled record a UKM for opening the context menu with the search by image option.
*/
private void maybeRecordUkmSearchByImageShown() {
if (LensUtils.shouldLogUkmForLensContextMenuFeatures()) {
maybeRecordBooleanUkm("ContextMenuAndroid.Shown", "SearchByImage");
}
}
/** If not disabled record a UKM for opening the context menu with the lens item. */
private void maybeRecordUkmLensShown() {
maybeRecordBooleanUkm("ContextMenuAndroid.Shown", "SearchWithGoogleLens");
}
private void maybeRecordUkmLensChipShown(int chipType) {
String actionName = null;
switch (chipType) {
case ChipRenderParams.ChipType.LENS_TRANSLATE_CHIP:
if (!LensUtils.shouldLogUkmByFeature(
ChromeFeatureList.CONTEXT_MENU_TRANSLATE_WITH_GOOGLE_LENS)) {
return;
}
actionName = "TranslateWithGoogleLensChip";
break;
default:
// Unreachable value.
assert false : "Invalid chip type provided to callback.";
}
maybeRecordBooleanUkm("ContextMenuAndroid.Shown", actionName);
}
/** Initialize the bridge if not yet created. */
private void initializeUkmRecorderBridge() {
if (mUkmRecorderBridge == null) {
mUkmRecorderBridge = new UkmRecorder.Bridge();
}
}
/**
* Record a boolean UKM if the lens feature is enabled.
* @param eventName The name of the UKM event to record.
* @param metricName The name of the UKM metric to record.
*/
private void maybeRecordBooleanUkm(String eventName, String metricName) {
// Disable UKM reporting when incognito.
if (mItemDelegate.isIncognito()) return;
initializeUkmRecorderBridge();
WebContents webContents = mItemDelegate.getWebContents();
if (webContents != null) {
mUkmRecorderBridge.recordEventWithBooleanMetric(webContents, eventName, metricName);
}
}
/**
* Record a UKM for a menu action if the lens feature is enabled.
* @param eventName The name of the boolean UKM event to record.
* @param actionId The id of the action corresponding the ContextMenuUma.Action enum.
*/
private void maybeRecordActionUkm(String eventName, int actionId) {
// Disable UKM reporting when incognito.
if (mItemDelegate.isIncognito()) return;
initializeUkmRecorderBridge();
WebContents webContents = mItemDelegate.getWebContents();
if (webContents != null) {
mUkmRecorderBridge.recordEventWithIntegerMetric(
webContents, eventName, "Action", actionId);
}
}
/**
* Check if the search by image is supported.
* @param isSrcDownloadableScheme Whether the source url has a downloadable scheme.
* @return True if search by image is supported.
*/
private boolean checkSupportsGoogleSearchByImage(boolean isSrcDownloadableScheme) {
final TemplateUrlService templateUrlServiceInstance = getTemplateUrlService();
return isSrcDownloadableScheme
&& templateUrlServiceInstance.isLoaded()
&& templateUrlServiceInstance.isSearchByImageAvailable()
&& templateUrlServiceInstance.getDefaultSearchEngineTemplateUrl() != null
&& !LocaleManager.getInstance().needToCheckForSearchEnginePromo();
}
/** Returns the profile of the current tab via the item delegate. */
private Profile getProfile() {
return Profile.fromWebContents(mItemDelegate.getWebContents());
}
private boolean enableShareFromContextMenu() {
return ShareUtils.enableShareForAutomotive(
mMode == ContextMenuMode.CUSTOM_TAB || mMode == ContextMenuMode.WEB_APP);
}
}