blob: 23e48e10519f3e2facf5512bae6ab0adf21e25a1 [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 <stddef.h>
#include <algorithm>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "content/browser/accessibility/render_accessibility_host.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/browser/web_contents_user_data.h"
#include "content/public/common/content_switches.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/accessibility/ax_mode_histogram_logger.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/color_utils.h"
namespace content {
namespace {
BrowserAccessibilityStateImpl* g_instance = nullptr;
// Auto-disable accessibility if this many seconds elapse with user input
// events but no accessibility API usage.
constexpr int kAutoDisableAccessibilityTimeSecs = 30;
// Minimum number of user input events with no accessibility API usage
// before auto-disabling accessibility.
constexpr int kAutoDisableAccessibilityEventCount = 3;
// Updating Active/Inactive time on every accessibility api calls would not be
// good for perf. Instead, delay the update task.
constexpr int kOnAccessibilityUsageUpdateDelaySecs = 5;
// Parameter values for --force-renderer-accessibility=[bundle-name].
const char kAXModeBundleBasic[] = "basic";
const char kAXModeBundleFormControls[] = "form-controls";
const char kAXModeBundleComplete[] = "complete";
const char kAXModeBundleOnScreen[] = "on-screen";
// A data holder attached to a WebContents while it is hidden and has
// accessibility enabled. Used only when the disable_on_hide feature of
// ProgressiveAccessibility is enabled and an active screen reader has not been
// detected.
//
// An instance of this class is attached to a WebContents when it is hidden
// (thereby recording the TimeTicks at which the hide event took place). Its
// `Schedule()` method can later be called to schedule disablement of
// accessibility after the WebContents has been hidden for at least five
// minutes (+/- a randomizer of up to twenty seconds).
//
// The instance is removed from the WebContents and destroyed on the first of:
// * the WebContents is destroyed (by virtue of being a WebContentsUserData),
// * the WebContents is revealed (see
// `BrowserAccessibilityStateImpl::OnWebContentsRevealed()`),
// * an active screen reader is detected (see
// `BrowserAccessibilityStateImpl::OnAssistiveTechFound()`), or
// * the task to disable accessibility runs.
class AccessibilityDisabler
: public WebContentsUserData<AccessibilityDisabler> {
public:
WEB_CONTENTS_USER_DATA_KEY_DECL();
// Constructs an instance for `web_contents`; see comment above for
// details. `callback` will be run if this instance is destroyed (either
// because `web_contents` is destroyed or because `Remove()` is called) before
// `Schedule()` is called.
using OnDestroyedBeforeScheduleCallback =
base::OnceCallback<void(WebContentsImpl* web_contents)>;
AccessibilityDisabler(WebContents* web_contents,
OnDestroyedBeforeScheduleCallback callback)
: WebContentsUserData(*web_contents),
on_destroyed_before_schedule_(std::move(callback)) {}
// This destructor is run either when the WebContents to which this instance
// is attached is destroyed or when `Remove()` is called.
~AccessibilityDisabler() override {
// If the the instance still has the on_destroyed_before_schedule_ callback,
// then `Schedule()` has not yet been called. Run the callback now so that
// the BrowserAccessibilityStateImpl can remove the WebContents from its
// last_hidden_ collection.
if (on_destroyed_before_schedule_) {
std::move(on_destroyed_before_schedule_)
.Run(&static_cast<WebContentsImpl&>(GetWebContents()));
}
}
// Removes (and destroys) an instance attached to `web_contents`.
static void Remove(WebContentsImpl* web_contents) {
web_contents->RemoveUserData(UserDataKey());
}
// Schedules a task that will disable accessibility for `web_contents` once
// it has been hidden for at least five minutes +/- twenty seconds.
static void Schedule(WebContentsImpl* web_contents) {
auto* disabler = FromWebContents(web_contents);
CHECK(disabler);
// Elapsed ticks since the WebContents was hidden.
const base::TimeDelta since_hidden =
base::TimeTicks::Now() - disabler->hide_instant_;
// Ticks until accessibility should be disabled.
const base::TimeDelta disable_in =
BrowserAccessibilityStateImpl::GetRandomizedDisableDelay() -
since_hidden;
disabler->disable_ax_timer_.Start(
FROM_HERE, std::max(disable_in, base::TimeDelta()), disabler,
&AccessibilityDisabler::DisableAccessibility);
// Now that this WebContents has been scheduled for disablement, it is no
// longer in the BrowserAccessibilityStateImpl's last_hidden_ collection,
// therefore it is no longer necessary to notify it upon destruction.
disabler->on_destroyed_before_schedule_.Reset();
}
private:
void DisableAccessibility() {
base::UmaHistogramBoolean("Accessibility.DisabledAfterHide", true);
auto& web_contents = static_cast<WebContentsImpl&>(GetWebContents());
web_contents.SetAccessibilityMode({});
web_contents.RemoveUserData(UserDataKey()); // deletes `this`.
}
// A callback to be run if the WebContents is destroyed before `Schedule()` is
// called.
OnDestroyedBeforeScheduleCallback on_destroyed_before_schedule_;
// The time the WebContents was hidden.
base::TimeTicks hide_instant_{base::TimeTicks::Now()};
// A timer to disable accessibility after a delay.
base::OneShotTimer disable_ax_timer_;
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(AccessibilityDisabler);
// A holder of a ScopedModeCollection targeting a specific BrowserContext or
// WebContents. The collection is bound to the lifetime of the target.
class ModeCollectionForTarget : public base::SupportsUserData::Data,
public ScopedModeCollection::Delegate {
public:
using OnModeChangedCallback =
base::RepeatingCallback<void(ui::AXMode old_mode, ui::AXMode new_mode)>;
ModeCollectionForTarget(base::SupportsUserData* target,
OnModeChangedCallback on_mode_changed)
: target_(target), on_mode_changed_(std::move(on_mode_changed)) {}
ModeCollectionForTarget(const ModeCollectionForTarget&) = delete;
ModeCollectionForTarget& operator=(const ModeCollectionForTarget&) = delete;
static ui::AXMode GetAccessibilityMode(base::SupportsUserData* target) {
auto* instance = FromTarget(target);
return instance ? instance->scoped_mode_collection_.accessibility_mode()
: ui::AXMode();
}
// Adds a new scoper targeting `target` (a BrowserContext or a WebContents)
// that applies the accessibility mode flags in `mode`. `on_changed_function`
// is a pointer to a member function of `BrowserAccessibilityStateImpl` that
// is called when the effective mode for `target` changes; see
// `ScopedModeCollection::OnModeChangedCallback`. It is bound into a callback
// (along with `impl`) when this is the first addition for `target`;
// otherwise, it (and `impl`) are ignored.
template <class Target>
static std::unique_ptr<ScopedAccessibilityMode> Add(
Target* target,
void (BrowserAccessibilityStateImpl::*on_changed_function)(Target*,
ui::AXMode,
ui::AXMode),
BrowserAccessibilityStateImpl* impl,
ui::AXMode mode) {
auto* instance = FromTarget(target);
if (!instance) {
auto holder = std::make_unique<ModeCollectionForTarget>(
target,
base::BindRepeating(on_changed_function, base::Unretained(impl),
base::Unretained(target)));
instance = holder.get();
target->SetUserData(&kUserDataKey, std::move(holder));
}
return instance->scoped_mode_collection_.Add(mode);
}
private:
static ModeCollectionForTarget* FromTarget(base::SupportsUserData* target) {
return static_cast<ModeCollectionForTarget*>(
target->GetUserData(&ModeCollectionForTarget::kUserDataKey));
}
void OnModeChanged(ui::AXMode old_mode, ui::AXMode new_mode) override {
// If the collection is no longer bound to the target, the target is in the
// process of being destroyed. Ignore changes when this is the case.
if (auto* const collection = FromTarget(target_); collection) {
on_mode_changed_.Run(old_mode, new_mode);
}
}
ui::AXMode FilterModeFlags(ui::AXMode mode) override { return mode; }
static const int kUserDataKey = 0;
raw_ptr<base::SupportsUserData> target_;
OnModeChangedCallback on_mode_changed_;
ScopedModeCollection scoped_mode_collection_{*this};
};
// static
const int ModeCollectionForTarget::kUserDataKey;
// Returns a subset of `mode` for delivery to a WebContents.
ui::AXMode FilterAccessibilityModeInvariants(ui::AXMode mode) {
// kFromPlatform is never sent to WebContents.
CHECK(!mode.has_mode(ui::AXMode::kFromPlatform));
// Strip kLabelImages if kExtendedProperties is absent.
// TODO(grt): kLabelImages is a feature of //chrome. Find a way to
// achieve this filtering without teaching //content about it. Perhaps via
// the delegate interface to be added in support of https://crbug.com/1470199.
if (ui::AXMode(mode.flags() ^ ui::AXMode::kExtendedProperties)
.has_mode(ui::AXMode::kLabelImages |
ui::AXMode::kExtendedProperties)) {
mode.set_mode(ui::AXMode::kLabelImages, false);
}
// Modes above kNativeAPIs and kWebContents require kWebContents. Some
// components may enable higher bits, but those should only be given to a
// WebContents if that WebContents also has the kWebContents mode enabled;
// see `content::RenderFrameHostImpl::UpdateAccessibilityMode()` and
// `content::RenderAccessibilityManager::SetMode()`.
if (!mode.has_mode(ui::AXMode::kWebContents)) {
return mode & ui::AXMode::kNativeAPIs;
}
// Form controls mode is restrictive. There are other modes that should not be
// used in combination with it. This could occur if something that needs
// screen reader mode is turned on after forms control mode. In that case,
// forms mode must be removed.
if (mode.has_mode(ui::AXMode::kInlineTextBoxes) ||
mode.has_mode(ui::AXMode::kExtendedProperties)) {
return ui::AXMode(mode.flags(),
mode.filter_flags() & ~ui::AXMode::kFormsAndLabelsOnly);
}
return mode;
}
// Determines if the given `mode` contains flags that conflict
// with the performance experiment. Certain AXMode flags are allowed because
// they are either extra annotations or not relevant to web content (the
// primary focus of the performance measurement).
bool IsAXModeConflictingWithExperiment(ui::AXMode mode) {
// Remove the allowed flags from the 'mode'.
// If any flags remain after this operation, they are considered conflicting.
mode &=
~ui::AXMode(ui::AXMode::kAnnotateMainNode | ui::AXMode::kFromPlatform |
ui::AXMode::kLabelImages | ui::AXMode::kNativeAPIs);
// If 'mode' is not entirely cleared after removing allowed flags, then
// conflicting flags were present.
return !mode.is_mode_off();
}
} // namespace
// static
BrowserAccessibilityState* BrowserAccessibilityState::GetInstance() {
return BrowserAccessibilityStateImpl::GetInstance();
}
// static
BrowserAccessibilityStateImpl* BrowserAccessibilityStateImpl::GetInstance() {
CHECK(g_instance);
return g_instance;
}
// On Android, Mac, Windows and Linux there are platform-specific subclasses.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_WIN) && !BUILDFLAG(IS_MAC) && \
!BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
// static
std::unique_ptr<BrowserAccessibilityStateImpl>
BrowserAccessibilityStateImpl::Create() {
return base::WrapUnique(new BrowserAccessibilityStateImpl());
}
#endif
namespace {
constexpr base::TimeDelta kDisableDelay = base::Minutes(5);
constexpr int kDisableDelayVarianceSeconds = 20;
} // namespace
// static
base::TimeDelta BrowserAccessibilityStateImpl::GetRandomizedDisableDelay() {
const base::TimeDelta variance = base::Seconds(base::RandInt(
-kDisableDelayVarianceSeconds, kDisableDelayVarianceSeconds));
return kDisableDelay + variance;
}
// static
base::TimeDelta BrowserAccessibilityStateImpl::GetMaxDisableDelay() {
return kDisableDelay + base::Seconds(kDisableDelayVarianceSeconds);
}
BrowserAccessibilityStateImpl::BrowserAccessibilityStateImpl()
: platform_ax_mode_(CreateScopedModeForProcess(ui::AXMode())) {
DCHECK_EQ(g_instance, nullptr);
g_instance = this;
bool disallow_changes = false;
ui::AXMode initial_mode;
auto& command_line = *base::CommandLine::ForCurrentProcess();
if (command_line.HasSwitch(
switches::kDisablePlatformAccessibilityIntegration)) {
SetActivationFromPlatformEnabled(/*enabled=*/false);
}
if (command_line.HasSwitch(switches::kDisableRendererAccessibility)) {
disallow_changes = true;
} else if (command_line.HasSwitch(switches::kForceRendererAccessibility)) {
#if BUILDFLAG(IS_WIN)
std::string ax_mode_bundle =
base::WideToUTF8(command_line.GetSwitchValueNative(
switches::kForceRendererAccessibility));
#else
std::string ax_mode_bundle = command_line.GetSwitchValueNative(
switches::kForceRendererAccessibility);
#endif
if (ax_mode_bundle.empty()) {
// For backwards compatibility, when --force-renderer-accessibility has no
// parameter, use the screen reader bundle but allow changes.
// This is the best general choice in development and testing scenarios.
initial_mode = ui::kAXModeComplete | ui::AXMode::kScreenReader;
} else {
// Support
// --force-renderer-accessibility=[basic|form-controls|complete|
// screen-reader|on-screen]
if (ax_mode_bundle.compare(kAXModeBundleBasic) == 0) {
initial_mode = ui::kAXModeBasic;
} else if (ax_mode_bundle.compare(kAXModeBundleFormControls) == 0) {
initial_mode = ui::kAXModeFormControls;
} else if (ax_mode_bundle.compare(kAXModeBundleComplete) == 0) {
initial_mode = ui::kAXModeComplete;
} else if (ax_mode_bundle.compare(kAXModeBundleOnScreen) == 0) {
initial_mode = ui::kAXModeOnScreen;
} else {
// If 'screen-reader', or invalid, default to screen reader bundle,
// which is the most useful in development and testing scenarios.
initial_mode = ui::kAXModeComplete | ui::AXMode::kScreenReader;
}
disallow_changes = true;
}
}
if (::features::IsAccessibilityOnScreenAXModeEnabled()) {
initial_mode |= ui::kAXModeOnScreen;
}
// Create an initial process-wide ScopedAccessibilityMode whether any flags
// are enabled or not. Always creating a ScopedAccessibilityMode
// (even if it holds a mode with all flags off) allows us to avoid null
// checks elsewhere, thereby simplifying other logic.
forced_accessibility_mode_ = CreateScopedModeForProcess(initial_mode);
// Configure the performance experiment if no command-line switches were used.
if (!disallow_changes && initial_mode.is_mode_off()) {
experiment_accessibility_mode_ =
ConfigureAccessibilityPerformanceExperiment();
}
UMA_HISTOGRAM_BOOLEAN("Accessibility.ManuallyEnabled",
!initial_mode.is_mode_off());
SetAXModeChangeAllowed(!disallow_changes);
}
BrowserAccessibilityStateImpl::~BrowserAccessibilityStateImpl() {
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
CHECK(last_hidden_.empty());
}
void BrowserAccessibilityStateImpl::OnAssistiveTechFound(
ui::AssistiveTech assistive_tech) {
const bool was_screenreader_active = ax_platform_.IsScreenReaderActive();
ax_platform_.NotifyAssistiveTechChanged(assistive_tech);
// Terminate disable_on_hide if a screen reader has just become active. Do
// this without first checking the feature to avoid activating the field trial
// when it's not already active. Performing this removal when the feature is
// off is harmless.
if (!was_screenreader_active && ax_platform_.IsScreenReaderActive()) {
// Cancel all disablers. There is one for each WebContents in `last_hidden_`
// and one for each that has had `AccessibilityDisabler::Schedule()` called.
// Since these are not specifically tracked, remove a potential disabler
// from every WebContents. OnDisablerDestroyedForWebContents will be called
// to remove a WebContents from `last_hidden_` if its disabler has not yet
// been scheduled.
std::ranges::for_each(WebContentsImpl::GetAllWebContents(),
[](WebContentsImpl* web_contents) {
if (!web_contents->IsBeingDestroyed() &&
!web_contents->IsNeverComposited()) {
AccessibilityDisabler::Remove(web_contents);
}
});
}
}
void BrowserAccessibilityStateImpl::RefreshAssistiveTech() {
bool sr_active = GetAccessibilityMode().has_mode(ui::AXMode::kScreenReader);
OnAssistiveTechFound(sr_active ? ui::AssistiveTech::kGenericScreenReader
: ui::AssistiveTech::kNone);
}
std::unique_ptr<ScopedAccessibilityMode>
BrowserAccessibilityStateImpl::ConfigureAccessibilityPerformanceExperiment() {
if (!features::IsAccessibilityPerformanceMeasurementExperimentEnabled()) {
// This is the control group.
return nullptr;
}
// Checking the flag is what causes the study to be active, so we need to
// configure the AXModes based on which experiment arm we are in.
switch (features::GetAccessibilityPerformanceMeasurementExperimentGroup()) {
case features::AccessibilityPerformanceMeasurementExperimentGroup::
kAXModeComplete:
return CreateScopedModeForProcess(ui::kAXModeComplete);
case features::AccessibilityPerformanceMeasurementExperimentGroup::
kWebContentsOnly:
// TODO(accessibility): there seems to be a strange naming here.
// kWebContentsOnly helper function in ax_mode.h defines almost a
// kAXModeComplete. However, in experiment setup discussions, we wanted
// more likely kAXModeBasic, where only the real, AXMode, kWebContents
// is set. Which one is it?
return CreateScopedModeForProcess(ui::kAXModeBasic);
case features::AccessibilityPerformanceMeasurementExperimentGroup::
kAXModeCompleteNoInlineTextBoxes:
return CreateScopedModeForProcess(ui::kAXModeComplete &
~ui::AXMode::kInlineTextBoxes);
case features::AccessibilityPerformanceMeasurementExperimentGroup::
kRendererSerializationOnly:
RenderAccessibilityHost::SetRendererSerializationExperimentEnabled(true);
return CreateScopedModeForProcess(ui::kAXModeComplete);
}
NOTREACHED();
}
void BrowserAccessibilityStateImpl::RefreshAssistiveTechIfNecessary(
ui::AXMode new_mode) {
// Platforms that use this default implementation have a perfect signal
// for screen reader launches. These platforms use AXMode::kScreenReader to
// actively indicate that a screen reader is active.
// Other platforms don't have this perfect signal and compute this off-thread,
// adding/removing AXMode::kScreenReader after detection is complete.
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) {
RefreshAssistiveTech();
}
}
ui::AssistiveTech BrowserAccessibilityStateImpl::ActiveAssistiveTech() const {
return ax_platform_.active_assistive_tech();
}
void BrowserAccessibilityStateImpl::SetPerformanceFilteringAllowed(
bool allowed) {
performance_filtering_allowed_ = allowed;
}
bool BrowserAccessibilityStateImpl::IsPerformanceFilteringAllowed() {
return performance_filtering_allowed_;
}
void BrowserAccessibilityStateImpl::UpdateAccessibilityActivityTask() {
if (!g_instance) {
// There can be a race on shutdown since this is posted as a delayed task.
return;
}
base::TimeTicks now = ui::EventTimeForNow();
accessibility_last_usage_time_ = now;
if (accessibility_active_start_time_.is_null()) {
accessibility_active_start_time_ = now;
}
// If accessibility was enabled but inactive until now, log the amount
// of time between now and the last API usage.
if (!accessibility_inactive_start_time_.is_null()) {
base::UmaHistogramLongTimes("Accessibility.InactiveTime",
now - accessibility_inactive_start_time_);
accessibility_inactive_start_time_ = base::TimeTicks();
}
accessibility_update_task_pending_ = false;
}
ui::AXMode BrowserAccessibilityStateImpl::GetAccessibilityMode() {
return scoped_modes_for_process_.accessibility_mode();
}
ui::AXMode BrowserAccessibilityStateImpl::GetAccessibilityModeForBrowserContext(
BrowserContext* browser_context) {
return FilterAccessibilityModeInvariants(
GetAccessibilityMode() |
ModeCollectionForTarget::GetAccessibilityMode(browser_context));
}
bool BrowserAccessibilityStateImpl::ShouldBlockAutoDisable() {
// This condition should only occur if a known assistive tech is active.
// * If the assistive tech is actually still active, it indicates an error
// with the heuristic, and we should notify a histogram so that we can
// gather data and improve the heuristic's logic, as well as block the auto
// disable from occurring.
// * If the assistive tech is no longer active, then it has been unloaded
// and it is fine to auto-disable.
// Reaching here should be a rare case, and therefore we call the 'slow'
// code (uses system calls on Windows/Linux) to update the running active
// assistive tech state, before we make a determination.
return ActiveAssistiveTech() != ui::AssistiveTech::kNone;
}
void BrowserAccessibilityStateImpl::OnUserInputEvent() {
// No need to do anything if accessibility is off, or if it was forced on.
if (GetAccessibilityMode().is_mode_off() || !allow_ax_mode_changes_) {
return;
}
// If we get at least kAutoDisableAccessibilityEventCount user input
// events, more than kAutoDisableAccessibilityTimeSecs apart, with
// no accessibility API usage in-between disable accessibility.
// (See also OnAccessibilityApiUsage()).
// TODO(accessibility) This heuristic will possibly be removed because it's
// easy for user input events to occur without causing any changes to the
// a11y tree, or firing any events that an assistive tech would process.
// However, we should also consider whether to use this heuristic in addition
// to the focus/load complete one. Some categories of AT don't listen to focus
// or load complete either e.g. Select to Speak. It may not be necessary for
// Select-To-Speak to block auto disable if the disabling is lazy, e.g. on
// next page load and just for this WebContents.
base::TimeTicks now = ui::EventTimeForNow();
user_input_event_count_++;
if (user_input_event_count_ == 1) {
first_user_input_event_time_ = now;
return;
}
if (user_input_event_count_ < kAutoDisableAccessibilityEventCount) {
return;
}
if (ShouldBlockAutoDisable()) {
base::UmaHistogramEnumeration(
"Accessibility.AutoDisabled.BlockedAfter.UserInput",
ActiveAssistiveTech());
return;
}
if (now - first_user_input_event_time_ >
base::Seconds(kAutoDisableAccessibilityTimeSecs)) {
if (!accessibility_active_start_time_.is_null()) {
base::UmaHistogramLongTimes(
"Accessibility.ActiveTime",
accessibility_last_usage_time_ - accessibility_active_start_time_);
// This will help track the time accessibility spends enabled, but
// inactive.
if (!features::IsAutoDisableAccessibilityEnabled()) {
accessibility_inactive_start_time_ = accessibility_last_usage_time_;
}
accessibility_active_start_time_ = base::TimeTicks();
}
// Check if the feature to auto-disable accessibility is even enabled.
if (features::IsAutoDisableAccessibilityEnabled()) {
base::UmaHistogramCounts1000("Accessibility.AutoDisabled.EventCount",
user_input_event_count_);
DCHECK(!accessibility_enabled_time_.is_null());
base::UmaHistogramLongTimes("Accessibility.AutoDisabled.EnabledTime",
now - accessibility_enabled_time_);
accessibility_disabled_time_ = now;
// TODO(accessibility) Reimplement by making a11y dormant as opposed to
// turning off flags, which leads to thrashing.
}
}
}
void BrowserAccessibilityStateImpl::SetAXModeChangeAllowed(bool allowed) {
allow_ax_mode_changes_ = allowed;
ui::AXPlatformNode::SetAXModeChangeAllowed(allowed);
}
bool BrowserAccessibilityStateImpl::IsAXModeChangeAllowed() const {
return allow_ax_mode_changes_;
}
void BrowserAccessibilityStateImpl::SetActivationFromPlatformEnabled(
bool enabled) {
if (activation_from_platform_enabled_ == enabled) {
return;
}
activation_from_platform_enabled_ = enabled;
scoped_modes_for_process_.Recompute(MakePassKey());
}
bool BrowserAccessibilityStateImpl::IsActivationFromPlatformEnabled() {
return activation_from_platform_enabled_;
}
bool BrowserAccessibilityStateImpl::
IsAccessibilityPerformanceMeasurementExperimentActive() const {
return experiment_accessibility_mode_.get();
}
void BrowserAccessibilityStateImpl::NotifyWebContentsPreferencesChanged()
const {
for (WebContentsImpl* wc : WebContentsImpl::GetAllWebContents()) {
wc->OnWebPreferencesChanged();
}
}
base::CallbackListSubscription
BrowserAccessibilityStateImpl::RegisterFocusChangedCallback(
FocusChangedCallback callback) {
return focus_changed_callbacks_.Add(std::move(callback));
}
void BrowserAccessibilityStateImpl::EnableAXModeFromPlatform(
ui::AXMode modes_to_add) {
ui::AXMode old_mode = platform_ax_mode_->mode();
ui::AXMode new_mode = old_mode | modes_to_add;
if (old_mode != new_mode) {
platform_ax_mode_ =
CreateScopedModeForProcess(new_mode | ui::AXMode::kFromPlatform);
}
// If AXMode::kWebContent is being requested, turn off auto-disable.
// TODO(accessibility) Re-work the auto-disable feature.
// Platform accessibility API usage affects auto-disable.
// See OnUserInputEvent for how this is used to disable accessibility.
user_input_event_count_ = 0;
// See comment above kOnAccessibilityUsageUpdateDelaySecs for why we post a
// delayed task.
if (!accessibility_update_task_pending_) {
accessibility_update_task_pending_ = true;
GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&BrowserAccessibilityStateImpl::UpdateAccessibilityActivityTask,
base::Unretained(this)),
base::Seconds(kOnAccessibilityUsageUpdateDelaySecs));
}
}
void BrowserAccessibilityStateImpl::OnMinimalPropertiesUsed() {
// When only basic minimal functionality is used, just enable kNativeAPIs.
// Enabling kNativeAPIs gives little perf impact, but allows these APIs to
// interact with the BrowserAccessibilityManager allowing ATs to be able at
// least find the document without using any advanced APIs.
EnableAXModeFromPlatform(ui::AXMode::kNativeAPIs);
}
void BrowserAccessibilityStateImpl::OnPropertiesUsedInBrowserUI() {
EnableAXModeFromPlatform(ui::AXMode::kNativeAPIs);
}
void BrowserAccessibilityStateImpl::OnPropertiesUsedInWebContent() {
// When accessibility APIs have been used in content, enable basic web
// accessibility support. Full screen reader support is detected later when
// specific more advanced APIs are accessed.
EnableAXModeFromPlatform(ui::kAXModeBasic);
}
void BrowserAccessibilityStateImpl::OnInlineTextBoxesUsedInWebContent() {
EnableAXModeFromPlatform(ui::kAXModeBasic | ui::AXMode::kInlineTextBoxes);
}
void BrowserAccessibilityStateImpl::OnExtendedPropertiesUsedInWebContent() {
EnableAXModeFromPlatform(ui::kAXModeBasic | ui::AXMode::kExtendedProperties);
}
void BrowserAccessibilityStateImpl::OnHTMLAttributesUsed() {
EnableAXModeFromPlatform(ui::kAXModeBasic | ui::AXMode::kHTML);
}
void BrowserAccessibilityStateImpl::OnActionFromAssistiveTech() {
// See OnUserInputEvent for how this is used to disable accessibility.
user_input_event_count_ = 0;
if (has_recently_checked_for_screen_reader_) {
return;
}
has_recently_checked_for_screen_reader_ = true;
// Some platforms might not perfectly signal when an assistive technology (AT)
// is active, and may re-check for ATs after the AXMode
// changes. If the AXMode is already configured with `kAXModeComplete`
// (meaning all accessibility features are enabled), a new AT trying to access
// APIs won't cause a mode change because those flags are already present.
// This check allows for an AT to be detected and computed when a page loads
// and the AT requests an action, specifically in the rare scenario where the
// current mode is `ui::kAXModeComplete` but no AT has been identified yet.
// This ensures the algorithm is complete.
const bool has_ax_mode_complete =
(GetAccessibilityMode() & ui::kAXModeComplete) == ui::kAXModeComplete;
if (has_ax_mode_complete && !ax_platform_.IsScreenReaderActive()) {
if (discover_at_callback_for_testing_) {
discover_at_callback_for_testing_.Run();
return;
}
RefreshAssistiveTechIfNecessary(GetAccessibilityMode());
}
}
void BrowserAccessibilityStateImpl::OnPageNavigationComplete() {
++num_page_navs_before_first_use_;
has_recently_checked_for_screen_reader_ = false;
}
void BrowserAccessibilityStateImpl::OnWebContentsInitialized(
WebContentsImpl* web_contents) {
const ui::AXMode effective_mode = FilterAccessibilityModeInvariants(
GetAccessibilityMode() |
ModeCollectionForTarget::GetAccessibilityMode(
web_contents->GetBrowserContext()) |
ui::AXMode());
// Return early to avoid activating the field trial when accessibility is not
// enabled.
if (effective_mode.is_mode_off()) {
return;
}
// Do not set any initial accessibility mode if ProgressiveAccessibility is
// enabled and the WebContents is initially hidden. This behavior is the same
// for both the only_enable and disable_on_hide variants of the feature.
if (web_contents->GetVisibility() == Visibility::HIDDEN &&
base::FeatureList::IsEnabled(features::kProgressiveAccessibility)) {
return;
}
web_contents->SetAccessibilityMode(effective_mode);
}
void BrowserAccessibilityStateImpl::OnWebContentsRevealed(
WebContentsImpl* web_contents) {
// Unconditionally cancel the disabler; even if the "disable_on_hide" mode is
// not selected. Do this without first checking the feature to avoid
// activating the field trial when it's not already active. Performing this
// removal when the feature is off is harmless. When the feature is active,
// this removal will call OnDisablerDestroyedForWebContents to remove
// `web_contents` from `last_hidden_` if the disabler has not yet been
// scheduled.
AccessibilityDisabler::Remove(web_contents);
const ui::AXMode effective_mode = FilterAccessibilityModeInvariants(
GetAccessibilityMode() |
ModeCollectionForTarget::GetAccessibilityMode(
web_contents->GetBrowserContext()) |
ModeCollectionForTarget::GetAccessibilityMode(web_contents));
// Return early to avoid activating the field trial when accessibility is not
// enabled.
if (effective_mode == web_contents->GetAccessibilityMode()) {
return;
}
// No special behavior when ProgressiveAccessibility is not enabled.
if (!base::FeatureList::IsEnabled(features::kProgressiveAccessibility)) {
return;
}
// Send the current mode flags to the WebContents and its renderers.
web_contents->SetAccessibilityMode(effective_mode);
}
void BrowserAccessibilityStateImpl::OnWebContentsHidden(
WebContentsImpl* web_contents) {
// Return early to avoid activating the field trial when accessibility is not
// enabled.
if (web_contents->GetAccessibilityMode().is_mode_off()) {
return;
}
// No special behavior if ProgressiveAccessibility is not enabled, the
// "disable_on_hide" mode is not selected, or if a screen reader has been
// detected. This final limitation in in place because screen readers may lose
// their "point of regard" if the accessibility tree is destroyed and rebuilt;
// and because functional and fast accessibility is required to serve users of
// screen readers.
if (!base::FeatureList::IsEnabled(features::kProgressiveAccessibility) ||
features::kProgressiveAccessibilityModeParam.Get() !=
features::ProgressiveAccessibilityMode::kDisableOnHide ||
ax_platform_.IsScreenReaderActive()) {
return;
}
// Add `web_contents` to the list of the last five hidden WCs.
CHECK(!base::Contains(last_hidden_, web_contents));
last_hidden_.push_back(web_contents);
// Create the disabler for this WebContents. The provided callback will be run
// if `web_contents` is destroyed before the disabler's `Schedule()` method is
// called. This is the period in which the WebContents is in this instance's
// `last_hidden_` collection. `Unretained` is safe here because this instance
// outlives all WebContents.
AccessibilityDisabler::CreateForWebContents(
web_contents,
base::BindOnce(
&BrowserAccessibilityStateImpl::OnDisablerDestroyedForWebContents,
base::Unretained(this)));
// If there was a sixth, schedule it for dropping 5m after it was hidden.
if (last_hidden_.size() > kMaxPreservedWebContents) {
AccessibilityDisabler::Schedule(last_hidden_.front().get());
last_hidden_.pop_front();
}
}
void BrowserAccessibilityStateImpl::OnDisablerDestroyedForWebContents(
WebContentsImpl* web_contents) {
// Remove `web_contents` from the list of last five hidden WCs.
CHECK(std::erase(last_hidden_, web_contents));
}
void BrowserAccessibilityStateImpl::OnInputEvent(
const RenderWidgetHost& widget,
const blink::WebInputEvent& event) {
// |this| observer cares about user input events (specifically keyboard,
// mouse & touch events) to decide if the accessibility APIs can be disabled.
if (event.GetType() == blink::WebInputEvent::Type::kMouseDown ||
event.GetType() == blink::WebInputEvent::Type::kGestureTapDown ||
event.GetType() == blink::WebInputEvent::Type::kTouchStart ||
event.GetType() == blink::WebInputEvent::Type::kRawKeyDown ||
event.GetType() == blink::WebInputEvent::Type::kKeyDown) {
OnUserInputEvent();
}
}
std::unique_ptr<ScopedAccessibilityMode>
BrowserAccessibilityStateImpl::CreateScopedModeForProcess(ui::AXMode mode) {
auto scoped_mode_for_process = scoped_modes_for_process_.Add(mode);
if (IsAccessibilityPerformanceMeasurementExperimentActive() &&
IsAXModeConflictingWithExperiment(mode)) {
// A new mode is being added while the performance experiment is
// running, which indicates that user is turning on accessibility features.
// The experiment is stopped by posting a task to avoid
// synchronous destruction, which could be problematic if an accessibility
// service is currently in a callstack that's using the accessibility tree
// that this class might modify or destroy during cleanup.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&BrowserAccessibilityStateImpl::ExitPerformanceExperiment,
base::Unretained(this)));
}
return scoped_mode_for_process;
}
void BrowserAccessibilityStateImpl::ApplyAccessibilityModeToWebContents(
WebContentsImpl* web_contents,
ui::AXMode process_mode,
ui::AXMode browser_context_mode,
ui::AXMode web_contents_mode) {
const ui::AXMode effective_mode = FilterAccessibilityModeInvariants(
process_mode | browser_context_mode | web_contents_mode);
// Nothing to do if no change in the WebContents's accessibility mode.
if (effective_mode == web_contents->GetAccessibilityMode()) {
return;
}
// Unconditionally update the WebContents when turning accessibility off.
// TODO(accessibility): If there is evidence of jank induced by accessibility
// being turned off for all WebContentses at once (e.g., if VoiceOver is
// turned off), consider putting WCs in a queue (maybe only hidden ones) and
// sending the empty effective mode one at a time with some delay between
// each.
if (effective_mode.is_mode_off()) {
web_contents->SetAccessibilityMode(effective_mode);
return;
}
// Unconditionally update the WebContents if ProgressiveAccessibility is not
// enabled.
if (!base::FeatureList::IsEnabled(features::kProgressiveAccessibility)) {
web_contents->SetAccessibilityMode(effective_mode);
return;
}
// Only update the WebContents if it is not hidden.
if (web_contents->GetVisibility() != Visibility::HIDDEN) {
web_contents->SetAccessibilityMode(effective_mode);
} // else the WebContents will be updated when it is revealed.
}
// This ScopedModeCollection::Delegate override is called by
// scoped_modes_for_process_ when the effective mode for the collection of
// scopers targeting the process changes.
void BrowserAccessibilityStateImpl::OnModeChanged(ui::AXMode old_mode,
ui::AXMode new_mode) {
ui::RecordAccessibilityModeHistograms(ui::AXHistogramPrefix::kNone, new_mode,
old_mode);
// Track the time since start-up before the kWebContents mode was enabled,
// ensuring we record this value only one time.
if (!has_enabled_accessibility_in_session_ &&
new_mode.has_mode(ui::AXMode::kWebContents)) {
has_enabled_accessibility_in_session_ = true;
UMA_HISTOGRAM_LONG_TIMES_100("Accessibility.EngineUse.TimeUntilStart",
first_use_timer_.Elapsed());
UMA_HISTOGRAM_COUNTS_10000("Accessibility.EngineUse.PageNavsUntilStart",
num_page_navs_before_first_use_);
}
RefreshAssistiveTechIfNecessary(new_mode);
// Add a crash key with the ax_mode, to enable searching for top crashes that
// occur when accessibility is turned on. This adds it for the browser
// process, and elsewhere the same key is added to renderer processes.
static auto* const ax_mode_crash_key = base::debug::AllocateCrashKeyString(
"ax_mode", base::debug::CrashKeySize::Size64);
if (ax_mode_crash_key) {
base::debug::SetCrashKeyString(ax_mode_crash_key, new_mode.ToString());
}
// Combine the new mode for the process with the effective mode for each
// WebContents and its associated BrowserContext.
std::ranges::for_each(
WebContentsImpl::GetAllWebContents(),
[this, new_mode](WebContentsImpl* web_contents) {
if (!web_contents->IsBeingDestroyed() &&
!web_contents->IsNeverComposited()) {
ApplyAccessibilityModeToWebContents(
web_contents, new_mode,
ModeCollectionForTarget::GetAccessibilityMode(
web_contents->GetBrowserContext()),
ModeCollectionForTarget::GetAccessibilityMode(web_contents));
}
});
// Handle additions to the process's mode flags.
if (const auto additions = new_mode & ~old_mode; !additions.is_mode_off()) {
// Keep track of the total time accessibility is enabled, and the time
// it was previously disabled.
if (old_mode.is_mode_off()) {
base::TimeTicks now = ui::EventTimeForNow();
accessibility_enabled_time_ = now;
if (!accessibility_disabled_time_.is_null()) {
base::UmaHistogramLongTimes("Accessibility.AutoDisabled.DisabledTime",
now - accessibility_disabled_time_);
}
}
// Broadcast the new mode flags, if any, to the AXModeObservers.
ax_platform_.NotifyModeAdded(additions);
}
}
// This ScopedModeCollection::Delegate override is called by
// scoped_modes_for_process_ when recomputing the effective mode for the
// collection of scopers targeting the process.
ui::AXMode BrowserAccessibilityStateImpl::FilterModeFlags(ui::AXMode mode) {
if (activation_from_platform_enabled_) {
// Allow mode changes with `kFromPlatform`, but filter out that one bit.
// It need not be sent to renderers.
return mode & ~ui::AXMode(ui::AXMode::kFromPlatform);
}
// Otherwise, ignore any mode change with `kFromPlatform`.
return mode.has_mode(ui::AXMode::kFromPlatform) ? ui::AXMode() : mode;
}
std::unique_ptr<ScopedAccessibilityMode>
BrowserAccessibilityStateImpl::CreateScopedModeForBrowserContext(
BrowserContext* browser_context,
ui::AXMode mode) {
// kFromPlatform is only permissible for process-wide scopers.
CHECK(!mode.has_mode(ui::AXMode::kFromPlatform));
auto scoped_mode = ModeCollectionForTarget::Add(
browser_context,
&BrowserAccessibilityStateImpl::OnModeChangedForBrowserContext, this,
mode);
if (IsAccessibilityPerformanceMeasurementExperimentActive() &&
IsAXModeConflictingWithExperiment(mode)) {
// A new mode is being added while the performance experiment is
// running, which indicates that user is turning on accessibility features.
// The experiment is stopped by posting a task to avoid
// synchronous destruction, which could be problematic if an accessibility
// service is currently in a callstack that's using the accessibility tree
// that this class might modify or destroy during cleanup.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&BrowserAccessibilityStateImpl::ExitPerformanceExperiment,
base::Unretained(this)));
}
return scoped_mode;
}
void BrowserAccessibilityStateImpl::OnModeChangedForBrowserContext(
BrowserContext* browser_context,
ui::AXMode old_mode,
ui::AXMode new_mode) {
// Combine this with the effective mode for each WebContents associated with
// `browser_context`.
std::ranges::for_each(
WebContentsImpl::GetAllWebContents(),
[this, browser_context, process_mode = GetAccessibilityMode(),
new_mode](WebContentsImpl* web_contents) {
if (!web_contents->IsBeingDestroyed() &&
!web_contents->IsNeverComposited() &&
web_contents->GetBrowserContext() == browser_context) {
ApplyAccessibilityModeToWebContents(
web_contents, process_mode, new_mode,
ModeCollectionForTarget::GetAccessibilityMode(web_contents));
}
});
}
std::unique_ptr<ScopedAccessibilityMode>
BrowserAccessibilityStateImpl::CreateScopedModeForWebContents(
WebContents* web_contents,
ui::AXMode mode) {
// WebContents that are never shown must never have accessibility enabled.
CHECK(!web_contents->IsNeverComposited());
// kFromPlatform is only permissible for process-wide scopers.
CHECK(!mode.has_mode(ui::AXMode::kFromPlatform));
auto scoped_mode = ModeCollectionForTarget::Add(
web_contents, &BrowserAccessibilityStateImpl::OnModeChangedForWebContents,
this, mode);
if (IsAccessibilityPerformanceMeasurementExperimentActive() &&
IsAXModeConflictingWithExperiment(mode)) {
// A new mode is being added while the performance experiment is
// running, which indicates that user is turning on accessibility features.
// The experiment is stopped by posting a task to avoid
// synchronous destruction, which could be problematic if an accessibility
// service is currently in a callstack that's using the accessibility tree
// that this class might modify or destroy during cleanup.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&BrowserAccessibilityStateImpl::ExitPerformanceExperiment,
base::Unretained(this)));
}
return scoped_mode;
}
void BrowserAccessibilityStateImpl::OnModeChangedForWebContents(
WebContents* web_contents,
ui::AXMode old_mode,
ui::AXMode new_mode) {
if (web_contents->IsBeingDestroyed()) {
return;
}
// Combine the effective modes for the process, `web_contents`'s
// BrowserContext, and for `web_contents.
ApplyAccessibilityModeToWebContents(
static_cast<WebContentsImpl*>(web_contents), GetAccessibilityMode(),
ModeCollectionForTarget::GetAccessibilityMode(
web_contents->GetBrowserContext()),
new_mode);
}
void BrowserAccessibilityStateImpl::OnFocusChangedInPage(
const FocusedNodeDetails& details) {
focus_changed_callbacks_.Notify(details);
}
void BrowserAccessibilityStateImpl::ExitPerformanceExperiment() {
experiment_accessibility_mode_.reset();
if (features::GetAccessibilityPerformanceMeasurementExperimentGroup() !=
features::AccessibilityPerformanceMeasurementExperimentGroup::
kRendererSerializationOnly) {
return;
}
RenderAccessibilityHost::SetRendererSerializationExperimentEnabled(false);
if (BrowserAccessibilityState::GetInstance()->GetAccessibilityMode().has_mode(
ui::AXMode::kWebContents)) {
// If this experiment variant was discarding incoming accessibility
// events,
// and the accessibility mode still includes `ui::AXMode::kWebContents`
// after the experiment shutdown, force a reset on all WebContents.
// This ensures they rebuild the full accessibility tree.
std::ranges::for_each(WebContentsImpl::GetAllWebContents(),
[](WebContentsImpl* web_contents) {
if (!web_contents->IsBeingDestroyed() &&
!web_contents->IsNeverComposited()) {
web_contents->ResetAccessibility();
}
});
}
}
} // namespace content