chromium/ash/system/audio/audio_detailed_view.cc

// Copyright 2014 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/audio/audio_detailed_view.h"

#include <memory>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/ash_element_identifiers.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/pill_button.h"
#include "ash/style/rounded_container.h"
#include "ash/style/typography.h"
#include "ash/system/audio/audio_detailed_view_utils.h"
#include "ash/system/audio/labeled_slider_view.h"
#include "ash/system/audio/mic_gain_slider_controller.h"
#include "ash/system/audio/mic_gain_slider_view.h"
#include "ash/system/audio/unified_volume_slider_controller.h"
#include "ash/system/audio/unified_volume_view.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tri_view.h"
#include "ash/system/unified/quick_settings_slider.h"
#include "ash/system/unified/unified_slider_view.h"
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ash/components/audio/audio_device.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "components/live_caption/caption_util.h"
#include "components/live_caption/pref_names.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache_wrapper.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "components/vector_icons/vector_icons.h"
#include "media/base/media_switches.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace ash {
namespace {

const int kToggleButtonRowRightPadding = 16;
const int kNbsWarningMinHeight = 80;
constexpr auto kLiveCaptionContainerMargins = gfx::Insets::TLBR(0, 0, 8, 0);
constexpr auto kToggleButtonRowViewPadding =
    gfx::Insets::TLBR(0, 32, 0, kToggleButtonRowRightPadding);
constexpr auto kToggleButtonRowPreferredSize = gfx::Size(0, 32);
constexpr auto kToggleButtonRowLabelPadding = gfx::Insets::VH(8, 12);
constexpr auto kToggleButtonRowMargins = gfx::Insets::VH(4, 0);
constexpr auto kSeparatorMargins = gfx::Insets::TLBR(4, 32, 12, 32);
constexpr auto kTextRowInsets = gfx::Insets::VH(8, 24);

// This callback is only used for tests.
AudioDetailedView::NoiseCancellationCallback*
    g_noise_cancellation_toggle_callback = nullptr;

// This callback is only used for tests.
AudioDetailedView::StyleTransferCallback* g_style_transfer_toggle_callback =
    nullptr;

speech::LanguageCode GetLiveCaptionLocale() {
  std::string live_caption_locale = speech::kUsEnglishLocale;
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  if (pref_service) {
    live_caption_locale = ::prefs::GetLiveCaptionLanguageCode(pref_service);
  }
  return speech::GetLanguageCode(live_caption_locale);
}

std::vector<std::string> GetNamesOfAppsAccessingMic(
    apps::AppRegistryCache* app_registry_cache,
    apps::AppCapabilityAccessCache* app_capability_access_cache) {
  if (!app_registry_cache || !app_capability_access_cache) {
    return {};
  }

  std::vector<std::string> app_names;
  for (const std::string& app :
       app_capability_access_cache->GetAppsAccessingMicrophone()) {
    std::string name;
    app_registry_cache->ForOneApp(
        app, [&name](const apps::AppUpdate& update) { name = update.Name(); });
    if (!name.empty()) {
      app_names.push_back(name);
    }
  }

  return app_names;
}

std::u16string GetTextForAgcInfo(const std::vector<std::string>& app_names) {
  std::u16string agc_info_string = l10n_util::GetPluralStringFUTF16(
      IDS_ASH_STATUS_TRAY_AUDIO_INPUT_AGC_INFO, app_names.size());
  return app_names.size() == 1
             ? l10n_util::FormatString(
                   agc_info_string, {base::UTF8ToUTF16(app_names[0])}, nullptr)
             : agc_info_string;
}

void AddSeparator(views::View* container) {
  auto* separator =
      container->AddChildView(std::make_unique<views::Separator>());
  separator->SetColorId(cros_tokens::kCrosSysSeparator);
  separator->SetOrientation(views::Separator::Orientation::kHorizontal);
  separator->SetProperty(views::kMarginsKey, kSeparatorMargins);
}

}  // namespace

