chromium/ash/system/audio/unified_audio_detailed_view_controller_unittest.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/audio/unified_audio_detailed_view_controller.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/switch.h"
#include "ash/system/audio/audio_detailed_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/tray/hover_highlight_view.h"
#include "ash/system/unified/unified_slider_view.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/system/unified/unified_system_tray_model.h"
#include "ash/test/ash_test_base.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/components/audio/audio_devices_pref_handler.h"
#include "chromeos/ash/components/audio/audio_devices_pref_handler_stub.h"
#include "chromeos/ash/components/dbus/audio/cras_audio_client.h"
#include "chromeos/ash/components/dbus/audio/fake_cras_audio_client.h"
#include "components/live_caption/pref_names.h"
#include "components/soda/soda_installer_impl_chromeos.h"
#include "media/base/media_switches.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/events/base_event_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/slider.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {

constexpr uint64_t kMicJackId = 10010;
constexpr uint64_t kInternalMicId = 10003;
constexpr uint64_t kInternalMicStyleTransferId = 10004;
constexpr uint64_t kFrontMicId = 10012;
constexpr uint64_t kRearMicId = 10013;
constexpr uint64_t kNbsMicId = 10020;
const uint64_t kInternalSpeakerId = 10001;
const uint64_t kHeadphoneId = 10002;
constexpr uint64_t kDualInternalMicId = 0;
const int kFrontMicGainPercent = 50;
const int kRearMicGainPercent = 100;

const std::u16string kInitialLiveCaptionViewSubtitleText = u"This is a test";
const std::u16string kSodaDownloaded = u"Speech files downloaded";
const std::u16string kSodaInProgress25 =
    u"Downloading speech recognition files… 25%";
const std::u16string kSodaInProgress50 =
    u"Downloading speech recognition files… 50%";
const std::u16string kSodaFailed =
    u"Can't download speech files. Try again later.";

speech::LanguageCode en_us() {
  return speech::LanguageCode::kEnUs;
}

speech::LanguageCode fr_fr() {
  return speech::LanguageCode::kFrFr;
}

struct AudioNodeInfo {
  bool is_input;
  uint64_t id;
  const char* const device_name;
  const char* const type;
  const char* const name;
  const uint32_t audio_effect;
};

const uint32_t kInputMaxSupportedChannels = 1;
const uint32_t kOutputMaxSupportedChannels = 2;

const int32_t kInputNumberOfVolumeSteps = 0;
const int32_t kOutputNumberOfVolumeSteps = 25;

const AudioNodeInfo kNbsMic[] = {
    {true, kNbsMicId, "Fake Nbs Mic", "BLUETOOTH_NB_MIC", "Nbs Mic", 0}};

const AudioNodeInfo kMicJack[] = {
    {true, kMicJackId, "Fake Mic Jack", "MIC", "Mic Jack", 0}};

const AudioNodeInfo kInternalMic[] = {{true, kInternalMicId, "Fake Mic",
                                       "INTERNAL_MIC", "Internal Mic",
                                       cras::EFFECT_TYPE_NOISE_CANCELLATION}};

const AudioNodeInfo kInternalMicStyleTransfer[] = {
    {true, kInternalMicStyleTransferId, "Fake Mic", "INTERNAL_MIC", "Internal Mic",
     cras::EFFECT_TYPE_STYLE_TRANSFER}};

const AudioNodeInfo kFrontMic[] = {
    {true, kFrontMicId, "Fake Front Mic", "FRONT_MIC", "Front Mic", 0}};

const AudioNodeInfo kRearMic[] = {
    {true, kRearMicId, "Fake Rear Mic", "REAR_MIC", "Rear Mic", 0}};

const AudioNodeInfo kInternalSpeaker[] = {{false, kInternalSpeakerId,
                                           "Fake Speaker", "INTERNAL_SPEAKER",
                                           "Speaker", 0}};

const AudioNodeInfo kHeadphone[] = {
    {false, kHeadphoneId, "Fake Headphone", "HEADPHONE", "Headphone", 0}};

AudioNode GenerateAudioNode(const AudioNodeInfo* node_info) {
  uint64_t stable_device_id_v2 = 0;
  uint64_t stable_device_id_v1 = node_info->id;
  return AudioNode(node_info->is_input, node_info->id, false,
                   stable_device_id_v1, stable_device_id_v2,
                   node_info->device_name, node_info->type, node_info->name,
                   false /* is_active*/, 0 /* pluged_time */,
                   node_info->is_input ? kInputMaxSupportedChannels
                                       : kOutputMaxSupportedChannels,
                   node_info->audio_effect,
                   node_info->is_input ? kInputNumberOfVolumeSteps
                                       : kOutputNumberOfVolumeSteps);
}

