blob: c7fa31110a3884593068af21fef0a2fbbc613a9c [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/media_router/cast_dialog_view.h"
#include <optional>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser_actions.h"
#include "chrome/browser/ui/media_router/cast_dialog_controller.h"
#include "chrome/browser/ui/media_router/cast_dialog_model.h"
#include "chrome/browser/ui/media_router/media_cast_mode.h"
#include "chrome/browser/ui/media_router/media_route_starter.h"
#include "chrome/browser/ui/media_router/ui_media_sink.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_access_code_cast_button.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_no_sinks_view.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_sink_view.h"
#include "chrome/browser/ui/webui/access_code_cast/access_code_cast_dialog.h"
#include "chrome/grit/generated_resources.h"
#include "components/access_code_cast/common/access_code_cast_metrics.h"
#include "components/media_router/browser/media_router_metrics.h"
#include "components/media_router/common/media_sink.h"
#include "components/media_router/common/mojom/media_route_provider_id.mojom-shared.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view.h"
namespace media_router {
CastDialogView::CastDialogView(
views::View* anchor_view,
views::BubbleBorder::Arrow anchor_position,
CastDialogController* controller,
Profile* profile,
const base::Time& start_time,
MediaRouterDialogActivationLocation activation_location,
actions::ActionItem* action_item)
: BubbleDialogDelegateView(anchor_view, anchor_position),
controller_(controller),
profile_(profile),
metrics_(start_time, activation_location, profile),
action_item_(action_item) {
DCHECK(profile);
SetShowCloseButton(true);
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH));
InitializeSourcesButton();
MaybeShowAccessCodeCastButton();
ShowNoSinksView();
}
CastDialogView::~CastDialogView() {
if (controller_) {
controller_->RemoveObserver(this);
}
if (action_item_) {
action_item_->SetIsShowingBubble(false);
}
}
std::u16string CastDialogView::GetWindowTitle() const {
switch (selected_source_) {
case SourceType::kTab:
return dialog_title_;
case SourceType::kDesktop:
return l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_DESKTOP_MIRROR_CAST_MODE);
default:
NOTREACHED();
}
}
void CastDialogView::OnModelUpdated(const CastDialogModel& model) {
if (base::FeatureList::IsEnabled(kShowCastPermissionRejectedError) &&
model.is_permission_rejected()) {
ShowPermissionRejectedView();
} else if (model.media_sinks().empty()) {
ShowNoSinksView();
} else {
if (scroll_view_) {
scroll_position_ = scroll_view_->GetVisibleRect().y();
} else {
ShowScrollView();
}
PopulateScrollView(model.media_sinks());
RestoreSinkListState();
metrics_.OnSinksLoaded(base::Time::Now());
DisableUnsupportedSinks();
}
// If access code casting is enabled, the sources button needs to be enabled
// so that user can set the source before invoking the access code casting
// flow.
if (sources_button_) {
sources_button_->SetEnabled(!model.media_sinks().empty() ||
IsAccessCodeCastingEnabled());
}
dialog_title_ = model.dialog_header();
MaybeSizeToContents();
// Update the main action button.
DialogModelChanged();
observers_.Notify(&Observer::OnDialogModelUpdated, this);
}
void CastDialogView::OnControllerDestroying() {
controller_ = nullptr;
// We don't destroy the dialog here because if the destruction was caused by
// activating the toolbar icon in order to close the dialog, then it would
// cause the dialog to immediately open again.
}
bool CastDialogView::IsCommandIdChecked(int command_id) const {
return command_id == selected_source_;
}
bool CastDialogView::IsCommandIdEnabled(int command_id) const {
return true;
}
void CastDialogView::ExecuteCommand(int command_id, int event_flags) {
SelectSource(static_cast<SourceType>(command_id));
}
void CastDialogView::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void CastDialogView::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void CastDialogView::KeepShownForTesting() {
keep_shown_for_testing_ = true;
set_close_on_deactivate(false);
}
void CastDialogView::Init() {
auto* provider = ChromeLayoutProvider::Get();
set_margins(gfx::Insets::TLBR(
provider->GetDistanceMetric(
views::DISTANCE_DIALOG_CONTENT_MARGIN_TOP_CONTROL),
0,
provider->GetDistanceMetric(
views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL),
0));
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
controller_->AddObserver(this);
RecordSinkCountWithDelay();
}
void CastDialogView::WindowClosing() {
observers_.Notify(&Observer::OnDialogWillClose, this);
}
void CastDialogView::ShowAccessCodeCastDialog() {
if (!controller_) {
return;
}
CastModeSet cast_mode_set;
switch (selected_source_) {
case SourceType::kTab:
cast_mode_set = {MediaCastMode::PRESENTATION, MediaCastMode::TAB_MIRROR};
break;
case SourceType::kDesktop:
cast_mode_set = {MediaCastMode::DESKTOP_MIRROR};
break;
default:
NOTREACHED();
}
AccessCodeCastDialog::Show(
cast_mode_set, controller_->TakeMediaRouteStarter(),
AccessCodeCastDialogOpenLocation::kBrowserCastMenu);
}
void CastDialogView::MaybeShowAccessCodeCastButton() {
if (!IsAccessCodeCastingEnabled()) {
return;
}
auto callback = base::BindRepeating(&CastDialogView::ShowAccessCodeCastDialog,
base::Unretained(this));
access_code_cast_button_ =
AddChildView(std::make_unique<CastDialogAccessCodeCastButton>(callback));
}
void CastDialogView::ShowNoSinksView() {
if (no_sinks_view_) {
return;
}
ResetViews();
no_sinks_view_ = AddChildView(std::make_unique<CastDialogNoSinksView>(
profile_, /*permission_rejected*/ false));
}
void CastDialogView::ShowPermissionRejectedView() {
if (permission_rejected_view_) {
return;
}
ResetViews();
permission_rejected_view_ =
AddChildView(std::make_unique<CastDialogNoSinksView>(
profile_, /*permission_rejected*/ true));
}
void CastDialogView::ShowScrollView() {
if (scroll_view_) {
return;
}
ResetViews();
scroll_view_ = AddChildView(std::make_unique<views::ScrollView>());
constexpr int kSinkButtonHeight = 56;
scroll_view_->ClipHeightTo(0, kSinkButtonHeight * 6.5);
}
void CastDialogView::ResetViews() {
if (no_sinks_view_) {
auto temp = RemoveChildViewT(no_sinks_view_);
no_sinks_view_ = nullptr;
}
if (permission_rejected_view_) {
auto temp = RemoveChildViewT(permission_rejected_view_);
permission_rejected_view_ = nullptr;
}
if (scroll_view_) {
auto temp = RemoveChildViewT(scroll_view_);
scroll_view_ = nullptr;
sink_views_.clear();
scroll_position_ = 0;
}
}
void CastDialogView::RestoreSinkListState() {
if (selected_sink_index_ &&
selected_sink_index_.value() < sink_views_.size()) {
CastDialogSinkView* sink_view =
sink_views_.at(selected_sink_index_.value());
// Focus on the sink so that the screen reader reads its label, which has
// likely been updated.
sink_view->RequestFocus();
// If the state became AVAILABLE, the screen reader no longer needs to read
// the label until the user selects a sink again.
if (sink_view->sink().state == UIMediaSinkState::AVAILABLE) {
selected_sink_index_.reset();
}
}
views::ScrollBar* scroll_bar =
const_cast<views::ScrollBar*>(scroll_view_->vertical_scroll_bar());
if (scroll_bar) {
scroll_view_->ScrollToPosition(scroll_bar, scroll_position_);
scroll_view_->DeprecatedLayoutImmediately();
}
}
void CastDialogView::PopulateScrollView(const std::vector<UIMediaSink>& sinks) {
sink_views_.clear();
auto sink_list_view = std::make_unique<views::View>();
auto layout_manager = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical);
// Give a little extra space below all sink views, so that focus rings may be
// properly drawn.
layout_manager->set_inside_border_insets(gfx::Insets::TLBR(0, 0, 4, 0));
sink_list_view->SetLayoutManager(std::move(layout_manager));
for (size_t i = 0; i < sinks.size(); i++) {
auto* sink_view =
sink_list_view->AddChildView(std::make_unique<CastDialogSinkView>(
profile_, sinks.at(i),
base::BindRepeating(&CastDialogView::SinkPressed,
base::Unretained(this), i),
base::BindRepeating(&CastDialogView::IssuePressed,
base::Unretained(this), i),
base::BindRepeating(&CastDialogView::StopPressed,
base::Unretained(this), i),
base::BindRepeating(&CastDialogView::FreezePressed,
base::Unretained(this), i)));
sink_views_.push_back(sink_view);
}
scroll_view_->SetContents(std::move(sink_list_view));
MaybeSizeToContents();
DeprecatedLayoutImmediately();
}
void CastDialogView::InitializeSourcesButton() {
sources_button_ =
SetExtraView(std::make_unique<views::MdTextButtonWithDownArrow>(
base::BindRepeating(&CastDialogView::ShowSourcesMenu,
base::Unretained(this)),
l10n_util::GetStringUTF16(
IDS_MEDIA_ROUTER_ALTERNATIVE_SOURCES_BUTTON)));
sources_button_->SetEnabled(false);
sources_button_->SetStyle(ui::ButtonStyle::kTonal);
}
void CastDialogView::ShowSourcesMenu() {
if (!sources_menu_model_) {
sources_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
sources_menu_model_->AddCheckItemWithStringId(
SourceType::kTab, IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE);
sources_menu_model_->AddCheckItemWithStringId(
SourceType::kDesktop, IDS_MEDIA_ROUTER_DESKTOP_MIRROR_CAST_MODE);
}
sources_menu_runner_ = std::make_unique<views::MenuRunner>(
sources_menu_model_.get(), views::MenuRunner::COMBOBOX);
const gfx::Rect& screen_bounds = sources_button_->GetBoundsInScreen();
sources_menu_runner_->RunMenuAt(
sources_button_->GetWidget(), nullptr, screen_bounds,
views::MenuAnchorPosition::kTopLeft, ui::mojom::MenuSourceType::kMouse);
}
void CastDialogView::SelectSource(SourceType source) {
selected_source_ = source;
DisableUnsupportedSinks();
GetWidget()->UpdateWindowTitle();
}
void CastDialogView::SinkPressed(size_t index) {
if (!controller_) {
return;
}
selected_sink_index_ = index;
// sink() may get invalidated during CastDialogController::StartCasting()
// due to a model update, so make a copy here.
const UIMediaSink sink = sink_views_.at(index)->sink();
if (sink.issue) {
controller_->ClearIssue(sink.issue->id());
} else {
std::optional<MediaCastMode> cast_mode = GetCastModeToUse(sink);
if (cast_mode) {
controller_->StartCasting(sink.id, cast_mode.value());
}
}
}
void CastDialogView::IssuePressed(size_t index) {
if (!controller_) {
return;
}
selected_sink_index_ = index;
const UIMediaSink sink = sink_views_.at(index)->sink();
if (sink.issue) {
controller_->ClearIssue(sink.issue->id());
}
}
void CastDialogView::StopPressed(size_t index) {
if (!controller_) {
return;
}
selected_sink_index_ = index;
const UIMediaSink sink = sink_views_.at(index)->sink();
if (!sink.route) {
return;
}
if (sink.issue) {
controller_->ClearIssue(sink.issue->id());
}
// StopCasting() may trigger a model update and invalidate |sink|.
controller_->StopCasting(sink.route->media_route_id());
}
void CastDialogView::FreezePressed(size_t index) {
if (!controller_) {
return;
}
selected_sink_index_ = index;
const UIMediaSink sink = sink_views_.at(index)->sink();
if (!sink.route) {
return;
}
// If the route cannot be frozen / unfrozen, then the freeze button should
// not be shown at all. Even so, return early just in case the callback is
// triggered unexpectedly.
if (!sink.freeze_info.can_freeze) {
return;
}
if (sink.freeze_info.is_frozen) {
controller_->UnfreezeRoute(sink.route->media_route_id());
} else { /* is_frozen == false */
controller_->FreezeRoute(sink.route->media_route_id());
}
}
void CastDialogView::MaybeSizeToContents() {
// The widget may be null if this is called while the dialog is opening.
if (GetWidget()) {
SizeToContents();
}
}
std::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.
switch (selected_source_) {
case SourceType::kTab:
if (base::Contains(sink.cast_modes, PRESENTATION)) {
return std::make_optional<MediaCastMode>(PRESENTATION);
}
if (base::Contains(sink.cast_modes, TAB_MIRROR)) {
return std::make_optional<MediaCastMode>(TAB_MIRROR);
}
break;
case SourceType::kDesktop:
if (base::Contains(sink.cast_modes, DESKTOP_MIRROR)) {
return std::make_optional<MediaCastMode>(DESKTOP_MIRROR);
}
break;
}
return std::nullopt;
}
void CastDialogView::DisableUnsupportedSinks() {
// Go through the AVAILABLE sinks and enable or disable them depending on
// whether they support the selected cast mode.
for (CastDialogSinkView* sink_view : sink_views_) {
if (sink_view->sink().state != UIMediaSinkState::AVAILABLE) {
continue;
}
const bool enable = GetCastModeToUse(sink_view->sink()).has_value();
sink_view->SetEnabledState(enable);
}
}
void CastDialogView::RecordSinkCountWithDelay() {
// Record the number of sinks after three seconds. This is consistent with the
// WebUI dialog.
content::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CastDialogView::RecordSinkCount,
weak_factory_.GetWeakPtr()),
MediaRouterMetrics::kDeviceCountMetricDelay);
}
void CastDialogView::RecordSinkCount() {
metrics_.OnRecordSinkCount(sink_views_);
}
bool CastDialogView::IsAccessCodeCastingEnabled() const {
return GetAccessCodeCastEnabledPref(profile_);
}
BEGIN_METADATA(CastDialogView)
END_METADATA
} // namespace media_router