AudioDetailedView::AudioDetailedView(DetailedViewDelegate* delegate)
    : TrayDetailedView(delegate),
      num_stream_ignore_ui_gains_(
          CrasAudioHandler::Get()->num_stream_ignore_ui_gains()) {
  CreateItems();

  Shell::Get()->accessibility_controller()->AddObserver(this);
  CrasAudioHandler::Get()->AddAudioObserver(this);

  if (captions::IsLiveCaptionFeatureSupported()) {
    speech::SodaInstaller* soda_installer =
        speech::SodaInstaller::GetInstance();
    if (soda_installer) {
      soda_installer->AddObserver(this);
    }
  }

  // Session state observer currently only used for monitoring the microphone
  // usage which is only for the information for showing AGC control.
  if (base::FeatureList::IsEnabled(media::kIgnoreUiGains)) {
    session_observation_.Observe(Shell::Get()->session_controller());

    // Initialize with current session state.
    OnSessionStateChanged(
        Shell::Get()->session_controller()->GetSessionState());
  }
}

AudioDetailedView::~AudioDetailedView() {
  if (captions::IsLiveCaptionFeatureSupported()) {
    speech::SodaInstaller* soda_installer =
        speech::SodaInstaller::GetInstance();
    // `soda_installer` is not guaranteed to be valid, since it's possible for
    // this class to out-live it. This means that this class cannot use
    // ScopedObservation and needs to manage removing the observer itself.
    if (soda_installer) {
      soda_installer->RemoveObserver(this);
    }
  }
  CrasAudioHandler::Get()->RemoveAudioObserver(this);
  Shell::Get()->accessibility_controller()->RemoveObserver(this);
}

views::View* AudioDetailedView::GetAsView() {
  return this;
}

void AudioDetailedView::SetMapNoiseCancellationToggleCallbackForTest(
    AudioDetailedView::NoiseCancellationCallback*
        noise_cancellation_toggle_callback) {
  g_noise_cancellation_toggle_callback = noise_cancellation_toggle_callback;
}

void AudioDetailedView::SetMapStyleTransferToggleCallbackForTest(
    AudioDetailedView::StyleTransferCallback* style_transfer_toggle_callback) {
  g_style_transfer_toggle_callback = style_transfer_toggle_callback;
}

void AudioDetailedView::Update() {
  UpdateAudioDevices();
  DeprecatedLayoutImmediately();
}

void AudioDetailedView::OnAccessibilityStatusChanged() {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  // The live caption state has been updated.
  UpdateLiveCaptionView(controller->live_caption().enabled());
}

void AudioDetailedView::OnCapabilityAccessUpdate(
    const apps::CapabilityAccessUpdate& update) {
  UpdateAgcInfoRow();
}

void AudioDetailedView::OnAppCapabilityAccessCacheWillBeDestroyed(
    apps::AppCapabilityAccessCache* cache) {
  app_capability_observation_.Reset();
  app_capability_access_cache_ = nullptr;
}

void AudioDetailedView::OnSessionStateChanged(
    session_manager::SessionState state) {
  // Session state observer currently only used for monitoring the microphone
  // usage which is only for the information for showing AGC control.
  if (!base::FeatureList::IsEnabled(media::kIgnoreUiGains)) {
    return;
  }
  app_capability_observation_.Reset();
  app_registry_cache_ = nullptr;
  app_capability_access_cache_ = nullptr;
  if (state != session_manager::SessionState::ACTIVE) {
    return;
  }
  auto* session_controller = Shell::Get()->session_controller();
  if (!session_controller) {
    return;
  }

  AccountId active_user_account_id = session_controller->GetActiveAccountId();
  if (!active_user_account_id.is_valid()) {
    return;
  }

  app_registry_cache_ =
      apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(
          active_user_account_id);
  app_capability_access_cache_ =
      apps::AppCapabilityAccessCacheWrapper::Get().GetAppCapabilityAccessCache(
          active_user_account_id);

  if (app_capability_access_cache_) {
    app_capability_observation_.Observe(app_capability_access_cache_);
  }
}

void AudioDetailedView::AddAudioSubHeader(views::View* container,
                                          const gfx::VectorIcon& icon,
                                          const int text_id) {
  auto* sub_header_label_ = TrayPopupUtils::CreateDefaultLabel();
  sub_header_label_->SetText(l10n_util::GetStringUTF16(text_id));
  sub_header_label_->SetEnabledColorId(cros_tokens::kCrosSysOnSurfaceVariant);
  sub_header_label_->SetAutoColorReadabilityEnabled(false);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
                                        *sub_header_label_);
  sub_header_label_->SetBorder(views::CreateEmptyBorder(kTextRowInsets));
  container->AddChildView(sub_header_label_);
  return;
}

