0

[Live Caption] Display SODA download progress updates in quick settings.

Screenshots:
- Soda install message in accessibility tray:
  https://screenshot.googleplex.com/9iRmn7Y6xpoVDxJ
- Soda error message in audio settings tray:
  https://screenshot.googleplex.com/Az5xt44wAVTmKLy
- Soda progress message in audio settings tray:
  https://screenshot.googleplex.com/5mnxpXcY6MMkP2A

Bug: 1196105
Change-Id: Ie7b5708355b6269d5c37a340959e039750ac4227
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3217417
Reviewed-by: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: James Cook <jamescook@chromium.org>
Commit-Queue: Abigail Klein <abigailbklein@google.com>
Cr-Commit-Position: refs/heads/main@{#987183}
This commit is contained in:
Abigail Klein
2022-03-30 21:06:45 +00:00
committed by Chromium LUCI CQ
parent 50c7624533
commit e4c6b5ccb7
12 changed files with 457 additions and 142 deletions

@@ -57,6 +57,7 @@
#include "chromeos/services/assistant/public/cpp/assistant_prefs.h" #include "chromeos/services/assistant/public/cpp/assistant_prefs.h"
#include "components/language/core/browser/pref_names.h" #include "components/language/core/browser/pref_names.h"
#include "components/live_caption/pref_names.h" #include "components/live_caption/pref_names.h"
#include "components/soda/constants.h"
namespace ash { namespace ash {
@@ -117,6 +118,8 @@ void RegisterProfilePrefs(PrefRegistrySimple* registry, bool for_test) {
registry->RegisterBooleanPref(chromeos::prefs::kSuggestedContentEnabled, registry->RegisterBooleanPref(chromeos::prefs::kSuggestedContentEnabled,
true); true);
registry->RegisterBooleanPref(::prefs::kLiveCaptionEnabled, false); registry->RegisterBooleanPref(::prefs::kLiveCaptionEnabled, false);
registry->RegisterStringPref(::prefs::kLiveCaptionLanguageCode,
speech::kUsEnglishLocale);
registry->RegisterStringPref(language::prefs::kApplicationLocale, registry->RegisterStringPref(language::prefs::kApplicationLocale,
std::string()); std::string());
} }

@@ -4377,13 +4377,13 @@ Here are some things you can try to get started.
</message> </message>
<!-- SODA Download strings --> <!-- SODA Download strings -->
<message name="IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_COMPLETE" desc="Description explaining that the speech recognition library download has completed."> <message name="IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_COMPLETE" desc="Description explaining that the speech recognition library download has completed.">
Speech files downloaded Speech files downloaded
</message> </message>
<message name="IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_PROGRESS" desc="Description explaining the progress for the speech recognition library download."> <message name="IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_PROGRESS" desc="Description explaining the progress for the speech recognition library download.">
Downloading speech recognition files... <ph name="PERCENT">$1<ex>17</ex></ph>% Downloading speech recognition files... <ph name="PERCENT">$1<ex>17</ex></ph>%
</message> </message>
<message name="IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR" desc="Description explaining that there was an error with the speech recognition library download."> <message name="IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR" desc="Description explaining that there was an error with the speech recognition library download.">
Can't download speech files. Try again later. Can't download speech files. Try again later.
</message> </message>
<message name="IDS_ASH_ACCESSIBILITY_DICTATION_BUTTON_TOOLTIP_SODA_DOWNLOADING" desc="A tooltip explaining that speech recognition files are downloading." > <message name="IDS_ASH_ACCESSIBILITY_DICTATION_BUTTON_TOOLTIP_SODA_DOWNLOADING" desc="A tooltip explaining that speech recognition files are downloading." >

@@ -26,9 +26,11 @@
#include "ash/system/tray/tri_view.h" #include "ash/system/tray/tri_view.h"
#include "base/bind.h" #include "base/bind.h"
#include "base/metrics/user_metrics.h" #include "base/metrics/user_metrics.h"
#include "components/live_caption/pref_names.h"
#include "components/prefs/pref_service.h" #include "components/prefs/pref_service.h"
#include "components/soda/soda_installer.h" #include "components/soda/soda_installer.h"
#include "components/vector_icons/vector_icons.h" #include "components/vector_icons/vector_icons.h"
#include "media/base/media_switches.h"
#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_features.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h" #include "ui/gfx/image/image.h"
@@ -36,6 +38,8 @@
#include "ui/views/controls/separator.h" #include "ui/views/controls/separator.h"
namespace ash { namespace ash {
namespace tray {
namespace { namespace {
using ml::UserSettingsEvent; using ml::UserSettingsEvent;
@@ -68,21 +72,47 @@ void LogUserAccessibilityEvent(UserSettingsEvent::Event::AccessibilityId id,
} }
} }
speech::LanguageCode GetDictationLocale() { speech::LanguageCode GetSodaFeatureLocale(SodaFeature feature) {
std::string dictation_locale = speech::kUsEnglishLocale; std::string feature_locale = speech::kUsEnglishLocale;
PrefService* pref_service = PrefService* pref_service =
Shell::Get()->session_controller()->GetActivePrefService(); Shell::Get()->session_controller()->GetActivePrefService();
if (pref_service) { if (pref_service) {
dictation_locale = switch (feature) {
pref_service->GetString(prefs::kAccessibilityDictationLocale); case SodaFeature::kDictation:
feature_locale =
pref_service->GetString(prefs::kAccessibilityDictationLocale);
break;
case SodaFeature::kLiveCaption:
feature_locale = ::prefs::GetLiveCaptionLanguageCode(pref_service);
break;
}
} }
return speech::GetLanguageCode(dictation_locale); return speech::GetLanguageCode(feature_locale);
}
bool IsSodaFeatureEnabled(SodaFeature feature) {
AccessibilityControllerImpl* controller =
Shell::Get()->accessibility_controller();
switch (feature) {
case SodaFeature::kDictation:
return ::features::IsDictationOfflineAvailable() &&
controller->dictation().enabled();
case SodaFeature::kLiveCaption:
return controller->live_caption().enabled();
}
}
bool SodaFeatureHasUpdate(SodaFeature feature,
speech::LanguageCode language_code) {
// Only show updates for this feature if the language code applies to the SODA
// binary (encoded by by LanguageCode::kNone) or the language pack matching
// the feature locale.
return language_code == speech::LanguageCode::kNone ||
language_code == GetSodaFeatureLocale(feature);
} }
} // namespace } // namespace
namespace tray {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// ash::tray::AccessibilityDetailedView // ash::tray::AccessibilityDetailedView
@@ -95,19 +125,27 @@ AccessibilityDetailedView::AccessibilityDetailedView(
AppendAccessibilityList(); AppendAccessibilityList();
CreateTitleRow(IDS_ASH_STATUS_TRAY_ACCESSIBILITY_TITLE); CreateTitleRow(IDS_ASH_STATUS_TRAY_ACCESSIBILITY_TITLE);
Layout(); Layout();
UpdateSodaInstallerObserverStatus();
if (!::features::IsDictationOfflineAvailable() &&
!media::IsLiveCaptionFeatureEnabled()) {
return;
}
speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
if (soda_installer)
soda_installer->AddObserver(this);
} }
AccessibilityDetailedView::~AccessibilityDetailedView() { AccessibilityDetailedView::~AccessibilityDetailedView() {
if (!::features::IsDictationOfflineAvailable()) if (!::features::IsDictationOfflineAvailable() &&
!media::IsLiveCaptionFeatureEnabled()) {
return; return;
speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
if (soda_installer) {
// `soda_installer` is not guaranteed to be valid, since it's possible for
// this class to out-live it.
soda_installer->RemoveObserver(this);
} }
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);
} }
void AccessibilityDetailedView::OnAccessibilityStatusChanged() { void AccessibilityDetailedView::OnAccessibilityStatusChanged() {
@@ -133,7 +171,6 @@ void AccessibilityDetailedView::OnAccessibilityStatusChanged() {
dictation_enabled_ = controller->dictation().enabled(); dictation_enabled_ = controller->dictation().enabled();
TrayPopupUtils::UpdateCheckMarkVisibility(dictation_view_, TrayPopupUtils::UpdateCheckMarkVisibility(dictation_view_,
dictation_enabled_); dictation_enabled_);
UpdateSodaInstallerObserverStatus();
} }
if (high_contrast_view_ && controller->IsHighContrastSettingVisibleInTray()) { if (high_contrast_view_ && controller->IsHighContrastSettingVisibleInTray()) {
@@ -582,89 +619,66 @@ void AccessibilityDetailedView::ShowHelp() {
} }
} }
void AccessibilityDetailedView::UpdateSodaInstallerObserverStatus() {
if (!::features::IsDictationOfflineAvailable())
return;
speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
if (!soda_installer)
return;
bool dictation_enabled =
Shell::Get()->accessibility_controller()->dictation().enabled();
if (!dictation_enabled) {
soda_installer->RemoveObserver(this);
return;
}
if (!soda_installer->IsSodaInstalled(GetDictationLocale())) {
// Make sure this view observes SODA installation.
soda_installer->AddObserver(this);
}
}
// SodaInstaller::Observer: // SodaInstaller::Observer:
void AccessibilityDetailedView::OnSodaInstalled( void AccessibilityDetailedView::OnSodaInstalled(
speech::LanguageCode language_code) { speech::LanguageCode language_code) {
if (language_code != GetDictationLocale()) std::u16string message = l10n_util::GetStringUTF16(
return; IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_COMPLETE);
MaybeShowSodaMessage(SodaFeature::kDictation, language_code, message);
// Show the success message if both the SODA binary and the language pack MaybeShowSodaMessage(SodaFeature::kLiveCaption, language_code, message);
// matching the Dictation locale have been downloaded.
speech::SodaInstaller::GetInstance()->RemoveObserver(this);
AccessibilityControllerImpl* controller =
Shell::Get()->accessibility_controller();
if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) {
dictation_view_->SetSubText(l10n_util::GetStringUTF16(
IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_COMPLETE));
}
} }
void AccessibilityDetailedView::OnSodaError( void AccessibilityDetailedView::OnSodaError(
speech::LanguageCode language_code) { speech::LanguageCode language_code) {
if (language_code != speech::LanguageCode::kNone && std::u16string message = l10n_util::GetStringUTF16(
language_code != GetDictationLocale()) { IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR);
return; MaybeShowSodaMessage(SodaFeature::kDictation, language_code, message);
} MaybeShowSodaMessage(SodaFeature::kLiveCaption, language_code, message);
// Show the failed message if either the Dictation locale failed or the SODA
// binary failed (encoded by LanguageCode::kNone).
speech::SodaInstaller::GetInstance()->RemoveObserver(this);
AccessibilityControllerImpl* controller =
Shell::Get()->accessibility_controller();
if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) {
dictation_view_->SetSubText(l10n_util::GetStringUTF16(
IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR));
}
} }
void AccessibilityDetailedView::OnSodaProgress( void AccessibilityDetailedView::OnSodaProgress(
speech::LanguageCode language_code, speech::LanguageCode language_code,
int progress) { int progress) {
if (language_code != speech::LanguageCode::kNone && std::u16string message = l10n_util::GetStringFUTF16Int(
language_code != GetDictationLocale()) { IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_PROGRESS, progress);
return; MaybeShowSodaMessage(SodaFeature::kDictation, language_code, message);
} MaybeShowSodaMessage(SodaFeature::kLiveCaption, language_code, message);
}
// Only show the progress message if this applies to the SODA binary (encoded void AccessibilityDetailedView::MaybeShowSodaMessage(
// by LanguageCode::kNone) or the language pack matching the Dictation locale. SodaFeature feature,
speech::LanguageCode language_code,
std::u16string message) {
if (IsSodaFeatureEnabled(feature) && IsSodaFeatureInTray(feature) &&
SodaFeatureHasUpdate(feature, language_code)) {
SetSodaFeatureSubtext(feature, message);
}
}
bool AccessibilityDetailedView::IsSodaFeatureInTray(SodaFeature feature) {
AccessibilityControllerImpl* controller = AccessibilityControllerImpl* controller =
Shell::Get()->accessibility_controller(); Shell::Get()->accessibility_controller();
if (dictation_view_ && controller->IsDictationSettingVisibleInTray()) { switch (feature) {
dictation_view_->SetSubText(l10n_util::GetStringFUTF16Int( case SodaFeature::kDictation:
IDS_ASH_ACCESSIBILITY_DICTATION_SETTING_SUBTITLE_SODA_DOWNLOAD_PROGRESS, return dictation_view_ && controller->IsDictationSettingVisibleInTray();
progress)); case SodaFeature::kLiveCaption:
return live_caption_view_ &&
controller->IsLiveCaptionSettingVisibleInTray();
} }
} }
void AccessibilityDetailedView::SetDictationViewSubtitleTextForTesting( void AccessibilityDetailedView::SetSodaFeatureSubtext(SodaFeature feature,
std::u16string text) { std::u16string message) {
dictation_view_->SetSubText(text); switch (feature) {
} case SodaFeature::kDictation:
DCHECK(dictation_view_);
std::u16string dictation_view_->SetSubText(message);
AccessibilityDetailedView::GetDictationViewSubtitleTextForTesting() { break;
return dictation_view_->sub_text_label()->GetText(); case SodaFeature::kLiveCaption:
DCHECK(live_caption_view_);
live_caption_view_->SetSubText(message);
break;
}
} }
} // namespace tray } // namespace tray

