chromium/chrome/browser/notifications/notification_platform_bridge_lacros.cc

// 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);
}