void AudioDetailedView::CreateItems() {
  CreateScrollableList();
  CreateTitleRow(IDS_ASH_STATUS_TRAY_AUDIO_TITLE);

  if (captions::IsLiveCaptionFeatureSupported()) {
    CreateLiveCaptionView();
  }

  mic_gain_controller_ = std::make_unique<MicGainSliderController>();
  unified_volume_slider_controller_ =
      std::make_unique<UnifiedVolumeSliderController>();
}

void AudioDetailedView::CreateLiveCaptionView() {
  auto* live_caption_container =
      scroll_content()->AddChildViewAt(std::make_unique<RoundedContainer>(), 0);
  live_caption_container->SetProperty(views::kMarginsKey,
                                      kLiveCaptionContainerMargins);
  // Ensures the `HoverHighlightView` ink drop fills the whole container.
  live_caption_container->SetBorderInsets(gfx::Insets());

  live_caption_view_ = live_caption_container->AddChildView(
      std::make_unique<HoverHighlightView>(/*listener=*/this));
  live_caption_view_->SetFocusBehavior(FocusBehavior::NEVER);

  // Creates the icon and text for the `live_caption_view_`.
  const bool live_caption_enabled =
      Shell::Get()->accessibility_controller()->live_caption().enabled();
  auto toggle_icon = std::make_unique<views::ImageView>();
  toggle_icon->SetImage(ui::ImageModel::FromVectorIcon(
      live_caption_enabled ? kUnifiedMenuLiveCaptionIcon
                           : kUnifiedMenuLiveCaptionOffIcon,
      cros_tokens::kCrosSysOnSurface, kQsSliderIconSize));
  live_caption_icon_ = toggle_icon.get();
  live_caption_view_->AddViewAndLabel(
      std::move(toggle_icon),
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_LIVE_CAPTION));
  live_caption_view_->text_label()->SetEnabledColorId(
      cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton1,
                                        *live_caption_view_->text_label());

  // Creates a toggle button on the right.
  auto toggle = std::make_unique<Switch>(base::BindRepeating(
      &AudioDetailedView::ToggleLiveCaptionState, weak_factory_.GetWeakPtr()));
  toggle->SetIsOn(live_caption_enabled);
  std::u16string toggle_tooltip =
      live_caption_enabled
          ? l10n_util::GetStringUTF16(
                IDS_ASH_STATUS_TRAY_LIVE_CAPTION_ENABLED_STATE_TOOLTIP)
          : l10n_util::GetStringUTF16(
                IDS_ASH_STATUS_TRAY_LIVE_CAPTION_DISABLED_STATE_TOOLTIP);
  toggle->SetTooltipText(l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_LIVE_CAPTION_TOGGLE_TOOLTIP, toggle_tooltip));
  toggle->GetViewAccessibility().SetName(
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_LIVE_CAPTION));
  live_caption_button_ = toggle.get();
  live_caption_view_->AddRightView(toggle.release());

  // Allows the row to be taller than a typical tray menu item.
  live_caption_view_->SetExpandable(true);
  live_caption_view_->tri_view()->SetInsets(
      gfx::Insets::TLBR(8, 24, 8, kToggleButtonRowRightPadding));
}

std::unique_ptr<TriView> AudioDetailedView::CreateNbsWarningView() {
  std::unique_ptr<TriView> nbs_warning_view(
      TrayPopupUtils::CreateDefaultRowView(
          /*use_wide_layout=*/true));
  nbs_warning_view->SetMinHeight(kNbsWarningMinHeight);
  nbs_warning_view->SetContainerVisible(TriView::Container::END, false);
  nbs_warning_view->SetID(AudioDetailedViewID::kNbsWarningView);

  std::unique_ptr<views::ImageView> image_view =
      base::WrapUnique(TrayPopupUtils::CreateMainImageView(
          /*use_wide_layout=*/true));
  image_view->SetImage(
      ui::ImageModel::FromVectorIcon(vector_icons::kNotificationWarningIcon,
                                     kColorAshIconColorWarning, kMenuIconSize));
  nbs_warning_view->AddView(TriView::Container::START, std::move(image_view));

  std::unique_ptr<views::Label> label =
      base::WrapUnique(TrayPopupUtils::CreateDefaultLabel());
  label->SetText(
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_AUDIO_NBS_MESSAGE));
  label->SetMultiLine(/*multi_line=*/true);
  label->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
  label->SetEnabledColorId(kColorAshTextColorWarning);
  label->SetAutoColorReadabilityEnabled(false);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2, *label);

  nbs_warning_view->AddView(TriView::Container::CENTER, std::move(label));
  return nbs_warning_view;
}

