chromium/ash/system/focus_mode/sounds/focus_mode_sounds_view.cc

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

#include "ash/system/focus_mode/sounds/focus_mode_sounds_view.h"

#include <memory>

#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/pill_button.h"
#include "ash/style/rounded_container.h"
#include "ash/style/tab_slider.h"
#include "ash/style/tab_slider_button.h"
#include "ash/style/typography.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_detailed_view.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "ash/system/focus_mode/sounds/playlist_view.h"
#include "ash/system/focus_mode/sounds/sound_section_view.h"
#include "ash/system/model/system_tray_model.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "build/branding_buildflags.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/view_class_properties.h"

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
#include "chromeos/ash/resources/internal/grit/ash_internal_scaled_resources.h"
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)

namespace ash {

namespace {

// Margins between containers in the detailed view if the container is not
// connected to the container above it.
constexpr auto kDisconnectedContainerMargins = gfx::Insets::TLBR(8, 0, 0, 0);

constexpr auto kSoundViewBottomPadding = 22;
constexpr auto kSoundTabSliderInsets = gfx::Insets::VH(16, 0);
constexpr auto kFocusSoundsLabelInsets = gfx::Insets::VH(18, 24);

constexpr int kNonPremiumChildViewsSpacing = 16;
constexpr int kNonPremiumLabelViewMaxWidth = 288;

constexpr float kOfflineStateOpacity = 0.38f;
constexpr auto kLabelPadding = gfx::Insets::VH(0, 40);

std::optional<int> GetYouTubeMusicIconResourceId() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return IDR_YOUTUBE_MUSIC_ICON;
#else
  return std::nullopt;
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
}

std::unique_ptr<views::BoxLayoutView> CreateNonPremiumView() {
  auto box_view = std::make_unique<views::BoxLayoutView>();
  box_view->SetOrientation(views::BoxLayout::Orientation::kVertical);
  box_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  box_view->SetBetweenChildSpacing(kNonPremiumChildViewsSpacing);

  auto* label = box_view->AddChildView(
      std::make_unique<views::Label>(l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_NON_PREMIUM_LABEL)));
  label->SetMultiLine(true);
  // For the label view with multiple lines, we need to set the max width for
  // it to calculate the total height of multiple lines.
  label->SetMaximumWidth(kNonPremiumLabelViewMaxWidth);
  label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
  label->SetFontList(TypographyProvider::Get()->ResolveTypographyToken(
      TypographyToken::kCrosBody2));
  label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);

  auto* learn_more_button = box_view->AddChildView(std::make_unique<PillButton>(
      views::Button::PressedCallback(base::BindRepeating([]() {
        Shell::Get()
            ->system_tray_model()
            ->client()
            ->ShowYouTubeMusicPremiumPage();
      })),
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_LEARN_MORE_BUTTON),
      PillButton::Type::kDefaultElevatedWithIconLeading));

  // Add the YouTube Music icon for the `learn_more_button` if it's chrome
  // branded.
  const auto& resource_id = GetYouTubeMusicIconResourceId();
  if (resource_id.has_value()) {
    const gfx::ImageSkia* image =
        ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
            resource_id.value());

    CHECK(image);
    learn_more_button->SetImageModel(views::Button::ButtonState::STATE_NORMAL,
                                     ui::ImageModel::FromImageSkia(*image));
  }

  return box_view;
}

std::unique_ptr<views::Label> CreateOfflineLabel(const int message_id) {
  auto label =
      std::make_unique<views::Label>(l10n_util::GetStringUTF16(message_id));
  label->SetFontList(ash::TypographyProvider::Get()->ResolveTypographyToken(
      ash::TypographyToken::kCrosBody2));
  label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  label->SetMultiLine(true);
  label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
  return label;
}

std::unique_ptr<views::BoxLayoutView> CreateOfflineStateView() {
  auto box_view = std::make_unique<views::BoxLayoutView>();
  box_view->SetOrientation(views::BoxLayout::Orientation::kVertical);
  box_view->SetBorder(views::CreateEmptyBorder(kLabelPadding));
  box_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  box_view->AddChildView(CreateOfflineLabel(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_OFFLINE_LABEL_ONE));
  box_view->AddChildView(CreateOfflineLabel(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_OFFLINE_LABEL_TWO));
  return box_view;
}

}  // namespace

//---------------------------------------------------------------------
// FocusModeSoundsView:

