chromium/chrome/browser/password_manager/android/save_update_password_message_delegate.cc

// Copyright 2020 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/password_manager/android/save_update_password_message_delegate.h"

#include <optional>
#include <utility>

#include "base/android/build_info.h"
#include "base/android/jni_android.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/android/android_theme_resources.h"
#include "chrome/browser/android/resource_mapper.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/password_manager/android/password_infobar_utils.h"
#include "chrome/browser/password_manager/android/password_manager_android_util.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/passwords/ui_utils.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/messages/android/message_dispatcher_bridge.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_form_metrics_recorder.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "components/password_manager/core/browser/split_stores_and_local_upm.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/prefs/pref_service.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "save_update_password_message_delegate.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"

namespace {

using password_manager::PasswordForm;
using password_manager::UsesSplitStoresAndUPMForLocal;

// Duration of message before timeout; 20 seconds.
const int kMessageDismissDurationMs = 20000;

constexpr base::TimeDelta kUpdateGMSCoreMessageDisplayDelay =
    base::Milliseconds(500);

void TryToShowPasswordMigrationWarning(
    base::RepeatingCallback<
        void(gfx::NativeWindow,
             Profile*,
             password_manager::metrics_util::PasswordMigrationWarningTriggers)>
        callback,
    raw_ptr<content::WebContents> web_contents) {
  if (base::FeatureList::IsEnabled(
          password_manager::features::
              kUnifiedPasswordManagerLocalPasswordsMigrationWarning)) {
    callback.Run(
        web_contents->GetTopLevelNativeWindow(),
        Profile::FromBrowserContext(web_contents->GetBrowserContext()),
        password_manager::metrics_util::PasswordMigrationWarningTriggers::
            kPasswordSaveUpdateMessage);
  }
}

}  // namespace

SaveUpdatePasswordMessageDelegate::SaveUpdatePasswordMessageDelegate()
    : SaveUpdatePasswordMessageDelegate(
          base::BindRepeating(PasswordEditDialogBridge::Create),
          base::BindRepeating(&local_password_migration::ShowWarning)) {}

SaveUpdatePasswordMessageDelegate::SaveUpdatePasswordMessageDelegate(
    PasswordEditDialogFactory password_edit_dialog_factory,
    base::RepeatingCallback<
        void(gfx::NativeWindow,
             Profile*,
             password_manager::metrics_util::PasswordMigrationWarningTriggers)>
        create_migration_warning_callback)
    : password_edit_dialog_factory_(std::move(password_edit_dialog_factory)),
      create_migration_warning_callback_(
          std::move(create_migration_warning_callback)),
      device_lock_bridge_(std::make_unique<DeviceLockBridge>()) {}

SaveUpdatePasswordMessageDelegate::SaveUpdatePasswordMessageDelegate(
    base::PassKey<class SaveUpdatePasswordMessageDelegateTest>,
    PasswordEditDialogFactory password_edit_dialog_factory,
    base::RepeatingCallback<
        void(gfx::NativeWindow,
             Profile*,
             password_manager::metrics_util::PasswordMigrationWarningTriggers)>
        create_migration_warning_callback,
    std::unique_ptr<DeviceLockBridge> device_lock_bridge)
    : SaveUpdatePasswordMessageDelegate(password_edit_dialog_factory,
                                        create_migration_warning_callback) {
  device_lock_bridge_ = std::move(device_lock_bridge);
}

SaveUpdatePasswordMessageDelegate::~SaveUpdatePasswordMessageDelegate() {
  DCHECK(web_contents_ == nullptr);
}

void SaveUpdatePasswordMessageDelegate::DisplaySaveUpdatePasswordPrompt(
    content::WebContents* web_contents,
    std::unique_ptr<password_manager::PasswordFormManagerForUI> form_to_save,
    bool update_password,
    password_manager::PasswordManagerClient* password_manager_client) {
  DCHECK_NE(nullptr, web_contents);
  DCHECK(form_to_save);

  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());

  std::optional<AccountInfo> account_info =
      password_manager::GetAccountInfoForPasswordMessages(
          SyncServiceFactory::GetForProfile(profile),
          IdentityManagerFactory::GetForProfile(profile));
  DisplaySaveUpdatePasswordPromptInternal(
      web_contents, std::move(form_to_save), std::move(account_info),
      update_password, password_manager_client);
}

void SaveUpdatePasswordMessageDelegate::DismissSaveUpdatePasswordPrompt() {
  if (password_edit_dialog_ != nullptr) {
    password_edit_dialog_->Dismiss();
  }
  DismissSaveUpdatePasswordMessage(messages::DismissReason::UNKNOWN);
}

