// Copyright 2023 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/ui/media_router/cast_notification_controller_lacros.h"
#include <algorithm>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_handler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/generated_resources.h"
#include "components/media_router/browser/media_router.h"
#include "components/media_router/browser/media_router_factory.h"
#include "components/media_router/browser/mirroring_media_controller_host.h"
#include "components/media_router/common/media_source.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/browser_context.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.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_types.h"
namespace media_router {
namespace {
constexpr char kNotificationId[] = "browser.cast.session";
constexpr char kNotifierId[] = "browser.cast";
std::u16string GetNotificationTitle(const std::string& sink_name) {
if (sink_name.empty()) {
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_NOTIFICATION_TITLE_UNKNOWN);
}
return l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_NOTIFICATION_TITLE,
base::UTF8ToUTF16(sink_name));
}
std::u16string GetNotificationMessage(const MediaRoute& route,
MirroringMediaControllerHost* freeze_host,
bool freeze_enabled) {
if (!freeze_enabled || !freeze_host || !freeze_host->CanFreeze()) {
if (route.media_source().IsDesktopMirroringSource()) {
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_NOTIFICATION_MESSAGE_CAST_SCREEN);
}
return base::UTF8ToUTF16(route.description());
}
if (freeze_host->IsFrozen()) {
if (route.media_source().IsDesktopMirroringSource()) {
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_NOTIFICATION_MESSAGE_SCREEN_PAUSED);
}
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_NOTIFICATION_MESSAGE_PAUSED);
}
if (route.media_source().IsDesktopMirroringSource()) {
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_NOTIFICATION_MESSAGE_SCREEN_CAN_PAUSE);
}
if (route.media_source().IsTabMirroringSource()) {
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_NOTIFICATION_MESSAGE_TAB_CAN_PAUSE);
}
return base::UTF8ToUTF16(route.description());
}
} // namespace
CastNotificationControllerLacros::CastNotificationControllerLacros(
Profile* profile)
: CastNotificationControllerLacros(
profile,
NotificationDisplayService::GetForProfile(profile),
MediaRouterFactory::GetApiForBrowserContext(profile)) {}
CastNotificationControllerLacros::CastNotificationControllerLacros(
Profile* profile,
NotificationDisplayService* notification_service,
MediaRouter* router)
: MediaRoutesObserver(router),
profile_(profile),
notification_service_(notification_service),
media_router_(router) {}
CastNotificationControllerLacros::~CastNotificationControllerLacros() {
StopObservingFreezeHost();
}
void CastNotificationControllerLacros::OnRoutesUpdated(
const std::vector<MediaRoute>& routes) {
freeze_button_index_.reset();
stop_button_index_.reset();
displayed_route_is_frozen_ = false;
StopObservingFreezeHost();
auto route_it =
std::find_if(routes.begin(), routes.end(),
[](const MediaRoute& route) { return route.is_local(); });
if (route_it == routes.end()) {
// There was no active local route, so we hide the current outstanding
// notification, if it exists.
HideNotification();
return;
}
// This will overwrite the existing notification if there is one.
ShowNotification(*route_it);
}
void CastNotificationControllerLacros::OnFreezeInfoChanged() {
if (displayed_route_) {
ShowNotification(*displayed_route_);
}
}
void CastNotificationControllerLacros::ShowNotification(
const MediaRoute& route) {
displayed_route_ = route;
notification_service_->Display(NotificationHandler::Type::TRANSIENT,
CreateNotification(route), nullptr);
}
void CastNotificationControllerLacros::HideNotification() {
displayed_route_.reset();
notification_service_->Close(NotificationHandler::Type::TRANSIENT,
kNotificationId);
}
message_center::Notification
CastNotificationControllerLacros::CreateNotification(const MediaRoute& route) {
MirroringMediaControllerHost* freeze_host =
media_router_->GetMirroringMediaControllerHost(route.media_route_id());
if (freeze_host && freeze_host != freeze_host_) {
freeze_host->AddObserver(this);
freeze_host_ = freeze_host;
}
message_center::RichNotificationData data;
data.buttons = GetButtons(route, freeze_host);
// `vector_small_image` is ignored by the crosapi so we must convert it to
// `small_image`. Also, kAppIconImageSize=16 is used in AshNotificationView,
// but 16 here somehow results in a blurry image.
data.small_image = gfx::Image(gfx::CreateVectorIcon(
gfx::IconDescription(vector_icons::kMediaRouterIdleIcon, 32)));
message_center::Notification notification(
message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE,
kNotificationId, GetNotificationTitle(route.media_sink_name()),
GetNotificationMessage(route, freeze_host,
IsAccessCodeCastFreezeUiEnabled(profile_)),
/*icon=*/ui::ImageModel{},
/*display_source=*/u"",
/*origin_url=*/GURL{},
message_center::NotifierId{message_center::NotifierType::SYSTEM_COMPONENT,
kNotifierId},
std::move(data),
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(
&CastNotificationControllerLacros::OnNotificationClicked,
weak_ptr_factory_.GetWeakPtr())));
if (GlobalMediaControlsCastStartStopEnabled(profile_) &&
(route.media_source().IsCastPresentationUrl() ||
route.media_source().IsRemotePlaybackSource())) {
// The session was started via the Global Media Controls UI which would
// still likely be open. It'd be annoying to overlay the notificatin on top
// of it, so add the notification to the tray silently.
notification.set_priority(
message_center::NotificationPriority::LOW_PRIORITY);
}
return notification;
}
std::vector<message_center::ButtonInfo>
CastNotificationControllerLacros::GetButtons(
const MediaRoute& route,
MirroringMediaControllerHost* freeze_host) {
std::vector<message_center::ButtonInfo> buttons;
if (IsAccessCodeCastFreezeUiEnabled(profile_) && freeze_host &&
freeze_host->CanFreeze()) {
displayed_route_is_frozen_ = freeze_host->IsFrozen();
buttons.emplace_back(
displayed_route_is_frozen_
? l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_SINK_VIEW_RESUME)
: l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_SINK_VIEW_PAUSE));
freeze_button_index_ = buttons.size() - 1;
}
buttons.emplace_back(
l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_SINK_VIEW_STOP));
stop_button_index_ = buttons.size() - 1;
return buttons;
}
void CastNotificationControllerLacros::OnNotificationClicked(
std::optional<int> button_index) {
if (freeze_button_index_ && button_index == freeze_button_index_) {
FreezeOrUnfreezeCastStream();
} else if (button_index == stop_button_index_) {
StopCasting();
}
}
void CastNotificationControllerLacros::StopCasting() {
if (displayed_route_) {
media_router_->TerminateRoute(displayed_route_->media_route_id());
}
}
void CastNotificationControllerLacros::FreezeOrUnfreezeCastStream() {
if (!displayed_route_) {
return;
}
MirroringMediaControllerHost* freeze_host =
media_router_->GetMirroringMediaControllerHost(
displayed_route_->media_route_id());
if (!freeze_host) {
return;
}
if (displayed_route_is_frozen_) {
freeze_host->Unfreeze();
} else {
freeze_host->Freeze();
}
}
void CastNotificationControllerLacros::StopObservingFreezeHost() {
if (!displayed_route_) {
return;
}
// We don't reuse `freeze_host_` here, in case it's been freed.
MirroringMediaControllerHost* freeze_host =
media_router_->GetMirroringMediaControllerHost(
displayed_route_->media_route_id());
if (freeze_host) {
freeze_host->RemoveObserver(this);
freeze_host_ = nullptr;
}
}
} // namespace media_router