chromium/ash/system/media/media_tray.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/media/media_tray.h"

#include "ash/constants/ash_pref_names.h"
#include "ash/constants/tray_background_view_catalog.h"
#include "ash/focus_cycler.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/icon_button.h"
#include "ash/style/typography.h"
#include "ash/system/media/media_notification_provider.h"
#include "ash/system/tray/tray_bubble_view.h"
#include "ash/system/tray/tray_bubble_wrapper.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tray_utils.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "components/global_media_controls/public/constants.h"
#include "components/media_message_center/notification_theme.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/compositor/layer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

namespace {

constexpr int kNoMediaTextFontSize = 14;
constexpr int kTitleViewHeight = 60;

constexpr gfx::Insets kTitleViewInsets = gfx::Insets::VH(16, 16);

// Minimum screen diagonal (in inches) for pinning global media controls
// on shelf by default.
constexpr float kMinimumScreenSizeDiagonal = 10.0f;

// Calculate screen size and returns true if screen size is larger than
// kMinimumScreenSizeDiagonal.
bool GetIsPinnedToShelfByDefault() {
  // Happens in test.
  if (!Shell::HasInstance()) {
    return false;
  }

  display::ManagedDisplayInfo info =
      Shell::Get()->display_manager()->GetDisplayInfo(
          display::Screen::GetScreen()->GetPrimaryDisplay().id());
  DCHECK(info.device_dpi());
  float screen_width = info.size_in_pixel().width() / info.device_dpi();
  float screen_height = info.size_in_pixel().height() / info.device_dpi();

  float diagonal_len = sqrt(pow(screen_width, 2) + pow(screen_height, 2));
  return diagonal_len > kMinimumScreenSizeDiagonal;
}

// Enum that specifies the pin state of global media controls.
enum PinState {
  kDefault = 0,
  kUnpinned,
  kPinned,
};

// View that contains global media controls' title.
class GlobalMediaControlsTitleView : public views::View {
  METADATA_HEADER(GlobalMediaControlsTitleView, views::View)

 public:
  GlobalMediaControlsTitleView() {
    auto* box_layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kHorizontal, kTitleViewInsets));
    box_layout->set_cross_axis_alignment(
        views::BoxLayout::CrossAxisAlignment::kCenter);

    title_label_ = AddChildView(std::make_unique<views::Label>());
    title_label_->SetText(
        l10n_util::GetStringUTF16(IDS_ASH_GLOBAL_MEDIA_CONTROLS_TITLE));
    title_label_->SetAutoColorReadabilityEnabled(false);

    // Media tray should always be pinned to shelf when we are opening the
    // dialog.
    DCHECK(MediaTray::IsPinnedToShelf());
    pin_button_ = AddChildView(std::make_unique<MediaTray::PinButton>());

    title_label_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
    TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosTitle1,
                                          *title_label_);
    SetPreferredSize(gfx::Size(kWideTrayMenuWidth, kTitleViewHeight));

    // Makes the title in the center of the card horizontally.
    title_label_->SetBorder(views::CreateEmptyBorder(
        gfx::Insets::TLBR(0, pin_button_->GetPreferredSize().width(), 0, 0)));

    box_layout->SetFlexForView(title_label_, 1);
  }

  void OnThemeChanged() override {
    views::View::OnThemeChanged();
    title_label_->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
        AshColorProvider::ContentLayerType::kTextColorPrimary));
  }

  views::Button* pin_button() { return pin_button_; }

 private:
  raw_ptr<views::ImageButton> pin_button_ = nullptr;
  raw_ptr<views::Label> title_label_ = nullptr;
};

BEGIN_METADATA(GlobalMediaControlsTitleView)
END_METADATA

}  // namespace

// static
void MediaTray::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(prefs::kGlobalMediaControlsPinned,
                                PinState::kDefault);
}

