chromium/chromeos/ash/components/language_packs/language_pack_manager.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chromeos/ash/components/language_packs/language_pack_manager.h"

#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/check_is_test.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/hash/hash.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/sequence_checker.h"
#include "base/strings/string_split.h"
#include "base/task/single_thread_task_runner.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"
#include "chromeos/ash/components/language_packs/handwriting.h"
#include "chromeos/ash/components/language_packs/language_packs_util.h"
#include "components/prefs/pref_service.h"
#include "third_party/cros_system_api/dbus/dlcservice/dbus-constants.h"

using ::ash::input_method::InputMethodManager;

namespace ash::language_packs {
namespace {

LanguagePackManager* g_instance = nullptr;

const base::flat_map<std::string, std::string>& GetAllBasePackDlcIds() {
  // Map of all features and corresponding Base Pack DLC IDs.
  static const base::NoDestructor<base::flat_map<std::string, std::string>>
      all_dlc_ids({
          {kHandwritingFeatureId, "handwriting-base"},
      });

  return *all_dlc_ids;
}

// Finds the ID of the DLC corresponding to the Base Pack for a feature.
// Returns the DLC ID if the feature has a Base Pack or std::nullopt
// otherwise.
std::optional<std::string> GetDlcIdForBasePack(const std::string& feature_id) {
  // We search in the static list for the given |feature_id|.
  const auto it = GetAllBasePackDlcIds().find(feature_id);

  if (it == GetAllBasePackDlcIds().end()) {
    return std::nullopt;
  }

  return it->second;
}

// Run a callback later in the current `SingleThreadedTaskRunner`.
void RunCallbackLater(base::OnceClosure task) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
                                                              std::move(task));
}

void InstallDlc(const std::string& dlc_id,
                DlcserviceClient::InstallCallback callback) {
  DlcserviceClient* client = DlcserviceClient::Get();
  if (client) {
    dlcservice::InstallRequest install_request;
    install_request.set_id(dlc_id);
    client->Install(install_request, std::move(callback), base::DoNothing());
  } else {
    CHECK_IS_TEST();
    DlcserviceClient::InstallResult result;
    result.error = dlcservice::kErrorInternal;

    RunCallbackLater(base::BindOnce(std::move(callback), std::move(result)));
  }
}

void GetDlcState(const std::string& dlc_id,
                 DlcserviceClient::GetDlcStateCallback callback) {
  DlcserviceClient* client = DlcserviceClient::Get();
  if (client) {
    client->GetDlcState(dlc_id, std::move(callback));
  } else {
    CHECK_IS_TEST();
    dlcservice::DlcState state;
    state.set_id(dlc_id);
    state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
    RunCallbackLater(base::BindOnce(
        std::move(callback), dlcservice::kErrorInternal, std::move(state)));
  }
}

void UninstallDlc(const std::string& dlc_id,
                  DlcserviceClient::UninstallCallback callback) {
  DlcserviceClient* client = DlcserviceClient::Get();
  if (client) {
    client->Uninstall(dlc_id, std::move(callback));
  } else {
    CHECK_IS_TEST();
    RunCallbackLater(
        base::BindOnce(std::move(callback), dlcservice::kErrorInternal));
  }
}

// Warning: These DLCs are guaranteed to be downloaded (is_trusted), but not
// guaranteed to be installed (state == INSTALLED).
void GetExistingDlcs(DlcserviceClient::GetExistingDlcsCallback callback) {
  DlcserviceClient* client = DlcserviceClient::Get();
  if (client) {
    client->GetExistingDlcs(std::move(callback));
  } else {
    CHECK_IS_TEST();
    RunCallbackLater(base::BindOnce(std::move(callback),
                                    dlcservice::kErrorInternal,
                                    dlcservice::DlcsWithContent()));
  }
}