void SaveUpdatePasswordMessageDelegate::DismissSaveUpdatePasswordMessage(
    messages::DismissReason dismiss_reason) {
  if (message_ != nullptr) {
    messages::MessageDispatcherBridge::Get()->DismissMessage(message_.get(),
                                                             dismiss_reason);
  }
}

void SaveUpdatePasswordMessageDelegate::DisplaySaveUpdatePasswordPromptInternal(
    content::WebContents* web_contents,
    std::unique_ptr<password_manager::PasswordFormManagerForUI> form_to_save,
    std::optional<AccountInfo> account_info,
    bool update_password,
    password_manager::PasswordManagerClient* password_manager_client) {
  // Dismiss previous message if it is displayed.
  DismissSaveUpdatePasswordPrompt();
  DCHECK(message_ == nullptr);
  DCHECK(password_edit_dialog_ == nullptr);

  web_contents_ = web_contents;
  passwords_state_.set_client(password_manager_client);
  if (update_password) {
    passwords_state_.OnUpdatePassword(std::move(form_to_save));
  } else {
    passwords_state_.OnPendingPassword(std::move(form_to_save));
  }

  account_email_ = GetAccountForMessageDescription(account_info);

  CreateMessage(update_password);
  RecordMessageShownMetrics();
  password_manager::metrics_util::LogFormSubmissionsVsSavePromptsHistogram(
      password_manager::metrics_util::SaveFlowStep::kSavePromptShown);
  messages::MessageDispatcherBridge::Get()->EnqueueMessage(
      message_.get(), web_contents_, messages::MessageScopeType::WEB_CONTENTS,
      messages::MessagePriority::kUrgent);
}

void SaveUpdatePasswordMessageDelegate::CreateMessage(bool update_password) {
  // Binding with base::Unretained(this) is safe here because
  // SaveUpdatePasswordMessageDelegate owns message_. Callbacks won't be called
  // after the current object is destroyed.
  messages::MessageIdentifier message_id =
      update_password ? messages::MessageIdentifier::UPDATE_PASSWORD
                      : messages::MessageIdentifier::SAVE_PASSWORD;
  base::OnceClosure callback =
      update_password
          ? base::BindOnce(
                &SaveUpdatePasswordMessageDelegate::HandleUpdateButtonClicked,
                base::Unretained(this))
          : base::BindOnce(
                &SaveUpdatePasswordMessageDelegate::HandleSaveButtonClicked,
                base::Unretained(this));
  message_ = std::make_unique<messages::MessageWrapper>(
      message_id, std::move(callback),
      base::BindOnce(&SaveUpdatePasswordMessageDelegate::HandleMessageDismissed,
                     base::Unretained(this)));

  message_->SetDuration(kMessageDismissDurationMs);

  const password_manager::PasswordForm& pending_credentials =
      passwords_state_.form_manager()->GetPendingCredentials();

  int title_message_id;
  if (update_password) {
    title_message_id = IDS_UPDATE_PASSWORD;
  } else if (!pending_credentials.IsFederatedCredential()) {
    title_message_id = IDS_SAVE_PASSWORD;
  } else {
    title_message_id = IDS_SAVE_ACCOUNT;
  }
  message_->SetTitle(l10n_util::GetStringUTF16(title_message_id));

  std::u16string description =
      GetMessageDescription(pending_credentials, update_password);
  message_->SetDescription(description);

  update_password_ = update_password;

  bool use_followup_button = HasMultipleCredentialsStored();
  message_->SetPrimaryButtonText(l10n_util::GetStringUTF16(
      GetPrimaryButtonTextId(update_password, use_followup_button)));

  message_->SetIconResourceId(ResourceMapper::MapToJavaDrawableId(
      IDR_ANDROID_PASSWORD_MANAGER_LOGO_24DP));
  message_->DisableIconTint();

  // The cog button is always shown for the save message and for the update
  // message when there is just one password stored for the web site. When
  // there are multiple credentials stored, the dialog will be called anyway
  // from the followup button, so there are no options to put under the cog.
  if (!update_password || !use_followup_button) {
    SetupCogMenu(message_, update_password);
  }
}