std::unique_ptr<HoverHighlightView>
AudioDetailedView::CreateNoiseCancellationToggleRow(const AudioDevice& device) {
  bool noise_cancellation_state =
      CrasAudioHandler::Get()->GetNoiseCancellationState();

  auto noise_cancellation_view =
      std::make_unique<HoverHighlightView>(/*listener=*/this);

  auto toggle_icon =
      std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
          kUnifiedMenuMicNoiseCancelHighIcon, cros_tokens::kCrosSysOnSurface,
          kQsSliderIconSize));
  noise_cancellation_icon_ = toggle_icon.get();

  noise_cancellation_view->AddViewAndLabel(
      std::move(toggle_icon),
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_AUDIO_INPUT_NOISE_CANCELLATION));
  views::Label* noise_cancellation_label =
      noise_cancellation_view->text_label();
  noise_cancellation_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2,
                                        *noise_cancellation_label);

  // Create a non-clickable non-focusable toggle button on the right. The events
  // and focus behavior should be handled by `noise_cancellation_view_` instead.
  auto toggle = std::make_unique<Switch>();
  toggle->SetIsOn(noise_cancellation_state);
  toggle->SetCanProcessEventsWithinSubtree(false);
  toggle->SetFocusBehavior(views::View::FocusBehavior::NEVER);
  // Ignore the toggle for accessibility.
  auto& view_accessibility = toggle->GetViewAccessibility();
  view_accessibility.SetIsLeaf(true);
  view_accessibility.SetIsIgnored(true);
  noise_cancellation_button_ = toggle.get();
  noise_cancellation_view->AddRightView(toggle.release());

  noise_cancellation_view->tri_view()->SetInsets(kToggleButtonRowViewPadding);
  noise_cancellation_view->tri_view()->SetContainerLayout(
      TriView::Container::CENTER, std::make_unique<views::BoxLayout>(
                                      views::BoxLayout::Orientation::kVertical,
                                      kToggleButtonRowLabelPadding));
  noise_cancellation_view->SetPreferredSize(kToggleButtonRowPreferredSize);
  noise_cancellation_view->SetProperty(views::kMarginsKey,
                                       kToggleButtonRowMargins);
  noise_cancellation_view->SetAccessibilityState(
      noise_cancellation_state
          ? HoverHighlightView::AccessibilityState::CHECKED_CHECKBOX
          : HoverHighlightView::AccessibilityState::UNCHECKED_CHECKBOX);

  // This is only used for testing.
  if (g_noise_cancellation_toggle_callback) {
    g_noise_cancellation_toggle_callback->Run(device.id,
                                              noise_cancellation_view.get());
  }

  return noise_cancellation_view;
}

std::unique_ptr<HoverHighlightView>
AudioDetailedView::CreateStyleTransferToggleRow(const AudioDevice& device) {
  auto toggle_icon =
      std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
          kUnifiedMenuMicStyleTransferIcon, cros_tokens::kCrosSysOnSurface,
          kQsSliderIconSize));
  style_transfer_icon_ = toggle_icon.get();

  auto style_transfer_view =
      std::make_unique<HoverHighlightView>(/*listener=*/this);
  style_transfer_view->AddViewAndLabel(
      std::move(toggle_icon),
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_AUDIO_INPUT_STYLE_TRANSFER));
  views::Label* style_transfer_label = style_transfer_view->text_label();
  style_transfer_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2,
                                        *style_transfer_label);

  // Create a non-clickable non-focusable toggle button on the right. The events
  // and focus behavior should be handled by `style_transfer_view_` instead.
  const bool style_transfer_state =
      CrasAudioHandler::Get()->GetStyleTransferState();

  auto toggle = std::make_unique<Switch>();
  toggle->SetIsOn(style_transfer_state);
  toggle->SetCanProcessEventsWithinSubtree(false);
  toggle->SetFocusBehavior(views::View::FocusBehavior::NEVER);
  // Ignore the toggle for accessibility.
  auto& view_accessibility = toggle->GetViewAccessibility();
  view_accessibility.SetIsLeaf(true);
  view_accessibility.SetIsIgnored(true);
  style_transfer_button_ = toggle.get();
  style_transfer_view->AddRightView(toggle.release());

  style_transfer_view->tri_view()->SetInsets(kToggleButtonRowViewPadding);
  style_transfer_view->tri_view()->SetContainerLayout(
      TriView::Container::CENTER, std::make_unique<views::BoxLayout>(
                                      views::BoxLayout::Orientation::kVertical,
                                      kToggleButtonRowLabelPadding));
  style_transfer_view->SetPreferredSize(kToggleButtonRowPreferredSize);
  style_transfer_view->SetProperty(views::kMarginsKey, kToggleButtonRowMargins);
  style_transfer_view->SetAccessibilityState(
      style_transfer_state
          ? HoverHighlightView::AccessibilityState::CHECKED_CHECKBOX
          : HoverHighlightView::AccessibilityState::UNCHECKED_CHECKBOX);

  // This is only used for testing.
  if (g_style_transfer_toggle_callback) {
    g_style_transfer_toggle_callback->Run(device.id, style_transfer_view.get());
  }

  return style_transfer_view;
}