void OnInstallDlcComplete(OnInstallCompleteCallback callback,
                          const std::string& feature_id,
                          const std::string& locale,
                          const DlcserviceClient::InstallResult& dlc_result) {
  PackResult result = ConvertDlcInstallResultToPackResult(dlc_result);
  result.feature_id = feature_id;
  result.language_code = locale;

  const bool success = result.operation_error == PackResult::ErrorCode::kNone;
  if (!success) {
    if (feature_id == kHandwritingFeatureId) {
      base::UmaHistogramEnumeration(
          "ChromeOS.LanguagePacks.InstallError.Handwriting",
          GetDlcErrorTypeForUma(dlc_result.error));
    } else if (feature_id == kTtsFeatureId) {
      base::UmaHistogramEnumeration("ChromeOS.LanguagePacks.InstallError.Tts",
                                    GetDlcErrorTypeForUma(dlc_result.error));
    } else if (feature_id == kFontsFeatureId) {
      base::UmaHistogramEnumeration("ChromeOS.LanguagePacks.InstallError.Fonts",
                                    GetDlcErrorTypeForUma(dlc_result.error));
    }
  }

  base::UmaHistogramEnumeration("ChromeOS.LanguagePacks.InstallPack.Success",
                                GetSuccessValueForUma(feature_id, success));

  std::move(callback).Run(result);
}

void OnUninstallDlcComplete(OnUninstallCompleteCallback callback,
                            std::string_view feature_id,
                            const std::string& locale,
                            std::string_view err) {
  PackResult result;
  result.feature_id = feature_id;
  result.language_code = locale;
  result.operation_error = ConvertDlcErrorToErrorCode(err);

  const bool success = err == dlcservice::kErrorNone;
  if (success) {
    result.pack_state = PackResult::StatusCode::kNotInstalled;
  } else {
    result.pack_state = PackResult::StatusCode::kUnknown;
  }

  base::UmaHistogramBoolean("ChromeOS.LanguagePacks.UninstallComplete.Success",
                            success);

  std::move(callback).Run(result);
}

void OnGetDlcState(GetPackStateCallback callback,
                   std::string feature_id,
                   const std::string& locale,
                   std::string_view err,
                   const dlcservice::DlcState& dlc_state) {
  PackResult result;
  if (dlc_state.is_verified() &&
      dlc_state.state() == dlcservice::DlcState_State_NOT_INSTALLED) {
    // Mount the DLC for the client if it already exists on disk.
    // By pure coincidence, `GetPackStateCallback` is the same as
    // `OnInstallCompleteCallback`, so we can directly pass in the
    // client-supplied callback here.
    InstallDlc(dlc_state.id(),
               base::BindOnce(&OnInstallDlcComplete, std::move(callback),
                              std::move(feature_id), std::move(locale)));
    return;
  }

  // GetDlcState() returns 2 errors:
  // one for the DBus call and one for the actual DLC.
  // If the first error is set we can ignore the DLC state.
  if (err.empty() || err == dlcservice::kErrorNone) {
    result = ConvertDlcStateToPackResult(dlc_state);
  } else {
    result.operation_error = ConvertDlcErrorToErrorCode(err);
    result.pack_state = PackResult::StatusCode::kUnknown;
  }

  result.feature_id = feature_id;
  result.language_code = locale;

  std::move(callback).Run(result);
}

// This functions goes through the list of locales to install and remove,
// according to the diff. It performs the actual installation and uninstallation
// of DLCs on the device.
// It should be called whenever Input Methods are changed.
void InstallOrRemoveToMatchState(const std::string& feature_id,
                                 const StringsDiff& locale_diff) {
  for (const std::string& locale : locale_diff.remove) {
    LanguagePackManager::RemovePack(feature_id, locale, base::DoNothing());
  }
  for (const std::string& locale : locale_diff.add) {
    LanguagePackManager::InstallPack(feature_id, locale, base::DoNothing());
  }
}