FocusModeSoundsView::FocusModeSoundsView(
    const base::flat_set<focus_mode_util::SoundType>& sound_sections,
    bool is_network_connected) {
  SetProperty(views::kMarginsKey, kDisconnectedContainerMargins);
  SetBorderInsets(gfx::Insets::TLBR(0, 0, kSoundViewBottomPadding, 0));
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  if (sound_sections.empty()) {
    SetVisible(false);
    return;
  }
  CreateHeader(sound_sections, is_network_connected);

  auto* sounds_controller =
      FocusModeController::Get()->focus_mode_sounds_controller();

  const bool should_show_soundscapes =
      focus_mode_util::SoundType::kSoundscape ==
          sounds_controller->sound_type() ||
      !base::Contains(sound_sections,
                      focus_mode_util::SoundType::kYouTubeMusic);

  if (soundscape_button_ && youtube_music_button_) {
    if (should_show_soundscapes) {
      soundscape_button_->SetSelected(true);
    } else {
      youtube_music_button_->SetSelected(true);
    }
  }

  if (is_network_connected) {
    CreatesSoundSectionViews(sound_sections);

    if (soundscape_container_) {
      // Start downloading playlists for Soundscape.
      sounds_controller->DownloadPlaylistsForType(
          /*is_soundscape_type=*/true,
          base::BindOnce(&FocusModeSoundsView::UpdateSoundsView,
                         weak_factory_.GetWeakPtr()));
    }

    if (youtube_music_container_) {
      // Set the no premium callback and start downloading playlists for YouTube
      // Music.
      sounds_controller->SetYouTubeMusicNoPremiumCallback(base::BindRepeating(
          &FocusModeSoundsView::ToggleYouTubeMusicAlternateView,
          weak_factory_.GetWeakPtr(), /*show=*/true));
      sounds_controller->DownloadPlaylistsForType(
          /*is_soundscape_type=*/false,
          base::BindOnce(&FocusModeSoundsView::UpdateSoundsView,
                         weak_factory_.GetWeakPtr()));
    }

    if (should_show_soundscapes) {
      OnSoundscapeButtonToggled();
    } else {
      OnYouTubeMusicButtonToggled();
    }
  } else {
    AddChildView(CreateOfflineStateView());
  }

  sounds_controller->AddObserver(this);
}

FocusModeSoundsView::~FocusModeSoundsView() {
  FocusModeController::Get()->focus_mode_sounds_controller()->RemoveObserver(
      this);
}

void FocusModeSoundsView::OnSelectedPlaylistChanged() {
  const auto& selected_playlist = FocusModeController::Get()
                                      ->focus_mode_sounds_controller()
                                      ->selected_playlist();
  UpdateStateForSelectedPlaylist(selected_playlist);
}

void FocusModeSoundsView::OnPlaylistStateChanged() {
  const auto& selected_playlist = FocusModeController::Get()
                                      ->focus_mode_sounds_controller()
                                      ->selected_playlist();
  if (selected_playlist.empty()) {
    UpdateStateForSelectedPlaylist(selected_playlist);
    return;
  }

  switch (selected_playlist.type) {
    case focus_mode_util::SoundType::kSoundscape:
      if (soundscape_container_) {
        soundscape_container_->UpdateSelectedPlaylistForNewState(
            selected_playlist.state);
      }
      break;
    case focus_mode_util::SoundType::kYouTubeMusic:
      if (youtube_music_container_) {
        youtube_music_container_->UpdateSelectedPlaylistForNewState(
            selected_playlist.state);
      }
      break;
    case focus_mode_util::SoundType::kNone:
      NOTREACHED();
  }
}

void FocusModeSoundsView::UpdateSoundsView(bool is_soundscape_type) {
  auto* sounds_controller =
      FocusModeController::Get()->focus_mode_sounds_controller();
  if (is_soundscape_type) {
    if (!soundscape_container_) {
      return;
    }
    const auto& playlists = sounds_controller->soundscape_playlists();
    if (playlists.empty()) {
      return;
    }
    soundscape_container_->UpdateContents(playlists);
  } else {
    if (!youtube_music_container_) {
      return;
    }
    const auto& playlists = sounds_controller->youtube_music_playlists();
    if (playlists.empty()) {
      return;
    }
    youtube_music_container_->UpdateContents(playlists);
  }
}

void FocusModeSoundsView::UpdateStateForSelectedPlaylist(
    const focus_mode_util::SelectedPlaylist& selected_playlist) {
  if (soundscape_container_) {
    soundscape_container_->UpdateStateForSelectedPlaylist(selected_playlist);
  }
  if (youtube_music_container_) {
    youtube_music_container_->UpdateStateForSelectedPlaylist(selected_playlist);
  }
}

