1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/global_media_controls/media_notification_service.h"
6
7 #include "base/callback_list.h"
8 #include "base/metrics/field_trial_params.h"
9 #include "base/metrics/histogram_functions.h"
10 #include "base/ranges/algorithm.h"
11 #include "chrome/browser/media/router/media_router_feature.h"
12 #include "chrome/browser/profiles/profile.h"
13 #include "chrome/browser/ui/browser.h"
14 #include "chrome/browser/ui/browser_finder.h"
15 #include "chrome/browser/ui/browser_list.h"
16 #include "chrome/browser/ui/global_media_controls/media_dialog_delegate.h"
17 #include "chrome/browser/ui/global_media_controls/media_notification_container_impl.h"
18 #include "chrome/browser/ui/global_media_controls/media_notification_device_provider_impl.h"
19 #include "chrome/browser/ui/global_media_controls/media_notification_service_observer.h"
20 #include "chrome/browser/ui/global_media_controls/overlay_media_notification.h"
21 #include "chrome/browser/ui/media_router/media_router_ui.h"
22 #include "chrome/browser/ui/tabs/tab_strip_model.h"
23 #include "components/media_message_center/media_notification_item.h"
24 #include "components/media_message_center/media_notification_util.h"
25 #include "components/media_message_center/media_session_notification_item.h"
26 #include "components/media_router/browser/presentation/start_presentation_context.h"
27 #include "components/ukm/content/source_url_recorder.h"
28 #include "content/public/browser/audio_service.h"
29 #include "content/public/browser/media_session.h"
30 #include "content/public/browser/media_session_service.h"
31 #include "media/base/media_switches.h"
32 #include "services/media_session/public/mojom/media_session.mojom.h"
33 #include "services/metrics/public/cpp/ukm_builders.h"
34 #include "services/metrics/public/cpp/ukm_recorder.h"
35
36 namespace {
37
38 // The maximum number of actions we will record to UKM for a specific source.
39 constexpr int kMaxActionsRecordedToUKM = 100;
40
41 constexpr int kAutoDismissTimerInMinutesDefault = 60; // minutes
42
43 constexpr const char kAutoDismissTimerInMinutesParamName[] = "timer_in_minutes";
44
45 // These values are persisted to logs. Entries should not be renumbered and
46 // numeric values should never be reused.
47 enum class MediaNotificationClickSource {
48 kMedia = 0,
49 kPresentation,
50 kMediaFling,
51 kMaxValue = kMediaFling
52 };
53
54 // Returns the time value to be used for the auto-dismissing of the
55 // notifications after they are inactive.
56 // If the feature (auto-dismiss) is disabled, the returned value will be
57 // TimeDelta::Max() which is the largest int64 possible.
GetAutoDismissTimerValue()58 base::TimeDelta GetAutoDismissTimerValue() {
59 if (!base::FeatureList::IsEnabled(media::kGlobalMediaControlsAutoDismiss))
60 return base::TimeDelta::Max();
61
62 return base::TimeDelta::FromMinutes(base::GetFieldTrialParamByFeatureAsInt(
63 media::kGlobalMediaControlsAutoDismiss,
64 kAutoDismissTimerInMinutesParamName, kAutoDismissTimerInMinutesDefault));
65 }
66
67 // Here we check to see if the WebContents is focused. Note that since Session
68 // is a WebContentsObserver, we could in theory listen for
69 // |OnWebContentsFocused()| and |OnWebContentsLostFocus()|. However, this won't
70 // actually work since focusing the MediaDialogView causes the WebContents to
71 // "lose focus", so we'd never be focused.
IsWebContentsFocused(content::WebContents * web_contents)72 bool IsWebContentsFocused(content::WebContents* web_contents) {
73 DCHECK(web_contents);
74 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
75 if (!browser)
76 return false;
77
78 // If the given WebContents is not in the focused window, then it's not
79 // focused. Note that we know a Browser is focused because otherwise the user
80 // could not interact with the MediaDialogView.
81 if (BrowserList::GetInstance()->GetLastActive() != browser)
82 return false;
83
84 return browser->tab_strip_model()->GetActiveWebContents() == web_contents;
85 }
86
87 base::WeakPtr<media_router::WebContentsPresentationManager>
GetPresentationManager(content::WebContents * web_contents)88 GetPresentationManager(content::WebContents* web_contents) {
89 return web_contents
90 ? media_router::WebContentsPresentationManager::Get(web_contents)
91 : nullptr;
92 }
93
94 } // anonymous namespace
95
Session(MediaNotificationService * owner,const std::string & id,std::unique_ptr<media_message_center::MediaSessionNotificationItem> item,content::WebContents * web_contents,mojo::Remote<media_session::mojom::MediaController> controller)96 MediaNotificationService::Session::Session(
97 MediaNotificationService* owner,
98 const std::string& id,
99 std::unique_ptr<media_message_center::MediaSessionNotificationItem> item,
100 content::WebContents* web_contents,
101 mojo::Remote<media_session::mojom::MediaController> controller)
102 : content::WebContentsObserver(web_contents),
103 owner_(owner),
104 id_(id),
105 item_(std::move(item)),
106 presentation_manager_(GetPresentationManager(web_contents)) {
107 DCHECK(owner_);
108 DCHECK(item_);
109
110 SetController(std::move(controller));
111 if (presentation_manager_)
112 presentation_manager_->AddObserver(this);
113 }
114
~Session()115 MediaNotificationService::Session::~Session() {
116 if (presentation_manager_)
117 presentation_manager_->RemoveObserver(this);
118
119 // If we've been marked inactive, then we've already recorded inactivity as
120 // the dismiss reason.
121 if (is_marked_inactive_)
122 return;
123
124 RecordDismissReason(dismiss_reason_.value_or(
125 GlobalMediaControlsDismissReason::kMediaSessionStopped));
126 }
127
WebContentsDestroyed()128 void MediaNotificationService::Session::WebContentsDestroyed() {
129 // If the WebContents is destroyed, then we should just remove the item
130 // instead of freezing it.
131 set_dismiss_reason(GlobalMediaControlsDismissReason::kTabClosed);
132 owner_->RemoveItem(id_);
133 }
134
MediaSessionInfoChanged(media_session::mojom::MediaSessionInfoPtr session_info)135 void MediaNotificationService::Session::MediaSessionInfoChanged(
136 media_session::mojom::MediaSessionInfoPtr session_info) {
137 is_playing_ =
138 session_info && session_info->playback_state ==
139 media_session::mojom::MediaPlaybackState::kPlaying;
140
141 // If we've started playing, we don't want the inactive timer to be running.
142 if (is_playing_) {
143 if (inactive_timer_.IsRunning() || is_marked_inactive_) {
144 MarkActiveIfNecessary();
145 RecordInteractionDelayAfterPause();
146 inactive_timer_.Stop();
147 }
148 return;
149 }
150
151 // If we're in an overlay, then we don't want to count the session as
152 // inactive.
153 // TODO(https://crbug.com/1032841): This means we won't record interaction
154 // delays. Consider changing to record them.
155 if (is_in_overlay_)
156 return;
157
158 // If the timer is already running, we don't need to do anything.
159 if (inactive_timer_.IsRunning())
160 return;
161
162 last_interaction_time_ = base::TimeTicks::Now();
163 StartInactiveTimer();
164 }
165
MediaSessionActionsChanged(const std::vector<media_session::mojom::MediaSessionAction> & actions)166 void MediaNotificationService::Session::MediaSessionActionsChanged(
167 const std::vector<media_session::mojom::MediaSessionAction>& actions) {
168 bool is_audio_device_switching_supported =
169 base::ranges::find(
170 actions,
171 media_session::mojom::MediaSessionAction::kSwitchAudioDevice) !=
172 actions.end();
173 if (is_audio_device_switching_supported !=
174 is_audio_device_switching_supported_) {
175 is_audio_device_switching_supported_ = is_audio_device_switching_supported;
176 is_audio_device_switching_supported_callback_list_.Notify(
177 is_audio_device_switching_supported_);
178 }
179 }
180
MediaSessionPositionChanged(const base::Optional<media_session::MediaPosition> & position)181 void MediaNotificationService::Session::MediaSessionPositionChanged(
182 const base::Optional<media_session::MediaPosition>& position) {
183 OnSessionInteractedWith();
184 }
185
OnMediaRoutesChanged(const std::vector<media_router::MediaRoute> & routes)186 void MediaNotificationService::Session::OnMediaRoutesChanged(
187 const std::vector<media_router::MediaRoute>& routes) {
188 // Closes the media dialog after a cast session starts.
189 if (!routes.empty()) {
190 if (owner_->dialog_delegate_) {
191 owner_->dialog_delegate_->HideMediaDialog();
192 }
193 item_->Dismiss();
194 }
195 }
196
SetController(mojo::Remote<media_session::mojom::MediaController> controller)197 void MediaNotificationService::Session::SetController(
198 mojo::Remote<media_session::mojom::MediaController> controller) {
199 if (controller.is_bound()) {
200 observer_receiver_.reset();
201 controller->AddObserver(observer_receiver_.BindNewPipeAndPassRemote());
202 controller_ = std::move(controller);
203 }
204 }
205
set_dismiss_reason(GlobalMediaControlsDismissReason reason)206 void MediaNotificationService::Session::set_dismiss_reason(
207 GlobalMediaControlsDismissReason reason) {
208 DCHECK(!dismiss_reason_.has_value());
209 dismiss_reason_ = reason;
210 }
211
OnSessionInteractedWith()212 void MediaNotificationService::Session::OnSessionInteractedWith() {
213 // If we're not currently tracking inactive time, then no action is needed.
214 if (!inactive_timer_.IsRunning() && !is_marked_inactive_)
215 return;
216
217 MarkActiveIfNecessary();
218
219 RecordInteractionDelayAfterPause();
220 last_interaction_time_ = base::TimeTicks::Now();
221
222 // Otherwise, reset the timer.
223 inactive_timer_.Stop();
224 StartInactiveTimer();
225 }
226
OnSessionOverlayStateChanged(bool is_in_overlay)227 void MediaNotificationService::Session::OnSessionOverlayStateChanged(
228 bool is_in_overlay) {
229 is_in_overlay_ = is_in_overlay;
230
231 if (is_in_overlay_) {
232 // If we enter an overlay, then we don't want the session to be marked
233 // inactive.
234 if (inactive_timer_.IsRunning()) {
235 RecordInteractionDelayAfterPause();
236 inactive_timer_.Stop();
237 }
238 } else if (!is_playing_ && !inactive_timer_.IsRunning()) {
239 // If we exit an overlay and the session is paused, then the session is
240 // inactive.
241 StartInactiveTimer();
242 }
243 }
244
IsPlaying()245 bool MediaNotificationService::Session::IsPlaying() {
246 return is_playing_;
247 }
248
SetAudioSinkId(const std::string & id)249 void MediaNotificationService::Session::SetAudioSinkId(const std::string& id) {
250 controller_->SetAudioSinkId(id);
251 }
252
253 std::unique_ptr<base::RepeatingCallbackList<void(bool)>::Subscription>
254 MediaNotificationService::Session::
RegisterIsAudioDeviceSwitchingSupportedCallback(base::RepeatingCallback<void (bool)> callback)255 RegisterIsAudioDeviceSwitchingSupportedCallback(
256 base::RepeatingCallback<void(bool)> callback) {
257 callback.Run(is_audio_device_switching_supported_);
258 return is_audio_device_switching_supported_callback_list_.Add(
259 std::move(callback));
260 }
261
SetPresentationManagerForTesting(base::WeakPtr<media_router::WebContentsPresentationManager> presentation_manager)262 void MediaNotificationService::Session::SetPresentationManagerForTesting(
263 base::WeakPtr<media_router::WebContentsPresentationManager>
264 presentation_manager) {
265 presentation_manager_ = presentation_manager;
266 presentation_manager_->AddObserver(this);
267 }
268
269 // static
RecordDismissReason(GlobalMediaControlsDismissReason reason)270 void MediaNotificationService::Session::RecordDismissReason(
271 GlobalMediaControlsDismissReason reason) {
272 base::UmaHistogramEnumeration("Media.GlobalMediaControls.DismissReason",
273 reason);
274 }
275
StartInactiveTimer()276 void MediaNotificationService::Session::StartInactiveTimer() {
277 DCHECK(!inactive_timer_.IsRunning());
278
279 // Using |base::Unretained()| here is okay since |this| owns
280 // |inactive_timer_|.
281 // If the feature is disabled, the timer will run forever, in order for the
282 // rest of the code to continue running as expected.
283 inactive_timer_.Start(
284 FROM_HERE, GetAutoDismissTimerValue(),
285 base::BindOnce(&MediaNotificationService::Session::OnInactiveTimerFired,
286 base::Unretained(this)));
287 }
288
OnInactiveTimerFired()289 void MediaNotificationService::Session::OnInactiveTimerFired() {
290 // Overlay notifications should never be marked as inactive.
291 DCHECK(!is_in_overlay_);
292
293 // If the session has been paused and inactive for long enough, then mark it
294 // as inactive.
295 is_marked_inactive_ = true;
296 RecordDismissReason(GlobalMediaControlsDismissReason::kInactiveTimeout);
297 owner_->OnSessionBecameInactive(id_);
298 }
299
RecordInteractionDelayAfterPause()300 void MediaNotificationService::Session::RecordInteractionDelayAfterPause() {
301 base::TimeDelta time_since_last_interaction =
302 base::TimeTicks::Now() - last_interaction_time_;
303 base::UmaHistogramCustomTimes(
304 "Media.GlobalMediaControls.InteractionDelayAfterPause",
305 time_since_last_interaction, base::TimeDelta::FromMinutes(1),
306 base::TimeDelta::FromDays(1), 100);
307 }
308
MarkActiveIfNecessary()309 void MediaNotificationService::Session::MarkActiveIfNecessary() {
310 if (!is_marked_inactive_)
311 return;
312 is_marked_inactive_ = false;
313
314 owner_->OnSessionBecameActive(id_);
315 }
316
MediaNotificationService(Profile * profile,bool show_from_all_profiles)317 MediaNotificationService::MediaNotificationService(Profile* profile,
318 bool show_from_all_profiles)
319 : overlay_media_notifications_manager_(this) {
320 if (base::FeatureList::IsEnabled(media::kGlobalMediaControlsForCast) &&
321 media_router::MediaRouterEnabled(profile)) {
322 cast_notification_provider_ =
323 std::make_unique<CastMediaNotificationProvider>(
324 profile, this,
325 base::BindRepeating(
326 &MediaNotificationService::OnCastNotificationsChanged,
327 base::Unretained(this)));
328 }
329 if (media_router::GlobalMediaControlsCastStartStopEnabled()) {
330 presentation_request_notification_provider_ =
331 std::make_unique<PresentationRequestNotificationProvider>(this);
332 }
333
334 // Connect to the controller manager so we can create media controllers for
335 // media sessions.
336 content::GetMediaSessionService().BindMediaControllerManager(
337 controller_manager_remote_.BindNewPipeAndPassReceiver());
338
339 // Connect to receive audio focus events.
340 content::GetMediaSessionService().BindAudioFocusManager(
341 audio_focus_remote_.BindNewPipeAndPassReceiver());
342
343 if (show_from_all_profiles) {
344 audio_focus_remote_->AddObserver(
345 audio_focus_observer_receiver_.BindNewPipeAndPassRemote());
346
347 audio_focus_remote_->GetFocusRequests(
348 base::BindOnce(&MediaNotificationService::OnReceivedAudioFocusRequests,
349 weak_ptr_factory_.GetWeakPtr()));
350 } else {
351 const base::UnguessableToken& source_id =
352 content::MediaSession::GetSourceId(profile);
353
354 audio_focus_remote_->AddSourceObserver(
355 source_id, audio_focus_observer_receiver_.BindNewPipeAndPassRemote());
356
357 audio_focus_remote_->GetSourceFocusRequests(
358 source_id,
359 base::BindOnce(&MediaNotificationService::OnReceivedAudioFocusRequests,
360 weak_ptr_factory_.GetWeakPtr()));
361 }
362 }
363
~MediaNotificationService()364 MediaNotificationService::~MediaNotificationService() {
365 for (auto container_pair : observed_containers_)
366 container_pair.second->RemoveObserver(this);
367 }
368
AddObserver(MediaNotificationServiceObserver * observer)369 void MediaNotificationService::AddObserver(
370 MediaNotificationServiceObserver* observer) {
371 observers_.AddObserver(observer);
372 }
373
RemoveObserver(MediaNotificationServiceObserver * observer)374 void MediaNotificationService::RemoveObserver(
375 MediaNotificationServiceObserver* observer) {
376 observers_.RemoveObserver(observer);
377 }
378
OnFocusGained(media_session::mojom::AudioFocusRequestStatePtr session)379 void MediaNotificationService::OnFocusGained(
380 media_session::mojom::AudioFocusRequestStatePtr session) {
381 const std::string id = session->request_id->ToString();
382
383 // If we have an existing unfrozen item then this is a duplicate call and
384 // we should ignore it.
385 auto it = sessions_.find(id);
386 if (it != sessions_.end() && !it->second.item()->frozen())
387 return;
388
389 mojo::Remote<media_session::mojom::MediaController> item_controller;
390 mojo::Remote<media_session::mojom::MediaController> session_controller;
391
392 controller_manager_remote_->CreateMediaControllerForSession(
393 item_controller.BindNewPipeAndPassReceiver(), *session->request_id);
394 controller_manager_remote_->CreateMediaControllerForSession(
395 session_controller.BindNewPipeAndPassReceiver(), *session->request_id);
396
397 if (it != sessions_.end()) {
398 // If the notification was previously frozen then we should reset the
399 // controller because the mojo pipe would have been reset.
400 it->second.SetController(std::move(session_controller));
401 it->second.item()->SetController(std::move(item_controller),
402 std::move(session->session_info));
403 } else {
404 sessions_.emplace(
405 std::piecewise_construct, std::forward_as_tuple(id),
406 std::forward_as_tuple(
407 this, id,
408 std::make_unique<
409 media_message_center::MediaSessionNotificationItem>(
410 this, id, session->source_name.value_or(std::string()),
411 std::move(item_controller), std::move(session->session_info)),
412 content::MediaSession::GetWebContentsFromRequestId(
413 *session->request_id),
414 std::move(session_controller)));
415 }
416 }
417
OnFocusLost(media_session::mojom::AudioFocusRequestStatePtr session)418 void MediaNotificationService::OnFocusLost(
419 media_session::mojom::AudioFocusRequestStatePtr session) {
420 const std::string id = session->request_id->ToString();
421
422 auto it = sessions_.find(id);
423 if (it == sessions_.end())
424 return;
425
426 // If we're not currently showing this item, then we can just remove it.
427 if (!base::Contains(active_controllable_session_ids_, id) &&
428 !base::Contains(frozen_session_ids_, id) &&
429 !base::Contains(dragged_out_session_ids_, id)) {
430 RemoveItem(id);
431 return;
432 }
433
434 // Otherwise, freeze it in case it regains focus quickly.
435 it->second.item()->Freeze(base::BindOnce(
436 &MediaNotificationService::OnItemUnfrozen, base::Unretained(this), id));
437 active_controllable_session_ids_.erase(id);
438 frozen_session_ids_.insert(id);
439 OnNotificationChanged(&id);
440 }
441
ShowNotification(const std::string & id)442 void MediaNotificationService::ShowNotification(const std::string& id) {
443 // If the notification is currently hidden because it's inactive or because
444 // it's in an overlay notification, then do nothing.
445 if (base::Contains(dragged_out_session_ids_, id) ||
446 base::Contains(inactive_session_ids_, id)) {
447 return;
448 }
449
450 active_controllable_session_ids_.insert(id);
451 OnNotificationChanged(&id);
452
453 if (!dialog_delegate_)
454 return;
455
456 base::WeakPtr<media_message_center::MediaNotificationItem> item =
457 GetNotificationItem(id);
458 MediaNotificationContainerImpl* container =
459 dialog_delegate_->ShowMediaSession(id, item);
460
461 // Observe the container for dismissal.
462 if (container) {
463 container->AddObserver(this);
464 observed_containers_[id] = container;
465 }
466 }
467
HideNotification(const std::string & id)468 void MediaNotificationService::HideNotification(const std::string& id) {
469 active_controllable_session_ids_.erase(id);
470 frozen_session_ids_.erase(id);
471
472 if (base::Contains(dragged_out_session_ids_, id)) {
473 overlay_media_notifications_manager_.CloseOverlayNotification(id);
474 dragged_out_session_ids_.erase(id);
475 }
476
477 OnNotificationChanged(&id);
478
479 if (!dialog_delegate_)
480 return;
481
482 dialog_delegate_->HideMediaSession(id);
483 }
484
RemoveItem(const std::string & id)485 void MediaNotificationService::RemoveItem(const std::string& id) {
486 active_controllable_session_ids_.erase(id);
487 frozen_session_ids_.erase(id);
488 inactive_session_ids_.erase(id);
489 supplemental_notifications_.erase(id);
490
491 if (base::Contains(dragged_out_session_ids_, id)) {
492 overlay_media_notifications_manager_.CloseOverlayNotification(id);
493 dragged_out_session_ids_.erase(id);
494 }
495
496 // Copy |id| to avoid a dangling reference after the session is deleted. This
497 // happens when |id| refers to a string owned by the session being removed.
498 const auto id_copy{id};
499
500 sessions_.erase(id);
501
502 OnNotificationChanged(&id_copy);
503 }
504
505 scoped_refptr<base::SequencedTaskRunner>
GetTaskRunner() const506 MediaNotificationService::GetTaskRunner() const {
507 return nullptr;
508 }
509
LogMediaSessionActionButtonPressed(const std::string & id,media_session::mojom::MediaSessionAction action)510 void MediaNotificationService::LogMediaSessionActionButtonPressed(
511 const std::string& id,
512 media_session::mojom::MediaSessionAction action) {
513 auto it = sessions_.find(id);
514 if (it == sessions_.end())
515 return;
516
517 content::WebContents* web_contents = it->second.web_contents();
518 if (!web_contents)
519 return;
520
521 base::UmaHistogramBoolean("Media.GlobalMediaControls.UserActionFocus",
522 IsWebContentsFocused(web_contents));
523
524 ukm::UkmRecorder* recorder = ukm::UkmRecorder::Get();
525 ukm::SourceId source_id =
526 ukm::GetSourceIdForWebContentsDocument(web_contents);
527
528 if (++actions_recorded_to_ukm_[source_id] > kMaxActionsRecordedToUKM)
529 return;
530
531 ukm::builders::Media_GlobalMediaControls_ActionButtonPressed(source_id)
532 .SetMediaSessionAction(static_cast<int64_t>(action))
533 .Record(recorder);
534 }
535
OnContainerClicked(const std::string & id)536 void MediaNotificationService::OnContainerClicked(const std::string& id) {
537 auto it = sessions_.find(id);
538 if (it == sessions_.end())
539 return;
540
541 it->second.OnSessionInteractedWith();
542
543 content::WebContents* web_contents = it->second.web_contents();
544 if (!web_contents)
545 return;
546
547 content::WebContentsDelegate* delegate = web_contents->GetDelegate();
548 if (!delegate)
549 return;
550
551 base::UmaHistogramEnumeration("Media.Notification.Click",
552 MediaNotificationClickSource::kMedia);
553
554 delegate->ActivateContents(web_contents);
555 }
556
OnContainerDismissed(const std::string & id)557 void MediaNotificationService::OnContainerDismissed(const std::string& id) {
558 // If the notification is dragged out, then dismissing should just close the
559 // overlay notification.
560 if (base::Contains(dragged_out_session_ids_, id)) {
561 overlay_media_notifications_manager_.CloseOverlayNotification(id);
562 return;
563 }
564
565 Session* session = GetSession(id);
566 if (!session) {
567 auto item = GetNonSessionNotificationItem(id);
568 if (item)
569 item->Dismiss();
570 return;
571 }
572
573 session->set_dismiss_reason(
574 GlobalMediaControlsDismissReason::kUserDismissedNotification);
575 session->item()->Dismiss();
576 }
577
OnContainerDestroyed(const std::string & id)578 void MediaNotificationService::OnContainerDestroyed(const std::string& id) {
579 auto iter = observed_containers_.find(id);
580 DCHECK(iter != observed_containers_.end());
581
582 iter->second->RemoveObserver(this);
583 observed_containers_.erase(iter);
584 }
585
OnContainerDraggedOut(const std::string & id,gfx::Rect bounds)586 void MediaNotificationService::OnContainerDraggedOut(const std::string& id,
587 gfx::Rect bounds) {
588 // If the session has been destroyed, no action is needed.
589 auto it = sessions_.find(id);
590 if (it == sessions_.end())
591 return;
592
593 // Inform the Session that it's in an overlay so should not timeout as
594 // inactive.
595 it->second.OnSessionOverlayStateChanged(/*is_in_overlay=*/true);
596
597 if (!dialog_delegate_)
598 return;
599
600 std::unique_ptr<OverlayMediaNotification> overlay_notification =
601 dialog_delegate_->PopOut(id, bounds);
602 if (!overlay_notification)
603 return;
604
605 overlay_media_notifications_manager_.ShowOverlayNotification(
606 id, std::move(overlay_notification));
607 active_controllable_session_ids_.erase(id);
608 dragged_out_session_ids_.insert(id);
609 OnNotificationChanged(&id);
610 }
611
OnAudioSinkChosen(const std::string & id,const std::string & sink_id)612 void MediaNotificationService::OnAudioSinkChosen(const std::string& id,
613 const std::string& sink_id) {
614 auto it = sessions_.find(id);
615 DCHECK(it != sessions_.end());
616 it->second.SetAudioSinkId(sink_id);
617 }
618
Shutdown()619 void MediaNotificationService::Shutdown() {
620 // |cast_notification_provider_| and
621 // |presentation_request_notification_provider_| depend on MediaRouter, which
622 // is another keyed service.
623 cast_notification_provider_.reset();
624 presentation_request_notification_provider_.reset();
625 }
626
AddSupplementalNotification(const std::string & id,content::WebContents * web_contents)627 void MediaNotificationService::AddSupplementalNotification(
628 const std::string& id,
629 content::WebContents* web_contents) {
630 DCHECK(web_contents);
631 supplemental_notifications_.emplace(id, web_contents);
632 if (!HasSessionForWebContents(web_contents))
633 ShowNotification(id);
634 }
635
OnOverlayNotificationClosed(const std::string & id)636 void MediaNotificationService::OnOverlayNotificationClosed(
637 const std::string& id) {
638 // If the session has been destroyed, no action is needed.
639 auto it = sessions_.find(id);
640 if (it == sessions_.end())
641 return;
642
643 it->second.OnSessionOverlayStateChanged(/*is_in_overlay=*/false);
644
645 // Since the overlay is closing, we no longer need to observe the associated
646 // container.
647 auto observed_iter = observed_containers_.find(id);
648 if (observed_iter != observed_containers_.end()) {
649 observed_iter->second->RemoveObserver(this);
650 observed_containers_.erase(observed_iter);
651 }
652
653 // Otherwise, if it's a non-frozen item, then it's now an active one.
654 if (!base::Contains(frozen_session_ids_, id))
655 active_controllable_session_ids_.insert(id);
656 dragged_out_session_ids_.erase(id);
657
658 OnNotificationChanged(&id);
659
660 // If there's a dialog currently open, then we should show the item in the
661 // dialog.
662 if (!dialog_delegate_)
663 return;
664
665 MediaNotificationContainerImpl* container =
666 dialog_delegate_->ShowMediaSession(id, it->second.item()->GetWeakPtr());
667
668 if (container) {
669 container->AddObserver(this);
670 observed_containers_[id] = container;
671 }
672 }
673
OnCastNotificationsChanged()674 void MediaNotificationService::OnCastNotificationsChanged() {
675 OnNotificationChanged(nullptr);
676 }
677
SetDialogDelegate(MediaDialogDelegate * delegate)678 void MediaNotificationService::SetDialogDelegate(
679 MediaDialogDelegate* delegate) {
680 DCHECK(!delegate || !dialog_delegate_);
681 dialog_delegate_ = delegate;
682
683 if (dialog_delegate_) {
684 for (auto& observer : observers_)
685 observer.OnMediaDialogOpened();
686 } else {
687 for (auto& observer : observers_)
688 observer.OnMediaDialogClosed();
689 }
690
691 if (!dialog_delegate_)
692 return;
693
694 std::list<std::string> sorted_session_ids;
695 for (const std::string& id : active_controllable_session_ids_) {
696 if (sessions_.find(id)->second.IsPlaying())
697 sorted_session_ids.push_front(id);
698 else
699 sorted_session_ids.push_back(id);
700 }
701
702 for (const std::string& id : sorted_session_ids) {
703 base::WeakPtr<media_message_center::MediaNotificationItem> item =
704 GetNotificationItem(id);
705 MediaNotificationContainerImpl* container =
706 dialog_delegate_->ShowMediaSession(id, item);
707
708 // Observe the container for dismissal.
709 if (container) {
710 container->AddObserver(this);
711 observed_containers_[id] = container;
712 }
713 }
714
715 media_message_center::RecordConcurrentNotificationCount(
716 active_controllable_session_ids_.size());
717
718 if (cast_notification_provider_) {
719 media_message_center::RecordConcurrentCastNotificationCount(
720 cast_notification_provider_->GetItemCount());
721 }
722 }
723
HasActiveNotifications() const724 bool MediaNotificationService::HasActiveNotifications() const {
725 return !active_controllable_session_ids_.empty();
726 }
727
HasFrozenNotifications() const728 bool MediaNotificationService::HasFrozenNotifications() const {
729 return !frozen_session_ids_.empty();
730 }
731
HasOpenDialog() const732 bool MediaNotificationService::HasOpenDialog() const {
733 return !!dialog_delegate_;
734 }
735
OnSessionBecameActive(const std::string & id)736 void MediaNotificationService::OnSessionBecameActive(const std::string& id) {
737 DCHECK(base::Contains(inactive_session_ids_, id));
738
739 auto it = sessions_.find(id);
740 DCHECK(it != sessions_.end());
741
742 inactive_session_ids_.erase(id);
743
744 if (it->second.item()->frozen())
745 frozen_session_ids_.insert(id);
746 else
747 active_controllable_session_ids_.insert(id);
748
749 OnNotificationChanged(&id);
750
751 // If there's a dialog currently open, then we should show the item in the
752 // dialog.
753 if (!dialog_delegate_)
754 return;
755
756 MediaNotificationContainerImpl* container =
757 dialog_delegate_->ShowMediaSession(id, it->second.item()->GetWeakPtr());
758
759 if (container) {
760 container->AddObserver(this);
761 observed_containers_[id] = container;
762 }
763 }
764
OnSessionBecameInactive(const std::string & id)765 void MediaNotificationService::OnSessionBecameInactive(const std::string& id) {
766 // If this session is already marked inactive, then there's nothing to do.
767 if (base::Contains(inactive_session_ids_, id))
768 return;
769
770 inactive_session_ids_.insert(id);
771
772 HideNotification(id);
773 }
774
775 std::unique_ptr<
776 MediaNotificationDeviceProvider::GetOutputDevicesCallbackList::Subscription>
RegisterAudioOutputDeviceDescriptionsCallback(MediaNotificationDeviceProvider::GetOutputDevicesCallback callback)777 MediaNotificationService::RegisterAudioOutputDeviceDescriptionsCallback(
778 MediaNotificationDeviceProvider::GetOutputDevicesCallback callback) {
779 if (!device_provider_)
780 device_provider_ = std::make_unique<MediaNotificationDeviceProviderImpl>(
781 content::CreateAudioSystemForAudioService());
782 return device_provider_->RegisterOutputDeviceDescriptionsCallback(
783 std::move(callback));
784 }
785
786 std::unique_ptr<base::RepeatingCallbackList<void(bool)>::Subscription>
RegisterIsAudioOutputDeviceSwitchingSupportedCallback(const std::string & id,base::RepeatingCallback<void (bool)> callback)787 MediaNotificationService::RegisterIsAudioOutputDeviceSwitchingSupportedCallback(
788 const std::string& id,
789 base::RepeatingCallback<void(bool)> callback) {
790 auto it = sessions_.find(id);
791 DCHECK(it != sessions_.end());
792
793 return it->second.RegisterIsAudioDeviceSwitchingSupportedCallback(
794 std::move(callback));
795 }
796
OnStartPresentationContextCreated(std::unique_ptr<media_router::StartPresentationContext> context)797 void MediaNotificationService::OnStartPresentationContextCreated(
798 std::unique_ptr<media_router::StartPresentationContext> context) {
799 if (presentation_request_notification_provider_) {
800 presentation_request_notification_provider_
801 ->OnStartPresentationContextCreated(std::move(context));
802 }
803 }
804
set_device_provider_for_testing(std::unique_ptr<MediaNotificationDeviceProvider> device_provider)805 void MediaNotificationService::set_device_provider_for_testing(
806 std::unique_ptr<MediaNotificationDeviceProvider> device_provider) {
807 device_provider_ = std::move(device_provider);
808 }
809
810 std::unique_ptr<media_router::CastDialogController>
CreateCastDialogControllerForSession(const std::string & session_id)811 MediaNotificationService::CreateCastDialogControllerForSession(
812 const std::string& session_id) {
813 auto it = sessions_.find(session_id);
814 if (it != sessions_.end()) {
815 auto ui = std::make_unique<media_router::MediaRouterUI>(
816 it->second.web_contents());
817 ui->InitWithDefaultMediaSource();
818 return ui;
819 }
820 return nullptr;
821 }
822
OnItemUnfrozen(const std::string & id)823 void MediaNotificationService::OnItemUnfrozen(const std::string& id) {
824 frozen_session_ids_.erase(id);
825
826 if (!base::Contains(dragged_out_session_ids_, id))
827 active_controllable_session_ids_.insert(id);
828
829 OnNotificationChanged(&id);
830 }
831
OnReceivedAudioFocusRequests(std::vector<media_session::mojom::AudioFocusRequestStatePtr> sessions)832 void MediaNotificationService::OnReceivedAudioFocusRequests(
833 std::vector<media_session::mojom::AudioFocusRequestStatePtr> sessions) {
834 for (auto& session : sessions)
835 OnFocusGained(std::move(session));
836 }
837
838 base::WeakPtr<media_message_center::MediaNotificationItem>
GetNotificationItem(const std::string & id)839 MediaNotificationService::GetNotificationItem(const std::string& id) {
840 Session* session = GetSession(id);
841 if (session)
842 return session->item()->GetWeakPtr();
843 return GetNonSessionNotificationItem(id);
844 }
845
GetSession(const std::string & id)846 MediaNotificationService::Session* MediaNotificationService::GetSession(
847 const std::string& id) {
848 auto it = sessions_.find(id);
849 return it == sessions_.end() ? nullptr : &it->second;
850 }
851
852 base::WeakPtr<media_message_center::MediaNotificationItem>
GetNonSessionNotificationItem(const std::string & id)853 MediaNotificationService::GetNonSessionNotificationItem(const std::string& id) {
854 if (cast_notification_provider_) {
855 auto item = cast_notification_provider_->GetNotificationItem(id);
856 if (item)
857 return item;
858 }
859
860 if (presentation_request_notification_provider_) {
861 auto item =
862 presentation_request_notification_provider_->GetNotificationItem(id);
863 if (item)
864 return item;
865 }
866
867 return nullptr;
868 }
869
OnNotificationChanged(const std::string * changed_notification_id)870 void MediaNotificationService::OnNotificationChanged(
871 const std::string* changed_notification_id) {
872 for (auto& observer : observers_)
873 observer.OnNotificationListChanged();
874
875 // Avoid re-examining the supplemental notifications as a side-effect of
876 // hiding a supplemental notification.
877 if (!changed_notification_id ||
878 base::Contains(supplemental_notifications_, *changed_notification_id))
879 return;
880
881 // Hide supplemental notifications if necessary.
882 for (const auto& pair : supplemental_notifications_) {
883 // If there is an active session associated with the same web contents as
884 // this supplemental notification, hide it.
885 if (HasSessionForWebContents(pair.second)) {
886 HideNotification(pair.first);
887 }
888 }
889 }
890
HasSessionForWebContents(content::WebContents * web_contents) const891 bool MediaNotificationService::HasSessionForWebContents(
892 content::WebContents* web_contents) const {
893 DCHECK(web_contents);
894 return std::any_of(sessions_.begin(), sessions_.end(),
895 [web_contents, this](const auto& pair) {
896 return pair.second.web_contents() == web_contents &&
897 base::Contains(active_controllable_session_ids_,
898 pair.first);
899 });
900 }
901