// Updates packs for input methods based on the user prefs and the currently
// installed DLCs.
// TODO: b/294162606 - Write unit tests for this function if possible.
void UpdateFromInputMethodPrefs(
    base::span<const std::string> installed_hwr_locales,
    input_method::InputMethodUtil* input_method_util,
    PrefService* prefs) {
  const std::vector<std::string> input_method_ids =
      ExtractInputMethodsFromPrefs(prefs);
  const base::flat_set<std::string> target_hwr_locales = MapThenFilterStrings(
      input_method_ids, base::BindRepeating(MapInputMethodIdToHandwritingLocale,
                                            input_method_util));

  const StringsDiff locale_diff = ComputeStringsDiff(
      {installed_hwr_locales.begin(), installed_hwr_locales.end()},
      target_hwr_locales);

  InstallOrRemoveToMatchState(kHandwritingFeatureId, locale_diff);
}

// Callback for dlcservice::GetExistingDlcs().
// TODO: b/294162606 - Write unit tests for this function if possible.
void OnGetExistingDlcs(PrefService* prefs,
                       std::string_view err,
                       const dlcservice::DlcsWithContent& dlcs_with_content) {
  if (!err.empty() && err != dlcservice::kErrorNone) {
    DLOG(ERROR) << "DlcserviceClient::GetExisingDlcs() returned error";
    // TODO: b/285985206 - Record a UMA histogram.
    return;
  }

  const base::flat_set<std::string> hwr_locales =
      ConvertDlcsWithContentToHandwritingLocales(dlcs_with_content);
  UpdateFromInputMethodPrefs({hwr_locales.begin(), hwr_locales.end()},
                             InputMethodManager::Get()->GetInputMethodUtil(),
                             prefs);
}

}  // namespace

