chromium/components/soda/soda_installer_impl_chromeos.cc

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

#include "components/soda/soda_installer_impl_chromeos.h"

#include <string>
#include <string_view>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_split.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"
#include "components/live_caption/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/soda/constants.h"
#include "components/soda/pref_names.h"
#include "components/soda/soda_features.h"
#include "components/soda/soda_installer.h"
#include "media/base/media_switches.h"
#include "ui/base/l10n/l10n_util.h"

namespace speech {
namespace {

constexpr char kSodaDlcName[] = "libsoda";
constexpr char kSodaEnglishUsDlcName[] = "libsoda-model-en-us";

SodaInstaller::ErrorCode DlcCodeToSodaErrorCode(const std::string& code) {
  return (code == dlcservice::kErrorNeedReboot)
             ? SodaInstaller::ErrorCode::kNeedsReboot
             : SodaInstaller::ErrorCode::kUnspecifiedError;
}
const constexpr char* const kDefaultCrOSEnabledLanguages[] = {"da-DK", "nl-NL",
                                                              "nb-NO", "sv-SE"};

}  // namespace

SodaInstallerImplChromeOS::SodaInstallerImplChromeOS() {
  available_languages_ = ConstructAvailableLanguages();
}

void SodaInstallerImplChromeOS::InitLanguages(PrefService* profile_prefs,
                                              PrefService* global_prefs) {
  if (global_prefs->GetList(prefs::kSodaRegisteredLanguagePacks).empty()) {
    // TODO(crbug.com/1200667): Register the default language used by
    // Dictation on ChromeOS.
    std::string projector_language_code =
        profile_prefs->GetString(ash::prefs::kProjectorCreationFlowLanguage);
    RegisterLanguage(projector_language_code, global_prefs);

    RegisterLanguage(prefs::GetLiveCaptionLanguageCode(profile_prefs),
                     global_prefs);
  }

  for (const auto& language :
       global_prefs->GetList(prefs::kSodaRegisteredLanguagePacks)) {
    SodaInstaller::GetInstance()->InstallLanguage(language.GetString(),
                                                  global_prefs);
  }
}

base::flat_map<std::string, SodaInstallerImplChromeOS::LanguageInfo>
SodaInstallerImplChromeOS::ConstructAvailableLanguages() const {
  base::flat_map<std::string, LanguageInfo> available_languages;
  // Defaults checked in.
  if (!base::FeatureList::IsEnabled(kCrosExpandSodaLanguages)) {
    available_languages.insert(
        {kUsEnglishLocale, {kSodaEnglishUsDlcName, LanguageCode::kEnUs}});
    return available_languages;
  }
  available_languages.insert(
      {kUsEnglishLocale, {"libsoda-model-en-us-df24d1", LanguageCode::kEnUs}});
  available_languages.insert(
      {"ja-JP", {"libsoda-model-ja-jp-df24d1", LanguageCode::kJaJp}});
  available_languages.insert(
      {"de-DE", {"libsoda-model-de-de-df24d1", LanguageCode::kDeDe}});
  available_languages.insert(
      {"fr-FR", {"libsoda-model-fr-fr-df24d1", LanguageCode::kFrFr}});
  available_languages.insert(
      {"it-IT", {"libsoda-model-it-it-df24d1", LanguageCode::kItIt}});
  available_languages.insert(
      {"en-CA", {"libsoda-model-en-ca-df24d1", LanguageCode::kEnCa}});
  available_languages.insert(
      {"en-AU", {"libsoda-model-en-au-df24d1", LanguageCode::kEnAu}});
  available_languages.insert(
      {"en-GB", {"libsoda-model-en-gb-df24d1", LanguageCode::kEnGb}});
  available_languages.insert(
      {"en-IE", {"libsoda-model-en-ie-df24d1", LanguageCode::kEnIe}});
  available_languages.insert(
      {"en-SG", {"libsoda-model-en-sg-df24d1", LanguageCode::kEnSg}});
  available_languages.insert(
      {"fr-BE", {"libsoda-model-fr-be-df24d1", LanguageCode::kFrBe}});
  available_languages.insert(
      {"fr-CH", {"libsoda-model-fr-ch-df24d1", LanguageCode::kFrCh}});
  available_languages.insert(
      {"en-IN", {"libsoda-model-en-in-df24d1", LanguageCode::kEnIn}});
  available_languages.insert(
      {"it-CH", {"libsoda-model-it-ch-df24d1", LanguageCode::kItCh}});
  available_languages.insert(
      {"de-AT", {"libsoda-model-de-at-df24d1", LanguageCode::kDeAt}});
  available_languages.insert(
      {"de-BE", {"libsoda-model-de-be-df24d1", LanguageCode::kDeBe}});
  available_languages.insert(
      {"de-CH", {"libsoda-model-de-ch-df24d1", LanguageCode::kDeCh}});
  available_languages.insert(
      {"es-US", {"libsoda-model-es-us-df24d1", LanguageCode::kEsUs}});
  available_languages.insert(
      {"es-ES", {"libsoda-model-es-us-df24d1", LanguageCode::kEsEs}});
  available_languages.insert(
      {"fr-CA", {"libsoda-model-fr-ca-df24d1", LanguageCode::kFrCa}});
  available_languages.insert(
      {"hi-IN", {"libsoda-model-hi-in-df24d1", LanguageCode::kHiIn}});
  available_languages.insert(
      {"id-ID", {"libsoda-model-id-id-df24d1", LanguageCode::kIdId}});
  available_languages.insert(
      {"ko-KR", {"libsoda-model-ko-kr-df24d1", LanguageCode::kKoKr}});
  available_languages.insert(
      {"pl-PL", {"libsoda-model-pl-pl-df24d1", LanguageCode::kPlPl}});
  available_languages.insert(
      {"th-TH", {"libsoda-model-th-th-df24d1", LanguageCode::kThTh}});
  available_languages.insert(
      {"tr-TR", {"libsoda-model-tr-tr-df24d1", LanguageCode::kTrTr}});
  available_languages.insert(
      {"cmn-Hant-TW", {"libsoda-model-zh-tw-df24d1", LanguageCode::kZhTw}});
  available_languages.insert(
      {"cmn-Hans-CN", {"libsoda-model-zh-cn-df24d1", LanguageCode::kZhCn}});
  available_languages.insert(
      {"pt-BR", {"libsoda-model-pt-br-df24d1", LanguageCode::kPtBr}});
  available_languages.insert(
      {"ru-RU", {"libsoda-model-ru-ru-df24d1", LanguageCode::kRuRu}});
  available_languages.insert(
      {"vi-VN", {"libsoda-model-vi-vn-df24d1", LanguageCode::kViVn}});
  available_languages.insert({"da-DK", {"", LanguageCode::kDaDk}});
  available_languages.insert({"nb-NO", {"", LanguageCode::kNbNo}});
  available_languages.insert({"nl-NL", {"", LanguageCode::kNlNl}});
  available_languages.insert({"sv-SE", {"", LanguageCode::kSvSe}});

  if (base::FeatureList::IsEnabled(kFeatureManagementCrosSodaConchLanguages) &&
      base::FeatureList::IsEnabled(kCrosSodaConchLanguages)) {
    available_languages["da-DK"] = {"libsoda-model-da-dk-cnch24d2",
                                    LanguageCode::kDaDk};
    available_languages["de-AT"] = {"libsoda-model-de-at-cnch24d2",
                                    LanguageCode::kDeAt};
    available_languages["de-BE"] = {"libsoda-model-de-be-cnch24d2",
                                    LanguageCode::kDeBe};
    available_languages["de-CH"] = {"libsoda-model-de-ch-cnch24d2",
                                    LanguageCode::kDeCh};
    available_languages["de-DE"] = {"libsoda-model-de-de-cnch24d2",
                                    LanguageCode::kDeDe};
    available_languages["en-AU"] = {"libsoda-model-en-au-cnch24d2",
                                    LanguageCode::kEnAu};
    available_languages["en-CA"] = {"libsoda-model-en-ca-cnch24d2",
                                    LanguageCode::kEnCa};
    available_languages["en-GB"] = {"libsoda-model-en-gb-cnch24d2",
                                    LanguageCode::kEnGb};
    available_languages["en-IE"] = {"libsoda-model-en-ie-cnch24d2",
                                    LanguageCode::kEnIe};
    available_languages["en-IN"] = {"libsoda-model-en-in-cnch24d2",
                                    LanguageCode::kEnIn};
    available_languages["en-SG"] = {"libsoda-model-en-sg-cnch24d2",
                                    LanguageCode::kEnSg};
    available_languages["en-US"] = {"libsoda-model-en-us-cnch24d2",
                                    LanguageCode::kEnUs};
    available_languages["es-ES"] = {"libsoda-model-es-es-cnch24d2",
                                    LanguageCode::kEsEs};
    available_languages["es-US"] = {"libsoda-model-es-us-cnch24d2",
                                    LanguageCode::kEsUs};
    available_languages["fr-BE"] = {"libsoda-model-fr-be-cnch24d2",
                                    LanguageCode::kFrBe};
    available_languages["fr-CA"] = {"libsoda-model-fr-ca-cnch24d2",
                                    LanguageCode::kFrCa};
    available_languages["fr-CH"] = {"libsoda-model-fr-Ch-cnch24d2",
                                    LanguageCode::kFrCh};
    available_languages["fr-FR"] = {"libsoda-model-fr-fr-cnch24d2",
                                    LanguageCode::kFrFr};
    available_languages["hi-IN"] = {"libsoda-model-hi-in-cnch24d2",
                                    LanguageCode::kHiIn};
    available_languages["it-IT"] = {"libsoda-model-it-it-cnch24d2",
                                    LanguageCode::kItIt};
    available_languages["ja-JP"] = {"libsoda-model-ja-jp-cnch24d2",
                                    LanguageCode::kJaJp};
    available_languages["ko-KR"] = {"libsoda-model-ko-kr-cnch24d2",
                                    LanguageCode::kKoKr};
    available_languages["nb-NO"] = {"libsoda-model-nb-no-cnch24d2",
                                    LanguageCode::kNbNo};
    available_languages["nl-NL"] = {"libsoda-model-nl-nl-cnch24d2",
                                    LanguageCode::kNlNl};
    available_languages["sv-SE"] = {"libsoda-model-sv-se-cnch24d2",
                                    LanguageCode::kSvSe};
  }

  // Add in from feature flags. the value is of the format:
  // "en-AU:libsoda-modelname,fr-CA:,de-CH:libsoda-pizzaface,"
  // Note that fr-CA is removed explicitly in example.
  std::vector<std::string> langs =
      base::SplitString(base::GetFieldTrialParamValueByFeature(
                            kCrosExpandSodaLanguages, "available_languages"),
                        ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  for (const auto& unparsed_pair : langs) {
    std::vector<std::string> lang_model_pair = base::SplitString(
        unparsed_pair, ":", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
    if (lang_model_pair.size() != 2) {
      // skip, but log.
      LOG(DFATAL) << "Unable to parse a language pair, in wrong format. "
                     "value received and ignored is "
                  << unparsed_pair;
      continue;
    }
    if (lang_model_pair[1].rfind("libsoda", 0) == std::string::npos &&
        !lang_model_pair[1].empty()) {
      LOG(ERROR) << "Incorrect prefix for " << lang_model_pair[0]
                 << " given, is: " << lang_model_pair[1] << " and ignoring.";
      continue;
    }
    const auto& lang_it = available_languages.find(lang_model_pair[0]);
    if (lang_it == available_languages.end()) {
      LOG(ERROR) << "Unable to find language " << lang_model_pair[0]
                 << ", ignoring.";
      continue;
    }
    lang_it->second.dlc_name = lang_model_pair[1];
  }

  // Remove empty.
  base::EraseIf(available_languages,
                [](const auto& it) { return it.second.dlc_name.empty(); });
  return available_languages;
}

SodaInstallerImplChromeOS::~SodaInstallerImplChromeOS() = default;

base::FilePath SodaInstallerImplChromeOS::GetSodaBinaryPath() const {
  return soda_lib_path_;
}

base::FilePath SodaInstallerImplChromeOS::GetLanguagePath(
    const std::string& language) const {
  auto available_it = available_languages_.find(language);
  if (available_it == available_languages_.end()) {
    LOG(DFATAL) << "Asked for unavailable language " << language;
    return base::FilePath();
  }
  auto it = installed_language_paths_.find(available_it->second.language_code);
  if (it == installed_language_paths_.end()) {
    return base::FilePath();
  }
  return it->second;
}

void SodaInstallerImplChromeOS::InstallSoda(PrefService* global_prefs) {
  if (soda_binary_installed_ || never_download_soda_for_testing_) {
    return;
  }

  // Clear cached path in case this is a reinstallation (path could
  // change).
  SetSodaBinaryPath(base::FilePath());

  is_soda_downloading_ = true;
  soda_progress_ = 0.0;

  // Install SODA DLC.
  dlcservice::InstallRequest install_request;
  install_request.set_id(kSodaDlcName);
  ash::DlcserviceClient::Get()->Install(
      install_request,
      base::BindOnce(&SodaInstallerImplChromeOS::OnSodaInstalled,
                     base::Unretained(this), base::Time::Now()),
      base::BindRepeating(&SodaInstallerImplChromeOS::OnSodaProgress,
                          base::Unretained(this)));
}

void SodaInstallerImplChromeOS::InstallLanguage(const std::string& language,
                                                PrefService* global_prefs) {
  if (never_download_soda_for_testing_)
    return;
  SodaInstaller::RegisterLanguage(language, global_prefs);
  // Clear cached path in case this is a reinstallation (path could
  // change).

  auto language_info = available_languages_.find(language);
  if (language_info == available_languages_.end()) {
    LOG(DFATAL)
        << "Language " << language
        << " not in list of available languages. refusing to install anything.";
    return;
  }

  SetLanguagePath(language_info->second.language_code, base::FilePath());
  language_pack_progress_.insert({language_info->second.language_code, 0.0});

  dlcservice::InstallRequest install_request;
  install_request.set_id(language_info->second.dlc_name);
  ash::DlcserviceClient::Get()->Install(
      install_request,
      base::BindOnce(&SodaInstallerImplChromeOS::OnLanguageInstalled,
                     base::Unretained(this),
                     language_info->second.language_code, language,
                     base::Time::Now()),
      base::BindRepeating(&SodaInstallerImplChromeOS::OnLanguageProgress,
                          base::Unretained(this),
                          language_info->second.language_code));
}

void SodaInstallerImplChromeOS::UninstallLanguage(const std::string& language,
                                                  PrefService* global_prefs) {
  SodaInstaller::UnregisterLanguage(language, global_prefs);
  const auto& language_info = available_languages_.find(language);
  if (language_info == available_languages_.end()) {
    LOG(FATAL) << "Unable to uninstall language " << language
               << " as it is not in the list of available languages.";
  }
  const auto& dlc_name = language_info->second.dlc_name;
  installed_languages_.erase(language_info->second.language_code);
  installed_language_paths_.erase(language_info->second.language_code);
  language_pack_progress_.erase(language_info->second.language_code);

  ash::DlcserviceClient::Get()->Uninstall(
      dlc_name, base::BindOnce(&SodaInstallerImplChromeOS::OnDlcUninstalled,
                               base::Unretained(this), dlc_name));
}

std::vector<std::string> SodaInstallerImplChromeOS::GetAvailableLanguages()
    const {
  std::vector<std::string> languages;
  for (const auto& it : available_languages_) {
    languages.push_back(it.first);
  }
  return languages;
}

std::vector<std::string>
SodaInstallerImplChromeOS::GetLiveCaptionEnabledLanguages() const {
  auto enabled_languages = SodaInstaller::GetLiveCaptionEnabledLanguages();
  // extra CrOS languages.
  if (base::FeatureList::IsEnabled(kFeatureManagementCrosSodaConchLanguages) &&
      base::FeatureList::IsEnabled(kCrosSodaConchLanguages)) {
    for (const char* const enabled_language : kDefaultCrOSEnabledLanguages) {
      enabled_languages.push_back(enabled_language);
    }
  }
  return enabled_languages;
}

std::string SodaInstallerImplChromeOS::GetLanguageDlcNameForLocale(
    const std::string& locale) const {
  const auto& language_info = available_languages_.find(locale);
  if (language_info == available_languages_.end()) {
    LOG(DFATAL) << "Asked for unavailable language " << locale;
    return std::string();
  }

  return language_info->second.dlc_name;
}

void SodaInstallerImplChromeOS::UninstallSoda(PrefService* global_prefs) {
  soda_binary_installed_ = false;
  SetSodaBinaryPath(base::FilePath());
  ash::DlcserviceClient::Get()->Uninstall(
      kSodaDlcName, base::BindOnce(&SodaInstallerImplChromeOS::OnDlcUninstalled,
                                   base::Unretained(this), kSodaDlcName));
  // We iterate through all languages and check for installation, in order to
  // decide what's happened.
  for (const auto& it : available_languages_) {
    const auto path = GetLanguagePath(it.first);
    if (!path.empty()) {
      const auto dlc_name = it.second.dlc_name;
      LOG(ERROR) << "Removing dlc " << dlc_name << " for " << it.first;
      ash::DlcserviceClient::Get()->Uninstall(
          dlc_name, base::BindOnce(&SodaInstallerImplChromeOS::OnDlcUninstalled,
                                   base::Unretained(this), dlc_name));
    }
  }
  installed_languages_.clear();
  language_pack_progress_.clear();
  SodaInstaller::UnregisterLanguages(global_prefs);
  installed_language_paths_.clear();
  global_prefs->SetTime(prefs::kSodaScheduledDeletionTime, base::Time());
}

void SodaInstallerImplChromeOS::SetSodaBinaryPath(base::FilePath new_path) {
  soda_lib_path_ = new_path;
}

void SodaInstallerImplChromeOS::SetLanguagePath(
    const LanguageCode language_code,
    base::FilePath new_path) {
  installed_language_paths_[language_code] = new_path;
}

void SodaInstallerImplChromeOS::OnSodaInstalled(
    const base::Time start_time,
    const ash::DlcserviceClient::InstallResult& install_result) {
  is_soda_downloading_ = false;
  if (install_result.error == dlcservice::kErrorNone) {
    soda_binary_installed_ = true;
    soda_progress_ = 1.0;
    SetSodaBinaryPath(base::FilePath(install_result.root_path));
    for (const auto& available_lang : available_languages_) {
      // Check every installed language and notify on it, in case the language
      // came before soda.
      if (IsLanguageInstalled(available_lang.second.language_code)) {
        NotifyOnSodaInstalled(available_lang.second.language_code);
      }
    }

    base::UmaHistogramTimes(kSodaBinaryInstallationSuccessTimeTaken,
                            base::Time::Now() - start_time);
  } else {
    soda_binary_installed_ = false;
    soda_progress_ = 0.0;
    NotifyOnSodaInstallError(LanguageCode::kNone,
                             DlcCodeToSodaErrorCode(install_result.error));
    base::UmaHistogramTimes(kSodaBinaryInstallationFailureTimeTaken,
                            base::Time::Now() - start_time);
  }

  base::UmaHistogramBoolean(kSodaBinaryInstallationResult,
                            install_result.error == dlcservice::kErrorNone);
}

void SodaInstallerImplChromeOS::OnLanguageInstalled(
    const LanguageCode language_code,
    const std::string language,
    const base::Time start_time,
    const ash::DlcserviceClient::InstallResult& install_result) {
  language_pack_progress_.erase(language_code);
  if (install_result.error == dlcservice::kErrorNone) {
    installed_languages_.insert(language_code);
    SetLanguagePath(language_code, base::FilePath(install_result.root_path));
    if (soda_binary_installed_) {
      NotifyOnSodaInstalled(language_code);
    }
    base::UmaHistogramTimes(
        GetInstallationSuccessTimeMetricForLanguage(language),
        base::Time::Now() - start_time);

  } else {
    // TODO: Notify the observer of the specific language pack that failed
    // to install. ChromeOS currently only supports the en-US language pack.
    NotifyOnSodaInstallError(language_code,
                             DlcCodeToSodaErrorCode(install_result.error));

    base::UmaHistogramTimes(
        GetInstallationFailureTimeMetricForLanguage(language),
        base::Time::Now() - start_time);
  }

  base::UmaHistogramBoolean(GetInstallationResultMetricForLanguage(language),
                            install_result.error == dlcservice::kErrorNone);
}

void SodaInstallerImplChromeOS::OnSodaProgress(double progress) {
  soda_progress_ = progress;
  OnSodaCombinedProgress();
}

void SodaInstallerImplChromeOS::OnLanguageProgress(
    const LanguageCode language_code,
    double progress) {
  language_pack_progress_[language_code] = progress;
  OnSodaCombinedProgress();
}

void SodaInstallerImplChromeOS::OnSodaCombinedProgress() {
  // Each language progress is weighed a little with the overall soda progress,
  // so that we only reach 100% if and only if soda binary itself is installed.
  // When the binary is installed, we use the plain percentage.
  const double language_weight = 4.0;
  for (const auto& per_language_progress : language_pack_progress_) {
    double progress = per_language_progress.second;

    // If SODA is downloading, report a combined progress for this language.
    if (is_soda_downloading_) {
      progress =
          (soda_progress_ + language_weight * progress) / (1 + language_weight);
    }
    NotifyOnSodaProgress(per_language_progress.first,
                         base::ClampFloor(100 * progress));
  }
}

void SodaInstallerImplChromeOS::OnDlcUninstalled(std::string_view dlc_id,
                                                 std::string_view err) {
  if (err != dlcservice::kErrorNone) {
    LOG(ERROR) << "Failed to uninstall DLC " << dlc_id << ". Error: " << err;
  } else {
    LOG(ERROR) << "Successful uninstall of dlc " << dlc_id;
  }
}

}  // namespace speech