AudioNodeList GenerateAudioNodeList(
    const std::vector<const AudioNodeInfo*>& nodes) {
  AudioNodeList node_list;
  for (auto* node_info : nodes) {
    node_list.push_back(GenerateAudioNode(node_info));
  }
  return node_list;
}

}  // namespace

// Test param is the version of stable device id used by audio node.
class UnifiedAudioDetailedViewControllerTest : public AshTestBase {
 public:
  UnifiedAudioDetailedViewControllerTest() = default;
  ~UnifiedAudioDetailedViewControllerTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    audio_pref_handler_ = base::MakeRefCounted<AudioDevicesPrefHandlerStub>();
    cras_audio_handler_ = CrasAudioHandler::Get();
    cras_audio_handler_->SetPrefHandlerForTesting(audio_pref_handler_);

    tray_model_ = base::MakeRefCounted<UnifiedSystemTrayModel>(nullptr);
    tray_controller_ =
        std::make_unique<UnifiedSystemTrayController>(tray_model_.get());
    audio_detailed_view_controller_ =
        std::make_unique<UnifiedAudioDetailedViewController>(
            tray_controller_.get());

    map_input_device_sliders_callback_ = base::BindRepeating(
        &UnifiedAudioDetailedViewControllerTest::AddViewToInputSliderDeviceMap,
        base::Unretained(this));
    MicGainSliderController::SetMapDeviceSliderCallbackForTest(
        &map_input_device_sliders_callback_);

    map_output_device_sliders_callback_ = base::BindRepeating(
        &UnifiedAudioDetailedViewControllerTest::AddViewToOutputSliderDeviceMap,
        base::Unretained(this));
    UnifiedVolumeSliderController::SetMapDeviceSliderCallbackForTest(
        &map_output_device_sliders_callback_);

    noise_cancellation_toggle_callback_ =
        base::BindRepeating(&UnifiedAudioDetailedViewControllerTest::
                                AddViewToNoiseCancellationToggleMap,
                            base::Unretained(this));
    AudioDetailedView::SetMapNoiseCancellationToggleCallbackForTest(
        &noise_cancellation_toggle_callback_);

