Commit cb9a2f55 authored by Tommy Steimel's avatar Tommy Steimel Committed by Commit Bot

GMC: Refactor to have profile-level session tracking

This CL pulls most of the logic out of MediaToolbarButtonController and
puts it into MediaNotificationService which lives as a profile-keyed
service. This allows the media session tracking to be shared between
windows, and makes the MediaToolbarButtonController make more sense.

Bug: 1021369
Change-Id: I03f302cb8b80099df011fa04e47df389c2d5d8f4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1898278Reviewed-by: default avatarElly Fong-Jones <ellyjones@chromium.org>
Reviewed-by: default avatarTakumi Fujimoto <takumif@chromium.org>
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#712761}
parent d1ed7671
......@@ -940,6 +940,11 @@ jumbo_static_library("ui") {
"global_media_controls/media_dialog_delegate.h",
"global_media_controls/media_notification_container_impl.h",
"global_media_controls/media_notification_container_observer.h",
"global_media_controls/media_notification_service.cc",
"global_media_controls/media_notification_service.h",
"global_media_controls/media_notification_service_factory.cc",
"global_media_controls/media_notification_service_factory.h",
"global_media_controls/media_notification_service_observer.h",
"global_media_controls/media_toolbar_button_controller.cc",
"global_media_controls/media_toolbar_button_controller.h",
"global_media_controls/media_toolbar_button_controller_delegate.cc",
......
// 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 "base/metrics/histogram_functions.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/browser_list.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_service_observer.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 "content/public/browser/media_session.h"
#include "media/base/media_switches.h"
#include "services/media_session/public/mojom/constants.mojom.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "services/service_manager/public/cpp/connector.h"
namespace {
// Here we check to see if the WebContents is focused. Note that since Session
// is a WebContentsObserver, we could in theory listen for
// |OnWebContentsFocused()| and |OnWebContentsLostFocus()|. However, this won't
// actually work since focusing the MediaDialogView causes the WebContents to
// "lose focus", so we'd never be focused.
bool IsWebContentsFocused(content::WebContents* web_contents) {
DCHECK(web_contents);
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return false;
// If the given WebContents is not in the focused window, then it's not
// focused. Note that we know a Browser is focused because otherwise the user
// could not interact with the MediaDialogView.
if (BrowserList::GetInstance()->GetLastActive() != browser)
return false;
return browser->tab_strip_model()->GetActiveWebContents() == web_contents;
}
} // anonymous namespace
MediaNotificationService::Session::Session(
MediaNotificationService* owner,
const std::string& id,
std::unique_ptr<media_message_center::MediaSessionNotificationItem> item,
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
owner_(owner),
id_(id),
item_(std::move(item)) {
DCHECK(owner_);
DCHECK(item_);
}
MediaNotificationService::Session::~Session() = default;
void MediaNotificationService::Session::WebContentsDestroyed() {
// If the WebContents is destroyed, then we should just remove the item
// instead of freezing it.
owner_->RemoveItem(id_);
}
MediaNotificationService::MediaNotificationService(
Profile* profile,
service_manager::Connector* connector)
: connector_(connector) {
if (base::FeatureList::IsEnabled(media::kGlobalMediaControlsForCast) &&
media_router::MediaRouterEnabled(profile)) {
cast_notification_provider_ =
std::make_unique<CastMediaNotificationProvider>(
profile, this,
base::BindRepeating(
&MediaNotificationService::OnCastNotificationsChanged,
base::Unretained(this)));
}
// |connector_| can be null in tests.
if (!connector_)
return;
const base::UnguessableToken& source_id =
content::MediaSession::GetSourceId(profile);
// Connect to the controller manager so we can create media controllers for
// media sessions.
connector_->Connect(media_session::mojom::kServiceName,
controller_manager_remote_.BindNewPipeAndPassReceiver());
// Connect to receive audio focus events.
connector_->Connect(media_session::mojom::kServiceName,
audio_focus_remote_.BindNewPipeAndPassReceiver());
audio_focus_remote_->AddSourceObserver(
source_id, audio_focus_observer_receiver_.BindNewPipeAndPassRemote());
audio_focus_remote_->GetSourceFocusRequests(
source_id,
base::BindOnce(&MediaNotificationService::OnReceivedAudioFocusRequests,
weak_ptr_factory_.GetWeakPtr()));
}
MediaNotificationService::~MediaNotificationService() {
for (auto container_pair : observed_containers_)
container_pair.second->RemoveObserver(this);
}
void MediaNotificationService::AddObserver(
MediaNotificationServiceObserver* observer) {
observers_.AddObserver(observer);
}
void MediaNotificationService::RemoveObserver(
MediaNotificationServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
void MediaNotificationService::OnFocusGained(
media_session::mojom::AudioFocusRequestStatePtr session) {
const std::string id = session->request_id->ToString();
// If we have an existing unfrozen item then this is a duplicate call and
// we should ignore it.
auto it = sessions_.find(id);
if (it != sessions_.end() && !it->second.item()->frozen())
return;
mojo::Remote<media_session::mojom::MediaController> controller;
// |controller_manager_remote_| may be null in tests where connector is
// unavailable.
if (controller_manager_remote_) {
controller_manager_remote_->CreateMediaControllerForSession(
controller.BindNewPipeAndPassReceiver(), *session->request_id);
}
if (it != sessions_.end()) {
// If the notification was previously frozen then we should reset the
// controller because the mojo pipe would have been reset.
it->second.item()->SetController(std::move(controller),
std::move(session->session_info));
active_controllable_session_ids_.insert(id);
frozen_session_ids_.erase(id);
for (auto& observer : observers_)
observer.OnNotificationListChanged();
} else {
sessions_.emplace(
std::piecewise_construct, std::forward_as_tuple(id),
std::forward_as_tuple(
this, id,
std::make_unique<
media_message_center::MediaSessionNotificationItem>(
this, id, session->source_name.value_or(std::string()),
std::move(controller), std::move(session->session_info)),
content::MediaSession::GetWebContentsFromRequestId(
*session->request_id)));
}
}
void MediaNotificationService::OnFocusLost(
media_session::mojom::AudioFocusRequestStatePtr session) {
const std::string id = session->request_id->ToString();
auto it = sessions_.find(id);
if (it == sessions_.end())
return;
it->second.item()->Freeze();
active_controllable_session_ids_.erase(id);
frozen_session_ids_.insert(id);
for (auto& observer : observers_)
observer.OnNotificationListChanged();
}
void MediaNotificationService::ShowNotification(const std::string& id) {
active_controllable_session_ids_.insert(id);
for (auto& observer : observers_)
observer.OnNotificationListChanged();
if (!dialog_delegate_)
return;
base::WeakPtr<media_message_center::MediaNotificationItem> item =
GetNotificationItem(id);
MediaNotificationContainerImpl* container =
dialog_delegate_->ShowMediaSession(id, item);
// Observe the container for dismissal.
if (container) {
container->AddObserver(this);
observed_containers_[id] = container;
}
}
void MediaNotificationService::HideNotification(const std::string& id) {
active_controllable_session_ids_.erase(id);
frozen_session_ids_.erase(id);
for (auto& observer : observers_)
observer.OnNotificationListChanged();
if (!dialog_delegate_)
return;
dialog_delegate_->HideMediaSession(id);
}
scoped_refptr<base::SequencedTaskRunner>
MediaNotificationService::GetTaskRunner() const {
return nullptr;
}
void MediaNotificationService::RemoveItem(const std::string& id) {
active_controllable_session_ids_.erase(id);
frozen_session_ids_.erase(id);
sessions_.erase(id);
for (auto& observer : observers_)
observer.OnNotificationListChanged();
}
void MediaNotificationService::LogMediaSessionActionButtonPressed(
const std::string& id) {
auto it = sessions_.find(id);
if (it == sessions_.end())
return;
content::WebContents* web_contents = it->second.web_contents();
if (!web_contents)
return;
base::UmaHistogramBoolean("Media.GlobalMediaControls.UserActionFocus",
IsWebContentsFocused(web_contents));
}
void MediaNotificationService::OnContainerClicked(const std::string& id) {
auto it = sessions_.find(id);
if (it == sessions_.end())
return;
content::WebContents* web_contents = it->second.web_contents();
if (!web_contents)
return;
content::WebContentsDelegate* delegate = web_contents->GetDelegate();
if (!delegate)
return;
delegate->ActivateContents(web_contents);
}
void MediaNotificationService::OnContainerDismissed(const std::string& id) {
auto it = sessions_.find(id);
if (it != sessions_.end())
it->second.item()->Dismiss();
}
void MediaNotificationService::OnContainerDestroyed(const std::string& id) {
auto iter = observed_containers_.find(id);
DCHECK(iter != observed_containers_.end());
iter->second->RemoveObserver(this);
observed_containers_.erase(iter);
}
void MediaNotificationService::OnCastNotificationsChanged() {
for (auto& observer : observers_)
observer.OnNotificationListChanged();
}
void MediaNotificationService::SetDialogDelegate(
MediaDialogDelegate* delegate) {
DCHECK(!delegate || !dialog_delegate_);
dialog_delegate_ = delegate;
for (auto& observer : observers_)
observer.OnMediaDialogOpenedOrClosed();
if (!dialog_delegate_)
return;
for (const std::string& id : active_controllable_session_ids_) {
base::WeakPtr<media_message_center::MediaNotificationItem> item =
GetNotificationItem(id);
MediaNotificationContainerImpl* container =
dialog_delegate_->ShowMediaSession(id, item);
// Observe the container for dismissal.
if (container) {
container->AddObserver(this);
observed_containers_[id] = container;
}
}
media_message_center::RecordConcurrentNotificationCount(
active_controllable_session_ids_.size());
}
bool MediaNotificationService::HasActiveNotifications() const {
return !active_controllable_session_ids_.empty() ||
(cast_notification_provider_ &&
cast_notification_provider_->HasItems());
}
bool MediaNotificationService::HasFrozenNotifications() const {
return !frozen_session_ids_.empty();
}
bool MediaNotificationService::HasOpenDialog() const {
return !!dialog_delegate_;
}
void MediaNotificationService::OnReceivedAudioFocusRequests(
std::vector<media_session::mojom::AudioFocusRequestStatePtr> sessions) {
for (auto& session : sessions)
OnFocusGained(std::move(session));
}
base::WeakPtr<media_message_center::MediaNotificationItem>
MediaNotificationService::GetNotificationItem(const std::string& id) {
auto it = sessions_.find(id);
if (it != sessions_.end()) {
return it->second.item()->GetWeakPtr();
} else if (cast_notification_provider_) {
return cast_notification_provider_->GetNotificationItem(id);
}
return nullptr;
}
// 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.
#ifndef CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_H_
#define CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_H_
#include <map>
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/global_media_controls/cast_media_notification_provider.h"
#include "chrome/browser/ui/global_media_controls/media_notification_container_observer.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/media_message_center/media_notification_controller.h"
#include "content/public/browser/web_contents_observer.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/media_session/public/mojom/audio_focus.mojom.h"
#include "services/media_session/public/mojom/media_controller.mojom.h"
namespace content {
class WebContents;
} // namespace content
namespace media_message_center {
class MediaSessionNotificationItem;
} // namespace media_message_center
namespace service_manager {
class Connector;
} // namespace service_manager
class MediaDialogDelegate;
class MediaNotificationContainerImpl;
class MediaNotificationServiceObserver;
class MediaNotificationService
: public KeyedService,
public media_session::mojom::AudioFocusObserver,
public media_message_center::MediaNotificationController,
public MediaNotificationContainerObserver {
public:
MediaNotificationService(Profile* profile,
service_manager::Connector* connector);
MediaNotificationService(const MediaNotificationService&) = delete;
MediaNotificationService& operator=(const MediaNotificationService&) = delete;
~MediaNotificationService() override;
void AddObserver(MediaNotificationServiceObserver* observer);
void RemoveObserver(MediaNotificationServiceObserver* observer);
// media_session::mojom::AudioFocusObserver implementation.
void OnFocusGained(
media_session::mojom::AudioFocusRequestStatePtr session) override;
void OnFocusLost(
media_session::mojom::AudioFocusRequestStatePtr session) override;
// media_message_center::MediaNotificationController implementation.
void ShowNotification(const std::string& id) override;
void HideNotification(const std::string& id) override;
void RemoveItem(const std::string& id) override;
scoped_refptr<base::SequencedTaskRunner> GetTaskRunner() const override;
void LogMediaSessionActionButtonPressed(const std::string& id) override;
// MediaNotificationContainerObserver implementation.
void OnContainerExpanded(bool expanded) override {}
void OnContainerMetadataChanged() override {}
void OnContainerClicked(const std::string& id) override;
void OnContainerDismissed(const std::string& id) override;
void OnContainerDestroyed(const std::string& id) override;
void OnCastNotificationsChanged();
void SetDialogDelegate(MediaDialogDelegate* delegate);
// True if there are active non-frozen media session notifications or active
// cast notifications.
bool HasActiveNotifications() const;
// True if there are active frozen media session notifications.
bool HasFrozenNotifications() const;
// True if there is an open MediaDialogView associated with this service.
bool HasOpenDialog() const;
private:
friend class MediaNotificationServiceTest;
friend class MediaToolbarButtonControllerTest;
class Session : public content::WebContentsObserver {
public:
Session(MediaNotificationService* owner,
const std::string& id,
std::unique_ptr<media_message_center::MediaSessionNotificationItem>
item,
content::WebContents* web_contents);
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
~Session() override;
// content::WebContentsObserver implementation.
void WebContentsDestroyed() override;
media_message_center::MediaSessionNotificationItem* item() {
return item_.get();
}
private:
MediaNotificationService* owner_;
const std::string id_;
std::unique_ptr<media_message_center::MediaSessionNotificationItem> item_;
};
void OnReceivedAudioFocusRequests(
std::vector<media_session::mojom::AudioFocusRequestStatePtr> sessions);
base::WeakPtr<media_message_center::MediaNotificationItem>
GetNotificationItem(const std::string& id);
service_manager::Connector* const connector_;
MediaDialogDelegate* dialog_delegate_ = nullptr;
// Used to track whether there are any active controllable media sessions. If
// not, then there's nothing to show in the dialog and we can hide the toolbar
// icon.
std::unordered_set<std::string> active_controllable_session_ids_;
// Tracks the sessions that are currently frozen. If there are only frozen
// sessions, we will disable the toolbar icon and wait to hide it.
std::unordered_set<std::string> frozen_session_ids_;
// Stores a Session for each media session keyed by its |request_id| in string
// format.
std::map<std::string, Session> sessions_;
// A map of all containers we're currently observing.
std::map<std::string, MediaNotificationContainerImpl*> observed_containers_;
// Connections with the media session service to listen for audio focus
// updates and control media sessions.
mojo::Remote<media_session::mojom::AudioFocusManager> audio_focus_remote_;
mojo::Remote<media_session::mojom::MediaControllerManager>
controller_manager_remote_;
mojo::Receiver<media_session::mojom::AudioFocusObserver>
audio_focus_observer_receiver_{this};
std::unique_ptr<CastMediaNotificationProvider> cast_notification_provider_;
base::ObserverList<MediaNotificationServiceObserver> observers_;
base::WeakPtrFactory<MediaNotificationService> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_H_
// 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_factory.h"
#include <memory>
#include "base/memory/singleton.h"
#include "chrome/browser/profiles/incognito_helpers.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/global_media_controls/media_notification_service.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "content/public/browser/system_connector.h"
MediaNotificationServiceFactory::MediaNotificationServiceFactory()
: BrowserContextKeyedServiceFactory(
"MediaNotificationService",
BrowserContextDependencyManager::GetInstance()) {}
MediaNotificationServiceFactory::~MediaNotificationServiceFactory() {}
// static
MediaNotificationServiceFactory*
MediaNotificationServiceFactory::GetInstance() {
return base::Singleton<MediaNotificationServiceFactory>::get();
}
// static
MediaNotificationService* MediaNotificationServiceFactory::GetForProfile(
Profile* profile) {
return static_cast<MediaNotificationService*>(
GetInstance()->GetServiceForBrowserContext(profile, true));
}
KeyedService* MediaNotificationServiceFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
return new MediaNotificationService(Profile::FromBrowserContext(context),
content::GetSystemConnector());
}
content::BrowserContext*
MediaNotificationServiceFactory::GetBrowserContextToUse(
content::BrowserContext* context) const {
return chrome::GetBrowserContextOwnInstanceInIncognito(context);
}
// 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.
#ifndef CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_FACTORY_H_
#define CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_FACTORY_H_
#include "base/macros.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
class Profile;
namespace base {
template <typename T>
struct DefaultSingletonTraits;
} // namespace base
namespace content {
class BrowserContext;
} // namespace content
class MediaNotificationService;
class MediaNotificationServiceFactory
: public BrowserContextKeyedServiceFactory {
public:
MediaNotificationServiceFactory(const MediaNotificationServiceFactory&) =
delete;
MediaNotificationServiceFactory& operator=(
const MediaNotificationServiceFactory&) = delete;
static MediaNotificationServiceFactory* GetInstance();
static MediaNotificationService* GetForProfile(Profile* profile);
private:
friend struct base::DefaultSingletonTraits<MediaNotificationServiceFactory>;
MediaNotificationServiceFactory();
~MediaNotificationServiceFactory() override;
// BrowserContextKeyedServiceFactory overrides:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override;
content::BrowserContext* GetBrowserContextToUse(
content::BrowserContext* context) const override;
};
#endif // CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_FACTORY_H_
// 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.
#ifndef CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_OBSERVER_H_
#define CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_OBSERVER_H_
#include "base/observer_list_types.h"
class MediaNotificationServiceObserver : public base::CheckedObserver {
public:
// Called when the list of active, cast, or frozen media notifications
// changes.
virtual void OnNotificationListChanged() = 0;
// Called when a media dialog associated with the service is either opened or
// closed.
virtual void OnMediaDialogOpenedOrClosed() = 0;
protected:
~MediaNotificationServiceObserver() override = default;
};
#endif // CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_NOTIFICATION_SERVICE_OBSERVER_H_
// 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/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "chrome/browser/media/router/media_router_factory.h"
#include "chrome/browser/media/router/test/mock_media_router.h"
#include "chrome/browser/ui/global_media_controls/cast_media_notification_provider.h"
#include "chrome/browser/ui/global_media_controls/media_dialog_delegate.h"
#include "chrome/browser/ui/global_media_controls/media_notification_service_observer.h"
#include "chrome/test/base/testing_profile.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 "content/public/test/browser_task_environment.h"
#include "media/base/media_switches.h"
#include "services/media_session/public/mojom/audio_focus.mojom.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using media_session::mojom::AudioFocusRequestState;
using media_session::mojom::AudioFocusRequestStatePtr;
using media_session::mojom::MediaSessionInfo;
using media_session::mojom::MediaSessionInfoPtr;
using testing::_;
using testing::AtLeast;
namespace {
class MockMediaNotificationServiceObserver
: public MediaNotificationServiceObserver {
public:
MockMediaNotificationServiceObserver() = default;
MockMediaNotificationServiceObserver(
const MockMediaNotificationServiceObserver&) = delete;
MockMediaNotificationServiceObserver& operator=(
const MockMediaNotificationServiceObserver&) = delete;
~MockMediaNotificationServiceObserver() override = default;
// MediaNotificationServiceObserver implementation.
MOCK_METHOD0(OnNotificationListChanged, void());
MOCK_METHOD0(OnMediaDialogOpenedOrClosed, void());
};
class MockMediaDialogDelegate : public MediaDialogDelegate {
public:
MockMediaDialogDelegate() = default;
~MockMediaDialogDelegate() override { Close(); }
void Open(MediaNotificationService* service) {
ASSERT_NE(nullptr, service);
service_ = service;
service_->SetDialogDelegate(this);
}
void Close() {
if (!service_)
return;
service_->SetDialogDelegate(nullptr);
service_ = nullptr;
}
// MediaDialogDelegate implementation.
MOCK_METHOD2(
ShowMediaSession,
MediaNotificationContainerImpl*(
const std::string& id,
base::WeakPtr<media_message_center::MediaNotificationItem> item));
MOCK_METHOD1(HideMediaSession, void(const std::string& id));
private:
MediaNotificationService* service_;
DISALLOW_COPY_AND_ASSIGN(MockMediaDialogDelegate);
};
} // anonymous namespace
class MediaNotificationServiceTest : public testing::Test {
public:
MediaNotificationServiceTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME,
base::test::TaskEnvironment::MainThreadType::UI) {}
~MediaNotificationServiceTest() override = default;
void SetUp() override {
media_router::MediaRouterFactory::GetInstance()->SetTestingFactory(
&profile_, base::BindRepeating(&media_router::MockMediaRouter::Create));
service_ = std::make_unique<MediaNotificationService>(&profile_, nullptr);
service_->AddObserver(&observer_);
}
void TearDown() override { service_.reset(); }
protected:
void AdvanceClockMilliseconds(int milliseconds) {
task_environment_.FastForwardBy(
base::TimeDelta::FromMilliseconds(milliseconds));
}
base::UnguessableToken SimulatePlayingControllableMedia() {
base::UnguessableToken id = base::UnguessableToken::Create();
SimulateFocusGained(id, true);
SimulateNecessaryMetadata(id);
return id;
}
AudioFocusRequestStatePtr CreateFocusRequest(const base::UnguessableToken& id,
bool controllable) {
MediaSessionInfoPtr session_info(MediaSessionInfo::New());
session_info->is_controllable = controllable;
AudioFocusRequestStatePtr focus(AudioFocusRequestState::New());
focus->request_id = id;
focus->session_info = std::move(session_info);
return focus;
}
void SimulateFocusGained(const base::UnguessableToken& id,
bool controllable) {
service_->OnFocusGained(CreateFocusRequest(id, controllable));
}
void SimulateFocusLost(const base::UnguessableToken& id) {
AudioFocusRequestStatePtr focus(AudioFocusRequestState::New());
focus->request_id = id;
service_->OnFocusLost(std::move(focus));
}
void SimulateNecessaryMetadata(const base::UnguessableToken& id) {
// In order for the MediaNotificationItem to tell the
// MediaNotificationService to show a media session, that session needs
// a title and artist. Typically this would happen through the media session
// service, but since the service doesn't run for this test, we'll manually
// grab the MediaNotificationItem from the MediaNotificationService and
// set the metadata.
auto item_itr = service_->sessions_.find(id.ToString());
ASSERT_NE(service_->sessions_.end(), item_itr);
media_session::MediaMetadata metadata;
metadata.title = base::ASCIIToUTF16("title");
metadata.artist = base::ASCIIToUTF16("artist");
item_itr->second.item()->MediaSessionMetadataChanged(std::move(metadata));
}
void SimulateHasArtwork(const base::UnguessableToken& id) {
auto item_itr = service_->sessions_.find(id.ToString());
ASSERT_NE(service_->sessions_.end(), item_itr);
SkBitmap image;
image.allocN32Pixels(10, 10);
image.eraseColor(SK_ColorMAGENTA);
item_itr->second.item()->MediaControllerImageChanged(
media_session::mojom::MediaSessionImageType::kArtwork, image);
}
void SimulateHasNoArtwork(const base::UnguessableToken& id) {
auto item_itr = service_->sessions_.find(id.ToString());
ASSERT_NE(service_->sessions_.end(), item_itr);
item_itr->second.item()->MediaControllerImageChanged(
media_session::mojom::MediaSessionImageType::kArtwork, SkBitmap());
}
void SimulateReceivedAudioFocusRequests(
std::vector<AudioFocusRequestStatePtr> requests) {
service_->OnReceivedAudioFocusRequests(std::move(requests));
}
bool IsSessionFrozen(const base::UnguessableToken& id) const {
auto item_itr = service_->sessions_.find(id.ToString());
EXPECT_NE(service_->sessions_.end(), item_itr);
return item_itr->second.item()->frozen();
}
bool HasActiveNotifications() const {
return service_->HasActiveNotifications();
}
bool HasFrozenNotifications() const {
return service_->HasFrozenNotifications();
}
bool HasOpenDialog() const { return service_->HasOpenDialog(); }
void SimulateDialogOpened(MockMediaDialogDelegate* delegate) {
delegate->Open(service_.get());
}
void SimulateTabClosed(const base::UnguessableToken& id) {
// When a tab is closing, audio focus will be lost before the WebContents is
// destroyed, so to simulate closer to reality we will also simulate audio
// focus lost here.
SimulateFocusLost(id);
// Now, close the tab.
auto item_itr = service_->sessions_.find(id.ToString());
EXPECT_NE(service_->sessions_.end(), item_itr);
item_itr->second.WebContentsDestroyed();
}
void SimulateDismissButtonClicked(const base::UnguessableToken& id) {
service_->OnContainerDismissed(id.ToString());
}
void ExpectHistogramCountRecorded(int count, int size) {
histogram_tester_.ExpectBucketCount(
media_message_center::kCountHistogramName, count, size);
}
void SimulateMediaRoutesUpdate(
const std::vector<media_router::MediaRoute>& routes) {
service_->cast_notification_provider_->OnRoutesUpdated(routes, {});
}
MockMediaNotificationServiceObserver& observer() { return observer_; }
private:
content::BrowserTaskEnvironment task_environment_;
MockMediaNotificationServiceObserver observer_;
std::unique_ptr<MediaNotificationService> service_;
base::HistogramTester histogram_tester_;
TestingProfile profile_;
};
// TODO(takumif): Remove this class once |kGlobalMediaControlsForCast| is
// enabled by default.
class MediaNotificationServiceCastTest : public MediaNotificationServiceTest {
public:
void SetUp() override {
feature_list_.InitAndEnableFeature(media::kGlobalMediaControlsForCast);
MediaNotificationServiceTest::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(MediaNotificationServiceTest, ShowControllableOnGainAndHideOnLoss) {
// Simulate a new active, controllable media session.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
EXPECT_FALSE(HasActiveNotifications());
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_FALSE(IsSessionFrozen(id));
EXPECT_TRUE(HasActiveNotifications());
// Ensure that the observer was notified of the new notification.
testing::Mock::VerifyAndClearExpectations(&observer());
// Simulate opening a MediaDialogView.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
EXPECT_CALL(observer(), OnMediaDialogOpenedOrClosed());
EXPECT_FALSE(HasOpenDialog());
SimulateDialogOpened(&dialog_delegate);
// Ensure that the session was shown.
ExpectHistogramCountRecorded(1, 1);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Ensure that the observer was notified of the dialog opening.
EXPECT_TRUE(HasOpenDialog());
testing::Mock::VerifyAndClearExpectations(&observer());
// Simulate the active session ending.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString())).Times(0);
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
EXPECT_FALSE(HasFrozenNotifications());
SimulateFocusLost(id);
// Ensure that the session was frozen and not hidden.
EXPECT_TRUE(IsSessionFrozen(id));
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Ensure that the observer was notification of the frozen notification.
EXPECT_TRUE(HasFrozenNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
// Once the freeze timer fires, we should hide the media session.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString()));
AdvanceClockMilliseconds(2500);
testing::Mock::VerifyAndClearExpectations(&observer());
}
TEST_F(MediaNotificationServiceTest, DoesNotShowUncontrollableSession) {
base::UnguessableToken id = base::UnguessableToken::Create();
EXPECT_FALSE(HasActiveNotifications());
SimulateFocusGained(id, false);
SimulateNecessaryMetadata(id);
EXPECT_FALSE(HasActiveNotifications());
}
TEST_F(MediaNotificationServiceTest, ShowsAllInitialControllableSessions) {
base::UnguessableToken controllable1_id = base::UnguessableToken::Create();
base::UnguessableToken uncontrollable_id = base::UnguessableToken::Create();
base::UnguessableToken controllable2_id = base::UnguessableToken::Create();
std::vector<AudioFocusRequestStatePtr> requests;
requests.push_back(CreateFocusRequest(controllable1_id, true));
requests.push_back(CreateFocusRequest(uncontrollable_id, false));
requests.push_back(CreateFocusRequest(controllable2_id, true));
EXPECT_FALSE(HasActiveNotifications());
// Having controllable sessions should count as active.
SimulateReceivedAudioFocusRequests(std::move(requests));
SimulateNecessaryMetadata(controllable1_id);
SimulateNecessaryMetadata(uncontrollable_id);
SimulateNecessaryMetadata(controllable2_id);
EXPECT_TRUE(HasActiveNotifications());
// If we open a dialog, it should be told to show the controllable sessions,
// but not the uncontrollable one.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate,
ShowMediaSession(controllable1_id.ToString(), _));
EXPECT_CALL(dialog_delegate,
ShowMediaSession(uncontrollable_id.ToString(), _))
.Times(0);
EXPECT_CALL(dialog_delegate,
ShowMediaSession(controllable2_id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Ensure that we properly recorded the number of active sessions shown.
ExpectHistogramCountRecorded(2, 1);
}
TEST_F(MediaNotificationServiceTest, HideAfterTimeoutAndActiveAgainOnPlay) {
// First, start an active session.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, stop playing media so the session is frozen, but not yet hidden.
SimulateFocusLost(id);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_TRUE(HasFrozenNotifications());
// If the time hasn't elapsed yet, the session should still be frozen.
AdvanceClockMilliseconds(2400);
EXPECT_TRUE(HasFrozenNotifications());
// Once the time is elapsed, the session should be hidden.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
AdvanceClockMilliseconds(200);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_FALSE(HasFrozenNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
// If media starts playing again, we should show and enable the button.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
}
TEST_F(MediaNotificationServiceTest,
BecomesActiveIfMediaStartsPlayingWithinTimeout) {
// First, start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, stop playing media so the session is frozen, but hasn't been hidden
// yet.
SimulateFocusLost(id);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_TRUE(HasFrozenNotifications());
// If the time hasn't elapsed yet, we should still not be hidden.
AdvanceClockMilliseconds(2400);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_TRUE(HasFrozenNotifications());
// If media starts playing again, we should become active again.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
EXPECT_TRUE(HasFrozenNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
}
TEST_F(MediaNotificationServiceTest, NewMediaSessionWhileDialogOpen) {
// First, start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
ExpectHistogramCountRecorded(1, 1);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Then, have a new media session start while the dialog is opened. This
// should update the dialog.
base::UnguessableToken new_id = base::UnguessableToken::Create();
EXPECT_CALL(dialog_delegate, ShowMediaSession(new_id.ToString(), _));
SimulateFocusGained(new_id, true);
SimulateNecessaryMetadata(new_id);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// If we close this dialog and open a new one, the new one should receive both
// media sessions immediately.
dialog_delegate.Close();
MockMediaDialogDelegate new_dialog;
EXPECT_CALL(new_dialog, ShowMediaSession(id.ToString(), _));
EXPECT_CALL(new_dialog, ShowMediaSession(new_id.ToString(), _));
SimulateDialogOpened(&new_dialog);
ExpectHistogramCountRecorded(1, 1);
ExpectHistogramCountRecorded(2, 1);
}
TEST_F(MediaNotificationServiceTest,
SessionIsRemovedImmediatelyWhenATabCloses) {
// Start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, close the tab. The session should immediately be hidden.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
SimulateTabClosed(id);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_FALSE(HasFrozenNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
}
TEST_F(MediaNotificationServiceTest, DismissesMediaSession) {
// First, start playing active media.
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_TRUE(HasActiveNotifications());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Then, click the dismiss button. This should stop and hide the session.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString()));
SimulateDismissButtonClicked(id);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
}
TEST_F(MediaNotificationServiceCastTest, CountCastSessionsAsActive) {
media_router::MediaRoute media_route("id",
media_router::MediaSource("source_id"),
"sink_id", "description", true, true);
media_route.set_controller_type(media_router::RouteControllerType::kGeneric);
EXPECT_CALL(observer(), OnNotificationListChanged());
EXPECT_FALSE(HasActiveNotifications());
SimulateMediaRoutesUpdate({media_route});
EXPECT_TRUE(HasActiveNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
EXPECT_CALL(observer(), OnNotificationListChanged());
SimulateMediaRoutesUpdate({});
EXPECT_FALSE(HasActiveNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
}
// Regression test for https://crbug.com/1015903: we could end up in a situation
// where the toolbar icon was disabled indefinitely.
TEST_F(MediaNotificationServiceTest, LoseGainLoseDoesNotCauseRaceCondition) {
// First, start an active session and include artwork.
base::UnguessableToken id = SimulatePlayingControllableMedia();
SimulateHasArtwork(id);
EXPECT_TRUE(HasActiveNotifications());
// Then, stop playing media so the session is frozen, but hasn't been hidden
// yet.
SimulateFocusLost(id);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_TRUE(HasFrozenNotifications());
// Simulate no artwork, so we wait for new artwork.
SimulateHasNoArtwork(id);
// Simulate regaining focus, but no artwork yet so we wait.
SimulateFocusGained(id, true);
SimulateNecessaryMetadata(id);
EXPECT_TRUE(HasActiveNotifications());
EXPECT_FALSE(HasFrozenNotifications());
// Then, lose focus again before getting artwork.
SimulateFocusLost(id);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_TRUE(HasFrozenNotifications());
// When the freeze timer fires, we should be hidden.
EXPECT_CALL(observer(), OnNotificationListChanged()).Times(AtLeast(1));
AdvanceClockMilliseconds(2600);
EXPECT_FALSE(HasActiveNotifications());
EXPECT_FALSE(HasFrozenNotifications());
testing::Mock::VerifyAndClearExpectations(&observer());
}
......@@ -4,294 +4,32 @@
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller.h"
#include "base/metrics/histogram_functions.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/browser_list.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_service.h"
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller_delegate.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 "content/public/browser/media_session.h"
#include "media/base/media_switches.h"
#include "services/media_session/public/mojom/constants.mojom.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "services/service_manager/public/cpp/connector.h"
namespace {
// Here we check to see if the WebContents is focused. Note that since Session
// is a WebContentsObserver, we could in theory listen for
// |OnWebContentsFocused()| and |OnWebContentsLostFocus()|. However, this won't
// actually work since focusing the MediaDialogView causes the WebContents to
// "lose focus", so we'd never be focused.
bool IsWebContentsFocused(content::WebContents* web_contents) {
DCHECK(web_contents);
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return false;
// If the given WebContents is not in the focused window, then it's not
// focused. Note that we know a Browser is focused because otherwise the user
// could not interact with the MediaDialogView.
if (BrowserList::GetInstance()->GetLastActive() != browser)
return false;
return browser->tab_strip_model()->GetActiveWebContents() == web_contents;
}
} // anonymous namespace
MediaToolbarButtonController::Session::Session(
MediaToolbarButtonController* owner,
const std::string& id,
std::unique_ptr<media_message_center::MediaSessionNotificationItem> item,
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
owner_(owner),
id_(id),
item_(std::move(item)) {
DCHECK(owner_);
DCHECK(item_);
}
MediaToolbarButtonController::Session::~Session() = default;
void MediaToolbarButtonController::Session::WebContentsDestroyed() {
// If the WebContents is destroyed, then we should just remove the item
// instead of freezing it.
owner_->RemoveItem(id_);
}
MediaToolbarButtonController::MediaToolbarButtonController(
const base::UnguessableToken& source_id,
service_manager::Connector* connector,
MediaToolbarButtonControllerDelegate* delegate,
Profile* profile)
: connector_(connector), delegate_(delegate) {
MediaNotificationService* service)
: delegate_(delegate), service_(service) {
DCHECK(delegate_);
if (base::FeatureList::IsEnabled(media::kGlobalMediaControlsForCast) &&
media_router::MediaRouterEnabled(profile)) {
cast_notification_provider_ =
std::make_unique<CastMediaNotificationProvider>(
profile, this,
base::BindRepeating(
&MediaToolbarButtonController::UpdateToolbarButtonState,
base::Unretained(this)));
}
// |connector| can be null in tests.
if (!connector_)
return;
// Connect to the controller manager so we can create media controllers for
// media sessions.
connector_->Connect(media_session::mojom::kServiceName,
controller_manager_remote_.BindNewPipeAndPassReceiver());
// Connect to receive audio focus events.
connector_->Connect(media_session::mojom::kServiceName,
audio_focus_remote_.BindNewPipeAndPassReceiver());
audio_focus_remote_->AddSourceObserver(
source_id, audio_focus_observer_receiver_.BindNewPipeAndPassRemote());
audio_focus_remote_->GetSourceFocusRequests(
source_id,
base::BindOnce(
&MediaToolbarButtonController::OnReceivedAudioFocusRequests,
weak_ptr_factory_.GetWeakPtr()));
}
MediaToolbarButtonController::~MediaToolbarButtonController() {
for (auto container_pair : observed_containers_)
container_pair.second->RemoveObserver(this);
}
void MediaToolbarButtonController::OnFocusGained(
media_session::mojom::AudioFocusRequestStatePtr session) {
const std::string id = session->request_id->ToString();
// If we have an existing unfrozen item then this is a duplicate call and
// we should ignore it.
auto it = sessions_.find(id);
if (it != sessions_.end() && !it->second.item()->frozen())
return;
mojo::Remote<media_session::mojom::MediaController> controller;
// |controller_manager_remote_| may be null in tests where connector is
// unavailable.
if (controller_manager_remote_) {
controller_manager_remote_->CreateMediaControllerForSession(
controller.BindNewPipeAndPassReceiver(), *session->request_id);
}
if (it != sessions_.end()) {
// If the notification was previously frozen then we should reset the
// controller because the mojo pipe would have been reset.
it->second.item()->SetController(std::move(controller),
std::move(session->session_info));
active_controllable_session_ids_.insert(id);
frozen_session_ids_.erase(id);
UpdateToolbarButtonState();
} else {
sessions_.emplace(
std::piecewise_construct, std::forward_as_tuple(id),
std::forward_as_tuple(
this, id,
std::make_unique<
media_message_center::MediaSessionNotificationItem>(
this, id, session->source_name.value_or(std::string()),
std::move(controller), std::move(session->session_info)),
content::MediaSession::GetWebContentsFromRequestId(
*session->request_id)));
}
}
void MediaToolbarButtonController::OnFocusLost(
media_session::mojom::AudioFocusRequestStatePtr session) {
const std::string id = session->request_id->ToString();
auto it = sessions_.find(id);
if (it == sessions_.end())
return;
it->second.item()->Freeze();
active_controllable_session_ids_.erase(id);
frozen_session_ids_.insert(id);
UpdateToolbarButtonState();
}
void MediaToolbarButtonController::ShowNotification(const std::string& id) {
active_controllable_session_ids_.insert(id);
UpdateToolbarButtonState();
if (!dialog_delegate_)
return;
base::WeakPtr<media_message_center::MediaNotificationItem> item =
GetNotificationItem(id);
MediaNotificationContainerImpl* container =
dialog_delegate_->ShowMediaSession(id, item);
// Observe the container for dismissal.
if (container) {
container->AddObserver(this);
observed_containers_[id] = container;
}
}
void MediaToolbarButtonController::HideNotification(const std::string& id) {
active_controllable_session_ids_.erase(id);
frozen_session_ids_.erase(id);
service_->AddObserver(this);
UpdateToolbarButtonState();
if (!dialog_delegate_)
return;
dialog_delegate_->HideMediaSession(id);
}
scoped_refptr<base::SequencedTaskRunner>
MediaToolbarButtonController::GetTaskRunner() const {
return nullptr;
MediaToolbarButtonController::~MediaToolbarButtonController() {
service_->RemoveObserver(this);
}
void MediaToolbarButtonController::RemoveItem(const std::string& id) {
active_controllable_session_ids_.erase(id);
frozen_session_ids_.erase(id);
sessions_.erase(id);
void MediaToolbarButtonController::OnNotificationListChanged() {
UpdateToolbarButtonState();
}
void MediaToolbarButtonController::LogMediaSessionActionButtonPressed(
const std::string& id) {
auto it = sessions_.find(id);
if (it == sessions_.end())
return;
content::WebContents* web_contents = it->second.web_contents();
if (!web_contents)
return;
base::UmaHistogramBoolean("Media.GlobalMediaControls.UserActionFocus",
IsWebContentsFocused(web_contents));
}
void MediaToolbarButtonController::OnContainerClicked(const std::string& id) {
auto it = sessions_.find(id);
if (it == sessions_.end())
return;
content::WebContents* web_contents = it->second.web_contents();
if (!web_contents)
return;
content::WebContentsDelegate* delegate = web_contents->GetDelegate();
if (!delegate)
return;
delegate->ActivateContents(web_contents);
}
void MediaToolbarButtonController::OnContainerDismissed(const std::string& id) {
auto it = sessions_.find(id);
if (it != sessions_.end())
it->second.item()->Dismiss();
}
void MediaToolbarButtonController::OnContainerDestroyed(const std::string& id) {
auto iter = observed_containers_.find(id);
DCHECK(iter != observed_containers_.end());
iter->second->RemoveObserver(this);
observed_containers_.erase(iter);
}
void MediaToolbarButtonController::SetDialogDelegate(
MediaDialogDelegate* delegate) {
DCHECK(!delegate || !dialog_delegate_);
dialog_delegate_ = delegate;
void MediaToolbarButtonController::OnMediaDialogOpenedOrClosed() {
UpdateToolbarButtonState();
if (!dialog_delegate_)
return;
for (const std::string& id : active_controllable_session_ids_) {
base::WeakPtr<media_message_center::MediaNotificationItem> item =
GetNotificationItem(id);
MediaNotificationContainerImpl* container =
dialog_delegate_->ShowMediaSession(id, item);
// Observe the container for dismissal.
if (container) {
container->AddObserver(this);
observed_containers_[id] = container;
}
}
media_message_center::RecordConcurrentNotificationCount(
active_controllable_session_ids_.size());
}
void MediaToolbarButtonController::OnReceivedAudioFocusRequests(
std::vector<media_session::mojom::AudioFocusRequestStatePtr> sessions) {
for (auto& session : sessions)
OnFocusGained(std::move(session));
}
void MediaToolbarButtonController::UpdateToolbarButtonState() {
if (!active_controllable_session_ids_.empty() ||
(cast_notification_provider_ &&
cast_notification_provider_->HasItems())) {
if (service_->HasActiveNotifications()) {
if (delegate_display_state_ != DisplayState::kShown) {
delegate_->Enable();
delegate_->Show();
......@@ -300,27 +38,16 @@ void MediaToolbarButtonController::UpdateToolbarButtonState() {
return;
}
if (frozen_session_ids_.empty()) {
if (!service_->HasFrozenNotifications()) {
if (delegate_display_state_ != DisplayState::kHidden)
delegate_->Hide();
delegate_display_state_ = DisplayState::kHidden;
return;
}
if (!dialog_delegate_) {
if (!service_->HasOpenDialog()) {
if (delegate_display_state_ != DisplayState::kDisabled)
delegate_->Disable();
delegate_display_state_ = DisplayState::kDisabled;
}
}
base::WeakPtr<media_message_center::MediaNotificationItem>
MediaToolbarButtonController::GetNotificationItem(const std::string& id) {
auto it = sessions_.find(id);
if (it != sessions_.end()) {
return it->second.item()->GetWeakPtr();
} else if (cast_notification_provider_) {
return cast_notification_provider_->GetNotificationItem(id);
}
return nullptr;
}
......@@ -5,77 +5,27 @@
#ifndef CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_TOOLBAR_BUTTON_CONTROLLER_H_
#define CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_TOOLBAR_BUTTON_CONTROLLER_H_
#include <map>
#include <string>
#include <vector>
#include "chrome/browser/ui/global_media_controls/media_notification_service_observer.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/media/router/media_routes_observer.h"
#include "chrome/browser/ui/global_media_controls/cast_media_notification_provider.h"
#include "chrome/browser/ui/global_media_controls/media_notification_container_observer.h"
#include "components/media_message_center/media_notification_controller.h"
#include "content/public/browser/web_contents_observer.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/media_session/public/mojom/audio_focus.mojom.h"
#include "services/media_session/public/mojom/media_controller.mojom.h"
namespace content {
class WebContents;
} // namespace content
namespace media_message_center {
class MediaSessionNotificationItem;
} // namespace media_message_center
namespace service_manager {
class Connector;
} // namespace service_manager
class MediaDialogDelegate;
class MediaNotificationContainerImpl;
class MediaNotificationService;
class MediaToolbarButtonControllerDelegate;
// Controller for the MediaToolbarButtonView that decides when to show or hide
// the icon from the toolbar. Also passes MediaNotificationItems to the
// MediaDialogView to display.
class MediaToolbarButtonController
: public media_session::mojom::AudioFocusObserver,
public media_message_center::MediaNotificationController,
public MediaNotificationContainerObserver {
// the icon from the toolbar.
class MediaToolbarButtonController : public MediaNotificationServiceObserver {
public:
MediaToolbarButtonController(const base::UnguessableToken& source_id,
service_manager::Connector* connector,
MediaToolbarButtonControllerDelegate* delegate,
Profile* profile);
MediaToolbarButtonController(MediaToolbarButtonControllerDelegate* delegate,
MediaNotificationService* service);
MediaToolbarButtonController(const MediaToolbarButtonController&) = delete;
MediaToolbarButtonController& operator=(const MediaToolbarButtonController&) =
delete;
~MediaToolbarButtonController() override;
// media_session::mojom::AudioFocusObserver implementation.
void OnFocusGained(
media_session::mojom::AudioFocusRequestStatePtr session) override;
void OnFocusLost(
media_session::mojom::AudioFocusRequestStatePtr session) override;
// media_message_center::MediaNotificationController implementation.
void ShowNotification(const std::string& id) override;
void HideNotification(const std::string& id) override;
void RemoveItem(const std::string& id) override;
scoped_refptr<base::SequencedTaskRunner> GetTaskRunner() const override;
void LogMediaSessionActionButtonPressed(const std::string& id) override;
// MediaNotificationContainerObserver implementation.
void OnContainerExpanded(bool expanded) override {}
void OnContainerMetadataChanged() override {}
void OnContainerClicked(const std::string& id) override;
void OnContainerDismissed(const std::string& id) override;
void OnContainerDestroyed(const std::string& id) override;
void SetDialogDelegate(MediaDialogDelegate* delegate);
// MediaNotificationServiceObserver implementation.
void OnNotificationListChanged() override;
void OnMediaDialogOpenedOrClosed() override;
private:
friend class MediaToolbarButtonControllerTest;
// Tracks the current display state of the toolbar button delegate.
enum class DisplayState {
kShown,
......@@ -83,74 +33,13 @@ class MediaToolbarButtonController
kHidden,
};
class Session : public content::WebContentsObserver {
public:
Session(MediaToolbarButtonController* owner,
const std::string& id,
std::unique_ptr<media_message_center::MediaSessionNotificationItem>
item,
content::WebContents* web_contents);
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
~Session() override;
// content::WebContentsObserver implementation.
void WebContentsDestroyed() override;
media_message_center::MediaSessionNotificationItem* item() {
return item_.get();
}
private:
MediaToolbarButtonController* owner_;
const std::string id_;
std::unique_ptr<media_message_center::MediaSessionNotificationItem> item_;
};
void OnReceivedAudioFocusRequests(
std::vector<media_session::mojom::AudioFocusRequestStatePtr> sessions);
void UpdateToolbarButtonState();
base::WeakPtr<media_message_center::MediaNotificationItem>
GetNotificationItem(const std::string& id);
service_manager::Connector* const connector_;
MediaToolbarButtonControllerDelegate* const delegate_;
MediaDialogDelegate* dialog_delegate_ = nullptr;
MediaNotificationService* const service_;
// The delegate starts hidden and isn't shown until media playback starts.
DisplayState delegate_display_state_ = DisplayState::kHidden;
// Used to track whether there are any active controllable media sessions. If
// not, then there's nothing to show in the dialog and we can hide the toolbar
// icon.
std::unordered_set<std::string> active_controllable_session_ids_;
// Tracks the sessions that are currently frozen. If there are only frozen
// sessions, we will disable the toolbar icon and wait to hide it.
std::unordered_set<std::string> frozen_session_ids_;
// Stores a Session for each media session keyed by its |request_id| in string
// format.
std::map<std::string, Session> sessions_;
// A map of all containers we're currently observing.
std::map<std::string, MediaNotificationContainerImpl*> observed_containers_;
// Connections with the media session service to listen for audio focus
// updates and control media sessions.
mojo::Remote<media_session::mojom::AudioFocusManager> audio_focus_remote_;
mojo::Remote<media_session::mojom::MediaControllerManager>
controller_manager_remote_;
mojo::Receiver<media_session::mojom::AudioFocusObserver>
audio_focus_observer_receiver_{this};
std::unique_ptr<CastMediaNotificationProvider> cast_notification_provider_;
base::WeakPtrFactory<MediaToolbarButtonController> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(MediaToolbarButtonController);
};
#endif // CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_MEDIA_TOOLBAR_BUTTON_CONTROLLER_H_
......@@ -7,13 +7,9 @@
#include <memory>
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "chrome/browser/media/router/media_router_factory.h"
#include "chrome/browser/media/router/test/mock_media_router.h"
#include "chrome/browser/ui/global_media_controls/cast_media_notification_provider.h"
#include "chrome/browser/ui/global_media_controls/media_dialog_delegate.h"
#include "chrome/browser/ui/global_media_controls/media_notification_service.h"
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller_delegate.h"
#include "chrome/test/base/testing_profile.h"
#include "components/media_message_center/media_notification_item.h"
......@@ -52,18 +48,18 @@ class MockMediaDialogDelegate : public MediaDialogDelegate {
MockMediaDialogDelegate() = default;
~MockMediaDialogDelegate() override { Close(); }
void Open(MediaToolbarButtonController* controller) {
ASSERT_NE(nullptr, controller);
controller_ = controller;
controller_->SetDialogDelegate(this);
void Open(MediaNotificationService* service) {
ASSERT_NE(nullptr, service);
service_ = service;
service_->SetDialogDelegate(this);
}
void Close() {
if (!controller_)
if (!service_)
return;
controller_->SetDialogDelegate(nullptr);
controller_ = nullptr;
service_->SetDialogDelegate(nullptr);
service_ = nullptr;
}
// MediaDialogDelegate implementation.
......@@ -75,7 +71,7 @@ class MockMediaDialogDelegate : public MediaDialogDelegate {
MOCK_METHOD1(HideMediaSession, void(const std::string& id));
private:
MediaToolbarButtonController* controller_;
MediaNotificationService* service_;
DISALLOW_COPY_AND_ASSIGN(MockMediaDialogDelegate);
};
......@@ -86,14 +82,13 @@ class MediaToolbarButtonControllerTest : public testing::Test {
public:
MediaToolbarButtonControllerTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME,
base::test::TaskEnvironment::MainThreadType::UI) {}
base::test::TaskEnvironment::MainThreadType::UI),
service_(&profile_, nullptr) {}
~MediaToolbarButtonControllerTest() override = default;
void SetUp() override {
media_router::MediaRouterFactory::GetInstance()->SetTestingFactory(
&profile_, base::BindRepeating(&media_router::MockMediaRouter::Create));
controller_ = std::make_unique<MediaToolbarButtonController>(
base::UnguessableToken::Create(), nullptr, &delegate_, &profile_);
controller_ =
std::make_unique<MediaToolbarButtonController>(&delegate_, &service_);
}
void TearDown() override { controller_.reset(); }
......@@ -124,24 +119,24 @@ class MediaToolbarButtonControllerTest : public testing::Test {
void SimulateFocusGained(const base::UnguessableToken& id,
bool controllable) {
controller_->OnFocusGained(CreateFocusRequest(id, controllable));
service_.OnFocusGained(CreateFocusRequest(id, controllable));
}
void SimulateFocusLost(const base::UnguessableToken& id) {
AudioFocusRequestStatePtr focus(AudioFocusRequestState::New());
focus->request_id = id;
controller_->OnFocusLost(std::move(focus));
service_.OnFocusLost(std::move(focus));
}
void SimulateNecessaryMetadata(const base::UnguessableToken& id) {
// In order for the MediaNotificationItem to tell the
// MediaToolbarButtonController to show a media session, that session needs
// MediaNotificationService to show a media session, that session needs
// a title and artist. Typically this would happen through the media session
// service, but since the service doesn't run for this test, we'll manually
// grab the MediaNotificationItem from the MediaToolbarButtonController and
// grab the MediaNotificationItem from the MediaNotificationService and
// set the metadata.
auto item_itr = controller_->sessions_.find(id.ToString());
ASSERT_NE(controller_->sessions_.end(), item_itr);
auto item_itr = service_.sessions_.find(id.ToString());
ASSERT_NE(service_.sessions_.end(), item_itr);
media_session::MediaMetadata metadata;
metadata.title = base::ASCIIToUTF16("title");
......@@ -149,65 +144,8 @@ class MediaToolbarButtonControllerTest : public testing::Test {
item_itr->second.item()->MediaSessionMetadataChanged(std::move(metadata));
}
void SimulateHasArtwork(const base::UnguessableToken& id) {
auto item_itr = controller_->sessions_.find(id.ToString());
ASSERT_NE(controller_->sessions_.end(), item_itr);
SkBitmap image;
image.allocN32Pixels(10, 10);
image.eraseColor(SK_ColorMAGENTA);
item_itr->second.item()->MediaControllerImageChanged(
media_session::mojom::MediaSessionImageType::kArtwork, image);
}
void SimulateHasNoArtwork(const base::UnguessableToken& id) {
auto item_itr = controller_->sessions_.find(id.ToString());
ASSERT_NE(controller_->sessions_.end(), item_itr);
item_itr->second.item()->MediaControllerImageChanged(
media_session::mojom::MediaSessionImageType::kArtwork, SkBitmap());
}
void SimulateReceivedAudioFocusRequests(
std::vector<AudioFocusRequestStatePtr> requests) {
controller_->OnReceivedAudioFocusRequests(std::move(requests));
}
bool IsSessionFrozen(const base::UnguessableToken& id) const {
auto item_itr = controller_->sessions_.find(id.ToString());
EXPECT_NE(controller_->sessions_.end(), item_itr);
return item_itr->second.item()->frozen();
}
void SimulateDialogOpened(MockMediaDialogDelegate* delegate) {
delegate->Open(controller_.get());
}
void SimulateTabClosed(const base::UnguessableToken& id) {
// When a tab is closing, audio focus will be lost before the WebContents is
// destroyed, so to simulate closer to reality we will also simulate audio
// focus lost here.
SimulateFocusLost(id);
// Now, close the tab.
auto item_itr = controller_->sessions_.find(id.ToString());
EXPECT_NE(controller_->sessions_.end(), item_itr);
item_itr->second.WebContentsDestroyed();
}
void SimulateDismissButtonClicked(const base::UnguessableToken& id) {
controller_->OnContainerDismissed(id.ToString());
}
void ExpectHistogramCountRecorded(int count, int size) {
histogram_tester_.ExpectBucketCount(
media_message_center::kCountHistogramName, count, size);
}
void SimulateMediaRoutesUpdate(
const std::vector<media_router::MediaRoute>& routes) {
controller_->cast_notification_provider_->OnRoutesUpdated(routes, {});
delegate->Open(&service_);
}
MockMediaToolbarButtonControllerDelegate& delegate() { return delegate_; }
......@@ -215,107 +153,15 @@ class MediaToolbarButtonControllerTest : public testing::Test {
private:
content::BrowserTaskEnvironment task_environment_;
MockMediaToolbarButtonControllerDelegate delegate_;
std::unique_ptr<MediaToolbarButtonController> controller_;
base::HistogramTester histogram_tester_;
TestingProfile profile_;
MediaNotificationService service_;
std::unique_ptr<MediaToolbarButtonController> controller_;
DISALLOW_COPY_AND_ASSIGN(MediaToolbarButtonControllerTest);
};
// TODO(takumif): Remove this class once |kGlobalMediaControlsForCast| is
// enabled by default.
class MediaToolbarButtonControllerCastTest
: public MediaToolbarButtonControllerTest {
public:
void SetUp() override {
feature_list_.InitAndEnableFeature(media::kGlobalMediaControlsForCast);
MediaToolbarButtonControllerTest::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(MediaToolbarButtonControllerTest, ShowControllableOnGainAndHideOnLoss) {
// Simulate a new active, controllable media session.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
EXPECT_FALSE(IsSessionFrozen(id));
// Ensure that the toolbar button was shown.
testing::Mock::VerifyAndClearExpectations(&delegate());
// Simulate opening a MediaDialogView.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Ensure that the session was shown.
ExpectHistogramCountRecorded(1, 1);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Simulate the active session ending.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString())).Times(0);
SimulateFocusLost(id);
// Ensure that the session was frozen and not hidden.
EXPECT_TRUE(IsSessionFrozen(id));
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Once the freeze timer fires, we should hide the media session.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString()));
AdvanceClockMilliseconds(2500);
}
TEST_F(MediaToolbarButtonControllerTest, DoesNotShowUncontrollableSession) {
base::UnguessableToken id = base::UnguessableToken::Create();
EXPECT_CALL(delegate(), Show()).Times(0);
SimulateFocusGained(id, false);
SimulateNecessaryMetadata(id);
}
TEST_F(MediaToolbarButtonControllerTest, ShowsAllInitialControllableSessions) {
base::UnguessableToken controllable1_id = base::UnguessableToken::Create();
base::UnguessableToken uncontrollable_id = base::UnguessableToken::Create();
base::UnguessableToken controllable2_id = base::UnguessableToken::Create();
std::vector<AudioFocusRequestStatePtr> requests;
requests.push_back(CreateFocusRequest(controllable1_id, true));
requests.push_back(CreateFocusRequest(uncontrollable_id, false));
requests.push_back(CreateFocusRequest(controllable2_id, true));
// Having active, controllable sessions should show the toolbar button.
EXPECT_CALL(delegate(), Show());
SimulateReceivedAudioFocusRequests(std::move(requests));
SimulateNecessaryMetadata(controllable1_id);
SimulateNecessaryMetadata(uncontrollable_id);
SimulateNecessaryMetadata(controllable2_id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// If we open a dialog, it should be told to show the controllable sessions,
// but not the uncontrollable one.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate,
ShowMediaSession(controllable1_id.ToString(), _));
EXPECT_CALL(dialog_delegate,
ShowMediaSession(uncontrollable_id.ToString(), _))
.Times(0);
EXPECT_CALL(dialog_delegate,
ShowMediaSession(controllable2_id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Ensure that we properly recorded the number of active sessions shown.
ExpectHistogramCountRecorded(2, 1);
}
TEST_F(MediaToolbarButtonControllerTest, HidesAfterTimeoutAndShowsAgainOnPlay) {
// First, show the button.
// First, show the button by playing media.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
......@@ -388,119 +234,3 @@ TEST_F(MediaToolbarButtonControllerTest,
SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerTest, NewMediaSessionWhileDialogOpen) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
ExpectHistogramCountRecorded(1, 1);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// Then, have a new media session start while the dialog is opened. This
// should update the dialog.
base::UnguessableToken new_id = base::UnguessableToken::Create();
EXPECT_CALL(dialog_delegate, ShowMediaSession(new_id.ToString(), _));
SimulateFocusGained(new_id, true);
SimulateNecessaryMetadata(new_id);
testing::Mock::VerifyAndClearExpectations(&dialog_delegate);
// If we close this dialog and open a new one, the new one should receive both
// media sessions immediately.
dialog_delegate.Close();
MockMediaDialogDelegate new_dialog;
EXPECT_CALL(new_dialog, ShowMediaSession(id.ToString(), _));
EXPECT_CALL(new_dialog, ShowMediaSession(new_id.ToString(), _));
SimulateDialogOpened(&new_dialog);
ExpectHistogramCountRecorded(1, 1);
ExpectHistogramCountRecorded(2, 1);
}
TEST_F(MediaToolbarButtonControllerTest,
SessionIsRemovedImmediatelyWhenATabCloses) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, close the tab. The button should immediately hide.
EXPECT_CALL(delegate(), Hide());
SimulateTabClosed(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerTest, DismissesMediaSession) {
// First, show the button.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, open a dialog.
MockMediaDialogDelegate dialog_delegate;
EXPECT_CALL(dialog_delegate, ShowMediaSession(id.ToString(), _));
SimulateDialogOpened(&dialog_delegate);
// Then, click the dismiss button. This should stop and hide the session.
EXPECT_CALL(dialog_delegate, HideMediaSession(id.ToString()));
SimulateDismissButtonClicked(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
}
TEST_F(MediaToolbarButtonControllerCastTest, ShowButtonForCastSession) {
media_router::MediaRoute media_route("id",
media_router::MediaSource("source_id"),
"sink_id", "description", true, true);
media_route.set_controller_type(media_router::RouteControllerType::kGeneric);
EXPECT_CALL(delegate(), Enable());
EXPECT_CALL(delegate(), Show());
SimulateMediaRoutesUpdate({media_route});
testing::Mock::VerifyAndClearExpectations(&delegate());
EXPECT_CALL(delegate(), Hide());
SimulateMediaRoutesUpdate({});
}
// Regression test for https://crbug.com/1015903: we could end up in a situation
// where the toolbar icon was disabled indefinitely.
TEST_F(MediaToolbarButtonControllerTest,
LoseGainLoseDoesNotCauseRaceCondition) {
// First, show the button, and include artwork.
EXPECT_CALL(delegate(), Show());
base::UnguessableToken id = SimulatePlayingControllableMedia();
SimulateHasArtwork(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, stop playing media so the button is disabled, but hasn't been hidden
// yet.
EXPECT_CALL(delegate(), Disable());
EXPECT_CALL(delegate(), Hide()).Times(0);
SimulateFocusLost(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// Simulate no artwork, so we wait for new artwork.
SimulateHasNoArtwork(id);
// Simulate regaining focus, but no artwork yet so we wait.
EXPECT_CALL(delegate(), Show());
SimulateFocusGained(id, true);
SimulateNecessaryMetadata(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// Then, lose focus again before getting artwork.
EXPECT_CALL(delegate(), Show()).Times(0);
EXPECT_CALL(delegate(), Disable());
EXPECT_CALL(delegate(), Hide()).Times(0);
SimulateFocusLost(id);
testing::Mock::VerifyAndClearExpectations(&delegate());
// When the freeze timer fires, we should be hidden.
EXPECT_CALL(delegate(), Hide());
AdvanceClockMilliseconds(2600);
testing::Mock::VerifyAndClearExpectations(&delegate());
}
......@@ -6,7 +6,7 @@
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller.h"
#include "chrome/browser/ui/global_media_controls/media_notification_service.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/global_media_controls/media_dialog_view_observer.h"
#include "chrome/browser/ui/views/global_media_controls/media_notification_container_impl_view.h"
......@@ -27,11 +27,10 @@ bool MediaDialogView::has_been_opened_ = false;
// static
void MediaDialogView::ShowDialog(views::View* anchor_view,
MediaToolbarButtonController* controller,
service_manager::Connector* connector) {
MediaNotificationService* service) {
DCHECK(!instance_);
DCHECK(controller);
instance_ = new MediaDialogView(anchor_view, controller, connector);
DCHECK(service);
instance_ = new MediaDialogView(anchor_view, service);
views::Widget* widget =
views::BubbleDialogDelegateView::CreateBubble(instance_);
......@@ -45,7 +44,7 @@ void MediaDialogView::ShowDialog(views::View* anchor_view,
// static
void MediaDialogView::HideDialog() {
if (IsShowing()) {
instance_->controller_->SetDialogDelegate(nullptr);
instance_->service_->SetDialogDelegate(nullptr);
instance_->GetWidget()->Close();
}
......@@ -110,7 +109,7 @@ void MediaDialogView::AddedToWidget() {
SetPaintToLayer();
layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(corner_radius));
controller_->SetDialogDelegate(this);
service_->SetDialogDelegate(this);
}
gfx::Size MediaDialogView::CalculatePreferredSize() const {
......@@ -155,13 +154,12 @@ MediaDialogView::GetNotificationsForTesting() const {
}
MediaDialogView::MediaDialogView(views::View* anchor_view,
MediaToolbarButtonController* controller,
service_manager::Connector* connector)
MediaNotificationService* service)
: BubbleDialogDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
controller_(controller),
service_(service),
active_sessions_view_(
AddChildView(std::make_unique<MediaNotificationListView>())) {
DCHECK(controller_);
DCHECK(service_);
}
MediaDialogView::~MediaDialogView() {
......@@ -179,6 +177,6 @@ void MediaDialogView::Init() {
void MediaDialogView::WindowClosing() {
if (instance_ == this) {
instance_ = nullptr;
controller_->SetDialogDelegate(nullptr);
service_->SetDialogDelegate(nullptr);
}
}
......@@ -11,14 +11,10 @@
#include "chrome/browser/ui/global_media_controls/media_notification_container_observer.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
namespace service_manager {
class Connector;
} // namespace service_manager
class MediaDialogViewObserver;
class MediaNotificationContainerImplView;
class MediaNotificationListView;
class MediaToolbarButtonController;
class MediaNotificationService;
// Dialog that shows media controls that control the active media session.
class MediaDialogView : public views::BubbleDialogDelegateView,
......@@ -26,8 +22,7 @@ class MediaDialogView : public views::BubbleDialogDelegateView,
public MediaNotificationContainerObserver {
public:
static void ShowDialog(views::View* anchor_view,
MediaToolbarButtonController* controller,
service_manager::Connector* connector);
MediaNotificationService* service);
static void HideDialog();
static bool IsShowing();
......@@ -62,8 +57,7 @@ class MediaDialogView : public views::BubbleDialogDelegateView,
private:
explicit MediaDialogView(views::View* anchor_view,
MediaToolbarButtonController* controller,
service_manager::Connector* connector);
MediaNotificationService* service);
~MediaDialogView() override;
static MediaDialogView* instance_;
......@@ -75,7 +69,7 @@ class MediaDialogView : public views::BubbleDialogDelegateView,
void Init() override;
void WindowClosing() override;
MediaToolbarButtonController* const controller_;
MediaNotificationService* const service_;
MediaNotificationListView* const active_sessions_view_;
......
......@@ -8,6 +8,7 @@
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/global_media_controls/media_notification_service_factory.h"
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller.h"
#include "chrome/browser/ui/in_product_help/global_media_controls_in_product_help.h"
#include "chrome/browser/ui/in_product_help/global_media_controls_in_product_help_factory.h"
......@@ -25,17 +26,10 @@
#include "ui/views/animation/ink_drop.h"
#include "ui/views/controls/button/button_controller.h"
MediaToolbarButtonView::MediaToolbarButtonView(
const base::UnguessableToken& source_id,
service_manager::Connector* connector,
const Browser* browser)
MediaToolbarButtonView::MediaToolbarButtonView(const Browser* browser)
: ToolbarButton(this),
connector_(connector),
controller_(
std::make_unique<MediaToolbarButtonController>(source_id,
connector_,
this,
browser->profile())),
service_(
MediaNotificationServiceFactory::GetForProfile(browser->profile())),
browser_(browser) {
GlobalMediaControlsInProductHelp* in_product_help =
GlobalMediaControlsInProductHelpFactory::GetForProfile(
......@@ -54,6 +48,10 @@ MediaToolbarButtonView::MediaToolbarButtonView(
// We start hidden and only show once |controller_| tells us to.
SetVisible(false);
// Wait until we're done with everything else before creating |controller_|
// since it can call |Show()| synchronously.
controller_ = std::make_unique<MediaToolbarButtonController>(this, service_);
}
MediaToolbarButtonView::~MediaToolbarButtonView() {
......@@ -77,7 +75,7 @@ void MediaToolbarButtonView::ButtonPressed(views::Button* sender,
if (MediaDialogView::IsShowing()) {
MediaDialogView::HideDialog();
} else {
MediaDialogView::ShowDialog(this, controller_.get(), connector_);
MediaDialogView::ShowDialog(this, service_);
// Inform observers. Since the promo controller cares about the dialog
// showing, we need to ensure that it's created.
......
......@@ -9,16 +9,9 @@
#include "chrome/browser/ui/global_media_controls/media_toolbar_button_controller_delegate.h"
#include "chrome/browser/ui/views/toolbar/toolbar_button.h"
namespace base {
class UnguessableToken;
} // namespace base
namespace service_manager {
class Connector;
} // namespace service_manager
class Browser;
class GlobalMediaControlsPromoController;
class MediaNotificationService;
class MediaToolbarButtonController;
class MediaToolbarButtonObserver;
......@@ -29,9 +22,7 @@ class MediaToolbarButtonView : public ToolbarButton,
public MediaToolbarButtonControllerDelegate,
public views::ButtonListener {
public:
MediaToolbarButtonView(const base::UnguessableToken& source_id,
service_manager::Connector* connector,
const Browser* browser);
explicit MediaToolbarButtonView(const Browser* browser);
~MediaToolbarButtonView() override;
void AddObserver(MediaToolbarButtonObserver* observer);
......@@ -81,7 +72,7 @@ class MediaToolbarButtonView : public ToolbarButton,
// True if the in-product help bubble is currently showing.
bool is_promo_showing_ = false;
service_manager::Connector* const connector_;
MediaNotificationService* const service_;
std::unique_ptr<MediaToolbarButtonController> controller_;
const Browser* const browser_;
......
......@@ -66,9 +66,7 @@
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/system_connector.h"
#include "content/public/browser/web_contents.h"
#include "media/base/media_switches.h"
#include "ui/accessibility/ax_node_data.h"
......@@ -213,10 +211,7 @@ void ToolbarView::Init() {
std::unique_ptr<MediaToolbarButtonView> media_button;
if (base::FeatureList::IsEnabled(media::kGlobalMediaControls)) {
const base::UnguessableToken& source_id =
content::MediaSession::GetSourceId(browser_->profile());
media_button = std::make_unique<MediaToolbarButtonView>(
source_id, content::GetSystemConnector(), browser_);
media_button = std::make_unique<MediaToolbarButtonView>(browser_);
}
std::unique_ptr<ToolbarAccountIconContainerView>
......
......@@ -3849,6 +3849,7 @@ test("unit_tests") {
"../browser/ui/extensions/extension_message_bubble_bridge_unittest.cc",
"../browser/ui/global_error/global_error_service_unittest.cc",
"../browser/ui/global_media_controls/cast_media_notification_provider_unittest.cc",
"../browser/ui/global_media_controls/media_notification_service_unittest.cc",
"../browser/ui/global_media_controls/media_toolbar_button_controller_unittest.cc",
"../browser/ui/hid/hid_chooser_controller_unittest.cc",
"../browser/ui/in_product_help/active_tab_tracker_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment