| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/autofill/autofill_context_menu_manager.h" |
| |
| #include <algorithm> |
| #include <string> |
| |
| #include "base/feature_list.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/notreached.h" |
| #include "base/values.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/feedback/show_feedback_page.h" |
| #include "chrome/browser/password_manager/chrome_password_manager_client.h" |
| #include "chrome/browser/password_manager/factories/password_counter_factory.h" |
| #include "chrome/browser/plus_addresses/plus_address_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/passwords/ui_utils.h" |
| #include "chrome/browser/ui/webauthn/context_menu_helper.h" |
| #include "chrome/browser/user_education/user_education_service.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/autofill/content/browser/content_autofill_client.h" |
| #include "components/autofill/content/browser/content_autofill_driver.h" |
| #include "components/autofill/core/browser/autofill_feedback_data.h" |
| #include "components/autofill/core/browser/foundations/autofill_driver.h" |
| #include "components/autofill/core/browser/foundations/autofill_manager.h" |
| #include "components/autofill/core/browser/integrators/autofill_ai/autofill_ai_manager.h" |
| #include "components/autofill/core/common/aliases.h" |
| #include "components/password_manager/content/browser/content_password_manager_driver.h" |
| #include "components/password_manager/core/browser/features/password_features.h" |
| #include "components/password_manager/core/browser/password_autofill_manager.h" |
| #include "components/password_manager/core/browser/password_counter.h" |
| #include "components/password_manager/core/browser/password_manager_client.h" |
| #include "components/password_manager/core/browser/password_manager_util.h" |
| #include "components/password_manager/core/browser/password_manual_fallback_metrics_recorder.h" |
| #include "components/password_manager/core/common/password_manager_pref_names.h" |
| #include "components/plus_addresses/core/browser/grit/plus_addresses_strings.h" |
| #include "components/plus_addresses/core/browser/plus_address_service.h" |
| #include "components/plus_addresses/core/common/features.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/renderer_context_menu/render_view_context_menu_base.h" |
| #include "components/variations/service/variations_service.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/image_model.h" |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| #include "components/plus_addresses/core/browser/resources/vector_icons.h" |
| #endif |
| |
| namespace autofill { |
| |
| namespace { |
| |
| using ::password_manager::ContentPasswordManagerDriver; |
| |
| constexpr char kFeedbackPlaceholder[] = |
| "What steps did you just take?\n" |
| "(1)\n" |
| "(2)\n" |
| "(3)\n" |
| "\n" |
| "What was the expected result?\n" |
| "\n" |
| "What happened instead? (Please include the screenshot below)"; |
| |
| // Constant determining the icon size in the context menu. |
| constexpr int kContextMenuIconSize = 16; |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| const gfx::VectorIcon& kPlusAddressLogoIcon = |
| plus_addresses::kPlusAddressLogoSmallIcon; |
| #else |
| const gfx::VectorIcon& kPlusAddressLogoIcon = vector_icons::kEmailIcon; |
| #endif |
| |
| bool ShouldShowAutofillContextMenu(const content::ContextMenuParams& params) { |
| if (!params.form_control_type) { |
| return false; |
| } |
| // Return true (only) on text fields. |
| // |
| // Note that this switch is over `blink::mojom::FormControlType`, not |
| // `autofill::FormControlType`. Therefore, it does not handle |
| // `autofill::FormControlType::kContentEditable`, which is covered by the |
| // above if-condition `!params.form_control_type`. |
| // |
| // TODO(crbug.com/40285492): Unify with functions from form_autofill_util.cc. |
| switch (*params.form_control_type) { |
| case blink::mojom::FormControlType::kInputEmail: |
| case blink::mojom::FormControlType::kInputMonth: |
| case blink::mojom::FormControlType::kInputNumber: |
| case blink::mojom::FormControlType::kInputPassword: |
| case blink::mojom::FormControlType::kInputSearch: |
| case blink::mojom::FormControlType::kInputTelephone: |
| case blink::mojom::FormControlType::kInputText: |
| case blink::mojom::FormControlType::kInputUrl: |
| case blink::mojom::FormControlType::kTextArea: |
| return true; |
| case blink::mojom::FormControlType::kButtonButton: |
| case blink::mojom::FormControlType::kButtonSubmit: |
| case blink::mojom::FormControlType::kButtonReset: |
| case blink::mojom::FormControlType::kButtonPopover: |
| case blink::mojom::FormControlType::kFieldset: |
| case blink::mojom::FormControlType::kInputButton: |
| case blink::mojom::FormControlType::kInputCheckbox: |
| case blink::mojom::FormControlType::kInputColor: |
| case blink::mojom::FormControlType::kInputDate: |
| case blink::mojom::FormControlType::kInputDatetimeLocal: |
| case blink::mojom::FormControlType::kInputFile: |
| case blink::mojom::FormControlType::kInputHidden: |
| case blink::mojom::FormControlType::kInputImage: |
| case blink::mojom::FormControlType::kInputRadio: |
| case blink::mojom::FormControlType::kInputRange: |
| case blink::mojom::FormControlType::kInputReset: |
| case blink::mojom::FormControlType::kInputSubmit: |
| case blink::mojom::FormControlType::kInputTime: |
| case blink::mojom::FormControlType::kInputWeek: |
| case blink::mojom::FormControlType::kOutput: |
| case blink::mojom::FormControlType::kSelectOne: |
| case blink::mojom::FormControlType::kSelectMultiple: |
| return false; |
| } |
| NOTREACHED(); |
| } |
| |
| // Returns true if the given id is one generated for autofill context menu. |
| bool IsAutofillCustomCommandId( |
| AutofillContextMenuManager::CommandId command_id) { |
| static constexpr auto kAutofillCommands = base::MakeFixedFlatSet<int>({ |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PLUS_ADDRESS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_USE_PASSKEY_FROM_ANOTHER_DEVICE, |
| }); |
| return kAutofillCommands.contains(command_id.value()); |
| } |
| |
| bool IsLikelyDogfoodClient() { |
| auto* variations_service = g_browser_process->variations_service(); |
| if (!variations_service) { |
| return false; |
| } |
| return variations_service->IsLikelyDogfoodClient(); |
| } |
| |
| // Returns true if the field is a username or password field. |
| bool IsPasswordFormField(ContentPasswordManagerDriver& password_manager_driver, |
| const content::ContextMenuParams& params) { |
| const autofill::FieldRendererId current_field_renderer_id( |
| params.field_renderer_id); |
| return password_manager_driver.GetPasswordManager() |
| ->GetPasswordFormCache() |
| ->GetPasswordForm(&password_manager_driver, current_field_renderer_id); |
| } |
| |
| base::Value::Dict LoadTriggerFormAndFieldLogs( |
| AutofillManager& manager, |
| const LocalFrameToken& frame_token, |
| const content::ContextMenuParams& params) { |
| if (!ShouldShowAutofillContextMenu(params)) { |
| return base::Value::Dict(); |
| } |
| |
| FormGlobalId form_global_id = {frame_token, |
| FormRendererId(params.form_renderer_id)}; |
| |
| base::Value::Dict trigger_form_logs; |
| if (FormStructure* form = manager.FindCachedFormById(form_global_id)) { |
| trigger_form_logs.Set("triggerFormSignature", form->FormSignatureAsStr()); |
| |
| if (params.form_control_type) { |
| FieldGlobalId field_global_id = { |
| frame_token, FieldRendererId(params.field_renderer_id)}; |
| auto field = |
| std::ranges::find_if(*form, [&field_global_id](const auto& field) { |
| return field->global_id() == field_global_id; |
| }); |
| if (field != form->end()) { |
| trigger_form_logs.Set("triggerFieldSignature", |
| (*field)->FieldSignatureAsStr()); |
| } |
| } |
| } |
| return trigger_form_logs; |
| } |
| |
| } // namespace |
| |
| AutofillContextMenuManager::AutofillContextMenuManager( |
| RenderViewContextMenuBase* delegate, |
| ui::SimpleMenuModel* menu_model) |
| : menu_model_(menu_model), delegate_(delegate) { |
| DCHECK(delegate_); |
| params_ = delegate_->params(); |
| } |
| |
| AutofillContextMenuManager::~AutofillContextMenuManager() = default; |
| |
| void AutofillContextMenuManager::AppendItems() { |
| if (base::FeatureList::IsEnabled( |
| password_manager::features::kPasswordManualFallbackAvailable)) { |
| MaybeAddAutofillManualFallbackItems(); |
| MaybeAddAutofillFeedbackItem(); |
| } else { |
| MaybeAddAutofillFeedbackItem(); |
| MaybeAddAutofillManualFallbackItems(); |
| } |
| } |
| |
| bool AutofillContextMenuManager::IsCommandIdSupported(int command_id) { |
| return IsAutofillCustomCommandId(CommandId(command_id)); |
| } |
| |
| bool AutofillContextMenuManager::IsCommandIdEnabled(int command_id) { |
| return true; |
| } |
| |
| void AutofillContextMenuManager::ExecuteCommand(int command_id) { |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| if (!rfh) { |
| return; |
| } |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_USE_PASSKEY_FROM_ANOTHER_DEVICE) { |
| webauthn::OnPasskeyFromAnotherDeviceContextMenuItemSelected(rfh); |
| return; |
| } |
| ContentAutofillDriver* autofill_driver = |
| ContentAutofillDriver::GetForRenderFrameHost(rfh); |
| if (!autofill_driver) { |
| return; |
| } |
| CHECK(IsAutofillCustomCommandId(CommandId(command_id))); |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK) { |
| ExecuteAutofillFeedbackCommand(autofill_driver->GetFrameToken(), |
| autofill_driver->GetAutofillManager()); |
| return; |
| } |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PLUS_ADDRESS) { |
| ExecuteFallbackForPlusAddressesCommand(*autofill_driver); |
| return; |
| } |
| |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD) { |
| ExecuteFallbackForSelectPasswordCommand(*autofill_driver); |
| return; |
| } |
| |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS) { |
| // This function also records metrics. |
| NavigateToManagePasswordsPage( |
| chrome::FindBrowserWithTab(web_contents), |
| password_manager::ManagePasswordsReferrer::kPasswordContextMenu); |
| return; |
| } |
| |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD) { |
| // This function also records metrics. |
| password_manager_util::UserTriggeredManualGenerationFromContextMenu( |
| ChromePasswordManagerClient::FromWebContents(web_contents), |
| autofill::ContentAutofillClient::FromWebContents(web_contents)); |
| return; |
| } |
| } |
| |
| void AutofillContextMenuManager::MaybeAddAutofillFeedbackItem() { |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| if (!rfh) { |
| return; |
| } |
| |
| ContentAutofillDriver* autofill_driver = |
| ContentAutofillDriver::GetForRenderFrameHost(rfh); |
| // Do not show autofill context menu options for input fields that cannot be |
| // filled by the driver. See crbug.com/1367547. |
| if (!autofill_driver || !autofill_driver->CanShowAutofillUi()) { |
| return; |
| } |
| |
| // Includes the option of submitting feedback on Autofill. |
| if (autofill_driver->GetAutofillClient().IsAutofillEnabled() && |
| IsLikelyDogfoodClient()) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, |
| ui::ImageModel::FromVectorIcon(vector_icons::kDogfoodIcon)); |
| |
| menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| } |
| |
| void AutofillContextMenuManager::MaybeAddAutofillManualFallbackItems() { |
| if (!ShouldShowAutofillContextMenu(params_)) { |
| // Autofill entries are only available in input or text area fields |
| return; |
| } |
| |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| if (!rfh) { |
| return; |
| } |
| |
| ContentAutofillDriver* autofill_driver = |
| ContentAutofillDriver::GetForRenderFrameHost(rfh); |
| ContentPasswordManagerDriver* password_manager_driver = |
| ContentPasswordManagerDriver::GetForRenderFrameHost(rfh); |
| |
| bool add_plus_address_fallback = false; |
| bool add_passwords_fallback = false; |
| |
| // Do not show autofill context menu options for input fields that cannot be |
| // filled by the driver. See crbug.com/1367547. |
| if (autofill_driver && autofill_driver->CanShowAutofillUi()) { |
| add_plus_address_fallback = |
| ShouldAddPlusAddressManualFallbackItem(*autofill_driver); |
| } |
| |
| // Do not show password manager context menu options for input fields that |
| // cannot be filled by the driver. See crbug.com/1367547. |
| if (password_manager_driver && password_manager_driver->CanShowAutofillUi()) { |
| add_passwords_fallback = |
| ShouldAddPasswordsManualFallbackItem(*password_manager_driver); |
| } |
| |
| if (!add_plus_address_fallback && !add_passwords_fallback) { |
| return; |
| } |
| |
| if (add_passwords_fallback) { |
| Profile* profile = Profile::FromBrowserContext(rfh->GetBrowserContext()); |
| password_manager::PasswordCounter* counter = |
| PasswordCounterFactory::GetForProfile(profile); |
| const bool select_passwords_option_shown = |
| counter && counter->autofillable_passwords() > 0; |
| AddPasswordsManualFallbackItems(*password_manager_driver, |
| select_passwords_option_shown); |
| |
| if (select_passwords_option_shown) { |
| LogSelectPasswordManualFallbackContextMenuEntryShown( |
| CHECK_DEREF(password_manager_driver)); |
| } |
| } |
| if (add_plus_address_fallback) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PLUS_ADDRESS, |
| IDS_PLUS_ADDRESS_FALLBACK_LABEL_CONTEXT_MENU, |
| ui::ImageModel::FromVectorIcon(kPlusAddressLogoIcon, ui::kColorIcon, |
| kContextMenuIconSize)); |
| MaybeMarkLastItemAsNewFeature( |
| plus_addresses::features::kPlusAddressFallbackFromContextMenu); |
| // TODO(crbug.com/327566698): Log metrics for plus address fallbacks, too. |
| } |
| menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| |
| bool AutofillContextMenuManager::ShouldAddPlusAddressManualFallbackItem( |
| ContentAutofillDriver& autofill_driver) { |
| if (params_.form_control_type && |
| params_.form_control_type.value() == |
| blink::mojom::FormControlType::kInputPassword) { |
| return false; |
| } |
| |
| auto* web_contents = content::WebContents::FromRenderFrameHost( |
| autofill_driver.render_frame_host()); |
| const plus_addresses::PlusAddressService* plus_address_service = |
| PlusAddressServiceFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext()); |
| AutofillClient& client = autofill_driver.GetAutofillManager().client(); |
| return plus_address_service && |
| plus_address_service->ShouldShowManualFallback( |
| client.GetLastCommittedPrimaryMainFrameOrigin(), |
| client.IsOffTheRecord()) && |
| base::FeatureList::IsEnabled( |
| plus_addresses::features::kPlusAddressFallbackFromContextMenu); |
| } |
| |
| bool AutofillContextMenuManager::ShouldAddPasswordsManualFallbackItem( |
| ContentPasswordManagerDriver& password_manager_driver) { |
| // Password suggestions should not be triggered on text areas. |
| if (params_.form_control_type == blink::mojom::FormControlType::kTextArea) { |
| return false; |
| } |
| |
| return password_manager_driver.GetPasswordManager() |
| ->GetClient() |
| ->IsFillingEnabled( |
| password_manager_driver.GetLastCommittedURL()) && |
| base::FeatureList::IsEnabled( |
| password_manager::features::kPasswordManualFallbackAvailable); |
| } |
| |
| void AutofillContextMenuManager::AddPasswordsManualFallbackItems( |
| ContentPasswordManagerDriver& password_manager_driver, |
| bool add_select_password_option) { |
| const bool add_password_generation_option = |
| password_manager_util::ManualPasswordGenerationEnabled( |
| &password_manager_driver) && |
| password_manager_driver.IsPasswordFieldForPasswordManager( |
| autofill::FieldRendererId(params_.field_renderer_id), |
| params_.form_control_type); |
| const bool add_passkey_from_another_device_option = |
| webauthn::IsPasskeyFromAnotherDeviceContextMenuEnabled( |
| delegate_->GetRenderFrameHost(), params_.form_renderer_id, |
| params_.field_renderer_id) && |
| base::FeatureList::IsEnabled( |
| password_manager::features:: |
| kWebAuthnUsePasskeyFromAnotherDeviceInContextMenu); |
| const bool add_import_passwords_option = !add_select_password_option; |
| |
| if (add_select_password_option) { |
| menu_model_->AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD); |
| MaybeMarkLastItemAsNewFeature( |
| password_manager::features::kPasswordManualFallbackAvailable); |
| } |
| if (add_password_generation_option) { |
| menu_model_->AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD); |
| } |
| if (add_passkey_from_another_device_option) { |
| menu_model_->AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_USE_PASSKEY_FROM_ANOTHER_DEVICE, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_USE_PASSKEY_FROM_ANOTHER_DEVICE); |
| } |
| if (add_import_passwords_option) { |
| menu_model_->AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS); |
| } |
| } |
| |
| void AutofillContextMenuManager:: |
| LogSelectPasswordManualFallbackContextMenuEntryShown( |
| ContentPasswordManagerDriver& password_manager_driver) { |
| password_manager_driver.GetPasswordAutofillManager() |
| ->GetPasswordManualFallbackMetricsRecorder() |
| .ContextMenuEntryShown( |
| /*classified_as_target_filling_password=*/ |
| IsPasswordFormField(password_manager_driver, params_)); |
| } |
| |
| void AutofillContextMenuManager:: |
| LogSelectPasswordManualFallbackContextMenuEntryAccepted() { |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| ContentPasswordManagerDriver* password_manager_driver = |
| rfh ? ContentPasswordManagerDriver::GetForRenderFrameHost(rfh) : nullptr; |
| |
| if (password_manager_driver) { |
| password_manager_driver->GetPasswordAutofillManager() |
| ->GetPasswordManualFallbackMetricsRecorder() |
| .ContextMenuEntryAccepted(/*classified_as_target_filling_password=*/ |
| IsPasswordFormField(*password_manager_driver, |
| params_)); |
| } |
| } |
| |
| void AutofillContextMenuManager::ExecuteAutofillFeedbackCommand( |
| const LocalFrameToken& frame_token, |
| AutofillManager& manager) { |
| // The cast is safe since the context menu is only available on Desktop. |
| auto& client = static_cast<ContentAutofillClient&>(manager.client()); |
| Browser* browser = chrome::FindBrowserWithTab(&client.GetWebContents()); |
| chrome::ShowFeedbackPage( |
| browser, feedback::kFeedbackSourceAutofillContextMenu, |
| /*description_template=*/std::string(), |
| /*description_placeholder_text=*/kFeedbackPlaceholder, |
| /*category_tag=*/"dogfood_autofill_feedback", |
| /*extra_diagnostics=*/std::string(), |
| /*autofill_metadata=*/ |
| data_logs::FetchAutofillFeedbackData( |
| &manager, |
| LoadTriggerFormAndFieldLogs(manager, frame_token, params_))); |
| } |
| |
| void AutofillContextMenuManager::ExecuteFallbackForPlusAddressesCommand( |
| AutofillDriver& autofill_driver) { |
| autofill_driver.RendererShouldTriggerSuggestions( |
| /*field_id=*/{autofill_driver.GetFrameToken(), |
| FieldRendererId(params_.field_renderer_id)}, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses); |
| |
| base::RecordAction(base::UserMetricsAction( |
| "PlusAddresses.ManualFallbackDesktopContextManualFallbackSelected")); |
| UserEducationService::MaybeNotifyNewBadgeFeatureUsed( |
| delegate_->GetBrowserContext(), |
| plus_addresses::features::kPlusAddressFallbackFromContextMenu); |
| } |
| |
| void AutofillContextMenuManager::ExecuteFallbackForSelectPasswordCommand( |
| AutofillDriver& autofill_driver) { |
| autofill_driver.RendererShouldTriggerSuggestions( |
| /*field_id=*/{autofill_driver.GetFrameToken(), |
| FieldRendererId(params_.field_renderer_id)}, |
| AutofillSuggestionTriggerSource::kManualFallbackPasswords); |
| |
| LogSelectPasswordManualFallbackContextMenuEntryAccepted(); |
| UserEducationService::MaybeNotifyNewBadgeFeatureUsed( |
| delegate_->GetBrowserContext(), |
| password_manager::features::kPasswordManualFallbackAvailable); |
| } |
| |
| void AutofillContextMenuManager::MaybeMarkLastItemAsNewFeature( |
| const base::Feature& feature) { |
| menu_model_->SetIsNewFeatureAt(menu_model_->GetItemCount() - 1, |
| UserEducationService::MaybeShowNewBadge( |
| delegate_->GetBrowserContext(), feature)); |
| } |
| |
| } // namespace autofill |