    style_transfer_toggle_callback_ =
        base::BindRepeating(&UnifiedAudioDetailedViewControllerTest::
                                AddViewToStyleTransferToggleMap,
                            base::Unretained(this));
    AudioDetailedView::SetMapStyleTransferToggleCallbackForTest(
        &style_transfer_toggle_callback_);
  }

  void TearDown() override {
    MicGainSliderController::SetMapDeviceSliderCallbackForTest(nullptr);
    UnifiedVolumeSliderController::SetMapDeviceSliderCallbackForTest(nullptr);
    audio_pref_handler_ = nullptr;
    audio_detailed_view_ = nullptr;
    audio_detailed_view_.reset();
    audio_detailed_view_controller_.reset();
    tray_controller_.reset();
    tray_model_.reset();

    AshTestBase::TearDown();
  }

  void AddViewToInputSliderDeviceMap(uint64_t device_id, views::View* view) {
    input_sliders_map_[device_id] = view;
  }

  void AddViewToOutputSliderDeviceMap(uint64_t device_id, views::View* view) {
    output_sliders_map_[device_id] = view;
  }

  void AddViewToNoiseCancellationToggleMap(uint64_t device_id,
                                           views::View* view) {
    toggles_map_[device_id] = view;
  }

  void AddViewToStyleTransferToggleMap(uint64_t device_id, views::View* view) {
    style_transfer_toggles_map_[device_id] = view;
  }

  void ToggleLiveCaption() {
    GetAudioDetailedView()->HandleViewClicked(live_caption_view());
  }

  // Toggles the noise cancellation button.
  void ToggleNoiseCancellation() {
    GetAudioDetailedView()->HandleViewClicked(
        GetAudioDetailedView()->noise_cancellation_view_);
  }

  // Toggles the style transfer button.
  void ToggleStyleTransfer(AudioDetailedView* audio_detailed_view = nullptr) {
    AudioDetailedView* view =
        audio_detailed_view ? audio_detailed_view : GetAudioDetailedView();
    view->HandleViewClicked(view->style_transfer_view_);
  }

 protected:
  FakeCrasAudioClient* fake_cras_audio_client() {
    return FakeCrasAudioClient::Get();
  }

  AudioDetailedView* GetAudioDetailedView() {
    if (!audio_detailed_view_) {
      audio_detailed_view_ = base::WrapUnique(static_cast<AudioDetailedView*>(
          audio_detailed_view_controller_->CreateView().release()));
    }
    return audio_detailed_view_.get();
  }

  void CheckSliderFocusBehavior(bool is_input_slider, uint64_t device_id) {
    SCOPED_TRACE(
        base::StringPrintf("Test params: is_input_slider=%d", is_input_slider));

    auto sliders_map =
        is_input_slider ? input_sliders_map_ : output_sliders_map_;
    EXPECT_TRUE(base::Contains(sliders_map, device_id));

    auto* unified_slider_view =
        static_cast<UnifiedSliderView*>(sliders_map.find(device_id)->second);

    views::Slider* slider = unified_slider_view->slider();
    IconButton* slider_button = unified_slider_view->slider_button();
    // `slider` is normally focusable, and `slider_button` is accessibility
    // focusable.
    EXPECT_TRUE(slider->IsFocusable());
    EXPECT_FALSE(slider_button->IsFocusable());
    EXPECT_TRUE(
        slider_button->GetViewAccessibility().IsAccessibilityFocusable());

    slider->RequestFocus();
    EXPECT_STREQ(slider->GetFocusManager()->GetFocusedView()->GetClassName(),
                 "QuickSettingsSlider");

    const bool is_muted =
        is_input_slider
            ? cras_audio_handler_->IsInputMutedForDevice(device_id)
            : cras_audio_handler_->IsOutputMutedForDevice(device_id);

    // Presses the enter key when focused on the slider will toggle mute state.
    GetEventGenerator()->PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
    if (is_input_slider) {
      EXPECT_EQ(cras_audio_handler_->IsInputMutedForDevice(device_id),
                !is_muted);
    } else {
      EXPECT_EQ(cras_audio_handler_->IsOutputMutedForDevice(device_id),
                !is_muted);
    }
  }

  HoverHighlightView* live_caption_view() {
    return GetAudioDetailedView()->live_caption_view_;
  }

  views::ToggleButton* noise_cancellation_button() {
    return GetAudioDetailedView()->noise_cancellation_button_;
  }

  views::ToggleButton* style_transfer_button() {
    return GetAudioDetailedView()->style_transfer_button_;
  }

  bool live_caption_enabled() {
    return Shell::Get()->accessibility_controller()->live_caption().enabled();
  }

  views::View* nbs_warning_view() {
    return GetAudioDetailedView()->GetViewByID(
        AudioDetailedView::AudioDetailedViewID::kNbsWarningView);
  }

  std::map<uint64_t, views::View*> input_sliders_map_;
  std::map<uint64_t, views::View*> output_sliders_map_;
  std::map<uint64_t, views::View*> toggles_map_;
  std::map<uint64_t, views::View*> style_transfer_toggles_map_;
  MicGainSliderController::MapDeviceSliderCallback
      map_input_device_sliders_callback_;
  UnifiedVolumeSliderController::MapDeviceSliderCallback
      map_output_device_sliders_callback_;
  AudioDetailedView::NoiseCancellationCallback
      noise_cancellation_toggle_callback_;
  AudioDetailedView::StyleTransferCallback style_transfer_toggle_callback_;
  raw_ptr<CrasAudioHandler, DanglingUntriaged> cras_audio_handler_ =
      nullptr;  // Not owned.
  scoped_refptr<AudioDevicesPrefHandlerStub> audio_pref_handler_;
  std::unique_ptr<UnifiedAudioDetailedViewController>
      audio_detailed_view_controller_;
  scoped_refptr<UnifiedSystemTrayModel> tray_model_;
  std::unique_ptr<UnifiedSystemTrayController> tray_controller_;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<AudioDetailedView> audio_detailed_view_;
  base::HistogramTester histogram_tester_;
};

TEST_F(UnifiedAudioDetailedViewControllerTest, ToggleNbsWarning) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kMicJack, kNbsMic}));

  auto jack_mic = AudioDevice(GenerateAudioNode(kMicJack));
  cras_audio_handler_->SwitchToDevice(jack_mic, true,
                                      DeviceActivateType::kActivateByUser);
  EXPECT_FALSE(nbs_warning_view());

  auto nbs_mic = AudioDevice(GenerateAudioNode(kNbsMic));
  cras_audio_handler_->SwitchToDevice(nbs_mic, true,
                                      DeviceActivateType::kActivateByUser);
  EXPECT_TRUE(nbs_warning_view());

  cras_audio_handler_->SwitchToDevice(jack_mic, true,
                                      DeviceActivateType::kActivateByUser);
  EXPECT_FALSE(nbs_warning_view());
}

