blob: 6bbf3b2a3286631f0c13a9f35a13ed79d848d756 [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/save_address_bubble_controller.h"
#include <array>
#include <optional>
#include <string>
#include <vector>
#include "base/check.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autofill/ui/ui_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/grit/theme_resources.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/core/browser/autofill_address_util.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/strings/grit/components_strings.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
namespace autofill {
namespace {
// CanvasImageSource that combines a background image with user's avatar,
// the avatar is positioned and resized in terms of the background image DIPs,
// it also is cropped in a circle.
class MigrationHeaderImageSource : public gfx::CanvasImageSource {
public:
MigrationHeaderImageSource(const ui::ImageModel& image,
const ui::ImageModel& avatar,
const gfx::Point& avatar_position,
size_t avatar_size)
: gfx::CanvasImageSource(image.Size()),
image_(image),
avatar_(avatar),
avatar_position_(avatar_position),
avatar_size_(avatar_size) {}
MigrationHeaderImageSource(const MigrationHeaderImageSource&) = delete;
MigrationHeaderImageSource& operator=(const MigrationHeaderImageSource&) =
delete;
~MigrationHeaderImageSource() override = default;
// gfx::CanvasImageSource:
void Draw(gfx::Canvas* canvas) override;
private:
const ui::ImageModel image_;
const ui::ImageModel avatar_;
const gfx::Point avatar_position_;
const size_t avatar_size_;
};
void MigrationHeaderImageSource::Draw(gfx::Canvas* canvas) {
// Draw the background image first.
gfx::ImageSkia image = image_.GetImage().AsImageSkia();
canvas->DrawImageInt(image, 0, 0);
// Setting a clippath makes subsequent avatar drawing cropped in a circle.
SkPath avatar_bound = SkPath().addOval(
SkRect::MakeXYWH(avatar_position_.x(), avatar_position_.y(),
/*w=*/avatar_size_, /*h=*/avatar_size_));
canvas->ClipPath(avatar_bound, /*do_anti_alias=*/true);
// Finally draw the avatar, above the background and cropped.
gfx::ImageSkia avatar = gfx::ImageSkiaOperations::CreateResizedImage(
avatar_.GetImage().AsImageSkia(),
skia::ImageOperations::ResizeMethod::RESIZE_BEST,
gfx::Size(avatar_size_, avatar_size_));
canvas->DrawImageInt(avatar, avatar_position_.x(), avatar_position_.y());
}
ui::ImageModel EmbedAvatar(int background_id,
const ui::ImageModel& avatar,
const gfx::Point& position,
size_t size) {
return ui::ImageModel::FromImageSkia(
gfx::CanvasImageSource::MakeImageSkia<MigrationHeaderImageSource>(
ui::ImageModel::FromResourceId(background_id), avatar, position,
size));
}
} // namespace
SaveAddressBubbleController::SaveAddressBubbleController(
base::WeakPtr<AddressBubbleControllerDelegate> delegate,
content::WebContents* web_contents,
const AutofillProfile& address_profile,
bool is_migration_to_account)
: content::WebContentsObserver(web_contents),
delegate_(delegate),
address_profile_(address_profile),
is_migration_to_account_(is_migration_to_account) {}
SaveAddressBubbleController::~SaveAddressBubbleController() = default;
std::u16string SaveAddressBubbleController::GetWindowTitle() const {
return l10n_util::GetStringUTF16(
is_migration_to_account_
? IDS_AUTOFILL_ACCOUNT_MIGRATE_ADDRESS_PROMPT_TITLE
: IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_TITLE);
}
std::optional<SaveAddressBubbleController::HeaderImages>
SaveAddressBubbleController::GetHeaderImages() const {
if (is_migration_to_account_ && web_contents()) {
std::optional<AccountInfo> account =
GetPrimaryAccountInfoFromBrowserContext(
web_contents()->GetBrowserContext());
if (account) {
ui::ImageModel avatar = ui::ImageModel::FromImage(account->account_image);
// The position and size must match the implied one in the image,
// so these numbers are exclusively for ..._AVATAR50_X135_Y54.
static constexpr gfx::Point kAvatarPosition{135, 54};
static constexpr size_t kAvatarSize{50};
return HeaderImages{
.light = EmbedAvatar(IDR_MIGRATE_ADDRESS_AVATAR50_X135_Y54, avatar,
kAvatarPosition, kAvatarSize),
.dark = EmbedAvatar(IDR_MIGRATE_ADDRESS_AVATAR50_X135_Y54_DARK,
avatar, kAvatarPosition, kAvatarSize)};
}
}
return HeaderImages{
.light = ui::ImageModel::FromResourceId(IDR_SAVE_ADDRESS),
.dark = ui::ImageModel::FromResourceId(IDR_SAVE_ADDRESS_DARK)};
}
std::u16string SaveAddressBubbleController::GetBodyText() const {
if (is_migration_to_account_ && web_contents()) {
PersonalDataManager* pdm =
ContentAutofillClient::FromWebContents(web_contents())
->GetPersonalDataManager();
std::optional<AccountInfo> account =
GetPrimaryAccountInfoFromBrowserContext(
web_contents()->GetBrowserContext());
int string_id =
pdm->address_data_manager().IsSyncFeatureEnabledForAutofill()
? IDS_AUTOFILL_SYNCABLE_PROFILE_MIGRATION_PROMPT_NOTICE
: IDS_AUTOFILL_LOCAL_PROFILE_MIGRATION_PROMPT_NOTICE;
return l10n_util::GetStringFUTF16(string_id,
base::UTF8ToUTF16(account->email));
}
return {};
}
std::u16string SaveAddressBubbleController::GetAddressSummary() const {
// Use a shorter version of the address summary for migration, it has
// a fixed set of fields and doesn't depend on libaddressinput.
if (is_migration_to_account_) {
static constexpr std::array fields = {
NAME_FULL, ADDRESS_HOME_LINE1, EMAIL_ADDRESS, PHONE_HOME_WHOLE_NUMBER};
std::vector<std::u16string> values;
for (FieldType field : fields) {
std::u16string value = address_profile_.GetInfo(
field, g_browser_process->GetApplicationLocale());
if (!value.empty()) {
values.push_back(value);
}
}
if (values.empty()) {
return {};
}
return base::JoinString(values, u"\n");
}
return GetEnvelopeStyleAddress(
address_profile_, g_browser_process->GetApplicationLocale(),
/*include_recipient=*/true, /*include_country=*/true);
}
std::u16string SaveAddressBubbleController::GetProfileEmail() const {
// Email is not shown as a separate field in the migration flow,
// it is included in the address summary, see GetAddressSummary().
if (is_migration_to_account_) {
return {};
}
return address_profile_.GetInfo(EMAIL_ADDRESS,
g_browser_process->GetApplicationLocale());
}
std::u16string SaveAddressBubbleController::GetProfilePhone() const {
// Phone is not shown as a separate field in the migration flow,
// it is included in the address summary, see GetAddressSummary().
if (is_migration_to_account_) {
return {};
}
return address_profile_.GetInfo(PHONE_HOME_WHOLE_NUMBER,
g_browser_process->GetApplicationLocale());
}
std::u16string SaveAddressBubbleController::GetOkButtonLabel() const {
return l10n_util::GetStringUTF16(
is_migration_to_account_
? IDS_AUTOFILL_MIGRATE_ADDRESS_DIALOG_OK_BUTTON_LABEL_SAVE
: IDS_AUTOFILL_EDIT_ADDRESS_DIALOG_OK_BUTTON_LABEL_SAVE);
}
AutofillClient::AddressPromptUserDecision
SaveAddressBubbleController::GetCancelCallbackValue() const {
// The migration prompt should not be shown again if the user explicitly
// rejects it (for a particular address, due to legal and privacy
// requirements). In other cases it is acceptable to show it a few times more.
return is_migration_to_account_
? AutofillClient::AddressPromptUserDecision::kNever
: AutofillClient::AddressPromptUserDecision::kDeclined;
}
std::u16string SaveAddressBubbleController::GetFooterMessage() const {
if (address_profile_.source() == AutofillProfile::Source::kAccount &&
web_contents()) {
std::optional<AccountInfo> account =
GetPrimaryAccountInfoFromBrowserContext(
web_contents()->GetBrowserContext());
return l10n_util::GetStringFUTF16(
IDS_AUTOFILL_SAVE_IN_ACCOUNT_PROMPT_ADDRESS_SOURCE_NOTICE,
base::UTF8ToUTF16(account->email));
}
return {};
}
std::u16string SaveAddressBubbleController::GetEditorFooterMessage() const {
if (is_migration_to_account_ && web_contents()) {
std::optional<AccountInfo> account =
GetPrimaryAccountInfoFromBrowserContext(
web_contents()->GetBrowserContext());
return l10n_util::GetStringFUTF16(
IDS_AUTOFILL_SAVE_IN_ACCOUNT_PROMPT_ADDRESS_SOURCE_NOTICE,
base::UTF8ToUTF16(account->email));
}
return GetFooterMessage();
}
void SaveAddressBubbleController::OnUserDecision(
AutofillClient::AddressPromptUserDecision decision,
base::optional_ref<const AutofillProfile> profile) {
if (delegate_) {
delegate_->OnUserDecision(decision, profile);
}
}
void SaveAddressBubbleController::OnEditButtonClicked() {
if (delegate_) {
delegate_->ShowEditor(address_profile_, /*title_override=*/u"",
GetEditorFooterMessage(),
/*is_editing_existing_address=*/false);
}
}
void SaveAddressBubbleController::OnBubbleClosed() {
if (delegate_) {
delegate_->OnBubbleClosed();
}
}
} // namespace autofill