0

[AGC] Add AGC information row in quick settings.

Add a row in audio input in quick setting page to notify users that the
ui gains is currently auto adjusted for specific apps.

Screenshots:
 Meet app: https://screenshot.googleplex.com/6rFCkVVBxmGu2ti.png
 Meet in chrome: https://screenshot.googleplex.com/AB8eoXPBAfEG3Yi.png
 Zoom in chrome: https://screenshot.googleplex.com/4YVhbgwS8aeHHuC.png

Design doc: https://docs.google.com/document/d/1K9elzEmCj3H9RabJPM0_uSAhdaQlQxwSpqki5hvjp3Q/edit#heading=h.nusylhai1f36
launch bug: launch/4226468

Cq-Depend: chromium:4577520, chromium:4576491
BUG=b:242548161
TEST=ash_unittests, chromeos_unittests, built and deploy to dut

Change-Id: I04a319fba9b8a12a16bcea62f990ea7b47ada96d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4504413
Reviewed-by: Tim Sergeant <tsergeant@chromium.org>
Reviewed-by: Alex Newcomer <newcomer@chromium.org>
Reviewed-by: Li-Yu Yu <aaronyu@google.com>
Commit-Queue: Eddy Hsu <eddyhsu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1159666}
This commit is contained in:
Eddy Hsu
2023-06-19 16:45:19 +00:00
committed by Chromium LUCI CQ
parent 170c3d4377
commit f69cfb413f
13 changed files with 649 additions and 6 deletions