TEST_F(UnifiedAudioDetailedViewControllerTest, OneInputSlider) {
  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kMicJack}));

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMic)), true,
      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(kInternalMicId, cras_audio_handler_->GetPrimaryActiveInputNode());
  EXPECT_TRUE(input_sliders_map_.find(kInternalMicId)->second->GetVisible());

  // Both sliders should be visible.
  EXPECT_TRUE(input_sliders_map_.find(kMicJackId)->second->GetVisible());

  cras_audio_handler_->SwitchToDevice(AudioDevice(GenerateAudioNode(kMicJack)),
                                      true,
                                      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(kMicJackId, cras_audio_handler_->GetPrimaryActiveInputNode());
  EXPECT_TRUE(input_sliders_map_.find(kMicJackId)->second->GetVisible());

  // Both sliders should be visible.
  EXPECT_TRUE(input_sliders_map_.find(kInternalMicId)->second->GetVisible());
}

TEST_F(UnifiedAudioDetailedViewControllerTest, OneOutputSlider) {
  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalSpeaker, kHeadphone}));

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalSpeaker)), true,
      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(kInternalSpeakerId,
            cras_audio_handler_->GetPrimaryActiveOutputNode());
  // Both sliders should be visible.
  EXPECT_TRUE(
      output_sliders_map_.find(kInternalSpeakerId)->second->GetVisible());
  EXPECT_TRUE(output_sliders_map_.find(kHeadphoneId)->second->GetVisible());

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kHeadphone)), true,
      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(kHeadphoneId, cras_audio_handler_->GetPrimaryActiveOutputNode());
  // Both sliders should be visible.
  EXPECT_TRUE(output_sliders_map_.find(kHeadphoneId)->second->GetVisible());
  EXPECT_TRUE(
      output_sliders_map_.find(kInternalSpeakerId)->second->GetVisible());
}

