blob: 8bd7bc0a02cc18a369409bdc330310cb6feae024 [file] [log] [blame]
// Copyright 2025 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/component_updater/wasm_tts_engine_component_installer.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "chrome/browser/ui/webui/side_panel/read_anything/read_anything_prefs.h"
#include "components/prefs/pref_registry_simple.h"
#include "content/public/browser/browser_thread.h"
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#include "chrome/browser/accessibility/embedded_a11y_extension_loader.h"
#include "chrome/common/extensions/extension_constants.h"
#include "ui/accessibility/accessibility_features.h"
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
namespace {
const base::FilePath::CharType kManifestFileName[] =
FILE_PATH_LITERAL("wasm_tts_manifest.json");
const base::FilePath::CharType kBindingsMainWasmFileName[] =
FILE_PATH_LITERAL("bindings_main.wasm");
const base::FilePath::CharType kBindingsMainJsFileName[] =
FILE_PATH_LITERAL("bindings_main.js");
const base::FilePath::CharType kTTSEngineJsBinFileName[] =
FILE_PATH_LITERAL("googletts_engine_js_bin.js");
const base::FilePath::CharType kWorkletProcessorJsFileName[] =
FILE_PATH_LITERAL("streaming_worklet_processor.js");
const base::FilePath::CharType kVoicesJsonFileName[] =
FILE_PATH_LITERAL("voices.json");
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
const base::FilePath::CharType kManifestV3FileName[] =
FILE_PATH_LITERAL("wasm_tts_manifest_v3.json");
const base::FilePath::CharType kOffscreenHtmlFileName[] =
FILE_PATH_LITERAL("offscreen.html");
const base::FilePath::CharType kOffscreenCompiledFileName[] =
FILE_PATH_LITERAL("offscreen_compiled.js");
const base::FilePath::CharType kBackgroundCompiledFileName[] =
FILE_PATH_LITERAL("background_compiled.js");
#endif
// The SHA256 of the SubjectPublicKeyInfo used to sign the extension.
// The extension id is: bjbcblmdcnggnibecjikpoljcgkbgphl
constexpr std::array<uint8_t, 32> kWasmTtsEnginePublicKeySHA256 = {
0x19, 0x12, 0x1b, 0xc3, 0x2d, 0x66, 0xd8, 0x14, 0x29, 0x8a, 0xfe,
0xb9, 0x26, 0xa1, 0x6f, 0x7b, 0xc2, 0x14, 0x17, 0xf1, 0xb0, 0x1e,
0x56, 0x89, 0xcb, 0x53, 0x8e, 0x13, 0x92, 0xc1, 0x44, 0x5d};
const char kWasmTtsEngineManifestName[] = "WASM TTS Engine";
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
class WasmTTSEngineDirectory {
public:
static WasmTTSEngineDirectory* Get() {
static base::NoDestructor<WasmTTSEngineDirectory> wasm_directory;
return wasm_directory.get();
}
void Get(base::OnceCallback<void(const base::FilePath&)> callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
callbacks_.push_back(std::move(callback));
if (!dir_.empty()) {
FireCallbacks();
}
}
void Set(const base::FilePath& new_dir) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(!new_dir.empty());
dir_ = new_dir;
FireCallbacks();
}
bool IsSet() const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return !dir_.empty();
}
private:
void FireCallbacks() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(!dir_.empty());
std::vector<base::OnceCallback<void(const base::FilePath&)>> callbacks;
std::swap(callbacks, callbacks_);
for (base::OnceCallback<void(const base::FilePath&)>& callback :
callbacks) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), dir_));
}
}
base::FilePath dir_;
std::vector<base::OnceCallback<void(const base::FilePath&)>> callbacks_;
};
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
} // namespace
namespace component_updater {
WasmTtsEngineComponentInstallerPolicy::WasmTtsEngineComponentInstallerPolicy(
PrefService* pref_service)
: pref_service_(pref_service) {}
// static
void WasmTtsEngineComponentInstallerPolicy::RegisterPrefs(
PrefRegistrySimple* registry) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
registry->RegisterTimePref(prefs::kAccessibilityReadAnythingDateLastOpened,
base::Time());
registry->RegisterBooleanPref(
prefs::kAccessibilityReadAnythingTTSEngineReinstalled, false);
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}
bool WasmTtsEngineComponentInstallerPolicy::
SupportsGroupPolicyEnabledComponentUpdates() const {
return true;
}
bool WasmTtsEngineComponentInstallerPolicy::RequiresNetworkEncryption() const {
return false;
}
update_client::CrxInstaller::Result
WasmTtsEngineComponentInstallerPolicy::OnCustomInstall(
const base::Value::Dict& /* manifest */,
const base::FilePath& /* install_dir */) {
return update_client::CrxInstaller::Result(0); // Nothing custom here.
}
void WasmTtsEngineComponentInstallerPolicy::OnCustomUninstall() {}
void WasmTtsEngineComponentInstallerPolicy::ComponentReady(
const base::Version& version,
const base::FilePath& install_dir,
base::Value::Dict /* manifest */) {
VLOG(1) << "Component ready, version " << version.GetString() << " in "
<< install_dir.value();
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
if (!features::IsWasmTtsEngineAutoInstallDisabled()) {
// Instead of installing the component extension as soon as it is ready,
// store the install directory, so that the install can be triggered
// via ReadAnythingService once the side panel has been opened. This
// prevents the extension from being installed unnecessarily for those
// who aren't using reading mode.
// An exception is made for reinstalls at THRESHOLD_RECENT and
// THRESHOLD_LONGER amounts of time since reading mode was last opened. At
// these points, the WASM TTS Engine is reinstalled in order to clean up
// any removed voices.
WasmTTSEngineDirectory* wasm_directory = WasmTTSEngineDirectory::Get();
wasm_directory->Set(install_dir);
MaybeReinstallTtsEngine(install_dir);
}
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}
// In order to uninstall unused voices when reading mode has been unused for an
// extended period of time, Chrome needs to reinstall the TTS engine. This will
// be removed the next time Chrome is restarted.
void WasmTtsEngineComponentInstallerPolicy::MaybeReinstallTtsEngine(
const base::FilePath& install_dir) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
const base::Time current_time = base::Time::Now();
const base::Time date_last_opened =
pref_service_->GetTime(prefs::kAccessibilityReadAnythingDateLastOpened);
const bool previously_reinstalled = pref_service_->GetBoolean(
prefs::kAccessibilityReadAnythingTTSEngineReinstalled);
// Reading mode hasn't been opened in the last 90 days, so don't reinstall
// the engine.
if (date_last_opened.is_null()) {
return;
}
// Reading mode was opened more than 14 days ago and less than 90 days ago
// but the engine was already installed after 14 days to remove unused voices.
// Reading mode doesn't need to re-install the engine to clean up voices
// until 90 days have passed.
if (previously_reinstalled &&
(current_time - date_last_opened) < kThresholdLonger) {
return;
}
// Reading mode was opened in the last 14 days, so don't reinstall the engine.
if ((current_time - date_last_opened) < kThresholdRecent) {
return;
}
const base::FilePath::CharType* manifest_file =
features::IsWasmTtsComponentUpdaterV3Enabled() ? kManifestV3FileName
: kManifestFileName;
// If it's been more than 14 days since reading mode was last opened,
// re-install the engine so that unused voices can be removed.
EmbeddedA11yExtensionLoader::GetInstance()->Init();
EmbeddedA11yExtensionLoader::GetInstance()->InstallExtensionWithIdAndPath(
extension_misc::kComponentUpdaterTTSEngineExtensionId, install_dir,
manifest_file,
/*should_localize=*/false);
// If reading mode hasn't been opened in longer than 14 days but less than
// 90 days, update that the TTS engine has been reinstalled to prevent
// subsequent reinstalls from day 14 to day 90.
if (!previously_reinstalled) {
pref_service_->SetBoolean(
prefs::kAccessibilityReadAnythingTTSEngineReinstalled, true);
}
// If it's been more than 90 days, clear both preferences for reading mode
// last opened state. The engine will now not be reinstalled until reading
// mode is reopened.
if ((current_time - date_last_opened) >= kThresholdLonger) {
pref_service_->ClearPref(prefs::kAccessibilityReadAnythingDateLastOpened);
pref_service_->ClearPref(
prefs::kAccessibilityReadAnythingTTSEngineReinstalled);
}
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}
// Called during startup and installation before ComponentReady().
bool WasmTtsEngineComponentInstallerPolicy::VerifyInstallation(
const base::Value::Dict& /* manifest */,
const base::FilePath& install_dir) const {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
if (features::IsWasmTtsComponentUpdaterV3Enabled()) {
return base::PathExists(install_dir.Append(kManifestV3FileName)) &&
base::PathExists(install_dir.Append(kBindingsMainWasmFileName)) &&
base::PathExists(install_dir.Append(kBindingsMainJsFileName)) &&
base::PathExists(install_dir.Append(kOffscreenHtmlFileName)) &&
base::PathExists(install_dir.Append(kOffscreenCompiledFileName)) &&
base::PathExists(install_dir.Append(kBackgroundCompiledFileName)) &&
base::PathExists(install_dir.Append(kWorkletProcessorJsFileName)) &&
base::PathExists(install_dir.Append(kVoicesJsonFileName));
}
#endif
return base::PathExists(install_dir.Append(kManifestFileName)) &&
base::PathExists(install_dir.Append(kBindingsMainWasmFileName)) &&
base::PathExists(install_dir.Append(kBindingsMainJsFileName)) &&
base::PathExists(install_dir.Append(kTTSEngineJsBinFileName)) &&
base::PathExists(install_dir.Append(kWorkletProcessorJsFileName)) &&
base::PathExists(install_dir.Append(kVoicesJsonFileName));
}
base::FilePath WasmTtsEngineComponentInstallerPolicy::GetRelativeInstallDir()
const {
return base::FilePath(FILE_PATH_LITERAL("WasmTtsEngine"));
}
void WasmTtsEngineComponentInstallerPolicy::GetHash(
std::vector<uint8_t>* hash) const {
hash->assign(std::begin(kWasmTtsEnginePublicKeySHA256),
std::end(kWasmTtsEnginePublicKeySHA256));
}
std::string WasmTtsEngineComponentInstallerPolicy::GetName() const {
return kWasmTtsEngineManifestName;
}
update_client::InstallerAttributes
WasmTtsEngineComponentInstallerPolicy::GetInstallerAttributes() const {
return update_client::InstallerAttributes();
}
void RegisterWasmTtsEngineComponent(ComponentUpdateService* cus,
PrefService* prefs) {
VLOG(1) << "Registering WASM TTS Engine component.";
auto installer = base::MakeRefCounted<ComponentInstaller>(
std::make_unique<WasmTtsEngineComponentInstallerPolicy>(prefs));
installer->Register(cus, base::OnceClosure());
}
void WasmTtsEngineComponentInstallerPolicy::GetWasmTTSEngineDirectory(
base::OnceCallback<void(const base::FilePath&)> callback) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
WasmTTSEngineDirectory* wasm_directory = WasmTTSEngineDirectory::Get();
wasm_directory->Get(std::move(callback));
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}
// static
bool WasmTtsEngineComponentInstallerPolicy::IsWasmTTSEngineDirectorySet() {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
return WasmTTSEngineDirectory::Get()->IsSet();
#else
return false;
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}
} // namespace component_updater