void FocusModeSoundsView::CreateHeader(
    const base::flat_set<focus_mode_util::SoundType>& sections,
    bool is_network_connected) {
  CHECK(!sections.empty());
  CHECK(base::Contains(sections, focus_mode_util::SoundType::kSoundscape));
  const bool contains_youtube_music =
      base::Contains(sections, focus_mode_util::SoundType::kYouTubeMusic);

  auto* sounds_container_header =
      AddChildView(std::make_unique<views::BoxLayoutView>());
  sounds_container_header->SetInsideBorderInsets(
      contains_youtube_music ? kSoundTabSliderInsets : kFocusSoundsLabelInsets);
  sounds_container_header->SetMainAxisAlignment(
      contains_youtube_music ? views::BoxLayout::MainAxisAlignment::kCenter
                             : views::BoxLayout::MainAxisAlignment::kStart);

  // If there is no YouTube Music type of playlists, we can just create a label.
  if (!contains_youtube_music) {
    auto* focus_sounds_label =
        sounds_container_header->AddChildView(std::make_unique<views::Label>());
    focus_sounds_label->SetText(l10n_util::GetStringUTF16(
        IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_SOUNDSCAPE_BUTTON));
    focus_sounds_label->SetHorizontalAlignment(
        gfx::HorizontalAlignment::ALIGN_LEFT);
    focus_sounds_label->SetEnabledColorId(
        cros_tokens::kCrosSysOnSurfaceVariant);
    TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
                                          *focus_sounds_label);
    return;
  }

  auto* sound_tab_slider = sounds_container_header->AddChildView(
      std::make_unique<TabSlider>(/*max_tab_num=*/2));
  sound_tab_slider->GetViewAccessibility().SetRole(ax::mojom::Role::kTabList);

  soundscape_button_ = sound_tab_slider->AddButton<IconLabelSliderButton>(
      base::BindRepeating(&FocusModeSoundsView::OnSoundscapeButtonToggled,
                          weak_factory_.GetWeakPtr()),
      &kFocusSoundsIcon,
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_SOUNDSCAPE_BUTTON),
      /*tooltip_text_base=*/u"", /*horizontal=*/true);
  soundscape_button_->GetViewAccessibility().SetRole(ax::mojom::Role::kTab);

  youtube_music_button_ = sound_tab_slider->AddButton<IconLabelSliderButton>(
      base::BindRepeating(&FocusModeSoundsView::OnYouTubeMusicButtonToggled,
                          weak_factory_.GetWeakPtr()),
      &kYtmIcon,
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_SOUNDS_YOUTUBE_MUSIC_BUTTON),
      /*tooltip_text_base=*/u"", /*horizontal=*/true);
  youtube_music_button_->GetViewAccessibility().SetRole(ax::mojom::Role::kTab);

  if (!is_network_connected) {
    sound_tab_slider->layer()->SetOpacity(kOfflineStateOpacity);
    sound_tab_slider->SetEnabled(false);
  }
}

void FocusModeSoundsView::CreatesSoundSectionViews(
    const base::flat_set<focus_mode_util::SoundType>& sound_sections) {
  if (base::Contains(sound_sections, focus_mode_util::SoundType::kSoundscape)) {
    soundscape_container_ = AddChildView(std::make_unique<SoundSectionView>(
        focus_mode_util::SoundType::kSoundscape));
  }

  if (base::Contains(sound_sections,
                     focus_mode_util::SoundType::kYouTubeMusic)) {
    youtube_music_container_ = AddChildView(std::make_unique<SoundSectionView>(
        focus_mode_util::SoundType::kYouTubeMusic));
    youtube_music_container_->SetAlternateView(CreateNonPremiumView());
    ToggleYouTubeMusicAlternateView(/*show=*/false);
  }
}

void FocusModeSoundsView::ToggleYouTubeMusicAlternateView(bool show) {
  CHECK(youtube_music_container_);
  youtube_music_container_->ShowAlternateView(show);
}

void FocusModeSoundsView::OnSoundscapeButtonToggled() {
  MayShowSoundscapeContainer(true);
}

void FocusModeSoundsView::OnYouTubeMusicButtonToggled() {
  MayShowSoundscapeContainer(false);
}

void FocusModeSoundsView::MayShowSoundscapeContainer(bool show) {
  if (soundscape_container_) {
    soundscape_container_->SetVisible(show);
  }
  if (youtube_music_container_) {
    youtube_music_container_->SetVisible(!show);
  }

  if (soundscape_button_) {
    soundscape_button_->GetViewAccessibility().SetIsSelected(show);
    youtube_music_button_->GetViewAccessibility().SetIsSelected(!show);
  }
}

BEGIN_METADATA(FocusModeSoundsView)
END_METADATA

}  // namespace ash