Implemented cast devices selector on ash side
1, Created `MediaCastAudioSelectorView` on ash side which is a subclass of the `global_media_controls::MediaItemUIDeviceSelector` - This view carries the cast list view and the audio list view - the audio list view will be added in a separate cl later 2, Created `MediaCastListView`. - Using the same device detecting logic as in the `global_media_controls::MediaItemUIDeviceSelectorView`. It will only detect the server casting devices, while in the cast detailed view we detect the screen casting devices. - This class extends the `TrayDetailedView`, so that each cast entry is in the same UI style as in the `CastDetailedView` 3, [browser/UI/ash] In the `MediaNotificationProviderImpl` returns the ash cast device selector view if it's ash build and the BackgroundListening flag is enabled. 4, [browser/UI/views] Refactored several helper functions to help build the ash cast device selector view in #3. 5, Hide the separator line if it's ash build and BL flag is enabled in the `global_media_controls::MediaItemUIDetailedView` Bug: b/327507429 Change-Id: Ic699221e861addfa95ad3d248918f14eef80a3b0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5393298 Commit-Queue: Jiaming Cheng <jiamingc@chromium.org> Reviewed-by: Andrew Xu <andrewxu@chromium.org> Reviewed-by: Tommy Steimel <steimel@chromium.org> Cr-Commit-Position: refs/heads/main@{#1282326}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
7e85fafa51
commit
ba0ef7c7e4
ash
chrome/browser/ui
ash
global_media_controls
views
global_media_controls
components/global_media_controls/public/views
@ -1572,6 +1572,10 @@ component("ash") {
|
||||
"system/cast/cast_notification_controller.h",
|
||||
"system/cast/cast_zero_state_view.cc",
|
||||
"system/cast/cast_zero_state_view.h",
|
||||
"system/cast/media_cast_audio_selector_view.cc",
|
||||
"system/cast/media_cast_audio_selector_view.h",
|
||||
"system/cast/media_cast_list_view.cc",
|
||||
"system/cast/media_cast_list_view.h",
|
||||
"system/cast/unified_cast_detailed_view_controller.cc",
|
||||
"system/cast/unified_cast_detailed_view_controller.h",
|
||||
"system/channel_indicator/channel_indicator.cc",
|
||||
@ -3778,6 +3782,8 @@ test("ash_unittests") {
|
||||
"system/cast/cast_detailed_view_unittest.cc",
|
||||
"system/cast/cast_feature_pod_controller_unittest.cc",
|
||||
"system/cast/cast_notification_controller_unittest.cc",
|
||||
"system/cast/media_cast_audio_selector_view_unittest.cc",
|
||||
"system/cast/media_cast_list_view_unittest.cc",
|
||||
"system/channel_indicator/channel_indicator_quick_settings_view_unittest.cc",
|
||||
"system/channel_indicator/channel_indicator_unittest.cc",
|
||||
"system/channel_indicator/channel_indicator_utils_unittest.cc",
|
||||
@ -4299,6 +4305,7 @@ test("ash_unittests") {
|
||||
"//components/feature_engagement/public",
|
||||
"//components/feature_engagement/test:test_support",
|
||||
"//components/global_media_controls",
|
||||
"//components/global_media_controls:test_support",
|
||||
"//components/language/core/browser:browser",
|
||||
"//components/live_caption:constants",
|
||||
"//components/media_message_center",
|
||||
|
@ -5359,6 +5359,10 @@ No devices connected.
|
||||
</message>
|
||||
|
||||
<!-- Media Tray -->
|
||||
<message name="IDS_ASH_GLOBAL_MEDIA_CONTROLS_CAST_LIST_HEADER" desc="Label for the Global Media Controls cast list header.">
|
||||
Cast to
|
||||
</message>
|
||||
|
||||
<message name="IDS_ASH_GLOBAL_MEDIA_CONTROLS_BUTTON_TOOLTIP_TEXT" desc="Tooltip for the Global Media Controls icon, which appears in the toolbar. The tooltip appears on mouseover of the icon.">
|
||||
Control your music, videos, and more
|
||||
</message>
|
||||
|
@ -0,0 +1 @@
|
||||
3caf27ffa8df68484d25b27fc86197f35cc988cc
|
143
ash/system/cast/media_cast_audio_selector_view.cc
Normal file
143
ash/system/cast/media_cast_audio_selector_view.cc
Normal file
@ -0,0 +1,143 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "ash/system/cast/media_cast_audio_selector_view.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "ash/system/cast/media_cast_list_view.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/observer_list.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "components/global_media_controls/public/views/media_item_ui_view.h"
|
||||
#include "components/vector_icons/vector_icons.h"
|
||||
#include "media/audio/audio_device_description.h"
|
||||
#include "media/base/media_switches.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/metadata/metadata_impl_macros.h"
|
||||
#include "ui/base/models/image_model.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/layout/box_layout.h"
|
||||
#include "ui/views/layout/box_layout_view.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
namespace {
|
||||
// Constants for the `MediaCastAudioSelectorView`
|
||||
|
||||
// Using a fixed width for this view which will be CHECKed on the global media
|
||||
// side (`media_item_ui_view`).
|
||||
constexpr int kSelectorViewWidth = 400;
|
||||
|
||||
} // namespace
|
||||
|
||||
MediaCastAudioSelectorView::MediaCastAudioSelectorView(
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListHost>
|
||||
device_list_host,
|
||||
mojo::PendingReceiver<global_media_controls::mojom::DeviceListClient>
|
||||
receiver,
|
||||
base::RepeatingClosure stop_casting_callback,
|
||||
bool show_devices)
|
||||
: device_list_host_(std::move(device_list_host)) {
|
||||
views::Builder<views::View>(this)
|
||||
.SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT))
|
||||
.SetLayoutManager(std::make_unique<views::BoxLayout>(
|
||||
views::BoxLayout::Orientation::kVertical))
|
||||
.SetPreferredSize(
|
||||
gfx::Size(kSelectorViewWidth, GetHeightForWidth(kSelectorViewWidth)))
|
||||
.AddChildren(
|
||||
views::Builder<views::BoxLayoutView>()
|
||||
.CopyAddressTo(&list_view_container_)
|
||||
.SetID(kListViewContainerId)
|
||||
.SetOrientation(views::BoxLayout::Orientation::kVertical)
|
||||
.SetVisible(false)
|
||||
.AddChildren(
|
||||
views::Builder<views::View>(
|
||||
std::make_unique<MediaCastListView>(
|
||||
std::move(stop_casting_callback),
|
||||
base::BindRepeating(
|
||||
&MediaCastAudioSelectorView::OnCastDeviceSelected,
|
||||
base::Unretained(this)),
|
||||
base::BindRepeating(
|
||||
&MediaCastAudioSelectorView::OnDevicesUpdated,
|
||||
base::Unretained(this)),
|
||||
std::move(receiver)))
|
||||
.SetID(kMediaCastListViewId)))
|
||||
.BuildChildren();
|
||||
|
||||
if (show_devices) {
|
||||
ShowDevices();
|
||||
}
|
||||
|
||||
DeprecatedLayoutImmediately();
|
||||
}
|
||||
|
||||
MediaCastAudioSelectorView::~MediaCastAudioSelectorView() = default;
|
||||
|
||||
void MediaCastAudioSelectorView::SetMediaItemUIView(
|
||||
global_media_controls::MediaItemUIView* view) {
|
||||
media_item_ui_ = view;
|
||||
}
|
||||
|
||||
// The colors are set with ash color ids, so we don't need to override this
|
||||
// method.
|
||||
void MediaCastAudioSelectorView::OnColorsChanged(SkColor foreground_color,
|
||||
SkColor background_color) {}
|
||||
|
||||
// This audio device related feature (under the flag
|
||||
// `media::kGlobalMediaControlsSeamlessTransfer`) were never launched, so no
|
||||
// longer maintained. On ash side here we override it with an empty
|
||||
// implementation, and we add our own output audio list to this view.
|
||||
void MediaCastAudioSelectorView::UpdateCurrentAudioDevice(
|
||||
const std::string& current_device_id) {}
|
||||
|
||||
void MediaCastAudioSelectorView::MediaCastAudioSelectorView::ShowDevices() {
|
||||
DCHECK(!is_expanded_);
|
||||
is_expanded_ = true;
|
||||
NotifyAccessibilityEvent(ax::mojom::Event::kExpandedChanged, true);
|
||||
|
||||
list_view_container_->SetVisible(true);
|
||||
|
||||
// Focus the first available device when the device list is shown for
|
||||
// accessibility.
|
||||
// TODO(b/327507429): Revisit the logic here when the audio list view is
|
||||
// added to see which view should be focused on.
|
||||
if (const auto& children = list_view_container_->children();
|
||||
!children.empty()) {
|
||||
children[0]->RequestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void MediaCastAudioSelectorView::HideDevices() {
|
||||
DCHECK(is_expanded_);
|
||||
is_expanded_ = false;
|
||||
NotifyAccessibilityEvent(ax::mojom::Event::kExpandedChanged, true);
|
||||
|
||||
list_view_container_->SetVisible(false);
|
||||
PreferredSizeChanged();
|
||||
}
|
||||
|
||||
bool MediaCastAudioSelectorView::IsDeviceSelectorExpanded() {
|
||||
return is_expanded_;
|
||||
}
|
||||
|
||||
void MediaCastAudioSelectorView::OnDevicesUpdated(bool has_devices) {
|
||||
if (media_item_ui_) {
|
||||
media_item_ui_->OnDeviceSelectorViewDevicesChanged(has_devices);
|
||||
}
|
||||
}
|
||||
|
||||
void MediaCastAudioSelectorView::OnCastDeviceSelected(
|
||||
const std::string& device_id) {
|
||||
if (device_list_host_) {
|
||||
device_list_host_->SelectDevice(device_id);
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN_METADATA(MediaCastAudioSelectorView)
|
||||
END_METADATA
|
||||
|
||||
} // namespace ash
|
91
ash/system/cast/media_cast_audio_selector_view.h
Normal file
91
ash/system/cast/media_cast_audio_selector_view.h
Normal file
@ -0,0 +1,91 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ASH_SYSTEM_CAST_MEDIA_CAST_AUDIO_SELECTOR_VIEW_H_
|
||||
#define ASH_SYSTEM_CAST_MEDIA_CAST_AUDIO_SELECTOR_VIEW_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ash/ash_export.h"
|
||||
#include "ash/system/cast/media_cast_list_view.h"
|
||||
#include "base/functional/callback_forward.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "components/global_media_controls/public/mojom/device_service.mojom.h"
|
||||
#include "components/global_media_controls/public/views/media_item_ui_device_selector.h"
|
||||
#include "mojo/public/cpp/bindings/pending_remote.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "third_party/skia/include/core/SkColor.h"
|
||||
#include "ui/base/metadata/metadata_header_macros.h"
|
||||
|
||||
namespace global_media_controls {
|
||||
class MediaItemUIView;
|
||||
} // namespace global_media_controls
|
||||
|
||||
namespace views {
|
||||
class View;
|
||||
} // namespace views
|
||||
|
||||
namespace ash {
|
||||
|
||||
// View ID's.
|
||||
inline constexpr int kListViewContainerId = kMediaCastListViewMaxId + 1;
|
||||
inline constexpr int kMediaCastListViewId = kListViewContainerId + 1;
|
||||
|
||||
// The device selector view on Ash side. It shows an audio list view and a cast
|
||||
// list view. This view will show under the global `MediaUIView`.
|
||||
// TODO(b/327507429): Add the audio list view.
|
||||
class ASH_EXPORT MediaCastAudioSelectorView
|
||||
: public global_media_controls::MediaItemUIDeviceSelector {
|
||||
METADATA_HEADER(MediaCastAudioSelectorView,
|
||||
global_media_controls::MediaItemUIDeviceSelector)
|
||||
public:
|
||||
MediaCastAudioSelectorView(
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListHost>
|
||||
device_list_host,
|
||||
mojo::PendingReceiver<global_media_controls::mojom::DeviceListClient>
|
||||
receiver,
|
||||
base::RepeatingClosure stop_casting_callback,
|
||||
bool show_devices);
|
||||
MediaCastAudioSelectorView(const MediaCastAudioSelectorView&) = delete;
|
||||
MediaCastAudioSelectorView& operator=(const MediaCastAudioSelectorView&) =
|
||||
delete;
|
||||
~MediaCastAudioSelectorView() override;
|
||||
|
||||
// global_media_controls::MediaItemUIDeviceSelector:
|
||||
void SetMediaItemUIView(
|
||||
global_media_controls::MediaItemUIView* view) override;
|
||||
void OnColorsChanged(SkColor foreground_color,
|
||||
SkColor background_color) override;
|
||||
void UpdateCurrentAudioDevice(const std::string& current_device_id) override;
|
||||
void ShowDevices() override;
|
||||
void HideDevices() override;
|
||||
bool IsDeviceSelectorExpanded() override;
|
||||
|
||||
private:
|
||||
// The callback that is passed to the `MediaCastListView` to inform the
|
||||
// panel that the devices is updated.
|
||||
void OnDevicesUpdated(bool has_devices);
|
||||
|
||||
// The callback that is passed to the `MediaCastListView` when a cast item
|
||||
// entry is pressed.
|
||||
void OnCastDeviceSelected(const std::string& device_id);
|
||||
|
||||
bool is_expanded_ = false;
|
||||
|
||||
// The container that carries the audio list view and the cast list view.
|
||||
raw_ptr<views::View> list_view_container_ = nullptr;
|
||||
|
||||
// The panel view of the media bubble.
|
||||
raw_ptr<global_media_controls::MediaItemUIView> media_item_ui_ = nullptr;
|
||||
|
||||
// Provides access to devices on which the media can be casted.
|
||||
mojo::Remote<global_media_controls::mojom::DeviceListHost> device_list_host_;
|
||||
|
||||
base::WeakPtrFactory<MediaCastAudioSelectorView> weak_ptr_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace ash
|
||||
|
||||
#endif // ASH_SYSTEM_CAST_MEDIA_CAST_AUDIO_SELECTOR_VIEW_H_
|
111
ash/system/cast/media_cast_audio_selector_view_unittest.cc
Normal file
111
ash/system/cast/media_cast_audio_selector_view_unittest.cc
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "ash/system/cast/media_cast_audio_selector_view.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ash/test/ash_test_base.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/test/scoped_feature_list.h"
|
||||
#include "components/global_media_controls/public/test/mock_device_service.h"
|
||||
#include "media/base/media_switches.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "ui/views/view.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
|
||||
using global_media_controls::test::MockDeviceListHost;
|
||||
|
||||
namespace ash {
|
||||
|
||||
class MediaCastAudioSelectorViewTest : public AshTestBase {
|
||||
public:
|
||||
MediaCastAudioSelectorViewTest() = default;
|
||||
|
||||
// Mock callbacks:
|
||||
MOCK_METHOD(void, OnStopCasting, (), ());
|
||||
|
||||
// AshTestBase:
|
||||
void SetUp() override {
|
||||
feature_list_.InitAndEnableFeature(media::kBackgroundListening);
|
||||
AshTestBase::SetUp();
|
||||
|
||||
widget_ = CreateFramelessTestWidget();
|
||||
widget_->SetFullscreen(true);
|
||||
view_ =
|
||||
widget_->SetContentsView(std::make_unique<MediaCastAudioSelectorView>(
|
||||
/*device_list_host=*/device_list_host_.PassRemote(),
|
||||
/*receiver=*/client_remote_.BindNewPipeAndPassReceiver(),
|
||||
/*stop_casting_callback=*/
|
||||
base::BindRepeating(&MediaCastAudioSelectorViewTest::OnStopCasting,
|
||||
base::Unretained(this)),
|
||||
/*show_devices=*/false));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
view_ = nullptr;
|
||||
widget_.reset();
|
||||
AshTestBase::TearDown();
|
||||
}
|
||||
|
||||
MediaCastAudioSelectorView* GetSelectorView() { return view_; }
|
||||
|
||||
views::View* GetListViewContainer() {
|
||||
return view_->GetViewByID(kListViewContainerId);
|
||||
}
|
||||
|
||||
MediaCastListView* GetMediaCastListView() {
|
||||
return static_cast<MediaCastListView*>(
|
||||
view_->GetViewByID(kMediaCastListViewId));
|
||||
}
|
||||
|
||||
std::vector<raw_ptr<views::View, VectorExperimental>>
|
||||
GetContainerChildViews() {
|
||||
return GetMediaCastListView()->item_container_->children();
|
||||
}
|
||||
|
||||
// Adds 1 simulated cast device.
|
||||
void AddCastDevices() {
|
||||
std::vector<global_media_controls::mojom::DevicePtr> devices;
|
||||
global_media_controls::mojom::DevicePtr device =
|
||||
global_media_controls::mojom::Device::New(
|
||||
/*id=*/"fake_sink_id_0",
|
||||
/*name=*/"Sink Name 0",
|
||||
/*status_text=*/"",
|
||||
/*icon=*/global_media_controls::mojom::IconType::kTv);
|
||||
devices.push_back(std::move(device));
|
||||
GetMediaCastListView()->OnDevicesUpdated(std::move(devices));
|
||||
}
|
||||
|
||||
base::test::ScopedFeatureList feature_list_;
|
||||
std::unique_ptr<views::Widget> widget_;
|
||||
raw_ptr<MediaCastAudioSelectorView> view_ = nullptr;
|
||||
MockDeviceListHost device_list_host_;
|
||||
mojo::Remote<global_media_controls::mojom::DeviceListClient> client_remote_;
|
||||
};
|
||||
|
||||
TEST_F(MediaCastAudioSelectorViewTest, VisibilityChanges) {
|
||||
// Adding cast devices creates views.
|
||||
AddCastDevices();
|
||||
EXPECT_EQ(GetContainerChildViews().size(), 2u);
|
||||
|
||||
// Seletor view is not visible before showing up the list views.
|
||||
EXPECT_FALSE(GetListViewContainer()->GetVisible());
|
||||
EXPECT_FALSE(GetSelectorView()->IsDeviceSelectorExpanded());
|
||||
|
||||
// Seletor view not visible after showing up the list views.
|
||||
GetSelectorView()->ShowDevices();
|
||||
EXPECT_TRUE(GetListViewContainer()->GetVisible());
|
||||
EXPECT_TRUE(GetSelectorView()->IsDeviceSelectorExpanded());
|
||||
|
||||
// Seletor view is not visible after hiding the list views.
|
||||
GetSelectorView()->HideDevices();
|
||||
EXPECT_FALSE(GetListViewContainer()->GetVisible());
|
||||
EXPECT_FALSE(GetSelectorView()->IsDeviceSelectorExpanded());
|
||||
}
|
||||
|
||||
} // namespace ash
|
196
ash/system/cast/media_cast_list_view.cc
Normal file
196
ash/system/cast/media_cast_list_view.cc
Normal file
@ -0,0 +1,196 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "ash/system/cast/media_cast_list_view.h"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ash/public/cpp/system_tray_client.h"
|
||||
#include "ash/resources/vector_icons/vector_icons.h"
|
||||
#include "ash/strings/grit/ash_strings.h"
|
||||
#include "ash/style/ash_color_provider.h"
|
||||
#include "ash/style/pill_button.h"
|
||||
#include "ash/style/typography.h"
|
||||
#include "ash/system/tray/hover_highlight_view.h"
|
||||
#include "ash/system/tray/tray_constants.h"
|
||||
#include "ash/system/tray/tray_detailed_view.h"
|
||||
#include "ash/system/tray/tray_popup_utils.h"
|
||||
#include "ash/system/tray/tri_view.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "components/global_media_controls/public/mojom/device_service.mojom-shared.h"
|
||||
#include "components/vector_icons/vector_icons.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/metadata/metadata_impl_macros.h"
|
||||
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
|
||||
#include "ui/gfx/paint_vector_icon.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/border.h"
|
||||
#include "ui/views/controls/throbber.h"
|
||||
#include "ui/views/layout/box_layout.h"
|
||||
#include "ui/views/layout/box_layout_view.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
namespace {
|
||||
|
||||
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(std::string, kDeviceIdKey, nullptr)
|
||||
|
||||
// Extra spacing to add between cast stop buttons and the edge of `tri_view()`
|
||||
// header entry.
|
||||
constexpr gfx::Insets kItemTriViewPaddings =
|
||||
gfx::Insets::TLBR(0, 0, 0, kTrayPopupLabelRightPadding);
|
||||
|
||||
// The paddings for the header entry and cast item entries.
|
||||
constexpr gfx::Insets kHeaderInsets = gfx::Insets::TLBR(0, 0, 0, 8);
|
||||
constexpr gfx::Insets kHighlightHoverViewInsets = gfx::Insets::VH(0, 16);
|
||||
|
||||
// Returns the correct vector icon for the icon type.
|
||||
// TODO(b/327507429): Revisit the icons for each casting type.
|
||||
const gfx::VectorIcon& GetVectorIcon(
|
||||
global_media_controls::mojom::IconType icon) {
|
||||
switch (icon) {
|
||||
case global_media_controls::mojom::IconType::kInfo:
|
||||
return kSystemMenuCastGenericIcon;
|
||||
case global_media_controls::mojom::IconType::kInput:
|
||||
return kSystemMenuCastGenericIcon;
|
||||
case global_media_controls::mojom::IconType::kSpeaker:
|
||||
return kSystemMenuCastAudioIcon;
|
||||
case global_media_controls::mojom::IconType::kSpeakerGroup:
|
||||
return kSystemMenuCastAudioGroupIcon;
|
||||
case global_media_controls::mojom::IconType::kTv:
|
||||
return kSystemMenuCastGenericIcon;
|
||||
// In these cases the icon is a placeholder and doesn't actually get
|
||||
// shown.
|
||||
// TODO(b/327507429): Show the loading throbber in the
|
||||
// `MediaCastListView`.
|
||||
case global_media_controls::mojom::IconType::kThrobber:
|
||||
case global_media_controls::mojom::IconType::kUnknown:
|
||||
return kSystemMenuCastGenericIcon;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MediaCastListView::MediaCastListView(
|
||||
base::RepeatingClosure stop_casting_callback,
|
||||
base::RepeatingCallback<void(const std::string& device_id)>
|
||||
start_casting_callback,
|
||||
base::RepeatingCallback<void(const bool has_devices)>
|
||||
on_devices_updated_callback,
|
||||
mojo::PendingReceiver<global_media_controls::mojom::DeviceListClient>
|
||||
receiver)
|
||||
: TrayDetailedView(/*delegate=*/nullptr),
|
||||
on_stop_casting_callback_(std::move(stop_casting_callback)),
|
||||
on_start_casting_callback_(std::move(start_casting_callback)),
|
||||
on_devices_updated_callback_(std::move(on_devices_updated_callback)),
|
||||
receiver_(this, std::move(receiver)) {
|
||||
// Creates the cast item container.
|
||||
item_container_ =
|
||||
AddChildView(views::Builder<views::BoxLayoutView>()
|
||||
.SetOrientation(views::BoxLayout::Orientation::kVertical)
|
||||
.Build());
|
||||
}
|
||||
|
||||
MediaCastListView::~MediaCastListView() = default;
|
||||
|
||||
void MediaCastListView::OnDevicesUpdated(
|
||||
std::vector<global_media_controls::mojom::DevicePtr> devices) {
|
||||
// Update UI.
|
||||
item_container_->RemoveAllChildViews();
|
||||
|
||||
if (!devices.empty()) {
|
||||
CreateCastingHeader();
|
||||
}
|
||||
|
||||
// Add a view for each receiver.
|
||||
for (const auto& device : devices) {
|
||||
HoverHighlightView* container =
|
||||
AddScrollListItem(item_container_, GetVectorIcon(device->icon),
|
||||
base::UTF8ToUTF16(device->name));
|
||||
container->tri_view()->SetInsets(kHighlightHoverViewInsets);
|
||||
container->SetProperty(kDeviceIdKey, device->id);
|
||||
}
|
||||
|
||||
// Inform the Panel view on devices update.
|
||||
on_devices_updated_callback_.Run(
|
||||
/*has_devices=*/!devices.empty());
|
||||
|
||||
item_container_->InvalidateLayout();
|
||||
}
|
||||
|
||||
void MediaCastListView::HandleViewClicked(views::View* view) {
|
||||
if (view->GetProperty(kDeviceIdKey)) {
|
||||
on_start_casting_callback_.Run(*view->GetProperty(kDeviceIdKey));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/327507429): Refactor this method to share it with the
|
||||
// CastDetailedView.
|
||||
std::unique_ptr<PillButton> MediaCastListView::CreateStopButton() {
|
||||
auto stop_button = std::make_unique<PillButton>(
|
||||
base::BindRepeating(
|
||||
[](base::WeakPtr<MediaCastListView> view) {
|
||||
view->on_stop_casting_callback_.Run();
|
||||
},
|
||||
weak_factory_.GetWeakPtr()),
|
||||
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_STOP_CASTING),
|
||||
PillButton::kDefaultWithIconLeading, &kQuickSettingsCircleStopIcon);
|
||||
stop_button->SetBackgroundColorId(cros_tokens::kCrosSysErrorContainer);
|
||||
stop_button->SetIconColorId(cros_tokens::kCrosSysError);
|
||||
stop_button->SetButtonTextColorId(cros_tokens::kCrosSysError);
|
||||
stop_button->SetID(kStopCastingButtonId);
|
||||
return stop_button;
|
||||
}
|
||||
|
||||
void MediaCastListView::CreateCastingHeader() {
|
||||
auto casting_header = base::WrapUnique(TrayPopupUtils::CreateDefaultRowView(
|
||||
/*use_wide_layout=*/false));
|
||||
TrayPopupUtils::ConfigureHeader(casting_header.get());
|
||||
|
||||
// Set casting icon on left side.
|
||||
auto image_view = base::WrapUnique(
|
||||
TrayPopupUtils::CreateMainImageView(/*use_wide_layout=*/false));
|
||||
image_view->SetImage(gfx::CreateVectorIcon(
|
||||
on_stop_casting_callback_.is_null() ? kQuickSettingsCastIcon
|
||||
: kQuickSettingsCastConnectedIcon,
|
||||
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface)));
|
||||
image_view->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
|
||||
casting_header->AddView(TriView::Container::START, image_view.release());
|
||||
|
||||
// Set message label in middle of row.
|
||||
std::unique_ptr<views::Label> label =
|
||||
base::WrapUnique(TrayPopupUtils::CreateDefaultLabel());
|
||||
label->SetText(l10n_util::GetStringUTF16(
|
||||
IDS_ASH_GLOBAL_MEDIA_CONTROLS_CAST_LIST_HEADER));
|
||||
label->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
|
||||
label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
|
||||
label->SetAutoColorReadabilityEnabled(false);
|
||||
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2, *label);
|
||||
casting_header->AddView(TriView::Container::CENTER, std::move(label));
|
||||
casting_header->SetContainerBorder(
|
||||
TriView::Container::CENTER,
|
||||
views::CreateEmptyBorder(kItemTriViewPaddings));
|
||||
|
||||
// Add stop button to the entry if it's casting.
|
||||
if (!on_stop_casting_callback_.is_null()) {
|
||||
std::unique_ptr<PillButton> stop_button = CreateStopButton();
|
||||
casting_header->AddView(TriView::Container::END, stop_button.release());
|
||||
casting_header->SetContainerVisible(TriView::Container::END, true);
|
||||
} else {
|
||||
// Nothing to the right of the text.
|
||||
casting_header->SetContainerVisible(TriView::Container::END, false);
|
||||
}
|
||||
casting_header->SetInsets(kHeaderInsets);
|
||||
item_container_->AddChildView(std::move(casting_header));
|
||||
}
|
||||
|
||||
BEGIN_METADATA(MediaCastListView)
|
||||
END_METADATA
|
||||
|
||||
} // namespace ash
|
91
ash/system/cast/media_cast_list_view.h
Normal file
91
ash/system/cast/media_cast_list_view.h
Normal file
@ -0,0 +1,91 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ASH_SYSTEM_CAST_MEDIA_CAST_LIST_VIEW_H_
|
||||
#define ASH_SYSTEM_CAST_MEDIA_CAST_LIST_VIEW_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ash/ash_export.h"
|
||||
#include "ash/system/tray/tray_detailed_view.h"
|
||||
#include "base/functional/callback_forward.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "components/global_media_controls/public/mojom/device_service.mojom.h"
|
||||
#include "mojo/public/cpp/bindings/receiver.h"
|
||||
#include "ui/base/metadata/metadata_header_macros.h"
|
||||
|
||||
namespace views {
|
||||
class View;
|
||||
} // namespace views
|
||||
|
||||
namespace ash {
|
||||
|
||||
class PillButton;
|
||||
|
||||
// View ID's.
|
||||
inline constexpr int kStopCastingButtonId = 100;
|
||||
inline constexpr int kMediaCastListViewMaxId = kStopCastingButtonId;
|
||||
|
||||
// This view displays a list of cast devices that can be clicked on and casted
|
||||
// to. If it's currently casting, it shows a stop casting button on the header
|
||||
// entry.
|
||||
class ASH_EXPORT MediaCastListView
|
||||
: public global_media_controls::mojom::DeviceListClient,
|
||||
public TrayDetailedView {
|
||||
METADATA_HEADER(MediaCastListView, TrayDetailedView)
|
||||
|
||||
public:
|
||||
MediaCastListView(
|
||||
base::RepeatingClosure stop_casting_callback,
|
||||
base::RepeatingCallback<void(const std::string& device_id)>
|
||||
start_casting_callback,
|
||||
base::RepeatingCallback<void(const bool has_devices)>
|
||||
on_devices_updated_callback,
|
||||
mojo::PendingReceiver<global_media_controls::mojom::DeviceListClient>
|
||||
receiver);
|
||||
|
||||
MediaCastListView(const MediaCastListView&) = delete;
|
||||
MediaCastListView& operator=(const MediaCastListView&) = delete;
|
||||
|
||||
~MediaCastListView() override;
|
||||
|
||||
// global_media_controls::mojom::DeviceListClient:
|
||||
void OnDevicesUpdated(
|
||||
std::vector<global_media_controls::mojom::DevicePtr> devices) override;
|
||||
|
||||
private:
|
||||
friend class MediaCastAudioSelectorViewTest;
|
||||
friend class MediaCastListViewTest;
|
||||
|
||||
// TrayDetailedView:
|
||||
void HandleViewClicked(views::View* view) override;
|
||||
|
||||
// Creates a stop button which, when pressed, stops casting.
|
||||
std::unique_ptr<PillButton> CreateStopButton();
|
||||
|
||||
// Creates the header of the list. If it's currently casting, add the stop
|
||||
// casting button.
|
||||
void CreateCastingHeader();
|
||||
|
||||
// Cast item container.
|
||||
raw_ptr<views::View> item_container_ = nullptr;
|
||||
|
||||
// Callbacks to stop/start casting.
|
||||
base::RepeatingClosure on_stop_casting_callback_;
|
||||
base::RepeatingCallback<void(const std::string&)> on_start_casting_callback_;
|
||||
|
||||
// Runs on devices updated.
|
||||
base::RepeatingCallback<void(const bool)> on_devices_updated_callback_;
|
||||
|
||||
mojo::Receiver<global_media_controls::mojom::DeviceListClient> receiver_;
|
||||
|
||||
base::WeakPtrFactory<MediaCastListView> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace ash
|
||||
|
||||
#endif // ASH_SYSTEM_CAST_MEDIA_CAST_LIST_VIEW_H_
|
179
ash/system/cast/media_cast_list_view_unittest.cc
Normal file
179
ash/system/cast/media_cast_list_view_unittest.cc
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "ash/system/cast/media_cast_list_view.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ash/style/pill_button.h"
|
||||
#include "ash/system/tray/hover_highlight_view.h"
|
||||
#include "ash/test/ash_test_base.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/test/scoped_feature_list.h"
|
||||
#include "media/base/media_switches.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "ui/views/view.h"
|
||||
#include "ui/views/view_utils.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
class MediaCastListViewTest : public AshTestBase {
|
||||
public:
|
||||
// Mock callbacks:
|
||||
MOCK_METHOD(void, OnStopCasting, (), ());
|
||||
MOCK_METHOD(void, OnCastDeviceSelected, (const std::string& device_id), ());
|
||||
MOCK_METHOD(void, OnDeviceListUpdated, (const bool has_devices), ());
|
||||
|
||||
// AshTestBase:
|
||||
void SetUp() override {
|
||||
feature_list_.InitAndEnableFeature(media::kBackgroundListening);
|
||||
AshTestBase::SetUp();
|
||||
// Create a widget so tests can click on views.
|
||||
widget_ = CreateFramelessTestWidget();
|
||||
widget_->SetFullscreen(true);
|
||||
view_ = widget_->SetContentsView(std::make_unique<MediaCastListView>(
|
||||
base::BindRepeating(&MediaCastListViewTest::OnStopCasting,
|
||||
base::Unretained(this)),
|
||||
base::BindRepeating(&MediaCastListViewTest::OnCastDeviceSelected,
|
||||
base::Unretained(this)),
|
||||
base::BindRepeating(&MediaCastListViewTest::OnDeviceListUpdated,
|
||||
base::Unretained(this)),
|
||||
client_remote_.BindNewPipeAndPassReceiver()));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
view_ = nullptr;
|
||||
widget_.reset();
|
||||
AshTestBase::TearDown();
|
||||
}
|
||||
|
||||
MediaCastListView* media_cast_list_view() { return view_; }
|
||||
std::vector<raw_ptr<views::View, VectorExperimental>>
|
||||
GetContainerChildViews() {
|
||||
return view_->item_container_->children();
|
||||
}
|
||||
|
||||
views::View* GetStopCastingButton() {
|
||||
return view_->GetViewByID(kStopCastingButtonId);
|
||||
}
|
||||
|
||||
// Adds 3 simulated cast devices.
|
||||
void AddCastDevices() {
|
||||
std::vector<global_media_controls::mojom::DevicePtr> devices;
|
||||
global_media_controls::mojom::DevicePtr device1 =
|
||||
global_media_controls::mojom::Device::New(
|
||||
/*id=*/"fake_sink_id_1",
|
||||
/*name=*/"Sink Name 1",
|
||||
/*status_text=*/"",
|
||||
/*icon=*/global_media_controls::mojom::IconType::kTv);
|
||||
devices.push_back(std::move(device1));
|
||||
global_media_controls::mojom::DevicePtr device2 =
|
||||
global_media_controls::mojom::Device::New(
|
||||
/*id=*/"fake_sink_id_2",
|
||||
/*name=*/"Sink Name 2",
|
||||
/*status_text=*/"",
|
||||
/*icon=*/global_media_controls::mojom::IconType::kSpeaker);
|
||||
devices.push_back(std::move(device2));
|
||||
global_media_controls::mojom::DevicePtr device3 =
|
||||
global_media_controls::mojom::Device::New(
|
||||
/*id=*/"fake_sink_id_3",
|
||||
/*name=*/"Sink Name 3",
|
||||
/*status_text=*/"",
|
||||
/*icon=*/global_media_controls::mojom::IconType::kSpeakerGroup);
|
||||
devices.push_back(std::move(device3));
|
||||
view_->OnDevicesUpdated(std::move(devices));
|
||||
}
|
||||
|
||||
// Removes simulated cast devices.
|
||||
void ResetCastDevices() { view_->OnDevicesUpdated({}); }
|
||||
|
||||
// The entries may not be rendered immediately due to the async call of
|
||||
// `InvalidateLayout`. Here we manually call `layout` to make sure the views
|
||||
// are rendered.
|
||||
void LayoutEntriesIfNecessary() {
|
||||
view_->GetWidget()->LayoutRootViewIfNecessary();
|
||||
}
|
||||
|
||||
base::test::ScopedFeatureList feature_list_;
|
||||
std::unique_ptr<views::Widget> widget_;
|
||||
raw_ptr<MediaCastListView> view_ = nullptr;
|
||||
bool stop_casting_ = false;
|
||||
bool start_casting_ = false;
|
||||
bool update_devices_callback_ = false;
|
||||
bool is_updated_devices_empty_ = false;
|
||||
std::string current_device_id_;
|
||||
mojo::Remote<global_media_controls::mojom::DeviceListClient> client_remote_;
|
||||
};
|
||||
|
||||
TEST_F(MediaCastListViewTest, ViewsCreatedForCastDevices) {
|
||||
// Adding cast devices creates views.
|
||||
AddCastDevices();
|
||||
LayoutEntriesIfNecessary();
|
||||
|
||||
// One header row and 3 device row.
|
||||
ASSERT_EQ(GetContainerChildViews().size(), 4u);
|
||||
|
||||
HoverHighlightView* row1 =
|
||||
views::AsViewClass<HoverHighlightView>(GetContainerChildViews()[1]);
|
||||
EXPECT_EQ(row1->text_label()->GetText(), u"Sink Name 1");
|
||||
EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_1")).Times(1);
|
||||
LeftClickOn(row1);
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
|
||||
HoverHighlightView* row2 =
|
||||
static_cast<HoverHighlightView*>(GetContainerChildViews()[2]);
|
||||
EXPECT_EQ(row2->text_label()->GetText(), u"Sink Name 2");
|
||||
EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_2")).Times(1);
|
||||
LeftClickOn(row2);
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
|
||||
HoverHighlightView* row3 =
|
||||
static_cast<HoverHighlightView*>(GetContainerChildViews()[3]);
|
||||
EXPECT_EQ(row3->text_label()->GetText(), u"Sink Name 3");
|
||||
EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_3")).Times(1);
|
||||
LeftClickOn(row3);
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
}
|
||||
|
||||
TEST_F(MediaCastListViewTest, WithNoDevices) {
|
||||
// Updates with an default device list.
|
||||
EXPECT_CALL(*this, OnDeviceListUpdated).Times(1);
|
||||
AddCastDevices();
|
||||
EXPECT_EQ(GetContainerChildViews().size(), 4u);
|
||||
ASSERT_TRUE(GetStopCastingButton());
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
|
||||
// Updates with an empty device list. Should not show `StopCastingButton`.
|
||||
EXPECT_CALL(*this, OnDeviceListUpdated).Times(1);
|
||||
ResetCastDevices();
|
||||
EXPECT_EQ(GetContainerChildViews().size(), 0u);
|
||||
EXPECT_EQ(GetStopCastingButton(), nullptr);
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
}
|
||||
|
||||
TEST_F(MediaCastListViewTest, StartAndStopCasting) {
|
||||
AddCastDevices();
|
||||
LayoutEntriesIfNecessary();
|
||||
|
||||
EXPECT_FALSE(GetContainerChildViews().empty());
|
||||
views::View* first_view = GetContainerChildViews()[1];
|
||||
|
||||
// Clicking on an entry triggers a cast session.
|
||||
EXPECT_CALL(*this, OnCastDeviceSelected("fake_sink_id_1")).Times(1);
|
||||
LeftClickOn(first_view);
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
|
||||
// Clicking on stop button triggers stop casting.
|
||||
EXPECT_CALL(*this, OnStopCasting).Times(1);
|
||||
LeftClickOn(GetStopCastingButton());
|
||||
testing::Mock::VerifyAndClearExpectations(this);
|
||||
}
|
||||
|
||||
} // namespace ash
|
@ -5,11 +5,13 @@
|
||||
#include "chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.h"
|
||||
|
||||
#include "ash/shell.h"
|
||||
#include "ash/system/cast/media_cast_audio_selector_view.h"
|
||||
#include "ash/system/media/media_color_theme.h"
|
||||
#include "ash/system/media/media_notification_provider.h"
|
||||
#include "ash/system/media/media_notification_provider_observer.h"
|
||||
#include "ash/system/media/media_tray.h"
|
||||
#include "ash/system/status_area_widget.h"
|
||||
#include "base/functional/callback_forward.h"
|
||||
#include "base/metrics/histogram_functions.h"
|
||||
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
|
||||
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
|
||||
@ -190,6 +192,23 @@ MediaNotificationProviderImpl::BuildDeviceSelectorView(
|
||||
base::WeakPtr<media_message_center::MediaNotificationItem> item,
|
||||
global_media_controls::GlobalMediaControlsEntryPoint entry_point,
|
||||
bool show_devices) {
|
||||
// Returns the Ash `MediaCastAudioSelectorView` if BackgroundListening feature
|
||||
// is enabled.
|
||||
if (base::FeatureList::IsEnabled(media::kBackgroundListening)) {
|
||||
auto* const profile = GetProfile();
|
||||
auto* const device_service = GetDeviceService(item);
|
||||
if (!ShouldShowDeviceSelectorView(profile, device_service, id, item,
|
||||
&device_selector_delegate_)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto device_set = CreateHostAndClient(profile, id, item, device_service);
|
||||
|
||||
return std::make_unique<MediaCastAudioSelectorView>(
|
||||
std::move(device_set.host), std::move(device_set.client),
|
||||
GetStopCastingCallback(profile, id, item), show_devices);
|
||||
}
|
||||
|
||||
return BuildDeviceSelector(id, item, GetDeviceService(item),
|
||||
&device_selector_delegate_, GetProfile(),
|
||||
entry_point, show_devices, media_color_theme_);
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
#include "chrome/browser/ui/views/global_media_controls/media_item_ui_helper.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "chrome/browser/media/router/media_router_feature.h"
|
||||
#include "chrome/browser/profiles/profile.h"
|
||||
#include "chrome/browser/ui/global_media_controls/cast_media_notification_item.h"
|
||||
@ -25,10 +27,40 @@
|
||||
|
||||
namespace {
|
||||
|
||||
bool ShouldShowDeviceSelectorView(
|
||||
const std::string& item_id,
|
||||
void UpdateMediaSessionItemReceiverName(
|
||||
base::WeakPtr<media_message_center::MediaNotificationItem> item,
|
||||
Profile* profile) {
|
||||
const std::optional<media_router::MediaRoute>& route) {
|
||||
if (item->GetSourceType() ==
|
||||
media_message_center::SourceType::kLocalMediaSession) {
|
||||
auto* media_session_item =
|
||||
static_cast<global_media_controls::MediaSessionNotificationItem*>(
|
||||
item.get());
|
||||
if (route.has_value()) {
|
||||
media_session_item->UpdateDeviceName(route->media_sink_name());
|
||||
} else {
|
||||
media_session_item->UpdateDeviceName(std::nullopt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HostAndClientPair::HostAndClientPair() = default;
|
||||
HostAndClientPair::HostAndClientPair(HostAndClientPair&&) = default;
|
||||
HostAndClientPair& HostAndClientPair::operator=(HostAndClientPair&&) = default;
|
||||
HostAndClientPair::~HostAndClientPair() = default;
|
||||
|
||||
bool ShouldShowDeviceSelectorView(
|
||||
Profile* profile,
|
||||
global_media_controls::mojom::DeviceService* device_service,
|
||||
const std::string& item_id,
|
||||
const base::WeakPtr<media_message_center::MediaNotificationItem>& item,
|
||||
|
||||
MediaItemUIDeviceSelectorDelegate* selector_delegate) {
|
||||
if (!device_service || !selector_delegate || !profile || !item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto source_type = item->GetSourceType();
|
||||
if (source_type == media_message_center::SourceType::kCast) {
|
||||
return false;
|
||||
@ -47,23 +79,68 @@ bool ShouldShowDeviceSelectorView(
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateMediaSessionItemReceiverName(
|
||||
base::WeakPtr<media_message_center::MediaNotificationItem> item,
|
||||
const std::optional<media_router::MediaRoute>& route) {
|
||||
if (item->GetSourceType() ==
|
||||
media_message_center::SourceType::kLocalMediaSession) {
|
||||
auto* media_session_item =
|
||||
static_cast<global_media_controls::MediaSessionNotificationItem*>(
|
||||
item.get());
|
||||
if (route.has_value()) {
|
||||
media_session_item->UpdateDeviceName(route->media_sink_name());
|
||||
HostAndClientPair CreateHostAndClient(
|
||||
Profile* profile,
|
||||
const std::string& id,
|
||||
const base::WeakPtr<media_message_center::MediaNotificationItem>& item,
|
||||
global_media_controls::mojom::DeviceService* device_service) {
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListHost> host;
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListClient> client;
|
||||
auto client_receiver = client.InitWithNewPipeAndPassReceiver();
|
||||
if (media_router::GlobalMediaControlsCastStartStopEnabled(profile)) {
|
||||
if (item->GetSourceType() ==
|
||||
media_message_center::SourceType::kLocalMediaSession) {
|
||||
device_service->GetDeviceListHostForSession(
|
||||
id, host.InitWithNewPipeAndPassReceiver(), std::move(client));
|
||||
} else {
|
||||
media_session_item->UpdateDeviceName(std::nullopt);
|
||||
device_service->GetDeviceListHostForPresentation(
|
||||
host.InitWithNewPipeAndPassReceiver(), std::move(client));
|
||||
}
|
||||
}
|
||||
HostAndClientPair host_and_client;
|
||||
host_and_client.host = std::move(host);
|
||||
host_and_client.client = std::move(client_receiver);
|
||||
|
||||
return host_and_client;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
base::RepeatingClosure GetStopCastingCallback(
|
||||
Profile* profile,
|
||||
const std::string& id,
|
||||
const base::WeakPtr<media_message_center::MediaNotificationItem>& item) {
|
||||
base::RepeatingClosure stop_casting_callback;
|
||||
|
||||
// Show a footer view for a local media item when it has an associated Remote
|
||||
// Playback session or a Tab Mirroring Session.
|
||||
if (item->GetSourceType() !=
|
||||
media_message_center::SourceType::kLocalMediaSession) {
|
||||
return stop_casting_callback;
|
||||
}
|
||||
auto route = GetSessionRoute(id, item, profile);
|
||||
UpdateMediaSessionItemReceiverName(item, route);
|
||||
if (!route.has_value()) {
|
||||
return stop_casting_callback;
|
||||
}
|
||||
|
||||
const auto& route_id = route->media_route_id();
|
||||
auto cast_mode = HasRemotePlaybackRoute(item)
|
||||
? media_router::MediaCastMode::REMOTE_PLAYBACK
|
||||
: media_router::MediaCastMode::TAB_MIRROR;
|
||||
|
||||
stop_casting_callback = base::BindRepeating(
|
||||
[](const std::string& route_id, media_router::MediaRouter* router,
|
||||
media_router::MediaCastMode cast_mode) {
|
||||
router->TerminateRoute(route_id);
|
||||
MediaItemUIMetrics::RecordStopCastingMetrics(cast_mode);
|
||||
if (cast_mode == media_router::MediaCastMode::TAB_MIRROR) {
|
||||
MediaDialogView::HideDialog();
|
||||
}
|
||||
},
|
||||
route_id,
|
||||
media_router::MediaRouterFactory::GetApiForBrowserContext(profile),
|
||||
cast_mode);
|
||||
return stop_casting_callback;
|
||||
}
|
||||
|
||||
bool HasRemotePlaybackRoute(
|
||||
base::WeakPtr<media_message_center::MediaNotificationItem> item) {
|
||||
@ -141,30 +218,20 @@ BuildDeviceSelector(
|
||||
global_media_controls::GlobalMediaControlsEntryPoint entry_point,
|
||||
bool show_devices,
|
||||
std::optional<media_message_center::MediaColorTheme> media_color_theme) {
|
||||
if (!device_service || !selector_delegate || !profile ||
|
||||
!ShouldShowDeviceSelectorView(id, item, profile)) {
|
||||
if (!ShouldShowDeviceSelectorView(profile, device_service, id, item,
|
||||
selector_delegate)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const bool is_local_media_session =
|
||||
item->GetSourceType() ==
|
||||
media_message_center::SourceType::kLocalMediaSession;
|
||||
const bool gmc_cast_start_stop_enabled =
|
||||
media_router::GlobalMediaControlsCastStartStopEnabled(profile);
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListHost> host;
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListClient> client;
|
||||
auto client_receiver = client.InitWithNewPipeAndPassReceiver();
|
||||
if (gmc_cast_start_stop_enabled) {
|
||||
if (is_local_media_session) {
|
||||
device_service->GetDeviceListHostForSession(
|
||||
id, host.InitWithNewPipeAndPassReceiver(), std::move(client));
|
||||
} else {
|
||||
device_service->GetDeviceListHostForPresentation(
|
||||
host.InitWithNewPipeAndPassReceiver(), std::move(client));
|
||||
}
|
||||
}
|
||||
|
||||
auto device_set = CreateHostAndClient(profile, id, item, device_service);
|
||||
|
||||
return std::make_unique<MediaItemUIDeviceSelectorView>(
|
||||
id, selector_delegate, std::move(host), std::move(client_receiver),
|
||||
id, selector_delegate, std::move(device_set.host),
|
||||
std::move(device_set.client),
|
||||
/*has_audio_output=*/is_local_media_session, entry_point, show_devices,
|
||||
media_color_theme);
|
||||
}
|
||||
@ -200,36 +267,12 @@ std::unique_ptr<global_media_controls::MediaItemUIFooter> BuildFooter(
|
||||
static_cast<CastMediaNotificationItem*>(item.get())->GetWeakPtr()));
|
||||
}
|
||||
|
||||
// Show a footer view for a local media item when it has an associated Remote
|
||||
// Playback session or a Tab Mirroring Session.
|
||||
if (item->GetSourceType() !=
|
||||
media_message_center::SourceType::kLocalMediaSession) {
|
||||
base::RepeatingClosure stop_casting_cb =
|
||||
GetStopCastingCallback(profile, id, item);
|
||||
if (stop_casting_cb.is_null()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto route = GetSessionRoute(id, item, profile);
|
||||
UpdateMediaSessionItemReceiverName(item, route);
|
||||
if (!route.has_value()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& route_id = route->media_route_id();
|
||||
auto cast_mode = HasRemotePlaybackRoute(item)
|
||||
? media_router::MediaCastMode::REMOTE_PLAYBACK
|
||||
: media_router::MediaCastMode::TAB_MIRROR;
|
||||
|
||||
auto stop_casting_cb = base::BindRepeating(
|
||||
[](const std::string& route_id, media_router::MediaRouter* router,
|
||||
media_router::MediaCastMode cast_mode) {
|
||||
router->TerminateRoute(route_id);
|
||||
MediaItemUIMetrics::RecordStopCastingMetrics(cast_mode);
|
||||
if (cast_mode == media_router::MediaCastMode::TAB_MIRROR) {
|
||||
MediaDialogView::HideDialog();
|
||||
}
|
||||
},
|
||||
route_id,
|
||||
media_router::MediaRouterFactory::GetApiForBrowserContext(profile),
|
||||
cast_mode);
|
||||
return std::make_unique<MediaItemUILegacyCastFooterView>(
|
||||
std::move(stop_casting_cb));
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "components/global_media_controls/public/constants.h"
|
||||
#include "components/global_media_controls/public/mojom/device_service.mojom.h"
|
||||
#include "components/media_message_center/notification_theme.h"
|
||||
|
||||
class MediaItemUIDeviceSelectorDelegate;
|
||||
@ -37,9 +38,44 @@ namespace media_router {
|
||||
class MediaRoute;
|
||||
} // namespace media_router
|
||||
|
||||
// A set of the pending remote of `DeviceListHost` and the pending receiver of
|
||||
// `DeviceListClient`.
|
||||
struct HostAndClientPair {
|
||||
HostAndClientPair();
|
||||
HostAndClientPair(HostAndClientPair&&);
|
||||
HostAndClientPair& operator=(HostAndClientPair&&);
|
||||
~HostAndClientPair();
|
||||
|
||||
mojo::PendingRemote<global_media_controls::mojom::DeviceListHost> host;
|
||||
mojo::PendingReceiver<global_media_controls::mojom::DeviceListClient> client;
|
||||
};
|
||||
|
||||
// These helper functions are shared among Chrome OS and other desktop
|
||||
// platforms.
|
||||
|
||||
// Checks if we should show the device selector view (cast device list) under
|
||||
// the media ui view. Returns false if any of `device_service`,
|
||||
// `selector_delegate`, `profile`, `item` is null.
|
||||
bool ShouldShowDeviceSelectorView(
|
||||
Profile* profile,
|
||||
global_media_controls::mojom::DeviceService* device_service,
|
||||
const std::string& item_id,
|
||||
const base::WeakPtr<media_message_center::MediaNotificationItem>& item,
|
||||
MediaItemUIDeviceSelectorDelegate* selector_delegate);
|
||||
|
||||
// Creates and returns a `HostAndClientPair`.
|
||||
HostAndClientPair CreateHostAndClient(
|
||||
Profile* profile,
|
||||
const std::string& id,
|
||||
const base::WeakPtr<media_message_center::MediaNotificationItem>& item,
|
||||
global_media_controls::mojom::DeviceService* device_service);
|
||||
|
||||
// Returns the stop casting callback if it's casting.
|
||||
base::RepeatingClosure GetStopCastingCallback(
|
||||
Profile* profile,
|
||||
const std::string& id,
|
||||
const base::WeakPtr<media_message_center::MediaNotificationItem>& item);
|
||||
|
||||
// Returns whether `item` has an associated Remote Playback route.
|
||||
bool HasRemotePlaybackRoute(
|
||||
base::WeakPtr<media_message_center::MediaNotificationItem> item);
|
||||
|
@ -675,7 +675,15 @@ void MediaItemUIDetailedView::UpdateCastingState() {
|
||||
start_casting_button_->UpdateText(
|
||||
IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_SHOW_DEVICE_LIST);
|
||||
}
|
||||
device_selector_view_separator_->SetVisible(is_expanded);
|
||||
|
||||
bool is_ash_background_listening_enabled = false;
|
||||
#if BUILDFLAG(IS_CHROMEOS_ASH)
|
||||
is_ash_background_listening_enabled =
|
||||
base::FeatureList::IsEnabled(media::kBackgroundListening);
|
||||
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
|
||||
|
||||
device_selector_view_separator_->SetVisible(
|
||||
is_expanded && !is_ash_background_listening_enabled);
|
||||
} else {
|
||||
device_selector_view_->SetVisible(false);
|
||||
device_selector_view_separator_->SetVisible(false);
|
||||
|
@ -501,4 +501,49 @@ TEST_F(MediaItemUIDetailedViewTest, ChapterList) {
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(MediaItemUIDetailedViewTest, ShouldNotShowDeviceSelectorViewForAsh) {
|
||||
#if BUILDFLAG(IS_CHROMEOS_ASH)
|
||||
base::test::ScopedFeatureList scoped_feature_list;
|
||||
scoped_feature_list.InitAndEnableFeature(media::kBackgroundListening);
|
||||
auto* start_casting_button = view()->GetStartCastingButtonForTesting();
|
||||
auto* separator = view()->GetDeviceSelectorSeparatorForTesting();
|
||||
auto* device_selector_view = view()->GetDeviceSelectorForTesting();
|
||||
|
||||
ASSERT_TRUE(start_casting_button);
|
||||
EXPECT_FALSE(start_casting_button->GetVisible());
|
||||
EXPECT_EQ(device_selector_view, device_selector());
|
||||
EXPECT_FALSE(device_selector_view->GetVisible());
|
||||
ASSERT_TRUE(separator);
|
||||
EXPECT_FALSE(separator->GetVisible());
|
||||
|
||||
// Add devices to the list to show the start casting button.
|
||||
EXPECT_CALL(*device_selector(), IsDeviceSelectorExpanded())
|
||||
.WillOnce(Return(false));
|
||||
view()->UpdateDeviceSelectorAvailability(/*has_devices=*/true);
|
||||
EXPECT_TRUE(start_casting_button->GetVisible());
|
||||
EXPECT_TRUE(device_selector_view->GetVisible());
|
||||
EXPECT_FALSE(separator->GetVisible());
|
||||
|
||||
// Click the start casting button to show devices.
|
||||
EXPECT_CALL(*device_selector(), ShowDevices());
|
||||
EXPECT_CALL(*device_selector(), IsDeviceSelectorExpanded())
|
||||
.WillOnce(Return(false))
|
||||
.WillOnce(Return(true));
|
||||
views::test::ButtonTestApi(start_casting_button)
|
||||
.NotifyClick(ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(),
|
||||
gfx::Point(), ui::EventTimeForNow(), 0, 0));
|
||||
EXPECT_FALSE(separator->GetVisible());
|
||||
|
||||
// Click the start casting button to hide devices.
|
||||
EXPECT_CALL(*device_selector(), HideDevices());
|
||||
EXPECT_CALL(*device_selector(), IsDeviceSelectorExpanded())
|
||||
.WillOnce(Return(true))
|
||||
.WillOnce(Return(false));
|
||||
views::test::ButtonTestApi(start_casting_button)
|
||||
.NotifyClick(ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(),
|
||||
gfx::Point(), ui::EventTimeForNow(), 0, 0));
|
||||
EXPECT_FALSE(separator->GetVisible());
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace global_media_controls
|
||||
|
Reference in New Issue
Block a user