TEST_F(UnifiedAudioDetailedViewControllerTest, DualInternalMicSlider) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kFrontMic, kRearMic}));

  // Verify the device has dual internal mics.
  EXPECT_TRUE(cras_audio_handler_->HasDualInternalMic());

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();

  // Verify there is only 1 slider in the view.
  EXPECT_EQ(input_sliders_map_.size(), 1u);

  // Verify the slider is visible.
  EXPECT_TRUE(input_sliders_map_.begin()->second->GetVisible());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       DualInternalMicSliderActiveState) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kFrontMic, kRearMic}));

  // Verify the device has dual internal mics.
  EXPECT_TRUE(cras_audio_handler_->HasDualInternalMic());

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();

  // Verify there is only 1 slider in the view.
  EXPECT_EQ(input_sliders_map_.size(), 1u);

  auto* mic_gain_slider_view =
      static_cast<MicGainSliderView*>(input_sliders_map_.begin()->second);

  // Verify the slider is visible.
  EXPECT_TRUE(mic_gain_slider_view->GetVisible());

  // Verifies the slider is active since it's the only slider.
  EXPECT_EQ(static_cast<QuickSettingsSlider*>(mic_gain_slider_view->slider())
                ->slider_style(),
            QuickSettingsSlider::Style::kRadioActive);
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       DualInternalMicSliderInactiveState) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kFrontMic, kRearMic, kMicJack}));

  // Verify the device has dual internal mics.
  EXPECT_TRUE(cras_audio_handler_->HasDualInternalMic());

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();

  // Verify there are 2 sliders in the view and one of them is the new dual
  // internal mic.
  EXPECT_EQ(input_sliders_map_.size(), 2u);
  EXPECT_TRUE(input_sliders_map_.contains(kDualInternalMicId));

  // Sets different volume gain levels for front and rear mics.
  cras_audio_handler_->SetVolumeGainPercentForDevice(kFrontMicId,
                                                     kFrontMicGainPercent);
  cras_audio_handler_->SetVolumeGainPercentForDevice(kRearMicId,
                                                     kRearMicGainPercent);

  // Switches to `kMicJack` to make the internal mic inactive.
  cras_audio_handler_->SwitchToDevice(AudioDevice(GenerateAudioNode(kMicJack)),
                                      true,
                                      DeviceActivateType::kActivateByUser);

  // Verifies the dual internal mic slider is inactive and its volume level
  // equals to the front mic's level.
  auto* mic_gain_slider_view = static_cast<MicGainSliderView*>(
      input_sliders_map_.find(kDualInternalMicId)->second);
  EXPECT_EQ(static_cast<QuickSettingsSlider*>(mic_gain_slider_view->slider())
                ->slider_style(),
            QuickSettingsSlider::Style::kRadioInactive);
  EXPECT_EQ(mic_gain_slider_view->slider()->GetValue(),
            kFrontMicGainPercent / 100.0);
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       NoiseCancellationToggleNotDisplayedIfNotSupported) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetNoiseCancellationSupported(false);

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMic)), true,
      DeviceActivateType::kActivateByUser);

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();
  EXPECT_EQ(0u, toggles_map_.size());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       NoiseCancellationToggleDisplayedIfSupportedAndInternal) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetNoiseCancellationSupported(true);
  cras_audio_handler_->RequestNoiseCancellationSupported(base::DoNothing());

  auto internal_mic = AudioDevice(GenerateAudioNode(kInternalMic));
  cras_audio_handler_->SwitchToDevice(internal_mic, true,
                                      DeviceActivateType::kActivateByUser);

  // If `audio_detailed_view_` doesn't exist, this getter method will create the
  // view first.
  GetAudioDetailedView();
  EXPECT_EQ(1u, toggles_map_.size());
  noise_cancellation_button()->GetIsOn();
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       NoiseCancellationToggleChangesPrefAndSendsDbusSignal) {
  audio_pref_handler_->SetNoiseCancellationState(false);

  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetNoiseCancellationSupported(true);
  cras_audio_handler_->RequestNoiseCancellationSupported(base::DoNothing());

  auto internal_mic = AudioDevice(GenerateAudioNode(kInternalMic));
  cras_audio_handler_->SwitchToDevice(internal_mic, true,
                                      DeviceActivateType::kActivateByUser);

  // If `audio_detailed_view_` doesn't exist, this getter method will create the
  // view first.
  GetAudioDetailedView();
  EXPECT_EQ(1u, toggles_map_.size());

  views::ToggleButton* toggle = noise_cancellation_button();
  auto widget = CreateFramelessTestWidget();
  widget->SetContentsView(toggle);

  // The toggle loaded the pref correctly.
  EXPECT_FALSE(toggle->GetIsOn());
  EXPECT_FALSE(audio_pref_handler_->GetNoiseCancellationState());

  // The entire row of `noise_cancellation_view_` is clickable.
  ToggleNoiseCancellation();
  EXPECT_TRUE(audio_pref_handler_->GetNoiseCancellationState());

  ToggleNoiseCancellation();
  EXPECT_FALSE(audio_pref_handler_->GetNoiseCancellationState());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       NoiseCancellationUpdatedWhenDeviceChanges) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetNoiseCancellationSupported(true);
  cras_audio_handler_->RequestNoiseCancellationSupported(base::DoNothing());

  cras_audio_handler_->SwitchToDevice(AudioDevice(GenerateAudioNode(kMicJack)),
                                      true,
                                      DeviceActivateType::kActivateByUser);

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();

  EXPECT_EQ(0u, toggles_map_.size());

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMic)), true,
      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(1u, toggles_map_.size());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       NoiseCancellationUpdatedWhenOnNoiseCancellationChanges) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetNoiseCancellationSupported(true);
  cras_audio_handler_->RequestNoiseCancellationSupported(base::DoNothing());

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMic)), true,
      DeviceActivateType::kActivateByUser);

  // If `audio_detailed_view_` doesn't exist, this getter method will create the
  // view first.
  GetAudioDetailedView();

  auto widget = CreateFramelessTestWidget();
  widget->SetContentsView(noise_cancellation_button());

  // The noise cancellation button loaded the pref correctly.
  EXPECT_TRUE(noise_cancellation_button()->GetIsOn());
  EXPECT_TRUE(audio_pref_handler_->GetNoiseCancellationState());

  cras_audio_handler_->SetNoiseCancellationState(
      /*noise_cancellation_on=*/false,
      CrasAudioHandler::AudioSettingsChangeSource::kSystemTray);

  // The noise cancellation button updates the pref correctly.
  EXPECT_FALSE(noise_cancellation_button()->GetIsOn());
  EXPECT_FALSE(audio_pref_handler_->GetNoiseCancellationState());
  histogram_tester_.ExpectBucketCount(
      CrasAudioHandler::kNoiseCancellationEnabledSourceHistogramName,
      CrasAudioHandler::AudioSettingsChangeSource::kSystemTray, 1);
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       StyleTransferToggleNotDisplayedIfNotSupported) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList(
          {kInternalMicStyleTransfer, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetStyleTransferSupported(false);

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMicStyleTransfer)), true,
      DeviceActivateType::kActivateByUser);

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();
  EXPECT_EQ(0u, style_transfer_toggles_map_.size());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       StyleTransferToggleDisplayedIfSupportedAndInternal) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList(
          {kInternalMicStyleTransfer, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetStyleTransferSupported(true);
  cras_audio_handler_->RequestStyleTransferSupported(base::DoNothing());

  auto internal_mic =
      AudioDevice(GenerateAudioNode(kInternalMicStyleTransfer));
  cras_audio_handler_->SwitchToDevice(internal_mic, true,
                                      DeviceActivateType::kActivateByUser);

  // If `audio_detailed_view_` doesn't exist, this getter method will create the
  // view first.
  GetAudioDetailedView();
  EXPECT_EQ(1u, style_transfer_toggles_map_.size());
  style_transfer_button()->GetIsOn();
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       StyleTransferToggleChangesPrefAndSendsDbusSignal) {
  audio_pref_handler_->SetStyleTransferState(false);

  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList(
          {kInternalMicStyleTransfer, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetStyleTransferSupported(true);
  cras_audio_handler_->RequestStyleTransferSupported(base::DoNothing());

  auto internal_mic =
      AudioDevice(GenerateAudioNode(kInternalMicStyleTransfer));
  cras_audio_handler_->SwitchToDevice(internal_mic, true,
                                      DeviceActivateType::kActivateByUser);

  // If `audio_detailed_view_` doesn't exist, this getter method will create the
  // view first.
  GetAudioDetailedView();
  EXPECT_EQ(1u, style_transfer_toggles_map_.size());

  views::ToggleButton* toggle = style_transfer_button();
  auto widget = CreateFramelessTestWidget();
  widget->SetContentsView(toggle);

  // The toggle loaded the pref correctly.
  EXPECT_FALSE(toggle->GetIsOn());
  EXPECT_FALSE(audio_pref_handler_->GetStyleTransferState());

  // The entire row of `style_transfer_view_` is clickable.
  ToggleStyleTransfer();
  EXPECT_TRUE(audio_pref_handler_->GetStyleTransferState());

  ToggleStyleTransfer();
  EXPECT_FALSE(audio_pref_handler_->GetStyleTransferState());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       StyleTransferUpdatedWhenDeviceChanges) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList(
          {kInternalMicStyleTransfer, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetStyleTransferSupported(true);
  cras_audio_handler_->RequestStyleTransferSupported(base::DoNothing());

  cras_audio_handler_->SwitchToDevice(AudioDevice(GenerateAudioNode(kMicJack)),
                                      true,
                                      DeviceActivateType::kActivateByUser);

  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();

  EXPECT_EQ(0u, style_transfer_toggles_map_.size());

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMicStyleTransfer)), true,
      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(1u, style_transfer_toggles_map_.size());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       StyleTransferUpdatedWhenOnStyleTransferChanges) {
  audio_pref_handler_->SetStyleTransferState(true);

  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList(
          {kInternalMicStyleTransfer, kMicJack, kFrontMic, kRearMic}));
  fake_cras_audio_client()->SetStyleTransferSupported(true);
  cras_audio_handler_->RequestStyleTransferSupported(base::DoNothing());

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMicStyleTransfer)), true,
      DeviceActivateType::kActivateByUser);

  // If `audio_detailed_view_` doesn't exist, this getter method will create the
  // view first.
  GetAudioDetailedView();

  auto widget = CreateFramelessTestWidget();
  widget->SetContentsView(style_transfer_button());

  // The style transfer button loaded the pref correctly.
  EXPECT_TRUE(style_transfer_button()->GetIsOn());
  EXPECT_TRUE(audio_pref_handler_->GetStyleTransferState());

  cras_audio_handler_->SetStyleTransferState(/*style_transfer_on=*/false);

  // The style transfer button updates the pref correctly.
  EXPECT_FALSE(style_transfer_button()->GetIsOn());
  EXPECT_FALSE(audio_pref_handler_->GetStyleTransferState());
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       StyleTransferViewHasFocusWhenPressed) {
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMicStyleTransfer}));
  fake_cras_audio_client()->SetStyleTransferSupported(true);
  cras_audio_handler_->RequestStyleTransferSupported(base::DoNothing());

  auto internal_mic = AudioDevice(GenerateAudioNode(kInternalMicStyleTransfer));
  cras_audio_handler_->SwitchToDevice(internal_mic, true,
                                      DeviceActivateType::kActivateByUser);

  // A widget is required to make `HasFocus` take effect.
  std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
  std::unique_ptr<views::View> audio_detailed_view =
      audio_detailed_view_controller_->CreateView();
  widget->SetContentsView(audio_detailed_view.get());

  ASSERT_EQ(1u, style_transfer_toggles_map_.size());
  EXPECT_FALSE(style_transfer_toggles_map_.begin()->second->HasFocus());

  ToggleStyleTransfer(
      static_cast<AudioDetailedView*>(audio_detailed_view.get()));

  ASSERT_EQ(1u, style_transfer_toggles_map_.size());
  EXPECT_TRUE(style_transfer_toggles_map_.begin()->second->HasFocus());
}

