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