// static
bool MediaTray::IsPinnedToShelf() {
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  DCHECK(pref_service);
  switch (pref_service->GetInteger(prefs::kGlobalMediaControlsPinned)) {
    case PinState::kPinned:
      return true;
    case PinState::kUnpinned:
      return false;
    case PinState::kDefault:
      return GetIsPinnedToShelfByDefault();
  }

  NOTREACHED();
}

// static
void MediaTray::SetPinnedToShelf(bool pinned) {
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  DCHECK(pref_service);
  pref_service->SetInteger(prefs::kGlobalMediaControlsPinned,
                           pinned ? PinState::kPinned : PinState::kUnpinned);
}

MediaTray::PinButton::PinButton()
    : IconButton(
          base::BindRepeating(&PinButton::ButtonPressed,
                              base::Unretained(this)),
          IconButton::Type::kMedium,
          &kUnpinnedIcon,
          MediaTray::IsPinnedToShelf()
              ? IDS_ASH_GLOBAL_MEDIA_CONTROLS_PINNED_BUTTON_TOOLTIP_TEXT
              : IDS_ASH_GLOBAL_MEDIA_CONTROLS_UNPINNED_BUTTON_TOOLTIP_TEXT,
          /*is_togglable=*/true,
          /*has_border=*/false) {
  SetIconSize(kTrayTopShortcutButtonIconSize);
  SetToggledVectorIcon(kPinnedIcon);
  SetIconColor(cros_tokens::kCrosSysOnSurface);
  SetBackgroundToggledColor(cros_tokens::kCrosSysSystemPrimaryContainer);
  SetToggled(MediaTray::IsPinnedToShelf());
}

void MediaTray::PinButton::ButtonPressed() {
  MediaTray::SetPinnedToShelf(!MediaTray::IsPinnedToShelf());
  base::UmaHistogramBoolean("Media.CrosGlobalMediaControls.PinAction",
                            MediaTray::IsPinnedToShelf());

  SetToggled(MediaTray::IsPinnedToShelf());
  SetTooltipText(l10n_util::GetStringUTF16(
      MediaTray::IsPinnedToShelf()
          ? IDS_ASH_GLOBAL_MEDIA_CONTROLS_PINNED_BUTTON_TOOLTIP_TEXT
          : IDS_ASH_GLOBAL_MEDIA_CONTROLS_UNPINNED_BUTTON_TOOLTIP_TEXT));
}

BEGIN_METADATA(MediaTray, PinButton)
END_METADATA

MediaTray::MediaTray(Shelf* shelf)
    : TrayBackgroundView(shelf, TrayBackgroundViewCatalogName::kMediaPlayer) {
  SetCallback(base::BindRepeating(&MediaTray::OnTrayButtonPressed,
                                  base::Unretained(this)));
  if (MediaNotificationProvider::Get()) {
    MediaNotificationProvider::Get()->AddObserver(this);
  }

  Shell::Get()->session_controller()->AddObserver(this);

  tray_container()->SetMargin(kMediaTrayPadding, 0);
  auto icon = std::make_unique<views::ImageView>();
  icon->SetTooltipText(l10n_util::GetStringUTF16(
      IDS_ASH_GLOBAL_MEDIA_CONTROLS_BUTTON_TOOLTIP_TEXT));
  icon_ = tray_container()->AddChildView(std::move(icon));
  UpdateTrayItemColor(is_active());
}

MediaTray::~MediaTray() {
  if (GetBubbleView()) {
    GetBubbleView()->ResetDelegate();
  }

  if (MediaNotificationProvider::Get()) {
    MediaNotificationProvider::Get()->RemoveObserver(this);
  }

  Shell::Get()->session_controller()->RemoveObserver(this);
}

void MediaTray::OnNotificationListChanged() {
  UpdateDisplayState();
}

void MediaTray::OnNotificationListViewSizeChanged() {
  if (!GetBubbleView()) {
    return;
  }

  GetBubbleView()->UpdateBubble();
}

std::u16string MediaTray::GetAccessibleNameForTray() {
  return l10n_util::GetStringUTF16(
      IDS_ASH_GLOBAL_MEDIA_CONTROLS_BUTTON_TOOLTIP_TEXT);
}

