Commit 470bb679 authored by Muyao Xu's avatar Muyao Xu Committed by Commit Bot

[Cast+Zenith] Start cast session from the GMC dialog

Users can start a cast session from the GMC dialog by clicking on the
cast sinks in the device selector list.
After a new session is started, the local media notification will be
hidden, and the cast media notification will show up in the dialog.

Bug: b/161610050, 1107162
Change-Id: Ia3ad0634f0a45159ed92581ca2c6f097566c1a72
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2422737Reviewed-by: default avatarTommy Steimel <steimel@chromium.org>
Reviewed-by: default avatarTakumi Fujimoto <takumif@chromium.org>
Commit-Queue: Muyao Xu <muyaoxu@google.com>
Cr-Commit-Position: refs/heads/master@{#812353}
parent 8f26b75c
......@@ -156,7 +156,6 @@ CastMediaNotificationItem::CastMediaNotificationItem(
base::Unretained(this))),
session_info_(CreateSessionInfo()) {
metadata_.source_title = GetSourceTitle(route);
notification_controller_->ShowNotification(media_route_id_);
base::UmaHistogramEnumeration(
kSourceHistogramName, route.is_local() ? Source::kLocalCastSession
: Source::kNonLocalCastSession);
......
......@@ -101,7 +101,6 @@ class MockSessionController : public CastMediaSessionController {
class CastMediaNotificationItemTest : public testing::Test {
public:
void SetUp() override {
EXPECT_CALL(notification_controller_, ShowNotification(kRouteId));
auto session_controller = std::make_unique<MockSessionController>(
mojo::Remote<media_router::mojom::MediaController>());
session_controller_ = session_controller.get();
......
......@@ -87,6 +87,7 @@ void CastMediaNotificationProvider::OnRoutesUpdated(
router_->GetMediaController(
route.media_route_id(), std::move(controller_receiver),
it_pair.first->second.GetObserverPendingRemote());
notification_controller_->ShowNotification(route.media_route_id());
} else {
item_it->second.OnRouteUpdated(route);
}
......
......@@ -352,6 +352,11 @@ void MediaRouterUI::InitWithDefaultMediaSource() {
}
}
void MediaRouterUI::InitWithDefaultMediaSourceAndMirroring() {
InitWithDefaultMediaSource();
InitMirroring();
}
void MediaRouterUI::InitWithStartPresentationContext(
std::unique_ptr<StartPresentationContext> context) {
DCHECK(context);
......@@ -365,6 +370,12 @@ void MediaRouterUI::InitWithStartPresentationContext(
&start_presentation_context_->presentation_request());
}
void MediaRouterUI::InitWithStartPresentationContextAndMirroring(
std::unique_ptr<StartPresentationContext> context) {
InitWithStartPresentationContext(std::move(context));
InitMirroring();
}
bool MediaRouterUI::CreateRoute(const MediaSink::Id& sink_id,
MediaCastMode cast_mode) {
logger_->LogInfo(mojom::LogCategory::kUi, kLoggerComponent,
......@@ -607,6 +618,18 @@ void MediaRouterUI::InitCommon() {
std::make_unique<QueryResultManager>(GetMediaRouter());
query_result_manager_->AddObserver(this);
// Get the current list of media routes, so that the WebUI will have routes
// information at initialization.
OnRoutesUpdated(GetMediaRouter()->GetCurrentRoutes(),
std::vector<MediaRoute::Id>());
display_observer_ = WebContentsDisplayObserver::Create(
initiator_,
base::BindRepeating(&MediaRouterUI::UpdateSinks, base::Unretained(this)));
StartObservingIssues();
}
void MediaRouterUI::InitMirroring() {
// Use a placeholder URL as origin for mirroring.
url::Origin origin = url::Origin::Create(GURL());
......@@ -626,16 +649,6 @@ void MediaRouterUI::InitCommon() {
query_result_manager_->SetSourcesForCastMode(MediaCastMode::TAB_MIRROR,
{mirroring_source}, origin);
}
// Get the current list of media routes, so that the WebUI will have routes
// information at initialization.
OnRoutesUpdated(GetMediaRouter()->GetCurrentRoutes(),
std::vector<MediaRoute::Id>());
display_observer_ = WebContentsDisplayObserver::Create(
initiator_,
base::BindRepeating(&MediaRouterUI::UpdateSinks, base::Unretained(this)));
StartObservingIssues();
}
void MediaRouterUI::OnDefaultPresentationChanged(
......@@ -965,6 +978,7 @@ UIMediaSink MediaRouterUI::ConvertToUISink(const MediaSinkWithCastModes& sink,
ui_sink.id = sink.sink.id();
ui_sink.friendly_name = GetSinkFriendlyName(sink.sink);
ui_sink.icon_type = sink.sink.icon_type();
ui_sink.cast_modes = sink.cast_modes;
if (route) {
ui_sink.status_text = base::UTF8ToUTF16(route->description());
......@@ -978,7 +992,6 @@ UIMediaSink MediaRouterUI::ConvertToUISink(const MediaSinkWithCastModes& sink,
sink.sink.id() == current_route_request()->sink_id
? UIMediaSinkState::CONNECTING
: UIMediaSinkState::AVAILABLE;
ui_sink.cast_modes = sink.cast_modes;
}
if (ui_sink.icon_type == SinkIconType::HANGOUT &&
ui_sink.state == UIMediaSinkState::AVAILABLE && sink.sink.domain()) {
......
......@@ -71,23 +71,27 @@ class MediaRouterUI
void ClearIssue(const Issue::Id& issue_id) override;
// Initializes internal state (e.g. starts listening for MediaSinks) for
// targeting the default MediaSource (if any) of |initiator_|, as well as
// mirroring sources of that tab. The contents of the UI will change as the
// default MediaSource changes. If there is a default MediaSource, then
// PRESENTATION MediaCastMode will be added to |cast_modes_|. Init* methods
// can only be called once.
// targeting the default MediaSource (if any) of |initiator_|. The contents of
// the UI will change as the default MediaSource changes. If there is a
// default MediaSource, then PRESENTATION MediaCastMode will be added to
// |cast_modes_|. Init* methods can only be called once.
void InitWithDefaultMediaSource();
// Initializes mirroring sources of the tab in addition to what is done by
// |InitWithDefaultMediaSource()|.
void InitWithDefaultMediaSourceAndMirroring();
// Initializes internal state targeting the presentation specified in
// |context|. Also sets up mirroring sources based on |initiator_|.
// This is different from InitWithDefaultMediaSource() in that it does not
// listen for default media source changes, as the UI is fixed to the source
// in |context|.
// Init* methods can only be called once.
// |context|. This is different from InitWithDefaultMediaSource*() in that it
// does not listen for default media source changes, as the UI is fixed to the
// source in |context|. Init* methods can only be called once.
// |context|: Context object for the PresentationRequest. This instance will
// take ownership of it. Must not be null.
// take ownership of it. Must not be null.
void InitWithStartPresentationContext(
std::unique_ptr<StartPresentationContext> context);
// Initializes mirroring sources of the tab in addition to what is done by
// |InitWithStartPresentationContext()|.
void InitWithStartPresentationContextAndMirroring(
std::unique_ptr<StartPresentationContext> context);
// Requests a route be created from the source mapped to
// |cast_mode|, to the sink given by |sink_id|.
......@@ -220,6 +224,7 @@ class MediaRouterUI
// Initializes the dialog with mirroring sources derived from |initiator_|.
virtual void InitCommon();
void InitMirroring();
// WebContentsPresentationManager::Observer
void OnDefaultPresentationChanged(
......@@ -376,8 +381,8 @@ class MediaRouterUI
// This contains a value only when tracking a pending route request.
base::Optional<RouteRequest> current_route_request_;
// Used for locale-aware sorting of sinks by name. Set during InitCommon()
// using the current locale.
// Used for locale-aware sorting of sinks by name. Set during
// InitCommon() using the current locale.
std::unique_ptr<icu::Collator> collator_;
std::vector<MediaSinkWithCastModes> sinks_;
......
......@@ -155,7 +155,7 @@ class MediaRouterViewsUITest : public ChromeRenderViewHostTestHarness {
CreateSessionServiceTabHelper(web_contents());
ui_ = std::make_unique<MediaRouterUI>(web_contents());
ui_->InitWithDefaultMediaSource();
ui_->InitWithDefaultMediaSourceAndMirroring();
}
void TearDown() override {
......@@ -175,7 +175,7 @@ class MediaRouterViewsUITest : public ChromeRenderViewHostTestHarness {
&web_contents()->GetController());
CreateSessionServiceTabHelper(web_contents());
ui_ = std::make_unique<MediaRouterUI>(web_contents());
ui_->InitWithDefaultMediaSource();
ui_->InitWithDefaultMediaSourceAndMirroring();
}
// These methods are used so that we don't have to friend each test case that
......
......@@ -24,8 +24,10 @@ void ChangeEntryColor(views::ImageView* image_view,
const gfx::VectorIcon* icon,
SkColor foreground_color,
SkColor background_color) {
image_view->SetImage(
gfx::CreateVectorIcon(*icon, kDeviceIconSize, foreground_color));
if (image_view) {
image_view->SetImage(
gfx::CreateVectorIcon(*icon, kDeviceIconSize, foreground_color));
}
title_view->SetDisplayedOnBackgroundColor(background_color);
if (!title_view->GetText().empty()) {
......@@ -122,12 +124,24 @@ CastDeviceEntryView::CastDeviceEntryView(views::ButtonListener* button_listener,
: DeviceEntryUI(sink.id,
base::UTF16ToUTF8(sink.friendly_name),
CastDialogSinkButton::GetVectorIcon(sink.icon_type)),
CastDialogSinkButton(button_listener,
sink,
/* TODO(muyaoxu): button tag */ -1) {
// TODO(muyaoxu): change the sink's style based on its UIMediaSinkState
ChangeEntryColor(static_cast<views::ImageView*>(icon_view()), title(),
subtitle(), icon_, foreground_color, background_color);
CastDialogSinkButton(button_listener, sink) {
switch (sink.state) {
// If the sink state is CONNECTING or DISCONNECTING, a throbber icon will
// show up. The icon's color remains unchanged.
case media_router::UIMediaSinkState::CONNECTING:
case media_router::UIMediaSinkState::DISCONNECTING:
ChangeEntryColor(nullptr, title(), subtitle(), nullptr, foreground_color,
background_color);
break;
case media_router::UIMediaSinkState::CONNECTED:
case media_router::UIMediaSinkState::AVAILABLE:
case media_router::UIMediaSinkState::UNAVAILABLE:
ChangeEntryColor(static_cast<views::ImageView*>(icon_view()), title(),
subtitle(), icon_, foreground_color, background_color);
break;
default:
NOTREACHED();
}
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
SetInkDropMode(Button::InkDropMode::ON);
......
......@@ -230,8 +230,19 @@ void MediaNotificationDeviceSelectorView::ButtonPressed(
ShowDevices();
delegate_->OnDeviceSelectorViewSizeChanged();
} else if (GetDeviceEntryUI(sender)->GetType() == DeviceEntryUIType::kAudio) {
delegate_->OnAudioSinkChosen(GetDeviceEntryUI(sender)->raw_device_id());
} else {
auto* device_entry = GetDeviceEntryUI(sender);
switch (device_entry->GetType()) {
case DeviceEntryUIType::kAudio:
delegate_->OnAudioSinkChosen(device_entry->raw_device_id());
break;
case DeviceEntryUIType::kCast:
StartCastSession(static_cast<CastDeviceEntryView*>(device_entry));
break;
default:
NOTREACHED();
break;
}
}
}
......@@ -296,8 +307,8 @@ void MediaNotificationDeviceSelectorView::UpdateVisibility() {
delegate_->OnDeviceSelectorViewSizeChanged();
}
// TODO(muyaoxu@):Change the logic to work with Cast devices
bool MediaNotificationDeviceSelectorView::ShouldBeVisible() {
// TODO(muyaoxu):Change the logic to work with Cast devices.
bool MediaNotificationDeviceSelectorView::ShouldBeVisible() const {
if (!is_audio_device_switching_enabled_)
return false;
// The UI should be visible if there are more than one unique devices. That is
......@@ -343,7 +354,7 @@ void MediaNotificationDeviceSelectorView::RemoveDevicesOfType(
}
DeviceEntryUI* MediaNotificationDeviceSelectorView::GetDeviceEntryUI(
views::View* view) {
views::View* view) const {
auto it = device_entry_ui_map_.find(static_cast<views::Button*>(view)->tag());
DCHECK(it != device_entry_ui_map_.end());
return it->second;
......@@ -367,3 +378,27 @@ void MediaNotificationDeviceSelectorView::OnModelUpdated(
void MediaNotificationDeviceSelectorView::OnControllerInvalidated() {
cast_controller_.reset();
}
void MediaNotificationDeviceSelectorView::StartCastSession(
CastDeviceEntryView* entry) {
if (!cast_controller_)
return;
const media_router::UIMediaSink& sink = entry->sink();
// Clicking on the device entry with an issue will clear the issue without
// starting casting.
if (sink.issue) {
cast_controller_->ClearIssue(sink.issue->id());
return;
}
// If users try to cast to a connected sink, a new cast session will get
// started and override the existing cast session.
if (sink.state == media_router::UIMediaSinkState::AVAILABLE ||
sink.state == media_router::UIMediaSinkState::CONNECTED) {
DCHECK(base::Contains(sink.cast_modes,
media_router::MediaCastMode::PRESENTATION));
cast_controller_->StartCasting(sink.id,
media_router::MediaCastMode::PRESENTATION);
// TODO(muyaoxu):Add metrics to record start casting usage.
}
}
......@@ -81,15 +81,18 @@ class MediaNotificationDeviceSelectorView
AudioDevicesCountHistogramRecorded);
FRIEND_TEST_ALL_PREFIXES(MediaNotificationDeviceSelectorViewTest,
DeviceSelectorOpenedHistogramRecorded);
FRIEND_TEST_ALL_PREFIXES(MediaNotificationDeviceSelectorViewTest,
CastDeviceButtonClickStartsCasting);
FRIEND_TEST_ALL_PREFIXES(MediaNotificationDeviceSelectorViewTest,
CastDeviceButtonClickClearsIssue);
void UpdateVisibility();
bool ShouldBeVisible();
bool ShouldBeVisible() const;
void ShowDevices();
void HideDevices();
void RemoveDevicesOfType(DeviceEntryUIType type);
DeviceEntryUI* GetDeviceEntryUI(views::View* view);
void StartCastSession(CastDeviceEntryView* entry);
DeviceEntryUI* GetDeviceEntryUI(views::View* view) const;
bool has_expand_button_been_shown_ = false;
bool have_devices_been_shown_ = false;
......
......@@ -23,16 +23,22 @@ using media_router::CastDialogController;
using media_router::CastDialogModel;
using media_router::UIMediaSink;
using media_router::UIMediaSinkState;
using testing::_;
class MediaNotificationContainerObserver;
namespace {
UIMediaSink CreateAvailableSink() {
constexpr char kSinkId[] = "sink_id";
constexpr char kSinkFriendlyName[] = "Nest Hub";
UIMediaSink CreateMediaSink(
UIMediaSinkState state = UIMediaSinkState::AVAILABLE) {
UIMediaSink sink;
sink.friendly_name = base::UTF8ToUTF16("Nest Hub");
sink.id = "sink_available";
sink.state = UIMediaSinkState::AVAILABLE;
sink.friendly_name = base::UTF8ToUTF16(kSinkFriendlyName);
sink.id = kSinkId;
sink.state = state;
sink.cast_modes = {media_router::MediaCastMode::PRESENTATION};
return sink;
}
......@@ -187,7 +193,7 @@ TEST_F(MediaNotificationDeviceSelectorViewTest, DeviceButtonsCreated) {
view_ = std::make_unique<MediaNotificationDeviceSelectorView>(
&delegate, std::make_unique<MockCastDialogController>(), "1",
gfx::kPlaceholderColor, gfx::kPlaceholderColor);
view_->OnModelUpdated(CreateModelWithSinks({CreateAvailableSink()}));
view_->OnModelUpdated(CreateModelWithSinks({CreateMediaSink()}));
ASSERT_TRUE(view_->device_entry_views_container_ != nullptr);
......@@ -197,7 +203,7 @@ TEST_F(MediaNotificationDeviceSelectorViewTest, DeviceButtonsCreated) {
EXPECT_EQ(EntryLabelText(container_children.at(0)), "Speaker");
EXPECT_EQ(EntryLabelText(container_children.at(1)), "Headphones");
EXPECT_EQ(EntryLabelText(container_children.at(2)), "Earbuds");
EXPECT_EQ(EntryLabelText(container_children.at(3)), "Nest Hub");
EXPECT_EQ(EntryLabelText(container_children.at(3)), kSinkFriendlyName);
}
TEST_F(MediaNotificationDeviceSelectorViewTest,
......@@ -233,6 +239,35 @@ TEST_F(MediaNotificationDeviceSelectorViewTest,
}
}
TEST_F(MediaNotificationDeviceSelectorViewTest,
CastDeviceButtonClickStartsCasting) {
MockMediaNotificationDeviceSelectorViewDelegate delegate;
auto cast_controller = std::make_unique<MockCastDialogController>();
auto* cast_controller_ptr = cast_controller.get();
view_ = std::make_unique<MediaNotificationDeviceSelectorView>(
&delegate, std::move(cast_controller), "1", gfx::kPlaceholderColor,
gfx::kPlaceholderColor);
// Clicking on connecting or disconnecting sinks will not start casting.
view_->OnModelUpdated(
CreateModelWithSinks({CreateMediaSink(UIMediaSinkState::CONNECTING),
CreateMediaSink(UIMediaSinkState::DISCONNECTING)}));
EXPECT_CALL(*cast_controller_ptr, StartCasting(_, _)).Times(0);
for (views::View* child : view_->device_entry_views_container_->children()) {
SimulateButtonClick(child);
}
// Clicking on available or connected sinks will start casting.
view_->OnModelUpdated(CreateModelWithSinks(
{CreateMediaSink(), CreateMediaSink(UIMediaSinkState::CONNECTED)}));
EXPECT_CALL(*cast_controller_ptr,
StartCasting(_, media_router::MediaCastMode::PRESENTATION))
.Times(2);
for (views::View* child : view_->device_entry_views_container_->children()) {
SimulateButtonClick(child);
}
}
TEST_F(MediaNotificationDeviceSelectorViewTest, CurrentAudioDeviceHighlighted) {
// The 'current' audio device should be highlighted in the UI and appear
// before other devices.
......@@ -375,6 +410,32 @@ TEST_F(MediaNotificationDeviceSelectorViewTest,
EXPECT_TRUE(view_->GetVisible());
}
TEST_F(MediaNotificationDeviceSelectorViewTest,
CastDeviceButtonClickClearsIssue) {
MockMediaNotificationDeviceSelectorViewDelegate delegate;
auto cast_controller = std::make_unique<MockCastDialogController>();
auto* cast_controller_ptr = cast_controller.get();
view_ = std::make_unique<MediaNotificationDeviceSelectorView>(
&delegate, std::move(cast_controller), "1", gfx::kPlaceholderColor,
gfx::kPlaceholderColor);
// Clicking on sinks with issue will clear up the issue instead of starting a
// cast session.
auto sink = CreateMediaSink();
media_router::IssueInfo issue_info(
"Issue Title", media_router::IssueInfo::Action::DISMISS,
media_router::IssueInfo::Severity::WARNING);
media_router::Issue issue(issue_info);
sink.issue = issue;
view_->OnModelUpdated(CreateModelWithSinks({sink}));
EXPECT_CALL(*cast_controller_ptr, StartCasting(_, _)).Times(0);
EXPECT_CALL(*cast_controller_ptr, ClearIssue(issue.id()));
for (views::View* child : view_->device_entry_views_container_->children()) {
SimulateButtonClick(child);
}
}
TEST_F(MediaNotificationDeviceSelectorViewTest,
AudioDevicesCountHistogramRecorded) {
MockMediaNotificationDeviceSelectorViewDelegate delegate;
......
......@@ -93,19 +93,25 @@ base::string16 GetStatusTextForSink(const UIMediaSink& sink) {
CastDialogSinkButton::CastDialogSinkButton(
views::ButtonListener* button_listener,
const UIMediaSink& sink,
int button_tag)
const UIMediaSink& sink)
: HoverButton(button_listener,
CreatePrimaryIconForSink(sink),
sink.friendly_name,
GetStatusTextForSink(sink),
/** secondary_icon_view */ nullptr),
sink_(sink) {
set_tag(button_tag);
SetEnabled(sink.state == UIMediaSinkState::AVAILABLE ||
sink.state == UIMediaSinkState::CONNECTED);
}
CastDialogSinkButton::CastDialogSinkButton(
views::ButtonListener* button_listener,
const UIMediaSink& sink,
int button_tag)
: CastDialogSinkButton(button_listener, sink) {
set_tag(button_tag);
}
CastDialogSinkButton::~CastDialogSinkButton() = default;
void CastDialogSinkButton::OverrideStatusText(
......
......@@ -19,6 +19,8 @@ namespace media_router {
// hovered.
class CastDialogSinkButton : public HoverButton {
public:
CastDialogSinkButton(views::ButtonListener* button_listener,
const UIMediaSink& sink);
CastDialogSinkButton(views::ButtonListener* button_listener,
const UIMediaSink& sink,
int button_tag);
......
......@@ -157,10 +157,10 @@ void MediaRouterDialogControllerViews::OnServiceDisabled() {
void MediaRouterDialogControllerViews::InitializeMediaRouterUI() {
ui_ = std::make_unique<MediaRouterUI>(initiator());
if (start_presentation_context_) {
ui_->InitWithStartPresentationContext(
ui_->InitWithStartPresentationContextAndMirroring(
std::move(start_presentation_context_));
} else {
ui_->InitWithDefaultMediaSource();
ui_->InitWithDefaultMediaSourceAndMirroring();
}
}
......
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