TEST_F(UnifiedAudioDetailedViewControllerTest, ToggleLiveCaption) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeatures(
      {ash::features::kOnDeviceSpeechRecognition}, {});

  EXPECT_TRUE(live_caption_view());
  EXPECT_FALSE(live_caption_enabled());

  ToggleLiveCaption();
  EXPECT_TRUE(live_caption_view());
  EXPECT_TRUE(live_caption_enabled());

  ToggleLiveCaption();
  EXPECT_TRUE(live_caption_view());
  EXPECT_FALSE(live_caption_enabled());
}

TEST_F(UnifiedAudioDetailedViewControllerTest, LiveCaptionNotAvailable) {
  // If the Live Caption feature flags are not set, the Live Caption toggle will
  // not be visible in audio settings.
  EXPECT_FALSE(live_caption_view());
  EXPECT_FALSE(live_caption_enabled());
}

TEST_F(UnifiedAudioDetailedViewControllerTest, SliderFocusToggleMute) {
  auto widget = CreateFramelessTestWidget();
  widget->SetContentsView(audio_detailed_view_controller_->CreateView());
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kInternalMic, kInternalSpeaker}));

  // Sets the level to make sure the slider's volume is not 0. Otherwise the
  // slider is still muted even if it's toggled on.
  const int gain = 80;
  cras_audio_handler_->SetVolumeGainPercentForDevice(kInternalMicId, gain);
  cras_audio_handler_->SetVolumeGainPercentForDevice(kInternalSpeakerId, gain);

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalMic)), true,
      DeviceActivateType::kActivateByUser);
  CheckSliderFocusBehavior(/*is_input_slider=*/true, kInternalMicId);

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kInternalSpeaker)), true,
      DeviceActivateType::kActivateByUser);
  CheckSliderFocusBehavior(/*is_input_slider=*/false, kInternalSpeakerId);
}