const base::flat_map<PackSpecPair, std::string>& GetAllLanguagePackDlcIds() {
  // Map of all DLCs and corresponding IDs.
  // It's a map from PackSpecPair to DLC ID. The pair is <feature id, locale>.
  // Whenever a new DLC is created, it needs to be added here.
  // Clients of Language Packs don't need to know the IDs.
  static const base::NoDestructor<base::flat_map<PackSpecPair, std::string>>
      all_dlc_ids({
          // Handwriting Recognition.
          {{kHandwritingFeatureId, "am"}, "handwriting-am"},
          {{kHandwritingFeatureId, "ar"}, "handwriting-ar"},
          {{kHandwritingFeatureId, "be"}, "handwriting-be"},
          {{kHandwritingFeatureId, "bg"}, "handwriting-bg"},
          {{kHandwritingFeatureId, "bn"}, "handwriting-bn"},
          {{kHandwritingFeatureId, "ca"}, "handwriting-ca"},
          {{kHandwritingFeatureId, "cs"}, "handwriting-cs"},
          {{kHandwritingFeatureId, "da"}, "handwriting-da"},
          {{kHandwritingFeatureId, "de"}, "handwriting-de"},
          {{kHandwritingFeatureId, "el"}, "handwriting-el"},
          {{kHandwritingFeatureId, "en"}, "handwriting-en"},
          {{kHandwritingFeatureId, "es"}, "handwriting-es"},
          {{kHandwritingFeatureId, "et"}, "handwriting-et"},
          {{kHandwritingFeatureId, "fa"}, "handwriting-fa"},
          {{kHandwritingFeatureId, "fi"}, "handwriting-fi"},
          {{kHandwritingFeatureId, "fil"}, "handwriting-fil"},
          {{kHandwritingFeatureId, "fr"}, "handwriting-fr"},
          {{kHandwritingFeatureId, "ga"}, "handwriting-ga"},
          {{kHandwritingFeatureId, "gu"}, "handwriting-gu"},
          {{kHandwritingFeatureId, "hi"}, "handwriting-hi"},
          {{kHandwritingFeatureId, "hr"}, "handwriting-hr"},
          {{kHandwritingFeatureId, "hu"}, "handwriting-hu"},
          {{kHandwritingFeatureId, "hy"}, "handwriting-hy"},
          {{kHandwritingFeatureId, "id"}, "handwriting-id"},
          {{kHandwritingFeatureId, "is"}, "handwriting-is"},
          {{kHandwritingFeatureId, "it"}, "handwriting-it"},
          {{kHandwritingFeatureId, "iw"}, "handwriting-iw"},
          {{kHandwritingFeatureId, "ja"}, "handwriting-ja"},
          {{kHandwritingFeatureId, "ka"}, "handwriting-ka"},
          {{kHandwritingFeatureId, "kk"}, "handwriting-kk"},
          {{kHandwritingFeatureId, "km"}, "handwriting-km"},
          {{kHandwritingFeatureId, "kn"}, "handwriting-kn"},
          {{kHandwritingFeatureId, "ko"}, "handwriting-ko"},
          {{kHandwritingFeatureId, "lo"}, "handwriting-lo"},
          {{kHandwritingFeatureId, "lt"}, "handwriting-lt"},
          {{kHandwritingFeatureId, "lv"}, "handwriting-lv"},
          {{kHandwritingFeatureId, "ml"}, "handwriting-ml"},
          {{kHandwritingFeatureId, "mn"}, "handwriting-mn"},
          {{kHandwritingFeatureId, "mr"}, "handwriting-mr"},
          {{kHandwritingFeatureId, "ms"}, "handwriting-ms"},
          {{kHandwritingFeatureId, "mt"}, "handwriting-mt"},
          {{kHandwritingFeatureId, "my"}, "handwriting-my"},
          {{kHandwritingFeatureId, "ne"}, "handwriting-ne"},
          {{kHandwritingFeatureId, "nl"}, "handwriting-nl"},
          {{kHandwritingFeatureId, "no"}, "handwriting-no"},
          {{kHandwritingFeatureId, "or"}, "handwriting-or"},
          {{kHandwritingFeatureId, "pa"}, "handwriting-pa"},
          {{kHandwritingFeatureId, "pl"}, "handwriting-pl"},
          {{kHandwritingFeatureId, "pt"}, "handwriting-pt"},
          {{kHandwritingFeatureId, "ro"}, "handwriting-ro"},
          {{kHandwritingFeatureId, "ru"}, "handwriting-ru"},
          {{kHandwritingFeatureId, "si"}, "handwriting-si"},
          {{kHandwritingFeatureId, "sk"}, "handwriting-sk"},
          {{kHandwritingFeatureId, "sl"}, "handwriting-sl"},
          {{kHandwritingFeatureId, "sr"}, "handwriting-sr"},
          {{kHandwritingFeatureId, "sv"}, "handwriting-sv"},
          {{kHandwritingFeatureId, "ta"}, "handwriting-ta"},
          {{kHandwritingFeatureId, "te"}, "handwriting-te"},
          {{kHandwritingFeatureId, "th"}, "handwriting-th"},
          {{kHandwritingFeatureId, "ti"}, "handwriting-ti"},
          {{kHandwritingFeatureId, "tr"}, "handwriting-tr"},
          {{kHandwritingFeatureId, "uk"}, "handwriting-uk"},
          {{kHandwritingFeatureId, "ur"}, "handwriting-ur"},
          {{kHandwritingFeatureId, "vi"}, "handwriting-vi"},
          {{kHandwritingFeatureId, "zh"}, "handwriting-zh"},
          {{kHandwritingFeatureId, "zh-HK"}, "handwriting-zh-HK"},

          // Text-To-Speech.
          {{kTtsFeatureId, "bn"}, "tts-bn-bd-c"},
          {{kTtsFeatureId, "cs"}, "tts-cs-cz-c"},
          {{kTtsFeatureId, "da"}, "tts-da-dk-c"},
          {{kTtsFeatureId, "de"}, "tts-de-de-c"},
          {{kTtsFeatureId, "el"}, "tts-el-gr-c"},
          {{kTtsFeatureId, "en-au"}, "tts-en-au-c"},
          {{kTtsFeatureId, "en-gb"}, "tts-en-gb-c"},
          {{kTtsFeatureId, "en-us"}, "tts-en-us-c"},
          {{kTtsFeatureId, "es-es"}, "tts-es-es-c"},
          {{kTtsFeatureId, "es-us"}, "tts-es-us-c"},
          {{kTtsFeatureId, "fi"}, "tts-fi-fi-c"},
          {{kTtsFeatureId, "fil"}, "tts-fil-ph-c"},
          {{kTtsFeatureId, "fr"}, "tts-fr-fr-c"},
          {{kTtsFeatureId, "hi"}, "tts-hi-in-c"},
          {{kTtsFeatureId, "hu"}, "tts-hu-hu-c"},
          {{kTtsFeatureId, "id"}, "tts-id-id-c"},
          {{kTtsFeatureId, "it"}, "tts-it-it-c"},
          {{kTtsFeatureId, "ja"}, "tts-ja-jp-c"},
          {{kTtsFeatureId, "km"}, "tts-km-kh-c"},
          {{kTtsFeatureId, "ko"}, "tts-ko-kr-c"},
          {{kTtsFeatureId, "nb"}, "tts-nb-no-c"},
          {{kTtsFeatureId, "ne"}, "tts-ne-np-c"},
          {{kTtsFeatureId, "nl"}, "tts-nl-nl-c"},
          {{kTtsFeatureId, "pl"}, "tts-pl-pl-c"},
          {{kTtsFeatureId, "pt-br"}, "tts-pt-br-c"},
          {{kTtsFeatureId, "pt-pt"}, "tts-pt-pt-c"},
          {{kTtsFeatureId, "si"}, "tts-si-lk-c"},
          {{kTtsFeatureId, "sk"}, "tts-sk-sk-c"},
          {{kTtsFeatureId, "sv"}, "tts-sv-se-c"},
          {{kTtsFeatureId, "th"}, "tts-th-th-c"},
          {{kTtsFeatureId, "tr"}, "tts-tr-tr-c"},
          {{kTtsFeatureId, "uk"}, "tts-uk-ua-c"},
          {{kTtsFeatureId, "vi"}, "tts-vi-vn-c"},
          {{kTtsFeatureId, "yue"}, "tts-yue-hk-c"},

          // Fonts.
          {{kFontsFeatureId, "ja"}, "extrafonts-ja"},
          {{kFontsFeatureId, "ko"}, "extrafonts-ko"},
      });

  return *all_dlc_ids;
}