@@ -35,6 +35,11 @@ class TrayAccessibilityTest;
namespace tray { namespace tray {
enum class SodaFeature {
kDictation,
kLiveCaption,
};
// Create the detailed view of accessibility tray. // Create the detailed view of accessibility tray.
class ASH_EXPORT AccessibilityDetailedView class ASH_EXPORT AccessibilityDetailedView
: public TrayDetailedView, : public TrayDetailedView,
@@ -74,16 +79,19 @@ class ASH_EXPORT AccessibilityDetailedView
// Add the accessibility feature list. // Add the accessibility feature list.
void AppendAccessibilityList(); void AppendAccessibilityList();
void UpdateSodaInstallerObserverStatus();
// SodaInstaller::Observer: // SodaInstaller::Observer:
void OnSodaInstalled(speech::LanguageCode language_code) override; void OnSodaInstalled(speech::LanguageCode language_code) override;
void OnSodaError(speech::LanguageCode language_code) override; void OnSodaError(speech::LanguageCode language_code) override;
void OnSodaProgress(speech::LanguageCode language_code, void OnSodaProgress(speech::LanguageCode language_code,
int combined_progress) override; int combined_progress) override;
void SetDictationViewSubtitleTextForTesting(std::u16string text); // Shows a message next to the feature icon in the tray if it is available
std::u16string GetDictationViewSubtitleTextForTesting(); // and if the language code provided is relevant to the feature.
void MaybeShowSodaMessage(SodaFeature feature,
speech::LanguageCode language_code,
std::u16string message);
bool IsSodaFeatureInTray(SodaFeature feature);
void SetSodaFeatureSubtext(SodaFeature feature, std::u16string message);
HoverHighlightView* spoken_feedback_view_ = nullptr; HoverHighlightView* spoken_feedback_view_ = nullptr;
HoverHighlightView* select_to_speak_view_ = nullptr; HoverHighlightView* select_to_speak_view_ = nullptr;

@@ -17,19 +17,23 @@
#include "ash/test/ash_test_base.h" #include "ash/test/ash_test_base.h"
#include "base/command_line.h" #include "base/command_line.h"
#include "base/test/scoped_feature_list.h" #include "base/test/scoped_feature_list.h"
#include "components/live_caption/pref_names.h"
#include "components/prefs/pref_service.h" #include "components/prefs/pref_service.h"
#include "components/soda/soda_installer_impl_chromeos.h" #include "components/soda/soda_installer_impl_chromeos.h"
#include "media/base/media_switches.h" #include "media/base/media_switches.h"
#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_enums.mojom-shared.h" #include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_node_data.h"
#include "ui/views/controls/label.h"
namespace ash { namespace ash {
namespace { namespace {
const std::u16string kInitialDictationViewSubtitleText = u"This is a test"; const std::u16string kInitialFeatureViewSubtitleText = u"This is a test";
const std::u16string kSodaDownloaded = u"Speech files downloaded"; const std::u16string kSodaDownloaded = u"Speech files downloaded";
const std::u16string kSodaInProgress = const std::u16string kSodaInProgress25 =
u"Downloading speech recognition files… 25%";
const std::u16string kSodaInProgress50 =
u"Downloading speech recognition files… 50%"; u"Downloading speech recognition files… 50%";
const std::u16string kSodaFailed = const std::u16string kSodaFailed =
u"Can't download speech files. Try again later."; u"Can't download speech files. Try again later.";
@@ -105,17 +109,24 @@ void EnableSwitchAccess(bool enabled) {
Shell::Get()->accessibility_controller()->switch_access().SetEnabled(enabled); Shell::Get()->accessibility_controller()->switch_access().SetEnabled(enabled);
} }
speech::LanguageCode en_us() {
return speech::LanguageCode::kEnUs;
}
speech::LanguageCode fr_fr() {
return speech::LanguageCode::kFrFr;
}
} // namespace } // namespace
class TrayAccessibilityTest : public AshTestBase, public AccessibilityObserver { class TrayAccessibilityTest : public AshTestBase, public AccessibilityObserver {
public: public:
TrayAccessibilityTest() = default;
TrayAccessibilityTest(const TrayAccessibilityTest&) = delete; TrayAccessibilityTest(const TrayAccessibilityTest&) = delete;
TrayAccessibilityTest& operator=(const TrayAccessibilityTest&) = delete; TrayAccessibilityTest& operator=(const TrayAccessibilityTest&) = delete;
protected:
TrayAccessibilityTest() = default;
~TrayAccessibilityTest() override = default; ~TrayAccessibilityTest() override = default;
protected:
void SetUp() override { void SetUp() override {
scoped_feature_list_.InitWithFeatures( scoped_feature_list_.InitWithFeatures(
{media::kLiveCaption, media::kLiveCaptionSystemWideOnChromeOS, {media::kLiveCaption, media::kLiveCaptionSystemWideOnChromeOS,
@@ -673,13 +684,20 @@ TEST_F(TrayAccessibilityTest, GetClassName) {
GetDetailedViewClassName()); GetDetailedViewClassName());
} }
class TrayAccessibilitySodaTest : public TrayAccessibilityTest { enum SodaFeature {
kDictation,
kLiveCaption,
};
class TrayAccessibilitySodaTest
: public TrayAccessibilityTest,
public testing::WithParamInterface<SodaFeature> {
protected: protected:
TrayAccessibilitySodaTest() { set_start_session(false); } TrayAccessibilitySodaTest() { set_start_session(false); }
~TrayAccessibilitySodaTest() override = default;
TrayAccessibilitySodaTest(const TrayAccessibilitySodaTest&) = delete; TrayAccessibilitySodaTest(const TrayAccessibilitySodaTest&) = delete;
TrayAccessibilitySodaTest& operator=(const TrayAccessibilitySodaTest&) = TrayAccessibilitySodaTest& operator=(const TrayAccessibilitySodaTest&) =
delete; delete;
~TrayAccessibilitySodaTest() override = default;
void SetUp() override { void SetUp() override {
TrayAccessibilityTest::SetUp(); TrayAccessibilityTest::SetUp();
@@ -687,42 +705,71 @@ class TrayAccessibilitySodaTest : public TrayAccessibilityTest {
// SodaInstallerImplChromeOS is never created (it's normally created when // SodaInstallerImplChromeOS is never created (it's normally created when
// `ChromeBrowserMainPartsAsh` initializes). Create it here so that // `ChromeBrowserMainPartsAsh` initializes). Create it here so that
// calling speech::SodaInstaller::GetInstance() returns a valid instance. // calling speech::SodaInstaller::GetInstance() returns a valid instance.
scoped_feature_list_.InitWithFeatures( std::vector<base::Feature> enabled_features(
{ash::features::kOnDeviceSpeechRecognition}, {}); {ash::features::kOnDeviceSpeechRecognition});
if (GetParam() == SodaFeature::kLiveCaption)
enabled_features.push_back(media::kLiveCaptionMultiLanguage);
scoped_feature_list_.InitWithFeatures(enabled_features, {});
soda_installer_impl_ = soda_installer_impl_ =
std::make_unique<speech::SodaInstallerImplChromeOS>(); std::make_unique<speech::SodaInstallerImplChromeOS>();
soda_installer()->UninstallSodaForTesting();
CreateDetailedMenu(); CreateDetailedMenu();
EnableDictation(true); EnableFeature(true);
SetDictationViewSubtitleText(kInitialDictationViewSubtitleText); SetFeatureViewSubtitleText(kInitialFeatureViewSubtitleText);
SetDictationLocale("en-US"); SetFeatureLocale("en-US");
} }
void TearDown() override { void TearDown() override {
soda_installer()->UninstallSodaForTesting();
soda_installer_impl_.reset(); soda_installer_impl_.reset();
TrayAccessibilityTest::TearDown(); TrayAccessibilityTest::TearDown();
} }
void SetDictationLocale(const std::string& locale) { void EnableFeature(bool enabled) {
Shell::Get()->session_controller()->GetActivePrefService()->SetString( switch (GetParam()) {
prefs::kAccessibilityDictationLocale, locale); case kDictation:
EnableDictation(enabled);
break;
case kLiveCaption:
EnableLiveCaption(enabled);
break;
}
}
void SetFeatureLocale(const std::string& locale) {
switch (GetParam()) {
case kDictation:
Shell::Get()->session_controller()->GetActivePrefService()->SetString(
prefs::kAccessibilityDictationLocale, locale);
break;
case kLiveCaption:
Shell::Get()->session_controller()->GetActivePrefService()->SetString(
::prefs::kLiveCaptionLanguageCode, locale);
break;
}
} }
speech::SodaInstaller* soda_installer() { speech::SodaInstaller* soda_installer() {
return speech::SodaInstaller::GetInstance(); return speech::SodaInstaller::GetInstance();
} }
speech::LanguageCode en_us() { return speech::LanguageCode::kEnUs; } void SetFeatureViewSubtitleText(std::u16string text) {
speech::LanguageCode fr_fr() { return speech::LanguageCode::kFrFr; } switch (GetParam()) {
case kDictation:
void SetDictationViewSubtitleText(std::u16string text) { detailed_menu()->dictation_view_->SetSubText(text);
detailed_menu()->SetDictationViewSubtitleTextForTesting(text); break;
case kLiveCaption:
detailed_menu()->live_caption_view_->SetSubText(text);
break;
}
} }
std::u16string GetDictationViewSubtitleText() { std::u16string GetFeatureViewSubtitleText() {
return detailed_menu()->GetDictationViewSubtitleTextForTesting(); switch (GetParam()) {
case kDictation:
return detailed_menu()->dictation_view_->sub_text_label()->GetText();
case kLiveCaption:
return detailed_menu()->live_caption_view_->sub_text_label()->GetText();
}
} }
private: private:
@@ -730,54 +777,67 @@ class TrayAccessibilitySodaTest : public TrayAccessibilityTest {
base::test::ScopedFeatureList scoped_feature_list_; base::test::ScopedFeatureList scoped_feature_list_;
}; };
// Ensures that the Dictation subtitle changes when SODA AND the language pack INSTANTIATE_TEST_SUITE_P(All,
// matching the Dictation locale are installed. TrayAccessibilitySodaTest,
TEST_F(TrayAccessibilitySodaTest, OnSodaInstalledNotification) { ::testing::Values(SodaFeature::kDictation,
SetDictationLocale("fr-FR"); SodaFeature::kLiveCaption));
// Ensures that the feature subtitle changes when SODA AND the language pack
// matching the feature locale are installed.
TEST_P(TrayAccessibilitySodaTest, OnSodaInstalledNotification) {
SetFeatureLocale("fr-FR");
// Pretend that the SODA binary was installed. We still need to wait for the // Pretend that the SODA binary was installed. We still need to wait for the
// correct language pack before doing anything. // correct language pack before doing anything.
soda_installer()->NotifySodaInstalledForTesting(); soda_installer()->NotifySodaInstalledForTesting();
EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText()); EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaInstalledForTesting(en_us()); soda_installer()->NotifySodaInstalledForTesting(en_us());
EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText()); EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaInstalledForTesting(fr_fr()); soda_installer()->NotifySodaInstalledForTesting(fr_fr());
EXPECT_EQ(kSodaDownloaded, GetDictationViewSubtitleText()); EXPECT_EQ(kSodaDownloaded, GetFeatureViewSubtitleText());
} }
// Ensures we only notify the user of progress for the language pack matching // Ensures we only notify the user of progress for the language pack matching
// the Dictation locale. // the feature locale.
TEST_F(TrayAccessibilitySodaTest, OnSodaProgressNotification) { TEST_P(TrayAccessibilitySodaTest, OnSodaProgressNotification) {
soda_installer()->NotifySodaProgressForTesting(50, fr_fr()); SetFeatureLocale("en-US");
EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText());
soda_installer()->NotifySodaProgressForTesting(75, fr_fr());
EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaProgressForTesting(50); soda_installer()->NotifySodaProgressForTesting(50);
EXPECT_EQ(kSodaInProgress, GetDictationViewSubtitleText()); EXPECT_EQ(kSodaInProgress50, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaProgressForTesting(50, en_us()); soda_installer()->NotifySodaProgressForTesting(25, en_us());
EXPECT_EQ(kSodaInProgress, GetDictationViewSubtitleText()); EXPECT_EQ(kSodaInProgress25, GetFeatureViewSubtitleText());
} }
// Ensures we only notify the user of an error when the SODA binary fails to // Ensures we notify the user of an error when the SODA binary fails to
// download. // download.
TEST_F(TrayAccessibilitySodaTest, SodaBinaryErrorNotification) { TEST_P(TrayAccessibilitySodaTest, SodaBinaryErrorNotification) {
soda_installer()->NotifySodaErrorForTesting(); soda_installer()->NotifySodaErrorForTesting();
EXPECT_EQ(kSodaFailed, GetDictationViewSubtitleText()); EXPECT_EQ(kSodaFailed, GetFeatureViewSubtitleText());
} }
TEST_F(TrayAccessibilitySodaTest, SodaLanguageErrorNotification) { // Ensures we only notify the user of an error if the failed language pack
// Do nothing if the failed language pack is different than the Dictation // matches the feature locale.
// locale. TEST_P(TrayAccessibilitySodaTest, SodaLanguageErrorNotification) {
SetFeatureLocale("en-US");
soda_installer()->NotifySodaErrorForTesting(fr_fr()); soda_installer()->NotifySodaErrorForTesting(fr_fr());
EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText()); EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaErrorForTesting(en_us()); soda_installer()->NotifySodaErrorForTesting(en_us());
EXPECT_EQ(kSodaFailed, GetDictationViewSubtitleText()); EXPECT_EQ(kSodaFailed, GetFeatureViewSubtitleText());
} }
// Ensures that we don't respond to SODA download updates when dictation is off. // Ensures that we don't respond to SODA download updates when the feature is
TEST_F(TrayAccessibilitySodaTest, SodaDownloadDictationDisabled) { // off.
EnableDictation(false); TEST_P(TrayAccessibilitySodaTest, SodaDownloadFeatureDisabled) {
EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText()); EnableFeature(false);
EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaErrorForTesting(); soda_installer()->NotifySodaErrorForTesting();
EXPECT_EQ(kInitialDictationViewSubtitleText, GetDictationViewSubtitleText()); EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaInstalledForTesting();
EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
soda_installer()->NotifySodaProgressForTesting(50);
EXPECT_EQ(kInitialFeatureViewSubtitleText, GetFeatureViewSubtitleText());
} }
class TrayAccessibilityLoginScreenTest : public TrayAccessibilityTest { class TrayAccessibilityLoginScreenTest : public TrayAccessibilityTest {

@@ -8,6 +8,7 @@
#include "ash/components/audio/cras_audio_handler.h" #include "ash/components/audio/cras_audio_handler.h"
#include "ash/constants/ash_features.h" #include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h" #include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h" #include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h" #include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h" #include "ash/style/ash_color_provider.h"
@@ -19,7 +20,9 @@
#include "ash/system/tray/tri_view.h" #include "ash/system/tray/tri_view.h"
#include "base/bind.h" #include "base/bind.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "components/live_caption/pref_names.h"
#include "components/vector_icons/vector_icons.h" #include "components/vector_icons/vector_icons.h"
#include "media/base/media_switches.h"
#include "third_party/cros_system_api/dbus/service_constants.h" #include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/views/border.h" #include "ui/views/border.h"
@@ -75,6 +78,16 @@ std::u16string GetAudioDeviceName(const AudioDevice& device) {
} }
} }
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);
}
} // namespace } // namespace
namespace tray { namespace tray {
@@ -84,10 +97,24 @@ AudioDetailedView::AudioDetailedView(DetailedViewDelegate* delegate)
CreateItems(); CreateItems();
Shell::Get()->accessibility_controller()->AddObserver(this); Shell::Get()->accessibility_controller()->AddObserver(this);
if (!media::IsLiveCaptionFeatureEnabled())
return;
speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
if (soda_installer)
soda_installer->AddObserver(this);
} }
AudioDetailedView::~AudioDetailedView() { AudioDetailedView::~AudioDetailedView() {
Shell::Get()->accessibility_controller()->RemoveObserver(this); Shell::Get()->accessibility_controller()->RemoveObserver(this);
if (!media::IsLiveCaptionFeatureEnabled())
return;
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);
} }
void AudioDetailedView::Update() { void AudioDetailedView::Update() {
@@ -326,5 +353,43 @@ void AudioDetailedView::OnAccessibilityStatusChanged() {
} }
} }
// 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::OnSodaError(speech::LanguageCode language_code) {
std::u16string message = l10n_util::GetStringUTF16(
IDS_ASH_ACCESSIBILITY_SETTING_SUBTITLE_SODA_DOWNLOAD_ERROR);
MaybeShowSodaMessage(language_code, 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::MaybeShowSodaMessage(speech::LanguageCode language_code,
std::u16string message) {
AccessibilityControllerImpl* controller =
Shell::Get()->accessibility_controller();
bool is_live_caption_enabled = controller->live_caption().enabled();
bool is_live_caption_in_tray =
live_caption_view_ && controller->IsLiveCaptionSettingVisibleInTray();
// Only show updates for this feature if the language code applies to the SODA
// binary (encoded by by LanguageCode::kNone) or the language pack matching
// the feature locale.
bool live_caption_has_update = language_code == speech::LanguageCode::kNone ||
language_code == GetLiveCaptionLocale();
if (is_live_caption_enabled && is_live_caption_in_tray &&
live_caption_has_update) {
live_caption_view_->SetSubText(message);
}
}
} // namespace tray } // namespace tray
} // namespace ash } // namespace ash

