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

#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/system/audio/mic_gain_slider_controller.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/unified/quick_settings_slider.h"
#include "chromeos/ash/components/audio/audio_device.h"
#include "chromeos/ash/components/audio/cras_audio_handler.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/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

using Style = QuickSettingsSlider::Style;

namespace {
// Gets resource ID for the string that should be used for mute state portion of
// the microphone toggle button tooltip.
int GetMuteStateTooltipTextResourceId(bool is_muted,
                                      bool is_muted_by_mute_switch) {
  if (is_muted_by_mute_switch) {
    return IDS_ASH_STATUS_TRAY_MIC_STATE_MUTED_BY_HW_SWITCH;
  }
  if (is_muted) {
    return IDS_ASH_STATUS_TRAY_MIC_STATE_MUTED;
  }
  return IDS_ASH_STATUS_TRAY_MIC_STATE_ON;
}

}  // namespace

MicGainSliderView::MicGainSliderView(MicGainSliderController* controller)
    : UnifiedSliderView(
          base::BindRepeating(&MicGainSliderController::SliderButtonPressed,
                              base::Unretained(controller)),
          controller,
          kImeMenuMicrophoneIcon,
          IDS_ASH_STATUS_TRAY_MIC_SLIDER_LABEL),
      device_id_(CrasAudioHandler::Get()->GetPrimaryActiveInputNode()),
      internal_(false) {
  CrasAudioHandler::Get()->AddAudioObserver(this);

  Update(/*by_user=*/false);
}

MicGainSliderView::MicGainSliderView(MicGainSliderController* controller,
                                     uint64_t device_id,
                                     bool internal)
    : UnifiedSliderView(
          base::BindRepeating(&MicGainSliderController::SliderButtonPressed,
                              base::Unretained(controller)),
          controller,
          kImeMenuMicrophoneIcon,
          IDS_ASH_STATUS_TRAY_MIC_SLIDER_LABEL,
          /*is_togglable=*/true,
          /*read_only=*/false,
          Style::kRadioActive),
      device_id_(device_id),
      internal_(internal) {
  CrasAudioHandler::Get()->AddAudioObserver(this);

  auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, kRadioSliderViewPadding,
      kSliderChildrenViewSpacing));
  slider()->SetBorder(views::CreateEmptyBorder(kRadioSliderPadding));
  slider()->SetPreferredSize(kRadioSliderPreferredSize);
  slider_button()->SetBorder(views::CreateEmptyBorder(kRadioSliderIconPadding));
  layout->SetFlexForView(slider(), /*flex=*/1);
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  SetPreferredSize(kRadioSliderPreferredSize);

  Update(/*by_user=*/false);
}

MicGainSliderView::~MicGainSliderView() {
  CrasAudioHandler::Get()->RemoveAudioObserver(this);
}

