blob: c9593869fd50cb05a220f6d4c4c733c5282365a7 [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/profiles/batch_upload/batch_upload_service.h"
#include <array>
#include <map>
#include "base/containers/contains.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/notreached.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/batch_upload/batch_upload_delegate.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/profiles/avatar_toolbar_button.h"
#include "chrome/grit/generated_resources.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/sync/base/data_type.h"
#include "components/sync/service/local_data_description.h"
#include "components/sync/service/sync_service.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
// Duration of displaying the saving to account text in the avatar button.
constexpr base::TimeDelta kBatchUploadAvatarButtonOverrideTextDuration =
base::Seconds(3);
// This list contains all the data types that are available for the Batch Upload
// dialog. Data types should not be repeated and the list is ordered based on
// the priority of showing in the dialog.
// An exception to the order of this list may happen to the first item, when the
// entry point used to open the batch upload dialog is tied to a data type. This
// data type will be shown first for consistency with the action from the entry
// point. E.g. Bookmarks promo card entry point will force
// `syncer::DataType::BOOKMARKS` to be the first data type shown (and not
// repeated afterwards).
const std::array<syncer::DataType, 6> kBatchUploadAvailableTypesOrder{
// clang-format off
syncer::DataType::PASSWORDS,
syncer::DataType::BOOKMARKS,
syncer::DataType::READING_LIST,
syncer::DataType::CONTACT_INFO,
syncer::DataType::EXTENSIONS,
syncer::DataType::THEMES,
// clang-format on
};
// Determines the primary type based on the entry point. Some entry point may be
// generic and not tied to any data type - std::nullopt is returned in this
// case.
std::optional<syncer::DataType> PrimaryTypeFromEntryPoint(
BatchUploadService::EntryPoint entry_point) {
switch (entry_point) {
case BatchUploadService::EntryPoint::kPasswordManagerSettings:
case BatchUploadService::EntryPoint::kPasswordPromoCard:
return syncer::PASSWORDS;
case BatchUploadService::EntryPoint::kBookmarksManagerPromoCard:
case BatchUploadService::EntryPoint::
kProfileMenuPrimaryButtonWithBookmarksAction:
return syncer::BOOKMARKS;
case BatchUploadService::EntryPoint::kProfileMenuRowButtonAction:
case BatchUploadService::EntryPoint::kProfileMenuPrimaryButtonAction:
case BatchUploadService::EntryPoint::
kProfileMenuPrimaryButtonWithWindows10DepreciationAction:
return std::nullopt;
}
}
// Return the primary local data description after performing additional checks.
syncer::LocalDataDescription GetPrimaryTypeLocalDataDescription(
const std::map<syncer::DataType, syncer::LocalDataDescription>&
local_data_descriptions_map,
syncer::DataType primary_type) {
CHECK(base::Contains(kBatchUploadAvailableTypesOrder, primary_type));
CHECK(local_data_descriptions_map.contains(primary_type));
const syncer::LocalDataDescription& primary_local_data_description =
local_data_descriptions_map.at(primary_type);
CHECK(!primary_local_data_description.local_data_models.empty())
<< "Primary data type should have local data since the entry point "
"is available.";
CHECK_EQ(primary_type, primary_local_data_description.type)
<< "Non empty data description's data type and the keyed mapping "
"value should always match.";
return primary_local_data_description;
}
// Returns the list of data descriptions in `local_data_descriptions_map`
// following the expected displayed order in the dialog.
// `entry_point` is used to determine the primary (first) type, if any, to be
// displayed, overriding the expected order for the first type only. Primary
// type should have local data - since the entry point was available.
// Generic entry points have no primary type associated with it, the regular
// order will be displayed.
// Other data descriptions with no local data will be filtered out.
std::vector<syncer::LocalDataDescription>
GetOrderedListOfNonEmptyDataDescriptions(
std::map<syncer::DataType, syncer::LocalDataDescription>
local_data_descriptions_map,
BatchUploadService::EntryPoint entry_point) {
std::vector<syncer::LocalDataDescription> local_data_description_list;
std::optional<syncer::DataType> primary_type =
PrimaryTypeFromEntryPoint(entry_point);
if (primary_type.has_value()) {
// Treat the primary type first to make sure it is the first type for
// display, overriding the expected order given in
// `kBatchUploadAvailableTypesOrder`.
local_data_description_list.push_back(GetPrimaryTypeLocalDataDescription(
local_data_descriptions_map, *primary_type));
local_data_descriptions_map.erase(*primary_type);
}
// Reorder the result from the `local_data_descriptions_map` based on the
// available types order.
for (syncer::DataType type : kBatchUploadAvailableTypesOrder) {
if (!local_data_descriptions_map.contains(type)) {
continue;
}
syncer::LocalDataDescription& local_data_description =
local_data_descriptions_map.at(type);
if (!local_data_description.local_data_models.empty()) {
CHECK_EQ(type, local_data_description.type)
<< "Non empty data description's data type and the keyed mapping "
"value should always match.";
local_data_description_list.push_back(std::move(local_data_description));
}
local_data_descriptions_map.erase(type);
}
// All initial inputs should be processed.
CHECK(local_data_descriptions_map.empty());
return local_data_description_list;
}
// Whether there exist a current local data item of any type.
bool HasLocalDataToShow(
const std::map<syncer::DataType, syncer::LocalDataDescription>&
local_data_descriptions) {
// As long as a data type has at least a single item to show, the dialog can
// be shown.
return std::ranges::any_of(
local_data_descriptions, [](const auto& local_data_description) {
return !local_data_description.second.local_data_models.empty();
});
}
} // namespace
BatchUploadService::BatchUploadService(
signin::IdentityManager* identity_manager,
syncer::SyncService* sync_service,
std::unique_ptr<BatchUploadDelegate> delegate)
: identity_manager_(*identity_manager),
sync_service_(*sync_service),
delegate_(std::move(delegate)) {}
BatchUploadService::~BatchUploadService() = default;
void BatchUploadService::OpenBatchUpload(
Browser* browser,
EntryPoint entry_point,
base::OnceCallback<void(bool)> success_callback) {
if (!IsUserEligibleToOpenDialog()) {
std::move(success_callback).Run(false);
return;
}
// Do not allow to have more than one dialog shown at a time.
if (IsDialogOpened()) {
// TODO(b/361330952): give focus to the browser that is showing the dialog
// currently.
std::move(success_callback).Run(false);
return;
}
// Create the state of the dialog that may be shown, in preparation for
// showing the dialog once all the local data descriptions are ready in
// `OnGetLocalDataDescriptionsReady()`. Allows to make sure that while getting
// the local data descriptions, no other dialog opening is triggered.
state_.dialog_state_ = std::make_unique<ResettableState::DialogState>();
state_.dialog_state_->browser_ = browser;
state_.dialog_state_->entry_point_ = entry_point;
state_.dialog_state_->dialog_shown_callback_ = std::move(success_callback);
GetLocalDataDescriptionsForAvailableTypes(
base::BindOnce(&BatchUploadService::OnGetLocalDataDescriptionsReady,
base::Unretained(this)));
}
void BatchUploadService::GetLocalDataDescriptionsForAvailableTypes(
base::OnceCallback<
void(std::map<syncer::DataType, syncer::LocalDataDescription>)>
result_callback) {
syncer::DataTypeSet data_types;
// Iterate over all available enums.
for (syncer::DataType type : kBatchUploadAvailableTypesOrder) {
data_types.Put(type);
}
sync_service_->GetLocalDataDescriptions(data_types,
std::move(result_callback));
}
void BatchUploadService::OnGetLocalDataDescriptionsReady(
std::map<syncer::DataType, syncer::LocalDataDescription> local_data_map) {
if (local_data_map.empty() || !HasLocalDataToShow(local_data_map)) {
std::move(state_.dialog_state_->dialog_shown_callback_).Run(false);
ResetDialogState();
return;
}
delegate_->ShowBatchUploadDialog(
state_.dialog_state_->browser_,
GetOrderedListOfNonEmptyDataDescriptions(
std::move(local_data_map), state_.dialog_state_->entry_point_),
state_.dialog_state_->entry_point_,
/*complete_callback=*/
base::BindOnce(&BatchUploadService::OnBatchUploadDialogResult,
base::Unretained(this)));
std::move(state_.dialog_state_->dialog_shown_callback_).Run(true);
}
void BatchUploadService::OnBatchUploadDialogResult(
const std::map<syncer::DataType,
std::vector<syncer::LocalDataItemModel::DataId>>&
item_ids_to_move) {
CHECK(state_.dialog_state_);
Browser* browser = state_.dialog_state_->browser_.get();
ResetDialogState();
if (item_ids_to_move.empty()) {
return;
}
sync_service_->TriggerLocalDataMigrationForItems(item_ids_to_move);
// `browser` may be null in tests.
if (browser) {
state_.saving_browser_state_ =
std::make_unique<ResettableState::SavingBrowserState>();
TriggerAvatarButtonSavingDataText(browser);
}
}
bool BatchUploadService::IsUserEligibleToOpenDialog() const {
AccountInfo primary_account = identity_manager_->FindExtendedAccountInfo(
identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin));
// If not signed in, the user should not have access to the dialog.
if (primary_account.IsEmpty() ||
identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
return false;
}
// If is in Sign in pending, the user should not have access to the dialog.
if (identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
primary_account.account_id)) {
return false;
}
return true;
}
void BatchUploadService::TriggerAvatarButtonSavingDataText(Browser* browser) {
CHECK(browser);
CHECK(state_.saving_browser_state_);
// Show the text.
state_.saving_browser_state_->avatar_override_clear_callback_ =
BrowserView::GetBrowserViewForBrowser(browser)
->toolbar_button_provider()
->GetAvatarToolbarButton()
->SetExplicitButtonState(
l10n_util::GetStringUTF16(
IDS_BATCH_UPLOAD_AVATAR_BUTTON_SAVING_TO_ACCOUNT),
/*accessibility_label=*/std::nullopt,
/*explicit_action=*/std::nullopt);
// Prepare the timer to stop the overridden text from showing.
state_.saving_browser_state_->avatar_override_timer_.Start(
FROM_HERE, kBatchUploadAvatarButtonOverrideTextDuration,
base::BindOnce(&BatchUploadService::OnAvatarOverrideTextTimeout,
// Unretained is fine here since the timer is a field
// member and will not fire if destroyed.
base::Unretained(this)));
}
void BatchUploadService::OnAvatarOverrideTextTimeout() {
CHECK(state_.saving_browser_state_ &&
state_.saving_browser_state_->avatar_override_clear_callback_);
state_.saving_browser_state_->avatar_override_clear_callback_.RunAndReset();
state_.saving_browser_state_.reset();
}
bool BatchUploadService::IsDialogOpened() const {
return state_.dialog_state_ != nullptr;
}
void BatchUploadService::ResetDialogState() {
state_.dialog_state_.reset();
}
BatchUploadService::ResettableState::ResettableState() = default;
BatchUploadService::ResettableState::~ResettableState() = default;
BatchUploadService::ResettableState::DialogState::DialogState() = default;
BatchUploadService::ResettableState::DialogState::~DialogState() = default;
// static
std::vector<syncer::DataType> BatchUploadService::AvailableTypesOrder() {
// Transforming to vector to avoid changing every definition on updates.
return base::ToVector(kBatchUploadAvailableTypesOrder);
}