// Copyright 2018 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/crostini/crostini_package_notification.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include "chrome/browser/ash/crostini/crostini_package_service.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/ui/views/crostini/crostini_package_install_failure_view.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/display/display.h"
#include "ui/display/screen.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
int CrostiniPackageNotification::GetButtonCountForTesting() {
return notification_->buttons().size();
}
const std::string& CrostiniPackageNotification::GetErrorMessageForTesting()
const {
return error_message_;
}
CrostiniPackageNotification::NotificationSettings::NotificationSettings() {}
CrostiniPackageNotification::NotificationSettings::NotificationSettings(
const NotificationSettings& rhs) = default;
CrostiniPackageNotification::NotificationSettings::~NotificationSettings() {}
CrostiniPackageNotification::CrostiniPackageNotification(
Profile* profile,
NotificationType notification_type,
PackageOperationStatus status,
const guest_os::GuestId& container_id,
const std::u16string& 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),
container_id_(container_id) {
if (status == PackageOperationStatus::RUNNING) {
running_start_time_ = base::TimeTicks::Now();
guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
->AddObserver(this);
}
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_id = cros_tokens::kCrosSysPrimary;
notification_ = std::make_unique<message_center::Notification>(
message_center::NOTIFICATION_TYPE_PROGRESS, notification_id,
std::u16string(), std::u16string(),
ui::ImageModel(), // icon
notification_settings_.source,
GURL(), // origin_url
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kNotifierCrostiniPackageOperation,
ash::NotificationCatalogName::kCrostiniPackage),
rich_notification_data,
base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
weak_ptr_factory_.GetWeakPtr()));
// Sets title and body
UpdateProgress(status, 0 /*progress_percent*/);
}
CrostiniPackageNotification::~CrostiniPackageNotification() {
guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
->RemoveObserver(this);
}
PackageOperationStatus CrostiniPackageNotification::GetOperationStatus() const {
return current_status_;
}
void CrostiniPackageNotification::OnRegistryUpdated(
guest_os::GuestOsRegistryService* registry_service,
guest_os::VmType vm_type,
const std::vector<std::string>& updated_apps,
const std::vector<std::string>& removed_apps,
const std::vector<std::string>& inserted_apps) {
if (vm_type != guest_os::VmType::TERMINA) {
return;
}
inserted_apps_.insert(inserted_apps.begin(), inserted_apps.end());
}
// static
CrostiniPackageNotification::NotificationSettings
CrostiniPackageNotification::GetNotificationSettingsForTypeAndAppName(
NotificationType notification_type,
const std::u16string& 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.queued_title = l10n_util::GetStringUTF16(
IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_QUEUED_TITLE);
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_IN_MIGRATION();
}
return result;
}
void CrostiniPackageNotification::UpdateProgress(
PackageOperationStatus status,
int progress_percent,
const std::string& error_message) {
if (status == PackageOperationStatus::RUNNING &&
current_status_ != PackageOperationStatus::RUNNING) {
running_start_time_ = base::TimeTicks::Now();
guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
->AddObserver(this);
}
current_status_ = status;
std::u16string title;
std::u16string body;
std::vector<message_center::ButtonInfo> buttons;
message_center::NotificationType notification_type =
message_center::NOTIFICATION_TYPE_SIMPLE;
bool never_timeout = false;
app_count_ = 0;
auto* registry_service =
guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_);
switch (status) {
case PackageOperationStatus::SUCCEEDED:
title = notification_settings_.success_title;
body = notification_settings_.success_body;
if (notification_type_ == NotificationType::PACKAGE_INSTALL) {
// Try and match up launcher icons with the install we just finished. We
// don't have a perfect solution to this, but under normal circumstances
// we shouldn't see icons appearing during an install that aren't
// because of that install.
for (const std::string& app_id : inserted_apps_) {
auto registration = registry_service->GetRegistration(app_id);
if (registration.has_value() &&
registration->VmName() == container_id_.vm_name &&
registration->ContainerName() == container_id_.container_name) {
app_id_ = app_id;
app_count_++;
}
}
if (app_count_ == 1) {
buttons.push_back(
message_center::ButtonInfo(l10n_util::GetStringUTF16(
IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_BUTTON)));
}
}
registry_service->RemoveObserver(this);
break;
case PackageOperationStatus::FAILED: {
title = notification_settings_.failure_title;
body = notification_settings_.failure_body;
error_message_ = error_message;
notification_->set_accent_color_id(cros_tokens::kCrosSysError);
break;
}
case PackageOperationStatus::WAITING_FOR_APP_REGISTRY_UPDATE:
// If a notification progress bar is set to a value outside of [0, 100],
// it becomes in infinite progress bar. Do that here because we have no
// way to know how long this will take or how close we are to completion.
progress_percent = -1;
[[fallthrough]];
case PackageOperationStatus::RUNNING:
never_timeout = true;
notification_type = message_center::NOTIFICATION_TYPE_PROGRESS;
title = notification_settings_.progress_title;
if (notification_type_ == NotificationType::APPLICATION_UNINSTALL &&
progress_percent >= 0) {
// Uninstalls have a time remaining instead of a fixed message.
body = GetTimeRemainingMessage(running_start_time_, progress_percent);
// else leave body blank
} else {
body = notification_settings_.progress_body;
}
break;
case PackageOperationStatus::QUEUED:
title = notification_settings_.queued_title;
body = notification_settings_.queued_body;
break;
default:
NOTREACHED_IN_MIGRATION();
}
notification_->set_title(title);
notification_->set_message(body);
notification_->set_buttons(buttons);
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::SUCCEEDED &&
current_status_ != PackageOperationStatus::FAILED) {
// 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::Click(
const std::optional<int>& button_index,
const std::optional<std::u16string>& reply) {
if (current_status_ == PackageOperationStatus::FAILED) {
crostini::ShowCrostiniPackageInstallFailureView(error_message_);
}
if (current_status_ != PackageOperationStatus::SUCCEEDED) {
return;
}
if (app_count_ == 0) {
LaunchTerminal(profile_,
display::Screen::GetScreen()->GetPrimaryDisplay().id(),
DefaultContainerId());
} else if (app_count_ == 1) {
DCHECK(!app_id_.empty());
LaunchCrostiniApp(profile_, app_id_,
display::Screen::GetScreen()->GetPrimaryDisplay().id());
} else {
AppListClientImpl::GetInstance()->ShowAppList(
ash::AppListShowSource::kBrowser);
}
}
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_,
/*metadata=*/nullptr);
}
} // namespace crostini