| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/chromeos/crostini/crostini_package_notification.h" |
| |
| #include "ash/public/cpp/notification_utils.h" |
| #include "ash/public/cpp/vector_icons/vector_icons.h" |
| #include "chrome/browser/chromeos/crostini/crostini_package_service.h" |
| #include "chrome/browser/notifications/notification_display_service.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/time_format.h" |
| #include "ui/message_center/public/cpp/message_center_constants.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "ui/message_center/public/cpp/notification_delegate.h" |
| |
| namespace crostini { |
| |
| namespace { |
| |
| constexpr char kNotifierCrostiniPackageOperation[] = |
| "crostini.package_operation"; |
| |
| } // namespace |
| |
| CrostiniPackageNotification::NotificationSettings::NotificationSettings() {} |
| CrostiniPackageNotification::NotificationSettings::NotificationSettings( |
| const NotificationSettings& rhs) = default; |
| CrostiniPackageNotification::NotificationSettings::~NotificationSettings() {} |
| |
| CrostiniPackageNotification::CrostiniPackageNotification( |
| Profile* profile, |
| NotificationType notification_type, |
| PackageOperationStatus status, |
| const base::string16& app_name, |
| const std::string& notification_id, |
| CrostiniPackageService* package_service) |
| : notification_type_(notification_type), |
| current_status_(status), |
| package_service_(package_service), |
| profile_(profile), |
| notification_settings_( |
| GetNotificationSettingsForTypeAndAppName(notification_type, |
| app_name)), |
| visible_(true), |
| weak_ptr_factory_(this) { |
| if (status == PackageOperationStatus::RUNNING) { |
| running_start_time_ = base::Time::Now(); |
| } |
| message_center::RichNotificationData rich_notification_data; |
| rich_notification_data.vector_small_image = &ash::kNotificationLinuxIcon; |
| rich_notification_data.never_timeout = true; |
| rich_notification_data.accent_color = ash::kSystemNotificationColorNormal; |
| |
| notification_ = std::make_unique<message_center::Notification>( |
| message_center::NOTIFICATION_TYPE_PROGRESS, notification_id, |
| base::string16(), base::string16(), |
| gfx::Image(), // icon |
| notification_settings_.source, |
| GURL(), // origin_url |
| message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT, |
| kNotifierCrostiniPackageOperation), |
| rich_notification_data, |
| base::MakeRefCounted<message_center::ThunkNotificationDelegate>( |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // Sets title and body |
| UpdateProgress(status, 0 /*progress_percent*/); |
| } |
| |
| CrostiniPackageNotification::~CrostiniPackageNotification() = default; |
| |
| // static |
| CrostiniPackageNotification::NotificationSettings |
| CrostiniPackageNotification::GetNotificationSettingsForTypeAndAppName( |
| NotificationType notification_type, |
| const base::string16& app_name) { |
| NotificationSettings result; |
| |
| switch (notification_type) { |
| case NotificationType::PACKAGE_INSTALL: |
| DCHECK(app_name.empty()); |
| result.source = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_DISPLAY_SOURCE); |
| result.progress_title = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_IN_PROGRESS_TITLE); |
| result.progress_body.clear(); |
| result.success_title = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_TITLE); |
| result.success_body = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_MESSAGE); |
| result.failure_title = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_ERROR_TITLE); |
| result.failure_body = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_ERROR_MESSAGE); |
| break; |
| |
| case NotificationType::APPLICATION_UNINSTALL: |
| result.source = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_DISPLAY_SOURCE); |
| result.queued_title = l10n_util::GetStringFUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_QUEUED_TITLE, |
| app_name); |
| result.queued_body = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_QUEUED_MESSAGE); |
| result.progress_title = l10n_util::GetStringFUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_IN_PROGRESS_TITLE, |
| app_name); |
| result.success_title = l10n_util::GetStringFUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_COMPLETED_TITLE, |
| app_name); |
| result.success_body = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_COMPLETED_MESSAGE); |
| result.failure_title = l10n_util::GetStringFUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_ERROR_TITLE, |
| app_name); |
| result.failure_body = l10n_util::GetStringUTF16( |
| IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_ERROR_MESSAGE); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| return result; |
| } |
| |
| // TODO(timloh): This doesn't get called if the user shuts down Crostini, so |
| // the notification will be stuck at whatever percentage it is at. |
| void CrostiniPackageNotification::UpdateProgress(PackageOperationStatus status, |
| int progress_percent) { |
| if (status == PackageOperationStatus::RUNNING && |
| current_status_ != PackageOperationStatus::RUNNING) { |
| running_start_time_ = base::Time::Now(); |
| } |
| current_status_ = status; |
| |
| base::string16 title; |
| base::string16 body; |
| message_center::NotificationType notification_type = |
| message_center::NOTIFICATION_TYPE_SIMPLE; |
| bool never_timeout = false; |
| |
| switch (status) { |
| case PackageOperationStatus::SUCCEEDED: |
| title = notification_settings_.success_title; |
| body = notification_settings_.success_body; |
| break; |
| |
| case PackageOperationStatus::FAILED: |
| title = notification_settings_.failure_title; |
| body = notification_settings_.failure_body; |
| notification_->set_accent_color( |
| ash::kSystemNotificationColorCriticalWarning); |
| break; |
| |
| case PackageOperationStatus::RUNNING: |
| never_timeout = true; |
| notification_type = message_center::NOTIFICATION_TYPE_PROGRESS; |
| title = notification_settings_.progress_title; |
| if (notification_type_ == NotificationType::APPLICATION_UNINSTALL) { |
| // Uninstalls have a time remaining instead of a fixed message. |
| base::TimeDelta time_since_started_running = |
| base::Time::Now() - running_start_time_; |
| |
| // Don't estimate if we don't have enough data yet. At the moment we |
| // start the uninstall, we have no idea how long it will take. Only |
| // estimate once we've spent at least 3 seconds OR gotten 10% of the |
| // way through the uninstall. |
| constexpr base::TimeDelta kMinTimeForEstimate = |
| base::TimeDelta::FromSeconds(3); |
| constexpr base::TimeDelta kTimeDeltaZero = |
| base::TimeDelta::FromSeconds(0); |
| constexpr int kMinPercentForEstimate = 10; |
| if ((time_since_started_running >= kMinTimeForEstimate && |
| progress_percent > 0) || |
| (progress_percent >= kMinPercentForEstimate && |
| time_since_started_running > kTimeDeltaZero)) { |
| base::TimeDelta total_time_expected = |
| (time_since_started_running * 100) / progress_percent; |
| base::TimeDelta time_remaining = |
| total_time_expected - time_since_started_running; |
| body = ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING, |
| ui::TimeFormat::LENGTH_SHORT, |
| time_remaining); |
| } |
| // else leave body blank |
| } else { |
| body = notification_settings_.progress_body; |
| } |
| break; |
| |
| case PackageOperationStatus::QUEUED: |
| // We don't have queued strings for some NotificationTypes; we shouldn't |
| // be asked to move to QUEUED status for those, |
| DCHECK(!notification_settings_.queued_title.empty()); |
| DCHECK(!notification_settings_.queued_body.empty()); |
| title = notification_settings_.queued_title; |
| body = notification_settings_.queued_body; |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| notification_->set_title(title); |
| notification_->set_message(body); |
| notification_->set_type(notification_type); |
| notification_->set_progress(progress_percent); |
| notification_->set_never_timeout(never_timeout); |
| UpdateDisplayedNotification(); |
| } |
| |
| void CrostiniPackageNotification::ForceAllowAutoHide() { |
| notification_->set_never_timeout(false); |
| UpdateDisplayedNotification(); |
| } |
| |
| void CrostiniPackageNotification::Close(bool by_user) { |
| if (current_status_ == PackageOperationStatus::RUNNING || |
| current_status_ == PackageOperationStatus::QUEUED) { |
| // We don't want to delete ourselves yet; we want to forcibly redisplay |
| // when we hit success or failure. Just note that we are hidden. |
| visible_ = false; |
| } else { |
| // This call deletes us. |
| package_service_->NotificationCompleted(this); |
| } |
| } |
| |
| void CrostiniPackageNotification::UpdateDisplayedNotification() { |
| if (current_status_ == PackageOperationStatus::SUCCEEDED || |
| current_status_ == PackageOperationStatus::FAILED) { |
| // If the user closes the notification when it is queued or running, we |
| // still want to tell them when it is actually finished. So force the |
| // notification back to visibility when we get our success / fail notice. |
| // Note that we only get one success / fail notice, so we won't keep |
| // reshowing this. |
| visible_ = true; |
| } |
| |
| if (!visible_) { |
| // User hid, don't re-display. |
| return; |
| } |
| |
| NotificationDisplayService* display_service = |
| NotificationDisplayService::GetForProfile(profile_); |
| display_service->Display(NotificationHandler::Type::TRANSIENT, |
| *notification_); |
| } |
| |
| } // namespace crostini |