@@ -14,6 +14,7 @@
#include "ash/system/tray/tray_detailed_view.h" #include "ash/system/tray/tray_detailed_view.h"
#include "ash/system/tray/tray_toggle_button.h" #include "ash/system/tray/tray_toggle_button.h"
#include "base/callback.h" #include "base/callback.h"
#include "components/soda/soda_installer.h"
#include "ui/views/controls/button/toggle_button.h" #include "ui/views/controls/button/toggle_button.h"
#include "ui/views/view.h" #include "ui/views/view.h"
@@ -23,12 +24,14 @@ struct VectorIcon;
namespace ash { namespace ash {
class MicGainSliderController; class MicGainSliderController;
class UnifiedAudioDetailedViewControllerSodaTest;
class UnifiedAudioDetailedViewControllerTest; class UnifiedAudioDetailedViewControllerTest;
namespace tray { namespace tray {
class ASH_EXPORT AudioDetailedView : public TrayDetailedView, class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
public ::ash::AccessibilityObserver { public ::ash::AccessibilityObserver,
public speech::SodaInstaller::Observer {
public: public:
explicit AudioDetailedView(DetailedViewDelegate* delegate); explicit AudioDetailedView(DetailedViewDelegate* delegate);
@@ -51,6 +54,7 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
void OnAccessibilityStatusChanged() override; void OnAccessibilityStatusChanged() override;
private: private:
friend class ::ash::UnifiedAudioDetailedViewControllerSodaTest;
friend class ::ash::UnifiedAudioDetailedViewControllerTest; friend class ::ash::UnifiedAudioDetailedViewControllerTest;
// Helper function to add non-clickable header rows within the scrollable // Helper function to add non-clickable header rows within the scrollable
@@ -70,6 +74,15 @@ class ASH_EXPORT AudioDetailedView : public TrayDetailedView,
// TrayDetailedView: // TrayDetailedView:
void HandleViewClicked(views::View* view) override; void HandleViewClicked(views::View* view) override;
// SodaInstaller::Observer:
void OnSodaInstalled(speech::LanguageCode language_code) override;
void OnSodaError(speech::LanguageCode language_code) override;
void OnSodaProgress(speech::LanguageCode language_code,
int combined_progress) override;
void MaybeShowSodaMessage(speech::LanguageCode language_code,
std::u16string message);
typedef std::map<views::View*, AudioDevice> AudioDeviceMap; typedef std::map<views::View*, AudioDevice> AudioDeviceMap;
std::unique_ptr<MicGainSliderController> mic_gain_controller_; std::unique_ptr<MicGainSliderController> mic_gain_controller_;

@@ -8,6 +8,7 @@
#include "ash/components/audio/audio_devices_pref_handler.h" #include "ash/components/audio/audio_devices_pref_handler.h"
#include "ash/components/audio/audio_devices_pref_handler_stub.h" #include "ash/components/audio/audio_devices_pref_handler_stub.h"
#include "ash/constants/ash_features.h" #include "ash/constants/ash_features.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h" #include "ash/shell.h"
#include "ash/system/audio/audio_detailed_view.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_controller.h"
@@ -20,11 +21,14 @@
#include "base/test/scoped_feature_list.h" #include "base/test/scoped_feature_list.h"
#include "chromeos/dbus/audio/cras_audio_client.h" #include "chromeos/dbus/audio/cras_audio_client.h"
#include "chromeos/dbus/audio/fake_cras_audio_client.h" #include "chromeos/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 "media/base/media_switches.h"
#include "mojo/public/cpp/bindings/receiver_set.h" #include "mojo/public/cpp/bindings/receiver_set.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
#include "third_party/cros_system_api/dbus/service_constants.h" #include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/events/base_event_utils.h" #include "ui/events/base_event_utils.h"
#include "ui/views/controls/label.h"
#include "ui/views/test/button_test_api.h" #include "ui/views/test/button_test_api.h"
#include "ui/views/widget/widget.h" #include "ui/views/widget/widget.h"
@@ -39,6 +43,23 @@ constexpr uint64_t kInternalMicId = 10003;
constexpr uint64_t kFrontMicId = 10012; constexpr uint64_t kFrontMicId = 10012;
constexpr uint64_t kRearMicId = 10013; constexpr uint64_t kRearMicId = 10013;
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 { struct AudioNodeInfo {
bool is_input; bool is_input;
uint64_t id; uint64_t id;
@@ -161,7 +182,7 @@ class UnifiedAudioDetailedViewControllerTest : public AshTestBase {
return audio_detailed_view_.get(); return audio_detailed_view_.get();
} }
views::View* live_caption_view() { HoverHighlightView* live_caption_view() {
return audio_detailed_view()->live_caption_view_; return audio_detailed_view()->live_caption_view_;
} }
@@ -347,4 +368,135 @@ TEST_F(UnifiedAudioDetailedViewControllerTest, LiveCaptionNotAvailable) {
EXPECT_FALSE(live_caption_enabled()); EXPECT_FALSE(live_caption_enabled());
} }
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::kLiveCaption,
media::kLiveCaptionMultiLanguage,
media::kLiveCaptionSystemWideOnChromeOS},
{});
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_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// 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 } // namespace ash

@@ -25,7 +25,7 @@ class ViewClickListener;
// A view that changes background color on hover, and triggers a callback in the // A view that changes background color on hover, and triggers a callback in the
// associated ViewClickListener on click. The view can also be forced to // associated ViewClickListener on click. The view can also be forced to
// maintain a fixed height. // maintain a fixed height.
class HoverHighlightView : public ActionableView { class ASH_EXPORT HoverHighlightView : public ActionableView {
public: public:
enum class AccessibilityState { enum class AccessibilityState {
// The default accessibility view. // The default accessibility view.