blob: 2c73c81bb6a39b3cbd5010d2a9cbfedeb5c3c35e [file] [log] [blame]
// 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/accessibility/pdf_ocr_controller.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/string_split.h"
#include "chrome/browser/accessibility/accessibility_state_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/screen_ai/screen_ai_service_router.h"
#include "chrome/browser/screen_ai/screen_ai_service_router_factory.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/common/language_util.h"
#include "components/pdf/common/pdf_util.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_iterator.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/browser/web_contents.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/crosapi/mojom/prefs.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#endif
namespace {
constexpr char kHtmlMimeType[] = "text/html";
// Returns true if a screen reader is present or (on Chrome OS only) if
// select-to-speak is enabled.
bool IsAccessibilityEnabled() {
// Active if a screen reader is present.
if (accessibility_state_utils::IsScreenReaderEnabled()) {
return true;
}
#if BUILDFLAG(IS_CHROMEOS)
// Conditionally active if select-to-speak is enabled.
if (features::IsAccessibilityPdfOcrForSelectToSpeakEnabled() &&
accessibility_state_utils::IsSelectToSpeakEnabled()) {
return true;
}
#endif // BUILDFLAG(IS_CHROMEOS)
return false;
}
// For a PDF tab, there are two associated processes (and two WebContentses):
// (i) PDF Viewer Mimehandler (mime type = text/html) and (ii) PDF renderer
// process (mime type = application/pdf). This helper function returns all PDF-
// related WebContentses associated with the Mimehandlers for a given Profile.
// Note that it does trigger PdfAccessibilityTree::AccessibilityModeChanged()
// if the AXMode with ui::AXMode::kPDFOcr is set on PDF WebContents with the
// text/html mime type; but it does not on PDF WebContents with the
// application/pdf mime type.
std::vector<content::WebContents*> GetPdfHtmlWebContentses(Profile* profile) {
// Code borrowed from `content::WebContentsImpl::GetAllWebContents()`.
std::vector<content::WebContents*> result;
std::unique_ptr<content::RenderWidgetHostIterator> widgets(
content::RenderWidgetHost::GetRenderWidgetHosts());
// Iterate over all RWHs and their RVHs and store a WebContents if the
// WebContents is associated with PDF Viewer Mimehandler and belongs to the
// given Profile.
while (content::RenderWidgetHost* rwh = widgets->GetNextHost()) {
content::RenderViewHost* rvh = content::RenderViewHost::From(rwh);
if (!rvh) {
continue;
}
content::WebContents* web_contents =
content::WebContents::FromRenderViewHost(rvh);
if (!web_contents) {
continue;
}
if (profile !=
Profile::FromBrowserContext(web_contents->GetBrowserContext())) {
continue;
}
// Check if WebContents is PDF's.
if (!IsPdfExtensionOrigin(
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin())) {
continue;
}
DCHECK_EQ(web_contents->GetContentsMimeType(), kHtmlMimeType);
result.push_back(web_contents);
}
return result;
}
// Invoke screen reader alert to notify the user of the state.
void AnnounceToScreenReader(const int message_id) {
const Browser* browser = BrowserList::GetInstance()->GetLastActive();
if (!browser) {
VLOG(2) << "Browser is not ready to announce";
return;
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
if (!browser_view) {
VLOG(2) << "Browser is not ready to announce";
return;
}
browser_view->GetViewAccessibility().AnnounceText(
l10n_util::GetStringUTF16(message_id));
}
void RecordAcceptLanguages(const std::string& accept_languages) {
for (std::string language :
base::SplitString(accept_languages, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
// Convert to a Chrome language code synonym. This language synonym is then
// converted into a `LocaleCodeISO639` enum value for a UMA histogram.
language::ToChromeLanguageSynonym(&language);
// TODO(crbug.com/40267312): Add a browser test to validate this UMA metric.
base::UmaHistogramSparse("Accessibility.PdfOcr.UserAcceptLanguage",
base::HashMetricName(language));
}
}
} // namespace
namespace screen_ai {
PdfOcrController::PdfOcrController(Profile* profile) : profile_(profile) {
// Initialize an observer for changes of PDF OCR pref.
DCHECK(profile_);
VLOG(2) << "Init PdfOcrController";
pref_change_registrar_.Init(profile_->GetPrefs());
pref_change_registrar_.Add(
prefs::kAccessibilityPdfOcrAlwaysActive,
base::BindRepeating(&PdfOcrController::OnPdfOcrAlwaysActiveChanged,
weak_ptr_factory_.GetWeakPtr()));
// Register for changes to screenreader/spoken feedback/select to speak.
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (auto* const accessibility_manager = ash::AccessibilityManager::Get();
accessibility_manager) {
// Unretained is safe because `this` owns the subscription.
accessibility_status_subscription_ =
accessibility_manager->RegisterCallback(
base::BindRepeating(&PdfOcrController::OnAccessibilityStatusEvent,
base::Unretained(this)));
}
#else // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO: Observe Chrome OS's select-to-speak setting.
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
ax_mode_observation_.Observe(&ui::AXPlatform::GetInstance());
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Trigger if the preference is already set, and a screen reader or Select-to-
// Speak on ChromeOS is enabled.
OnActivationChanged();
}
PdfOcrController::~PdfOcrController() = default;
// static
std::vector<content::WebContents*>
PdfOcrController::GetAllPdfWebContentsesForTesting(Profile* profile) {
return GetPdfHtmlWebContentses(profile);
}
bool PdfOcrController::IsEnabled() const {
return scoped_accessibility_mode_ != nullptr;
}
void PdfOcrController::OnPdfOcrAlwaysActiveChanged() {
const auto& pref_value =
profile_->GetPrefs()->GetValue(prefs::kAccessibilityPdfOcrAlwaysActive);
VLOG(2) << "PDF OCR Always Active changed: " << pref_value.GetBool();
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// This preference should be kept in sync with Ash.
auto* lacros_service = chromeos::LacrosService::Get();
if (!lacros_service ||
!lacros_service->IsAvailable<crosapi::mojom::Prefs>()) {
VLOG(0) << "Cannot sync the preference with Ash.";
} else {
lacros_service->GetRemote<crosapi::mojom::Prefs>()->SetPref(
crosapi::mojom::PrefPath::kAccessibilityPdfOcrAlwaysActive,
pref_value.Clone(), base::OnceClosure());
}
#endif
OnActivationChanged();
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void PdfOcrController::OnAccessibilityStatusEvent(
const ash::AccessibilityStatusEventDetails& details) {
if (details.notification_type ==
ash::AccessibilityNotificationType::kToggleSpokenFeedback ||
details.notification_type ==
ash::AccessibilityNotificationType::kToggleSelectToSpeak) {
OnActivationChanged();
}
}
#endif // BUIDLFLAG(IS_CHROMEOS_ASH)
void PdfOcrController::OnActivationChanged() {
const bool is_always_active =
IsAccessibilityEnabled() &&
profile_->GetPrefs()->GetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive);
if (is_always_active == IsEnabled()) {
return; // No change in activation.
}
if (is_always_active) {
RecordAcceptLanguages(
profile_->GetPrefs()->GetString(language::prefs::kAcceptLanguages));
if (!ocr_service_ready_) {
// Avoid repeated requests.
if (waiting_for_ocr_service_initialization_) {
return;
}
waiting_for_ocr_service_initialization_ = true;
if (ScreenAIInstallState::GetInstance()->get_state() !=
ScreenAIInstallState::State::kDownloaded &&
!component_ready_observer_.IsObserving()) {
// Start observing ScreenAIInstallState to report it to user.
component_ready_observer_.Observe(ScreenAIInstallState::GetInstance());
}
screen_ai::ScreenAIServiceRouterFactory::GetForBrowserContext(profile_)
->GetServiceStateAsync(
ScreenAIServiceRouter::Service::kOCR,
base::BindOnce(
&PdfOcrController::OCRServiceInitializationCallback,
weak_ptr_factory_.GetWeakPtr()));
return;
}
// This will send the `kPDFOcr` flag to all WebContents. Strictly speaking,
// it need only be sent to those associated with PDF Viewer Mimehandlers,
// but we have no filtering mechanism today. The others should simply ignore
// it.
scoped_accessibility_mode_ =
content::BrowserAccessibilityState::GetInstance()
->CreateScopedModeForBrowserContext(profile_, ui::AXMode::kPDFOcr);
} else {
scoped_accessibility_mode_.reset();
}
}
void PdfOcrController::OCRServiceInitializationCallback(bool successful) {
waiting_for_ocr_service_initialization_ = false;
ocr_service_ready_ = successful;
if (successful) {
OnActivationChanged();
} else {
// Call `StateChanged` to announce the state to user.
StateChanged(ScreenAIInstallState::State::kDownloadFailed);
}
// No more need for observing Screen AI state changes.
component_ready_observer_.Reset();
}
void PdfOcrController::StateChanged(ScreenAIInstallState::State state) {
switch (state) {
case ScreenAIInstallState::State::kNotDownloaded:
break;
case ScreenAIInstallState::State::kDownloading:
AnnounceToScreenReader(IDS_SETTINGS_PDF_OCR_DOWNLOADING);
break;
case ScreenAIInstallState::State::kDownloadFailed:
AnnounceToScreenReader(IDS_SETTINGS_PDF_OCR_DOWNLOAD_ERROR);
// Update the PDF OCR pref to be false to toggle off the button.
profile_->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive,
false);
break;
case ScreenAIInstallState::State::kDownloaded:
AnnounceToScreenReader(IDS_SETTINGS_PDF_OCR_DOWNLOAD_COMPLETE);
break;
}
}
#if !BUILDFLAG(IS_CHROMEOS_ASH)
void PdfOcrController::OnAXModeAdded(ui::AXMode mode) {
if (mode.has_mode(ui::AXMode::kScreenReader)) {
OnActivationChanged();
}
}
#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
} // namespace screen_ai