TEST_F(UnifiedAudioDetailedViewControllerTest,
       BluetoothOutputDeviceVolumeChange) {
  std::unique_ptr<views::View> view =
      audio_detailed_view_controller_->CreateView();
  fake_cras_audio_client()->SetAudioNodesAndNotifyObserversForTesting(
      GenerateAudioNodeList({kHeadphone}));

  cras_audio_handler_->SwitchToDevice(
      AudioDevice(GenerateAudioNode(kHeadphone)), true,
      DeviceActivateType::kActivateByUser);
  EXPECT_EQ(kHeadphoneId, cras_audio_handler_->GetPrimaryActiveOutputNode());

  // Sets a volume level greater than 0 and the device is unmuted.
  cras_audio_handler_->SetVolumeGainPercentForDevice(kHeadphoneId,
                                                     /*value=*/10);
  EXPECT_FALSE(cras_audio_handler_->IsOutputMutedForDevice(kHeadphoneId));

  // Sets the device level to 0. The mute state is controlled by the mute
  // button so the output is still unmuted.
  cras_audio_handler_->SetVolumeGainPercentForDevice(kHeadphoneId, /*value=*/0);
  EXPECT_FALSE(cras_audio_handler_->IsOutputMutedForDevice(kHeadphoneId));
}

class UnifiedAudioDetailedViewControllerSodaTest
    : public UnifiedAudioDetailedViewControllerTest {
 protected:
  UnifiedAudioDetailedViewControllerSodaTest() = default;
  UnifiedAudioDetailedViewControllerSodaTest(
      const UnifiedAudioDetailedViewControllerSodaTest&) = delete;
  UnifiedAudioDetailedViewControllerSodaTest& operator=(
      const UnifiedAudioDetailedViewControllerSodaTest&) = delete;
  ~UnifiedAudioDetailedViewControllerSodaTest() override = default;

  void SetUp() override {
    UnifiedAudioDetailedViewControllerTest::SetUp();
    // Since this test suite is part of ash unit tests, the
    // SodaInstallerImplChromeOS is never created (it's normally created when
    // `ChromeBrowserMainPartsAsh` initializes). Create it here so that
    // calling speech::SodaInstaller::GetInstance() returns a valid instance.
    scoped_feature_list_.InitWithFeatures(
        {ash::features::kOnDeviceSpeechRecognition,
         media::kLiveCaptionMultiLanguage},
        {});
    soda_installer_impl_ =
        std::make_unique<speech::SodaInstallerImplChromeOS>();

    EnableLiveCaption(true);
    SetLiveCaptionViewSubtitleText(kInitialLiveCaptionViewSubtitleText);
    SetLiveCaptionLocale("en-US");
  }

  void TearDown() override {
    soda_installer_impl_.reset();
    UnifiedAudioDetailedViewControllerTest::TearDown();
  }

  void EnableLiveCaption(bool enabled) {
    Shell::Get()->accessibility_controller()->live_caption().SetEnabled(
        enabled);
  }

  void SetLiveCaptionLocale(const std::string& locale) {
    Shell::Get()->session_controller()->GetActivePrefService()->SetString(
        ::prefs::kLiveCaptionLanguageCode, locale);
  }

  speech::SodaInstaller* soda_installer() {
    return speech::SodaInstaller::GetInstance();
  }

  void SetLiveCaptionViewSubtitleText(std::u16string text) {
    live_caption_view()->SetSubText(text);
  }

  std::u16string GetLiveCaptionViewSubtitleText() {
    return live_caption_view()->sub_text_label()->GetText();
  }

 private:
  std::unique_ptr<speech::SodaInstallerImplChromeOS> soda_installer_impl_;
};

