// 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/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "components/autofill/core/browser/autofill_browser_util.h"
#include "components/autofill/core/browser/autofill_experiments.h"
#include "components/autofill/core/browser/autofill_manager.h"
#include "components/autofill/core/browser/browser_autofill_manager.h"
#include "components/autofill/core/browser/data_model/credit_card.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/form_types.h"
#include "components/autofill/core/browser/logging/log_manager.h"
#include "components/autofill/core/browser/payments/iban_access_manager.h"
#include "components/autofill/core/browser/payments/payments_autofill_client.h"
#include "components/autofill/core/browser/payments_data_manager.h"
#include "components/autofill/core/browser/payments_suggestion_generator.h"
#include "components/autofill/core/browser/ui/fast_checkout_client.h"
#include "components/autofill/core/browser/ui/suggestion_hiding_reason.h"
#include "components/autofill/core/browser/ui/suggestion_type.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/autofill_internals/log_message.h"
#include "components/autofill/core/common/autofill_internals/logging_scope.h"
#include "components/autofill/core/common/autofill_payments_features.h"
#include "components/autofill/core/common/autofill_util.h"
#include "components/autofill/core/common/logging/log_macros.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
namespace autofill {
namespace {
// Checks if the field is focusable and empty.
bool IsFieldFocusableAndEmpty(const FormData& received_form,
FieldGlobalId field_id) {
// form_field->value extracted from FormData represents field's *current*
// value, not the original value.
const FormFieldData* form_field = received_form.FindFieldByGlobalId(field_id);
return form_field && form_field->IsFocusable() &&
SanitizedFieldIsEmpty(form_field->value());
}
bool IsTriggeredOnIbanField(const FormStructure* form_field,
const FormFieldData& field) {
if (!form_field) {
return false;
}
const autofill::AutofillField* autofill_field =
form_field->GetFieldById(field.global_id());
return autofill_field &&
autofill_field->Type().group() == FieldTypeGroup::kIban;
}
} // namespace
TouchToFillDelegateAndroidImpl::DryRunResult::DryRunResult(
TriggerOutcome outcome,
absl::variant<std::vector<CreditCard>, std::vector<Iban>> items_to_suggest)
: outcome(outcome), items_to_suggest(std::move(items_to_suggest)) {}
TouchToFillDelegateAndroidImpl::DryRunResult::DryRunResult(DryRunResult&&) =
default;
TouchToFillDelegateAndroidImpl::DryRunResult&
TouchToFillDelegateAndroidImpl::DryRunResult::operator=(DryRunResult&&) =
default;
TouchToFillDelegateAndroidImpl::DryRunResult::~DryRunResult() = default;
TouchToFillDelegateAndroidImpl::TouchToFillDelegateAndroidImpl(
BrowserAutofillManager* manager)
: manager_(manager) {
DCHECK(manager);
}
TouchToFillDelegateAndroidImpl::~TouchToFillDelegateAndroidImpl() {
// Invalidate pointers to avoid post hide callbacks.
weak_ptr_factory_.InvalidateWeakPtrs();
HideTouchToFill();
}
// TODO(crbug.com/40282650): Remove received FormData
TouchToFillDelegateAndroidImpl::DryRunResult
TouchToFillDelegateAndroidImpl::DryRun(FormGlobalId form_id,
FieldGlobalId field_id,
const FormData& received_form) {
// Trigger only on supported platforms.
if (!IsTouchToFillPaymentMethodSupported()) {
return {TriggerOutcome::kUnsupportedFieldType, {}};
}
const FormStructure* form = manager_->FindCachedFormById(form_id);
if (!form) {
return {TriggerOutcome::kUnknownForm, {}};
}
const AutofillField* field = form->GetFieldById(field_id);
if (!field) {
return {TriggerOutcome::kUnknownField, {}};
}
// Trigger only if not shown before.
if (ttf_payment_method_state_ != TouchToFillState::kShouldShow) {
return {TriggerOutcome::kShownBefore, {}};
}
// Trigger only if the client and the form are not insecure.
if (IsFormOrClientNonSecure(manager_->client(), *form)) {
return {TriggerOutcome::kFormOrClientNotSecure, {}};
}
// Trigger only on focusable empty field.
if (!IsFieldFocusableAndEmpty(received_form, field_id)) {
return {TriggerOutcome::kFieldNotEmptyOrNotFocusable, {}};
}
// Trigger only if the UI is available.
if (!manager_->CanShowAutofillUi()) {
return {TriggerOutcome::kCannotShowAutofillUi, {}};
}
if (field->Type().group() == FieldTypeGroup::kIban) {
return DryRunForIban();
} else if (field->Type().group() == FieldTypeGroup::kCreditCard) {
return DryRunForCreditCard(*field, *form, received_form);
}
return {TriggerOutcome::kUnsupportedFieldType, {}};
}
TouchToFillDelegateAndroidImpl::DryRunResult
TouchToFillDelegateAndroidImpl::DryRunForIban() {
PersonalDataManager* pdm = manager_->client().GetPersonalDataManager();
CHECK(pdm);
std::vector<Iban> ibans_to_suggest =
pdm->payments_data_manager().GetOrderedIbansToSuggest();
return ibans_to_suggest.empty() || !base::FeatureList::IsEnabled(
features::kAutofillEnableLocalIban)
? DryRunResult(TriggerOutcome::kNoValidPaymentMethods, {})
: DryRunResult(TriggerOutcome::kShown,
std::move(ibans_to_suggest));
}
TouchToFillDelegateAndroidImpl::DryRunResult
TouchToFillDelegateAndroidImpl::DryRunForCreditCard(
const AutofillField& field,
const FormStructure& form,
const FormData& received_form) {
// Trigger only for complete forms (containing the fields for the card number
// and the card expiration date).
if (!FormHasAllCreditCardFields(form)) {
return {TriggerOutcome::kIncompleteForm, {}};
}
if (IsFormPrefilled(received_form)) {
return {TriggerOutcome::kFormAlreadyFilled, {}};
}
// Trigger only if Fast Checkout was not shown before.
if (!manager_->client().GetFastCheckoutClient()->IsNotShownYet()) {
return {TriggerOutcome::kFastCheckoutWasShown, {}};
}
// Fetch all complete valid credit cards on file.
// Complete = contains number, expiration date and name on card.
// Valid = unexpired with valid number format.
// TODO(crbug.com/40227496): `*field` must contain the updated field
// information.
std::vector<CreditCard> cards_to_suggest = GetTouchToFillCardsToSuggest(
manager_->client(), field, field.Type().GetStorableType());
return cards_to_suggest.empty()
? DryRunResult(TriggerOutcome::kNoValidPaymentMethods, {})
: DryRunResult(TriggerOutcome::kShown,
std::move(cards_to_suggest));
}
// TODO(crbug.com/40282650): Remove received FormData
bool TouchToFillDelegateAndroidImpl::IntendsToShowTouchToFill(
FormGlobalId form_id,
FieldGlobalId field_id,
const FormData& form) {
TriggerOutcome outcome = DryRun(form_id, field_id, form).outcome;
LOG_AF(manager_->client().GetLogManager())
<< LoggingScope::kTouchToFill << LogMessage::kTouchToFill
<< "dry run before parsing for form " << form_id << " and field "
<< field_id << " was " << (outcome == TriggerOutcome::kShown ? "" : "un")
<< "successful (" << base::to_underlying(outcome) << ")";
return outcome == TriggerOutcome::kShown;
}
bool TouchToFillDelegateAndroidImpl::TryToShowTouchToFill(
const FormData& form,
const FormFieldData& field) {
// TODO(crbug.com/40247130): store only FormGlobalId and FieldGlobalId instead
// to avoid that FormData and FormFieldData may become obsolete during the
// bottomsheet being open.
query_form_ = form;
query_field_ = field;
DryRunResult dry_run = DryRun(form.global_id(), field.global_id(), form);
if (dry_run.outcome == TriggerOutcome::kShown) {
if (std::vector<CreditCard>* cards_to_suggest =
absl::get_if<std::vector<CreditCard>>(&dry_run.items_to_suggest);
cards_to_suggest &&
!manager_->client()
.GetPaymentsAutofillClient()
->ShowTouchToFillCreditCard(
GetWeakPtr(), *cards_to_suggest,
GetCreditCardSuggestionsForTouchToFill(*cards_to_suggest,
manager_->client()))) {
dry_run.outcome = TriggerOutcome::kFailedToDisplayBottomSheet;
} else if (std::vector<Iban>* ibans_to_suggest =
absl::get_if<std::vector<Iban>>(&dry_run.items_to_suggest);
ibans_to_suggest &&
(base::FeatureList::IsEnabled(
features::kAutofillSkipAndroidBottomSheetForIban) ||
!manager_->client()
.GetPaymentsAutofillClient()
->ShowTouchToFillIban(GetWeakPtr(),
std::move(*ibans_to_suggest)))) {
dry_run.outcome = TriggerOutcome::kFailedToDisplayBottomSheet;
}
}
if (dry_run.outcome != TriggerOutcome::kUnsupportedFieldType) {
if (IsTriggeredOnIbanField(manager_->FindCachedFormById(form.global_id()),
field)) {
base::UmaHistogramEnumeration(kUmaTouchToFillIbanTriggerOutcome,
dry_run.outcome);
} else {
base::UmaHistogramEnumeration(kUmaTouchToFillCreditCardTriggerOutcome,
dry_run.outcome);
}
}
LOG_AF(manager_->client().GetLogManager())
<< LoggingScope::kTouchToFill << LogMessage::kTouchToFill
<< "dry run after parsing for form " << form.global_id() << " and field "
<< field.global_id() << " was "
<< (dry_run.outcome == TriggerOutcome::kShown ? "" : "un")
<< "successful (" << base::to_underlying(dry_run.outcome) << ")";
if (dry_run.outcome != TriggerOutcome::kShown) {
return false;
}
ttf_payment_method_state_ = TouchToFillState::kIsShowing;
manager_->client().HideAutofillSuggestions(
SuggestionHidingReason::kOverlappingWithTouchToFillSurface);
if (absl::get_if<std::vector<CreditCard>>(&dry_run.items_to_suggest)) {
manager_->DidShowSuggestions(
std::vector<SuggestionType>({SuggestionType::kCreditCardEntry}), form,
field);
} else {
manager_->DidShowSuggestions(
std::vector<SuggestionType>({SuggestionType::kIbanEntry}), form, field);
}
return true;
}
bool TouchToFillDelegateAndroidImpl::IsShowingTouchToFill() {
return ttf_payment_method_state_ == TouchToFillState::kIsShowing;
}
// TODO(crbug.com/40233391): Create a central point for TTF hiding decision.
void TouchToFillDelegateAndroidImpl::HideTouchToFill() {
if (IsShowingTouchToFill()) {
manager_->client()
.GetPaymentsAutofillClient()
->HideTouchToFillPaymentMethod();
}
}
void TouchToFillDelegateAndroidImpl::Reset() {
HideTouchToFill();
ttf_payment_method_state_ = TouchToFillState::kShouldShow;
}
AutofillManager* TouchToFillDelegateAndroidImpl::GetManager() {
return manager_;
}
bool TouchToFillDelegateAndroidImpl::ShouldShowScanCreditCard() {
if (!manager_->client()
.GetPaymentsAutofillClient()
->HasCreditCardScanFeature()) {
return false;
}
return !IsFormOrClientNonSecure(manager_->client(), query_form_);
}
void TouchToFillDelegateAndroidImpl::ScanCreditCard() {
manager_->client().GetPaymentsAutofillClient()->ScanCreditCard(base::BindOnce(
&TouchToFillDelegateAndroidImpl::OnCreditCardScanned, GetWeakPtr()));
}
void TouchToFillDelegateAndroidImpl::OnCreditCardScanned(
const CreditCard& card) {
HideTouchToFill();
manager_->FillOrPreviewCreditCardForm(
mojom::ActionPersistence::kFill, query_form_, query_field_, card,
std::u16string(),
{.trigger_source = AutofillTriggerSource::kTouchToFillCreditCard});
}
void TouchToFillDelegateAndroidImpl::ShowPaymentMethodSettings() {
manager_->client().ShowAutofillSettings(SuggestionType::kManageCreditCard);
}
void TouchToFillDelegateAndroidImpl::CreditCardSuggestionSelected(
std::string unique_id,
bool is_virtual) {
HideTouchToFill();
PersonalDataManager* pdm = manager_->client().GetPersonalDataManager();
CHECK(pdm);
CreditCard* card =
pdm->payments_data_manager().GetCreditCardByGUID(unique_id);
// TODO(crbug.com/40071928): Figure out why `card` is sometimes nullptr.
if (!card) {
return;
}
if (is_virtual) {
// Virtual credit cards are not persisted in Chrome, modify record type
// locally.
manager_->AuthenticateThenFillCreditCardForm(
query_form_, query_field_, CreditCard::CreateVirtualCard(*card),
{.trigger_source = AutofillTriggerSource::kTouchToFillCreditCard});
} else {
manager_->AuthenticateThenFillCreditCardForm(
query_form_, query_field_, *card,
{.trigger_source = AutofillTriggerSource::kTouchToFillCreditCard});
}
}
void TouchToFillDelegateAndroidImpl::IbanSuggestionSelected(
absl::variant<Iban::Guid, Iban::InstrumentId> backend_id) {
HideTouchToFill();
manager_->client()
.GetPaymentsAutofillClient()
->GetIbanAccessManager()
->FetchValue(
absl::holds_alternative<Iban::Guid>(backend_id)
? Suggestion::BackendId(
Suggestion::Guid(absl::get<Iban::Guid>(backend_id).value()))
: Suggestion::BackendId(Suggestion::InstrumentId(
absl::get<Iban::InstrumentId>(backend_id).value())),
base::BindOnce(
[](base::WeakPtr<TouchToFillDelegateAndroidImpl> delegate,
const std::u16string& value) {
if (delegate) {
delegate->manager_->FillOrPreviewField(
mojom::ActionPersistence::kFill,
mojom::FieldActionType::kReplaceAll,
delegate->query_form_, delegate->query_field_, value,
SuggestionType::kIbanEntry, IBAN_VALUE);
}
},
GetWeakPtr()));
}
void TouchToFillDelegateAndroidImpl::OnDismissed(bool dismissed_by_user) {
if (IsShowingTouchToFill()) {
ttf_payment_method_state_ = TouchToFillState::kWasShown;
dismissed_by_user_ = dismissed_by_user;
}
}
void TouchToFillDelegateAndroidImpl::LogMetricsAfterSubmission(
const FormStructure& submitted_form) {
// Log whether autofill was used after dismissing the touch to fill (without
// selecting any credit card for filling)
if (ttf_payment_method_state_ == TouchToFillState::kWasShown &&
query_form_.global_id() == submitted_form.global_id() &&
HasAnyAutofilledFields(submitted_form)) {
base::UmaHistogramBoolean(
"Autofill.TouchToFill.CreditCard.AutofillUsedAfterTouchToFillDismissal",
dismissed_by_user_);
if (!dismissed_by_user_) {
base::UmaHistogramBoolean(
"Autofill.TouchToFill.CreditCard.PerfectFilling",
IsFillingPerfect(submitted_form));
base::UmaHistogramBoolean(
"Autofill.TouchToFill.CreditCard.FillingCorrectness",
IsFillingCorrect(submitted_form));
}
}
}
bool TouchToFillDelegateAndroidImpl::HasAnyAutofilledFields(
const FormStructure& submitted_form) const {
return base::ranges::any_of(
submitted_form, [](const auto& field) { return field->is_autofilled(); });
}
bool TouchToFillDelegateAndroidImpl::IsFillingPerfect(
const FormStructure& submitted_form) const {
return base::ranges::all_of(submitted_form, [](const auto& field) {
return field->value().empty() || field->is_autofilled();
});
}
bool TouchToFillDelegateAndroidImpl::IsFillingCorrect(
const FormStructure& submitted_form) const {
return !base::ranges::any_of(submitted_form, [](const auto& field) {
return field->previously_autofilled();
});
}
bool TouchToFillDelegateAndroidImpl::IsFormPrefilled(const FormData& form) {
return base::ranges::any_of(form.fields(), [&](const FormFieldData& field) {
AutofillField* autofill_field = manager_->GetAutofillField(form, field);
if (autofill_field && autofill_field->Type().GetStorableType() !=
FieldType::CREDIT_CARD_NUMBER) {
return false;
}
return !SanitizedFieldIsEmpty(field.value());
});
}
base::WeakPtr<TouchToFillDelegateAndroidImpl>
TouchToFillDelegateAndroidImpl::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
} // namespace autofill