std::unique_ptr<HoverHighlightView> AudioDetailedView::CreateAgcInfoRow(
    const AudioDevice& device) {
  auto agc_info_view = std::make_unique<HoverHighlightView>(/*listener=*/this);
  agc_info_view->SetID(AudioDetailedViewID::kAgcInfoView);

  auto info_icon =
      std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
          kUnifiedMenuInfoIcon, cros_tokens::kCrosSysOnSurface,
          kQsSliderIconSize));
  agc_info_view->AddViewAndLabel(
      std::move(info_icon),
      l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_AUDIO_INPUT_AGC_INFO,
                                 std::u16string()));
  views::Label* text_label = agc_info_view->text_label();
  CHECK(text_label);
  text_label->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);

  if (base::FeatureList::IsEnabled(media::kShowForceRespectUiGainsToggle)) {
    // Add settings button to link to the audio settings page.
    auto settings = std::make_unique<PillButton>(
        base::BindRepeating(&AudioDetailedView::OnSettingsButtonClicked,
                            weak_factory_.GetWeakPtr()),
        l10n_util::GetStringUTF16(
            IDS_ASH_STATUS_TRAY_AUDIO_SETTINGS_SHORT_STRING),
        PillButton::Type::kFloatingWithoutIcon,
        /*icon=*/nullptr);
    if (!TrayPopupUtils::CanOpenWebUISettings()) {
      settings->SetEnabled(false);
    }
    agc_info_view->AddRightView(settings.release());
  }

  agc_info_view->tri_view()->SetInsets(kToggleButtonRowViewPadding);
  agc_info_view->tri_view()->SetContainerLayout(
      TriView::Container::CENTER, std::make_unique<views::BoxLayout>(
                                      views::BoxLayout::Orientation::kVertical,
                                      kToggleButtonRowLabelPadding));
  agc_info_view->SetPreferredSize(kToggleButtonRowPreferredSize);
  agc_info_view->SetProperty(views::kMarginsKey, kToggleButtonRowMargins);
  agc_info_view->SetFocusBehavior(FocusBehavior::NEVER);

  return agc_info_view;
}

LabeledSliderView* AudioDetailedView::CreateLabeledSliderView(
    views::View* container,
    const AudioDevice& device) {
  std::unique_ptr<views::View> slider;
  if (device.is_input) {
    slider = mic_gain_controller_->CreateMicGainSlider(device.id,
                                                       device.IsInternalMic());
  } else {
    slider = unified_volume_slider_controller_->CreateVolumeSlider(device.id);
    if (device.active) {
      views::AsViewClass<QuickSettingsSlider>(
          views::AsViewClass<UnifiedVolumeView>(slider.get())->slider())
          ->set_is_toggleable_volume_slider(true);
    }
  }

  auto* labeled_slider_view = views::AsViewClass<LabeledSliderView>(
      container->AddChildView(std::make_unique<LabeledSliderView>(
          /*detailed_view=*/this, std::move(slider), device,
          /*is_wide_slider=*/false)));
  device_map_[labeled_slider_view->device_name_view()] = device;

  // If the `device_name_container` of this device is previously focused and
  // then becomes active, the slider of this device should preserve the focus.
  if (focused_device_id_ == device.id && device.active) {
    labeled_slider_view->unified_slider_view()->slider()->RequestFocus();
    // Resets the id.
    focused_device_id_ = std::nullopt;
  }

  return labeled_slider_view;
}

void AudioDetailedView::MaybeShowSodaMessage(speech::LanguageCode language_code,
                                             std::u16string message) {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  const bool is_live_caption_enabled = controller->live_caption().enabled();
  // Only show updates for this feature if the language code applies to the SODA
  // binary (encoded by `LanguageCode::kNone`) or the language pack matching
  // the feature locale.
  const bool live_caption_has_update =
      language_code == speech::LanguageCode::kNone ||
      language_code == GetLiveCaptionLocale();

  if (live_caption_has_update && is_live_caption_enabled) {
    live_caption_view_->SetSubText(message);
  }
}

