diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-11-18 16:35:47 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-11-18 15:45:54 +0000 |
commit | 32f5a1c56531e4210bc4cf8d8c7825d66e081888 (patch) | |
tree | eeeec6822f4d738d8454525233fd0e2e3a659e6d /chromium/components/media_message_center | |
parent | 99677208ff3b216fdfec551fbe548da5520cd6fb (diff) | |
download | qtwebengine-chromium-32f5a1c56531e4210bc4cf8d8c7825d66e081888.tar.gz |
BASELINE: Update Chromium to 87.0.4280.67
Change-Id: Ib157360be8c2ffb2c73125751a89f60e049c1d54
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/media_message_center')
16 files changed, 2288 insertions, 145 deletions
diff --git a/chromium/components/media_message_center/BUILD.gn b/chromium/components/media_message_center/BUILD.gn index 5fb78edf6f3..7becd24bbc8 100644 --- a/chromium/components/media_message_center/BUILD.gn +++ b/chromium/components/media_message_center/BUILD.gn @@ -6,8 +6,11 @@ component("media_message_center") { sources = [ "media_controls_progress_view.cc", "media_controls_progress_view.h", - "media_notification_background.cc", "media_notification_background.h", + "media_notification_background_ash_impl.cc", + "media_notification_background_ash_impl.h", + "media_notification_background_impl.cc", + "media_notification_background_impl.h", "media_notification_constants.cc", "media_notification_constants.h", "media_notification_container.h", @@ -19,6 +22,8 @@ component("media_message_center") { "media_notification_view.h", "media_notification_view_impl.cc", "media_notification_view_impl.h", + "media_notification_view_modern_impl.cc", + "media_notification_view_modern_impl.h", "media_session_notification_item.cc", "media_session_notification_item.h", ] @@ -45,8 +50,10 @@ source_set("unit_tests") { sources = [ "media_controls_progress_view_unittest.cc", - "media_notification_background_unittest.cc", + "media_notification_background_ash_impl_unittest.cc", + "media_notification_background_impl_unittest.cc", "media_notification_view_impl_unittest.cc", + "media_notification_view_modern_impl_unittest.cc", ] deps = [ diff --git a/chromium/components/media_message_center/media_notification_background.h b/chromium/components/media_message_center/media_notification_background.h index c3ffec25b8e..da45dd3b200 100644 --- a/chromium/components/media_message_center/media_notification_background.h +++ b/chromium/components/media_message_center/media_notification_background.h @@ -1,20 +1,14 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_H_ #define COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_H_ -#include <vector> - -#include "base/component_export.h" -#include "base/optional.h" -#include "ui/gfx/image/image_skia.h" #include "ui/views/background.h" namespace gfx { -class Rect; -class Size; +class ImageSkia; } // namespace gfx namespace views { @@ -23,58 +17,25 @@ class View; namespace media_message_center { -// MediaNotificationBackground draws a custom background for the media -// notification showing the artwork clipped to a rounded rectangle faded into a -// background color. -class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationBackground - : public views::Background { +// Interface for media notification view's background. +class MediaNotificationBackground : public views::Background { public: - MediaNotificationBackground(int top_radius, - int bottom_radius, - double artwork_max_width_pct); - ~MediaNotificationBackground() override; - - // views::Background - void Paint(gfx::Canvas* canvas, views::View* view) const override; - - void UpdateArtwork(const gfx::ImageSkia& image); - bool UpdateCornerRadius(int top_radius, int bottom_radius); - bool UpdateArtworkMaxWidthPct(double max_width_pct); - void UpdateFavicon(const gfx::ImageSkia& icon); - - SkColor GetBackgroundColor(const views::View& owner) const; - SkColor GetForegroundColor(const views::View& owner) const; - - private: - friend class MediaNotificationBackgroundTest; - friend class MediaNotificationViewImplTest; - FRIEND_TEST_ALL_PREFIXES(MediaNotificationBackgroundRTLTest, - BoundsSanityCheck); - - // Shade factor used on favicon dominant color before set as background color. - static constexpr double kBackgroundFaviconColorShadeFactor = 0.35; + // views::Background. + void Paint(gfx::Canvas* canvas, views::View* view) const override = 0; - int GetArtworkWidth(const gfx::Size& view_size) const; - int GetArtworkVisibleWidth(const gfx::Size& view_size) const; - gfx::Rect GetArtworkBounds(const views::View& owner) const; - gfx::Rect GetFilledBackgroundBounds(const views::View& owner) const; - gfx::Rect GetGradientBounds(const views::View& owner) const; - SkPoint GetGradientStartPoint(const gfx::Rect& draw_bounds) const; - SkPoint GetGradientEndPoint(const gfx::Rect& draw_bounds) const; - SkColor GetDefaultBackgroundColor(const views::View& owner) const; - void UpdateColorsInternal(); + virtual void UpdateArtwork(const gfx::ImageSkia& image) = 0; - int top_radius_; - int bottom_radius_; + // Return true if corner radius is successfully updated. + virtual bool UpdateCornerRadius(int top_radius, int bottom_radius) = 0; - gfx::ImageSkia favicon_; - gfx::ImageSkia artwork_; - double artwork_max_width_pct_; + // Retirm true if artwork max with percentage is successfully updated. + virtual bool UpdateArtworkMaxWidthPct(double max_width_pct) = 0; - base::Optional<SkColor> background_color_; - base::Optional<SkColor> foreground_color_; + virtual void UpdateFavicon(const gfx::ImageSkia& icon) = 0; + virtual void UpdateDeviceSelectorAvailability(bool availability) = 0; - DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackground); + virtual SkColor GetBackgroundColor(const views::View& ownser) const = 0; + virtual SkColor GetForegroundColor(const views::View& ownser) const = 0; }; } // namespace media_message_center diff --git a/chromium/components/media_message_center/media_notification_background_ash_impl.cc b/chromium/components/media_message_center/media_notification_background_ash_impl.cc new file mode 100644 index 00000000000..091d78fe26c --- /dev/null +++ b/chromium/components/media_message_center/media_notification_background_ash_impl.cc @@ -0,0 +1,99 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/media_message_center/media_notification_background_ash_impl.h" + +#include "ui/gfx/canvas.h" +#include "ui/gfx/image/image_skia_operations.h" +#include "ui/gfx/skia_util.h" +#include "ui/views/view.h" + +namespace media_message_center { + +namespace { + +constexpr SkColor kBackgroundColor = SK_ColorTRANSPARENT; +constexpr SkColor kForegroundColor = SK_ColorWHITE; + +constexpr gfx::Size kArtworkSize(80, 80); +constexpr int kArtworkBottomMargin = 16; +constexpr int kArtworkRightMargin = 16; +constexpr int kArtworkCornerRadius = 4; + +gfx::Size ScaleToFitSize(const gfx::Size& image_size) { + if ((image_size.width() > kArtworkSize.width() || + image_size.height() > kArtworkSize.height()) || + (image_size.width() < kArtworkSize.width() && + image_size.height() < kArtworkSize.height())) { + const float scale = std::min( + kArtworkSize.width() / static_cast<float>(image_size.width()), + kArtworkSize.height() / static_cast<float>(image_size.height())); + return gfx::ScaleToFlooredSize(image_size, scale); + } + + return image_size; +} + +} // namespace + +gfx::Rect MediaNotificationBackgroundAshImpl::GetArtworkBounds( + const gfx::Rect& view_bounds) const { + gfx::Size target_size = ScaleToFitSize(artwork_.size()); + + int vertical_offset = (kArtworkSize.height() - target_size.height()) / 2; + int horizontal_offset = (kArtworkSize.width() - target_size.width()) / 2; + + return gfx::Rect(view_bounds.right() - kArtworkRightMargin - + kArtworkSize.width() + horizontal_offset, + view_bounds.bottom() - kArtworkBottomMargin - + kArtworkSize.height() + vertical_offset, + target_size.width(), target_size.height()); +} + +void MediaNotificationBackgroundAshImpl::Paint(gfx::Canvas* canvas, + views::View* view) const { + gfx::Rect source_bounds(0, 0, artwork_.width(), artwork_.height()); + gfx::Rect target_bounds = GetArtworkBounds(view->GetContentsBounds()); + + SkPath path; + path.addRoundRect(gfx::RectToSkRect(target_bounds), kArtworkCornerRadius, + kArtworkCornerRadius); + + canvas->ClipPath(path, true); + + canvas->DrawImageInt( + artwork_, source_bounds.x(), source_bounds.y(), source_bounds.width(), + source_bounds.height(), target_bounds.x(), target_bounds.y(), + target_bounds.width(), target_bounds.height(), false /* filter */); +} + +void MediaNotificationBackgroundAshImpl::UpdateArtwork( + const gfx::ImageSkia& image) { + if (artwork_.BackedBySameObjectAs(image)) + return; + + artwork_ = image; +} + +bool MediaNotificationBackgroundAshImpl::UpdateCornerRadius(int top_radius, + int bottom_radius) { + return false; +} + +bool MediaNotificationBackgroundAshImpl::UpdateArtworkMaxWidthPct( + double max_width_pct) { + return false; +} + +SkColor MediaNotificationBackgroundAshImpl::GetBackgroundColor( + const views::View& owner) const { + return kBackgroundColor; +} + +SkColor MediaNotificationBackgroundAshImpl::GetForegroundColor( + const views::View& owner) const { + return kForegroundColor; +} + +} // namespace media_message_center diff --git a/chromium/components/media_message_center/media_notification_background_ash_impl.h b/chromium/components/media_message_center/media_notification_background_ash_impl.h new file mode 100644 index 00000000000..6742e6f4eff --- /dev/null +++ b/chromium/components/media_message_center/media_notification_background_ash_impl.h @@ -0,0 +1,46 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_ASH_IMPL_H_ +#define COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_ASH_IMPL_H_ + +#include "components/media_message_center/media_notification_background.h" + +#include "base/component_export.h" +#include "ui/gfx/image/image_skia.h" + +namespace media_message_center { + +// MediaNotificationBackground for CrOS media notifications. +class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationBackgroundAshImpl + : public MediaNotificationBackground { + public: + MediaNotificationBackgroundAshImpl() = default; + MediaNotificationBackgroundAshImpl( + const MediaNotificationBackgroundAshImpl&) = delete; + MediaNotificationBackgroundAshImpl& operator=( + const MediaNotificationBackgroundAshImpl&) = delete; + ~MediaNotificationBackgroundAshImpl() override = default; + + // MediaNotificationBackground implementations. + void Paint(gfx::Canvas* canvas, views::View* view) const override; + void UpdateArtwork(const gfx::ImageSkia& image) override; + bool UpdateCornerRadius(int top_radius, int bottom_radius) override; + bool UpdateArtworkMaxWidthPct(double max_width_pct) override; + void UpdateFavicon(const gfx::ImageSkia& icon) override {} + void UpdateDeviceSelectorAvailability(bool availability) override {} + SkColor GetBackgroundColor(const views::View& owner) const override; + SkColor GetForegroundColor(const views::View& owner) const override; + + private: + friend class MediaNotificationBackgroundAshImplTest; + + gfx::Rect GetArtworkBounds(const gfx::Rect& view_bounds) const; + + gfx::ImageSkia artwork_; +}; + +} // namespace media_message_center + +#endif // COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_ASH_IMPL_H_ diff --git a/chromium/components/media_message_center/media_notification_background_ash_impl_unittest.cc b/chromium/components/media_message_center/media_notification_background_ash_impl_unittest.cc new file mode 100644 index 00000000000..87bc0bfe085 --- /dev/null +++ b/chromium/components/media_message_center/media_notification_background_ash_impl_unittest.cc @@ -0,0 +1,50 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/media_message_center/media_notification_background_ash_impl.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace media_message_center { + +class MediaNotificationBackgroundAshImplTest : public testing::Test { + public: + MediaNotificationBackgroundAshImplTest() = default; + ~MediaNotificationBackgroundAshImplTest() override = default; + + void SetUp() override { + background_ = std::make_unique<MediaNotificationBackgroundAshImpl>(); + } + + void TearDown() override { background_.reset(); } + + gfx::ImageSkia CreateTestImage(int width, int height) { + SkBitmap bitmap; + bitmap.allocN32Pixels(width, height); + return gfx::ImageSkia::CreateFrom1xBitmap(bitmap); + } + + gfx::Rect GetArtworkBounds(gfx::Rect view_rect) { + return background_->GetArtworkBounds(view_rect); + } + + MediaNotificationBackgroundAshImpl* background() { return background_.get(); } + + private: + std::unique_ptr<MediaNotificationBackgroundAshImpl> background_; +}; + +TEST_F(MediaNotificationBackgroundAshImplTest, ArtworkBoundsTest) { + gfx::Rect parent_bounds(0, 0, 100, 100); + background()->UpdateArtwork(CreateTestImage(160, 60)); + EXPECT_EQ(GetArtworkBounds(parent_bounds).size(), gfx::Size(80, 30)); + + background()->UpdateArtwork(CreateTestImage(60, 160)); + EXPECT_EQ(GetArtworkBounds(parent_bounds).size(), gfx::Size(30, 80)); + + background()->UpdateArtwork(CreateTestImage(40, 20)); + EXPECT_EQ(GetArtworkBounds(parent_bounds).size(), gfx::Size(80, 40)); +} + +} // namespace media_message_center diff --git a/chromium/components/media_message_center/media_notification_background.cc b/chromium/components/media_message_center/media_notification_background_impl.cc index 97cadb59af3..d937748b84c 100644 --- a/chromium/components/media_message_center/media_notification_background.cc +++ b/chromium/components/media_message_center/media_notification_background_impl.cc @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "components/media_message_center/media_notification_background.h" +#include "components/media_message_center/media_notification_background_impl.h" #include <algorithm> #include <vector> @@ -233,7 +233,7 @@ base::Optional<SkColor> GetNotificationForegroundColor( } // namespace -MediaNotificationBackground::MediaNotificationBackground( +MediaNotificationBackgroundImpl::MediaNotificationBackgroundImpl( int top_radius, int bottom_radius, double artwork_max_width_pct) @@ -241,10 +241,10 @@ MediaNotificationBackground::MediaNotificationBackground( bottom_radius_(bottom_radius), artwork_max_width_pct_(artwork_max_width_pct) {} -MediaNotificationBackground::~MediaNotificationBackground() = default; +MediaNotificationBackgroundImpl::~MediaNotificationBackgroundImpl() = default; -void MediaNotificationBackground::Paint(gfx::Canvas* canvas, - views::View* view) const { +void MediaNotificationBackgroundImpl::Paint(gfx::Canvas* canvas, + views::View* view) const { DCHECK(view); gfx::ScopedCanvas scoped_canvas(canvas); @@ -302,9 +302,29 @@ void MediaNotificationBackground::Paint(gfx::Canvas* canvas, canvas->DrawRect(draw_bounds, flags); } + + if (audio_device_selector_availability_) { + // Draw a gradient to fade the color background of the audio device picker + // and the image together. + gfx::Rect draw_bounds = GetBottomGradientBounds(*view); + + const SkColor colors[2] = { + background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)}; + const SkPoint points[2] = {gfx::PointToSkPoint(draw_bounds.bottom_center()), + gfx::PointToSkPoint(draw_bounds.top_center())}; + + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setStyle(cc::PaintFlags::kFill_Style); + flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr, + 2, SkTileMode::kClamp)); + + canvas->DrawRect(draw_bounds, flags); + } } -void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) { +void MediaNotificationBackgroundImpl::UpdateArtwork( + const gfx::ImageSkia& image) { if (artwork_.BackedBySameObjectAs(image)) return; @@ -313,8 +333,8 @@ void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) { UpdateColorsInternal(); } -bool MediaNotificationBackground::UpdateCornerRadius(int top_radius, - int bottom_radius) { +bool MediaNotificationBackgroundImpl::UpdateCornerRadius(int top_radius, + int bottom_radius) { if (top_radius_ == top_radius && bottom_radius_ == bottom_radius) return false; @@ -323,7 +343,7 @@ bool MediaNotificationBackground::UpdateCornerRadius(int top_radius, return true; } -bool MediaNotificationBackground::UpdateArtworkMaxWidthPct( +bool MediaNotificationBackgroundImpl::UpdateArtworkMaxWidthPct( double max_width_pct) { if (artwork_max_width_pct_ == max_width_pct) return false; @@ -332,7 +352,8 @@ bool MediaNotificationBackground::UpdateArtworkMaxWidthPct( return true; } -void MediaNotificationBackground::UpdateFavicon(const gfx::ImageSkia& icon) { +void MediaNotificationBackgroundImpl::UpdateFavicon( + const gfx::ImageSkia& icon) { if (favicon_.BackedBySameObjectAs(icon)) return; @@ -344,14 +365,22 @@ void MediaNotificationBackground::UpdateFavicon(const gfx::ImageSkia& icon) { UpdateColorsInternal(); } -SkColor MediaNotificationBackground::GetBackgroundColor( +void MediaNotificationBackgroundImpl::UpdateDeviceSelectorAvailability( + bool availability) { + if (audio_device_selector_availability_ == availability) + return; + + audio_device_selector_availability_ = availability; +} + +SkColor MediaNotificationBackgroundImpl::GetBackgroundColor( const views::View& owner) const { if (background_color_.has_value()) return *background_color_; return GetDefaultBackgroundColor(owner); } -SkColor MediaNotificationBackground::GetForegroundColor( +SkColor MediaNotificationBackgroundImpl::GetForegroundColor( const views::View& owner) const { const SkColor foreground = foreground_color_.has_value() @@ -364,7 +393,7 @@ SkColor MediaNotificationBackground::GetForegroundColor( .color; } -int MediaNotificationBackground::GetArtworkWidth( +int MediaNotificationBackgroundImpl::GetArtworkWidth( const gfx::Size& view_size) const { if (artwork_.isNull()) return 0; @@ -375,14 +404,14 @@ int MediaNotificationBackground::GetArtworkWidth( return ceil(view_size.height() * aspect_ratio); } -int MediaNotificationBackground::GetArtworkVisibleWidth( +int MediaNotificationBackgroundImpl::GetArtworkVisibleWidth( const gfx::Size& view_size) const { // The artwork should only take up a maximum percentage of the notification. return std::min(GetArtworkWidth(view_size), (int)ceil(view_size.width() * artwork_max_width_pct_)); } -gfx::Rect MediaNotificationBackground::GetArtworkBounds( +gfx::Rect MediaNotificationBackgroundImpl::GetArtworkBounds( const views::View& owner) const { const gfx::Rect& view_bounds = owner.GetContentsBounds(); int width = GetArtworkWidth(view_bounds.size()); @@ -399,7 +428,7 @@ gfx::Rect MediaNotificationBackground::GetArtworkBounds( view_bounds.height())); } -gfx::Rect MediaNotificationBackground::GetFilledBackgroundBounds( +gfx::Rect MediaNotificationBackgroundImpl::GetFilledBackgroundBounds( const views::View& owner) const { // The filled background should take up the full notification except the area // taken up by the artwork. @@ -409,7 +438,7 @@ gfx::Rect MediaNotificationBackground::GetFilledBackgroundBounds( return owner.GetMirroredRect(bounds); } -gfx::Rect MediaNotificationBackground::GetGradientBounds( +gfx::Rect MediaNotificationBackgroundImpl::GetGradientBounds( const views::View& owner) const { if (artwork_.isNull()) return gfx::Rect(0, 0, 0, 0); @@ -421,25 +450,39 @@ gfx::Rect MediaNotificationBackground::GetGradientBounds( view_bounds.y(), kMediaImageGradientWidth, view_bounds.height())); } -SkPoint MediaNotificationBackground::GetGradientStartPoint( +gfx::Rect MediaNotificationBackgroundImpl::GetBottomGradientBounds( + const views::View& owner) const { + if (artwork_.isNull()) + return gfx::Rect(0, 0, 0, 0); + + const gfx::Rect& view_bounds = owner.GetContentsBounds(); + return owner.GetMirroredRect(gfx::Rect( + gfx::Point( + view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()), + view_bounds.bottom() - kMediaImageGradientWidth), + gfx::Size(GetArtworkVisibleWidth(view_bounds.size()), + kMediaImageGradientWidth))); +} + +SkPoint MediaNotificationBackgroundImpl::GetGradientStartPoint( const gfx::Rect& draw_bounds) const { return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.right_center() : draw_bounds.left_center()); } -SkPoint MediaNotificationBackground::GetGradientEndPoint( +SkPoint MediaNotificationBackgroundImpl::GetGradientEndPoint( const gfx::Rect& draw_bounds) const { return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.left_center() : draw_bounds.right_center()); } -SkColor MediaNotificationBackground::GetDefaultBackgroundColor( +SkColor MediaNotificationBackgroundImpl::GetDefaultBackgroundColor( const views::View& owner) const { return owner.GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_BubbleBackground); } -void MediaNotificationBackground::UpdateColorsInternal() { +void MediaNotificationBackgroundImpl::UpdateColorsInternal() { // If there is an artwork, it should be used. // If there is no artwork, neither a favicon, the artwork bitmap will be used // which is going to be a null bitmap and produce a default value. diff --git a/chromium/components/media_message_center/media_notification_background_impl.h b/chromium/components/media_message_center/media_notification_background_impl.h new file mode 100644 index 00000000000..beb99f826b6 --- /dev/null +++ b/chromium/components/media_message_center/media_notification_background_impl.h @@ -0,0 +1,86 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_IMPL_H_ +#define COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_IMPL_H_ + +#include <vector> + +#include "base/component_export.h" +#include "base/optional.h" +#include "components/media_message_center/media_notification_background.h" +#include "ui/gfx/image/image_skia.h" + +namespace gfx { +class Rect; +class Size; +} // namespace gfx + +namespace views { +class View; +} // namespace views + +namespace media_message_center { + +// MediaNotificationBackground draws a custom background for the media +// notification showing the artwork clipped to a rounded rectangle faded into a +// background color. +class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationBackgroundImpl + : public MediaNotificationBackground { + public: + MediaNotificationBackgroundImpl(int top_radius, + int bottom_radius, + double artwork_max_width_pct); + ~MediaNotificationBackgroundImpl() override; + + // views::Background + void Paint(gfx::Canvas* canvas, views::View* view) const override; + + void UpdateArtwork(const gfx::ImageSkia& image) override; + bool UpdateCornerRadius(int top_radius, int bottom_radius) override; + bool UpdateArtworkMaxWidthPct(double max_width_pct) override; + void UpdateFavicon(const gfx::ImageSkia& icon) override; + void UpdateDeviceSelectorAvailability(bool availability) override; + + SkColor GetBackgroundColor(const views::View& owner) const override; + SkColor GetForegroundColor(const views::View& owner) const override; + + private: + friend class MediaNotificationBackgroundImplTest; + friend class MediaNotificationViewImplTest; + friend class MediaNotificationViewModernImplTest; + FRIEND_TEST_ALL_PREFIXES(MediaNotificationBackgroundImplRTLTest, + BoundsSanityCheck); + + // Shade factor used on favicon dominant color before set as background color. + static constexpr double kBackgroundFaviconColorShadeFactor = 0.35; + + int GetArtworkWidth(const gfx::Size& view_size) const; + int GetArtworkVisibleWidth(const gfx::Size& view_size) const; + gfx::Rect GetArtworkBounds(const views::View& owner) const; + gfx::Rect GetFilledBackgroundBounds(const views::View& owner) const; + gfx::Rect GetGradientBounds(const views::View& owner) const; + gfx::Rect GetBottomGradientBounds(const views::View& owner) const; + SkPoint GetGradientStartPoint(const gfx::Rect& draw_bounds) const; + SkPoint GetGradientEndPoint(const gfx::Rect& draw_bounds) const; + SkColor GetDefaultBackgroundColor(const views::View& owner) const; + void UpdateColorsInternal(); + + int top_radius_; + int bottom_radius_; + + gfx::ImageSkia favicon_; + gfx::ImageSkia artwork_; + double artwork_max_width_pct_; + bool audio_device_selector_availability_ = false; + + base::Optional<SkColor> background_color_; + base::Optional<SkColor> foreground_color_; + + DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackgroundImpl); +}; + +} // namespace media_message_center + +#endif // COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_BACKGROUND_IMPL_H_ diff --git a/chromium/components/media_message_center/media_notification_background_unittest.cc b/chromium/components/media_message_center/media_notification_background_impl_unittest.cc index 1374a9a09e1..75213a3147f 100644 --- a/chromium/components/media_message_center/media_notification_background_unittest.cc +++ b/chromium/components/media_message_center/media_notification_background_impl_unittest.cc @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "components/media_message_center/media_notification_background.h" +#include "components/media_message_center/media_notification_background_impl.h" #include <memory> @@ -84,22 +84,23 @@ gfx::ImageSkia CreateTestBackgroundImage(SkColor color) { } // namespace -class MediaNotificationBackgroundTest : public testing::Test { +class MediaNotificationBackgroundImplTest : public testing::Test { public: - MediaNotificationBackgroundTest() = default; - ~MediaNotificationBackgroundTest() override = default; + MediaNotificationBackgroundImplTest() = default; + ~MediaNotificationBackgroundImplTest() override = default; void SetUp() override { - background_ = std::make_unique<MediaNotificationBackground>(10, 10, 0.1); + background_ = + std::make_unique<MediaNotificationBackgroundImpl>(10, 10, 0.1); EXPECT_FALSE(GetBackgroundColor().has_value()); } - void TearDown() override { - background_.reset(); - } + void TearDown() override { background_.reset(); } - MediaNotificationBackground* background() const { return background_.get(); } + MediaNotificationBackgroundImpl* background() const { + return background_.get(); + } base::Optional<SkColor> GetBackgroundColor() const { return background_->background_color_; @@ -110,38 +111,39 @@ class MediaNotificationBackgroundTest : public testing::Test { } double GetBackgroundFaviconColorShadeFactor() const { - return MediaNotificationBackground::kBackgroundFaviconColorShadeFactor; + return MediaNotificationBackgroundImpl::kBackgroundFaviconColorShadeFactor; } private: - std::unique_ptr<MediaNotificationBackground> background_; + std::unique_ptr<MediaNotificationBackgroundImpl> background_; - DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackgroundTest); + DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackgroundImplTest); }; // If we have no artwork then we should use the default background color. -TEST_F(MediaNotificationBackgroundTest, DeriveBackgroundColor_NoArtwork) { +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_NoArtwork) { background()->UpdateArtwork(gfx::ImageSkia()); EXPECT_FALSE(GetBackgroundColor().has_value()); } // If we have artwork with no popular color then we should use the default // background color. -TEST_F(MediaNotificationBackgroundTest, DeriveBackgroundColor_NoPopularColor) { +TEST_F(MediaNotificationBackgroundImplTest, + DeriveBackgroundColor_NoPopularColor) { background()->UpdateArtwork(CreateTestBackgroundImage(SK_ColorTRANSPARENT)); EXPECT_FALSE(GetBackgroundColor().has_value()); } // If the most popular color is not white or black then we should use that // color. -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_PopularNonWhiteBlackColor) { constexpr SkColor kTestColor = SK_ColorYELLOW; background()->UpdateArtwork(CreateTestBackgroundImage(kTestColor)); EXPECT_EQ(kTestColor, GetBackgroundColor()); } -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_NoArtworkAfterHavingOne) { constexpr SkColor kTestColor = SK_ColorYELLOW; background()->UpdateArtwork(CreateTestBackgroundImage(kTestColor)); @@ -152,7 +154,7 @@ TEST_F(MediaNotificationBackgroundTest, } // Favicons should be used when available but have a shade applying to them. -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_PopularNonWhiteBlackColorFavicon) { constexpr SkColor kTestColor = SK_ColorYELLOW; background()->UpdateFavicon(CreateTestBackgroundImage(kTestColor)); @@ -163,7 +165,7 @@ TEST_F(MediaNotificationBackgroundTest, EXPECT_EQ(expected_color, GetBackgroundColor()); } -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_NoFaviconAfterHavingOne) { constexpr SkColor kTestColor = SK_ColorYELLOW; background()->UpdateFavicon(CreateTestBackgroundImage(kTestColor)); @@ -177,7 +179,7 @@ TEST_F(MediaNotificationBackgroundTest, EXPECT_FALSE(GetBackgroundColor().has_value()); } -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_FaviconSetThenArtwork) { constexpr SkColor kArtworkColor = SK_ColorYELLOW; constexpr SkColor kFaviconColor = SK_ColorRED; @@ -188,7 +190,7 @@ TEST_F(MediaNotificationBackgroundTest, EXPECT_EQ(kArtworkColor, GetBackgroundColor()); } -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_ArtworkSetThenFavicon) { constexpr SkColor kArtworkColor = SK_ColorYELLOW; constexpr SkColor kFaviconColor = SK_ColorRED; @@ -199,7 +201,7 @@ TEST_F(MediaNotificationBackgroundTest, EXPECT_EQ(kArtworkColor, GetBackgroundColor()); } -TEST_F(MediaNotificationBackgroundTest, +TEST_F(MediaNotificationBackgroundImplTest, DeriveBackgroundColor_SetAndRemoveArtworkWithFavicon) { constexpr SkColor kArtworkColor = SK_ColorYELLOW; constexpr SkColor kFaviconColor = SK_ColorRED; @@ -215,17 +217,17 @@ TEST_F(MediaNotificationBackgroundTest, EXPECT_EQ(expected_color, GetBackgroundColor()); } -TEST_F(MediaNotificationBackgroundTest, GetBackgroundColorRespectsTheme) { +TEST_F(MediaNotificationBackgroundImplTest, GetBackgroundColorRespectsTheme) { TestDarkTheme dark_theme; views::View owner; owner.SetNativeThemeForTesting(&dark_theme); EXPECT_EQ(kDarkBackgroundColor, background()->GetBackgroundColor(owner)); } -// MediaNotificationBackgroundBlackWhiteTest will repeat these tests with a +// MediaNotificationBackgroundImplBlackWhiteTest will repeat these tests with a // parameter that is either black or white. -class MediaNotificationBackgroundBlackWhiteTest - : public MediaNotificationBackgroundTest, +class MediaNotificationBackgroundImplBlackWhiteTest + : public MediaNotificationBackgroundImplTest, public testing::WithParamInterface<SkColor> { public: bool IsBlack() const { return GetParam() == SK_ColorBLACK; } @@ -251,12 +253,12 @@ class MediaNotificationBackgroundBlackWhiteTest }; INSTANTIATE_TEST_SUITE_P(All, - MediaNotificationBackgroundBlackWhiteTest, + MediaNotificationBackgroundImplBlackWhiteTest, testing::Values(SK_ColorBLACK, SK_ColorWHITE)); // If the most popular color is black or white but there is no secondary color // we should use the most popular color. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveBackgroundColor_PopularBlackWhiteNoSecondaryColor) { background()->UpdateArtwork(CreateTestBackgroundImage(GetParam())); EXPECT_EQ(GetParam(), GetBackgroundColor()); @@ -264,7 +266,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If the most popular color is black or white and there is a secondary color // that is very minor then we should use the most popular color. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveBackgroundColor_VeryPopularBlackWhite) { background()->UpdateArtwork( CreateTestBackgroundImage(GetParam(), SK_ColorYELLOW, 20)); @@ -273,7 +275,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If the most popular color is black or white but it is not that popular then // we should use the secondary color. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveBackgroundColor_NotVeryPopularBlackWhite) { constexpr SkColor kTestColor = SK_ColorYELLOW; background()->UpdateArtwork( @@ -283,7 +285,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If there are multiple vibrant colors then the foreground color should be the // most popular one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_MultiVibrant) { const SkColor kTestColor = SK_ColorCYAN; @@ -297,7 +299,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If there is a vibrant and muted color then the foreground color should be the // more vibrant one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_Vibrant) { const SkColor kTestColor = GetColorFromSL(kVibrantSaturation, kNormalLuma); @@ -311,7 +313,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If there are multiple muted colors then the foreground color should be the // most popular one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_MultiMuted) { const SkColor kTestColor = GetColorFromSL(kMutedSaturation, kNormalLuma); @@ -325,7 +327,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If there is a normal and light muted color then the foreground color should // be the normal one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_Muted) { const SkColor kTestColor = GetColorFromSL(kMutedSaturation, kNormalLuma); const SkColor kSecondColor = @@ -340,7 +342,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If the best color is not the most popular one, but the most popular one is // not that popular then we should use the best color. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_NotPopular) { const SkColor kTestColor = SK_ColorMAGENTA; @@ -354,7 +356,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If we do not have a best color but we have a popular one over a threshold // then we should use that one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_MostPopular) { const SkColor kTestColor = GetColorFromSL(kMutedSaturation, kNormalLuma); @@ -367,7 +369,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If the background color is dark then we should select for a lighter color, // otherwise we should select for a darker one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_MoreVibrant) { const SkColor kTestColor = GetColorFromSL(kVibrantSaturation, IsBlack() ? kLightLuma : kDarkLuma); @@ -382,7 +384,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If the background color is dark then we should select for a lighter color, // otherwise we should select for a darker one. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Palette_MoreMuted) { const SkColor kTestColor = GetColorFromSL(kMutedSaturation, IsBlack() ? kLightLuma : kDarkLuma); @@ -398,7 +400,7 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, // If we do not have any colors then we should use the fallback color based on // the background color. -TEST_P(MediaNotificationBackgroundBlackWhiteTest, +TEST_P(MediaNotificationBackgroundImplBlackWhiteTest, DeriveForegroundColor_Fallback) { background()->UpdateArtwork(CreateTestForegroundArtwork( SK_ColorTRANSPARENT, SK_ColorTRANSPARENT, 0, 0)); @@ -408,10 +410,10 @@ TEST_P(MediaNotificationBackgroundBlackWhiteTest, GetForegroundColor()); } -// MediaNotificationBackgroundRTLTest will repeat these tests with RTL disabled -// and enabled. -class MediaNotificationBackgroundRTLTest - : public MediaNotificationBackgroundTest, +// MediaNotificationBackgroundImplRTLTest will repeat these tests with RTL +// disabled and enabled. +class MediaNotificationBackgroundImplRTLTest + : public MediaNotificationBackgroundImplTest, public testing::WithParamInterface<bool> { public: void SetUp() override { @@ -419,7 +421,7 @@ class MediaNotificationBackgroundRTLTest switches::kForceUIDirection, GetParam() ? switches::kForceDirectionRTL : switches::kForceDirectionLTR); - MediaNotificationBackgroundTest::SetUp(); + MediaNotificationBackgroundImplTest::SetUp(); ASSERT_EQ(IsRTL(), base::i18n::IsRTL()); } @@ -432,10 +434,10 @@ class MediaNotificationBackgroundRTLTest }; INSTANTIATE_TEST_SUITE_P(All, - MediaNotificationBackgroundRTLTest, + MediaNotificationBackgroundImplRTLTest, testing::Bool()); -TEST_P(MediaNotificationBackgroundRTLTest, BoundsSanityCheck) { +TEST_P(MediaNotificationBackgroundImplRTLTest, BoundsSanityCheck) { // The test notification will have a width of 200 and a height of 50. gfx::Rect bounds(0, 0, 200, 50); auto owner = std::make_unique<views::StaticSizedView>(); diff --git a/chromium/components/media_message_center/media_notification_view.h b/chromium/components/media_message_center/media_notification_view.h index 96aa7421f67..996ad77a930 100644 --- a/chromium/components/media_message_center/media_notification_view.h +++ b/chromium/components/media_message_center/media_notification_view.h @@ -45,6 +45,7 @@ class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationView // Sets the icon to be displayed in the notification's header section. // |vector_icon| must outlive the MediaNotificationView. virtual void UpdateWithVectorIcon(const gfx::VectorIcon& vector_icon) = 0; + virtual void UpdateDeviceSelectorAvailability(bool availability) = 0; }; } // namespace media_message_center diff --git a/chromium/components/media_message_center/media_notification_view_impl.cc b/chromium/components/media_message_center/media_notification_view_impl.cc index f571def9c92..a03909f33e5 100644 --- a/chromium/components/media_message_center/media_notification_view_impl.cc +++ b/chromium/components/media_message_center/media_notification_view_impl.cc @@ -6,7 +6,8 @@ #include "base/metrics/histogram_macros.h" #include "base/stl_util.h" -#include "components/media_message_center/media_notification_background.h" +#include "components/media_message_center/media_notification_background_ash_impl.h" +#include "components/media_message_center/media_notification_background_impl.h" #include "components/media_message_center/media_notification_constants.h" #include "components/media_message_center/media_notification_container.h" #include "components/media_message_center/media_notification_item.h" @@ -81,6 +82,7 @@ const gfx::VectorIcon* GetVectorIconForMediaAction(MediaSessionAction action) { case MediaSessionAction::kSkipAd: case MediaSessionAction::kSeekTo: case MediaSessionAction::kScrubTo: + case MediaSessionAction::kSwitchAudioDevice: NOTREACHED(); break; } @@ -109,7 +111,8 @@ MediaNotificationViewImpl::MediaNotificationViewImpl( std::unique_ptr<views::View> header_row_controls_view, const base::string16& default_app_name, int notification_width, - bool should_show_icon) + bool should_show_icon, + BackgroundStyle background_style) : container_(container), item_(std::move(item)), default_app_name_(default_app_name), @@ -264,9 +267,13 @@ MediaNotificationViewImpl::MediaNotificationViewImpl( picture_in_picture_button_ = button_row_->AddChildView(std::move(picture_in_picture_button)); - SetBackground(std::make_unique<MediaNotificationBackground>( - message_center::kNotificationCornerRadius, - message_center::kNotificationCornerRadius, kMediaImageMaxWidthPct)); + if (background_style == BackgroundStyle::kAshStyle) { + SetBackground(std::make_unique<MediaNotificationBackgroundAshImpl>()); + } else { + SetBackground(std::make_unique<MediaNotificationBackgroundImpl>( + message_center::kNotificationCornerRadius, + message_center::kNotificationCornerRadius, kMediaImageMaxWidthPct)); + } UpdateCornerRadius(message_center::kNotificationCornerRadius, message_center::kNotificationCornerRadius); @@ -469,6 +476,12 @@ void MediaNotificationViewImpl::UpdateWithVectorIcon( kIconMediaNotificationHeaderInsets); } +void MediaNotificationViewImpl::UpdateDeviceSelectorAvailability( + bool availability) { + GetMediaNotificationBackground()->UpdateDeviceSelectorAvailability( + availability); +} + void MediaNotificationViewImpl::OnThemeChanged() { MediaNotificationView::OnThemeChanged(); UpdateForegroundColor(); diff --git a/chromium/components/media_message_center/media_notification_view_impl.h b/chromium/components/media_message_center/media_notification_view_impl.h index 0ebcf0f238a..4c8eaeda792 100644 --- a/chromium/components/media_message_center/media_notification_view_impl.h +++ b/chromium/components/media_message_center/media_notification_view_impl.h @@ -52,13 +52,20 @@ class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationViewImpl kMaxValue = kSource, }; + // Allow MediaNotificationViewImpl show different styled background. + enum class BackgroundStyle { + kDefault, + kAshStyle, + }; + MediaNotificationViewImpl( MediaNotificationContainer* container, base::WeakPtr<MediaNotificationItem> item, std::unique_ptr<views::View> header_row_controls_view, const base::string16& default_app_name, int notification_width, - bool should_show_icon); + bool should_show_icon, + BackgroundStyle background_style = BackgroundStyle::kDefault); ~MediaNotificationViewImpl() override; // views::View: @@ -81,6 +88,8 @@ class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationViewImpl void UpdateWithMediaArtwork(const gfx::ImageSkia& image) override; void UpdateWithFavicon(const gfx::ImageSkia& icon) override; void UpdateWithVectorIcon(const gfx::VectorIcon& vector_icon) override; + void UpdateDeviceSelectorAvailability(bool availability) override; + void OnThemeChanged() override; const views::Label* title_label_for_testing() const { return title_label_; } diff --git a/chromium/components/media_message_center/media_notification_view_impl_unittest.cc b/chromium/components/media_message_center/media_notification_view_impl_unittest.cc index 6d11c7d55f9..1b9b81789bc 100644 --- a/chromium/components/media_message_center/media_notification_view_impl_unittest.cc +++ b/chromium/components/media_message_center/media_notification_view_impl_unittest.cc @@ -16,7 +16,7 @@ #include "base/test/task_environment.h" #include "base/unguessable_token.h" #include "build/build_config.h" -#include "components/media_message_center/media_notification_background.h" +#include "components/media_message_center/media_notification_background_impl.h" #include "components/media_message_center/media_notification_constants.h" #include "components/media_message_center/media_notification_container.h" #include "components/media_message_center/media_notification_controller.h" @@ -68,15 +68,16 @@ class MockMediaNotificationController : public MediaNotificationController { ~MockMediaNotificationController() = default; // MediaNotificationController implementation. - MOCK_METHOD1(ShowNotification, void(const std::string& id)); - MOCK_METHOD1(HideNotification, void(const std::string& id)); - MOCK_METHOD1(RemoveItem, void(const std::string& id)); + MOCK_METHOD(void, ShowNotification, (const std::string& id)); + MOCK_METHOD(void, HideNotification, (const std::string& id)); + MOCK_METHOD(void, RemoveItem, (const std::string& id)); scoped_refptr<base::SequencedTaskRunner> GetTaskRunner() const override { return nullptr; } - MOCK_METHOD2(LogMediaSessionActionButtonPressed, - void(const std::string& id, - media_session::mojom::MediaSessionAction action)); + MOCK_METHOD(void, + LogMediaSessionActionButtonPressed, + (const std::string& id, + media_session::mojom::MediaSessionAction action)); private: DISALLOW_COPY_AND_ASSIGN(MockMediaNotificationController); @@ -247,7 +248,9 @@ class MediaNotificationViewImplTest : public views::ViewsTestBase { MediaSessionNotificationItem* GetItem() const { return item_.get(); } const gfx::ImageSkia& GetArtworkImage() const { - return view()->GetMediaNotificationBackground()->artwork_; + return static_cast<MediaNotificationBackgroundImpl*>( + view()->GetMediaNotificationBackground()) + ->artwork_; } const gfx::ImageSkia& GetAppIcon() const { @@ -539,7 +542,7 @@ TEST_F(MAYBE_MediaNotificationViewImplTest, PlayToggle_FromObserver_Empty) { views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( GetButtonForAction(MediaSessionAction::kPlay)); ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); - EXPECT_FALSE(button->toggled()); + EXPECT_FALSE(button->GetToggled()); } view()->UpdateWithMediaSessionInfo( @@ -549,7 +552,7 @@ TEST_F(MAYBE_MediaNotificationViewImplTest, PlayToggle_FromObserver_Empty) { views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( GetButtonForAction(MediaSessionAction::kPlay)); ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); - EXPECT_FALSE(button->toggled()); + EXPECT_FALSE(button->GetToggled()); } } @@ -562,7 +565,7 @@ TEST_F(MAYBE_MediaNotificationViewImplTest, views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( GetButtonForAction(MediaSessionAction::kPlay)); ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); - EXPECT_FALSE(button->toggled()); + EXPECT_FALSE(button->GetToggled()); } media_session::mojom::MediaSessionInfoPtr session_info( @@ -576,7 +579,7 @@ TEST_F(MAYBE_MediaNotificationViewImplTest, views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( GetButtonForAction(MediaSessionAction::kPause)); ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); - EXPECT_TRUE(button->toggled()); + EXPECT_TRUE(button->GetToggled()); } session_info->playback_state = @@ -587,7 +590,7 @@ TEST_F(MAYBE_MediaNotificationViewImplTest, views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( GetButtonForAction(MediaSessionAction::kPlay)); ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); - EXPECT_FALSE(button->toggled()); + EXPECT_FALSE(button->GetToggled()); } } diff --git a/chromium/components/media_message_center/media_notification_view_modern_impl.cc b/chromium/components/media_message_center/media_notification_view_modern_impl.cc new file mode 100644 index 00000000000..16fa2155197 --- /dev/null +++ b/chromium/components/media_message_center/media_notification_view_modern_impl.cc @@ -0,0 +1,595 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/media_message_center/media_notification_view_modern_impl.h" + +#include "base/metrics/histogram_macros.h" +#include "base/stl_util.h" +#include "components/media_message_center/media_notification_background_impl.h" +#include "components/media_message_center/media_notification_constants.h" +#include "components/media_message_center/media_notification_container.h" +#include "components/media_message_center/media_notification_item.h" +#include "components/media_message_center/media_notification_util.h" +#include "components/media_message_center/vector_icons/vector_icons.h" +#include "components/strings/grit/components_strings.h" +#include "services/media_session/public/mojom/media_session.mojom.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/font.h" +#include "ui/gfx/font_list.h" +#include "ui/gfx/paint_vector_icon.h" +#include "ui/message_center/public/cpp/message_center_constants.h" +#include "ui/views/border.h" +#include "ui/views/controls/button/image_button_factory.h" +#include "ui/views/layout/box_layout.h" +#include "ui/views/style/typography.h" +#include "ui/views/view_class_properties.h" + +namespace media_message_center { + +using media_session::mojom::MediaSessionAction; + +namespace { + +constexpr gfx::Size kMediaNotificationViewBaseSize = {350, 175}; +constexpr gfx::Size kInfoContainerSize = { + kMediaNotificationViewBaseSize.width(), 100}; +constexpr gfx::Insets kArtworkContainerBorderInsets = {15, 10, 15, 0}; +constexpr gfx::Size kArtworkContainerSize = {100, kInfoContainerSize.height()}; +constexpr int kArtworkVignetteCornerRadius = 5; +constexpr gfx::Size kLabelsContainerBaseSize = { + kMediaNotificationViewBaseSize.width() - kArtworkContainerSize.width(), + kInfoContainerSize.height()}; +constexpr gfx::Insets kLabelsContainerBorderInsets = {15, 10, 0, 0}; +constexpr gfx::Size kPipSeparatorSize = {1, 14}; +constexpr gfx::Size kPipButtonSize = {20, 20}; +constexpr int kPipButtonIconSize = 18; +constexpr gfx::Size kNotificationControlsSpacerSize = {100, 1}; +constexpr gfx::Insets kNotificationControlsInsets = {5, 0, 0, 5}; +constexpr gfx::Size kMediaControlsContainerSize = {350, 75}; +constexpr int kMediaControlsButtonSpacing = 16; +constexpr gfx::Insets kMediaControlsBorderInsets = {0, 0, 10, 0}; + +constexpr int kTitleArtistLineHeight = 20; +constexpr gfx::Size kMediaButtonSize = gfx::Size(36, 36); +constexpr int kMediaButtonIconSize = 20; + +// An image view with a rounded rectangle vignette +class MediaArtworkView : public views::ImageView { + public: + explicit MediaArtworkView(float corner_radius) + : corner_radius_(corner_radius) {} + + void SetVignetteColor(const SkColor& vignette_color) { + vignette_color_ = vignette_color; + } + + // ImageView + void OnPaint(gfx::Canvas* canvas) override; + + private: + SkColor vignette_color_; + float corner_radius_; +}; + +void MediaArtworkView::OnPaint(gfx::Canvas* canvas) { + views::ImageView::OnPaint(canvas); + auto path = SkPath().addRoundRect(RectToSkRect(GetLocalBounds()), + corner_radius_, corner_radius_); + path.toggleInverseFillType(); + cc::PaintFlags paint_flags; + paint_flags.setStyle(cc::PaintFlags::kFill_Style); + paint_flags.setAntiAlias(true); + paint_flags.setColor(vignette_color_); + canvas->DrawPath(path, paint_flags); +} + +void RecordMetadataHistogram( + MediaNotificationViewModernImpl::Metadata metadata) { + UMA_HISTOGRAM_ENUMERATION( + MediaNotificationViewModernImpl::kMetadataHistogramName, metadata); +} + +const gfx::VectorIcon* GetVectorIconForMediaAction(MediaSessionAction action) { + switch (action) { + case MediaSessionAction::kPreviousTrack: + return &kMediaPreviousTrackIcon; + case MediaSessionAction::kSeekBackward: + return &kMediaSeekBackwardIcon; + case MediaSessionAction::kPlay: + return &kPlayArrowIcon; + case MediaSessionAction::kPause: + return &kPauseIcon; + case MediaSessionAction::kSeekForward: + return &kMediaSeekForwardIcon; + case MediaSessionAction::kNextTrack: + return &kMediaNextTrackIcon; + case MediaSessionAction::kEnterPictureInPicture: + return &kMediaEnterPipIcon; + case MediaSessionAction::kExitPictureInPicture: + return &kMediaExitPipIcon; + case MediaSessionAction::kStop: + case MediaSessionAction::kSkipAd: + case MediaSessionAction::kSeekTo: + case MediaSessionAction::kScrubTo: + case MediaSessionAction::kSwitchAudioDevice: + NOTREACHED(); + break; + } + + return nullptr; +} + +} // anonymous namespace + +// static +const char MediaNotificationViewModernImpl::kArtworkHistogramName[] = + "Media.Notification.ArtworkPresent"; + +// static +const char MediaNotificationViewModernImpl::kMetadataHistogramName[] = + "Media.Notification.MetadataPresent"; + +MediaNotificationViewModernImpl::MediaNotificationViewModernImpl( + MediaNotificationContainer* container, + base::WeakPtr<MediaNotificationItem> item, + std::unique_ptr<views::View> notification_controls_view, + int notification_width) + : container_(container), item_(std::move(item)) { + DCHECK(container_); + + SetPreferredSize(kMediaNotificationViewBaseSize); + + DCHECK(notification_width >= kMediaNotificationViewBaseSize.width()) + << "MediaNotificationViewModernImpl expects a width of at least " + << kMediaNotificationViewBaseSize.width(); + auto border_insets = gfx::Insets( + 0, (kMediaNotificationViewBaseSize.width() - notification_width) / 2); + SetBorder(views::CreateEmptyBorder(border_insets)); + + SetLayoutManager(std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0)); + + SetBackground(std::make_unique<MediaNotificationBackgroundImpl>( + message_center::kNotificationCornerRadius, + message_center::kNotificationCornerRadius, 0)); + + UpdateCornerRadius(message_center::kNotificationCornerRadius, + message_center::kNotificationCornerRadius); + + { + // The info container contains the notification artwork, the labels for the + // title and artist text, the picture in picture button, and the dismiss + // button. + auto info_container = std::make_unique<views::View>(); + info_container->SetPreferredSize(kInfoContainerSize); + + auto* info_container_layout = + info_container->SetLayoutManager(std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), 0)); + info_container_layout->set_cross_axis_alignment( + views::BoxLayout::CrossAxisAlignment::kStart); + + { + auto artwork_container = std::make_unique<views::View>(); + artwork_container->SetBorder( + views::CreateEmptyBorder(kArtworkContainerBorderInsets)); + artwork_container->SetPreferredSize(kArtworkContainerSize); + + // The artwork container will become visible once artwork has been set in + // UpdateWithMediaArtwork + artwork_container->SetVisible(false); + + auto* artwork_container_layout = artwork_container->SetLayoutManager( + std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), 0)); + artwork_container_layout->set_main_axis_alignment( + views::BoxLayout::MainAxisAlignment::kCenter); + artwork_container_layout->set_cross_axis_alignment( + views::BoxLayout::CrossAxisAlignment::kCenter); + + { + auto artwork = + std::make_unique<MediaArtworkView>(kArtworkVignetteCornerRadius); + artwork_ = artwork_container->AddChildView(std::move(artwork)); + } + + artwork_container_ = + info_container->AddChildView(std::move(artwork_container)); + } + + { + auto labels_container = std::make_unique<views::View>(); + + labels_container->SetPreferredSize( + {kLabelsContainerBaseSize.width() - + (notification_controls_view->GetPreferredSize().width() + + kNotificationControlsInsets.width()), + kLabelsContainerBaseSize.height()}); + labels_container->SetBorder( + views::CreateEmptyBorder(kLabelsContainerBorderInsets)); + + auto* labels_container_layout_manager = + labels_container->SetLayoutManager(std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0)); + labels_container_layout_manager->set_main_axis_alignment( + views::BoxLayout::MainAxisAlignment::kStart); + labels_container_layout_manager->set_cross_axis_alignment( + views::BoxLayout::CrossAxisAlignment::kStart); + + { + auto title_label = std::make_unique<views::Label>( + base::EmptyString16(), views::style::CONTEXT_LABEL, + views::style::STYLE_PRIMARY); + title_label->SetLineHeight(kTitleArtistLineHeight); + title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); + title_label_ = labels_container->AddChildView(std::move(title_label)); + } + + { + auto subtitle_label = std::make_unique<views::Label>( + base::EmptyString16(), views::style::CONTEXT_LABEL, + views::style::STYLE_SECONDARY); + subtitle_label->SetLineHeight(18); + subtitle_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); + subtitle_label_ = + labels_container->AddChildView(std::move(subtitle_label)); + } + + { + // Put a vertical spacer between the labels and the pip button. + auto spacer = std::make_unique<views::View>(); + spacer->SetPreferredSize(kPipSeparatorSize); + labels_container->AddChildView(std::move(spacer)); + } + + { + // The picture-in-picture button appears directly under the media + // labels. + auto picture_in_picture_button = + views::CreateVectorToggleImageButton(this); + picture_in_picture_button->set_tag( + static_cast<int>(MediaSessionAction::kEnterPictureInPicture)); + picture_in_picture_button->SetPreferredSize(kPipButtonSize); + picture_in_picture_button->SetFocusBehavior( + views::View::FocusBehavior::ALWAYS); + picture_in_picture_button->SetTooltipText(l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_ENTER_PIP)); + picture_in_picture_button->SetToggledTooltipText( + l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_EXIT_PIP)); + picture_in_picture_button->EnableCanvasFlippingForRTLUI(false); + views::SetImageFromVectorIconWithColor( + picture_in_picture_button.get(), + *GetVectorIconForMediaAction( + MediaSessionAction::kEnterPictureInPicture), + kPipButtonIconSize, SK_ColorBLACK); + picture_in_picture_button_ = labels_container->AddChildView( + std::move(picture_in_picture_button)); + } + + info_container->AddChildView(std::move(labels_container)); + } + + { + // If there is no artwork to display, a vertical spacer should be added + // between the labels container and the dismiss button. + auto notification_controls_spacer = std::make_unique<views::View>(); + notification_controls_spacer->SetPreferredSize( + kNotificationControlsSpacerSize); + notification_controls_spacer_ = + info_container->AddChildView(std::move(notification_controls_spacer)); + } + + notification_controls_view->SetProperty(views::kMarginsKey, + kNotificationControlsInsets); + info_container->AddChildView(std::move(notification_controls_view)); + + AddChildView(std::move(info_container)); + } + + { + // The media controls container contains buttons for media playback. This + // includes play/pause, fast-forward/rewind, and skip controls. + auto media_controls_container = std::make_unique<views::View>(); + media_controls_container->SetPreferredSize(kMediaControlsContainerSize); + auto* media_controls_layout = media_controls_container->SetLayoutManager( + std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), + kMediaControlsButtonSpacing)); + media_controls_layout->set_cross_axis_alignment( + views::BoxLayout::CrossAxisAlignment::kCenter); + media_controls_layout->set_main_axis_alignment( + views::BoxLayout::MainAxisAlignment::kCenter); + media_controls_container->SetBorder( + views::CreateEmptyBorder(kMediaControlsBorderInsets)); + + // Media controls should always be presented left-to-right, + // regardless of the local UI direction. + media_controls_container->SetMirrored(false); + + CreateMediaButton( + media_controls_container.get(), MediaSessionAction::kPreviousTrack, + l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_PREVIOUS_TRACK)); + CreateMediaButton( + media_controls_container.get(), MediaSessionAction::kSeekBackward, + l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_SEEK_BACKWARD)); + + { + auto play_pause_button = views::CreateVectorToggleImageButton(this); + play_pause_button->set_tag(static_cast<int>(MediaSessionAction::kPlay)); + play_pause_button->SetPreferredSize(kMediaButtonSize); + play_pause_button->SetFocusBehavior(views::View::FocusBehavior::ALWAYS); + play_pause_button->SetTooltipText(l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_PLAY)); + play_pause_button->SetToggledTooltipText(l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_PAUSE)); + play_pause_button->EnableCanvasFlippingForRTLUI(false); + play_pause_button_ = + media_controls_container->AddChildView(std::move(play_pause_button)); + } + + CreateMediaButton( + media_controls_container.get(), MediaSessionAction::kSeekForward, + l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_SEEK_FORWARD)); + CreateMediaButton( + media_controls_container.get(), MediaSessionAction::kNextTrack, + l10n_util::GetStringUTF16( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACTION_NEXT_TRACK)); + + media_controls_container_ = + AddChildView(std::move(media_controls_container)); + } + + if (item_) + item_->SetView(this); +} + +MediaNotificationViewModernImpl::~MediaNotificationViewModernImpl() { + if (item_) + item_->SetView(nullptr); +} + +void MediaNotificationViewModernImpl::UpdateCornerRadius(int top_radius, + int bottom_radius) { + if (GetMediaNotificationBackground()->UpdateCornerRadius(top_radius, + bottom_radius)) { + SchedulePaint(); + } +} + +void MediaNotificationViewModernImpl::GetAccessibleNodeData( + ui::AXNodeData* node_data) { + node_data->role = ax::mojom::Role::kListItem; + node_data->AddStringAttribute( + ax::mojom::StringAttribute::kRoleDescription, + l10n_util::GetStringUTF8( + IDS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_ACCESSIBLE_NAME)); + + if (!accessible_name_.empty()) + node_data->SetName(accessible_name_); +} + +void MediaNotificationViewModernImpl::ButtonPressed(views::Button* sender, + const ui::Event& event) { + if (item_) { + item_->OnMediaSessionActionButtonPressed(GetActionFromButtonTag(*sender)); + } +} + +void MediaNotificationViewModernImpl::UpdateWithMediaSessionInfo( + const media_session::mojom::MediaSessionInfoPtr& session_info) { + bool playing = + session_info && session_info->playback_state == + media_session::mojom::MediaPlaybackState::kPlaying; + play_pause_button_->SetToggled(playing); + + MediaSessionAction action = + playing ? MediaSessionAction::kPause : MediaSessionAction::kPlay; + play_pause_button_->set_tag(static_cast<int>(action)); + + bool in_picture_in_picture = + session_info && + session_info->picture_in_picture_state == + media_session::mojom::MediaPictureInPictureState::kInPictureInPicture; + picture_in_picture_button_->SetToggled(in_picture_in_picture); + + action = in_picture_in_picture ? MediaSessionAction::kExitPictureInPicture + : MediaSessionAction::kEnterPictureInPicture; + picture_in_picture_button_->set_tag(static_cast<int>(action)); + + UpdateActionButtonsVisibility(); + + container_->OnMediaSessionInfoChanged(session_info); + + PreferredSizeChanged(); + Layout(); + SchedulePaint(); +} + +void MediaNotificationViewModernImpl::UpdateWithMediaMetadata( + const media_session::MediaMetadata& metadata) { + title_label_->SetText(metadata.title); + subtitle_label_->SetText(metadata.source_title); + + accessible_name_ = GetAccessibleNameFromMetadata(metadata); + + // The title label should only be a11y-focusable when there is text to be + // read. + if (metadata.title.empty()) { + title_label_->SetFocusBehavior(FocusBehavior::NEVER); + } else { + title_label_->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); + RecordMetadataHistogram(Metadata::kTitle); + } + + // The subtitle label should only be a11y-focusable when there is text to be + // read. + if (metadata.source_title.empty()) { + subtitle_label_->SetFocusBehavior(FocusBehavior::NEVER); + } else { + subtitle_label_->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); + RecordMetadataHistogram(Metadata::kSource); + } + + RecordMetadataHistogram(Metadata::kCount); + + container_->OnMediaSessionMetadataChanged(metadata); + + PreferredSizeChanged(); + Layout(); + SchedulePaint(); +} + +void MediaNotificationViewModernImpl::UpdateWithMediaActions( + const base::flat_set<media_session::mojom::MediaSessionAction>& actions) { + enabled_actions_ = actions; + + UpdateActionButtonsVisibility(); + + PreferredSizeChanged(); + Layout(); + SchedulePaint(); +} + +void MediaNotificationViewModernImpl::UpdateWithMediaArtwork( + const gfx::ImageSkia& image) { + GetMediaNotificationBackground()->UpdateArtwork(image); + + UMA_HISTOGRAM_BOOLEAN(kArtworkHistogramName, !image.isNull()); + + if (!image.isNull()) { + artwork_container_->SetVisible(true); + // When there is artwork to display, this spacer is no logner needed + notification_controls_spacer_->SetVisible(false); + } + + artwork_->SetImage(image); + artwork_->SetPreferredSize({70, 70}); + artwork_->SetVignetteColor( + GetMediaNotificationBackground()->GetBackgroundColor(*this)); + + UpdateForegroundColor(); + + container_->OnMediaArtworkChanged(image); + + PreferredSizeChanged(); + Layout(); + SchedulePaint(); +} + +void MediaNotificationViewModernImpl::UpdateWithFavicon( + const gfx::ImageSkia& icon) { + GetMediaNotificationBackground()->UpdateFavicon(icon); + + UpdateForegroundColor(); + SchedulePaint(); +} + +void MediaNotificationViewModernImpl::OnThemeChanged() { + MediaNotificationView::OnThemeChanged(); + UpdateForegroundColor(); +} + +void MediaNotificationViewModernImpl::UpdateDeviceSelectorAvailability( + bool availability) { + GetMediaNotificationBackground()->UpdateDeviceSelectorAvailability( + availability); +} + +void MediaNotificationViewModernImpl::UpdateActionButtonsVisibility() { + for (auto* view : media_controls_container_->children()) { + views::Button* action_button = views::Button::AsButton(view); + bool should_show = base::Contains(enabled_actions_, + GetActionFromButtonTag(*action_button)); + bool should_invalidate = should_show != action_button->GetVisible(); + + action_button->SetVisible(should_show); + + if (should_invalidate) + action_button->InvalidateLayout(); + } + + container_->OnVisibleActionsChanged(enabled_actions_); +} + +void MediaNotificationViewModernImpl::CreateMediaButton( + views::View* parent_view, + MediaSessionAction action, + const base::string16& accessible_name) { + auto button = views::CreateVectorImageButton(this); + button->set_tag(static_cast<int>(action)); + button->SetPreferredSize(kMediaButtonSize); + button->SetAccessibleName(accessible_name); + button->SetTooltipText(accessible_name); + button->SetFocusBehavior(views::View::FocusBehavior::ALWAYS); + button->EnableCanvasFlippingForRTLUI(false); + parent_view->AddChildView(std::move(button)); +} + +MediaNotificationBackground* +MediaNotificationViewModernImpl::GetMediaNotificationBackground() { + return static_cast<MediaNotificationBackground*>(background()); +} + +void MediaNotificationViewModernImpl::UpdateForegroundColor() { + const SkColor background = + GetMediaNotificationBackground()->GetBackgroundColor(*this); + const SkColor foreground = + GetMediaNotificationBackground()->GetForegroundColor(*this); + const SkColor disabled_icon_color = + SkColorSetA(foreground, gfx::kDisabledControlAlpha); + + // Update the colors for the labels + title_label_->SetEnabledColor(foreground); + subtitle_label_->SetEnabledColor(disabled_icon_color); + + title_label_->SetBackgroundColor(background); + subtitle_label_->SetBackgroundColor(background); + + // Update the colors for the toggle buttons (play/pause and + // picture-in-picture) + views::SetImageFromVectorIconWithColor( + play_pause_button_, + *GetVectorIconForMediaAction(MediaSessionAction::kPlay), + kMediaButtonIconSize, foreground); + views::SetToggledImageFromVectorIconWithColor( + play_pause_button_, + *GetVectorIconForMediaAction(MediaSessionAction::kPause), + kMediaButtonIconSize, foreground, disabled_icon_color); + + views::SetImageFromVectorIconWithColor( + picture_in_picture_button_, + *GetVectorIconForMediaAction(MediaSessionAction::kEnterPictureInPicture), + kPipButtonIconSize, foreground); + views::SetToggledImageFromVectorIconWithColor( + picture_in_picture_button_, + *GetVectorIconForMediaAction(MediaSessionAction::kExitPictureInPicture), + kPipButtonIconSize, foreground, disabled_icon_color); + + // Update the colors for the media control buttons. + for (views::View* child : media_controls_container_->children()) { + // Skip the play pause button since it is a special case. + if (child == play_pause_button_) + continue; + + views::ImageButton* button = static_cast<views::ImageButton*>(child); + + views::SetImageFromVectorIconWithColor( + button, *GetVectorIconForMediaAction(GetActionFromButtonTag(*button)), + kMediaButtonIconSize, foreground); + + button->SchedulePaint(); + } + + SchedulePaint(); + container_->OnColorsChanged(foreground, background); +} + +} // namespace media_message_center diff --git a/chromium/components/media_message_center/media_notification_view_modern_impl.h b/chromium/components/media_message_center/media_notification_view_modern_impl.h new file mode 100644 index 00000000000..96b0291da44 --- /dev/null +++ b/chromium/components/media_message_center/media_notification_view_modern_impl.h @@ -0,0 +1,154 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_VIEW_MODERN_IMPL_H_ +#define COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_VIEW_MODERN_IMPL_H_ + +#include "components/media_message_center/media_notification_view.h" + +#include "base/component_export.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "components/media_message_center/media_notification_view.h" +#include "services/media_session/public/mojom/media_session.mojom.h" +#include "ui/views/controls/button/button.h" +#include "ui/views/controls/button/image_button.h" +#include "ui/views/controls/image_view.h" +#include "ui/views/controls/label.h" + +namespace views { +class ToggleImageButton; +} // namespace views + +namespace media_message_center { + +namespace { +class MediaArtworkView; +} // anonymous namespace + +class MediaNotificationBackground; +class MediaNotificationContainer; +class MediaNotificationItem; + +class COMPONENT_EXPORT(MEDIA_MESSAGE_CENTER) MediaNotificationViewModernImpl + : public MediaNotificationView, + public views::ButtonListener { + public: + // The name of the histogram used when recording whether the artwork was + // present. + static const char kArtworkHistogramName[]; + + // The name of the histogram used when recording the type of metadata that was + // displayed. + static const char kMetadataHistogramName[]; + + // The type of metadata that was displayed. This is used in metrics so new + // values must only be added to the end. + enum class Metadata { + kTitle, + kArtist, + kAlbum, + kCount, + kSource, + kMaxValue = kSource, + }; + + MediaNotificationViewModernImpl( + MediaNotificationContainer* container, + base::WeakPtr<MediaNotificationItem> item, + std::unique_ptr<views::View> notification_controls_view, + int notification_width); + MediaNotificationViewModernImpl(const MediaNotificationViewModernImpl&) = + delete; + MediaNotificationViewModernImpl& operator=( + const MediaNotificationViewModernImpl&) = delete; + ~MediaNotificationViewModernImpl() override; + + // views::View: + void GetAccessibleNodeData(ui::AXNodeData* node_data) override; + void OnThemeChanged() override; + + // views::ButtonListener: + void ButtonPressed(views::Button* sender, const ui::Event& event) override; + + // MediaNotificationView + void SetForcedExpandedState(bool* forced_expanded_state) override {} + void SetExpanded(bool expanded) override {} + void UpdateCornerRadius(int top_radius, int bottom_radius) override; + void UpdateWithMediaSessionInfo( + const media_session::mojom::MediaSessionInfoPtr& session_info) override; + void UpdateWithMediaMetadata( + const media_session::MediaMetadata& metadata) override; + void UpdateWithMediaActions( + const base::flat_set<media_session::mojom::MediaSessionAction>& actions) + override; + void UpdateWithMediaArtwork(const gfx::ImageSkia& image) override; + void UpdateWithFavicon(const gfx::ImageSkia& icon) override; + void UpdateWithVectorIcon(const gfx::VectorIcon& vector_icon) override {} + void UpdateDeviceSelectorAvailability(bool availability) override; + + // Testing methods + const views::Label* title_label_for_testing() const { return title_label_; } + + const views::Label* subtitle_label_for_testing() const { + return subtitle_label_; + } + + const views::Button* picture_in_picture_button_for_testing() const { + return picture_in_picture_button_; + } + + const views::View* media_controls_container_for_testing() const { + return media_controls_container_; + } + + private: + friend class MediaNotificationViewModernImplTest; + + // Creates an image button with an icon that matches |action| and adds it + // to |parent_view|. When clicked it will trigger |action| on the session. + // |accessible_name| is the text used for screen readers and the + // button's tooltip. + void CreateMediaButton(views::View* parent_view, + media_session::mojom::MediaSessionAction action, + const base::string16& accessible_name); + + void UpdateActionButtonsVisibility(); + + MediaNotificationBackground* GetMediaNotificationBackground(); + + void UpdateForegroundColor(); + + // Container that receives events. + MediaNotificationContainer* const container_; + + // Keeps track of media metadata and controls the session when buttons are + // clicked. + base::WeakPtr<MediaNotificationItem> item_; + + bool has_artwork_ = false; + + // Set of enabled actions. + base::flat_set<media_session::mojom::MediaSessionAction> enabled_actions_; + + // Stores the text to be read by screen readers describing the notification. + // Contains the title, artist and album separated by hyphens. + base::string16 accessible_name_; + + MediaNotificationBackground* background_; + + // Container views directly attached to this view. + views::View* artwork_container_ = nullptr; + MediaArtworkView* artwork_ = nullptr; + views::Label* title_label_ = nullptr; + views::Label* subtitle_label_ = nullptr; + views::ToggleImageButton* picture_in_picture_button_ = nullptr; + views::View* notification_controls_spacer_ = nullptr; + views::View* media_controls_container_ = nullptr; + views::ToggleImageButton* play_pause_button_ = nullptr; +}; + +} // namespace media_message_center + +#endif // COMPONENTS_MEDIA_MESSAGE_CENTER_MEDIA_NOTIFICATION_VIEW_MODERN_IMPL_H_ diff --git a/chromium/components/media_message_center/media_notification_view_modern_impl_unittest.cc b/chromium/components/media_message_center/media_notification_view_modern_impl_unittest.cc new file mode 100644 index 00000000000..6fa81fba794 --- /dev/null +++ b/chromium/components/media_message_center/media_notification_view_modern_impl_unittest.cc @@ -0,0 +1,1074 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/media_message_center/media_notification_view_modern_impl.h" + +#include <memory> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/containers/flat_set.h" +#include "base/macros.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/mock_callback.h" +#include "base/test/task_environment.h" +#include "base/unguessable_token.h" +#include "build/build_config.h" +#include "components/media_message_center/media_notification_background_impl.h" +#include "components/media_message_center/media_notification_constants.h" +#include "components/media_message_center/media_notification_container.h" +#include "components/media_message_center/media_notification_controller.h" +#include "components/media_message_center/media_notification_util.h" +#include "components/media_message_center/media_session_notification_item.h" +#include "services/media_session/public/cpp/test/test_media_controller.h" +#include "services/media_session/public/mojom/audio_focus.mojom.h" +#include "services/media_session/public/mojom/media_session.mojom.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" +#include "ui/events/base_event_utils.h" +#include "ui/message_center/message_center.h" +#include "ui/message_center/public/cpp/message_center_constants.h" +#include "ui/message_center/views/message_view_factory.h" +#include "ui/message_center/views/notification_control_buttons_view.h" +#include "ui/message_center/views/notification_header_view.h" +#include "ui/views/controls/image_view.h" +#include "ui/views/test/views_test_base.h" + +namespace media_message_center { + +using media_session::mojom::MediaSessionAction; +using media_session::test::TestMediaController; +using testing::_; +using testing::Expectation; +using testing::Invoke; + +namespace { + +const int kMediaButtonIconSize = 20; +const int kPipButtonIconSize = 18; + +const gfx::Size kWidgetSize(500, 500); + +constexpr int kViewWidth = 350; +const gfx::Size kViewSize(kViewWidth, 400); + +class MockMediaNotificationController : public MediaNotificationController { + public: + MockMediaNotificationController() = default; + ~MockMediaNotificationController() override = default; + + // MediaNotificationController implementation. + MOCK_METHOD1(ShowNotification, void(const std::string& id)); + MOCK_METHOD1(HideNotification, void(const std::string& id)); + MOCK_METHOD1(RemoveItem, void(const std::string& id)); + scoped_refptr<base::SequencedTaskRunner> GetTaskRunner() const override { + return nullptr; + } + MOCK_METHOD2(LogMediaSessionActionButtonPressed, + void(const std::string& id, + media_session::mojom::MediaSessionAction action)); + + private: + DISALLOW_COPY_AND_ASSIGN(MockMediaNotificationController); +}; + +class MockMediaNotificationContainer : public MediaNotificationContainer { + public: + MockMediaNotificationContainer() = default; + ~MockMediaNotificationContainer() override = default; + + // MediaNotificationContainer implementation. + MOCK_METHOD1(OnExpanded, void(bool expanded)); + MOCK_METHOD1( + OnMediaSessionInfoChanged, + void(const media_session::mojom::MediaSessionInfoPtr& session_info)); + MOCK_METHOD1(OnMediaSessionMetadataChanged, + void(const media_session::MediaMetadata& metadata)); + MOCK_METHOD1(OnVisibleActionsChanged, + void(const base::flat_set<MediaSessionAction>& actions)); + MOCK_METHOD1(OnMediaArtworkChanged, void(const gfx::ImageSkia& image)); + MOCK_METHOD2(OnColorsChanged, void(SkColor foreground, SkColor background)); + MOCK_METHOD0(OnHeaderClicked, void()); + + MediaNotificationViewModernImpl* view() const { return view_; } + void SetView(MediaNotificationViewModernImpl* view) { view_ = view; } + + private: + MediaNotificationViewModernImpl* view_; + + DISALLOW_COPY_AND_ASSIGN(MockMediaNotificationContainer); +}; + +} // namespace + +class MediaNotificationViewModernImplTest : public views::ViewsTestBase { + public: + MediaNotificationViewModernImplTest() + : views::ViewsTestBase( + base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} + ~MediaNotificationViewModernImplTest() override = default; + + void SetUp() override { + views::ViewsTestBase::SetUp(); + + request_id_ = base::UnguessableToken::Create(); + + // Create a new MediaNotificationViewModernImpl whenever the + // MediaSessionNotificationItem says to show the notification. + EXPECT_CALL(controller_, ShowNotification(request_id_.ToString())) + .WillRepeatedly(InvokeWithoutArgs( + this, &MediaNotificationViewModernImplTest::CreateView)); + + // Create a widget to show on the screen for testing screen coordinates and + // focus. + widget_ = std::make_unique<views::Widget>(); + views::Widget::InitParams params = + CreateParams(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); + params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.bounds = gfx::Rect(kWidgetSize); + widget_->Init(std::move(params)); + widget_->Show(); + + CreateViewFromMediaSessionInfo( + media_session::mojom::MediaSessionInfo::New()); + } + + void CreateViewFromMediaSessionInfo( + media_session::mojom::MediaSessionInfoPtr session_info) { + session_info->is_controllable = true; + mojo::Remote<media_session::mojom::MediaController> controller; + item_ = std::make_unique<MediaSessionNotificationItem>( + &controller_, request_id_.ToString(), std::string(), + std::move(controller), std::move(session_info)); + + // Update the metadata. + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title"); + metadata.artist = base::ASCIIToUTF16("artist"); + metadata.source_title = base::ASCIIToUTF16("source title"); + item_->MediaSessionMetadataChanged(metadata); + + // Inject the test media controller into the item. + media_controller_ = std::make_unique<TestMediaController>(); + item_->SetMediaControllerForTesting( + media_controller_->CreateMediaControllerRemote()); + } + + void TearDown() override { + container_.SetView(nullptr); + widget_.reset(); + + actions_.clear(); + + views::ViewsTestBase::TearDown(); + } + + void EnableAllActions() { + actions_.insert(MediaSessionAction::kPlay); + actions_.insert(MediaSessionAction::kPause); + actions_.insert(MediaSessionAction::kPreviousTrack); + actions_.insert(MediaSessionAction::kNextTrack); + actions_.insert(MediaSessionAction::kSeekBackward); + actions_.insert(MediaSessionAction::kSeekForward); + actions_.insert(MediaSessionAction::kStop); + actions_.insert(MediaSessionAction::kEnterPictureInPicture); + actions_.insert(MediaSessionAction::kExitPictureInPicture); + + NotifyUpdatedActions(); + } + + void EnableAction(MediaSessionAction action) { + actions_.insert(action); + NotifyUpdatedActions(); + } + + void DisableAction(MediaSessionAction action) { + actions_.erase(action); + NotifyUpdatedActions(); + } + + MockMediaNotificationContainer& container() { return container_; } + + MockMediaNotificationController& controller() { return controller_; } + + MediaNotificationViewModernImpl* view() const { return container_.view(); } + + TestMediaController* media_controller() const { + return media_controller_.get(); + } + + const base::string16& accessible_name() const { + return view()->accessible_name_; + } + + views::Label* title_label() const { return view()->title_label_; } + + views::Label* subtitle_label() const { return view()->subtitle_label_; } + + views::View* artwork_container() const { return view()->artwork_container_; } + + views::View* media_controls_container() const { + return view()->media_controls_container_; + } + + std::vector<views::Button*> media_control_buttons() const { + std::vector<views::Button*> buttons; + auto children = view()->media_controls_container_->children(); + std::transform( + children.begin(), children.end(), std::back_inserter(buttons), + [](views::View* child) { return views::Button::AsButton(child); }); + buttons.push_back( + views::Button::AsButton(view()->picture_in_picture_button_)); + return buttons; + } + + views::Button* picture_in_picture_button() const { + return view()->picture_in_picture_button_; + } + + views::Button* GetButtonForAction(MediaSessionAction action) const { + auto buttons = media_control_buttons(); + const auto i = std::find_if( + buttons.begin(), buttons.end(), [action](const views::Button* button) { + return button->tag() == static_cast<int>(action); + }); + return (i == buttons.end()) ? nullptr : *i; + } + + bool IsActionButtonVisible(MediaSessionAction action) const { + return GetButtonForAction(action)->GetVisible(); + } + + MediaSessionNotificationItem* GetItem() const { return item_.get(); } + + const gfx::ImageSkia& GetArtworkImage() const { + return static_cast<MediaNotificationBackgroundImpl*>( + view()->GetMediaNotificationBackground()) + ->artwork_; + } + + void SimulateButtonClick(MediaSessionAction action) { + views::Button* button = GetButtonForAction(action); + EXPECT_TRUE(button->GetVisible()); + + view()->ButtonPressed( + button, ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), + ui::EventTimeForNow(), 0, 0)); + } + + void SimulateTab() { + ui::KeyEvent pressed_tab(ui::ET_KEY_PRESSED, ui::VKEY_TAB, ui::EF_NONE); + view()->GetFocusManager()->OnKeyEvent(pressed_tab); + } + + void ExpectHistogramActionRecorded(MediaSessionAction action) { + histogram_tester_.ExpectUniqueSample( + MediaSessionNotificationItem::kUserActionHistogramName, + static_cast<base::HistogramBase::Sample>(action), 1); + } + + void ExpectHistogramArtworkRecorded(bool present, int count) { + histogram_tester_.ExpectBucketCount( + MediaNotificationViewModernImpl::kArtworkHistogramName, + static_cast<base::HistogramBase::Sample>(present), count); + } + + void ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata metadata, + int count) { + histogram_tester_.ExpectBucketCount( + MediaNotificationViewModernImpl::kMetadataHistogramName, + static_cast<base::HistogramBase::Sample>(metadata), count); + } + + void AdvanceClockMilliseconds(int milliseconds) { + task_environment()->FastForwardBy( + base::TimeDelta::FromMilliseconds(milliseconds)); + } + + private: + void NotifyUpdatedActions() { + item_->MediaSessionActionsChanged( + std::vector<MediaSessionAction>(actions_.begin(), actions_.end())); + } + + void CreateView() { + // Create a MediaNotificationViewModernImpl. + auto view = std::make_unique<MediaNotificationViewModernImpl>( + &container_, item_->GetWeakPtr(), std::make_unique<views::View>(), + kViewWidth); + view->SetSize(kViewSize); + + // Display it in |widget_|. Widget now owns |view|. + // And associate it with |container_|. + container_.SetView(widget_->SetContentsView(std::move(view))); + } + + base::UnguessableToken request_id_; + + base::HistogramTester histogram_tester_; + + base::flat_set<MediaSessionAction> actions_; + + std::unique_ptr<TestMediaController> media_controller_; + MockMediaNotificationContainer container_; + MockMediaNotificationController controller_; + std::unique_ptr<MediaSessionNotificationItem> item_; + std::unique_ptr<views::Widget> widget_; + + DISALLOW_COPY_AND_ASSIGN(MediaNotificationViewModernImplTest); +}; + +// TODO(crbug.com/1009287): many of these tests are failing on TSan builds. +#if defined(THREAD_SANITIZER) +#define MAYBE_MediaNotificationViewModernImplTest \ + DISABLED_MediaNotificationViewModernImplTest +class DISABLED_MediaNotificationViewModernImplTest + : public MediaNotificationViewModernImplTest {}; +#else +#define MAYBE_MediaNotificationViewModernImplTest \ + MediaNotificationViewModernImplTest +#endif + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, ButtonsSanityCheck) { + EnableAllActions(); + + EXPECT_TRUE(media_controls_container()->GetVisible()); + EXPECT_GT(media_controls_container()->width(), 0); + EXPECT_GT(media_controls_container()->height(), 0); + + auto buttons = media_control_buttons(); + EXPECT_EQ(6u, buttons.size()); + + for (auto* button : buttons) { + EXPECT_TRUE(button->GetVisible()); + if (button == picture_in_picture_button()) { + EXPECT_LT(kPipButtonIconSize, button->width()); + EXPECT_LT(kPipButtonIconSize, button->height()); + } else { + EXPECT_LT(kMediaButtonIconSize, button->width()); + EXPECT_LT(kMediaButtonIconSize, button->height()); + } + EXPECT_FALSE(views::Button::AsButton(button)->GetAccessibleName().empty()); + } + + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPreviousTrack)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kNextTrack)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kSeekBackward)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kSeekForward)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kEnterPictureInPicture)); + + // |kPause| cannot be present if |kPlay| is. + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kExitPictureInPicture)); +} + +#if defined(OS_WIN) +#define MAYBE_ButtonsFocusCheck DISABLED_ButtonsFocusCheck +#else +#define MAYBE_ButtonsFocusCheck ButtonsFocusCheck +#endif +TEST_F(MAYBE_MediaNotificationViewModernImplTest, MAYBE_ButtonsFocusCheck) { + // Expand and enable all actions to show all buttons. + EnableAllActions(); + + views::FocusManager* focus_manager = view()->GetFocusManager(); + + { + // Focus the first action button. + auto* button = GetButtonForAction(MediaSessionAction::kPreviousTrack); + focus_manager->SetFocusedView(button); + EXPECT_EQ(button, focus_manager->GetFocusedView()); + } + + SimulateTab(); + EXPECT_EQ(GetButtonForAction(MediaSessionAction::kSeekBackward), + focus_manager->GetFocusedView()); + + SimulateTab(); + EXPECT_EQ(GetButtonForAction(MediaSessionAction::kPlay), + focus_manager->GetFocusedView()); + + SimulateTab(); + EXPECT_EQ(GetButtonForAction(MediaSessionAction::kSeekForward), + focus_manager->GetFocusedView()); + + SimulateTab(); + EXPECT_EQ(GetButtonForAction(MediaSessionAction::kNextTrack), + focus_manager->GetFocusedView()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, PlayPauseButtonTooltipCheck) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + + EXPECT_CALL(container(), OnMediaSessionInfoChanged(_)); + + auto* button = GetButtonForAction(MediaSessionAction::kPlay); + base::string16 tooltip = button->GetTooltipText(gfx::Point()); + EXPECT_FALSE(tooltip.empty()); + + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + session_info->is_controllable = true; + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + base::string16 new_tooltip = button->GetTooltipText(gfx::Point()); + EXPECT_FALSE(new_tooltip.empty()); + EXPECT_NE(tooltip, new_tooltip); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, NextTrackButtonClick) { + EXPECT_CALL(controller(), LogMediaSessionActionButtonPressed( + _, MediaSessionAction::kNextTrack)); + EnableAction(MediaSessionAction::kNextTrack); + + EXPECT_EQ(0, media_controller()->next_track_count()); + + SimulateButtonClick(MediaSessionAction::kNextTrack); + GetItem()->FlushForTesting(); + + EXPECT_EQ(1, media_controller()->next_track_count()); + ExpectHistogramActionRecorded(MediaSessionAction::kNextTrack); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, PlayButtonClick) { + EXPECT_CALL(controller(), + LogMediaSessionActionButtonPressed(_, MediaSessionAction::kPlay)); + EnableAction(MediaSessionAction::kPlay); + + EXPECT_EQ(0, media_controller()->resume_count()); + + SimulateButtonClick(MediaSessionAction::kPlay); + GetItem()->FlushForTesting(); + + EXPECT_EQ(1, media_controller()->resume_count()); + ExpectHistogramActionRecorded(MediaSessionAction::kPlay); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, PauseButtonClick) { + EXPECT_CALL(controller(), LogMediaSessionActionButtonPressed( + _, MediaSessionAction::kPause)); + EnableAction(MediaSessionAction::kPause); + EXPECT_CALL(container(), OnMediaSessionInfoChanged(_)); + + EXPECT_EQ(0, media_controller()->suspend_count()); + + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + session_info->is_controllable = true; + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + SimulateButtonClick(MediaSessionAction::kPause); + GetItem()->FlushForTesting(); + + EXPECT_EQ(1, media_controller()->suspend_count()); + ExpectHistogramActionRecorded(MediaSessionAction::kPause); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, PreviousTrackButtonClick) { + EXPECT_CALL(controller(), LogMediaSessionActionButtonPressed( + _, MediaSessionAction::kPreviousTrack)); + EnableAction(MediaSessionAction::kPreviousTrack); + + EXPECT_EQ(0, media_controller()->previous_track_count()); + + SimulateButtonClick(MediaSessionAction::kPreviousTrack); + GetItem()->FlushForTesting(); + + EXPECT_EQ(1, media_controller()->previous_track_count()); + ExpectHistogramActionRecorded(MediaSessionAction::kPreviousTrack); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, SeekBackwardButtonClick) { + EXPECT_CALL(controller(), LogMediaSessionActionButtonPressed( + _, MediaSessionAction::kSeekBackward)); + EnableAction(MediaSessionAction::kSeekBackward); + + EXPECT_EQ(0, media_controller()->seek_backward_count()); + + SimulateButtonClick(MediaSessionAction::kSeekBackward); + GetItem()->FlushForTesting(); + + EXPECT_EQ(1, media_controller()->seek_backward_count()); + ExpectHistogramActionRecorded(MediaSessionAction::kSeekBackward); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, SeekForwardButtonClick) { + EXPECT_CALL(controller(), LogMediaSessionActionButtonPressed( + _, MediaSessionAction::kSeekForward)); + EnableAction(MediaSessionAction::kSeekForward); + + EXPECT_EQ(0, media_controller()->seek_forward_count()); + + SimulateButtonClick(MediaSessionAction::kSeekForward); + GetItem()->FlushForTesting(); + + EXPECT_EQ(1, media_controller()->seek_forward_count()); + ExpectHistogramActionRecorded(MediaSessionAction::kSeekForward); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + PlayToggle_FromObserver_Empty) { + EnableAction(MediaSessionAction::kPlay); + + { + views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( + GetButtonForAction(MediaSessionAction::kPlay)); + ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); + EXPECT_FALSE(button->GetToggled()); + } + + view()->UpdateWithMediaSessionInfo( + media_session::mojom::MediaSessionInfo::New()); + + { + views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( + GetButtonForAction(MediaSessionAction::kPlay)); + ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); + EXPECT_FALSE(button->GetToggled()); + } +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + PlayToggle_FromObserver_PlaybackState) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + + { + views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( + GetButtonForAction(MediaSessionAction::kPlay)); + ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); + EXPECT_FALSE(button->GetToggled()); + } + + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + view()->UpdateWithMediaSessionInfo(session_info.Clone()); + + { + views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( + GetButtonForAction(MediaSessionAction::kPause)); + ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); + EXPECT_TRUE(button->GetToggled()); + } + + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPaused; + view()->UpdateWithMediaSessionInfo(session_info.Clone()); + + { + views::ToggleImageButton* button = static_cast<views::ToggleImageButton*>( + GetButtonForAction(MediaSessionAction::kPlay)); + ASSERT_EQ(views::ToggleImageButton::kViewClassName, button->GetClassName()); + EXPECT_FALSE(button->GetToggled()); + } +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, MetadataIsDisplayed) { + EnableAllActions(); + + EXPECT_TRUE(title_label()->GetVisible()); + EXPECT_TRUE(subtitle_label()->GetVisible()); + + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, UpdateMetadata_FromObserver) { + EnableAllActions(); + + ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata::kTitle, 1); + ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata::kSource, 1); + ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata::kCount, 1); + + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title2"); + metadata.source_title = base::ASCIIToUTF16("source title2"); + metadata.artist = base::ASCIIToUTF16("artist2"); + metadata.album = base::ASCIIToUTF16("album"); + + EXPECT_CALL(container(), OnMediaSessionMetadataChanged(_)); + GetItem()->MediaSessionMetadataChanged(metadata); + + EXPECT_TRUE(title_label()->GetVisible()); + EXPECT_TRUE(subtitle_label()->GetVisible()); + + EXPECT_EQ(metadata.title, title_label()->GetText()); + EXPECT_EQ(metadata.source_title, subtitle_label()->GetText()); + + EXPECT_EQ(base::ASCIIToUTF16("title2 - artist2 - album"), accessible_name()); + + ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata::kTitle, 2); + ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata::kSource, 2); + ExpectHistogramMetadataRecorded( + MediaNotificationViewModernImpl::Metadata::kCount, 2); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + ActionButtonsHiddenByDefault) { + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kPlay)); + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kNextTrack)); + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kPreviousTrack)); + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kSeekForward)); + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kSeekBackward)); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + ActionButtonsToggleVisibility) { + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kNextTrack)); + + EnableAction(MediaSessionAction::kNextTrack); + + EXPECT_TRUE(IsActionButtonVisible(MediaSessionAction::kNextTrack)); + + DisableAction(MediaSessionAction::kNextTrack); + + EXPECT_FALSE(IsActionButtonVisible(MediaSessionAction::kNextTrack)); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, UpdateArtworkFromItem) { + int labels_container_width = title_label()->parent()->width(); + gfx::Size size = view()->size(); + EXPECT_CALL(container(), OnMediaArtworkChanged(_)).Times(2); + EXPECT_CALL(container(), OnColorsChanged(_, _)).Times(2); + + SkBitmap image; + image.allocN32Pixels(10, 10); + image.eraseColor(SK_ColorGREEN); + + EXPECT_TRUE(GetArtworkImage().isNull()); + + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, image); + + ExpectHistogramArtworkRecorded(true, 1); + + // The size of the labels container should not change when there is artwork. + EXPECT_EQ(labels_container_width, title_label()->parent()->width()); + + // Ensure that the labels container does not extend into the artwork bounds. + EXPECT_FALSE(artwork_container()->bounds().Intersects( + title_label()->parent()->bounds())); + + // Ensure that when the image is displayed that the size of the notification + // was not affected. + EXPECT_FALSE(GetArtworkImage().isNull()); + EXPECT_EQ(gfx::Size(10, 10), GetArtworkImage().size()); + EXPECT_EQ(size, view()->size()); + + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, SkBitmap()); + + ExpectHistogramArtworkRecorded(false, 1); + + // Ensure the labels container goes back to the original width now that we + // do not have any artwork. + EXPECT_EQ(labels_container_width, title_label()->parent()->width()); + + // Ensure that the artwork was reset and the size was still not + // affected. + EXPECT_TRUE(GetArtworkImage().isNull()); + EXPECT_EQ(size, view()->size()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, AccessibleNodeData) { + ui::AXNodeData data; + view()->GetAccessibleNodeData(&data); + + EXPECT_TRUE( + data.HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription)); + EXPECT_EQ(base::ASCIIToUTF16("title - artist"), accessible_name()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + Freezing_DoNotUpdateMetadata) { + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title2"); + metadata.artist = base::ASCIIToUTF16("artist2"); + metadata.album = base::ASCIIToUTF16("album"); + + EXPECT_CALL(container(), OnMediaSessionMetadataChanged(_)).Times(0); + GetItem()->Freeze(base::DoNothing()); + GetItem()->MediaSessionMetadataChanged(metadata); + + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, Freezing_DoNotUpdateImage) { + SkBitmap image; + image.allocN32Pixels(10, 10); + image.eraseColor(SK_ColorMAGENTA); + EXPECT_CALL(container(), OnMediaArtworkChanged(_)).Times(0); + EXPECT_CALL(container(), OnColorsChanged(_, _)).Times(0); + + GetItem()->Freeze(base::DoNothing()); + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, image); + + EXPECT_TRUE(GetArtworkImage().isNull()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + Freezing_DoNotUpdatePlaybackState) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + + EXPECT_CALL(container(), OnMediaSessionInfoChanged(_)).Times(0); + + GetItem()->Freeze(base::DoNothing()); + + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, Freezing_DoNotUpdateActions) { + EXPECT_FALSE( + GetButtonForAction(MediaSessionAction::kSeekForward)->GetVisible()); + + GetItem()->Freeze(base::DoNothing()); + EnableAction(MediaSessionAction::kSeekForward); + + EXPECT_FALSE( + GetButtonForAction(MediaSessionAction::kSeekForward)->GetVisible()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, Freezing_DisableInteraction) { + EnableAllActions(); + + EXPECT_EQ(0, media_controller()->next_track_count()); + + GetItem()->Freeze(base::DoNothing()); + + SimulateButtonClick(MediaSessionAction::kNextTrack); + GetItem()->FlushForTesting(); + + EXPECT_EQ(0, media_controller()->next_track_count()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, UnfreezingDoesntMissUpdates) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + + // Freeze the item and clear the metadata. + base::MockOnceClosure unfrozen_callback; + EXPECT_CALL(unfrozen_callback, Run).Times(0); + GetItem()->Freeze(unfrozen_callback.Get()); + GetItem()->MediaSessionInfoChanged(nullptr); + GetItem()->MediaSessionMetadataChanged(base::nullopt); + + // The item should be frozen and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + + // Bind the item to a new controller that's playing instead of paused. + auto new_media_controller = std::make_unique<TestMediaController>(); + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + session_info->is_controllable = true; + GetItem()->SetController(new_media_controller->CreateMediaControllerRemote(), + session_info.Clone()); + + // The item will receive a MediaSessionInfoChanged. + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + // The item should still be frozen, and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + + // Update the metadata. + EXPECT_CALL(unfrozen_callback, Run); + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title2"); + metadata.source_title = base::ASCIIToUTF16("source title 2"); + GetItem()->MediaSessionMetadataChanged(metadata); + + // The item should no longer be frozen, and we should see the updated data. + EXPECT_FALSE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title2"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title 2"), subtitle_label()->GetText()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + UnfreezingWaitsForArtwork_Timeout) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + + // Set an image before freezing. + SkBitmap initial_image; + initial_image.allocN32Pixels(10, 10); + initial_image.eraseColor(SK_ColorMAGENTA); + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, initial_image); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Freeze the item and clear the metadata. + base::MockOnceClosure unfrozen_callback; + EXPECT_CALL(unfrozen_callback, Run).Times(0); + GetItem()->Freeze(unfrozen_callback.Get()); + GetItem()->MediaSessionInfoChanged(nullptr); + GetItem()->MediaSessionMetadataChanged(base::nullopt); + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, SkBitmap()); + + // The item should be frozen and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Bind the item to a new controller that's playing instead of paused. + auto new_media_controller = std::make_unique<TestMediaController>(); + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + session_info->is_controllable = true; + GetItem()->SetController(new_media_controller->CreateMediaControllerRemote(), + session_info.Clone()); + + // The item will receive a MediaSessionInfoChanged. + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + // The item should still be frozen, and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Update the metadata. + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title2"); + metadata.source_title = base::ASCIIToUTF16("source title 2"); + GetItem()->MediaSessionMetadataChanged(metadata); + + // The item should still be frozen, and waiting for a new image. + EXPECT_TRUE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Once the freeze timer fires, the item should unfreeze even if there's no + // artwork. + EXPECT_CALL(unfrozen_callback, Run); + AdvanceClockMilliseconds(2600); + + EXPECT_FALSE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title2"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title 2"), subtitle_label()->GetText()); + EXPECT_TRUE(GetArtworkImage().isNull()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, UnfreezingWaitsForActions) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + EnableAction(MediaSessionAction::kNextTrack); + EnableAction(MediaSessionAction::kPreviousTrack); + + // Freeze the item and clear the metadata and actions. + base::MockOnceClosure unfrozen_callback; + EXPECT_CALL(unfrozen_callback, Run).Times(0); + GetItem()->Freeze(unfrozen_callback.Get()); + GetItem()->MediaSessionInfoChanged(nullptr); + GetItem()->MediaSessionMetadataChanged(base::nullopt); + DisableAction(MediaSessionAction::kPlay); + DisableAction(MediaSessionAction::kPause); + DisableAction(MediaSessionAction::kNextTrack); + DisableAction(MediaSessionAction::kPreviousTrack); + + // The item should be frozen and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kNextTrack)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPreviousTrack)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + + // Bind the item to a new controller that's playing instead of paused. + auto new_media_controller = std::make_unique<TestMediaController>(); + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + session_info->is_controllable = true; + GetItem()->SetController(new_media_controller->CreateMediaControllerRemote(), + session_info.Clone()); + + // The item will receive a MediaSessionInfoChanged. + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + // The item should still be frozen, and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kNextTrack)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPreviousTrack)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + + // Update the metadata. + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title2"); + metadata.source_title = base::ASCIIToUTF16("source title 2"); + GetItem()->MediaSessionMetadataChanged(metadata); + + // The item should still be frozen, and waiting for new actions. + EXPECT_TRUE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kNextTrack)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPreviousTrack)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + + // Once we receive actions, the item should unfreeze. + EXPECT_CALL(unfrozen_callback, Run); + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + EnableAction(MediaSessionAction::kSeekForward); + EnableAction(MediaSessionAction::kSeekBackward); + + EXPECT_FALSE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kSeekForward)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kSeekBackward)); + EXPECT_EQ(base::ASCIIToUTF16("title2"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title 2"), subtitle_label()->GetText()); +} + +TEST_F(MAYBE_MediaNotificationViewModernImplTest, + UnfreezingWaitsForArtwork_ReceiveArtwork) { + EnableAction(MediaSessionAction::kPlay); + EnableAction(MediaSessionAction::kPause); + + // Set an image before freezing. + SkBitmap initial_image; + initial_image.allocN32Pixels(10, 10); + initial_image.eraseColor(SK_ColorMAGENTA); + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, initial_image); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Freeze the item and clear the metadata. + base::MockOnceClosure unfrozen_callback; + EXPECT_CALL(unfrozen_callback, Run).Times(0); + GetItem()->Freeze(unfrozen_callback.Get()); + GetItem()->MediaSessionInfoChanged(nullptr); + GetItem()->MediaSessionMetadataChanged(base::nullopt); + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, SkBitmap()); + + // The item should be frozen and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Bind the item to a new controller that's playing instead of paused. + auto new_media_controller = std::make_unique<TestMediaController>(); + media_session::mojom::MediaSessionInfoPtr session_info( + media_session::mojom::MediaSessionInfo::New()); + session_info->playback_state = + media_session::mojom::MediaPlaybackState::kPlaying; + session_info->is_controllable = true; + GetItem()->SetController(new_media_controller->CreateMediaControllerRemote(), + session_info.Clone()); + + // The item will receive a MediaSessionInfoChanged. + GetItem()->MediaSessionInfoChanged(session_info.Clone()); + + // The item should still be frozen, and the view should contain the old data. + EXPECT_TRUE(GetItem()->frozen()); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Update the metadata. + media_session::MediaMetadata metadata; + metadata.title = base::ASCIIToUTF16("title2"); + metadata.source_title = base::ASCIIToUTF16("source title 2"); + GetItem()->MediaSessionMetadataChanged(metadata); + + // The item should still be frozen, and waiting for a new image. + EXPECT_TRUE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); + + // Once we receive artwork, the item should unfreeze. + EXPECT_CALL(unfrozen_callback, Run); + SkBitmap new_image; + new_image.allocN32Pixels(10, 10); + new_image.eraseColor(SK_ColorYELLOW); + GetItem()->MediaControllerImageChanged( + media_session::mojom::MediaSessionImageType::kArtwork, new_image); + + EXPECT_FALSE(GetItem()->frozen()); + testing::Mock::VerifyAndClearExpectations(&unfrozen_callback); + EXPECT_FALSE(GetButtonForAction(MediaSessionAction::kPlay)); + EXPECT_TRUE(GetButtonForAction(MediaSessionAction::kPause)); + EXPECT_EQ(base::ASCIIToUTF16("title2"), title_label()->GetText()); + EXPECT_EQ(base::ASCIIToUTF16("source title 2"), subtitle_label()->GetText()); + EXPECT_FALSE(GetArtworkImage().isNull()); +} + +} // namespace media_message_center diff --git a/chromium/components/media_message_center/vector_icons/BUILD.gn b/chromium/components/media_message_center/vector_icons/BUILD.gn index 4f197ada75d..9b6608faabb 100644 --- a/chromium/components/media_message_center/vector_icons/BUILD.gn +++ b/chromium/components/media_message_center/vector_icons/BUILD.gn @@ -4,18 +4,18 @@ import("//components/vector_icons/vector_icons.gni") -aggregate_vector_icons("media_vector_icons") { +aggregate_vector_icons2("media_vector_icons") { icon_directory = "." - icons = [ + sources = [ "media_enter_pip.icon", "media_exit_pip.icon", "media_next_track.icon", "media_previous_track.icon", - "media_seek_forward.icon", "media_seek_backward.icon", - "play_arrow.icon", + "media_seek_forward.icon", "pause.icon", + "play_arrow.icon", ] } |