| // Copyright 2021 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/extensions/file_manager/system_notification_manager.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "ash/components/arc/arc_prefs.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/webui/file_manager/file_manager_ui.h" |
| #include "ash/webui/settings/public/constants/routes.mojom.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "chrome/browser/ash/drive/file_system_util.h" |
| #include "chrome/browser/ash/extensions/file_manager/drivefs_event_router.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/io_task.h" |
| #include "chrome/browser/ash/file_manager/io_task_controller.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/policy/dlp/dialogs/files_policy_dialog.h" |
| #include "chrome/browser/ash/policy/dlp/files_policy_notification_manager.h" |
| #include "chrome/browser/ash/policy/dlp/files_policy_notification_manager_factory.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/ui/settings_window_manager_chromeos.h" |
| #include "chrome/common/extensions/api/file_manager_private.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/browser/extension_event_histogram_value.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/chromeos/strings/grit/ui_chromeos_strings.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| |
| namespace file_manager { |
| namespace { |
| |
| namespace fmp = extensions::api::file_manager_private; |
| |
| using base::BindRepeating; |
| using base::MakeRefCounted; |
| using base::RepeatingClosure; |
| using base::UTF8ToUTF16; |
| using extensions::Event; |
| using file_manager::io_task::IOTaskController; |
| using file_manager::io_task::IOTaskId; |
| using file_manager::io_task::OperationType; |
| using file_manager::io_task::ProgressStatus; |
| using file_manager::util::GetDisplayablePath; |
| using fmp::MountCompletedEvent; |
| using fmp::ToString; |
| using l10n_util::GetStringFUTF16; |
| using l10n_util::GetStringUTF16; |
| using message_center::ButtonInfo; |
| using message_center::HandleNotificationClickDelegate; |
| using message_center::Notification; |
| using message_center::NotificationDelegate; |
| using message_center::NotifierId; |
| using message_center::RichNotificationData; |
| using message_center::SystemNotificationWarningLevel; |
| using NotificationPtr = std::unique_ptr<Notification>; |
| using DelegatePtr = scoped_refptr<NotificationDelegate>; |
| using OperationID = storage::FileSystemOperationRunner::OperationID; |
| using FileSystemContextPtr = scoped_refptr<storage::FileSystemContext>; |
| |
| using enum extensions::events::HistogramValue; |
| using enum message_center::NotificationType; |
| |
| void CancelCopyOnIOThread(FileSystemContextPtr file_system_context, |
| OperationID operation_id) { |
| file_system_context->operation_runner()->Cancel( |
| operation_id, base::BindOnce([](base::File::Error error) { |
| DLOG_IF(WARNING, error != base::File::FILE_OK) |
| << "Failed to cancel copy: " << error; |
| })); |
| } |
| |
| constexpr char kSwaFileOperationPrefix[] = "swa-file-operation-"; |
| |
| bool NotificationIdToOperationId(const std::string& notification_id, |
| OperationID* operation_id) { |
| *operation_id = 0; |
| std::string id_string; |
| if (base::RemoveChars(notification_id, kSwaFileOperationPrefix, &id_string)) { |
| if (base::StringToUint64(id_string, operation_id)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void RecordDeviceNotificationMetric(DeviceNotificationUmaType type) { |
| UMA_HISTOGRAM_ENUMERATION(kNotificationShowHistogramName, type); |
| } |
| |
| void RecordDeviceNotificationUserActionMetric( |
| DeviceNotificationUserActionUmaType type) { |
| UMA_HISTOGRAM_ENUMERATION(kNotificationUserActionHistogramName, type); |
| } |
| |
| std::u16string GetIOTaskMessage(Profile* profile, |
| const ProgressStatus& status) { |
| int single_file_message_id; |
| int multiple_file_message_id; |
| |
| // Display special copy to help users understand that pasting files to "My |
| // Drive" does not mean that they are immediately synced. |
| drive::DriveIntegrationService* const service = |
| drive::util::GetIntegrationServiceByProfile(profile); |
| bool is_destination_drive = |
| service && service->GetMountPointPath().IsParent( |
| status.GetDestinationFolder().path()); |
| |
| switch (status.type) { |
| case OperationType::kCopy: |
| if (is_destination_drive) { |
| single_file_message_id = IDS_FILE_BROWSER_PREPARING_FILE_NAME_MY_DRIVE; |
| multiple_file_message_id = IDS_FILE_BROWSER_PREPARING_ITEMS_MY_DRIVE; |
| } else { |
| single_file_message_id = IDS_FILE_BROWSER_COPY_FILE_NAME; |
| multiple_file_message_id = IDS_FILE_BROWSER_COPY_ITEMS_REMAINING; |
| } |
| break; |
| |
| case OperationType::kMove: |
| if (is_destination_drive) { |
| single_file_message_id = IDS_FILE_BROWSER_PREPARING_FILE_NAME_MY_DRIVE; |
| multiple_file_message_id = IDS_FILE_BROWSER_PREPARING_ITEMS_MY_DRIVE; |
| } else { |
| single_file_message_id = IDS_FILE_BROWSER_MOVE_FILE_NAME; |
| multiple_file_message_id = IDS_FILE_BROWSER_MOVE_ITEMS_REMAINING; |
| } |
| break; |
| |
| case OperationType::kDelete: |
| single_file_message_id = IDS_FILE_BROWSER_DELETE_FILE_NAME; |
| multiple_file_message_id = IDS_FILE_BROWSER_DELETE_ITEMS_REMAINING; |
| break; |
| |
| case OperationType::kExtract: |
| single_file_message_id = IDS_FILE_BROWSER_EXTRACT_FILE_NAME; |
| multiple_file_message_id = IDS_FILE_BROWSER_EXTRACT_ITEMS_REMAINING; |
| break; |
| |
| case OperationType::kZip: |
| single_file_message_id = IDS_FILE_BROWSER_ZIP_FILE_NAME; |
| multiple_file_message_id = IDS_FILE_BROWSER_ZIP_ITEMS_REMAINING; |
| break; |
| |
| case OperationType::kRestoreToDestination: |
| single_file_message_id = IDS_FILE_BROWSER_RESTORING_FROM_TRASH_FILE_NAME; |
| multiple_file_message_id = |
| IDS_FILE_BROWSER_RESTORING_FROM_TRASH_ITEMS_REMAINING; |
| break; |
| |
| case OperationType::kTrash: |
| single_file_message_id = IDS_FILE_BROWSER_MOVE_TO_TRASH_FILE_NAME; |
| multiple_file_message_id = IDS_FILE_BROWSER_MOVE_TO_TRASH_ITEMS_REMAINING; |
| break; |
| |
| case OperationType::kEmptyTrash: |
| case OperationType::kRestore: |
| default: |
| NOTREACHED() << "Unexpected operation type " << status.type; |
| } |
| |
| if (status.sources.size() > 1) { |
| return GetStringFUTF16(multiple_file_message_id, |
| base::NumberToString16(status.sources.size())); |
| } |
| |
| return GetStringFUTF16( |
| single_file_message_id, |
| UTF8ToUTF16(GetDisplayablePath(profile, status.sources.back().url) |
| .value_or(base::FilePath()) |
| .BaseName() |
| .value())); |
| } |
| } // namespace |
| |
| std::string GetNotificationId(io_task::IOTaskId task_id) { |
| return base::StrCat({kSwaFileOperationPrefix, base::NumberToString(task_id)}); |
| } |
| |
| NotificationPtr CreateSystemNotification( |
| const std::string& notification_id, |
| const std::u16string& title, |
| const std::u16string& message, |
| DelegatePtr delegate, |
| message_center::RichNotificationData optional_fields) { |
| return ash::CreateSystemNotificationPtr( |
| NOTIFICATION_TYPE_SIMPLE, notification_id, title, message, |
| GetStringUTF16(IDS_FILEMANAGER_APP_NAME), GURL(), NotifierId(), |
| optional_fields, std::move(delegate), ash::kFolderIcon, |
| SystemNotificationWarningLevel::NORMAL); |
| } |
| |
| NotificationPtr CreateSystemNotification(const std::string& notification_id, |
| int title_id, |
| int message_id, |
| DelegatePtr delegate) { |
| return CreateSystemNotification(notification_id, GetStringUTF16(title_id), |
| GetStringUTF16(message_id), |
| std::move(delegate)); |
| } |
| |
| NotificationPtr CreateSystemNotification( |
| const std::string& notification_id, |
| const std::u16string& title, |
| const std::u16string& message, |
| const RepeatingClosure& click_callback) { |
| return CreateSystemNotification( |
| notification_id, title, message, |
| MakeRefCounted<HandleNotificationClickDelegate>(click_callback)); |
| } |
| |
| SystemNotificationManager::SystemNotificationManager(Profile* profile) |
| : profile_(profile), app_name_(GetStringUTF16(IDS_FILEMANAGER_APP_NAME)) {} |
| |
| SystemNotificationManager::~SystemNotificationManager() = default; |
| |
| bool SystemNotificationManager::DoFilesSwaWindowsExist() { |
| return ash::file_manager::FileManagerUI::GetNumInstances() != 0; |
| } |
| |
| NotificationPtr SystemNotificationManager::CreateNotification( |
| const std::string& notification_id, |
| const std::u16string& title, |
| const std::u16string& message) { |
| return CreateSystemNotification( |
| notification_id, title, message, |
| BindRepeating(&SystemNotificationManager::Dismiss, |
| weak_ptr_factory_.GetWeakPtr(), notification_id)); |
| } |
| |
| NotificationPtr SystemNotificationManager::CreateNotification( |
| const std::string& notification_id, |
| int title_id, |
| int message_id) { |
| return CreateNotification(notification_id, GetStringUTF16(title_id), |
| GetStringUTF16(message_id)); |
| } |
| |
| void SystemNotificationManager::HandleProgressClick( |
| const std::string& notification_id, |
| std::optional<int> button_index) { |
| if (button_index) { |
| // Cancel the copy operation. |
| FileSystemContextPtr file_system_context = |
| util::GetFileManagerFileSystemContext(profile_); |
| OperationID operation_id; |
| if (NotificationIdToOperationId(notification_id, &operation_id)) { |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&CancelCopyOnIOThread, file_system_context, |
| operation_id)); |
| } |
| } |
| } |
| |
| NotificationPtr SystemNotificationManager::CreateProgressNotification( |
| const std::string& notification_id, |
| const std::u16string& title, |
| const std::u16string& message, |
| int progress) { |
| RichNotificationData rich_data; |
| rich_data.progress = progress; |
| rich_data.progress_status = message; |
| |
| return ash::CreateSystemNotificationPtr( |
| NOTIFICATION_TYPE_PROGRESS, notification_id, title, message, app_name_, |
| GURL(), NotifierId(), rich_data, |
| MakeRefCounted<HandleNotificationClickDelegate>( |
| BindRepeating(&SystemNotificationManager::HandleProgressClick, |
| weak_ptr_factory_.GetWeakPtr(), notification_id)), |
| ash::kFolderIcon, SystemNotificationWarningLevel::NORMAL); |
| } |
| |
| NotificationPtr SystemNotificationManager::CreateIOTaskProgressNotification( |
| IOTaskId task_id, |
| const std::string& notification_id, |
| const std::u16string& title, |
| const std::u16string& message, |
| const bool paused, |
| int progress) { |
| RichNotificationData rich_data; |
| rich_data.progress = progress; |
| rich_data.progress_status = message; |
| |
| // Button click delegate to handle the state::PAUSED IOTask case, where the |
| // user [X] closes this system notification, but did not press its buttons. |
| // In that case, default behavior is to auto-click button 1. |
| // TODO(b/255264604): ask UX here, which button should be the default? |
| class IOTaskProgressNotificationClickDelegate |
| : public HandleNotificationClickDelegate { |
| public: |
| IOTaskProgressNotificationClickDelegate(const ButtonClickCallback& callback, |
| bool paused) |
| : HandleNotificationClickDelegate(callback), paused_(paused) {} |
| |
| void Close(bool by_user) override { |
| if (paused_ && by_user) { // Click button at index 1. |
| HandleNotificationClickDelegate::Click(1, {}); |
| } |
| } |
| |
| protected: |
| ~IOTaskProgressNotificationClickDelegate() override = default; |
| |
| private: |
| bool paused_; // True if the IOTask is in state::PAUSED. |
| }; |
| |
| auto notification_click_handler = BindRepeating( |
| &SystemNotificationManager::HandleIOTaskProgressNotificationClick, |
| weak_ptr_factory_.GetWeakPtr(), task_id, notification_id, paused); |
| |
| auto notification = ash::CreateSystemNotificationPtr( |
| NOTIFICATION_TYPE_PROGRESS, notification_id, title, message, app_name_, |
| GURL(), NotifierId(), rich_data, |
| MakeRefCounted<IOTaskProgressNotificationClickDelegate>( |
| std::move(notification_click_handler), paused), |
| ash::kFolderIcon, SystemNotificationWarningLevel::NORMAL); |
| |
| std::vector<ButtonInfo> notification_buttons; |
| |
| // Add "Cancel" button. |
| notification_buttons.emplace_back( |
| GetStringUTF16(IDS_FILE_BROWSER_CANCEL_LABEL)); |
| |
| if (paused) { // For paused tasks, add "Open Files app" button. |
| notification_buttons.emplace_back( |
| GetStringUTF16(IDS_REMOVABLE_DEVICE_NAVIGATION_BUTTON_LABEL)); |
| } |
| |
| notification->set_buttons(notification_buttons); |
| return notification; |
| } |
| |
| void SystemNotificationManager::HandleIOTaskProgressNotificationClick( |
| IOTaskId task_id, |
| const std::string& notification_id, |
| const bool paused, |
| std::optional<int> button_index) { |
| if (!button_index.has_value()) { |
| return; |
| } |
| |
| if (button_index.value() == 0) { |
| CancelTask(task_id); |
| } |
| |
| if (paused && button_index.value() == 1) { |
| platform_util::ShowItemInFolder( |
| profile_, file_manager::util::GetMyFilesFolderForProfile(profile_)); |
| Dismiss(notification_id); |
| } |
| } |
| |
| void SystemNotificationManager::Dismiss(const std::string& notification_id) { |
| GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT, |
| notification_id); |
| } |
| |
| static const char kDeviceFailNotificationId[] = "swa-device-fail-id"; |
| |
| void SystemNotificationManager::HandleDeviceEvent( |
| const fmp::DeviceEvent& event) { |
| NotificationPtr notification; |
| const std::string id = ToString(event.type); |
| switch (event.type) { |
| case fmp::DeviceEventType::kDisabled: |
| notification = |
| CreateNotification(id, IDS_REMOVABLE_DEVICE_DETECTION_TITLE, |
| IDS_EXTERNAL_STORAGE_DISABLED_MESSAGE); |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_EXTERNAL_STORAGE_DISABLED); |
| break; |
| |
| case fmp::DeviceEventType::kRemoved: |
| // Hide device fail & storage disabled notifications. |
| GetNotificationDisplayService()->Close( |
| NotificationHandler::Type::TRANSIENT, kDeviceFailNotificationId); |
| GetNotificationDisplayService()->Close( |
| NotificationHandler::Type::TRANSIENT, |
| ToString(fmp::DeviceEventType::kDisabled)); |
| // Remove the device from the mount status map. |
| mount_status_.erase(event.device_path); |
| break; |
| |
| case fmp::DeviceEventType::kHardUnplugged: |
| notification = CreateNotification(id, IDS_DEVICE_HARD_UNPLUGGED_TITLE, |
| IDS_DEVICE_HARD_UNPLUGGED_MESSAGE); |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_HARD_UNPLUGGED); |
| break; |
| |
| case fmp::DeviceEventType::kFormatStart: |
| notification = CreateNotification( |
| id, |
| GetStringFUTF16(IDS_FILE_BROWSER_FORMAT_DIALOG_TITLE, |
| UTF8ToUTF16(event.device_label)), |
| GetStringFUTF16(IDS_FILE_BROWSER_FORMAT_PROGRESS_MESSAGE, |
| UTF8ToUTF16(event.device_label))); |
| RecordDeviceNotificationMetric(DeviceNotificationUmaType::FORMAT_START); |
| break; |
| |
| case fmp::DeviceEventType::kFormatSuccess: |
| case fmp::DeviceEventType::kFormatFail: |
| case fmp::DeviceEventType::kPartitionFail: { |
| // Hide the formatting notification. |
| GetNotificationDisplayService()->Close( |
| NotificationHandler::Type::TRANSIENT, |
| ToString(fmp::DeviceEventType::kFormatStart)); |
| std::u16string message; |
| if (event.type == fmp::DeviceEventType::kFormatSuccess) { |
| message = GetStringFUTF16(IDS_FILE_BROWSER_FORMAT_SUCCESS_MESSAGE, |
| UTF8ToUTF16(event.device_label)); |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::FORMAT_SUCCESS); |
| } else { |
| message = GetStringFUTF16(IDS_FILE_BROWSER_FORMAT_FAILURE_MESSAGE, |
| UTF8ToUTF16(event.device_label)); |
| RecordDeviceNotificationMetric( |
| event.type == fmp::DeviceEventType::kFormatFail |
| ? DeviceNotificationUmaType::FORMAT_FAIL |
| : DeviceNotificationUmaType::PARTITION_FAIL); |
| } |
| notification = CreateNotification( |
| id, |
| GetStringFUTF16(IDS_FILE_BROWSER_FORMAT_DIALOG_TITLE, |
| UTF8ToUTF16(event.device_label)), |
| std::move(message)); |
| break; |
| } |
| |
| case fmp::DeviceEventType::kPartitionStart: |
| case fmp::DeviceEventType::kPartitionSuccess: |
| // No-op. |
| break; |
| |
| case fmp::DeviceEventType::kRenameFail: |
| notification = |
| CreateNotification(id, IDS_RENAMING_OF_DEVICE_FAILED_TITLE, |
| IDS_RENAMING_OF_DEVICE_FINISHED_FAILURE_MESSAGE); |
| RecordDeviceNotificationMetric(DeviceNotificationUmaType::RENAME_FAIL); |
| break; |
| |
| case fmp::DeviceEventType::kNone: |
| case fmp::DeviceEventType::kRenameStart: |
| case fmp::DeviceEventType::kRenameSuccess: |
| default: |
| VLOG(1) << "No notification for device event " << id; |
| break; |
| } |
| |
| if (notification) { |
| GetNotificationDisplayService()->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| } |
| |
| static const char kBulkPinningNotificationId[] = "drive-bulk-pinning-error"; |
| |
| void SystemNotificationManager::HandleBulkPinningNotificationClick() { |
| chrome::SettingsWindowManager::GetInstance()->ShowOSSettings( |
| profile_, chromeos::settings::mojom::kGoogleDriveSubpagePath); |
| GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT, |
| kBulkPinningNotificationId); |
| } |
| |
| NotificationPtr SystemNotificationManager::MakeBulkPinningErrorNotification( |
| const Event& event) { |
| // Parse the event args as a bulk-pinning progress struct. |
| DCHECK(!event.event_args.empty()); |
| auto progress = fmp::BulkPinProgress::FromValue(event.event_args[0]); |
| if (!progress) { |
| LOG(ERROR) << "Cannot parse BulkPinProgress from " << event.event_args[0]; |
| return nullptr; |
| } |
| |
| // Remember the bulk-pinning stage. |
| using enum BulkPinStage; |
| const BulkPinStage old_stage = bulk_pin_stage_; |
| bulk_pin_stage_ = progress->stage; |
| |
| if (!progress->should_pin) { |
| return nullptr; |
| } |
| |
| if (old_stage != BulkPinStage::kListingFiles && |
| old_stage != BulkPinStage::kSyncing) { |
| return nullptr; |
| } |
| |
| // Check the bulk-pinning stage. |
| switch (bulk_pin_stage_) { |
| case BulkPinStage::kNone: |
| case BulkPinStage::kStopped: |
| case BulkPinStage::kPausedOffline: |
| case BulkPinStage::kPausedBatterySaver: |
| case BulkPinStage::kGettingFreeSpace: |
| case BulkPinStage::kListingFiles: |
| case BulkPinStage::kSyncing: |
| case BulkPinStage::kSuccess: |
| return nullptr; |
| |
| case BulkPinStage::kNotEnoughSpace: |
| case BulkPinStage::kCannotGetFreeSpace: |
| case BulkPinStage::kCannotListFiles: |
| case BulkPinStage::kCannotEnableDocsOffline: |
| break; |
| } |
| |
| VLOG(1) << "Creating bulk-pinning error notification"; |
| int title_id, message_id; |
| |
| if (bulk_pin_stage_ == BulkPinStage::kNotEnoughSpace) { |
| if (progress->emptied_queue) { |
| title_id = IDS_FILE_BROWSER_DRIVE_SYNC_TURNED_OFF_TITLE; |
| message_id = |
| IDS_FILE_BROWSER_BULK_PINNING_NOT_ENOUGH_SPACE_NOTIFICATION_2; |
| } else { |
| title_id = IDS_FILE_BROWSER_DRIVE_SYNC_ERROR_TITLE; |
| message_id = IDS_FILE_BROWSER_BULK_PINNING_NOT_ENOUGH_SPACE_NOTIFICATION; |
| } |
| } else { |
| title_id = IDS_FILE_BROWSER_DRIVE_SYNC_TURNED_OFF_TITLE; |
| message_id = IDS_FILE_BROWSER_BULK_PINNING_ERROR; |
| } |
| |
| NotificationPtr notification = CreateSystemNotification( |
| kBulkPinningNotificationId, GetStringUTF16(title_id), |
| GetStringUTF16(message_id), |
| BindRepeating( |
| &SystemNotificationManager::HandleBulkPinningNotificationClick, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| return notification; |
| } |
| |
| NotificationPtr SystemNotificationManager::MakeDriveSyncErrorNotification( |
| const Event& event) { |
| DCHECK(!event.event_args.empty()); |
| auto sync_error = fmp::DriveSyncErrorEvent::FromValue(event.event_args[0]); |
| if (!sync_error) { |
| LOG(ERROR) << "Cannot parse DriveSyncErrorEvent from " |
| << event.event_args[0]; |
| return nullptr; |
| } |
| |
| const std::u16string title = |
| GetStringUTF16(IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL); |
| const std::string id = ToString(sync_error->type); |
| const GURL file_url(sync_error->file_url); |
| |
| switch (sync_error->type) { |
| case fmp::DriveSyncErrorType::kDeleteWithoutPermission: |
| return CreateNotification( |
| id, title, |
| GetStringFUTF16(IDS_FILE_BROWSER_SYNC_DELETE_WITHOUT_PERMISSION_ERROR, |
| util::GetDisplayableFileName16(file_url))); |
| |
| case fmp::DriveSyncErrorType::kServiceUnavailable: |
| return CreateNotification( |
| id, IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL, |
| IDS_FILE_BROWSER_SYNC_SERVICE_UNAVAILABLE_ERROR); |
| |
| case fmp::DriveSyncErrorType::kNoServerSpace: |
| return CreateNotification( |
| id, title, GetStringUTF16(IDS_FILE_BROWSER_SYNC_NO_SERVER_SPACE)); |
| |
| case fmp::DriveSyncErrorType::kNoServerSpaceOrganization: |
| return CreateNotification( |
| id, title, |
| GetStringUTF16(IDS_FILE_BROWSER_SYNC_NO_SERVER_SPACE_ORGANIZATION)); |
| |
| case fmp::DriveSyncErrorType::kNoLocalSpace: |
| return CreateNotification(id, IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL, |
| IDS_FILE_BROWSER_DRIVE_OUT_OF_SPACE_HEADER); |
| |
| case fmp::DriveSyncErrorType::kMisc: |
| return CreateNotification( |
| id, title, |
| GetStringFUTF16(IDS_FILE_BROWSER_SYNC_MISC_ERROR, |
| util::GetDisplayableFileName16(file_url))); |
| |
| case fmp::DriveSyncErrorType::kNoSharedDriveSpace: |
| if (!sync_error->shared_drive.has_value()) { |
| DLOG(WARNING) << "No shared drive provided for error notification"; |
| return nullptr; |
| } |
| |
| return CreateNotification( |
| id, title, |
| GetStringFUTF16(IDS_FILE_BROWSER_SYNC_ERROR_SHARED_DRIVE_OUT_OF_SPACE, |
| UTF8ToUTF16(sync_error->shared_drive.value()))); |
| |
| case fmp::DriveSyncErrorType::kNone: |
| break; |
| } |
| |
| LOG(ERROR) << "Unexpected Drive sync error: " |
| << base::to_underlying(sync_error->type); |
| return nullptr; |
| } |
| |
| static const char kDriveDialogId[] = "swa-drive-confirm-dialog"; |
| |
| void SystemNotificationManager::HandleDriveDialogClick( |
| std::optional<int> button_index) { |
| drivefs::mojom::DialogResult result = drivefs::mojom::DialogResult::kDismiss; |
| if (button_index) { |
| if (button_index.value() == 1) { |
| result = drivefs::mojom::DialogResult::kAccept; |
| } else { |
| result = drivefs::mojom::DialogResult::kReject; |
| } |
| } |
| // Send the dialog result to the callback stored in DriveFS on dialog |
| // creation. |
| if (drivefs_event_router_) { |
| drivefs_event_router_->OnDialogResult(result); |
| } |
| GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT, |
| kDriveDialogId); |
| } |
| |
| NotificationPtr SystemNotificationManager::MakeDriveConfirmDialogNotification( |
| const Event& event) { |
| DCHECK(!event.event_args.empty()); |
| auto dialog_event = |
| fmp::DriveConfirmDialogEvent::FromValue(event.event_args[0]); |
| if (!dialog_event) { |
| LOG(ERROR) << "Cannot parse DriveConfirmDialogEvent from " |
| << event.event_args[0]; |
| return nullptr; |
| } |
| |
| NotificationPtr notification = CreateSystemNotification( |
| kDriveDialogId, IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL, |
| IDS_FILE_BROWSER_OFFLINE_ENABLE_MESSAGE, |
| MakeRefCounted<HandleNotificationClickDelegate>( |
| BindRepeating(&SystemNotificationManager::HandleDriveDialogClick, |
| weak_ptr_factory_.GetWeakPtr()))); |
| |
| std::vector<ButtonInfo> buttons; |
| buttons.emplace_back(GetStringUTF16(IDS_FILE_BROWSER_OFFLINE_ENABLE_REJECT)); |
| buttons.emplace_back(GetStringUTF16(IDS_FILE_BROWSER_OFFLINE_ENABLE_ACCEPT)); |
| notification->set_buttons(buttons); |
| |
| return notification; |
| } |
| |
| void SystemNotificationManager::HandleEvent(const Event& event) { |
| if (event.event_args.empty()) { |
| DLOG(WARNING) << "Ignored empty Event {name: " << event.event_name |
| << ", histogram_value: " << event.histogram_value << "}"; |
| return; |
| } |
| |
| // For some events we always display a system notification regardless of if |
| // there are any SWA windows open. |
| bool force_as_system_notification = false; |
| NotificationPtr notification; |
| switch (event.histogram_value) { |
| case FILE_MANAGER_PRIVATE_ON_DRIVE_SYNC_ERROR: |
| notification = MakeDriveSyncErrorNotification(event); |
| break; |
| |
| case FILE_MANAGER_PRIVATE_ON_DRIVE_CONFIRM_DIALOG: |
| notification = MakeDriveConfirmDialogNotification(event); |
| force_as_system_notification = true; |
| break; |
| |
| case FILE_MANAGER_PRIVATE_ON_BULK_PIN_PROGRESS: |
| notification = MakeBulkPinningErrorNotification(event); |
| force_as_system_notification = true; |
| break; |
| |
| default: |
| VLOG(1) << "Ignored Event {name: " << event.event_name |
| << ", histogram_value: " << event.histogram_value |
| << ", args: " << event.event_args << "}"; |
| return; |
| } |
| |
| if (!notification) { |
| return; |
| } |
| |
| // Check if we need to remove any progress notification when there |
| // are active SWA windows. |
| if (!force_as_system_notification && DoFilesSwaWindowsExist()) { |
| GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT, |
| notification->id()); |
| return; |
| } |
| |
| GetNotificationDisplayService()->Display(NotificationHandler::Type::TRANSIENT, |
| *notification, nullptr); |
| } |
| |
| void SystemNotificationManager::HandleIOTaskProgress( |
| const ProgressStatus& status) { |
| std::string id = GetNotificationId(status.task_id); |
| |
| // If there are any SWA windows open, remove the IOTask progress from system |
| // notifications. |
| if (!status.show_notification || DoFilesSwaWindowsExist()) { |
| Dismiss(id); |
| return; |
| } |
| |
| // If there's a warning or security error, show a data protection |
| // notification. |
| if (status.HasWarning() || status.HasPolicyError()) { |
| policy::FilesPolicyNotificationManager* manager = |
| policy::FilesPolicyNotificationManagerFactory::GetForBrowserContext( |
| profile_); |
| if (!manager) { |
| LOG(ERROR) << "No FilesPolicyNotificationManager instantiated," |
| "can't show policy dialog for task_id " |
| << status.task_id; |
| return; |
| } |
| Dismiss(id); |
| manager->ShowFilesPolicyNotification(id, status); |
| return; |
| } |
| |
| // If the task is currently in the scanning state, show a data protection |
| // progress notification. |
| if (status.IsScanning()) { |
| Dismiss(id); |
| NotificationPtr notification = |
| MakeDataProtectionPolicyProgressNotification(id, status); |
| GetNotificationDisplayService()->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| return; |
| } |
| |
| // If the IOTask state has completed, remove the IOTask progress from system |
| // notifications. |
| if (status.IsCompleted()) { |
| Dismiss(id); |
| return; |
| } |
| |
| // From here state is kQueued, kInProgress, or kPaused. |
| const bool paused = status.IsPaused(); |
| |
| std::u16string title; |
| std::u16string message; |
| if (!paused) { |
| title = app_name_; |
| message = GetIOTaskMessage(profile_, status); |
| } else { |
| title = GetIOTaskMessage(profile_, status); |
| int message_id = IDS_FILE_BROWSER_CONFLICT_DIALOG_MESSAGE; |
| if (status.pause_params.conflict_params.has_value() && |
| status.pause_params.conflict_params->conflict_is_directory) { |
| message_id = IDS_FILE_BROWSER_CONFLICT_DIALOG_FOLDER_MESSAGE; |
| } |
| auto& item_name = status.pause_params.conflict_params->conflict_name; |
| message = GetStringFUTF16(message_id, UTF8ToUTF16(item_name)); |
| } |
| |
| int progress = 0; |
| if (status.total_bytes > 0) { |
| progress = status.bytes_transferred * 100.0 / status.total_bytes; |
| } |
| |
| NotificationPtr notification = CreateIOTaskProgressNotification( |
| status.task_id, id, title, message, paused, progress); |
| |
| GetNotificationDisplayService()->Display(NotificationHandler::Type::TRANSIENT, |
| *notification, |
| /*metadata=*/nullptr); |
| } |
| |
| constexpr char kRemovableNotificationId[] = "swa-removable-device-id"; |
| |
| void SystemNotificationManager::HandleRemovableNotificationClick( |
| const std::string& path, |
| const std::vector<DeviceNotificationUserActionUmaType>& |
| uma_types_for_buttons, |
| std::optional<int> button_index) { |
| if (button_index) { |
| if (button_index.value() == 0) { |
| base::FilePath volume_root(path); |
| platform_util::ShowItemInFolder(profile_, volume_root); |
| } else { |
| chrome::SettingsWindowManager::GetInstance()->ShowOSSettings( |
| profile_, chromeos::settings::mojom::kExternalStorageSubpagePath); |
| } |
| if (base::checked_cast<size_t>(button_index.value()) < |
| uma_types_for_buttons.size()) { |
| RecordDeviceNotificationUserActionMetric( |
| uma_types_for_buttons.at(button_index.value())); |
| } |
| } |
| |
| GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT, |
| kRemovableNotificationId); |
| } |
| |
| void SystemNotificationManager::HandleDataProtectionPolicyNotificationClick( |
| RepeatingClosure proceed_callback, |
| RepeatingClosure cancel_callback, |
| std::optional<int> button_index) { |
| if (!button_index.has_value()) { |
| return; |
| } |
| |
| if (button_index.value() == 0) { |
| proceed_callback.Run(); |
| } |
| |
| if (button_index.value() == 1 && cancel_callback) { |
| cancel_callback.Run(); |
| } |
| } |
| |
| NotificationPtr SystemNotificationManager::MakeMountErrorNotification( |
| MountCompletedEvent& event, |
| const Volume& volume) { |
| const auto it = mount_status_.find(volume.storage_device_path().value()); |
| if (it == mount_status_.end()) { |
| return nullptr; |
| } |
| |
| const std::u16string title = |
| GetStringUTF16(IDS_REMOVABLE_DEVICE_DETECTION_TITLE); |
| std::u16string message; |
| std::vector<ButtonInfo> buttons; |
| std::vector<DeviceNotificationUserActionUmaType> uma_types_for_buttons; |
| switch (it->second) { |
| // We have either an unsupported or unknown filesystem on the mount. |
| case MOUNT_STATUS_ONLY_PARENT_ERROR: |
| case MOUNT_STATUS_CHILD_ERROR: |
| if (event.status == fmp::MountError::kUnsupportedFilesystem) { |
| if (volume.drive_label().empty()) { |
| message = GetStringUTF16(IDS_DEVICE_UNSUPPORTED_DEFAULT_MESSAGE); |
| } else { |
| message = GetStringFUTF16(IDS_DEVICE_UNSUPPORTED_MESSAGE, |
| UTF8ToUTF16(volume.drive_label())); |
| } |
| RecordDeviceNotificationMetric(DeviceNotificationUmaType::DEVICE_FAIL); |
| } else { |
| if (volume.drive_label().empty()) { |
| message = GetStringUTF16(IDS_DEVICE_UNKNOWN_DEFAULT_MESSAGE); |
| } else { |
| message = GetStringFUTF16(IDS_DEVICE_UNKNOWN_MESSAGE, |
| UTF8ToUTF16(volume.drive_label())); |
| } |
| |
| if (!volume.is_read_only()) { |
| // Give a format device button on the notification. |
| buttons.emplace_back(GetStringUTF16(IDS_DEVICE_UNKNOWN_BUTTON_LABEL)); |
| uma_types_for_buttons.push_back( |
| DeviceNotificationUserActionUmaType::OPEN_MEDIA_DEVICE_FAIL); |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_FAIL_UNKNOWN); |
| } else { |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_FAIL_UNKNOWN_READONLY); |
| } |
| } |
| break; |
| |
| // We have a multi-partition device for which at least one mount |
| // failed. |
| case MOUNT_STATUS_MULTIPART_ERROR: |
| if (volume.drive_label().empty()) { |
| message = |
| GetStringUTF16(IDS_MULTIPART_DEVICE_UNSUPPORTED_DEFAULT_MESSAGE); |
| } else { |
| message = GetStringFUTF16(IDS_MULTIPART_DEVICE_UNSUPPORTED_MESSAGE, |
| UTF8ToUTF16(volume.drive_label())); |
| } |
| RecordDeviceNotificationMetric(DeviceNotificationUmaType::DEVICE_FAIL); |
| break; |
| |
| case MOUNT_STATUS_NO_RESULT: |
| case MOUNT_STATUS_SUCCESS: |
| default: |
| VLOG(1) << "Unhandled mount status " << it->second; |
| return nullptr; |
| } |
| |
| NotificationPtr notification = CreateSystemNotification( |
| kDeviceFailNotificationId, title, message, |
| MakeRefCounted<HandleNotificationClickDelegate>(BindRepeating( |
| &SystemNotificationManager::HandleRemovableNotificationClick, |
| weak_ptr_factory_.GetWeakPtr(), volume.mount_path().value(), |
| uma_types_for_buttons))); |
| |
| DCHECK_EQ(buttons.size(), uma_types_for_buttons.size()); |
| notification->set_buttons(buttons); |
| |
| return notification; |
| } |
| |
| SystemNotificationManagerMountStatus |
| SystemNotificationManager::UpdateDeviceMountStatus(MountCompletedEvent& event, |
| const Volume& volume) { |
| SystemNotificationManagerMountStatus status = MOUNT_STATUS_NO_RESULT; |
| const std::string& device_path = volume.storage_device_path().value(); |
| auto device_mount_status = mount_status_.find(device_path); |
| if (device_mount_status == mount_status_.end()) { |
| status = MOUNT_STATUS_NO_RESULT; |
| } else { |
| status = device_mount_status->second; |
| } |
| switch (status) { |
| case MOUNT_STATUS_MULTIPART_ERROR: |
| // Do nothing, status has already been detected. |
| break; |
| case MOUNT_STATUS_ONLY_PARENT_ERROR: |
| if (!volume.is_parent()) { |
| // Hide Device Fail notification. |
| GetNotificationDisplayService()->Close( |
| NotificationHandler::Type::TRANSIENT, kDeviceFailNotificationId); |
| } |
| [[fallthrough]]; |
| case MOUNT_STATUS_NO_RESULT: |
| if (event.status == fmp::MountError::kSuccess) { |
| status = MOUNT_STATUS_SUCCESS; |
| } else if (event.volume_metadata.is_parent_device) { |
| status = MOUNT_STATUS_ONLY_PARENT_ERROR; |
| } else { |
| status = MOUNT_STATUS_CHILD_ERROR; |
| } |
| break; |
| case MOUNT_STATUS_SUCCESS: |
| case MOUNT_STATUS_CHILD_ERROR: |
| if (status == MOUNT_STATUS_SUCCESS && |
| event.status == fmp::MountError::kSuccess) { |
| status = MOUNT_STATUS_SUCCESS; |
| } else { |
| // Multi partition device with at least one partition in error. |
| status = MOUNT_STATUS_MULTIPART_ERROR; |
| } |
| break; |
| } |
| mount_status_[device_path] = status; |
| |
| return status; |
| } |
| |
| NotificationPtr SystemNotificationManager::MakeRemovableNotification( |
| MountCompletedEvent& event, |
| const Volume& volume) { |
| NotificationPtr notification; |
| if (event.status == fmp::MountError::kSuccess) { |
| bool show_settings_button = false; |
| std::u16string title = GetStringUTF16(IDS_REMOVABLE_DEVICE_DETECTION_TITLE); |
| std::u16string message; |
| std::vector<DeviceNotificationUserActionUmaType> uma_types_for_buttons; |
| if (volume.is_read_only() && !volume.is_read_only_removable_device()) { |
| message = GetStringUTF16( |
| IDS_REMOVABLE_DEVICE_NAVIGATION_MESSAGE_READONLY_POLICY); |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_NAVIGATION_READONLY_POLICY); |
| uma_types_for_buttons.push_back( |
| DeviceNotificationUserActionUmaType::OPEN_MEDIA_DEVICE_NAVIGATION); |
| } else { |
| const PrefService* const service = profile_->GetPrefs(); |
| DCHECK(service); |
| bool arc_enabled = service->GetBoolean(arc::prefs::kArcEnabled); |
| bool arc_removable_media_access_enabled = |
| service->GetBoolean(arc::prefs::kArcHasAccessToRemovableMedia); |
| if (!arc_enabled) { |
| message = GetStringUTF16(IDS_REMOVABLE_DEVICE_NAVIGATION_MESSAGE); |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_NAVIGATION); |
| uma_types_for_buttons.push_back( |
| DeviceNotificationUserActionUmaType::OPEN_MEDIA_DEVICE_NAVIGATION); |
| } else if (arc_removable_media_access_enabled) { |
| message = base::StrCat( |
| {GetStringUTF16(IDS_REMOVABLE_DEVICE_NAVIGATION_MESSAGE), u" ", |
| GetStringUTF16( |
| IDS_REMOVABLE_DEVICE_PLAY_STORE_APPS_HAVE_ACCESS_MESSAGE)}); |
| show_settings_button = true; |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_NAVIGATION_APPS_HAVE_ACCESS); |
| uma_types_for_buttons.insert(uma_types_for_buttons.end(), |
| {DeviceNotificationUserActionUmaType:: |
| OPEN_MEDIA_DEVICE_NAVIGATION_ARC, |
| DeviceNotificationUserActionUmaType:: |
| OPEN_SETTINGS_FOR_ARC_STORAGE}); |
| } else { |
| message = base::StrCat( |
| {GetStringUTF16(IDS_REMOVABLE_DEVICE_NAVIGATION_MESSAGE), u" ", |
| GetStringUTF16( |
| IDS_REMOVABLE_DEVICE_ALLOW_PLAY_STORE_ACCESS_MESSAGE)}); |
| show_settings_button = true; |
| RecordDeviceNotificationMetric( |
| DeviceNotificationUmaType::DEVICE_NAVIGATION_ALLOW_APP_ACCESS); |
| uma_types_for_buttons.insert(uma_types_for_buttons.end(), |
| {DeviceNotificationUserActionUmaType:: |
| OPEN_MEDIA_DEVICE_NAVIGATION_ARC, |
| DeviceNotificationUserActionUmaType:: |
| OPEN_SETTINGS_FOR_ARC_STORAGE}); |
| } |
| } |
| |
| notification = CreateSystemNotification( |
| kRemovableNotificationId, title, message, |
| MakeRefCounted<HandleNotificationClickDelegate>(BindRepeating( |
| &SystemNotificationManager::HandleRemovableNotificationClick, |
| weak_ptr_factory_.GetWeakPtr(), volume.mount_path().value(), |
| uma_types_for_buttons))); |
| std::vector<ButtonInfo> notification_buttons; |
| notification_buttons.emplace_back( |
| GetStringUTF16(IDS_REMOVABLE_DEVICE_NAVIGATION_BUTTON_LABEL)); |
| if (show_settings_button) { |
| notification_buttons.emplace_back( |
| GetStringUTF16(IDS_REMOVABLE_DEVICE_OPEN_SETTTINGS_BUTTON_LABEL)); |
| } |
| DCHECK_EQ(notification_buttons.size(), uma_types_for_buttons.size()); |
| notification->set_buttons(notification_buttons); |
| } |
| if (volume.device_type() != ash::DeviceType::kUnknown && |
| !volume.storage_device_path().empty()) { |
| if (UpdateDeviceMountStatus(event, volume) != MOUNT_STATUS_SUCCESS) { |
| notification = MakeMountErrorNotification(event, volume); |
| } |
| } |
| |
| return notification; |
| } |
| |
| NotificationPtr |
| SystemNotificationManager::MakeDataProtectionPolicyProgressNotification( |
| const std::string& notification_id, |
| const ProgressStatus& status) { |
| std::u16string message = |
| status.sources.size() > 1 |
| ? GetStringUTF16(IDS_FILE_BROWSER_SCANNING_LABEL_PLURAL) |
| : GetStringUTF16(IDS_FILE_BROWSER_SCANNING_LABEL); |
| int progress = status.sources_scanned * 100.0 / status.sources.size(); |
| return CreateIOTaskProgressNotification(status.task_id, notification_id, |
| app_name_, message, /*paused=*/false, |
| progress); |
| } |
| |
| void SystemNotificationManager::ShowDataProtectionPolicyDialog( |
| IOTaskId task_id, |
| policy::FilesDialogType type) { |
| policy::FilesPolicyNotificationManager* manager = |
| policy::FilesPolicyNotificationManagerFactory::GetForBrowserContext( |
| profile_); |
| if (!manager) { |
| LOG(ERROR) << "No FilesPolicyNotificationManager instantiated," |
| "can't show policy dialog for task_id " |
| << task_id; |
| return; |
| } |
| manager->ShowDialog(task_id, type); |
| } |
| |
| void SystemNotificationManager::CancelTask(IOTaskId task_id) { |
| if (io_task_controller_) { |
| io_task_controller_->Cancel(task_id); |
| } else { |
| LOG(ERROR) << "No TaskController, can't cancel task_id: " << task_id; |
| } |
| } |
| |
| void SystemNotificationManager::ResumeTask(IOTaskId task_id, |
| policy::Policy policy) { |
| if (io_task_controller_) { |
| io_task::ResumeParams params; |
| params.policy_params->type = policy; |
| io_task_controller_->Resume(task_id, std::move(params)); |
| } else { |
| LOG(ERROR) << "No TaskController, can't resume task_id: " << task_id; |
| } |
| } |
| |
| void SystemNotificationManager::HandleMountCompletedEvent( |
| MountCompletedEvent& event, |
| const Volume& volume) { |
| NotificationPtr notification; |
| |
| switch (event.event_type) { |
| case fmp::MountCompletedEventType::kMount: |
| if (event.should_notify) { |
| notification = MakeRemovableNotification(event, volume); |
| } |
| break; |
| |
| case fmp::MountCompletedEventType::kUnmount: |
| GetNotificationDisplayService()->Close( |
| NotificationHandler::Type::TRANSIENT, kRemovableNotificationId); |
| |
| if (volume.device_type() != ash::DeviceType::kUnknown && |
| !volume.storage_device_path().empty()) { |
| UpdateDeviceMountStatus(event, volume); |
| } |
| break; |
| |
| case fmp::MountCompletedEventType::kNone: |
| default: |
| VLOG(1) << "Unexpected mount event " |
| << base::to_underlying(event.event_type); |
| break; |
| } |
| |
| if (notification) { |
| GetNotificationDisplayService()->Display( |
| NotificationHandler::Type::TRANSIENT, *notification, |
| /*metadata=*/nullptr); |
| } |
| } |
| |
| NotificationDisplayService* |
| SystemNotificationManager::GetNotificationDisplayService() { |
| return NotificationDisplayServiceFactory::GetForProfile(profile_); |
| } |
| |
| void SystemNotificationManager::SetDriveFSEventRouter( |
| DriveFsEventRouter* drivefs_event_router) { |
| drivefs_event_router_ = drivefs_event_router; |
| } |
| |
| void SystemNotificationManager::SetIOTaskController( |
| IOTaskController* io_task_controller) { |
| io_task_controller_ = io_task_controller; |
| } |
| |
| } // namespace file_manager |