// TODO: b/294162606 - Calling this function with a `std::string_view` or a
// `const char*` argument causes two string copies per argument - one to call
// the function, and one to create the `PackSpecPair` to look up in the map.
// Either refactor this function to take in a `std::string_view` to reduce it
// down to one string copy per argument, use heterogeneous lookup
// (https://abseil.io/tips/144) to reduce it down to zero string copies, or
// rewrite this function completely.
std::optional<std::string> GetDlcIdForLanguagePack(
    const std::string& feature_id,
    const std::string& locale) {
  // We search in the static list for the given Pack spec.
  const PackSpecPair spec(feature_id, locale);
  const auto it = GetAllLanguagePackDlcIds().find(spec);

  if (it == GetAllLanguagePackDlcIds().end()) {
    return std::nullopt;
  }

  return it->second;
}

std::optional<std::string> DlcToTtsLocale(std::string_view dlc_id) {
  const base::flat_map<PackSpecPair, std::string>& all_ids =
      GetAllLanguagePackDlcIds();
  // Relies on the fact that TTS `PackSpecPair`s are "grouped together" in the
  // sorted `flat_map`.
  auto it = all_ids.upper_bound({kTtsFeatureId, ""});
  while (it != all_ids.end() && it->first.feature_id == kTtsFeatureId) {
    if (it->second == dlc_id) {
      return it->first.locale;
    }
    ++it;
  }

  return std::nullopt;
}

///////////////////////////////////////////////////////////
// PackResult constructors and destructors.
PackResult::PackResult() {
  this->pack_state = PackResult::StatusCode::kUnknown;
}

PackResult::~PackResult() = default;

