// Copyright 2020 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/notifications/notification_platform_bridge_lacros.h"
#include <algorithm>
#include <optional>
#include <utility>
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "chrome/browser/notifications/notification_platform_bridge_delegate.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chromeos/crosapi/mojom/message_center.mojom.h"
#include "chromeos/crosapi/mojom/notification.mojom-shared.h"
#include "chromeos/crosapi/mojom/notification.mojom.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_manager.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/native_theme/native_theme.h"
namespace {
crosapi::mojom::NotificationType ToMojo(message_center::NotificationType type) {
switch (type) {
case message_center::NOTIFICATION_TYPE_SIMPLE:
case message_center::DEPRECATED_NOTIFICATION_TYPE_BASE_FORMAT:
return crosapi::mojom::NotificationType::kSimple;
case message_center::NOTIFICATION_TYPE_IMAGE:
return crosapi::mojom::NotificationType::kImage;
case message_center::NOTIFICATION_TYPE_MULTIPLE:
return crosapi::mojom::NotificationType::kList;
case message_center::NOTIFICATION_TYPE_PROGRESS:
return crosapi::mojom::NotificationType::kProgress;
case message_center::NOTIFICATION_TYPE_CUSTOM:
// TYPE_CUSTOM exists only within ash.
NOTREACHED_IN_MIGRATION();
return crosapi::mojom::NotificationType::kSimple;
case message_center::NOTIFICATION_TYPE_CONVERSATION:
// TYPE_CONVERSATION is not currently supported for Lacros.
NOTREACHED_IN_MIGRATION();
return crosapi::mojom::NotificationType::kSimple;
}
}
crosapi::mojom::NotifierType ToMojo(message_center::NotifierType type) {
switch (type) {
case message_center::NotifierType::APPLICATION:
return crosapi::mojom::NotifierType::kApplication;
case message_center::NotifierType::ARC_APPLICATION:
return crosapi::mojom::NotifierType::kArcApplication;
case message_center::NotifierType::WEB_PAGE:
return crosapi::mojom::NotifierType::kWebPage;
case message_center::NotifierType::SYSTEM_COMPONENT:
return crosapi::mojom::NotifierType::kSystemComponent;
case message_center::NotifierType::CROSTINI_APPLICATION:
return crosapi::mojom::NotifierType::kCrostiniApplication;
case message_center::NotifierType::PHONE_HUB:
return crosapi::mojom::NotifierType::kPhoneHub;
}
}
crosapi::mojom::FullscreenVisibility ToMojo(
message_center::FullscreenVisibility visibility) {
switch (visibility) {
case message_center::FullscreenVisibility::NONE:
return crosapi::mojom::FullscreenVisibility::kNone;
case message_center::FullscreenVisibility::OVER_USER:
return crosapi::mojom::FullscreenVisibility::kOverUser;
}
}
crosapi::mojom::SettingsButtonHandler ToMojo(
message_center::SettingsButtonHandler settings_button_handler) {
switch (settings_button_handler) {
case message_center::SettingsButtonHandler::NONE:
return crosapi::mojom::SettingsButtonHandler::kNone;
case message_center::SettingsButtonHandler::INLINE:
return crosapi::mojom::SettingsButtonHandler::kInline;
case message_center::SettingsButtonHandler::DELEGATE:
return crosapi::mojom::SettingsButtonHandler::kDelegate;
}
}
crosapi::mojom::NotificationPtr ToMojo(
const message_center::Notification& notification,
const ui::ColorProvider* color_provider) {
auto mojo_note = crosapi::mojom::Notification::New();
mojo_note->type = ToMojo(notification.type());
mojo_note->id = notification.id();
mojo_note->title = notification.title();
mojo_note->message = notification.message();
mojo_note->display_source = notification.display_source();
mojo_note->origin_url = notification.origin_url();
if (!notification.icon().IsEmpty())
mojo_note->icon = notification.icon().Rasterize(color_provider);
mojo_note->priority = std::clamp(notification.priority(), -2, 2);
mojo_note->require_interaction = notification.never_timeout();
mojo_note->timestamp = notification.timestamp();
if (!notification.image().IsEmpty())
mojo_note->image = notification.image().AsImageSkia();
if (!notification.small_image().IsEmpty()) {
mojo_note->badge = notification.small_image().AsImageSkia();
mojo_note->badge_needs_additional_masking_has_value = true;
mojo_note->badge_needs_additional_masking =
notification.small_image_needs_additional_masking();
}
for (const auto& item : notification.items()) {
auto mojo_item = crosapi::mojom::NotificationItem::New();
mojo_item->title = item.title();
mojo_item->message = item.message();
mojo_note->items.push_back(std::move(mojo_item));
}
mojo_note->progress = std::clamp(notification.progress(), -1, 100);
mojo_note->progress_status = notification.progress_status();
for (const auto& button : notification.buttons()) {
auto mojo_button = crosapi::mojom::ButtonInfo::New();
mojo_button->title = button.title;
mojo_button->placeholder = button.placeholder;
mojo_note->buttons.push_back(std::move(mojo_button));
}
mojo_note->pinned = notification.pinned();
mojo_note->renotify = notification.renotify();
mojo_note->silent = notification.silent();
mojo_note->accessible_name = notification.accessible_name();
mojo_note->fullscreen_visibility =
ToMojo(notification.fullscreen_visibility());
if (notification.accent_color_id().has_value()) {
// Colors have to be resolved in lacros since color ids are not guaranteed
// to be stable across the process boundary.
mojo_note->accent_color =
color_provider->GetColor(*notification.accent_color_id());
} else {
// TODO(b/308208767): Remove when this isn't used anymore.
mojo_note->accent_color = notification.accent_color();
}
mojo_note->notifier_id = crosapi::mojom::NotifierId::New();
mojo_note->notifier_id->type = ToMojo(notification.notifier_id().type);
mojo_note->notifier_id->id = notification.notifier_id().id;
mojo_note->notifier_id->url = notification.notifier_id().url;
mojo_note->notifier_id->title = notification.notifier_id().title;
mojo_note->notifier_id->profile_id = notification.notifier_id().profile_id;
const std::optional<base::FilePath>& image_path =
notification.rich_notification_data().image_path;
if (image_path.has_value()) {
mojo_note->image_path = image_path;
}
mojo_note->settings_button_handler =
ToMojo(notification.rich_notification_data().settings_button_handler);
return mojo_note;
}
} // namespace
// Keeps track of notifications being displayed in the remote message center.
class NotificationPlatformBridgeLacros::RemoteNotificationDelegate
: public crosapi::mojom::NotificationDelegate {
public:
RemoteNotificationDelegate(
const std::string& notification_id,
NotificationPlatformBridgeDelegate* bridge_delegate,
base::WeakPtr<NotificationPlatformBridgeLacros> owner)
: notification_id_(notification_id),
bridge_delegate_(bridge_delegate),
owner_(owner) {
DCHECK(!notification_id_.empty());
DCHECK(bridge_delegate_);
DCHECK(owner_);
}
RemoteNotificationDelegate(const RemoteNotificationDelegate&) = delete;
RemoteNotificationDelegate& operator=(const RemoteNotificationDelegate&) =
delete;
~RemoteNotificationDelegate() override = default;
mojo::PendingRemote<crosapi::mojom::NotificationDelegate>
BindNotificationDelegate() {
return receiver_.BindNewPipeAndPassRemoteWithVersion();
}
// crosapi::mojom::NotificationDelegate:
void OnNotificationClosed(bool by_user) override {
bridge_delegate_->HandleNotificationClosed(notification_id_, by_user);
if (owner_)
owner_->OnRemoteNotificationClosed(notification_id_);
// NOTE: |this| is deleted.
}
void OnNotificationClicked() override {
bridge_delegate_->HandleNotificationClicked(notification_id_);
}
void OnNotificationButtonClicked(
uint32_t button_index,
const std::optional<::std::u16string>& reply) override {
bridge_delegate_->HandleNotificationButtonClicked(
notification_id_, base::checked_cast<int>(button_index), reply);
}
void OnNotificationSettingsButtonClicked() override {
bridge_delegate_->HandleNotificationSettingsButtonClicked(notification_id_);
}
void OnNotificationDisabled() override {
bridge_delegate_->DisableNotification(notification_id_);
}
private:
const std::string notification_id_;
const raw_ptr<NotificationPlatformBridgeDelegate> bridge_delegate_;
base::WeakPtr<NotificationPlatformBridgeLacros> owner_;
mojo::Receiver<crosapi::mojom::NotificationDelegate> receiver_{this};
};
NotificationPlatformBridgeLacros::NotificationPlatformBridgeLacros(
NotificationPlatformBridgeDelegate* delegate,
mojo::Remote<crosapi::mojom::MessageCenter>* message_center_remote)
: bridge_delegate_(delegate),
message_center_remote_(message_center_remote) {
DCHECK(bridge_delegate_);
}
NotificationPlatformBridgeLacros::~NotificationPlatformBridgeLacros() = default;
void NotificationPlatformBridgeLacros::Display(
NotificationHandler::Type notification_type,
Profile* profile,
const message_center::Notification& notification,
std::unique_ptr<NotificationCommon::Metadata> metadata) {
if (!message_center_remote_)
return;
// |profile| is ignored because Profile management is handled in
// NotificationPlatformBridgeChromeOs, which includes a profile ID as part of
// the notification ID. Lacros does not support Chrome OS multi-signin, so we
// don't need to handle inactive user notification blockers in ash.
auto pending_notification = std::make_unique<RemoteNotificationDelegate>(
notification.id(), bridge_delegate_, weak_factory_.GetWeakPtr());
// Display the notification, or update an existing one with the same ID.
// `profile` may be null for e.g. system notifications.
const auto* const color_provider =
profile
? ThemeServiceFactory::GetForProfile(profile)->GetColorProvider()
: ui::ColorProviderManager::Get().GetColorProviderFor(
ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(
nullptr));
(*message_center_remote_)
->DisplayNotification(ToMojo(notification, color_provider),
pending_notification->BindNotificationDelegate());
remote_notifications_[notification.id()] = std::move(pending_notification);
}
void NotificationPlatformBridgeLacros::Close(
Profile* profile,
const std::string& notification_id) {
if (!message_center_remote_)
return;
(*message_center_remote_)->CloseNotification(notification_id);
// |remote_notifications_| is cleaned up after the remote notification closes
// and notifies us via the delegate.
}
void NotificationPlatformBridgeLacros::GetDisplayed(
Profile* profile,
GetDisplayedNotificationsCallback callback) const {
NOTIMPLEMENTED();
std::move(callback).Run(/*notification_ids=*/{}, /*supports_sync=*/false);
}
void NotificationPlatformBridgeLacros::GetDisplayedForOrigin(
Profile* profile,
const GURL& origin,
GetDisplayedNotificationsCallback callback) const {
NOTIMPLEMENTED();
std::move(callback).Run(/*notification_ids=*/{}, /*supports_sync=*/false);
}
void NotificationPlatformBridgeLacros::SetReadyCallback(
NotificationBridgeReadyCallback callback) {
// Always return success even if |message_center_remote_| is not valid as we
// don't have another way of displaying notifications on ChromeOS via Lacros.
std::move(callback).Run(/*success=*/true);
}
void NotificationPlatformBridgeLacros::DisplayServiceShutDown(
Profile* profile) {
remote_notifications_.clear();
}
void NotificationPlatformBridgeLacros::OnRemoteNotificationClosed(
const std::string& id) {
remote_notifications_.erase(id);
}