void AudioDetailedView::OnInputNoiseCancellationTogglePressed() {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  const bool new_state = !audio_handler->GetNoiseCancellationState();
  audio_handler->SetNoiseCancellationState(
      new_state, CrasAudioHandler::AudioSettingsChangeSource::kSystemTray);
  noise_cancellation_button_->SetIsOn(new_state);
}

void AudioDetailedView::OnInputStyleTransferTogglePressed() {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  const bool new_state = !audio_handler->GetStyleTransferState();
  audio_handler->SetStyleTransferState(new_state);
  style_transfer_button_->SetIsOn(new_state);
  style_transfer_view_->RequestFocus();
}

void AudioDetailedView::OnSettingsButtonClicked() {
  if (!TrayPopupUtils::CanOpenWebUISettings()) {
    return;
  }

  CloseBubble();  // Deletes |this|.
  Shell::Get()->system_tray_model()->client()->ShowAudioSettings();
}

void AudioDetailedView::ToggleLiveCaptionState() {
  AccessibilityController* controller =
      Shell::Get()->accessibility_controller();
  // Updates the enable state for live caption.
  controller->live_caption().SetEnabled(!controller->live_caption().enabled());
}

void AudioDetailedView::UpdateLiveCaptionView(bool is_enabled) {
  live_caption_icon_->SetImage(ui::ImageModel::FromVectorIcon(
      is_enabled ? kUnifiedMenuLiveCaptionIcon : kUnifiedMenuLiveCaptionOffIcon,
      cros_tokens::kCrosSysOnSurface, kQsSliderIconSize));

  // Updates the toggle button tooltip.
  std::u16string toggle_tooltip =
      is_enabled ? l10n_util::GetStringUTF16(
                       IDS_ASH_STATUS_TRAY_LIVE_CAPTION_ENABLED_STATE_TOOLTIP)
                 : l10n_util::GetStringUTF16(
                       IDS_ASH_STATUS_TRAY_LIVE_CAPTION_DISABLED_STATE_TOOLTIP);
  live_caption_button_->SetTooltipText(l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_LIVE_CAPTION_TOGGLE_TOOLTIP, toggle_tooltip));

  // Ensures the toggle button is in sync with the current Live Caption state.
  if (live_caption_button_->GetIsOn() != is_enabled) {
    live_caption_button_->SetIsOn(is_enabled);
  }

  InvalidateLayout();
}

void AudioDetailedView::UpdateAudioDevices() {
  output_devices_.clear();
  input_devices_.clear();
  AudioDeviceList devices;
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  audio_handler->GetAudioDevices(&devices);
  const bool has_dual_internal_mic = audio_handler->HasDualInternalMic();
  bool is_front_or_rear_mic_active = false;
  for (const auto& device : devices) {
    // Only display devices if they are for simple usage.
    if (!device.is_for_simple_usage()) {
      continue;
    }
    if (device.is_input) {
      // Do not expose the internal front and rear mic to UI.
      if (has_dual_internal_mic && audio_handler->IsFrontOrRearMic(device)) {
        if (device.active) {
          is_front_or_rear_mic_active = true;
        }
        continue;
      }
      input_devices_.push_back(device);
    } else {
      output_devices_.push_back(device);
    }
  }

  // Expose the dual internal mics as one device (internal mic) to user.
  if (has_dual_internal_mic) {
    // Create stub internal mic entry for UI rendering, which representing
    // both internal front and rear mics.
    AudioDevice internal_mic;
    internal_mic.is_input = true;
    // `stable_device_id_version` is used to differentiate `stable_device_id`
    // for backward compatibility. Version 2 means `deprecated_stable_device_id`
    // will contain deprecated, v1 stable device id version.
    internal_mic.stable_device_id_version = 2;
    internal_mic.type = AudioDeviceType::kInternalMic;
    internal_mic.active = is_front_or_rear_mic_active;
    input_devices_.push_back(internal_mic);
  }

  UpdateScrollableList();
}

void AudioDetailedView::AddSeparatorIfNotLast(views::View* container,
                                             const AudioDevice& device) {
  if (device.is_input ? &device != &input_devices_.back()
                      : &device != &output_devices_.back()) {
    AddSeparator(container);
  }
}

