// Copyright 2023 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/lacros/embedded_a11y_manager_lacros.h"
#include <memory>
#include <optional>
#include "base/memory/singleton.h"
#include "base/path_service.h"
#include "chrome/browser/accessibility/embedded_a11y_extension_loader.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/api/accessibility_service_private.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/embedded_accessibility_helper.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/file_util.h"
#include "ui/gfx/animation/animation.h"
// static
EmbeddedA11yManagerLacros* EmbeddedA11yManagerLacros::GetInstance() {
return base::Singleton<
EmbeddedA11yManagerLacros,
base::LeakySingletonTraits<EmbeddedA11yManagerLacros>>::get();
}
EmbeddedA11yManagerLacros::EmbeddedA11yManagerLacros() = default;
EmbeddedA11yManagerLacros::~EmbeddedA11yManagerLacros() = default;
void EmbeddedA11yManagerLacros::ClipboardCopyInActiveGoogleDoc(
const std::string& url) {
// Get the `Profile` last used (the `Profile` which owns the most
// recently focused window). This is the one on which we want to
// request speech.
Profile* profile = ProfileManager::GetLastUsedProfile();
extensions::EventRouter* event_router = extensions::EventRouter::Get(profile);
auto event_args(extensions::api::accessibility_service_private::
ClipboardCopyInActiveGoogleDoc::Create(url));
std::unique_ptr<extensions::Event> event(new extensions::Event(
extensions::events::
ACCESSIBILITY_SERVICE_PRIVATE_CLIPBOARD_COPY_IN_ACTIVE_GOOGLE_DOC,
extensions::api::accessibility_service_private::
ClipboardCopyInActiveGoogleDoc::kEventName,
std::move(event_args)));
event_router->DispatchEventWithLazyListener(
extension_misc::kEmbeddedA11yHelperExtensionId, std::move(event));
}
void EmbeddedA11yManagerLacros::Init() {
CHECK(!chromevox_enabled_observer_)
<< "EmbeddedA11yManagerLacros::Init should only be called once.";
// Initial values are obtained when the observers are created, there is no
// need to do so explicitly.
chromevox_enabled_observer_ = std::make_unique<CrosapiPrefObserver>(
crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled,
base::BindRepeating(&EmbeddedA11yManagerLacros::OnChromeVoxEnabledChanged,
weak_ptr_factory_.GetWeakPtr()));
select_to_speak_enabled_observer_ = std::make_unique<CrosapiPrefObserver>(
crosapi::mojom::PrefPath::kAccessibilitySelectToSpeakEnabled,
base::BindRepeating(
&EmbeddedA11yManagerLacros::OnSelectToSpeakEnabledChanged,
weak_ptr_factory_.GetWeakPtr()));
switch_access_enabled_observer_ = std::make_unique<CrosapiPrefObserver>(
crosapi::mojom::PrefPath::kAccessibilitySwitchAccessEnabled,
base::BindRepeating(
&EmbeddedA11yManagerLacros::OnSwitchAccessEnabledChanged,
weak_ptr_factory_.GetWeakPtr()));
chromeos::LacrosService* impl = chromeos::LacrosService::Get();
if (impl->IsAvailable<
crosapi::mojom::EmbeddedAccessibilityHelperClientFactory>()) {
auto& remote = impl->GetRemote<
crosapi::mojom::EmbeddedAccessibilityHelperClientFactory>();
remote->BindEmbeddedAccessibilityHelperClient(
a11y_helper_remote_.BindNewPipeAndPassReceiver());
remote->BindEmbeddedAccessibilityHelper(
a11y_helper_receiver_.BindNewPipeAndPassRemote());
}
if (impl->GetInterfaceVersion<
crosapi::mojom::EmbeddedAccessibilityHelperClient>() >=
static_cast<int>(crosapi::mojom::EmbeddedAccessibilityHelperClient::
kFocusChangedMinVersion)) {
// Only observe focus highlight pref if the Ash version is able to support
// focus highlight enabled changed. Otherwise this just adds overhead.
focus_highlight_enabled_observer_ = std::make_unique<CrosapiPrefObserver>(
crosapi::mojom::PrefPath::kAccessibilityFocusHighlightEnabled,
base::BindRepeating(
&EmbeddedA11yManagerLacros::OnFocusHighlightEnabledChanged,
weak_ptr_factory_.GetWeakPtr()));
}
reduced_animations_enabled_observer_ = std::make_unique<CrosapiPrefObserver>(
crosapi::mojom::PrefPath::kAccessibilityReducedAnimationsEnabled,
base::BindRepeating(
&EmbeddedA11yManagerLacros::OnReducedAnimationsEnabledChanged,
weak_ptr_factory_.GetWeakPtr()));
overscroll_history_navigation_enabled_observer_ =
std::make_unique<CrosapiPrefObserver>(
crosapi::mojom::PrefPath::kOverscrollHistoryNavigationEnabled,
base::BindRepeating(&EmbeddedA11yManagerLacros::
OnOverscrollHistoryNavigationEnabledChanged,
weak_ptr_factory_.GetWeakPtr()));
EmbeddedA11yExtensionLoader::GetInstance()->Init();
ProfileManager* profile_manager = g_browser_process->profile_manager();
profile_manager_observation_.Observe(profile_manager);
// Observe all existing profiles.
std::vector<Profile*> profiles =
g_browser_process->profile_manager()->GetLoadedProfiles();
for (auto* profile : profiles) {
observed_profiles_.AddObservation(profile);
}
UpdateEmbeddedA11yHelperExtension();
UpdateChromeVoxHelperExtension();
}
void EmbeddedA11yManagerLacros::SpeakSelectedText() {
// Check the remote is bound. It might not be bound on older versions
// of Ash.
if (a11y_helper_remote_.is_bound()) {
a11y_helper_remote_->SpeakSelectedText();
}
if (speak_selected_text_callback_for_test_) {
speak_selected_text_callback_for_test_.Run();
}
}
bool EmbeddedA11yManagerLacros::IsSelectToSpeakEnabled() {
return select_to_speak_enabled_;
}
void EmbeddedA11yManagerLacros::AddSpeakSelectedTextCallbackForTest(
base::RepeatingClosure callback) {
speak_selected_text_callback_for_test_ = std::move(callback);
}
void EmbeddedA11yManagerLacros::AddFocusChangedCallbackForTest(
base::RepeatingCallback<void(gfx::Rect)> callback) {
focus_changed_callback_for_test_ = std::move(callback);
}
void EmbeddedA11yManagerLacros::OnProfileWillBeDestroyed(Profile* profile) {
observed_profiles_.RemoveObservation(profile);
}
void EmbeddedA11yManagerLacros::OnOffTheRecordProfileCreated(
Profile* off_the_record) {
observed_profiles_.AddObservation(off_the_record);
}
void EmbeddedA11yManagerLacros::OnProfileAdded(Profile* profile) {
observed_profiles_.AddObservation(profile);
}
void EmbeddedA11yManagerLacros::OnProfileManagerDestroying() {
profile_manager_observation_.Reset();
}
void EmbeddedA11yManagerLacros::UpdateOverscrollHistoryNavigationEnabled() {
if (overscroll_history_navigation_enabled_.has_value()) {
g_browser_process->local_state()->SetBoolean(
prefs::kOverscrollHistoryNavigationEnabled,
overscroll_history_navigation_enabled_.value());
}
}
void EmbeddedA11yManagerLacros::OnChromeVoxEnabledChanged(base::Value value) {
CHECK(value.is_bool());
chromevox_enabled_ = value.GetBool();
UpdateChromeVoxHelperExtension();
}
void EmbeddedA11yManagerLacros::OnSelectToSpeakEnabledChanged(
base::Value value) {
CHECK(value.is_bool());
select_to_speak_enabled_ = value.GetBool();
UpdateEmbeddedA11yHelperExtension();
}
void EmbeddedA11yManagerLacros::OnSwitchAccessEnabledChanged(
base::Value value) {
CHECK(value.is_bool());
switch_access_enabled_ = value.GetBool();
UpdateEmbeddedA11yHelperExtension();
}
void EmbeddedA11yManagerLacros::OnFocusHighlightEnabledChanged(
base::Value value) {
CHECK(value.is_bool());
if (value.GetBool()) {
focus_changed_subscription_ =
content::BrowserAccessibilityState::GetInstance()
->RegisterFocusChangedCallback(base::BindRepeating(
&EmbeddedA11yManagerLacros::OnFocusChangedInPage,
weak_ptr_factory_.GetWeakPtr()));
} else {
focus_changed_subscription_ = {};
}
}
void EmbeddedA11yManagerLacros::OnReducedAnimationsEnabledChanged(
base::Value value) {
CHECK(value.is_bool());
gfx::Animation::SetPrefersReducedMotionForA11y(value.GetBool());
}
void EmbeddedA11yManagerLacros::OnOverscrollHistoryNavigationEnabledChanged(
base::Value value) {
CHECK(value.is_bool());
overscroll_history_navigation_enabled_ = value.GetBool();
UpdateOverscrollHistoryNavigationEnabled();
}
void EmbeddedA11yManagerLacros::OnFocusChangedInPage(
const content::FocusedNodeDetails& details) {
if (a11y_helper_remote_.is_bound()) {
a11y_helper_remote_->FocusChanged(details.node_bounds_in_screen);
}
if (focus_changed_callback_for_test_) {
focus_changed_callback_for_test_.Run(details.node_bounds_in_screen);
}
}
void EmbeddedA11yManagerLacros::SetReadingModeEnabled(bool enabled) {
if (reading_mode_enabled_ != enabled) {
reading_mode_enabled_ = enabled;
UpdateEmbeddedA11yHelperExtension();
}
}
bool EmbeddedA11yManagerLacros::IsReadingModeEnabled() {
return reading_mode_enabled_;
}
void EmbeddedA11yManagerLacros::UpdateEmbeddedA11yHelperExtension() {
// Switch Access and Select to Speak share a helper extension which has a
// manifest content script to tell Google Docs to annotate the HTML canvas.
if (select_to_speak_enabled_ || switch_access_enabled_ ||
reading_mode_enabled_) {
EmbeddedA11yExtensionLoader::GetInstance()->InstallExtensionWithId(
extension_misc::kEmbeddedA11yHelperExtensionId,
extension_misc::kEmbeddedA11yHelperExtensionPath,
extension_misc::kEmbeddedA11yHelperManifestFilename,
/*should_localize=*/true);
} else {
EmbeddedA11yExtensionLoader::GetInstance()->RemoveExtensionWithId(
extension_misc::kEmbeddedA11yHelperExtensionId);
}
}
void EmbeddedA11yManagerLacros::UpdateChromeVoxHelperExtension() {
// ChromeVox has a helper extension which has a content script to tell Google
// Docs that ChromeVox is enabled.
if (chromevox_enabled_) {
EmbeddedA11yExtensionLoader::GetInstance()->InstallExtensionWithId(
extension_misc::kChromeVoxHelperExtensionId,
extension_misc::kChromeVoxHelperExtensionPath,
extension_misc::kChromeVoxHelperManifestFilename,
/*should_localize=*/false);
} else {
EmbeddedA11yExtensionLoader::GetInstance()->RemoveExtensionWithId(
extension_misc::kChromeVoxHelperExtensionId);
}
}