void SaveUpdatePasswordMessageDelegate::SetupCogMenu(
    std::unique_ptr<messages::MessageWrapper>& message,
    bool update_password) {
  message->SetSecondaryIconResourceId(
      ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_MESSAGE_SETTINGS));
  if (update_password) {
    message->SetSecondaryActionCallback(base::BindRepeating(
        &SaveUpdatePasswordMessageDelegate::DisplayEditDialog,
        base::Unretained(this), update_password));
  } else {
    message_->SetSecondaryMenuItemSelectedCallback(base::BindRepeating(
        &SaveUpdatePasswordMessageDelegate::HandleSaveMessageMenuItemClick,
        base::Unretained(this)));
    message_->AddSecondaryMenuItem(
        static_cast<int>(SavePasswordDialogMenuItem::kNeverSave),
        /*resource_id=*/0,
        l10n_util::GetStringUTF16(IDS_PASSWORD_MESSAGE_NEVER_SAVE_MENU_ITEM),
        l10n_util::GetStringUTF16(
            IDS_PASSWORD_MESSAGE_NEVER_SAVE_MENU_ITEM_DESC));
    message_->AddSecondaryMenuItem(
        static_cast<int>(SavePasswordDialogMenuItem::kEditPassword),
        /*resource_id=*/0,
        l10n_util::GetStringUTF16(
            IDS_PASSWORD_MESSAGE_EDIT_PASSWORD_MENU_ITEM));
  }
}

void SaveUpdatePasswordMessageDelegate::HandleSaveMessageMenuItemClick(
    int item_id) {
  switch (static_cast<SavePasswordDialogMenuItem>(item_id)) {
    case SavePasswordDialogMenuItem::kNeverSave:
      HandleNeverSaveClicked();
      break;
    case SavePasswordDialogMenuItem::kEditPassword:
      DisplayEditDialog(/*update_password=*/false);
      break;
  }
}

std::u16string SaveUpdatePasswordMessageDelegate::GetMessageDescription(
    const password_manager::PasswordForm& pending_credentials,
    bool update_password) {
  // If password is being updated in the account storage, the description should
  // contain for which account the update is made.
  if (IsUsingAccountStorage(pending_credentials.username_value)) {
    return l10n_util::GetStringFUTF16(
        update_password
            ? IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION
            : IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION,
        base::UTF8ToUTF16(account_email_.value()));
  }
  return l10n_util::GetStringUTF16(
      update_password
          ? IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_OUT_MESSAGE_DESCRIPTION
          : IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_OUT_MESSAGE_DESCRIPTION);
}

std::optional<std::string>
SaveUpdatePasswordMessageDelegate::GetAccountForMessageDescription(
    const std::optional<AccountInfo>& account_info) {
  if (!account_info.has_value()) {
    return std::nullopt;
  }

  return account_info->CanHaveEmailAddressDisplayed()
             ? account_info.value().email
             : account_info.value().full_name;
}

int SaveUpdatePasswordMessageDelegate::GetPrimaryButtonTextId(
    bool update_password,
    bool use_followup_button_text) {
  if (!update_password) {
    return IDS_PASSWORD_MANAGER_SAVE_BUTTON;
  }
  if (!use_followup_button_text) {
    return IDS_PASSWORD_MANAGER_UPDATE_BUTTON;
  }
  return IDS_PASSWORD_MANAGER_CONTINUE_BUTTON;
}

unsigned int SaveUpdatePasswordMessageDelegate::GetDisplayUsernames(
    std::vector<std::u16string>* usernames) {
  unsigned int selected_username_index = 0;
  // TODO(crbug.com/40675711): Fix the update logic to use all best matches,
  // rather than current_forms which is best_matches without PSL-matched
  // credentials.
  const std::vector<std::unique_ptr<password_manager::PasswordForm>>&
      password_forms = passwords_state_.GetCurrentForms();
  const std::u16string& default_username =
      passwords_state_.form_manager()->GetPendingCredentials().username_value;
  for (const auto& form : password_forms) {
    usernames->push_back(form->username_value);
    if (form->username_value == default_username) {
      selected_username_index = usernames->size() - 1;
    }
  }
  return selected_username_index;
}

void SaveUpdatePasswordMessageDelegate::HandleSaveButtonClicked() {
  SavePassword();
}

void SaveUpdatePasswordMessageDelegate::SavePassword() {
  if (!device_lock_bridge_->ShouldShowDeviceLockUi()) {
    passwords_state_.form_manager()->Save();
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &SaveUpdatePasswordMessageDelegate::MaybeNudgeToUpdateGmsCore,
            weak_ptr_factory_.GetWeakPtr()),
        kUpdateGMSCoreMessageDisplayDelay);
    return;
  }
  device_lock_bridge_->LaunchDeviceLockUiIfNeededBeforeRunningCallback(
      web_contents_->GetNativeView()->GetWindowAndroid(),
      base::BindOnce(
          &SaveUpdatePasswordMessageDelegate::SavePasswordAfterDeviceLockUi,
          weak_ptr_factory_.GetWeakPtr()));
}