@ -3011,6 +3011,12 @@ Connect your device to power.
Your Chromebook or Bluetooth device is using an older version of Bluetooth. Use another input source for better audio quality.
</message>
<message name="IDS_ASH_STATUS_TRAY_AUDIO_INPUT_AGC_INFO" desc="label used for the information of who is controlling mic input gain. [ICU Syntax]">
{NUM_APPS, plural,
=1 {Mic input controlled by <ph name="app_name">$1<ex>App</ex></ph>}
other {Mic input controlled by # apps}}
</message>
<!-- Status tray Live Caption strings. -->
<message name="IDS_ASH_STATUS_TRAY_LIVE_CAPTION" desc="The label used in the accessibility menu of the system tray to toggle on/off Live Caption feature.">
Live Caption
@ -3062,6 +3068,9 @@ Connect your device to power.
<message name="IDS_ASH_STATUS_TRAY_AUDIO_SETTINGS" desc="The label used in audio detailed page for the button that launches the OS audio settings page.">
Audio settings
</message>
<message name="IDS_ASH_STATUS_TRAY_AUDIO_SETTINGS_SHORT_STRING" desc="The label used in audio detailed page for the button that launches the OS audio settings page.">
Settings
</message>
<message name="IDS_ASH_STATUS_TRAY_DISPLAY" desc="The label used for the button in the status tray to show the display detailed page.">
Display
</message>

@ -0,0 +1 @@
4250ca7845e492b728d721e7f20043c20e6d1ad7

@ -0,0 +1 @@
e393620c76a82aeabfdc8ff87c87f5f4b412c11e

@ -37,7 +37,10 @@
#include "chromeos/constants/chromeos_features.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"
@ -51,6 +54,7 @@
#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/button/toggle_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
@ -160,10 +164,43 @@ class DeviceNameContainerHighlightPathGenerator
const raw_ptr<QuickSettingsSlider, ExperimentalAsh> slider_;
};
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.ShortName();
});
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;
}
} // namespace
AudioDetailedView::AudioDetailedView(DetailedViewDelegate* delegate)
: TrayDetailedView(delegate) {
: TrayDetailedView(delegate),
num_stream_ignore_ui_gains_(
CrasAudioHandler::Get()->num_stream_ignore_ui_gains()) {
CreateItems();
Shell::Get()->accessibility_controller()->AddObserver(this);
@ -176,6 +213,16 @@ AudioDetailedView::AudioDetailedView(DetailedViewDelegate* delegate)
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() {
@ -221,6 +268,56 @@ void AudioDetailedView::OnAccessibilityStatusChanged() {
}
}
void AudioDetailedView::OnCapabilityAccessUpdate(
const apps::CapabilityAccessUpdate& update) {
if (!features::IsQsRevampEnabled()) {
UpdateAgcInfoRow();
} else {
UpdateQsAgcInfoRow();
}
}
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) {
@ -497,6 +594,73 @@ AudioDetailedView::CreateQsNoiseCancellationToggleRow(
return noise_cancellation_view;
}
views::Builder<views::BoxLayoutView> AudioDetailedView::CreateAgcInfoRow(
const AudioDevice& device) {
return views::Builder<views::BoxLayoutView>()
.SetID(AudioDetailedViewID::kAgcInfoRow)
.SetDefaultFlex(1)
.SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
kToggleButtonRowViewPadding, kToggleButtonRowViewSpacing))
.AddChild(views::Builder<views::ImageView>().SetImage(
ui::ImageModel::FromVectorIcon(kUnifiedMenuInfoIcon,
cros_tokens::kCrosSysOnSurface,
kQsSliderIconSize)))
.AddChild(
views::Builder<views::Label>()
.SetText(std::u16string())
.SetEnabledColorId(kColorAshTextColorPrimary)
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
.SetFontList(
gfx::FontList().DeriveWithSizeDelta(kLabelFontSizeDelta))
.SetAutoColorReadabilityEnabled(false)
.SetSubpixelRenderingEnabled(false)
.SetBorder(views::CreateEmptyBorder(kToggleButtonRowLabelPadding))
.SetID(AudioDetailedViewID::kAgcInfoLabel))
.AddChild(views::Builder<views::LabelButton>(
std::make_unique<views::LabelButton>(
base::BindRepeating(&AudioDetailedView::OnSettingsButtonClicked,
weak_factory_.GetWeakPtr()),
l10n_util::GetStringUTF16(
IDS_ASH_STATUS_TRAY_AUDIO_SETTINGS_SHORT_STRING))));
}
std::unique_ptr<HoverHighlightView> AudioDetailedView::CreateQsAgcInfoRow(
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()));
// Add settings button to link to the audio settings page.
auto settings = std::make_unique<views::LabelButton>(
base::BindRepeating(&AudioDetailedView::OnSettingsButtonClicked,
weak_factory_.GetWeakPtr()),
l10n_util::GetStringUTF16(
IDS_ASH_STATUS_TRAY_AUDIO_SETTINGS_SHORT_STRING));
if (!TrayPopupUtils::CanOpenWebUISettings()) {
settings->SetEnabled(false);
}
agc_info_view->AddRightView(settings.release());
agc_info_view->tri_view()->SetInsets(kQsToggleButtonRowViewPadding);
agc_info_view->tri_view()->SetContainerLayout(
TriView::Container::CENTER, std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
kQsToggleButtonRowLabelPadding));
agc_info_view->SetPreferredSize(kQsToggleButtonRowPreferredSize);
agc_info_view->SetProperty(views::kMarginsKey, kQsToggleButtonRowMargins);
return agc_info_view;
}
void AudioDetailedView::MaybeShowSodaMessage(speech::LanguageCode language_code,
std::u16string message) {
AccessibilityControllerImpl* controller =
@ -706,6 +870,17 @@ void AudioDetailedView::UpdateScrollableList() {
/*is_output_device=*/false);
}
// 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) {
if (features::IsQsRevampEnabled()) {
container->AddChildView(
AudioDetailedView::CreateQsAgcInfoRow(device));
UpdateQsAgcInfoRow();
}
}
}
// Adds the input noise cancellation toggle.
if (audio_handler->GetPrimaryActiveInputNode() == device.id &&
audio_handler->IsNoiseCancellationSupportedForDevice(device.id)) {
@ -730,6 +905,13 @@ void AudioDetailedView::UpdateScrollableList() {
if (!features::IsQsRevampEnabled()) {
scroll_content()->AddChildView(mic_gain_controller_->CreateMicGainSlider(
device.id, device.IsInternalMic()));
// 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(CreateAgcInfoRow(device).Build());
UpdateAgcInfoRow();
}
}
}
// Adds a warning message if NBS is selected.
@ -815,6 +997,70 @@ void AudioDetailedView::UpdateActiveDeviceColor(bool is_input, bool is_muted) {
is_muted);
}
void AudioDetailedView::UpdateAgcInfoRow() {
if (!base::FeatureList::IsEnabled(media::kIgnoreUiGains)) {
return;
}
if (!scroll_content()) {
return;
}
views::Label* label = static_cast<views::Label*>(
scroll_content()->GetViewByID(AudioDetailedViewID::kAgcInfoLabel));
if (!label) {
return;
}
std::vector<std::string> app_names = GetNamesOfAppsAccessingMic(
app_registry_cache_, app_capability_access_cache_);
label->SetText(GetTextForAgcInfo(app_names));
views::View* agc_info_row =
scroll_content()->GetViewByID(AudioDetailedViewID::kAgcInfoRow);
CHECK(agc_info_row);
agc_info_row->SetVisible(ShowAgcInfoRow() && !app_names.empty());
}
void AudioDetailedView::UpdateQsAgcInfoRow() {
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_);
text_label->SetText(GetTextForAgcInfo(app_names));
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();
@ -907,6 +1153,15 @@ void AudioDetailedView::OnInputMutedByMicrophoneMuteSwitchChanged(bool muted) {
UpdateActiveDeviceColor(/*is_input=*/true, muted);
}
void AudioDetailedView::OnNumStreamIgnoreUiGainsChanged(int32_t num) {
num_stream_ignore_ui_gains_ = num;
if (!features::IsQsRevampEnabled()) {
UpdateAgcInfoRow();
} else {
UpdateQsAgcInfoRow();
}
}
BEGIN_METADATA(AudioDetailedView, views::View)
END_METADATA