PackResult::PackResult(const PackResult&) = default;
///////////////////////////////////////////////////////////

bool LanguagePackManager::IsPackAvailable(const std::string& feature_id,
                                          const std::string& input_locale) {
  const std::string locale = ResolveLocale(feature_id, input_locale);

  // We search in the static list for the given Pack spec.
  const PackSpecPair spec(feature_id, locale);
  return base::Contains(GetAllLanguagePackDlcIds(), spec);
}

void LanguagePackManager::InstallPack(const std::string& feature_id,
                                      const std::string& input_locale,
                                      OnInstallCompleteCallback callback) {
  const std::string locale = ResolveLocale(feature_id, input_locale);
  const std::optional<std::string> dlc_id =
      GetDlcIdForLanguagePack(feature_id, locale);

  // If the given Language Pack doesn't exist, run callback and don't reach the
  // DLC Service.
  if (!dlc_id) {
    std::move(callback).Run(CreateInvalidDlcPackResult());
    return;
  }

  InstallDlc(*dlc_id, base::BindOnce(&OnInstallDlcComplete, std::move(callback),
                                     feature_id, locale));
}

void LanguagePackManager::GetPackState(const std::string& feature_id,
                                       const std::string& input_locale,
                                       GetPackStateCallback callback) {
  const std::string locale = ResolveLocale(feature_id, input_locale);
  const std::optional<std::string> dlc_id =
      GetDlcIdForLanguagePack(feature_id, locale);

  // If the given Language Pack doesn't exist, run callback and don't reach the
  // DLC Service.
  if (!dlc_id) {
    std::move(callback).Run(CreateInvalidDlcPackResult());
    return;
  }

  // TODO: b/351723265 - Split this language code metric into a metric for each
  // feature.
  base::UmaHistogramSparse("ChromeOS.LanguagePacks.GetPackState.LanguageCode",
                           static_cast<int32_t>(base::PersistentHash(locale)));
  base::UmaHistogramEnumeration("ChromeOS.LanguagePacks.GetPackState.FeatureId",
                                GetFeatureIdValueForUma(feature_id));

  GetDlcState(*dlc_id, base::BindOnce(&OnGetDlcState, std::move(callback),
                                      feature_id, locale));
}

void LanguagePackManager::RemovePack(const std::string& feature_id,
                                     const std::string& input_locale,
                                     OnUninstallCompleteCallback callback) {
  const std::string locale = ResolveLocale(feature_id, input_locale);
  const std::optional<std::string> dlc_id =
      GetDlcIdForLanguagePack(feature_id, locale);

  // If the given Language Pack doesn't exist, run callback and don't reach the
  // DLC Service.
  if (!dlc_id) {
    std::move(callback).Run(CreateInvalidDlcPackResult());
    return;
  }

  UninstallDlc(*dlc_id,
               base::BindOnce(&OnUninstallDlcComplete, std::move(callback),
                              feature_id, locale));
}

void LanguagePackManager::InstallBasePack(
    const std::string& feature_id,
    OnInstallBasePackCompleteCallback callback) {
  const std::optional<std::string> dlc_id = GetDlcIdForBasePack(feature_id);

  // If the given |feature_id| doesn't have a Base Pack, run callback and
  // don't reach the DLC Service.
  if (!dlc_id) {
    std::move(callback).Run(CreateInvalidDlcPackResult());
    return;
  }

  base::UmaHistogramEnumeration(
      "ChromeOS.LanguagePacks.InstallBasePack.FeatureId",
      GetFeatureIdValueForUma(feature_id));

  InstallDlc(*dlc_id, base::BindOnce(&OnInstallDlcComplete, std::move(callback),
                                     feature_id, ""));
}