void MediaTray::HideBubble(const TrayBubbleView* bubble_view) {
  CloseBubble();
}

void MediaTray::UpdateAfterLoginStatusChange() {
  UpdateDisplayState();
  PreferredSizeChanged();
}

void MediaTray::HandleLocaleChange() {
  icon_->SetTooltipText(l10n_util::GetStringUTF16(
      IDS_ASH_GLOBAL_MEDIA_CONTROLS_BUTTON_TOOLTIP_TEXT));
}

views::Widget* MediaTray::GetBubbleWidget() const {
  return bubble_ ? bubble_->GetBubbleWidget() : nullptr;
}

TrayBubbleView* MediaTray::GetBubbleView() {
  return bubble_ ? bubble_->GetBubbleView() : nullptr;
}

void MediaTray::ShowBubble() {
  ShowBubbleWithItem("");
}

void MediaTray::CloseBubbleInternal() {
  if (!bubble_) {
    CHECK(!is_active());
    CHECK(!pin_button_);
    CHECK(!content_view_);
    CHECK(!empty_state_view_);
    return;
  }
  if (MediaNotificationProvider::Get()) {
    MediaNotificationProvider::Get()->OnBubbleClosing();
  }
  SetIsActive(false);
  pin_button_ = nullptr;
  content_view_ = nullptr;
  empty_state_view_ = nullptr;
  bubble_.reset();
  UpdateDisplayState();
  shelf()->UpdateAutoHideState();
}

void MediaTray::HideBubbleWithView(const TrayBubbleView* bubble_view) {
  if (GetBubbleView() && GetBubbleView() == bubble_view) {
    CloseBubble();
  }
}

void MediaTray::ClickedOutsideBubble(const ui::LocatedEvent& event) {
  CloseBubble();
}

void MediaTray::UpdateTrayItemColor(bool is_active) {
  icon_->SetImage(ui::ImageModel::FromVectorIcon(
      kGlobalMediaControlsIcon,
      is_active ? cros_tokens::kCrosSysSystemOnPrimaryContainer
                : cros_tokens::kCrosSysOnSurface));
}

void MediaTray::OnLockStateChanged(bool locked) {
  UpdateDisplayState();
}

void MediaTray::OnActiveUserPrefServiceChanged(PrefService* pref_service) {
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(pref_service);
  pref_change_registrar_->Add(
      prefs::kGlobalMediaControlsPinned,
      base::BindRepeating(&MediaTray::OnGlobalMediaControlsPinPrefChanged,
                          base::Unretained(this)));
  OnGlobalMediaControlsPinPrefChanged();
}

void MediaTray::OnTrayButtonPressed() {
  if (GetBubbleWidget()) {
    CloseBubble();
    return;
  }

  ShowBubble();
}

void MediaTray::UpdateDisplayState() {
  if (!MediaNotificationProvider::Get()) {
    return;
  }

  if (bubble_ && Shell::Get()->session_controller()->IsScreenLocked()) {
    CloseBubble();
  }

  bool has_session =
      MediaNotificationProvider::Get()->HasActiveNotifications() ||
      MediaNotificationProvider::Get()->HasFrozenNotifications();

  // Verify the bubble view still exists before referencing `empty_state_view_`.
  if (GetBubbleView()) {
    if (has_session && empty_state_view_) {
      empty_state_view_->SetVisible(false);
    }
    if (!has_session) {
      ShowEmptyState();
    }
  }

  bool should_show = has_session &&
                     !Shell::Get()->session_controller()->IsScreenLocked() &&
                     IsPinnedToShelf();

  // If the bubble is open, we don't want to hide the media tray.
  if (!bubble_) {
    SetVisiblePreferred(should_show);
  }
}