@ -11,15 +11,20 @@
#include "ash/accessibility/accessibility_observer.h"
#include "ash/ash_export.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/style/switch.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_detailed_view.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/ash/components/audio/audio_device.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/soda/soda_installer.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/view.h"
namespace gfx {
@ -32,10 +37,13 @@ class UnifiedAudioDetailedViewControllerSodaTest;
class UnifiedAudioDetailedViewControllerTest;
class UnifiedVolumeSliderController;
class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
public AccessibilityObserver,
public speech::SodaInstaller::Observer,
public CrasAudioHandler::AudioObserver {
class ASH_EXPORT AudioDetailedView
: public AccessibilityObserver,
public apps::AppCapabilityAccessCache::Observer,
public CrasAudioHandler::AudioObserver,
public SessionObserver,
public speech::SodaInstaller::Observer,
public TrayDetailedView {
public:
METADATA_HEADER(AudioDetailedView);
@ -46,6 +54,19 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
~AudioDetailedView() override;
// IDs used for the views that compose the Audio UI.
// Note that these IDs are only guaranteed to be unique inside
// `AudioDetailedView`.
enum AudioDetailedViewID {
// Starts at 1000 to prevent potential overlapping.
kAudioDetailedView = 1000,
// Agc information row and corresponding text label.
kAgcInfoRow,
kAgcInfoLabel,
// For QsRevamp: AGC information row.
kAgcInfoView,
};
using NoiseCancellationCallback =
base::RepeatingCallback<void(uint64_t, views::View*)>;
static void SetMapNoiseCancellationToggleCallbackForTest(
@ -59,8 +80,21 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
// AccessibilityObserver:
void OnAccessibilityStatusChanged() override;
// apps::AppCapabilityAccessCache::Observer:
void OnCapabilityAccessUpdate(
const apps::CapabilityAccessUpdate& update) override;
void OnAppCapabilityAccessCacheWillBeDestroyed(
apps::AppCapabilityAccessCache* cache) override;
// SessionObserver:
void OnSessionStateChanged(session_manager::SessionState state) override;
// CrasAudioHandler::AudioObserver:
void OnNumStreamIgnoreUiGainsChanged(int32_t num) override;
private:
friend class AudioDetailedViewTest;
friend class AudioDetailedViewAgcInfoTest;
friend class UnifiedAudioDetailedViewControllerSodaTest;
friend class UnifiedAudioDetailedViewControllerTest;
@ -91,6 +125,14 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
std::unique_ptr<HoverHighlightView> CreateQsNoiseCancellationToggleRow(
const AudioDevice& device);
// Creates the agc info row in the input subsection.
views::Builder<views::BoxLayoutView> CreateAgcInfoRow(
const AudioDevice& device);
// For QsRevamp: Creates the agc info row in the input subsection.
std::unique_ptr<HoverHighlightView> CreateQsAgcInfoRow(
const AudioDevice& device);
// Sets the subtext for `live_caption_view_` based on whether live caption has
// updated if this feature is enabled and visible in tray.
void MaybeShowSodaMessage(speech::LanguageCode language_code,
@ -124,6 +166,12 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
// called when the input/output node's mute state changes.
void UpdateActiveDeviceColor(bool is_input, bool is_muted);
// Updates the label of AGC info when accessibility to microphone changed.
// Hide AGC info row if no apps is requesting AGC stream.
void UpdateAgcInfoRow();
void UpdateQsAgcInfoRow();
bool ShowAgcInfoRow();
// TrayDetailedView:
void HandleViewClicked(views::View* view) override;
void CreateExtraTitleRowButtons() override;
@ -151,6 +199,9 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
AudioDeviceList input_devices_;
AudioDeviceMap device_map_;
uint64_t focused_device_id_ = -1;
int num_stream_ignore_ui_gains_ = 0;
// Owned by the views hierarchy.
raw_ptr<HoverHighlightView, ExperimentalAsh> live_caption_view_ = nullptr;
raw_ptr<views::ImageView, ExperimentalAsh> live_caption_icon_ = nullptr;
@ -161,6 +212,14 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
raw_ptr<Switch, ExperimentalAsh> noise_cancellation_button_ = nullptr;
raw_ptr<views::Button, ExperimentalAsh> settings_button_ = nullptr;
base::ScopedObservation<SessionController, SessionObserver>
session_observation_{this};
base::ScopedObservation<apps::AppCapabilityAccessCache,
apps::AppCapabilityAccessCache::Observer>
app_capability_observation_{this};
raw_ptr<apps::AppRegistryCache> app_registry_cache_;
raw_ptr<apps::AppCapabilityAccessCache> app_capability_access_cache_;
base::WeakPtrFactory<AudioDetailedView> weak_factory_{this};
};

@ -8,9 +8,17 @@
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/test/test_system_tray_client.h"
#include "ash/shell.h"
#include "ash/system/tray/detailed_view_delegate.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache_wrapper.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "components/user_manager/fake_user_manager.h"
#include "media/base/media_switches.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/widget.h"
@ -83,4 +91,137 @@ TEST_F(AudioDetailedViewTest, PressingSettingsButtonOpensSettings) {
EXPECT_EQ(1, detailed_view_delegate_.close_bubble_count_);
}
} // namespace ash
class AudioDetailedViewAgcInfoTest
: public AudioDetailedViewTest,
public testing::WithParamInterface<testing::tuple<bool, bool, bool>> {
public:
void SetUp() override {
scoped_feature_list_.InitWithFeatureStates(
{{media::kIgnoreUiGains, IsIgnoreUiGainsEnabled()},
{features::kQsRevamp, IsQsRevampEnabled()}});
AudioDetailedViewTest::SetUp();
CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
CHECK(audio_handler);
audio_handler->SetForceRespectUiGainsState(IsForceRespectUiGainsEnabled());
account_id_ = Shell::Get()->session_controller()->GetActiveAccountId();
registry_cache_.SetAccountId(account_id_);
apps::AppRegistryCacheWrapper::Get().AddAppRegistryCache(account_id_,
&registry_cache_);
capability_access_cache_.SetAccountId(account_id_);
apps::AppCapabilityAccessCacheWrapper::Get().AddAppCapabilityAccessCache(
account_id_, &capability_access_cache_);
audio_detailed_view_->Update();
}
void TearDown() override {
AudioDetailedViewTest::TearDown();
apps::AppRegistryCacheWrapper::Get().RemoveAppRegistryCache(
&registry_cache_);
apps::AppCapabilityAccessCacheWrapper::Get().RemoveAppCapabilityAccessCache(
&capability_access_cache_);
registry_cache_.ReinitializeForTesting();
}
bool IsIgnoreUiGainsEnabled() { return std::get<0>(GetParam()); }
bool IsForceRespectUiGainsEnabled() { return std::get<1>(GetParam()); }
bool IsQsRevampEnabled() { return std::get<2>(GetParam()); }
views::View* GetAgcInfoView() {
if (IsQsRevampEnabled()) {
return audio_detailed_view_->GetViewByID(
AudioDetailedView::AudioDetailedViewID::kAgcInfoView);
} else {
return audio_detailed_view_->GetViewByID(
AudioDetailedView::AudioDetailedViewID::kAgcInfoRow);
}
}
static apps::AppPtr MakeApp(const char* app_id, const char* name) {
apps::AppPtr app =
std::make_unique<apps::App>(apps::AppType::kChromeApp, app_id);
app->name = name;
app->short_name = name;
return app;
}
static apps::CapabilityAccessPtr MakeCapabilityAccess(
const char* app_id,
absl::optional<bool> mic) {
apps::CapabilityAccessPtr access =
std::make_unique<apps::CapabilityAccess>(app_id);
access->camera = false;
access->microphone = mic;
return access;
}
void LaunchApp(const char* id,
const char* name,
absl::optional<bool> use_mic) {
std::vector<apps::AppPtr> registry_deltas;
registry_deltas.push_back(MakeApp(id, name));
registry_cache_.OnApps(std::move(registry_deltas), apps::AppType::kUnknown,
/* should_notify_initialized = */ false);
std::vector<apps::CapabilityAccessPtr> capability_access_deltas;
capability_access_deltas.push_back(MakeCapabilityAccess(id, use_mic));
capability_access_cache_.OnCapabilityAccesses(
std::move(capability_access_deltas));
}
AccountId account_id_;
apps::AppRegistryCache registry_cache_;
apps::AppCapabilityAccessCache capability_access_cache_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_P(AudioDetailedViewAgcInfoTest, AgcInfoRowShowInProperConditions) {
const char* app_id = "app";
const char* app_name = "App name";
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::ACTIVE);
audio_detailed_view_->OnSessionStateChanged(
session_manager::SessionState::ACTIVE);
views::View* agc_info = GetAgcInfoView();
if (!IsIgnoreUiGainsEnabled()) {
ASSERT_EQ(agc_info, nullptr);
return;
}
ASSERT_NE(agc_info, nullptr);
// Launch an app accessing mic and requesting ignore UI gains.
LaunchApp(app_id, app_name, true);
audio_detailed_view_->OnNumStreamIgnoreUiGainsChanged(1);
EXPECT_EQ(agc_info->GetVisible(), !IsForceRespectUiGainsEnabled());
// Launch an app accessing mic but not requesting ignore UI gains.
LaunchApp(app_id, app_name, true);
audio_detailed_view_->OnNumStreamIgnoreUiGainsChanged(0);
EXPECT_EQ(agc_info->GetVisible(), false);
// Launch an app not accessing mic but requesting ignore UI gains.
// This should not happen in real cases though.
LaunchApp(app_id, app_name, false);
audio_detailed_view_->OnNumStreamIgnoreUiGainsChanged(1);
EXPECT_EQ(agc_info->GetVisible(), false);
// Launch an app not accessing mic and not requesting ignore UI gains.
LaunchApp(app_id, app_name, false);
audio_detailed_view_->OnNumStreamIgnoreUiGainsChanged(0);
EXPECT_EQ(agc_info->GetVisible(), false);
}
INSTANTIATE_TEST_SUITE_P(AudioDetailedViewAgcInfoVisibleTest,
AudioDetailedViewAgcInfoTest,
testing::Combine(testing::Bool(),
testing::Bool(),
testing::Bool()));
} // namespace ash

@ -134,6 +134,9 @@ void CrasAudioHandler::AudioObserver::OnSurveyTriggered(
void CrasAudioHandler::AudioObserver::OnSpeakOnMuteDetected() {}
void CrasAudioHandler::AudioObserver::OnNumStreamIgnoreUiGainsChanged(
int32_t num) {}
void CrasAudioHandler::NumberOfNonChromeOutputStreamsChanged() {
GetNumberOfNonChromeOutputStreams();
}
@ -1262,6 +1265,13 @@ void CrasAudioHandler::SpeakOnMuteDetected() {
}
}
void CrasAudioHandler::NumStreamIgnoreUiGains(int32_t num) {
num_stream_ignore_ui_gains_ = num;
for (auto& observer : observers_) {
observer.OnNumStreamIgnoreUiGainsChanged(num);
}
}
void CrasAudioHandler::ResendBluetoothBattery() {
CrasAudioClient::Get()->ResendBluetoothBattery();
}
@ -1423,6 +1433,7 @@ void CrasAudioHandler::InitializeAudioAfterCrasServiceAvailable(
GetNumberOfOutputStreams();
GetNumberOfNonChromeOutputStreams();
GetNumberOfInputStreamsWithPermissionInternal();
GetNumStreamIgnoreUiGains();
CrasAudioClient::Get()->SetFixA2dpPacketSize(
base::FeatureList::IsEnabled(features::kBluetoothFixA2dpPacketSize));
@ -2474,6 +2485,10 @@ bool CrasAudioHandler::system_agc_supported() const {
return system_agc_supported_;
}
int32_t CrasAudioHandler::num_stream_ignore_ui_gains() const {
return num_stream_ignore_ui_gains_;
}
// GetSystemAgcSupported() is only called in the same thread
// as the CrasAudioHandler constructor. We are safe here without
// thread check, because unittest may not have the task runner
@ -2493,6 +2508,30 @@ void CrasAudioHandler::HandleGetSystemAgcSupported(
system_agc_supported_ = system_agc_supported.value();
}
void CrasAudioHandler::GetNumStreamIgnoreUiGains() {
CrasAudioClient::Get()->GetNumStreamIgnoreUiGains(
base::BindOnce(&CrasAudioHandler::HandleGetNumStreamIgnoreUiGains,
weak_ptr_factory_.GetWeakPtr()));
}
void CrasAudioHandler::HandleGetNumStreamIgnoreUiGains(
absl::optional<int32_t> new_stream_ignore_ui_gains_count) {
if (!new_stream_ignore_ui_gains_count.has_value()) {
LOG(ERROR) << "Failed to retrieve number of ignore ui gains streams.";
return;
}
DCHECK_GE(*new_stream_ignore_ui_gains_count, 0);
if (*new_stream_ignore_ui_gains_count != num_stream_ignore_ui_gains_) {
for (auto& observer : observers_) {
observer.OnNumStreamIgnoreUiGainsChanged(
*new_stream_ignore_ui_gains_count);
}
}
num_stream_ignore_ui_gains_ = *new_stream_ignore_ui_gains_count;
}
ScopedCrasAudioHandlerForTesting::ScopedCrasAudioHandlerForTesting() {
CHECK(!CrasAudioClient::Get())
<< "ScopedCrasAudioHandlerForTesting expects that there is no "

@ -187,6 +187,9 @@ class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_AUDIO) CrasAudioHandler
// Called when a speak-on-mute is detected.
virtual void OnSpeakOnMuteDetected();
// Called when num-stream-ignore-ui-gains state is changed.
virtual void OnNumStreamIgnoreUiGainsChanged(int32_t num);
protected:
AudioObserver();
virtual ~AudioObserver();
@ -534,6 +537,9 @@ class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_AUDIO) CrasAudioHandler
// Returns if system AGC is supported in CRAS or not.
bool system_agc_supported() const;
// Returns number of streams ignoring UI gains.
int32_t num_stream_ignore_ui_gains() const;
// Asks CRAS to resend BluetoothBatteryChanged signal, used in cases when
// Chrome cleans up the stored battery information but still has the device
// connected afterward. For example: User logout.
@ -569,6 +575,7 @@ class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_AUDIO) CrasAudioHandler
survey_specific_data) override;
void SpeakOnMuteDetected() override;
void NumberOfNonChromeOutputStreamsChanged() override;
void NumStreamIgnoreUiGains(int32_t num) override;
// AudioPrefObserver overrides.
void OnAudioPolicyPrefChanged() override;
@ -840,6 +847,13 @@ class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_AUDIO) CrasAudioHandler
// Handle null Metadata from MediaSession.
void HandleMediaSessionMetadataReset();
// Calls CRAS over D-Bus to get the number of streams ignoring Ui Gains.
void GetNumStreamIgnoreUiGains();
// Handle dbus callback for GetNumStreamIgnoreUiGains.
void HandleGetNumStreamIgnoreUiGains(
absl::optional<int32_t> num_stream_ignore_ui_gains);
mojo::Remote<media_session::mojom::MediaControllerManager>
media_controller_manager_;
@ -924,6 +938,8 @@ class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_AUDIO) CrasAudioHandler
cras::DisplayRotation display_rotation_ = cras::DisplayRotation::ROTATE_0;
int num_stream_ignore_ui_gains_ = 0;
base::WeakPtrFactory<CrasAudioHandler> weak_ptr_factory_{this};
};