void LanguagePackManager::UpdatePacksForOobe(
    const std::string& input_locale,
    OnUpdatePacksForOobeCallback callback) {
  if (!IsOobe()) {
    DLOG(ERROR) << "Language Packs: UpdatePackForOobe called while not in OOBE";
    return;
  }

  // For now, TTS is the only feature we want to install during OOBE.
  // In the future we'll have a function that returns the list of features to
  // install.
  const std::string locale = ResolveLocale(kTtsFeatureId, input_locale);
  const std::optional<std::string> dlc_id =
      GetDlcIdForLanguagePack(kTtsFeatureId, locale);

  if (dlc_id) {
    base::UmaHistogramBoolean("ChromeOS.LanguagePacks.Oobe.ValidLocale", true);
    InstallDlc(*dlc_id,
               base::BindOnce(&OnInstallDlcComplete, std::move(callback),
                              kTtsFeatureId, locale));
  } else {
    base::UmaHistogramBoolean("ChromeOS.LanguagePacks.Oobe.ValidLocale", false);
    DLOG(ERROR) << "Language Packs: UpdatePacksForOobe locale does not exist";
    std::move(callback).Run(CreateInvalidDlcPackResult());
  }
}

void LanguagePackManager::CheckAndUpdateDlcsForInputMethods(
    PrefService* pref_service) {
  // The list of input methods have changed. We need to get the list of current
  // DLCs installed on device, which is an asynchronous method.
  GetExistingDlcs(base::BindOnce(&OnGetExistingDlcs, pref_service));
}

void LanguagePackManager::ObservePrefs(PrefService* pref_service) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // This is the main gate for the functionality of observing Prefs.
  // If this flag is false, all of the cascading logic is disabled.
  if (base::FeatureList::IsEnabled(features::kLanguagePacksInSettings)) {
    pref_change_registrar_.Init(pref_service);
    base::RepeatingClosure callback = base::BindRepeating(
        &LanguagePackManager::CheckAndUpdateDlcsForInputMethods, pref_service);
    pref_change_registrar_.Add(ash::prefs::kLanguagePreloadEngines, callback);
  }
}

void LanguagePackManager::AddObserver(Observer* const observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.AddObserver(observer);
}

void LanguagePackManager::RemoveObserver(Observer* const observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.RemoveObserver(observer);
}

void LanguagePackManager::NotifyPackStateChanged(
    std::string_view feature_id,
    std::string_view locale,
    const dlcservice::DlcState& dlc_state) {
  PackResult result = ConvertDlcStateToPackResult(dlc_state);
  result.feature_id = feature_id;
  result.language_code = locale;
  for (Observer& observer : observers_) {
    observer.OnPackStateChanged(result);
  }
}

void LanguagePackManager::OnDlcStateChanged(
    const dlcservice::DlcState& dlc_state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const std::optional<std::string> handwriting_locale =
      DlcToHandwritingLocale(dlc_state.id());
  if (handwriting_locale.has_value()) {
    NotifyPackStateChanged(kHandwritingFeatureId, *handwriting_locale,
                           dlc_state);
  }

  const std::optional<std::string> tts_locale = DlcToTtsLocale(dlc_state.id());
  if (tts_locale.has_value()) {
    NotifyPackStateChanged(kTtsFeatureId, *tts_locale, dlc_state);
  }
}

LanguagePackManager::LanguagePackManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(!g_instance);
  g_instance = this;
  DlcserviceClient* client = DlcserviceClient::Get();
  if (client) {
    obs_.Observe(client);
  } else {
    CHECK_IS_TEST();
    // No observation.
  }
}

LanguagePackManager::~LanguagePackManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_EQ(g_instance, this);
  pref_change_registrar_.RemoveAll();
  g_instance = nullptr;
}

void LanguagePackManager::Initialise() {
  // Heap-allocates an instance, which is then set in `g_instance` in the
  // constructor.
  // This instance will be cleaned up in `Shutdown()`.
  // Calling this while `g_instance` is set will result in a `CHECK` failure
  // instead of a memory leak.
  new LanguagePackManager();
}

void LanguagePackManager::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
}

// static
LanguagePackManager* LanguagePackManager::GetInstance() {
  return g_instance;
}

}  // namespace ash::language_packs