| // Copyright 2019 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/ui/global_media_controls/media_notification_service.h" |
| |
| #include <memory> |
| |
| #include "base/callback_list.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/ranges/algorithm.h" |
| #include "chrome/browser/media/router/media_router_feature.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/global_media_controls/media_dialog_delegate.h" |
| #include "chrome/browser/ui/global_media_controls/media_notification_container_impl.h" |
| #include "chrome/browser/ui/global_media_controls/media_notification_device_provider_impl.h" |
| #include "chrome/browser/ui/global_media_controls/media_notification_service_observer.h" |
| #include "chrome/browser/ui/global_media_controls/media_session_notification_producer.h" |
| #include "chrome/browser/ui/global_media_controls/overlay_media_notification.h" |
| #include "chrome/browser/ui/media_router/media_router_ui.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "components/media_message_center/media_notification_item.h" |
| #include "components/media_message_center/media_notification_util.h" |
| #include "components/media_message_center/media_session_notification_item.h" |
| #include "components/media_router/browser/presentation/start_presentation_context.h" |
| #include "components/ukm/content/source_url_recorder.h" |
| #include "content/public/browser/audio_service.h" |
| #include "content/public/browser/media_session.h" |
| #include "content/public/browser/media_session_service.h" |
| #include "media/base/media_switches.h" |
| #include "services/media_session/public/mojom/media_session.mojom.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| |
| MediaNotificationService::MediaNotificationService( |
| Profile* profile, |
| bool show_from_all_profiles) { |
| media_session_notification_producer_ = |
| std::make_unique<MediaSessionNotificationProducer>( |
| this, profile, show_from_all_profiles); |
| notification_producers_.insert(media_session_notification_producer_.get()); |
| |
| if (media_router::MediaRouterEnabled(profile)) { |
| if (base::FeatureList::IsEnabled(media::kGlobalMediaControlsForCast)) { |
| cast_notification_producer_ = |
| std::make_unique<CastMediaNotificationProducer>( |
| profile, this, |
| base::BindRepeating( |
| &MediaNotificationService::OnCastNotificationsChanged, |
| base::Unretained(this))); |
| notification_producers_.insert(cast_notification_producer_.get()); |
| } |
| if (media_router::GlobalMediaControlsCastStartStopEnabled()) { |
| presentation_request_notification_producer_ = |
| std::make_unique<PresentationRequestNotificationProducer>(this); |
| notification_producers_.insert( |
| presentation_request_notification_producer_.get()); |
| } |
| } |
| } |
| |
| MediaNotificationService::~MediaNotificationService() = default; |
| |
| void MediaNotificationService::AddObserver( |
| MediaNotificationServiceObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void MediaNotificationService::RemoveObserver( |
| MediaNotificationServiceObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void MediaNotificationService::ShowNotification(const std::string& id) { |
| if (media_session_notification_producer_->HasSession(id) && |
| !media_session_notification_producer_->ActivateItem(id)) { |
| return; |
| } |
| |
| // If new notifications come up while the dialog is open for a |
| // PresentationRequest, do not show the new notifications. |
| if (!HasOpenDialogForPresentationRequest()) { |
| ShowAndObserveContainer(id); |
| } |
| } |
| |
| void MediaNotificationService::HideNotification(const std::string& id) { |
| if (media_session_notification_producer_) { |
| media_session_notification_producer_->HideItem(id); |
| } |
| OnNotificationChanged(); |
| if (!dialog_delegate_) { |
| return; |
| } |
| dialog_delegate_->HideMediaSession(id); |
| } |
| |
| void MediaNotificationService::RemoveItem(const std::string& id) { |
| // Copy |id| to avoid a dangling reference after the item is deleted. This |
| // happens when |id| refers to a string owned by the item being removed. |
| const auto id_copy{id}; |
| if (media_session_notification_producer_) |
| media_session_notification_producer_->RemoveItem(id_copy); |
| OnNotificationChanged(); |
| } |
| |
| scoped_refptr<base::SequencedTaskRunner> |
| MediaNotificationService::GetTaskRunner() const { |
| return nullptr; |
| } |
| |
| void MediaNotificationService::LogMediaSessionActionButtonPressed( |
| const std::string& id, |
| media_session::mojom::MediaSessionAction action) { |
| media_session_notification_producer_->LogMediaSessionActionButtonPressed( |
| id, action); |
| } |
| |
| void MediaNotificationService::Shutdown() { |
| // |cast_notification_producer_| and |
| // |presentation_request_notification_producer_| depend on MediaRouter, |
| // which is another keyed service. |
| cast_notification_producer_.reset(); |
| presentation_request_notification_producer_.reset(); |
| } |
| |
| void MediaNotificationService::OnOverlayNotificationClosed( |
| const std::string& id) { |
| if (!media_session_notification_producer_->OnOverlayNotificationClosed(id)) { |
| return; |
| } |
| ShowAndObserveContainer(id); |
| } |
| |
| void MediaNotificationService::OnCastNotificationsChanged() { |
| OnNotificationChanged(); |
| } |
| |
| void MediaNotificationService::SetDialogDelegate( |
| MediaDialogDelegate* delegate) { |
| dialog_opened_from_presentation_ = false; |
| SetDialogDelegateCommon(delegate); |
| if (!dialog_delegate_) |
| return; |
| |
| auto notification_ids = GetActiveControllableNotificationIds(); |
| std::list<std::string> sorted_notification_ids; |
| for (const std::string& id : notification_ids) { |
| if (media_session_notification_producer_->IsSessionPlaying(id)) { |
| sorted_notification_ids.push_front(id); |
| } else { |
| sorted_notification_ids.push_back(id); |
| } |
| } |
| |
| for (const std::string& id : sorted_notification_ids) { |
| base::WeakPtr<media_message_center::MediaNotificationItem> item = |
| GetNotificationItem(id); |
| MediaNotificationContainerImpl* container = |
| dialog_delegate_->ShowMediaSession(id, item); |
| auto* notification_producer = GetNotificationProducer(id); |
| if (notification_producer) |
| notification_producer->OnItemShown(id, container); |
| } |
| |
| media_message_center::RecordConcurrentNotificationCount( |
| notification_ids.size()); |
| |
| if (cast_notification_producer_) { |
| media_message_center::RecordConcurrentCastNotificationCount( |
| cast_notification_producer_->GetActiveItemCount()); |
| } |
| } |
| |
| void MediaNotificationService::SetDialogDelegateForWebContents( |
| MediaDialogDelegate* delegate, |
| content::WebContents* contents) { |
| dialog_opened_from_presentation_ = true; |
| SetDialogDelegateCommon(delegate); |
| if (!dialog_delegate_) |
| return; |
| |
| // When the dialog is opened by a PresentationRequest, there should be only |
| // one notification, in the following priority order: |
| // 1. A cast session associated with |contents|. |
| // 2. A local media session associated with |contents|. |
| // 3. A supplemental notification populated using the PresentationRequest. |
| base::WeakPtr<media_message_center::MediaNotificationItem> item; |
| std::string item_id; |
| |
| // Find the cast notification item associated with |contents|. |
| auto routes = media_router::WebContentsPresentationManager::Get(contents) |
| ->GetMediaRoutes(); |
| if (!routes.empty()) { |
| // It is possible for a sender page to connect to two routes. For the |
| // sake of the Zenith dialog, only one notification is needed. |
| item_id = routes.begin()->media_route_id(); |
| item = cast_notification_producer_->GetNotificationItem(item_id); |
| } else if (media_session_notification_producer_ |
| ->HasActiveControllableSessionForWebContents(contents)) { |
| item_id = media_session_notification_producer_ |
| ->GetActiveControllableSessionForWebContents(contents); |
| item = GetNotificationItem(item_id); |
| } else { |
| auto presentation_item = |
| presentation_request_notification_producer_->GetNotificationItem(); |
| item_id = presentation_item->id(); |
| item = presentation_item; |
| DCHECK(presentation_request_notification_producer_->GetWebContents() == |
| contents); |
| } |
| |
| DCHECK(item); |
| MediaNotificationContainerImpl* container = |
| dialog_delegate_->ShowMediaSession(item_id, item); |
| auto* notification_producer = GetNotificationProducer(item_id); |
| if (notification_producer) |
| notification_producer->OnItemShown(item_id, container); |
| } |
| |
| std::set<std::string> |
| MediaNotificationService::GetActiveControllableNotificationIds() const { |
| std::set<std::string> ids; |
| for (auto* notification_provider : notification_producers_) { |
| const std::set<std::string>& provider_ids = |
| notification_provider->GetActiveControllableNotificationIds(); |
| ids.insert(provider_ids.begin(), provider_ids.end()); |
| } |
| return ids; |
| } |
| |
| bool MediaNotificationService::HasActiveNotifications() const { |
| return !GetActiveControllableNotificationIds().empty(); |
| } |
| |
| bool MediaNotificationService::HasActiveNotificationsForWebContents( |
| content::WebContents* web_contents) const { |
| bool has_cast_session = |
| !media_router::WebContentsPresentationManager::Get(web_contents) |
| ->GetMediaRoutes() |
| .empty(); |
| bool has_media_session = |
| media_session_notification_producer_ && |
| media_session_notification_producer_ |
| ->HasActiveControllableSessionForWebContents(web_contents); |
| return has_cast_session || has_media_session; |
| } |
| |
| bool MediaNotificationService::HasFrozenNotifications() const { |
| return media_session_notification_producer_->HasFrozenNotifications(); |
| } |
| |
| bool MediaNotificationService::HasOpenDialog() const { |
| return !!dialog_delegate_; |
| } |
| |
| bool MediaNotificationService::HasOpenDialogForPresentationRequest() const { |
| return HasOpenDialog() && dialog_opened_from_presentation_; |
| } |
| |
| void MediaNotificationService::HideMediaDialog() { |
| if (dialog_delegate_) { |
| dialog_delegate_->HideMediaDialog(); |
| } |
| } |
| |
| std::unique_ptr<OverlayMediaNotification> |
| MediaNotificationService::PopOutNotification(const std::string& id, |
| gfx::Rect bounds) { |
| return dialog_delegate_ ? dialog_delegate_->PopOut(id, bounds) : nullptr; |
| } |
| |
| base::CallbackListSubscription |
| MediaNotificationService::RegisterAudioOutputDeviceDescriptionsCallback( |
| MediaNotificationDeviceProvider::GetOutputDevicesCallback callback) { |
| return media_session_notification_producer_ |
| ->RegisterAudioOutputDeviceDescriptionsCallback(std::move(callback)); |
| } |
| |
| base::CallbackListSubscription |
| MediaNotificationService::RegisterIsAudioOutputDeviceSwitchingSupportedCallback( |
| const std::string& id, |
| base::RepeatingCallback<void(bool)> callback) { |
| return media_session_notification_producer_ |
| ->RegisterIsAudioOutputDeviceSwitchingSupportedCallback( |
| id, std::move(callback)); |
| } |
| |
| void MediaNotificationService::OnStartPresentationContextCreated( |
| std::unique_ptr<media_router::StartPresentationContext> context) { |
| if (presentation_request_notification_producer_) { |
| presentation_request_notification_producer_ |
| ->OnStartPresentationContextCreated(std::move(context)); |
| } else { |
| context->InvokeErrorCallback(blink::mojom::PresentationError( |
| blink::mojom::PresentationErrorType::PRESENTATION_REQUEST_CANCELLED, |
| "Unable to start presentation.")); |
| } |
| } |
| |
| std::unique_ptr<media_router::CastDialogController> |
| MediaNotificationService::CreateCastDialogControllerForSession( |
| const std::string& id) { |
| return media_session_notification_producer_ |
| ->CreateCastDialogControllerForSession(id); |
| } |
| |
| std::unique_ptr<media_router::CastDialogController> |
| MediaNotificationService::CreateCastDialogControllerForPresentationRequest() { |
| auto* web_contents = |
| presentation_request_notification_producer_->GetWebContents(); |
| if (!web_contents) |
| return nullptr; |
| |
| auto ui = std::make_unique<media_router::MediaRouterUI>(web_contents); |
| ui->InitWithDefaultMediaSource(); |
| return ui; |
| } |
| |
| void MediaNotificationService::ShowAndObserveContainer(const std::string& id) { |
| OnNotificationChanged(); |
| if (!dialog_delegate_) { |
| return; |
| } |
| base::WeakPtr<media_message_center::MediaNotificationItem> item = |
| GetNotificationItem(id); |
| MediaNotificationContainerImpl* container = |
| dialog_delegate_->ShowMediaSession(id, item); |
| auto* notification_producer = GetNotificationProducer(id); |
| if (notification_producer) |
| notification_producer->OnItemShown(id, container); |
| } |
| |
| base::WeakPtr<media_message_center::MediaNotificationItem> |
| MediaNotificationService::GetNotificationItem(const std::string& id) { |
| for (auto* notification_provider : notification_producers_) { |
| auto item = notification_provider->GetNotificationItem(id); |
| if (item) { |
| return item; |
| } |
| } |
| return nullptr; |
| } |
| |
| void MediaNotificationService::OnNotificationChanged() { |
| for (auto& observer : observers_) |
| observer.OnNotificationListChanged(); |
| } |
| |
| void MediaNotificationService::SetDialogDelegateCommon( |
| MediaDialogDelegate* delegate) { |
| DCHECK(!delegate || !dialog_delegate_); |
| dialog_delegate_ = delegate; |
| |
| if (dialog_delegate_) { |
| for (auto& observer : observers_) |
| observer.OnMediaDialogOpened(); |
| } else { |
| for (auto& observer : observers_) |
| observer.OnMediaDialogClosed(); |
| } |
| } |
| |
| MediaNotificationProducer* MediaNotificationService::GetNotificationProducer( |
| const std::string& notification_id) { |
| for (auto* notification_producer : notification_producers_) { |
| if (notification_producer->GetNotificationItem(notification_id)) { |
| return notification_producer; |
| } |
| } |
| return nullptr; |
| } |