void MediaTray::ShowBubbleWithItem(const std::string& item_id) {
  DCHECK(MediaNotificationProvider::Get());
  SetNotificationColorTheme();

  std::unique_ptr<TrayBubbleView> bubble_view =
      std::make_unique<TrayBubbleView>(CreateInitParamsForTrayBubble(this));

  auto* title_view = bubble_view->AddChildView(
      std::make_unique<GlobalMediaControlsTitleView>());
  title_view->SetPaintToLayer();
  title_view->layer()->SetFillsBoundsOpaquely(false);
  pin_button_ = title_view->pin_button();
  global_media_controls::GlobalMediaControlsEntryPoint entry_point =
      item_id.empty()
          ? global_media_controls::GlobalMediaControlsEntryPoint::kSystemTray
          : global_media_controls::GlobalMediaControlsEntryPoint::kPresentation;

  content_view_ = bubble_view->AddChildView(
      MediaNotificationProvider::Get()->GetMediaNotificationListView(
          kMenuSeparatorWidth, /*should_clip_height=*/true, entry_point,
          item_id));
  bubble_view->SetPreferredWidth(kWideTrayMenuWidth);
  content_view_->SetBorder(views::CreateEmptyBorder(
      gfx::Insets::TLBR(0, 0, kMediaNotificationListViewBottomPadding, 0)));

  bubble_ = std::make_unique<TrayBubbleWrapper>(this);
  bubble_->ShowBubble(std::move(bubble_view));
  SetIsActive(true);

  base::UmaHistogramBoolean("Media.CrosGlobalMediaControls.RepeatUsageOnShelf",
                            bubble_has_shown_);
  bubble_has_shown_ = true;
}

std::u16string MediaTray::GetAccessibleNameForBubble() {
  return l10n_util::GetStringUTF16(IDS_ASH_GLOBAL_MEDIA_CONTROLS_TITLE);
}

void MediaTray::SetNotificationColorTheme() {
  if (!MediaNotificationProvider::Get()) {
    return;
  }

  media_message_center::NotificationTheme theme;
  theme.primary_text_color = AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kTextColorPrimary);
  theme.secondary_text_color = AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kTextColorSecondary);
  theme.enabled_icon_color = AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kIconColorPrimary);
  theme.disabled_icon_color = AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kIconColorSecondary);
  theme.separator_color = AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kSeparatorColor);
  theme.background_color =
      GetColorProvider()->GetColor(kColorAshControlBackgroundColorInactive);
  MediaNotificationProvider::Get()->SetColorTheme(theme);
}

void MediaTray::OnGlobalMediaControlsPinPrefChanged() {
  UpdateDisplayState();
}

void MediaTray::ShowEmptyState() {
  CHECK(content_view_);
  CHECK(GetBubbleView());

  if (empty_state_view_) {
    empty_state_view_->SetVisible(true);
    return;
  }

  // Create and add empty state view containing a label indicating there's no
  // active session
  auto empty_state_view = std::make_unique<views::View>();
  auto* layout =
      empty_state_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kHorizontal));
  layout->set_minimum_cross_axis_size(content_view_->bounds().height());
  layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);

  auto no_media_label = std::make_unique<views::Label>();
  no_media_label->SetAutoColorReadabilityEnabled(false);
  no_media_label->SetSubpixelRenderingEnabled(false);
  no_media_label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
      AshColorProvider::ContentLayerType::kTextColorSecondary));
  no_media_label->SetText(
      l10n_util::GetStringUTF16(IDS_ASH_GLOBAL_MEDIA_CONTROLS_NO_MEDIA_TEXT));
  no_media_label->SetFontList(
      gfx::FontList({"Google Sans", "Roboto"}, gfx::Font::NORMAL,
                    kNoMediaTextFontSize, gfx::Font::Weight::NORMAL));
  empty_state_view->AddChildView(std::move(no_media_label));

  empty_state_view->SetPaintToLayer();
  empty_state_view->layer()->SetFillsBoundsOpaquely(false);
  empty_state_view_ =
      GetBubbleView()->AddChildView(std::move(empty_state_view));
}

BEGIN_METADATA(MediaTray)
END_METADATA

}  // namespace ash