void SaveUpdatePasswordMessageDelegate::SavePasswordAfterDeviceLockUi(
    bool is_device_lock_requirement_met) {
  CHECK(device_lock_bridge_->RequiresDeviceLock());
  if (is_device_lock_requirement_met) {
    passwords_state_.form_manager()->Save();
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &SaveUpdatePasswordMessageDelegate::MaybeNudgeToUpdateGmsCore,
            weak_ptr_factory_.GetWeakPtr()),
        kUpdateGMSCoreMessageDisplayDelay);
    TryToShowPasswordMigrationWarning(create_migration_warning_callback_,
                                      web_contents_);
  }
  ClearState();
}

void SaveUpdatePasswordMessageDelegate::HandleNeverSaveClicked() {
  passwords_state_.form_manager()->Blocklist();
  DismissSaveUpdatePasswordMessage(messages::DismissReason::SECONDARY_ACTION);
}

void SaveUpdatePasswordMessageDelegate::HandleUpdateButtonClicked() {
  std::vector<std::u16string> usernames;
  if (HasMultipleCredentialsStored()) {
    DisplayEditDialog(/*update_password=*/true);
  } else {
    SavePassword();
  }
}

void SaveUpdatePasswordMessageDelegate::DisplayEditDialog(
    bool update_password) {
  const password_manager::PasswordForm& password_form =
      passwords_state_.form_manager()->GetPendingCredentials();
  const std::u16string& current_username = password_form.username_value;
  const std::u16string& current_password = password_form.password_value;

  CreatePasswordEditDialog();

  // Password edit dialog factory method can return nullptr when web_contents
  // is not attached to a window. See crbug.com/1049090 for details.
  if (!password_edit_dialog_) {
    return;
  }

  std::vector<std::u16string> usernames;
  GetDisplayUsernames(&usernames);
  password_edit_dialog_->ShowPasswordEditDialog(
      usernames, current_username, current_password, account_email_);

  DismissSaveUpdatePasswordMessage(messages::DismissReason::SECONDARY_ACTION);
}

void SaveUpdatePasswordMessageDelegate::HandleMessageDismissed(
    messages::DismissReason dismiss_reason) {
  message_.reset();
  if (password_edit_dialog_) {
    // The user triggered password edit dialog. Don't cleanup internal
    // datastructures, dialog dismiss callback will perform cleanup.
    return;
  }
  // Record metrics and cleanup state.
  RecordDismissalReasonMetrics(
      MessageDismissReasonToPasswordManagerUIDismissalReason(dismiss_reason));

  // If Device Lock UI needs to be shown and can be (i.e. WindowAndroid is
  // available), these lines are handled in the SavePasswordAfterDeviceLockUi()
  // callback.
  if (!(device_lock_bridge_->ShouldShowDeviceLockUi() &&
        web_contents_->GetNativeView()->GetWindowAndroid())) {
    if (dismiss_reason == messages::DismissReason::PRIMARY_ACTION) {
      TryToShowPasswordMigrationWarning(create_migration_warning_callback_,
                                        web_contents_);
    }
    ClearState();
  }
}

bool SaveUpdatePasswordMessageDelegate::HasMultipleCredentialsStored() {
  // TODO(crbug.com/40675711): Fix the update logic to use all best matches,
  // rather than current_forms which is best_matches without PSL-matched
  // credentials.
  const std::vector<std::unique_ptr<password_manager::PasswordForm>>&
      password_forms = passwords_state_.GetCurrentForms();
  return password_forms.size() > 1;
}

void SaveUpdatePasswordMessageDelegate::CreatePasswordEditDialog() {
  password_edit_dialog_ =
      password_edit_dialog_factory_.Run(web_contents_.get(), this);
}

void SaveUpdatePasswordMessageDelegate::HandleDialogDismissed(
    bool dialog_accepted) {
  RecordDismissalReasonMetrics(
      dialog_accepted ? password_manager::metrics_util::CLICKED_ACCEPT
                      : password_manager::metrics_util::CLICKED_CANCEL);

  password_edit_dialog_.reset();

  // If Device Lock UI needs to be shown and can be (i.e. WindowAndroid is
  // available), these lines are handled in the SavePasswordAfterDeviceLockUi()
  // callback.
  if (!(device_lock_bridge_->ShouldShowDeviceLockUi() &&
        web_contents_->GetNativeView()->GetWindowAndroid())) {
    TryToShowPasswordMigrationWarning(create_migration_warning_callback_,
                                      web_contents_);
    ClearState();
  }
}