void MicGainSliderView::Update(bool by_user) {
  auto* audio_handler = CrasAudioHandler::Get();
  uint64_t active_device_id = audio_handler->GetPrimaryActiveInputNode();
  auto* active_device = audio_handler->GetDeviceFromId(active_device_id);

  // For device that has dual internal mics, a new device will be created to
  // show only one slider for both the internal mics, and the new device has a
  // new id that doesn't match either of the internal mic id. If the device has
  // dual internal mics and the internal mic shown in the ui is a stub, we need
  // to show this slider despite the `device_id_` not matching the active input
  // node.
  const bool show_internal_stub =
      internal_ && (active_device && active_device->IsInternalMic()) &&
      audio_handler->HasDualInternalMic();

  SetVisible(true);
  bool is_muted = audio_handler->IsInputMuted();
  const bool is_muted_by_mute_switch =
      audio_handler->input_muted_by_microphone_mute_switch();

  if (button()) {
    // The button should be gray when muted and colored otherwise.
    button()->SetToggled(!is_muted);
    button()->SetEnabled(!is_muted_by_mute_switch);
    button()->SetVectorIcon(is_muted ? kMutedMicrophoneIcon
                                     : kImeMenuMicrophoneIcon);
    std::u16string state_tooltip_text = l10n_util::GetStringUTF16(
        GetMuteStateTooltipTextResourceId(is_muted, is_muted_by_mute_switch));
    button()->SetTooltipText(l10n_util::GetStringFUTF16(
        IDS_ASH_STATUS_TRAY_MIC_GAIN, state_tooltip_text));
  }

  float level = audio_handler->GetInputGainPercent() / 100.f;

  // Gets the input gain for each device to draw each slider in
  // `AudioDetailedView`.
  uint64_t device_id;
  if (audio_handler->GetDeviceFromId(device_id_)) {
    // If the device can be found by its id, this slider must not be one of
    // the dual internal mic, thus the `device_id_` is the actual id for it.
    device_id = device_id_;
  } else if (show_internal_stub) {
    // If the device cannot be found by its id and the dual internal mic is
    // active, this slider must be the one for the dual internal mic, which is
    // the active input node.
    device_id = audio_handler->GetPrimaryActiveInputNode();
  } else {
    // If the device cannot be found by its id and the dual internal mic is
    // inactive, this slider must be the one for the dual internal mic, which
    // is currently inactive. Sets its level as the front internal mic level
    // by default.
    device_id = audio_handler->GetDeviceByType(AudioDeviceType::kFrontMic)->id;
  }

  level = audio_handler->GetInputGainPercentForDevice(device_id) / 100.f;
  // Still needs to check if `level` is 0 because toggling the mic mute by
  // keyboard will not set the input to be muted in `MicGainSliderController`.
  is_muted = audio_handler->IsInputMutedForDevice(device_id) || level == 0;
  // For active internal mic stub, `show_internal_stub` indicates whether it's
  // showing and `device_id_` doesn't match with `active_device_id`.
  const bool is_active = show_internal_stub || active_device_id == device_id_;
  auto* qs_slider = static_cast<QuickSettingsSlider*>(slider());
  const Style slider_style = qs_slider->slider_style();
  // The default style is for the slider in the toast, and the radio style is
  // for all other mic gain sliders in the audio subpage.
  const bool is_default_style =
      (slider_style == Style::kDefault || slider_style == Style::kDefaultMuted);

  if (is_default_style) {
    qs_slider->SetSliderStyle(is_muted ? Style::kDefaultMuted
                                       : Style::kDefault);
  } else {
    qs_slider->SetSliderStyle(
        is_active ? (is_muted ? Style::kRadioActiveMuted : Style::kRadioActive)
                  : Style::kRadioInactive);
  }
  slider_button()->SetVectorIcon(is_muted ? kMutedMicrophoneIcon
                                          : kImeMenuMicrophoneIcon);
  slider_button()->SetIconColor(
      is_active ? (is_muted ? cros_tokens::kCrosSysOnSurface
                            : cros_tokens::kCrosSysSystemOnPrimaryContainer)
                : cros_tokens::kCrosSysOnSurfaceVariant);
  // Updates the tooltip for `slider_button()` based on the mute state.
  std::u16string state_tooltip_text = l10n_util::GetStringUTF16(
      GetMuteStateTooltipTextResourceId(is_muted, is_muted_by_mute_switch));
  slider_button()->SetTooltipText(l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_MIC_GAIN, state_tooltip_text));

  // Slider's value is in finer granularity than audio volume level(0.01),
  // there will be a small discrepancy between slider's value and volume level
  // on audio side. To avoid the jittering in slider UI, use the slider's
  // current value.
  if (std::abs(level - slider()->GetValue()) <
      kAudioSliderIgnoreUpdateThreshold) {
    level = slider()->GetValue();
  }
  // Note: even if the value does not change, we still need to call this
  // function to enable accessibility events (crbug.com/1013251).
  SetSliderValue(level, by_user);
}

void MicGainSliderView::OnInputNodeGainChanged(uint64_t node_id, int gain) {
  Update(/*by_user=*/true);
}

void MicGainSliderView::OnInputMuteChanged(
    bool mute_on,
    CrasAudioHandler::InputMuteChangeMethod method) {
  Update(/*by_user=*/true);
}

void MicGainSliderView::OnInputMutedByMicrophoneMuteSwitchChanged(bool muted) {
  Update(/*by_user=*/true);
}

void MicGainSliderView::OnActiveInputNodeChanged() {
  Update(/*by_user=*/true);
}

void MicGainSliderView::VisibilityChanged(View* starting_from,
                                          bool is_visible) {
  // Only trigger the visibility change when it's from parent as there are also
  // visibility changes in `Update()`.
  if (starting_from != this) {
    Update(/*by_user=*/true);
  }
}

BEGIN_METADATA(MicGainSliderView)
END_METADATA

}  // namespace ash