chromium/chrome/browser/ui/webui/ash/settings/pages/a11y/accessibility_handler.cc

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/webui/ash/settings/pages/a11y/accessibility_handler.h"

#include <set>
#include <string_view>

#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/dictation.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/url_handler/os_url_handler.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/common/locale_util.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_ui.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "extensions/common/manifest_handlers/options_page_info.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/base/ime/ash/input_method_util.h"
#include "ui/base/l10n/l10n_util.h"

namespace ash::settings {
namespace {

void RecordShowShelfNavigationButtonsValueChange(bool enabled) {
  base::UmaHistogramBoolean(
      "Accessibility.CrosShelfNavigationButtonsInTabletModeChanged."
      "OsSettings",
      enabled);
}

}  // namespace

AccessibilityHandler::AccessibilityHandler(Profile* profile)
    : profile_(profile) {}

AccessibilityHandler::~AccessibilityHandler() {
  if (a11y_nav_buttons_toggle_metrics_reporter_timer_.IsRunning()) {
    a11y_nav_buttons_toggle_metrics_reporter_timer_.FireNow();
  }
}

void AccessibilityHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      "showBrowserAppearanceSettings",
      base::BindRepeating(
          &AccessibilityHandler::HandleShowBrowserAppearanceSettings,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "setStartupSoundEnabled",
      base::BindRepeating(&AccessibilityHandler::HandleSetStartupSoundEnabled,
                          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "recordSelectedShowShelfNavigationButtonValue",
      base::BindRepeating(
          &AccessibilityHandler::
              HandleRecordSelectedShowShelfNavigationButtonsValue,
          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "manageA11yPageReady",
      base::BindRepeating(&AccessibilityHandler::HandleManageA11yPageReady,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "showChromeVoxTutorial",
      base::BindRepeating(&AccessibilityHandler::HandleShowChromeVoxTutorial,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "updateBluetoothBrailleDisplayAddress",
      base::BindRepeating(
          &AccessibilityHandler::HandleUpdateBluetoothBrailleDisplayAddress,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getStartupSoundEnabled",
      base::BindRepeating(&AccessibilityHandler::HandleGetStartupSoundEnabled,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "previewFlashNotification",
      base::BindRepeating(&AccessibilityHandler::HandlePreviewFlashNotification,
                          base::Unretained(this)));
}

void AccessibilityHandler::HandleShowBrowserAppearanceSettings(
    const base::Value::List& args) {
  ash::NewWindowDelegate::GetPrimary()->OpenUrl(
      GURL(chrome::kChromeUISettingsURL).Resolve(chrome::kAppearanceSubPage),
      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      ash::NewWindowDelegate::Disposition::kSwitchToTab);
}

void AccessibilityHandler::HandleSetStartupSoundEnabled(
    const base::Value::List& args) {
  DCHECK_EQ(1U, args.size());
  bool enabled = false;
  if (args[0].is_bool()) {
    enabled = args[0].GetBool();
  }
  AccessibilityManager::Get()->SetStartupSoundEnabled(enabled);
  base::UmaHistogramBoolean(
      "ChromeOS.Settings.Accessibility.OOBEStartupSound.Enabled", enabled);
}

void AccessibilityHandler::HandleRecordSelectedShowShelfNavigationButtonsValue(
    const base::Value::List& args) {
  DCHECK_EQ(1U, args.size());
  bool enabled = false;
  if (args[0].is_bool()) {
    enabled = args[0].GetBool();
  }

  a11y_nav_buttons_toggle_metrics_reporter_timer_.Start(
      FROM_HERE, base::Seconds(10),
      base::BindOnce(&RecordShowShelfNavigationButtonsValueChange, enabled));
}

void AccessibilityHandler::HandleManageA11yPageReady(
    const base::Value::List& args) {
  AllowJavascript();
}

void AccessibilityHandler::OnJavascriptAllowed() {
  FireWebUIListener(
      "initial-data-ready",
      base::Value(AccessibilityManager::Get()->GetStartupSoundEnabled()));
  MaybeAddSodaInstallerObserver();
  MaybeAddDictationLocales();
}

void AccessibilityHandler::OnJavascriptDisallowed() {
  if (features::IsDictationOfflineAvailable()) {
    soda_observation_.Reset();
  }
}

void AccessibilityHandler::HandleShowChromeVoxTutorial(
    const base::Value::List& args) {
  AccessibilityManager::Get()->ShowChromeVoxTutorial();
}

void AccessibilityHandler::HandleUpdateBluetoothBrailleDisplayAddress(
    const base::Value::List& args) {
  CHECK_EQ(1U, args.size());
  const std::string address = args[0].GetString();
  AccessibilityManager::Get()->UpdateBluetoothBrailleDisplayAddress(address);
}

void AccessibilityHandler::HandleGetStartupSoundEnabled(
    const base::Value::List& args) {
  AllowJavascript();
  FireWebUIListener(
      "startup-sound-setting-retrieved",
      base::Value(AccessibilityManager::Get()->GetStartupSoundEnabled()));
}

void AccessibilityHandler::HandlePreviewFlashNotification(
    const base::Value::List& args) {
  AccessibilityManager::Get()->PreviewFlashNotification();
}

void AccessibilityHandler::OpenExtensionOptionsPage(const char extension_id[]) {
  const extensions::Extension* extension =
      extensions::ExtensionRegistry::Get(profile_)
          ->enabled_extensions()
          .GetByID(extension_id);
  if (!extension) {
    return;
  }

  // If Lacros is the only browser, we need to open the options page in an Ash
  // app window instead of a regular Ash browser window so that the user can't
  // navigate in Ash. We do so using the OsUrlHandler SWA. Exception: Kiosk mode
  // doesn't support SWA but already hide the navigation bar.
  bool open_with_os_url_handler =
      !crosapi::browser_util::IsAshWebBrowserEnabled() &&
      !chromeos::IsKioskSession();
  if (open_with_os_url_handler) {
    DCHECK(extensions::OptionsPageInfo::ShouldOpenInTab(extension));
    GURL url = extensions::OptionsPageInfo::GetOptionsPage(extension);
    bool launched = ash::TryLaunchOsUrlHandler(url);
    DCHECK(launched);
  } else {
    extensions::ExtensionTabUtil::OpenOptionsPage(
        extension, chrome::FindBrowserWithTab(web_ui()->GetWebContents()));
  }
}

void AccessibilityHandler::MaybeAddSodaInstallerObserver() {
  if (!features::IsDictationOfflineAvailable()) {
    return;
  }

  speech::SodaInstaller* soda_installer = speech::SodaInstaller::GetInstance();
  if (!soda_installer->IsSodaInstalled(GetDictationLocale())) {
    // Add self as an observer. If this was a page refresh we don't want to
    // get added twice.
    soda_observation_.Observe(soda_installer);
  }
}

// SodaInstaller::Observer:
void AccessibilityHandler::OnSodaInstalled(speech::LanguageCode language_code) {
  if (language_code != GetDictationLocale()) {
    return;
  }

  // Only show the success message if both the SODA binary and the language pack
  // matching the Dictation locale have been downloaded.
  FireWebUIListener(
      "dictation-locale-menu-subtitle-changed",
      base::Value(l10n_util::GetStringFUTF16(
          IDS_SETTINGS_ACCESSIBILITY_DICTATION_LOCALE_SUB_LABEL_OFFLINE,
          GetDictationLocaleDisplayName())));
}

void AccessibilityHandler::OnSodaProgress(speech::LanguageCode language_code,
                                          int progress) {
  if (language_code != speech::LanguageCode::kNone &&
      language_code != GetDictationLocale()) {
    return;
  }

  // Only show the progress message if either the Dictation locale or the SODA
  // binary has progress (encoded by LanguageCode::kNone).
  FireWebUIListener(
      "dictation-locale-menu-subtitle-changed",
      base::Value(l10n_util::GetStringFUTF16Int(
          IDS_SETTINGS_ACCESSIBILITY_DICTATION_SUBTITLE_SODA_DOWNLOAD_PROGRESS,
          progress)));
}

void AccessibilityHandler::OnSodaInstallError(
    speech::LanguageCode language_code,
    speech::SodaInstaller::ErrorCode error_code) {
  if (language_code != speech::LanguageCode::kNone &&
      language_code != GetDictationLocale()) {
    return;
  }

  // Show the failed message if either the Dictation locale failed or the SODA
  // binary failed (encoded by LanguageCode::kNone).
  FireWebUIListener(
      "dictation-locale-menu-subtitle-changed",
      base::Value(l10n_util::GetStringFUTF16(
          IDS_SETTINGS_ACCESSIBILITY_DICTATION_SUBTITLE_SODA_DOWNLOAD_ERROR,
          GetDictationLocaleDisplayName())));
}

void AccessibilityHandler::MaybeAddDictationLocales() {
  base::flat_map<std::string, Dictation::LocaleData> locales =
      Dictation::GetAllSupportedLocales();

  // Get application locale.
  std::string application_locale = g_browser_process->GetApplicationLocale();
  std::pair<std::string_view, std::string_view> application_lang_and_locale =
      language::SplitIntoMainAndTail(application_locale);

  // Get IME locales
  input_method::InputMethodManager* ime_manager =
      input_method::InputMethodManager::Get();
  std::vector<std::string> input_method_ids =
      ime_manager->GetActiveIMEState()->GetEnabledInputMethodIds();
  std::vector<std::string> ime_languages;
  ime_manager->GetInputMethodUtil()->GetLanguageCodesFromInputMethodIds(
      input_method_ids, &ime_languages);

  // Get enabled preferred UI languages.
  std::string preferred_languages =
      profile_->GetPrefs()->GetString(language::prefs::kPreferredLanguages);
  std::vector<std::string_view> enabled_languages =
      base::SplitStringPiece(preferred_languages, ",", base::TRIM_WHITESPACE,
                             base::SPLIT_WANT_NONEMPTY);

  // Combine these into one set for recommending Dication languages.
  std::set<std::string_view> ui_languages;
  ui_languages.insert(application_lang_and_locale.first);
  for (auto& ime_language : ime_languages) {
    ui_languages.insert(language::SplitIntoMainAndTail(ime_language).first);
  }
  for (auto& enabled_language : enabled_languages) {
    ui_languages.insert(language::SplitIntoMainAndTail(enabled_language).first);
  }

  base::Value::List locales_list;
  for (auto& locale : locales) {
    base::Value::Dict option;
    option.Set("value", locale.first);
    option.Set("name",
               l10n_util::GetDisplayNameForLocale(
                   locale.first, application_locale, /*is_for_ui=*/true));
    option.Set("worksOffline", locale.second.works_offline);
    option.Set("installed", locale.second.installed);

    // We can recommend languages that match the current application
    // locale, IME languages or enabled preferred languages.
    std::pair<std::string_view, std::string_view> lang_and_locale =
        language::SplitIntoMainAndTail(locale.first);
    bool is_recommended = base::Contains(ui_languages, lang_and_locale.first);

    option.Set("recommended", is_recommended);
    locales_list.Append(std::move(option));
  }

  FireWebUIListener("dictation-locales-set", locales_list);
}

// Returns the Dictation locale as a language code.
speech::LanguageCode AccessibilityHandler::GetDictationLocale() {
  const std::string dictation_locale =
      profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale);
  return speech::GetLanguageCode(dictation_locale);
}

std::u16string AccessibilityHandler::GetDictationLocaleDisplayName() {
  const std::string dictation_locale =
      profile_->GetPrefs()->GetString(prefs::kAccessibilityDictationLocale);

  return l10n_util::GetDisplayNameForLocale(
      /*locale=*/dictation_locale,
      /*display_locale=*/g_browser_process->GetApplicationLocale(),
      /*is_ui=*/true);
}

}  // namespace ash::settings