| // 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/ash/policy/skyvault/migration_notification_manager.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "ash/constants/notifier_catalogs.h" |
| #include "ash/public/cpp/new_window_delegate.h" |
| #include "ash/public/cpp/notification_utils.h" |
| #include "base/callback_list.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/no_destructor.h" |
| #include "base/notreached.h" |
| #include "base/sequence_checker.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/ash/file_manager/open_util.h" |
| #include "chrome/browser/ash/policy/skyvault/policy_utils.h" |
| #include "chrome/browser/ash/policy/skyvault/signin_notification_helper.h" |
| #include "chrome/browser/notifications/notification_display_service.h" |
| #include "chrome/browser/notifications/notification_display_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_selections.h" |
| #include "chrome/browser/ui/webui/ash/skyvault/local_files_migration_dialog.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/browser/browser_context.h" |
| #include "net/base/filename_util.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| |
| namespace policy::local_user_files { |
| |
| namespace { |
| |
| // Creates a notification with `kSkyvaultNotificationId`, `title` and `message`, |
| // that invokes `callback` when clicked on. |
| std::unique_ptr<message_center::Notification> CreateNotificationPtr( |
| const std::u16string title, |
| const std::u16string message, |
| base::RepeatingCallback<void(std::optional<int>)> callback = |
| base::DoNothing()) { |
| message_center::RichNotificationData optional_fields; |
| optional_fields.never_timeout = true; |
| return ash::CreateSystemNotificationPtr( |
| message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE, |
| kSkyVaultMigrationNotificationId, title, message, |
| /*display_source=*/std::u16string(), /*origin_url=*/GURL(), |
| message_center::NotifierId(), optional_fields, |
| base::MakeRefCounted<message_center::HandleNotificationClickDelegate>( |
| callback), |
| vector_icons::kBusinessIcon, |
| message_center::SystemNotificationWarningLevel::NORMAL); |
| } |
| |
| // Closes the notification with `kSkyVaultMigrationNotificationId`. |
| void CloseNotification(Profile* profile) { |
| NotificationDisplayServiceFactory::GetForProfile(profile)->Close( |
| NotificationHandler::Type::TRANSIENT, kSkyVaultMigrationNotificationId); |
| } |
| |
| // Closes the notification and, if the button is clicked, opens `path` in the |
| // Files App. |
| void HandleCompletedNotificationClick(Profile* profile, |
| const base::FilePath& path, |
| std::optional<int> button) { |
| if (button.has_value() && button == 0) { |
| file_manager::util::ShowItemInFolder(profile, path, base::DoNothing()); |
| ash::NewWindowDelegate::GetInstance()->OpenUrl( |
| net::FilePathToFileURL(path), |
| ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction, |
| ash::NewWindowDelegate::Disposition::kNewForegroundTab); |
| } |
| CloseNotification(profile); |
| } |
| |
| // Closes the notification and, if the button is clicked, opens `path` in the |
| // browser. |
| void HandleErrorNotificationClick(Profile* profile, |
| const base::FilePath& path, |
| std::optional<int> button) { |
| if (button.has_value() && button == 0) { |
| ash::NewWindowDelegate::GetInstance()->OpenUrl( |
| net::FilePathToFileURL(path), |
| ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction, |
| ash::NewWindowDelegate::Disposition::kNewForegroundTab); |
| } |
| CloseNotification(profile); |
| } |
| |
| // Returns the translation string corresponding to `destination`. |
| std::u16string CloudProviderToString(MigrationDestination destination) { |
| switch (destination) { |
| case MigrationDestination::kGoogleDrive: |
| return l10n_util::GetStringUTF16( |
| IDS_POLICY_SKYVAULT_CLOUD_PROVIDER_GOOGLE_DRIVE); |
| case MigrationDestination::kOneDrive: |
| return l10n_util::GetStringUTF16( |
| IDS_POLICY_SKYVAULT_CLOUD_PROVIDER_ONEDRIVE); |
| case MigrationDestination::kNotSpecified: |
| case MigrationDestination::kDelete: |
| NOTREACHED(); |
| } |
| } |
| |
| } // namespace |
| |
| MigrationNotificationManager::MigrationNotificationManager( |
| content::BrowserContext* context) |
| : context_(context) {} |
| |
| MigrationNotificationManager::~MigrationNotificationManager() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void MigrationNotificationManager::ShowMigrationInfoDialog( |
| MigrationDestination destination, |
| base::Time migration_start_time, |
| base::OnceClosure migration_callback) { |
| if (destination == MigrationDestination::kDelete && |
| !base::FeatureList::IsEnabled(features::kSkyVaultV3)) { |
| LOG(ERROR) << "Destination set to MigrationDestination::kDelete, but the " |
| "flag is disabled; ignoring."; |
| return; |
| } |
| LocalFilesMigrationDialog::Show(destination, migration_start_time, |
| std::move(migration_callback)); |
| } |
| |
| void MigrationNotificationManager::ShowMigrationProgressNotification( |
| MigrationDestination destination) { |
| DCHECK(IsCloudDestination(destination)); |
| |
| std::u16string provider_str = CloudProviderToString(destination); |
| |
| std::u16string title = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_PROGRESS_TITLE), |
| provider_str, |
| /*offset=*/nullptr); |
| std::u16string message = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_PROGRESS_MESSAGE), |
| provider_str, |
| /*offset=*/nullptr); |
| |
| auto notification = CreateNotificationPtr(title, message); |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile())->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void MigrationNotificationManager::ShowMigrationCompletedNotification( |
| MigrationDestination destination, |
| const base::FilePath& destination_path) { |
| DCHECK(IsCloudDestination(destination)); |
| |
| std::u16string provider_str = CloudProviderToString(destination); |
| std::u16string folder_name = destination_path.BaseName().AsUTF16Unsafe(); |
| |
| std::u16string title = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_COMPLETED_TITLE), |
| {folder_name, provider_str}, |
| /*offsets=*/nullptr); |
| std::u16string message = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16( |
| IDS_POLICY_SKYVAULT_MIGRATION_COMPLETED_MESSAGE), |
| provider_str, |
| /*offset=*/nullptr); |
| std::u16string button = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_COMPLETED_BUTTON), |
| provider_str, |
| /*offset=*/nullptr); |
| |
| auto notification = CreateNotificationPtr( |
| title, message, |
| base::BindRepeating(&HandleCompletedNotificationClick, profile(), |
| destination_path)); |
| notification->set_buttons({message_center::ButtonInfo(button)}); |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile())->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void MigrationNotificationManager::ShowDeletionCompletedNotification() { |
| std::u16string title = |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_DELETION_COMPLETED_TITLE); |
| std::u16string message = |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_DELETION_COMPLETED_MESSAGE); |
| auto notification = CreateNotificationPtr(title, message); |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile())->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void MigrationNotificationManager::ShowMigrationErrorNotification( |
| MigrationDestination destination, |
| const std::string& folder_name, |
| const base::FilePath& error_log_path) { |
| DCHECK(!error_log_path.empty()); |
| DCHECK(IsCloudDestination(destination)); |
| |
| std::u16string provider_str = CloudProviderToString(destination); |
| |
| std::u16string title = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_ERROR_TITLE), |
| provider_str, |
| /*offset=*/nullptr); |
| std::u16string message = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_ERROR_MESSAGE), |
| {base::UTF8ToUTF16(folder_name), provider_str}, |
| /*offsets=*/nullptr); |
| std::u16string button = |
| l10n_util::GetStringUTF16(IDS_POLICY_SKYVAULT_MIGRATION_ERROR_BUTTON); |
| |
| auto notification = |
| CreateNotificationPtr(title, message, |
| base::BindRepeating(&HandleErrorNotificationClick, |
| profile(), error_log_path)); |
| notification->set_buttons({message_center::ButtonInfo(button)}); |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile())->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void MigrationNotificationManager::ShowConfigurationErrorNotification( |
| MigrationDestination destination) { |
| DCHECK(IsCloudDestination(destination)); |
| |
| std::u16string provider_str = CloudProviderToString(destination); |
| |
| std::u16string title = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16( |
| IDS_POLICY_SKYVAULT_MIGRATION_CONFIG_ERROR_TITLE), |
| provider_str, |
| /*offset=*/nullptr); |
| std::u16string message = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16( |
| IDS_POLICY_SKYVAULT_MIGRATION_CONFIG_ERROR_MESSAGE), |
| provider_str, |
| /*offset=*/nullptr); |
| |
| auto notification = CreateNotificationPtr(title, message, |
| /*callback=*/base::DoNothing()); |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile())->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| |
| base::CallbackListSubscription |
| MigrationNotificationManager::ShowOneDriveSignInNotification( |
| SignInCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (sign_in_callbacks_.empty()) { |
| policy::skyvault_ui_utils::ShowSignInNotification( |
| Profile::FromBrowserContext(context_), /*id=*/0, |
| UploadTrigger::kMigration, |
| /*file_path=*/base::FilePath(), |
| base::BindOnce(&MigrationNotificationManager::OnSignInResponse, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // sign_in_callback_subscriptions_.emplace_back( |
| return sign_in_callbacks_.Add(std::move(callback)); |
| } |
| |
| void MigrationNotificationManager::CloseNotifications() { |
| // TODO(b/349097807): Potential race condition. When migration stopping is |
| // fully implemented, make sure this runs after uploads were already stopped |
| // (otherwise upload might fail before it's cancelled) and/or post this to |
| // same sequence & fail new requests that come in (if closing exactly when an |
| // upload job was getting paused for sign in). |
| CloseNotification(profile()); |
| } |
| |
| void MigrationNotificationManager::CloseDialog() { |
| LocalFilesMigrationDialog* dialog = LocalFilesMigrationDialog::GetDialog(); |
| if (dialog) { |
| dialog->Close(); |
| } |
| } |
| |
| Profile* MigrationNotificationManager::profile() { |
| return Profile::FromBrowserContext(context_); |
| } |
| |
| void MigrationNotificationManager::OnSignInResponse(base::File::Error error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (error == base::File::Error::FILE_OK) { |
| // This is only reached for OneDrive. |
| ShowMigrationProgressNotification(MigrationDestination::kOneDrive); |
| } |
| // If there was an error, the notification will be shown when migration fails. |
| sign_in_callbacks_.Notify(error); |
| } |
| |
| // static |
| MigrationNotificationManagerFactory* |
| MigrationNotificationManagerFactory::GetInstance() { |
| static base::NoDestructor<MigrationNotificationManagerFactory> factory; |
| return factory.get(); |
| } |
| |
| MigrationNotificationManager* |
| MigrationNotificationManagerFactory::GetForBrowserContext( |
| content::BrowserContext* context) { |
| return static_cast<MigrationNotificationManager*>( |
| GetInstance()->GetServiceForBrowserContext(context, /*create=*/true)); |
| } |
| |
| MigrationNotificationManagerFactory::MigrationNotificationManagerFactory() |
| : ProfileKeyedServiceFactory( |
| "MigrationNotificationManager", |
| ProfileSelections::Builder() |
| .WithRegular(ProfileSelection::kOriginalOnly) |
| // TODO(crbug.com/41488885): Check if this service is needed for |
| // Ash Internals. |
| .WithAshInternals(ProfileSelection::kOriginalOnly) |
| .Build()) { |
| DependsOn(NotificationDisplayServiceFactory::GetInstance()); |
| } |
| |
| MigrationNotificationManagerFactory::~MigrationNotificationManagerFactory() = |
| default; |
| |
| bool MigrationNotificationManagerFactory::ServiceIsNULLWhileTesting() const { |
| return true; |
| } |
| |
| std::unique_ptr<KeyedService> |
| MigrationNotificationManagerFactory::BuildServiceInstanceForBrowserContext( |
| content::BrowserContext* context) const { |
| return std::make_unique<MigrationNotificationManager>(context); |
| } |
| |
| } // namespace policy::local_user_files |