| // 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); |
| } |
| } |