blob: c4a06d5ebcbf49063f2ea36c96cab20d2cccc62c [file] [log] [blame]
// Copyright 2024 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/on_device_translation/service_controller.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/component_updater/translate_kit_language_pack_component_installer.h"
#include "chrome/browser/on_device_translation/constants.h"
#include "chrome/browser/on_device_translation/language_pack_util.h"
#include "chrome/browser/on_device_translation/pref_names.h"
#include "components/component_updater/component_updater_paths.h"
#include "components/prefs/pref_service.h"
#include "components/services/on_device_translation/public/cpp/features.h"
#include "components/services/on_device_translation/public/mojom/on_device_translation_service.mojom.h"
#include "components/services/on_device_translation/public/mojom/translator.mojom.h"
#include "content/public/browser/service_process_host.h"
#if BUILDFLAG(IS_WIN)
#include "base/strings/utf_string_conversions.h"
#endif // BUILDFLAG(IS_WIN)
using on_device_translation::kLanguagePackComponentConfigMap;
using on_device_translation::LanguagePackKey;
using on_device_translation::ToLanguageCode;
using on_device_translation::mojom::OnDeviceTranslationLanguagePackage;
using on_device_translation::mojom::OnDeviceTranslationLanguagePackageFile;
using on_device_translation::mojom::OnDeviceTranslationLanguagePackagePtr;
using on_device_translation::mojom::OnDeviceTranslationServiceConfig;
using on_device_translation::mojom::OnDeviceTranslationServiceConfigPtr;
namespace {
const char kTranslateKitPackagePaths[] = "translate-kit-packages";
const char kOnDeviceTranslationServiceDisplayName[] =
"On-device Translation Service";
base::FilePath GetFilePathFromGlobalPrefs(std::string_view pref_name) {
PrefService* global_prefs = g_browser_process->local_state();
CHECK(global_prefs);
base::FilePath path_in_pref = global_prefs->GetFilePath(pref_name);
return path_in_pref;
}
base::FilePath GetTranslateKitLibraryPath() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(on_device_translation::kTranslateKitBinaryPath)) {
return command_line->GetSwitchValuePath(
on_device_translation::kTranslateKitBinaryPath);
}
if (base::FeatureList::IsEnabled(
on_device_translation::kEnableTranslateKitComponent)) {
return GetFilePathFromGlobalPrefs(prefs::kTranslateKitBinaryPath);
}
return base::FilePath();
}
std::string ToString(base::FilePath path) {
#if BUILDFLAG(IS_WIN)
// TODO(crbug.com/362123222): Get rid of conditional decoding.
return path.AsUTF8Unsafe();
#else
return path.value();
#endif // BUILDFLAG(IS_WIN)
}
// Returns the language packs that are installed.
std::set<LanguagePackKey> GetInstalledLanguagePacks() {
std::set<LanguagePackKey> insalled_pack_keys;
for (const auto& it : kLanguagePackComponentConfigMap) {
if (!GetFilePathFromGlobalPrefs(it.second->config_path_pref).empty()) {
insalled_pack_keys.insert(it.first);
}
}
return insalled_pack_keys;
}
} // namespace
// static
base::FilePath
OnDeviceTranslationServiceController::GetTranslateKitComponentPath() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(on_device_translation::kTranslateKitBinaryPath)) {
return command_line->GetSwitchValuePath(
on_device_translation::kTranslateKitBinaryPath);
}
if (base::FeatureList::IsEnabled(
on_device_translation::kEnableTranslateKitComponent)) {
base::FilePath components_dir;
base::PathService::Get(component_updater::DIR_COMPONENT_USER,
&components_dir);
return components_dir.empty()
? base::FilePath()
: components_dir.Append(
on_device_translation::
kTranslateKitBinaryInstallationRelativeDir);
}
return base::FilePath();
}
std::optional<
std::vector<OnDeviceTranslationServiceController::LanguagePackInfo>>
OnDeviceTranslationServiceController::GetLanguagePackInfoFromCommandLine() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (!command_line->HasSwitch(kTranslateKitPackagePaths)) {
return std::nullopt;
}
const auto packages_string =
command_line->GetSwitchValueNative(kTranslateKitPackagePaths);
std::vector<base::CommandLine::StringType> splitted_strings =
base::SplitString(packages_string,
#if BUILDFLAG(IS_WIN)
L",",
#else // !BUILDFLAG(IS_WIN)
",",
#endif // BUILDFLAG(IS_WIN)
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (splitted_strings.size() % 3 != 0) {
LOG(ERROR) << "Invalid --translate-kit-packages flag";
return std::nullopt;
}
std::vector<OnDeviceTranslationServiceController::LanguagePackInfo> packages;
auto it = splitted_strings.begin();
while (it != splitted_strings.end()) {
if (!base::IsStringASCII(*it) || !base::IsStringASCII(*(it + 1))) {
LOG(ERROR) << "Invalid --translate-kit-packages flag";
return std::nullopt;
}
OnDeviceTranslationServiceController::LanguagePackInfo package;
#if BUILDFLAG(IS_WIN)
package.language1 = base::WideToUTF8(*(it++));
package.language2 = base::WideToUTF8(*(it++));
#else // !BUILDFLAG(IS_WIN)
package.language1 = *(it++);
package.language2 = *(it++);
#endif
package.package_path = base::FilePath(*(it++));
packages.push_back(std::move(package));
}
return packages;
}
OnDeviceTranslationServiceController::OnDeviceTranslationServiceController()
: language_packs_from_command_line_(GetLanguagePackInfoFromCommandLine()) {
// Initialize the pref change registrar.
pref_change_registrar_.Init(g_browser_process->local_state());
// Start listening to pref changes for language pack keys.
for (const auto& it : kLanguagePackComponentConfigMap) {
pref_change_registrar_.Add(
it.second->config_path_pref,
base::BindRepeating(
&OnDeviceTranslationServiceController::OnLanguagePackKeyPrefChanged,
base::Unretained(this)));
}
// Register all the installed language pack components.
RegisterInstalledLanguagePackComponent();
auto receiver = service_remote_.BindNewPipeAndPassReceiver();
service_remote_.reset_on_disconnect();
const std::string binary_path = ToString(GetTranslateKitLibraryPath());
if (binary_path.empty()) {
LOG(ERROR) << "Got an empty path to TranslateKit binary on the device.";
}
std::vector<std::string> extra_switches;
extra_switches.push_back(base::StrCat(
{on_device_translation::kTranslateKitBinaryPath, "=", binary_path}));
content::ServiceProcessHost::Launch<
on_device_translation::mojom::OnDeviceTranslationService>(
std::move(receiver),
content::ServiceProcessHost::Options()
.WithDisplayName(kOnDeviceTranslationServiceDisplayName)
.WithExtraCommandLineSwitches(extra_switches)
.Pass());
StartOpeningLanguagePackFiles();
}
OnDeviceTranslationServiceController::~OnDeviceTranslationServiceController() =
default;
void OnDeviceTranslationServiceController::CreateTranslator(
const std::string& source_lang,
const std::string& target_lang,
mojo::PendingReceiver<on_device_translation::mojom::Translator> receiver,
base::OnceCallback<void(bool)> callback) {
MaybeTriggerLanguagePackInstall(source_lang, target_lang);
// TODO(crbug.com/358030919): Implement a logic to defer the CreateTranslator
// IPC call when a new language pack was installed.
if (!initial_config_passed_) {
// Queue the task to `pending_tasks_` until the initial configuration is
// sent to the service.
pending_tasks_.emplace_back(
base::BindOnce(&OnDeviceTranslationServiceController::CreateTranslator,
base::Unretained(this), source_lang, target_lang,
std::move(receiver), std::move(callback)));
return;
}
service_remote_->CreateTranslator(source_lang, target_lang,
std::move(receiver), std::move(callback));
}
void OnDeviceTranslationServiceController::CanTranslate(
const std::string& source_lang,
const std::string& target_lang,
base::OnceCallback<void(bool)> callback) {
MaybeTriggerLanguagePackInstall(source_lang, target_lang);
// TODO(crbug.com/358030919): Implement a logic to defer the CanTranslate
// IPC call when a new language pack was installed.
if (!initial_config_passed_) {
// Queue the task to `pending_tasks_` until the initial configuration is
// sent to the service.
pending_tasks_.emplace_back(base::BindOnce(
&OnDeviceTranslationServiceController::CanTranslate,
base::Unretained(this), source_lang, target_lang, std::move(callback)));
return;
}
service_remote_->CanTranslate(source_lang, target_lang, std::move(callback));
}
// Returns the language packs that are installed or set by the command line.
std::vector<OnDeviceTranslationServiceController::LanguagePackInfo>
OnDeviceTranslationServiceController::GetLanguagePackInfo() {
if (language_packs_from_command_line_) {
return *language_packs_from_command_line_;
}
std::vector<OnDeviceTranslationServiceController::LanguagePackInfo> packages;
for (const auto& it : kLanguagePackComponentConfigMap) {
auto file_path = GetFilePathFromGlobalPrefs(it.second->config_path_pref);
if (!file_path.empty()) {
OnDeviceTranslationServiceController::LanguagePackInfo package;
package.language1 = ToLanguageCode(it.second->language1);
package.language2 = ToLanguageCode(it.second->language2);
package.package_path = file_path;
packages.push_back(std::move(package));
}
}
return packages;
}
// Register the installed language pack components.
void OnDeviceTranslationServiceController::
RegisterInstalledLanguagePackComponent() {
for (const auto& language_pack : GetInstalledLanguagePacks()) {
RegisterLanguagePackComponent(language_pack);
}
}
// Maybe trigger the language pack install if the required language packs are
// not installed.
void OnDeviceTranslationServiceController::MaybeTriggerLanguagePackInstall(
const std::string& source_lang,
const std::string& target_lang) {
const auto required_packs =
on_device_translation::CalculateRequiredLanguagePacks(source_lang,
target_lang);
if (required_packs.empty()) {
return;
}
const auto installed_packs = GetInstalledLanguagePacks();
std::vector<LanguagePackKey> differences;
base::ranges::set_difference(required_packs, installed_packs,
std::back_inserter(differences));
if (differences.empty()) {
return;
}
std::vector<LanguagePackKey> to_be_installed;
base::ranges::set_difference(differences, registered_language_packs_,
std::back_inserter(to_be_installed));
for (const auto& language_pack : to_be_installed) {
RegisterLanguagePackComponent(language_pack);
}
}
// Register the language pack component.
void OnDeviceTranslationServiceController::RegisterLanguagePackComponent(
LanguagePackKey language_pack) {
CHECK(!registered_language_packs_.contains(language_pack));
registered_language_packs_.insert(language_pack);
component_updater::RegisterTranslateKitLanguagePackComponent(
g_browser_process->component_updater(), g_browser_process->local_state(),
language_pack, base::BindOnce([]() {
// TODO(crbug.com/358030919): Consider calling
// OnDemandUpdater::OnDemandUpdate() to trigger an update check.
}));
}
// Called when the language pack key pref is changed.
void OnDeviceTranslationServiceController::OnLanguagePackKeyPrefChanged(
const std::string& pref_name) {
StartOpeningLanguagePackFiles();
}
// Start opening the language pack files.
void OnDeviceTranslationServiceController::StartOpeningLanguagePackFiles() {
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
task_runner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&OnDeviceTranslationServiceController::
OpenLanguagePackFilesOnBackgrond,
GetLanguagePackInfo()),
base::BindOnce(
&OnDeviceTranslationServiceController::OnLauguagePackagesOpened,
base::Unretained(this)));
}
// static
// Opens the language pack files on the background thread.
OnDeviceTranslationServiceConfigPtr
OnDeviceTranslationServiceController::OpenLanguagePackFilesOnBackgrond(
std::vector<LanguagePackInfo> packages) {
auto config = OnDeviceTranslationServiceConfig::New();
for (const auto& package : packages) {
auto mojo_package = OnDeviceTranslationLanguagePackage::New();
mojo_package->language1 = package.language1;
mojo_package->language2 = package.language2;
// Retrieves all the files in the sub-directories. The language package
// files are stored in the sub-directories of the package path.
base::FileEnumerator(package.package_path,
/*recursive=*/false, base::FileEnumerator::DIRECTORIES)
.ForEach([&](const base::FilePath& directory_path) {
// Ignore the directories that are not ASCII.
if (!base::IsStringASCII(directory_path.BaseName().value())) {
return;
}
base::FileEnumerator(directory_path,
/*recursive=*/false, base::FileEnumerator::FILES)
.ForEach([&](const base::FilePath& file_path) {
// Ignore the files that are not ASCII.
if (!base::IsStringASCII(file_path.BaseName().value())) {
return;
}
auto file = base::File(
file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (file.IsValid()) {
// Calling AsUTF8Unsafe() is safe here because we have already
// checked the file name is ASCII.
// We intentionally use '/' for the directory separator even
// on Windows, because TranslateKit uses '/' as the directory
// separator.
const std::string file_path_str =
base::StrCat({directory_path.BaseName().AsUTF8Unsafe(),
"/", file_path.BaseName().AsUTF8Unsafe()});
mojo_package->files.push_back(
OnDeviceTranslationLanguagePackageFile::New(
base::FilePath::FromASCII(file_path_str),
std::move(file)));
} else {
LOG(ERROR) << "Invalid " << file_path;
}
});
});
config->packages.push_back(std::move(mojo_package));
}
return config;
}
// Called when the language packages are opened.
void OnDeviceTranslationServiceController::OnLauguagePackagesOpened(
OnDeviceTranslationServiceConfigPtr config) {
// Note: we call SetServiceConfig() even when `initial_config_passed_` is set.
// This is intended to notify the new language pack component update.
service_remote_->SetServiceConfig(std::move(config));
if (initial_config_passed_) {
return;
}
// Runs queued tasks after sending the config to the service.
initial_config_passed_ = true;
auto tasks = std::move(pending_tasks_);
for (auto& task : tasks) {
std::move(task).Run();
}
}
// static
OnDeviceTranslationServiceController*
OnDeviceTranslationServiceController::GetInstance() {
static base::NoDestructor<OnDeviceTranslationServiceController> instance;
return instance.get();
}