@ -162,6 +162,15 @@ class CrasAudioClientImpl : public CrasAudioClient {
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&CrasAudioClientImpl::SignalConnected,
weak_ptr_factory_.GetWeakPtr()));
// Monitor the D-Bus signal for is any stream ignore ui gains changed.
cras_proxy_->ConnectToSignal(
cras::kCrasControlInterface, cras::kNumStreamIgnoreUiGainsChanged,
base::BindRepeating(
&CrasAudioClientImpl::NumStreamIgnoreUiGainsReceived,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&CrasAudioClientImpl::SignalConnected,
weak_ptr_factory_.GetWeakPtr()));
}
CrasAudioClientImpl(const CrasAudioClientImpl&) = delete;
@ -606,6 +615,16 @@ class CrasAudioClientImpl : public CrasAudioClient {
base::DoNothing());
}
void GetNumStreamIgnoreUiGains(
chromeos::DBusMethodCallback<int32_t> callback) override {
dbus::MethodCall method_call(cras::kCrasControlInterface,
cras::kGetNumStreamIgnoreUiGains);
cras_proxy_->CallMethod(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&CrasAudioClientImpl::OnGetNumStreamIgnoreUiGains,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
private:
// Called when the cras signal is initially connected.
void SignalConnected(const std::string& interface_name,
@ -865,6 +884,17 @@ class CrasAudioClientImpl : public CrasAudioClient {
}
}
void NumStreamIgnoreUiGainsReceived(dbus::Signal* signal) {
dbus::MessageReader reader(signal);
int32_t num;
if (!reader.PopInt32(&num)) {
LOG(ERROR) << "Error reading signal from cras:" << signal->ToString();
}
for (auto& observer : observers_) {
observer.NumStreamIgnoreUiGains(num);
}
}
void OnGetDefaultOutputBufferSize(chromeos::DBusMethodCallback<int> callback,
dbus::Response* response) {
if (!response) {
@ -1250,6 +1280,25 @@ class CrasAudioClientImpl : public CrasAudioClient {
std::move(callback).Run(speak_on_mute_detection_enabled);
}
void OnGetNumStreamIgnoreUiGains(
chromeos::DBusMethodCallback<int32_t> callback,
dbus::Response* response) {
if (!response) {
LOG(ERROR) << "Error calling " << cras::kGetNumStreamIgnoreUiGains;
std::move(callback).Run(absl::nullopt);
return;
}
int32_t num_stream_ignore_ui_gains = 0;
dbus::MessageReader reader(response);
if (!reader.PopInt32(&num_stream_ignore_ui_gains)) {
LOG(ERROR) << "Error reading response from cras: "
<< response->ToString();
std::move(callback).Run(absl::nullopt);
return;
}
std::move(callback).Run(num_stream_ignore_ui_gains);
}
raw_ptr<dbus::ObjectProxy, ExperimentalAsh> cras_proxy_ = nullptr;
base::ObserverList<Observer>::Unchecked observers_;
@ -1299,6 +1348,8 @@ void CrasAudioClient::Observer::SpeakOnMuteDetected() {}
void CrasAudioClient::Observer::NumberOfNonChromeOutputStreamsChanged() {}
void CrasAudioClient::Observer::NumStreamIgnoreUiGains(int32_t num) {}
CrasAudioClient::CrasAudioClient() {
DCHECK(!g_instance);
g_instance = this;

@ -81,6 +81,9 @@ class COMPONENT_EXPORT(DBUS_AUDIO) CrasAudioClient {
// Called when NumberOfNonChromeOutputStreamsChanged is detected.
virtual void NumberOfNonChromeOutputStreamsChanged();
// Called when num-stream-ignore-ui-gains is changed.
virtual void NumStreamIgnoreUiGains(int32_t num);
protected:
virtual ~Observer();
};
@ -275,6 +278,10 @@ class COMPONENT_EXPORT(DBUS_AUDIO) CrasAudioClient {
// Sets input force respect ui gains state to |force_repsect_ui_gains| value.
virtual void SetForceRespectUiGains(bool force_respect_ui_gains) = 0;
// Gets the number of streams ignoring UI Gains.
virtual void GetNumStreamIgnoreUiGains(
chromeos::DBusMethodCallback<int> callback) = 0;
protected:
friend class CrasAudioClientTest;

@ -127,6 +127,7 @@ class MockObserver : public CrasAudioClient::Observer {
survey_specific_data));
MOCK_METHOD0(SpeakOnMuteDetected, void());
MOCK_METHOD0(NumberOfNonChromeOutputStreamsChanged, void());
MOCK_METHOD1(NumStreamIgnoreUiGains, void(int32_t num));
};
// Expect the reader to be empty.
@ -507,6 +508,15 @@ class CrasAudioClientTest : public testing::Test {
.WillRepeatedly(
Invoke(this, &CrasAudioClientTest::OnSpeakOnMuteDetected));
// Set an expectation so mock_cras_proxy's monitoring
// SurveyTrigger ConnectToSignal will use
// OnNumStreamIgnoreUiGains() to run the callback.
EXPECT_CALL(*mock_cras_proxy_.get(),
DoConnectToSignal(interface_name_,
cras::kNumStreamIgnoreUiGainsChanged, _, _))
.WillRepeatedly(Invoke(
this, &CrasAudioClientTest::OnNumStreamIgnoreUiGainsChanged));
// Set an expectation so mock_bus's GetObjectProxy() for the given
// service name and the object path will return mock_cras_proxy_.
EXPECT_CALL(*mock_bus_.get(),
@ -629,6 +639,12 @@ class CrasAudioClientTest : public testing::Test {
number_of_non_chrome_output_streams_changed_handler_.Run(signal);
}
// Send num-stream-ignore-ui-gains changed signal to the tested client.
void SendNumStreamIgnoreUiGainsSignal(dbus::Signal* signal) {
ASSERT_FALSE(num_stream_ignore_ui_gains_handler_.is_null());
num_stream_ignore_ui_gains_handler_.Run(signal);
}
CrasAudioClient* client() { return CrasAudioClient::Get(); }
// The interface name.
@ -672,6 +688,8 @@ class CrasAudioClientTest : public testing::Test {
// tested client.
dbus::ObjectProxy::SignalCallback
number_of_non_chrome_output_streams_changed_handler_;
// The NumStreamIgnoreUiGains signal handler given by the tested client.
dbus::ObjectProxy::SignalCallback num_stream_ignore_ui_gains_handler_;
// The name of the method which is expected to be called.
std::string expected_method_name_;
// The response which the mock cras proxy returns.
@ -871,6 +889,20 @@ class CrasAudioClientTest : public testing::Test {
interface_name, signal_name, success));
}
// Checks the requested interface name and signal name.
// Used to implement the mock cras proxy.
void OnNumStreamIgnoreUiGainsChanged(
const std::string& interface_name,
const std::string& signal_name,
const dbus::ObjectProxy::SignalCallback& signal_callback,
dbus::ObjectProxy::OnConnectedCallback* on_connected_callback) {
num_stream_ignore_ui_gains_handler_ = signal_callback;
constexpr bool success = true;
task_environment_.GetMainThreadTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(std::move(*on_connected_callback),
interface_name, signal_name, success));
}
// Checks the content of the method call and returns the response.
// Used to implement the mock cras proxy.
void OnCallMethod(dbus::MethodCall* method_call,
@ -1139,6 +1171,31 @@ TEST_F(CrasAudioClientTest, SpeakOnMuteDetected) {
base::RunLoop().RunUntilIdle();
}
TEST_F(CrasAudioClientTest, NumStreamIgnoreUiGainsChanged) {
const int32_t kNumStream = 1;
dbus::Signal signal(cras::kCrasControlInterface,
cras::kNumStreamIgnoreUiGainsChanged);
dbus::MessageWriter writer(&signal);
writer.AppendInt32(kNumStream);
MockObserver observer;
EXPECT_CALL(observer, NumStreamIgnoreUiGains(kNumStream)).Times(1);
client()->AddObserver(&observer);
SendNumStreamIgnoreUiGainsSignal(&signal);
client()->RemoveObserver(&observer);
EXPECT_CALL(observer, NumStreamIgnoreUiGains(kNumStream)).Times(0);
// Run the signal callback again and make sure the observer isn't called.
SendNumStreamIgnoreUiGainsSignal(&signal);
base::RunLoop().RunUntilIdle();
}
TEST_F(CrasAudioClientTest, NodesChanged) {
// Create a signal.
dbus::Signal signal(cras::kCrasControlInterface, cras::kNodesChanged);

@ -458,4 +458,9 @@ void FakeCrasAudioClient::SetForceRespectUiGains(
force_respect_ui_gains_enabled_ = force_respect_ui_gains_enabled;
}
void FakeCrasAudioClient::GetNumStreamIgnoreUiGains(
chromeos::DBusMethodCallback<int> callback) {
std::move(callback).Run(false);
}
} // namespace ash

@ -95,6 +95,8 @@ class COMPONENT_EXPORT(DBUS_AUDIO) FakeCrasAudioClient
void WaitForServiceToBeAvailable(
chromeos::WaitForServiceToBeAvailableCallback callback) override;
void SetForceRespectUiGains(bool force_respect_ui_gains_enabled) override;
void GetNumStreamIgnoreUiGains(
chromeos::DBusMethodCallback<int> callback) override;
// Sets the number of non chrome audio streams in output mode.
void SetNumberOfNonChromeOutputStreams(int32_t streams);