blob: 47df90413275c52b46e9e753df9b209379c86c50 [file] [log] [blame]
// 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.
#include "chrome/browser/ui/autofill/autofill_suggestion_controller_utils.h"
#include <string>
#include <variant>
#include <vector>
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/core/browser/metrics/autofill_metrics.h"
#include "components/autofill/core/browser/suggestions/suggestion.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/dense_set.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/password_manager/content/browser/content_password_manager_driver.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#if !BUILDFLAG(IS_ANDROID)
// UserEducationService is not implemented on Android.
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "chrome/browser/user_education/user_education_service.h"
#endif // !BUILDFLAG(IS_ANDROID)
namespace autofill {
bool IsAcceptableSuggestionType(SuggestionType id) {
using enum SuggestionType;
static constexpr auto kUnacceptableItemIds =
DenseSet({kSeparator, kInsecureContextPaymentDisabledMessage,
kMixedFormMessage, kTitle});
return !kUnacceptableItemIds.contains(id);
}
bool IsFooterSuggestionType(SuggestionType type) {
switch (type) {
case SuggestionType::kAllLoyaltyCardsEntry:
case SuggestionType::kAllSavedPasswordsEntry:
case SuggestionType::kFreeformFooter:
case SuggestionType::kManageAddress:
case SuggestionType::kManageAutofillAi:
case SuggestionType::kManageCreditCard:
case SuggestionType::kManageIban:
case SuggestionType::kManageLoyaltyCard:
case SuggestionType::kManagePlusAddress:
case SuggestionType::kScanCreditCard:
case SuggestionType::kSeePromoCodeDetails:
case SuggestionType::kUndoOrClear:
case SuggestionType::kViewPasswordDetails:
case SuggestionType::kPendingStateSignin:
return true;
case SuggestionType::kAccountStoragePasswordEntry:
case SuggestionType::kAddressEntry:
case SuggestionType::kAddressEntryOnTyping:
case SuggestionType::kAddressFieldByFieldFilling:
case SuggestionType::kAutocompleteEntry:
case SuggestionType::kComposeResumeNudge:
case SuggestionType::kComposeProactiveNudge:
case SuggestionType::kComposeDisable:
case SuggestionType::kComposeGoToSettings:
case SuggestionType::kComposeNeverShowOnThisSiteAgain:
case SuggestionType::kComposeSavedStateNotification:
case SuggestionType::kCreateNewPlusAddress:
case SuggestionType::kCreateNewPlusAddressInline:
case SuggestionType::kCreditCardEntry:
case SuggestionType::kDatalistEntry:
case SuggestionType::kDevtoolsTestAddressByCountry:
case SuggestionType::kDevtoolsTestAddressEntry:
case SuggestionType::kDevtoolsTestAddresses:
case SuggestionType::kFillExistingPlusAddress:
case SuggestionType::kFillPassword:
case SuggestionType::kGeneratePasswordEntry:
case SuggestionType::kIbanEntry:
case SuggestionType::kInsecureContextPaymentDisabledMessage:
case SuggestionType::kLoyaltyCardEntry:
case SuggestionType::kMerchantPromoCodeEntry:
case SuggestionType::kMixedFormMessage:
case SuggestionType::kPasswordEntry:
case SuggestionType::kBackupPasswordEntry:
case SuggestionType::kTroubleSigningInEntry:
case SuggestionType::kPasswordFieldByFieldFilling:
case SuggestionType::kPlusAddressError:
case SuggestionType::kSaveAndFillCreditCardEntry:
case SuggestionType::kSeparator:
case SuggestionType::kTitle:
case SuggestionType::kVirtualCreditCardEntry:
case SuggestionType::kIdentityCredential:
case SuggestionType::kWebauthnCredential:
case SuggestionType::kFillAutofillAi:
case SuggestionType::kBnplEntry:
case SuggestionType::kOneTimePasswordEntry:
return false;
case SuggestionType::kWebauthnSignInWithAnotherDevice:
// The hybrid item is reintroduced as a footer.
#if !BUILDFLAG(IS_ANDROID)
return base::FeatureList::IsEnabled(
password_manager::features::
kAutofillReintroduceHybridPasskeyDropdownItem);
#else
return false;
#endif // !BUILDFLAG(IS_ANDROID)
}
}
bool IsFooterItem(const std::vector<Suggestion>& suggestions,
size_t line_number) {
if (line_number >= suggestions.size()) {
return false;
}
// Separators are a special case: They belong into the footer iff the next
// item exists and is a footer item.
SuggestionType type = suggestions[line_number].type;
return type == SuggestionType::kSeparator
? IsFooterItem(suggestions, line_number + 1)
: IsFooterSuggestionType(type);
}
bool IsStandaloneSuggestionType(SuggestionType type) {
return !IsFooterSuggestionType(type) ||
(type == SuggestionType::kScanCreditCard);
}
content::RenderFrameHost* GetRenderFrameHost(
AutofillSuggestionDelegate& delegate) {
return std::visit(
absl::Overload{
[](AutofillDriver* driver) {
return static_cast<ContentAutofillDriver*>(driver)
->render_frame_host();
},
[](password_manager::PasswordManagerDriver* driver) {
return static_cast<password_manager::ContentPasswordManagerDriver*>(
driver)
->render_frame_host();
}},
delegate.GetDriver());
}
bool IsAncestorOf(content::RenderFrameHost* ancestor,
content::RenderFrameHost* descendant) {
for (auto* rfh = descendant; rfh; rfh = rfh->GetParent()) {
if (rfh == ancestor) {
return true;
}
}
return false;
}
bool IsPointerLocked(content::WebContents* web_contents) {
content::RenderFrameHost* rfh;
content::RenderWidgetHostView* rwhv;
return web_contents && (rfh = web_contents->GetFocusedFrame()) &&
(rwhv = rfh->GetView()) && rwhv->IsPointerLocked();
}
void NotifyUserEducationAboutAcceptedSuggestion(content::WebContents* contents,
const Suggestion& suggestion) {
#if BUILDFLAG(IS_ANDROID)
if (suggestion.iph_metadata.feature) {
using IphEventPair = std::pair<const base::Feature*, const char*>;
static const auto kIphFeatures = std::to_array<IphEventPair>(
{IphEventPair{&feature_engagement::kIPHAutofillCreditCardBenefitFeature,
"autofill_credit_card_benefit_iph_accepted"},
IphEventPair{&feature_engagement::
kIPHAutofillExternalAccountProfileSuggestionFeature,
"autofill_external_account_profile_suggestion_accepted"},
IphEventPair{
&feature_engagement::kIPHAutofillVirtualCardSuggestionFeature,
"autofill_virtual_card_suggestion_accepted"},
IphEventPair{&feature_engagement::
kIPHAutofillCardInfoRetrievalSuggestionFeature,
"autofill_card_info_retrieval_suggestion_accepted"},
IphEventPair{&feature_engagement::
kIPHAutofillDisabledVirtualCardSuggestionFeature,
"autofill_disabled_virtual_card_suggestion_accepted"},
IphEventPair{
&feature_engagement::kIPHAutofillVirtualCardCVCSuggestionFeature,
"autofill_virtual_card_cvc_suggestion_accepted"}});
if (auto it =
std::ranges::find(kIphFeatures, suggestion.iph_metadata.feature,
&IphEventPair::first);
it != kIphFeatures.end()) {
feature_engagement::TrackerFactory::GetForBrowserContext(
contents->GetBrowserContext())
->NotifyEvent(it->second);
}
}
#else
if (suggestion.iph_metadata.feature) {
if (auto* interface =
BrowserUserEducationInterface::MaybeGetForWebContentsInTab(
contents)) {
interface->NotifyFeaturePromoFeatureUsed(
*suggestion.iph_metadata.feature,
FeaturePromoFeatureUsedAction::kClosePromoIfPresent);
}
}
if (suggestion.feature_for_new_badge &&
suggestion.feature_for_new_badge != suggestion.iph_metadata.feature) {
UserEducationService::MaybeNotifyNewBadgeFeatureUsed(
contents->GetBrowserContext(), *suggestion.feature_for_new_badge);
}
#endif
}
std::vector<Suggestion> UpdateSuggestionsFromDataList(
base::span<const SelectOption> options,
std::vector<Suggestion> suggestions) {
// Remove all the old data list values, which should always be at the top of
// the list if they are present.
std::erase_if(suggestions, [](const Suggestion& suggestion) {
return suggestion.type == SuggestionType::kDatalistEntry;
});
// If there are no new data list values, exit (clearing the separator if there
// is one).
if (options.empty()) {
if (!suggestions.empty() &&
suggestions[0].type == SuggestionType::kSeparator) {
suggestions.erase(suggestions.begin());
}
return suggestions;
}
AutofillMetrics::LogDataListSuggestionsUpdated();
// Add a separator if there are any other values.
if (!suggestions.empty() &&
suggestions[0].type != SuggestionType::kSeparator) {
suggestions.insert(suggestions.begin(),
Suggestion(SuggestionType::kSeparator));
}
// Prepend the parameters to the suggestions we already have.
suggestions.insert(suggestions.begin(), options.size(),
Suggestion(SuggestionType::kDatalistEntry));
for (size_t i = 0; i < options.size(); i++) {
suggestions[i].main_text =
Suggestion::Text(options[i].value, Suggestion::Text::IsPrimary(true));
suggestions[i].labels = {{Suggestion::Text(options[i].text)}};
}
return suggestions;
}
} // namespace autofill