Commit 7a07736b authored by Takumi Fujimoto's avatar Takumi Fujimoto Committed by Commit Bot

[Harmony Cast Dialog] Add local files as a source

Add local files as one of the options in the sources dropdown. When it
is selected, a file picker dialog is opened via MediaRouterViewsUI. After
selecting a file, the user can select a sink to start casting.

The dialog has a functionality to close itself on blur, which will be
disabled while a file picker is open, and when the file is opened in a
new tab, to prevent the dialog from closing at those moments.

Bug: 842787
Change-Id: I3e9ebf0b1958c026f418111feb792a9d94fce533
Reviewed-on: https://chromium-review.googlesource.com/c/1299657
Commit-Queue: Takumi Fujimoto <takumif@chromium.org>
Reviewed-by: default avatarmark a. foltz <mfoltz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#607337}
parent 86dea917
......@@ -5,10 +5,15 @@
#ifndef CHROME_BROWSER_UI_MEDIA_ROUTER_CAST_DIALOG_CONTROLLER_H_
#define CHROME_BROWSER_UI_MEDIA_ROUTER_CAST_DIALOG_CONTROLLER_H_
#include "base/callback_forward.h"
#include "chrome/browser/ui/media_router/media_cast_mode.h"
#include "chrome/common/media_router/media_route.h"
#include "chrome/common/media_router/media_sink.h"
namespace ui {
struct SelectedFileInfo;
} // namespace ui
namespace media_router {
class CastDialogModel;
......@@ -42,6 +47,11 @@ class CastDialogController {
// Stops casting by terminating the route given by |route_id|. No-op if the ID
// is invalid.
virtual void StopCasting(const MediaRoute::Id& route_id) = 0;
// Prompts the user to select a local file to cast. The callback is called
// with the info for the selected file, or nullptr if the user declined.
virtual void ChooseLocalFile(
base::OnceCallback<void(const ui::SelectedFileInfo*)> callback) = 0;
};
} // namespace media_router
......
......@@ -6,8 +6,10 @@
#include "base/location.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/media/router/media_router_metrics.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/media_router/cast_dialog_controller.h"
......@@ -47,10 +49,6 @@ namespace {
// This value is negative so that it doesn't overlap with a sink index.
constexpr int kAlternativeSourceButtonId = -1;
// In the sources menu, we have a single item for "tab", which includes both
// presenting and mirroring a tab.
constexpr int kTabSource = PRESENTATION | TAB_MIRROR;
} // namespace
// static
......@@ -99,12 +97,20 @@ bool CastDialogView::ShouldShowCloseButton() const {
}
base::string16 CastDialogView::GetWindowTitle() const {
switch (selected_source_) {
case SourceType::kTab:
return dialog_title_;
case SourceType::kDesktop:
// |dialog_title_| may contain the presentation URL origin which is not
// relevant for non-tab sources. So we override it with the default title for
// those sources.
return selected_source_ == kTabSource
? dialog_title_
: l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_CAST_DIALOG_TITLE);
// relevant for the desktop source, so we use the default title string.
return l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_CAST_DIALOG_TITLE);
case SourceType::kLocalFile:
return l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_CAST_LOCAL_MEDIA_TITLE,
local_file_name_.value());
default:
NOTREACHED();
return base::string16();
}
}
int CastDialogView::GetDialogButtons() const {
......@@ -190,10 +196,18 @@ bool CastDialogView::IsCommandIdEnabled(int command_id) const {
}
void CastDialogView::ExecuteCommand(int command_id, int event_flags) {
selected_source_ = command_id;
DisableUnsupportedSinks();
GetWidget()->UpdateWindowTitle();
metrics_.OnCastModeSelected();
// This method is called when the user selects a source in the source picker.
if (command_id == SourceType::kLocalFile) {
// When the file picker dialog opens, the Cast dialog loses focus. So we
// must temporarily prevent it from closing when losing focus.
set_close_on_deactivate(false);
controller_->ChooseLocalFile(base::BindOnce(
&CastDialogView::OnFilePickerClosed, weak_factory_.GetWeakPtr()));
} else {
if (local_file_name_)
local_file_name_.reset();
SelectSource(static_cast<SourceType>(command_id));
}
}
// static
......@@ -217,7 +231,7 @@ CastDialogView::CastDialogView(views::View* anchor_view,
Browser* browser,
const base::Time& start_time)
: BubbleDialogDelegateView(anchor_view, anchor_position),
selected_source_(kTabSource),
selected_source_(SourceType::kTab),
controller_(controller),
browser_(browser),
metrics_(start_time),
......@@ -322,9 +336,11 @@ void CastDialogView::ShowSourcesMenu() {
sources_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
sources_menu_model_->AddCheckItemWithStringId(
kTabSource, IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE);
SourceType::kTab, IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE);
sources_menu_model_->AddCheckItemWithStringId(
DESKTOP_MIRROR, IDS_MEDIA_ROUTER_DESKTOP_MIRROR_CAST_MODE);
SourceType::kDesktop, IDS_MEDIA_ROUTER_DESKTOP_MIRROR_CAST_MODE);
sources_menu_model_->AddCheckItemWithStringId(
SourceType::kLocalFile, IDS_MEDIA_ROUTER_LOCAL_FILE_CAST_MODE);
}
sources_menu_runner_ = std::make_unique<views::MenuRunner>(
......@@ -335,6 +351,13 @@ void CastDialogView::ShowSourcesMenu() {
ui::MENU_SOURCE_MOUSE);
}
void CastDialogView::SelectSource(SourceType source) {
selected_source_ = source;
DisableUnsupportedSinks();
GetWidget()->UpdateWindowTitle();
metrics_.OnCastModeSelected();
}
void CastDialogView::SinkPressed(size_t index) {
if (!controller_)
return;
......@@ -344,7 +367,16 @@ void CastDialogView::SinkPressed(size_t index) {
if (sink.route_id.empty()) {
base::Optional<MediaCastMode> cast_mode = GetCastModeToUse(sink);
if (cast_mode) {
// Starting local file casting may open a new tab synchronously on the UI
// thread, which deactivates the dialog. So we must prevent it from
// closing and getting destroyed.
if (cast_mode == LOCAL_FILE)
set_close_on_deactivate(false);
controller_->StartCasting(sink.id, cast_mode.value());
// Re-enable close on deactivate so the user can click elsewhere to close
// the dialog.
if (cast_mode == LOCAL_FILE)
set_close_on_deactivate(true);
metrics_.OnStartCasting(base::Time::Now(), index);
}
} else {
......@@ -363,11 +395,21 @@ base::Optional<MediaCastMode> CastDialogView::GetCastModeToUse(
const UIMediaSink& sink) const {
// Go through cast modes in the order of preference to find one that is
// supported and selected.
for (MediaCastMode cast_mode : {PRESENTATION, TAB_MIRROR, DESKTOP_MIRROR}) {
if ((cast_mode & selected_source_) &&
base::ContainsKey(sink.cast_modes, cast_mode)) {
return cast_mode;
}
switch (selected_source_) {
case SourceType::kTab:
if (base::ContainsKey(sink.cast_modes, PRESENTATION))
return base::make_optional<MediaCastMode>(PRESENTATION);
if (base::ContainsKey(sink.cast_modes, TAB_MIRROR))
return base::make_optional<MediaCastMode>(TAB_MIRROR);
break;
case SourceType::kDesktop:
if (base::ContainsKey(sink.cast_modes, DESKTOP_MIRROR))
return base::make_optional<MediaCastMode>(DESKTOP_MIRROR);
break;
case SourceType::kLocalFile:
if (base::ContainsKey(sink.cast_modes, LOCAL_FILE))
return base::make_optional<MediaCastMode>(LOCAL_FILE);
break;
}
return base::nullopt;
}
......@@ -393,6 +435,19 @@ void CastDialogView::RecordSinkCount() {
metrics_.OnRecordSinkCount(sink_buttons_.size());
}
void CastDialogView::OnFilePickerClosed(const ui::SelectedFileInfo* file_info) {
// Re-enable the setting to close the dialog when it loses focus.
set_close_on_deactivate(true);
if (file_info) {
#if defined(OS_WIN)
local_file_name_ = file_info->display_name;
#else
local_file_name_ = base::UTF8ToUTF16(file_info->display_name);
#endif // defined(OS_WIN)
SelectSource(SourceType::kLocalFile);
}
}
// static
CastDialogView* CastDialogView::instance_ = nullptr;
......
......@@ -12,6 +12,7 @@
#include "chrome/browser/ui/media_router/cast_dialog_controller.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_metrics.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/shell_dialogs/selected_file_info.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/button.h"
......@@ -101,7 +102,13 @@ class CastDialogView : public views::BubbleDialogDelegateView,
private:
friend class CastDialogViewTest;
FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, CancelLocalFileSelection);
FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, CastLocalFile);
FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, DisableUnsupportedSinks);
FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, ShowAndHideDialog);
FRIEND_TEST_ALL_PREFIXES(CastDialogViewTest, ShowSourcesMenu);
enum SourceType { kTab, kDesktop, kLocalFile };
// Instantiates and shows the singleton dialog. The dialog must not be
// currently shown.
......@@ -134,6 +141,10 @@ class CastDialogView : public views::BubbleDialogDelegateView,
// Shows the sources menu that allows the user to choose a source to cast.
void ShowSourcesMenu();
// Stores |source| as the source to be used when user selects a sink to start
// casting, and updates the UI to reflect the selection.
void SelectSource(SourceType source);
void SinkPressed(size_t index);
void MaybeSizeToContents();
......@@ -153,6 +164,9 @@ class CastDialogView : public views::BubbleDialogDelegateView,
// Records the number of sinks shown with the metrics recorder.
void RecordSinkCount();
// Sets local file as the selected source if |file_info| is not null.
void OnFilePickerClosed(const ui::SelectedFileInfo* file_info);
// The singleton dialog instance. This is a nullptr when a dialog is not
// shown.
static CastDialogView* instance_;
......@@ -163,7 +177,7 @@ class CastDialogView : public views::BubbleDialogDelegateView,
// The source selected in the sources menu. This defaults to "tab"
// (presentation or tab mirroring). "Tab" is represented by a single item in
// the sources menu.
int selected_source_;
SourceType selected_source_ = SourceType::kTab;
// Contains references to sink buttons in the order they appear.
std::vector<CastDialogSinkButton*> sink_buttons_;
......@@ -195,6 +209,9 @@ class CastDialogView : public views::BubbleDialogDelegateView,
// multiple sinks at the same time, the last activated sink is used.
base::Optional<size_t> selected_sink_index_;
// This value is set if the user has chosen a local file to cast.
base::Optional<base::string16> local_file_name_;
base::WeakPtrFactory<CastDialogView> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(CastDialogView);
......
......@@ -76,6 +76,9 @@ class MockCastDialogController : public CastDialogController {
MOCK_METHOD2(StartCasting,
void(const std::string& sink_id, MediaCastMode cast_mode));
MOCK_METHOD1(StopCasting, void(const std::string& route_id));
MOCK_METHOD1(
ChooseLocalFile,
void(base::OnceCallback<void(const ui::SelectedFileInfo*)> callback));
};
class CastDialogViewTest : public ChromeViewsTestBase {
......@@ -205,11 +208,13 @@ TEST_F(CastDialogViewTest, ShowSourcesMenu) {
InitializeDialogWithModel(model);
// Press the button to show the sources menu.
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
// The items should be "tab" (includes tab mirroring and presentation) and
// "desktop".
EXPECT_EQ(2, sources_menu_model()->GetItemCount());
EXPECT_EQ(PRESENTATION | TAB_MIRROR, sources_menu_model()->GetCommandIdAt(0));
EXPECT_EQ(DESKTOP_MIRROR, sources_menu_model()->GetCommandIdAt(1));
// The items should be "tab" (includes tab mirroring and presentation),
// "desktop", and "local file".
EXPECT_EQ(3, sources_menu_model()->GetItemCount());
EXPECT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
EXPECT_EQ(CastDialogView::kDesktop, sources_menu_model()->GetCommandIdAt(1));
EXPECT_EQ(CastDialogView::kLocalFile,
sources_menu_model()->GetCommandIdAt(2));
// When there are no sinks, the sources button should be disabled.
model.set_media_sinks({});
......@@ -217,14 +222,15 @@ TEST_F(CastDialogViewTest, ShowSourcesMenu) {
EXPECT_FALSE(sources_button()->enabled());
}
TEST_F(CastDialogViewTest, CastToAlternativeSources) {
TEST_F(CastDialogViewTest, CastAlternativeSources) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, DESKTOP_MIRROR};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
// Press the button to show the sources menu.
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
ASSERT_EQ(2, sources_menu_model()->GetItemCount());
// There should be three sources: tab, desktop, and local file.
ASSERT_EQ(3, sources_menu_model()->GetItemCount());
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, TAB_MIRROR));
sources_menu_model()->ActivatedAt(0);
......@@ -237,6 +243,67 @@ TEST_F(CastDialogViewTest, CastToAlternativeSources) {
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, CastLocalFile) {
const std::string file_name = "example.mp4";
const std::string file_path = "path/to/" + file_name;
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, LOCAL_FILE};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
#if defined(OS_WIN)
ui::SelectedFileInfo file_info{base::FilePath(base::UTF8ToUTF16(file_name)),
base::FilePath(base::UTF8ToUTF16(file_path))};
#else
ui::SelectedFileInfo file_info{base::FilePath(file_name),
base::FilePath(file_path)};
#endif // defined(OS_WIN)
EXPECT_CALL(controller_, ChooseLocalFile(_))
.WillOnce(
[file_info](base::OnceCallback<void(const ui::SelectedFileInfo*)>
file_callback) {
std::move(file_callback).Run(&file_info);
});
ASSERT_EQ(CastDialogView::kLocalFile,
sources_menu_model()->GetCommandIdAt(2));
sources_menu_model()->ActivatedAt(2);
EXPECT_EQ(dialog_->GetWindowTitle(),
l10n_util::GetStringFUTF16(IDS_MEDIA_ROUTER_CAST_LOCAL_MEDIA_TITLE,
base::UTF8ToUTF16(file_name)));
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, LOCAL_FILE));
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, CancelLocalFileSelection) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink()};
media_sinks[0].cast_modes = {TAB_MIRROR, LOCAL_FILE};
CastDialogModel model = CreateModelWithSinks(std::move(media_sinks));
InitializeDialogWithModel(model);
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
// The tab source should be selected by default.
ASSERT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
ASSERT_TRUE(sources_menu_model()->IsItemCheckedAt(0));
// Select the local file source, then cancel file selection by passing a
// nullptr into the callback.
EXPECT_CALL(controller_, ChooseLocalFile(_))
.WillOnce(
[](base::OnceCallback<void(const ui::SelectedFileInfo*)>
file_callback) { std::move(file_callback).Run(nullptr); });
ASSERT_EQ(CastDialogView::kLocalFile,
sources_menu_model()->GetCommandIdAt(2));
sources_menu_model()->ActivatedAt(2);
// Since we cancelled file selection, "tab" should still be the selected
// source.
EXPECT_TRUE(sources_menu_model()->IsItemCheckedAt(0));
EXPECT_CALL(controller_, StartCasting(model.media_sinks()[0].id, TAB_MIRROR));
SinkPressedAtIndex(0);
}
TEST_F(CastDialogViewTest, DisableUnsupportedSinks) {
std::vector<UIMediaSink> media_sinks = {CreateAvailableSink(),
CreateAvailableSink()};
......@@ -247,7 +314,7 @@ TEST_F(CastDialogViewTest, DisableUnsupportedSinks) {
InitializeDialogWithModel(model);
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
EXPECT_EQ(DESKTOP_MIRROR, sources_menu_model()->GetCommandIdAt(1));
EXPECT_EQ(CastDialogView::kDesktop, sources_menu_model()->GetCommandIdAt(1));
sources_menu_model()->ActivatedAt(1);
// Sink at index 0 doesn't support desktop mirroring, so it should be
// disabled.
......@@ -255,7 +322,7 @@ TEST_F(CastDialogViewTest, DisableUnsupportedSinks) {
EXPECT_TRUE(sink_buttons().at(1)->enabled());
dialog_->ButtonPressed(sources_button(), CreateMouseEvent());
EXPECT_EQ(PRESENTATION | TAB_MIRROR, sources_menu_model()->GetCommandIdAt(0));
EXPECT_EQ(CastDialogView::kTab, sources_menu_model()->GetCommandIdAt(0));
sources_menu_model()->ActivatedAt(0);
// Both sinks support tab or presentation casting, so they should be enabled.
EXPECT_TRUE(sink_buttons().at(0)->enabled());
......
......@@ -60,13 +60,8 @@ void MediaRouterViewsUI::RemoveObserver(
void MediaRouterViewsUI::StartCasting(const std::string& sink_id,
MediaCastMode cast_mode) {
if (cast_mode == LOCAL_FILE) {
local_file_sink_id_ = sink_id;
OpenFileDialog();
} else {
CreateRoute(sink_id, cast_mode);
UpdateSinks();
}
}
void MediaRouterViewsUI::StopCasting(const std::string& route_id) {
......@@ -77,6 +72,12 @@ void MediaRouterViewsUI::StopCasting(const std::string& route_id) {
TerminateRoute(terminating_route_id_.value());
}
void MediaRouterViewsUI::ChooseLocalFile(
base::OnceCallback<void(const ui::SelectedFileInfo*)> callback) {
file_selection_callback_ = std::move(callback);
OpenFileDialog();
}
std::vector<MediaSinkWithCastModes> MediaRouterViewsUI::GetEnabledSinks()
const {
std::vector<MediaSinkWithCastModes> sinks =
......@@ -196,13 +197,17 @@ void MediaRouterViewsUI::UpdateModelHeader() {
void MediaRouterViewsUI::FileDialogFileSelected(
const ui::SelectedFileInfo& file_info) {
CreateRoute(local_file_sink_id_.value(), LOCAL_FILE);
local_file_sink_id_.reset();
std::move(file_selection_callback_).Run(&file_info);
}
void MediaRouterViewsUI::FileDialogSelectionFailed(const IssueInfo& issue) {
MediaRouterUIBase::FileDialogSelectionFailed(issue);
local_file_sink_id_.reset();
std::move(file_selection_callback_).Run(nullptr);
}
void MediaRouterViewsUI::FileDialogSelectionCanceled() {
MediaRouterUIBase::FileDialogSelectionCanceled();
std::move(file_selection_callback_).Run(nullptr);
}
} // namespace media_router
......@@ -26,6 +26,8 @@ class MediaRouterViewsUI : public MediaRouterUIBase,
void StartCasting(const std::string& sink_id,
MediaCastMode cast_mode) override;
void StopCasting(const std::string& route_id) override;
void ChooseLocalFile(
base::OnceCallback<void(const ui::SelectedFileInfo*)> callback) override;
// MediaRouterUIBase:
std::vector<MediaSinkWithCastModes> GetEnabledSinks() const override;
......@@ -62,6 +64,7 @@ class MediaRouterViewsUI : public MediaRouterUIBase,
// MediaRouterFileDialogDelegate:
void FileDialogFileSelected(const ui::SelectedFileInfo& file_info) override;
void FileDialogSelectionFailed(const IssueInfo& issue) override;
void FileDialogSelectionCanceled() override;
// This value is set whenever there is an outstanding issue.
base::Optional<Issue> issue_;
......@@ -81,6 +84,9 @@ class MediaRouterViewsUI : public MediaRouterUIBase,
// TODO(takumif): CastDialogModel should manage the observers.
base::ObserverList<CastDialogController::Observer>::Unchecked observers_;
base::OnceCallback<void(const ui::SelectedFileInfo*)>
file_selection_callback_;
DISALLOW_COPY_AND_ASSIGN(MediaRouterViewsUI);
};
......
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