// Ensures that the Dictation subtitle changes when SODA AND the language pack
// matching the Live Caption locale are installed.
TEST_F(UnifiedAudioDetailedViewControllerSodaTest,
       OnSodaInstalledNotification) {
  SetLiveCaptionLocale("fr-FR");

  // Pretend that the SODA binary was installed. We still need to wait for the
  // correct language pack before doing anything.
  soda_installer()->NotifySodaInstalledForTesting();
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaInstalledForTesting(en_us());
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaInstalledForTesting(fr_fr());
  EXPECT_EQ(kSodaDownloaded, GetLiveCaptionViewSubtitleText());
}

// Ensures we only notify the user of progress for the language pack matching
// the Live Caption locale.
TEST_F(UnifiedAudioDetailedViewControllerSodaTest, OnSodaProgressNotification) {
  SetLiveCaptionLocale("en-US");

  soda_installer()->NotifySodaProgressForTesting(75, fr_fr());
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaProgressForTesting(50);
  EXPECT_EQ(kSodaInProgress50, GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaProgressForTesting(25, en_us());
  EXPECT_EQ(kSodaInProgress25, GetLiveCaptionViewSubtitleText());
}

// Ensures we notify the user of an error when the SODA binary fails to
// download.
TEST_F(UnifiedAudioDetailedViewControllerSodaTest,
       SodaBinaryErrorNotification) {
  soda_installer()->NotifySodaErrorForTesting();
  EXPECT_EQ(kSodaFailed, GetLiveCaptionViewSubtitleText());
}

// Ensures we only notify the user of an error if the failed language pack
// matches the Live Caption locale.
TEST_F(UnifiedAudioDetailedViewControllerSodaTest,
       SodaLanguageErrorNotification) {
  SetLiveCaptionLocale("en-US");
  soda_installer()->NotifySodaErrorForTesting(fr_fr());
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaErrorForTesting(en_us());
  EXPECT_EQ(kSodaFailed, GetLiveCaptionViewSubtitleText());
}

// Ensures that we don't respond to SODA download updates when Live Caption is
// off.
TEST_F(UnifiedAudioDetailedViewControllerSodaTest,
       SodaDownloadLiveCaptionDisabled) {
  EnableLiveCaption(false);
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaErrorForTesting();
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaInstalledForTesting();
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
  soda_installer()->NotifySodaProgressForTesting(50);
  EXPECT_EQ(kInitialLiveCaptionViewSubtitleText,
            GetLiveCaptionViewSubtitleText());
}

}  // namespace ash