blob: 55106500c38d9fa350d49b82aef50155456dbebc [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/ui/read_anything/read_anything_service.h"
#include "base/check_is_test.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/accessibility/embedded_a11y_extension_loader.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/read_anything/read_anything_service_factory.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry_key.h"
#include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
#include "chrome/browser/ui/webui/side_panel/read_anything/read_anything_prefs.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/grit/browser_resources.h"
#include "extensions/browser/extension_system.h"
#include "ui/accessibility/accessibility_features.h"
#if !BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/component_updater/wasm_tts_engine_component_installer.h"
#include "chrome/browser/extensions/component_loader.h"
#endif // !BUILDFLAG(IS_CHROMEOS)
namespace {
// The number of seconds to wait before removing the extension. This avoids
// removing an extension only to add it back immediately.
constexpr int kRemoveExtensionDelaySeconds = 30;
} // namespace
#if !BUILDFLAG(IS_CHROMEOS)
const base::FilePath::CharType kManifestFileName[] =
FILE_PATH_LITERAL("wasm_tts_manifest.json");
const base::FilePath::CharType kManifestV3FileName[] =
FILE_PATH_LITERAL("wasm_tts_manifest_v3.json");
#endif // !BUILDFLAG(IS_CHROMEOS)
ReadAnythingService::ReadAnythingService(Profile* profile) : profile_(profile) {
if (features::IsReadAnythingDocsIntegrationEnabled()) {
EmbeddedA11yExtensionLoader::GetInstance()->Init();
// The extension may still be installed from a previous session. Queue the
// timer to uninstall it.
// TODO(https://crbug.com/362787711): This logic also needs to run if the
// feature is disabled.
local_side_panel_switch_delay_timer_.Start(
FROM_HERE, base::Seconds(kRemoveExtensionDelaySeconds),
base::BindRepeating(
&ReadAnythingService::OnLocalSidePanelSwitchDelayTimeout,
weak_ptr_factory_.GetWeakPtr()));
}
if (features::IsDataCollectionModeForScreen2xEnabled()) {
browser_list_observer_.Observe(BrowserList::GetInstance());
}
}
// The service is shutting down which means the profile is destroying, at which
// point we should not be re-entrantly trying to modify the profile by removing
// the extension. Instead remove the extension at startup.
ReadAnythingService::~ReadAnythingService() = default;
// static
ReadAnythingService* ReadAnythingService::Get(Profile* profile) {
return ReadAnythingServiceFactory::GetInstance()->GetForBrowserContext(
profile);
}
void ReadAnythingService::OnReadAnythingSidePanelEntryShown() {
// The TTS download extension should only be installed on non-ChromeOS devices
// when the Read Aloud flag is enabled.
#if !BUILDFLAG(IS_CHROMEOS)
SetupDesktopEngine();
#endif // !BUILDFLAG(IS_CHROMEOS)
if (!features::IsReadAnythingDocsIntegrationEnabled()) {
return;
}
active_local_side_panel_count_++;
InstallGDocsHelperExtension();
}
#if !BUILDFLAG(IS_CHROMEOS)
void ReadAnythingService::SetupDesktopEngine() {
// If the extension was previously installed but now the Read Aloud flag
// is disabled, or if the component updater flag is enabled, we should
// uninstall the component extension.
// TODO(crbug.com/428043296): RemoveTtsDownloadExtension should be left in
// until the IsWasmTtsComponentUpdaterEnabled flag has been removed for
// enough time to be sure that no one has that extension installed. If they
// do, it could cause issues when the component updater extension is
// installed.
RemoveTtsDownloadExtension();
// Install the TTS extension via the component updater if the
// component updater flag is enabled.
if (features::IsReadAnythingReadAloudEnabled() &&
!features::IsWasmTtsEngineAutoInstallDisabled()) {
// Signal that the reading mode panel is opened and it's now safe to
// install the WasmTtsEngineComponent.
component_updater::WasmTtsEngineComponentInstallerPolicy::
GetWasmTTSEngineDirectory(base::BindOnce(InstallComponent));
}
}
#endif // !BUILDFLAG(IS_CHROMEOS)
void ReadAnythingService::OnReadAnythingSidePanelEntryHidden() {
if (!features::IsReadAnythingDocsIntegrationEnabled()) {
return;
}
active_local_side_panel_count_--;
local_side_panel_switch_delay_timer_.Reset();
}
void ReadAnythingService::InstallGDocsHelperExtension() {
#if BUILDFLAG(IS_CHROMEOS)
EmbeddedA11yExtensionLoader::GetInstance()->InstallExtensionWithId(
extension_misc::kReadingModeGDocsHelperExtensionId,
extension_misc::kReadingModeGDocsHelperExtensionPath,
extension_misc::kReadingModeGDocsHelperManifestFilename,
/*should_localize=*/false);
#else
auto* component_loader = extensions::ComponentLoader::Get(profile_);
if (!component_loader) {
// In tests, the loader might not be created.
CHECK_IS_TEST();
return;
}
if (!component_loader->Exists(
extension_misc::kReadingModeGDocsHelperExtensionId)) {
component_loader->Add(
IDR_READING_MODE_GDOCS_HELPER_MANIFEST,
base::FilePath(FILE_PATH_LITERAL("reading_mode_gdocs_helper")));
}
#endif // BUILDFLAG(IS_CHROMEOS)
}
void ReadAnythingService::RemoveGDocsHelperExtension() {
#if BUILDFLAG(IS_CHROMEOS)
EmbeddedA11yExtensionLoader::GetInstance()->RemoveExtensionWithId(
extension_misc::kReadingModeGDocsHelperExtensionId);
#else
auto* component_loader = extensions::ComponentLoader::Get(profile_);
if (!component_loader) {
// In tests, the loader might not be created.
CHECK_IS_TEST();
return;
}
component_loader->Remove(extension_misc::kReadingModeGDocsHelperExtensionId);
#endif // BUILDFLAG(IS_CHROMEOS)
}
void ReadAnythingService::OnLocalSidePanelSwitchDelayTimeout() {
if (active_local_side_panel_count_ > 0) {
return;
}
RemoveGDocsHelperExtension();
}
void ReadAnythingService::OnBrowserSetLastActive(Browser* browser) {
if (!features::IsDataCollectionModeForScreen2xEnabled() ||
browser->profile() != profile_) {
return;
}
// This code is called as part of a screen2x data generation workflow, where
// the browser is opened by a CLI and the read-anything side panel is
// automatically opened. Therefore we force the UI to show right away, as in
// tests.
// TODO(https://crbug.com/358191922): Remove this code.
auto* side_panel_ui = browser->GetFeatures().side_panel_ui();
if (!side_panel_ui->IsSidePanelEntryShowing(
SidePanelEntryKey(SidePanelEntryId::kReadAnything))) {
side_panel_ui->SetNoDelaysForTesting(true); // IN-TEST
side_panel_ui->Show(SidePanelEntryId::kReadAnything);
}
}
void ReadAnythingService::RemoveTtsDownloadExtension() {
#if !BUILDFLAG(IS_CHROMEOS)
// Remove the legacy TTS extension for all profiles.
// This code for removing the extension installed in the legacy way
// should remain in place until at least milestone 141 to ensure there
// are no conflicts with installing the component loader extension.
EmbeddedA11yExtensionLoader::GetInstance()->Init();
EmbeddedA11yExtensionLoader::GetInstance()->RemoveExtensionWithId(
extension_misc::kTTSEngineExtensionId);
#endif // !BUILDFLAG(IS_CHROMEOS)
}
#if !BUILDFLAG(IS_CHROMEOS)
void ReadAnythingService::InstallComponent(const base::FilePath& new_dir) {
const base::FilePath::CharType* manifest;
if (features::IsWasmTtsComponentUpdaterV3Enabled()) {
VLOG(1) << "Installing TTS component using V3 engine";
manifest = kManifestV3FileName;
} else {
VLOG(1) << "Installing TTS component using V2 engine";
manifest = kManifestFileName;
}
RecordEngineVersion(new_dir.BaseName());
EmbeddedA11yExtensionLoader::GetInstance()->Init();
EmbeddedA11yExtensionLoader::GetInstance()->InstallExtensionWithIdAndPath(
extension_misc::kComponentUpdaterTTSEngineExtensionId, new_dir, manifest,
/*should_localize=*/false);
// Store the last time reading mode was opened and the TTS engine was
// installed to be used to uninstall voices if reading mode is unopened for a
// long time.
g_browser_process->local_state()->SetTime(
prefs::kAccessibilityReadAnythingDateLastOpened, base::Time::Now());
g_browser_process->local_state()->SetBoolean(
prefs::kAccessibilityReadAnythingTTSEngineReinstalled, false);
}
void ReadAnythingService::RecordEngineVersion(
const base::FilePath& engine_version) {
// Per FilePath documentation, Windows uses std::wstring, so string
// so string manipulations must be handled slightly differently.
#if BUILDFLAG(IS_WIN)
using path_string_t = std::wstring;
constexpr auto delimiter = L'.';
#else
using path_string_t = std::string;
constexpr auto delimiter = '.';
#endif
path_string_t file = engine_version.value();
int version_number = 0;
size_t pos = file.find(delimiter);
if (pos != std::string::npos) {
file.erase(pos, 1);
}
// In order for the engine to be recognized by component updater, it must be
// of the format YYYYMMDD.x. Convert the string representation of the engine
// version to an integer of format YYYMMDDx in order to be logged.
if (base::StringToInt(file, &version_number)) {
base::UmaHistogramSparse(
"Accessibility.ReadAnything.ReadAloud.EngineVersion", version_number);
}
}
#endif // !BUILDFLAG(IS_CHROMEOS)