void AudioDetailedView::UpdateScrollableList() {
  // Resets all raw pointers inside the `scroll_content()`. Otherwise it can
  // lead to a crash when the the view is clicked. Also clears `device_map_`
  // before removing all child views since it holds pointers to child views in
  // `scroll_content()`.
  noise_cancellation_view_ = nullptr;
  noise_cancellation_icon_ = nullptr;
  noise_cancellation_button_ = nullptr;
  style_transfer_view_ = nullptr;
  style_transfer_icon_ = nullptr;
  style_transfer_button_ = nullptr;
  live_caption_view_ = nullptr;
  live_caption_icon_ = nullptr;
  live_caption_button_ = nullptr;
  device_map_.clear();
  scroll_content()->RemoveAllChildViews();

  views::View* container =
      scroll_content()->AddChildView(std::make_unique<RoundedContainer>());

  if (captions::IsLiveCaptionFeatureSupported()) {
    // Adds the live caption toggle.
    CreateLiveCaptionView();
  }

  // Adds audio output devices.
  const bool has_output_devices = output_devices_.size() > 0;
  if (has_output_devices) {
    AddAudioSubHeader(container, kSystemMenuAudioOutputIcon,
                      IDS_ASH_STATUS_TRAY_AUDIO_OUTPUT);
  }

  LabeledSliderView* last_output_device = nullptr;
  for (const auto& device : output_devices_) {
    last_output_device = CreateLabeledSliderView(container, device);
  }

  if (has_output_devices) {
    last_output_device->SetProperty(views::kMarginsKey, kSubsectionMargins);
  }

  // Adds audio input devices.
  const bool has_input_devices = input_devices_.size() > 0;
  if (has_input_devices) {
    AddAudioSubHeader(container, kSystemMenuAudioInputIcon,
                      IDS_ASH_STATUS_TRAY_AUDIO_INPUT);
  }

  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  for (const auto& device : input_devices_) {
    CreateLabeledSliderView(container, device);

    // AGC info row is only meaningful when UI gains is going to be ignored.
    if (base::FeatureList::IsEnabled(media::kIgnoreUiGains)) {
      if (audio_handler->GetPrimaryActiveInputNode() == device.id) {
        container->AddChildView(AudioDetailedView::CreateAgcInfoRow(device));
        UpdateAgcInfoRow();
      }
    }

    // Adds the input style transfer toggle.
    if (audio_handler->GetPrimaryActiveInputNode() == device.id &&
        audio_handler->IsStyleTransferSupportedForDevice(device.id)) {
      style_transfer_view_ = container->AddChildView(
          AudioDetailedView::CreateStyleTransferToggleRow(device));

      AddSeparatorIfNotLast(container, device);
    }

    // Adds the input noise cancellation toggle.
    if (audio_handler->GetPrimaryActiveInputNode() == device.id &&
        audio_handler->IsNoiseCancellationSupportedForDevice(device.id)) {
      noise_cancellation_view_ = container->AddChildView(
          AudioDetailedView::CreateNoiseCancellationToggleRow(device));

      AddSeparatorIfNotLast(container, device);
    }

    // Adds a warning message if NBS is selected.
    if (audio_handler->GetPrimaryActiveInputNode() == device.id &&
        device.type == AudioDeviceType::kBluetoothNbMic) {
      container->AddChildView(AudioDetailedView::CreateNbsWarningView());
    }
  }

  container->SizeToPreferredSize();
  scroller()->DeprecatedLayoutImmediately();
}

void AudioDetailedView::UpdateAgcInfoRow() {
  if (!base::FeatureList::IsEnabled(media::kIgnoreUiGains)) {
    return;
  }
  if (!scroll_content()) {
    return;
  }
  HoverHighlightView* agc_info_view = static_cast<HoverHighlightView*>(
      scroll_content()->GetViewByID(AudioDetailedViewID::kAgcInfoView));
  if (!agc_info_view) {
    return;
  }
  views::Label* text_label = agc_info_view->text_label();
  CHECK(text_label);

  std::vector<std::string> app_names = GetNamesOfAppsAccessingMic(
      app_registry_cache_, app_capability_access_cache_);

  std::u16string agc_info_text = GetTextForAgcInfo(app_names);
  text_label->SetText(agc_info_text);

  agc_info_view->GetViewAccessibility().SetName(agc_info_text);
  agc_info_view->SetVisible(ShowAgcInfoRow() && !app_names.empty());
}

