blob: 4ae099e81a40c2f05484fe8e7d87cba9ac640a28 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include <windows.h> // Must be in front of other Windows header files.
#include <stddef.h>
#include <memory>
#include "base/check_deref.h"
#include "base/containers/heap_array.h"
#include "base/debug/crash_logging.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/thread_pool.h"
#include "base/win/registry.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/platform/ax_platform.h"
#include "ui/accessibility/platform/ax_platform_node_win.h"
#include "ui/gfx/animation/animation.h"
#include "ui/gfx/win/singleton_hwnd_observer.h"
namespace content {
namespace {
// Killswitch to turn off this feature remotely in case it affects ATs in a way
// we didn't expect. This is temporary.
// TODO(crbug.com/407891291): Remove this feature flag in Chrome 139.
BASE_FEATURE(kDisableUiaProviderWhenJawsIsRunning,
"DisableUiaProviderWhenJawsIsRunning",
base::FEATURE_ENABLED_BY_DEFAULT);
const wchar_t kNarratorRegistryKey[] = L"Software\\Microsoft\\Narrator\\NoRoam";
const wchar_t kWinMagnifierRegistryKey[] =
L"Software\\Microsoft\\ScreenMagnifier";
const wchar_t kWinATRunningStateValueName[] = L"RunningState";
enum class AccessibilityTarget {
kStickyKeys,
kUia,
kJaws,
kNarrator,
kNvda,
kWinMagnifier,
kSupernova,
kZoomText,
kZdsr,
};
struct ModuleVersion {
uint16_t major = 0, minor = 0, build = 0, revision = 0;
bool IsLowerThan(const ModuleVersion& other) const {
if (major != other.major) {
return major < other.major;
}
if (minor != other.minor) {
return minor < other.minor;
}
if (build != other.build) {
return build < other.build;
}
return revision < other.revision;
}
std::string ToString() const {
return base::StringPrintf("%u.%u.%u.%u", major, minor, build, revision);
}
};
struct AssistiveTechInfo {
AccessibilityTarget tech;
std::optional<ModuleVersion> version;
};
std::optional<ModuleVersion> GetModuleVersion(const std::wstring& filename) {
DWORD dummy = 0;
DWORD size = ::GetFileVersionInfoSizeW(filename.c_str(), &dummy);
if (size == 0) {
return std::nullopt;
}
std::vector<BYTE> buffer(size);
if (!::GetFileVersionInfoW(filename.c_str(), dummy, size, buffer.data())) {
return std::nullopt;
}
VS_FIXEDFILEINFO* ffi = nullptr;
UINT len = 0;
if (::VerQueryValueW(buffer.data(), L"\\", reinterpret_cast<void**>(&ffi),
&len) &&
len != 0 && ffi->dwSignature == VS_FFI_SIGNATURE) {
uint16_t major = HIWORD(ffi->dwProductVersionMS);
uint16_t minor = LOWORD(ffi->dwProductVersionMS);
uint16_t build = HIWORD(ffi->dwProductVersionLS);
uint16_t revision = LOWORD(ffi->dwProductVersionLS);
return ModuleVersion{major, minor, build, revision};
}
return std::nullopt;
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(JawsMajorVersion)
enum class JawsMajorVersion {
kLegacy = 0,
k2020 = 1,
k2021 = 2,
k2022 = 3,
k2023 = 4,
k2024 = 5,
k2025 = 6,
k2026 = 7,
k2027 = 8,
k2028 = 9,
k2029 = 10,
k2030 = 11,
k2031 = 12,
k2032 = 13,
k2033 = 14,
k2034 = 15,
k2035 = 16,
k2036 = 17,
k2037 = 18,
k2038 = 19,
k2039 = 20,
k2040 = 21,
kPost2040 = 22,
kMaxValue = kPost2040,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/accessibility/enums.xml:JAWSMajorVersion)
JawsMajorVersion MapModuleVersionToJaws(const ModuleVersion& version) {
constexpr uint16_t kFirstKnownVersion = 2020;
constexpr uint16_t kLastKnownVersion = 2040;
if (version.major > kLastKnownVersion) {
return JawsMajorVersion::kPost2040;
}
if (version.major >= kFirstKnownVersion &&
version.major <= kLastKnownVersion) {
return static_cast<JawsMajorVersion>(version.major -
(kFirstKnownVersion - 1));
}
return JawsMajorVersion::kLegacy;
}
// Older versions of JAWS are known to not work well with text fields when we
// expose the native UIA provider. Disable it when we detect an older version
// version of JAWS. JAWS fixed the issue in versions:
// * 2022.2402.1+
// * 2023.2402.1+
// * 2024.2312.99+
// * 2025+
bool IsJawsCompatibleWithUIA(const ModuleVersion& version) {
return !(version.IsLowerThan(ModuleVersion{2022, 0, 0, 0}) ||
(version.major == 2022 &&
version.IsLowerThan(ModuleVersion{2022, 2402, 1, 0})) ||
(version.major == 2023 &&
version.IsLowerThan(ModuleVersion{2023, 2402, 1, 0})) ||
(version.major == 2024 &&
version.IsLowerThan(ModuleVersion{2024, 2312, 99, 0})));
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(NvdaMajorVersion)
enum class NvdaMajorVersion {
kLegacy = 0,
k2020 = 1,
k2021 = 2,
k2022 = 3,
k2023 = 4,
k2024 = 5,
k2025 = 6,
k2026 = 7,
k2027 = 8,
k2028 = 9,
k2029 = 10,
k2030 = 11,
k2031 = 12,
k2032 = 13,
k2033 = 14,
k2034 = 15,
k2035 = 16,
k2036 = 17,
k2037 = 18,
k2038 = 19,
k2039 = 20,
k2040 = 21,
kPost2040 = 22,
kMaxValue = kPost2040,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/accessibility/enums.xml:NVDAMajorVersion)
NvdaMajorVersion MapModuleVersionToNvda(const ModuleVersion& version) {
constexpr uint16_t kFirstKnownVersion = 2020;
constexpr uint16_t kLastKnownVersion = 2040;
if (version.major > kLastKnownVersion) {
return NvdaMajorVersion::kPost2040;
}
if (version.major >= kFirstKnownVersion &&
version.major <= kLastKnownVersion) {
return static_cast<NvdaMajorVersion>(version.major -
(kFirstKnownVersion - 1));
}
return NvdaMajorVersion::kLegacy;
}
// Returns a vector of all Assistive Technologies that are currently running,
// and their versions if available. We return a vector instead of a map
// because it's technically possible to have multiple versions of the same
// AT running at the same time.
std::vector<AssistiveTechInfo> DiscoverAssistiveTech() {
std::vector<AssistiveTechInfo> discovered_ats;
// NOTE: this method is run from another thread to reduce jank, since
// there's no guarantee these system calls will return quickly.
STICKYKEYS sticky_keys = {.cbSize = sizeof(STICKYKEYS)};
SystemParametersInfo(SPI_GETSTICKYKEYS, 0, &sticky_keys, 0);
if (sticky_keys.dwFlags & SKF_STICKYKEYSON) {
discovered_ats.push_back({AccessibilityTarget::kStickyKeys, std::nullopt});
}
// Narrator detection. Narrator is not injected in process so it needs to be
// detected in a different way.
DWORD narrator_value = 0;
if (base::win::RegKey(HKEY_CURRENT_USER, kNarratorRegistryKey,
KEY_QUERY_VALUE)
.ReadValueDW(kWinATRunningStateValueName, &narrator_value) ==
ERROR_SUCCESS &&
narrator_value) {
discovered_ats.push_back({AccessibilityTarget::kNarrator, std::nullopt});
}
// Windows magnifier detection.
DWORD windows_magnifier_value = 0;
if (base::win::RegKey(HKEY_CURRENT_USER, kWinMagnifierRegistryKey,
KEY_QUERY_VALUE)
.ReadValueDW(kWinATRunningStateValueName,
&windows_magnifier_value) == ERROR_SUCCESS &&
windows_magnifier_value) {
discovered_ats.push_back(
{AccessibilityTarget::kWinMagnifier, std::nullopt});
}
std::vector<HMODULE> snapshot;
if (!base::win::GetLoadedModulesSnapshot(::GetCurrentProcess(), &snapshot)) {
return discovered_ats;
}
TCHAR filename[MAX_PATH];
for (HMODULE module : snapshot) {
auto name_length =
::GetModuleFileName(module, filename, std::size(filename));
if (name_length == 0 || name_length >= std::size(filename)) {
continue;
}
std::string module_name(base::FilePath(filename).BaseName().AsUTF8Unsafe());
if (base::EqualsCaseInsensitiveASCII(module_name, "fsdomsrv.dll")) {
discovered_ats.push_back(
{AccessibilityTarget::kJaws, GetModuleVersion(filename)});
}
if (base::EqualsCaseInsensitiveASCII(module_name,
"vbufbackend_gecko_ia2.dll") ||
base::EqualsCaseInsensitiveASCII(module_name, "nvdahelperremote.dll")) {
discovered_ats.push_back(
{AccessibilityTarget::kNvda, GetModuleVersion(filename)});
}
if (base::EqualsCaseInsensitiveASCII(module_name, "dolwinhk.dll")) {
discovered_ats.push_back(
{AccessibilityTarget::kSupernova, GetModuleVersion(filename)});
}
if (base::EqualsCaseInsensitiveASCII(module_name, "outhelper.dll") ||
base::EqualsCaseInsensitiveASCII(module_name, "outhelper_x64.dll")) {
discovered_ats.push_back(
{AccessibilityTarget::kZdsr, GetModuleVersion(filename)});
}
if (base::EqualsCaseInsensitiveASCII(module_name, "zslhook.dll") ||
base::EqualsCaseInsensitiveASCII(module_name, "zslhook64.dll")) {
discovered_ats.push_back(
{AccessibilityTarget::kZoomText, GetModuleVersion(filename)});
}
if (base::EqualsCaseInsensitiveASCII(module_name, "uiautomation.dll") ||
base::EqualsCaseInsensitiveASCII(module_name, "uiautomationcore.dll")) {
discovered_ats.push_back(
{AccessibilityTarget::kUia, GetModuleVersion(filename)});
}
}
return discovered_ats;
}
void OnWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (message == WM_SETTINGCHANGE && wparam == SPI_SETCLIENTAREAANIMATION) {
gfx::Animation::UpdatePrefersReducedMotion();
BrowserAccessibilityStateImpl::GetInstance()
->NotifyWebContentsPreferencesChanged();
}
}
} // namespace
class BrowserAccessibilityStateImplWin : public BrowserAccessibilityStateImpl {
public:
BrowserAccessibilityStateImplWin();
protected:
void RefreshAssistiveTech() override;
void RefreshAssistiveTechIfNecessary(ui::AXMode new_mode) override;
ui::AXPlatform::ProductStrings GetProductStrings() override;
void OnUiaProviderRequested(bool uia_provider_enabled) override;
void OnUiaProviderDisabled() override;
private:
void OnDiscoveredAssistiveTech(
const std::vector<AssistiveTechInfo>& discovered_ats);
std::unique_ptr<gfx::SingletonHwndObserver> singleton_hwnd_observer_;
// A ScopedAccessibilityMode that holds AXMode::kScreenReader when
// an active screen reader has been detected.
std::unique_ptr<ScopedAccessibilityMode> screen_reader_mode_;
// The presence of an AssistiveTech is currently being recomputed.
// Will be updated via DiscoverAssistiveTech().
bool awaiting_known_assistive_tech_computation_ = false;
};
BrowserAccessibilityStateImplWin::BrowserAccessibilityStateImplWin() {
if (base::SingleThreadTaskRunner::HasCurrentDefault()) {
singleton_hwnd_observer_ = std::make_unique<gfx::SingletonHwndObserver>(
base::BindRepeating(&OnWndProc));
}
}
void BrowserAccessibilityStateImplWin::RefreshAssistiveTech() {
if (!awaiting_known_assistive_tech_computation_) {
awaiting_known_assistive_tech_computation_ = true;
// Using base::Unretained() instead of a weak pointer as the lifetime of
// this is tied to BrowserMainLoop.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&DiscoverAssistiveTech),
base::BindOnce(
&BrowserAccessibilityStateImplWin::OnDiscoveredAssistiveTech,
base::Unretained(this)));
}
}
void BrowserAccessibilityStateImplWin::RefreshAssistiveTechIfNecessary(
ui::AXMode new_mode) {
bool was_screen_reader_active = ax_platform().IsScreenReaderActive();
bool has_screen_reader_mode = new_mode.has_mode(ui::AXMode::kScreenReader);
if (was_screen_reader_active != has_screen_reader_mode) {
OnAssistiveTechFound(has_screen_reader_mode
? ui::AssistiveTech::kGenericScreenReader
: ui::AssistiveTech::kNone);
return;
}
// An expensive check is required to determine which type of assistive tech is
// in use. Make this check only when `kExtendedProperties` is added or removed
// from the process-wide mode flags and no previous assistive tech has been
// discovered (in the former case) or one had been discovered (in the latter
// case). `kScreenReader` will be added/removed from the process-wide mode
// flags on completion and `OnAssistiveTechFound()` will be called with the
// results of the check.
bool has_extended_properties =
new_mode.has_mode(ui::AXMode::kExtendedProperties);
if (was_screen_reader_active != has_extended_properties) {
// Perform expensive assistive tech detection.
RefreshAssistiveTech();
}
}
void BrowserAccessibilityStateImplWin::OnDiscoveredAssistiveTech(
const std::vector<AssistiveTechInfo>& at_infos) {
awaiting_known_assistive_tech_computation_ = false;
if (ActiveAssistiveTech() == ui::AssistiveTech::kGenericScreenReader) {
// A test has overridden the screen reader state manually.
// In such cases, we don't want to alter it.
return;
}
// Older versions of JAWS are known to not work well with text fields when we
// expose the native UIA provider. Disable it when we detect an older version
// version of JAWS. JAWS fixed the issue in versions:
// * 2022.2402.1+
// * 2023.2402.1+
// * 2024.2312.99+
// * 2025+
if (base::FeatureList::IsEnabled(kDisableUiaProviderWhenJawsIsRunning) &&
ui::AXPlatform::GetInstance().IsUiaProviderEnabled()) {
for (const auto& info : at_infos) {
if (info.tech == AccessibilityTarget::kJaws && info.version.has_value()) {
if (!IsJawsCompatibleWithUIA(*info.version)) {
ui::AXPlatform::GetInstance().DisableActiveUiaProvider();
break;
}
}
}
}
// Helper lambda to check for a specific AT.
auto HasTarget = [&at_infos](AccessibilityTarget target) -> bool {
for (const auto& info : at_infos) {
if (info.tech == target) {
return true;
}
}
return false;
};
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinJAWS",
HasTarget(AccessibilityTarget::kJaws));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinNarrator",
HasTarget(AccessibilityTarget::kNarrator));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinNVDA",
HasTarget(AccessibilityTarget::kNvda));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinSupernova",
HasTarget(AccessibilityTarget::kSupernova));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinMagnifier",
HasTarget(AccessibilityTarget::kWinMagnifier));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinZoomText",
HasTarget(AccessibilityTarget::kZoomText));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinAPIs.UIAutomation",
HasTarget(AccessibilityTarget::kUia));
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinStickyKeys",
HasTarget(AccessibilityTarget::kStickyKeys));
static auto* ax_jaws_crash_key = base::debug::AllocateCrashKeyString(
"ax_jaws", base::debug::CrashKeySize::Size32);
static auto* ax_narrator_crash_key = base::debug::AllocateCrashKeyString(
"ax_narrator", base::debug::CrashKeySize::Size32);
static auto* ax_win_magnifier_crash_key = base::debug::AllocateCrashKeyString(
"ax_win_magnifier", base::debug::CrashKeySize::Size32);
static auto* ax_nvda_crash_key = base::debug::AllocateCrashKeyString(
"ax_nvda", base::debug::CrashKeySize::Size32);
static auto* ax_supernova_crash_key = base::debug::AllocateCrashKeyString(
"ax_supernova", base::debug::CrashKeySize::Size32);
static auto* ax_zdsr_crash_key = base::debug::AllocateCrashKeyString(
"ax_zdsr", base::debug::CrashKeySize::Size32);
static auto* ax_zoomtext_crash_key = base::debug::AllocateCrashKeyString(
"ax_zoomtext", base::debug::CrashKeySize::Size32);
static auto* ax_uia_crash_key = base::debug::AllocateCrashKeyString(
"ax_ui_automation", base::debug::CrashKeySize::Size32);
// API support library, not an actual AT.
if (HasTarget(AccessibilityTarget::kUia)) {
base::debug::SetCrashKeyString(ax_uia_crash_key, "true");
} else {
base::debug::ClearCrashKeyString(ax_uia_crash_key);
}
// More than one type of assistive tech can be running at the same time.
// Will prefer to report screen reader over other types of assistive tech,
// because screen readers have the strongest effect on the user experience.
ui::AssistiveTech most_important_assistive_tech = ui::AssistiveTech::kNone;
if (HasTarget(AccessibilityTarget::kWinMagnifier)) {
base::debug::SetCrashKeyString(ax_narrator_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kWinMagnifier;
} else {
base::debug::ClearCrashKeyString(ax_win_magnifier_crash_key);
}
if (HasTarget(AccessibilityTarget::kZoomText)) {
base::debug::SetCrashKeyString(ax_zoomtext_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kZoomText;
} else {
base::debug::ClearCrashKeyString(ax_zoomtext_crash_key);
}
if (HasTarget(AccessibilityTarget::kJaws)) {
base::debug::SetCrashKeyString(ax_jaws_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kJaws;
} else {
base::debug::ClearCrashKeyString(ax_jaws_crash_key);
}
if (HasTarget(AccessibilityTarget::kNarrator)) {
base::debug::SetCrashKeyString(ax_narrator_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kNarrator;
} else {
base::debug::ClearCrashKeyString(ax_narrator_crash_key);
}
if (HasTarget(AccessibilityTarget::kNvda)) {
base::debug::SetCrashKeyString(ax_nvda_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kNvda;
} else {
base::debug::ClearCrashKeyString(ax_nvda_crash_key);
}
if (HasTarget(AccessibilityTarget::kSupernova)) {
base::debug::SetCrashKeyString(ax_supernova_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kSupernova;
} else {
base::debug::ClearCrashKeyString(ax_supernova_crash_key);
}
if (HasTarget(AccessibilityTarget::kZdsr)) {
base::debug::SetCrashKeyString(ax_zdsr_crash_key, "true");
most_important_assistive_tech = ui::AssistiveTech::kZdsr;
} else {
base::debug::ClearCrashKeyString(ax_zdsr_crash_key);
}
// Histograms for the JAWS and NVDA versions.
for (const auto& info : at_infos) {
if (info.tech == AccessibilityTarget::kJaws && info.version) {
UMA_HISTOGRAM_BOOLEAN("Accessibility.WinJAWSCompatibleWithUIA",
IsJawsCompatibleWithUIA(*info.version));
JawsMajorVersion jaws_version = MapModuleVersionToJaws(*info.version);
base::UmaHistogramEnumeration("Accessibility.WinJAWSVersion",
jaws_version);
continue;
}
if (info.tech == AccessibilityTarget::kNvda && info.version) {
NvdaMajorVersion nvda_version = MapModuleVersionToNvda(*info.version);
base::UmaHistogramEnumeration("Accessibility.WinNVDAVersion",
nvda_version);
continue;
}
}
// Save the current assistive tech before toggling AXModes, so
// that RefreshAssistiveTechIfNecessary() is a noop.
OnAssistiveTechFound(most_important_assistive_tech);
// Add kScreenReader mode flag for products with screen reader features, which
// includes some magnifiers with light screen reader features (e.g. heading
// navigation).
if (ui::IsScreenReader(most_important_assistive_tech)) {
if (!screen_reader_mode_) {
screen_reader_mode_ = CreateScopedModeForProcess(
ui::kAXModeComplete | ui::AXMode::kScreenReader);
}
} else {
screen_reader_mode_.reset();
}
}
ui::AXPlatform::ProductStrings
BrowserAccessibilityStateImplWin::GetProductStrings() {
ContentClient& content_client = CHECK_DEREF(content::GetContentClient());
// GetProduct() returns a string like "Chrome/aa.bb.cc.dd", split out
// the part before and after the "/".
std::vector<std::string> product_components = base::SplitString(
CHECK_DEREF(CHECK_DEREF(content::GetContentClient()).browser())
.GetProduct(),
"/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (product_components.size() != 2) {
return {{}, {}, CHECK_DEREF(content_client.browser()).GetUserAgent()};
}
return {product_components[0], product_components[1],
CHECK_DEREF(content_client.browser()).GetUserAgent()};
}
void BrowserAccessibilityStateImplWin::OnUiaProviderRequested(
bool uia_provider_enabled) {
CHECK_DEREF(CHECK_DEREF(GetContentClient()).browser())
.OnUiaProviderRequested(uia_provider_enabled);
}
void BrowserAccessibilityStateImplWin::OnUiaProviderDisabled() {
CHECK_DEREF(CHECK_DEREF(GetContentClient()).browser())
.OnUiaProviderDisabled();
}
// static
std::unique_ptr<BrowserAccessibilityStateImpl>
BrowserAccessibilityStateImpl::Create() {
return std::make_unique<BrowserAccessibilityStateImplWin>();
}
} // namespace content