void SaveUpdatePasswordMessageDelegate::HandleSavePasswordFromDialog(
    const std::u16string& username,
    const std::u16string& password) {
  UpdatePasswordFormUsernameAndPassword(username, password,
                                        passwords_state_.form_manager());
  SavePassword();
}

bool SaveUpdatePasswordMessageDelegate::IsUsingAccountStorage(
    const std::u16string& username) {
  if (!account_email_) {
    return false;
  }

  // Pre-UPM the profile storage was used in fact as the account store (when
  // sync is on). So this is the cut-off for the users who are not using UPM
  // (this evaluates to using account store when the user is syncing and using
  // profile store when they are not syncing).
  Profile* profile =
      Profile::FromBrowserContext(web_contents_->GetBrowserContext());
  if (!UsesSplitStoresAndUPMForLocal(profile->GetPrefs())) {
    return account_email_.has_value();
  }

  // Copy the pending password form here and assign the new username.
  password_manager::PasswordForm updated_credentials =
      passwords_state_.form_manager()->GetPendingCredentials();
  updated_credentials.username_value = username;
  return (passwords_state_.form_manager()->GetPasswordStoreForSaving(
              updated_credentials) &
          PasswordForm::Store::kAccountStore) ==
         PasswordForm::Store::kAccountStore;
}

void SaveUpdatePasswordMessageDelegate::ClearState() {
  DCHECK(message_ == nullptr);
  DCHECK(password_edit_dialog_ == nullptr);

  passwords_state_.OnInactive();
  // web_contents_ is set in DisplaySaveUpdatePasswordPromptInternal().
  // Resetting it here to keep the state clean when no message is enqueued.
  web_contents_ = nullptr;
}

void SaveUpdatePasswordMessageDelegate::RecordMessageShownMetrics() {
  if (auto* recorder = passwords_state_.form_manager()->GetMetricsRecorder()) {
    recorder->RecordPasswordBubbleShown(
        passwords_state_.form_manager()->GetCredentialSource(),
        password_manager::metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING);
  }
}

void SaveUpdatePasswordMessageDelegate::RecordDismissalReasonMetrics(
    password_manager::metrics_util::UIDismissalReason ui_dismissal_reason) {
  if (update_password_) {
    password_manager::metrics_util::LogUpdateUIDismissalReason(
        ui_dismissal_reason);
  } else {
    password_manager::metrics_util::LogSaveUIDismissalReason(
        ui_dismissal_reason, /*user_state=*/std::nullopt,
        /*log_adoption_metric=*/false);
  }
  if (auto* recorder = passwords_state_.form_manager()->GetMetricsRecorder()) {
    recorder->RecordUIDismissalReason(ui_dismissal_reason);
  }
}

// static
password_manager::metrics_util::UIDismissalReason
SaveUpdatePasswordMessageDelegate::
    MessageDismissReasonToPasswordManagerUIDismissalReason(
        messages::DismissReason dismiss_reason) {
  password_manager::metrics_util::UIDismissalReason ui_dismissal_reason;
  switch (dismiss_reason) {
    case messages::DismissReason::PRIMARY_ACTION:
      ui_dismissal_reason = password_manager::metrics_util::CLICKED_ACCEPT;
      break;
    case messages::DismissReason::SECONDARY_ACTION:
      ui_dismissal_reason = password_manager::metrics_util::CLICKED_NEVER;
      break;
    case messages::DismissReason::GESTURE:
      ui_dismissal_reason = password_manager::metrics_util::CLICKED_CANCEL;
      break;
    default:
      ui_dismissal_reason =
          password_manager::metrics_util::NO_DIRECT_INTERACTION;
      break;
  }
  return ui_dismissal_reason;
}

void SaveUpdatePasswordMessageDelegate::MaybeNudgeToUpdateGmsCore() {
  if (passwords_state_.client()
          ->GetPasswordFeatureManager()
          ->ShouldUpdateGmsCore()) {
    passwords_state_.client()->ShowPasswordManagerErrorMessage(
        password_manager::ErrorMessageFlowType::kSaveFlow,
        password_manager::PasswordStoreBackendErrorType::
            kGMSCoreOutdatedSavingPossible);
  }
}