bool AudioDetailedView::ShowAgcInfoRow() {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  CHECK(audio_handler);

  // If UI gains is not going to be ignored.
  if (!base::FeatureList::IsEnabled(media::kIgnoreUiGains)) {
    return false;
  }
  // If UI gains is to be force respected.
  if (audio_handler->GetForceRespectUiGainsState()) {
    return false;
  }
  // If there's no stream ignoring UI gains.
  if (num_stream_ignore_ui_gains_ == 0) {
    return false;
  }

  return true;
}

void AudioDetailedView::HandleViewClicked(views::View* view) {
  if (live_caption_view_ && view == live_caption_view_) {
    ToggleLiveCaptionState();
    return;
  }

  if (noise_cancellation_view_ && view == noise_cancellation_view_) {
    OnInputNoiseCancellationTogglePressed();
    return;
  }

  if (style_transfer_view_ && view == style_transfer_view_) {
    OnInputStyleTransferTogglePressed();
    return;
  }

  AudioDeviceViewMap::iterator iter = device_map_.find(view);
  if (iter == device_map_.end()) {
    return;
  }
  AudioDevice device = iter->second;

  // If the clicked view is focused, save the id of this device to preserve the
  // focus ring.
  if (view->HasFocus()) {
    focused_device_id_ = device.id;
  }

  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  if (device.type == AudioDeviceType::kInternalMic &&
      audio_handler->HasDualInternalMic()) {
    audio_handler->SwitchToFrontOrRearMic();
  } else {
    audio_handler->SwitchToDevice(device, true,
                                  DeviceActivateType::kActivateByUser);
  }
}

void AudioDetailedView::CreateExtraTitleRowButtons() {
  tri_view()->SetContainerVisible(TriView::Container::END, /*visible=*/true);
  std::unique_ptr<views::Button> settings =
      base::WrapUnique(CreateSettingsButton(
          base::BindRepeating(&AudioDetailedView::OnSettingsButtonClicked,
                              weak_factory_.GetWeakPtr()),
          IDS_ASH_STATUS_TRAY_AUDIO_SETTINGS));
  settings->SetProperty(
      views::kElementIdentifierKey,
      kQuickSettingsAudioDetailedViewAudioSettingsButtonElementId);
  settings_button_ =
      tri_view()->AddView(TriView::Container::END, std::move(settings));
}

// SodaInstaller::Observer:
void AudioDetailedView::OnSodaInstalled(speech::LanguageCode language_code) {
  std::u16string message = l10n_util::GetStringUTF16(
      IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_COMPLETE);
  MaybeShowSodaMessage(language_code, message);
}

void AudioDetailedView::OnSodaInstallError(
    speech::LanguageCode language_code,
    speech::SodaInstaller::ErrorCode error_code) {
  std::u16string error_message;
  switch (error_code) {
    case speech::SodaInstaller::ErrorCode::kUnspecifiedError: {
      error_message = l10n_util::GetStringUTF16(
          IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR);
      break;
    }
    case speech::SodaInstaller::ErrorCode::kNeedsReboot: {
      error_message = l10n_util::GetStringUTF16(
          IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR_REBOOT_REQUIRED);
      break;
    }
  }

  MaybeShowSodaMessage(language_code, error_message);
}

void AudioDetailedView::OnSodaProgress(speech::LanguageCode language_code,
                                       int progress) {
  std::u16string message = l10n_util::GetStringFUTF16Int(
      IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_PROGRESS, progress);
  MaybeShowSodaMessage(language_code, message);
}

void AudioDetailedView::OnOutputMuteChanged(bool mute_on) {
  MaybeUpdateActiveDeviceColor(/*is_input=*/false, mute_on, device_map_);
}

void AudioDetailedView::OnInputMuteChanged(
    bool mute_on,
    CrasAudioHandler::InputMuteChangeMethod method) {
  MaybeUpdateActiveDeviceColor(/*is_input=*/true, mute_on, device_map_);
}

void AudioDetailedView::OnInputMutedByMicrophoneMuteSwitchChanged(bool muted) {
  MaybeUpdateActiveDeviceColor(/*is_input=*/true, muted, device_map_);
}

void AudioDetailedView::OnNumStreamIgnoreUiGainsChanged(int32_t num) {
  num_stream_ignore_ui_gains_ = num;
  UpdateAgcInfoRow();
}

BEGIN_METADATA(AudioDetailedView)
END_METADATA

}  // namespace ash