| // 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; |
| |
| |
| |
| // 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 ProgressiveAccessibilityPhase2 |
| // feature 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); |
| } |
| |
| // If the native adapted mode is requested, and native platform APIs are |
| // active, automatically upgrade it to include kWebContents. This ensures |
| // adapted views (like Webium's toolbar) are accessible to any native client |
| // querying the UI, while keeping accessibility off (zero performance cost) |
| // when no client is active. |
| if (mode.has_mode(ui::AXMode::kNativeAdaptedWebContents) && |
| mode.has_mode(ui::AXMode::kNativeAPIs)) { |
| mode.set_mode(ui::AXMode::kWebContents, true); |
| } |
| |
| // 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::RandIntInclusive( |
| -kDisableDelayVarianceSeconds, kDisableDelayVarianceSeconds)); |
| return kDisableDelay + variance; |
| } |
| |
| // static |
| base::TimeDelta BrowserAccessibilityStateImpl::GetMaxDisableDelay() { |
| return kDisableDelay + base::Seconds(kDisableDelayVarianceSeconds); |
| } |
| |
| BrowserAccessibilityStateImpl::BrowserAccessibilityStateImpl() |
| : platform_ax_mode_(CreateScopedModeForProcess(ui::AXMode())) { |
| CHECK_EQ(g_instance, nullptr, base::NotFatalUntil::M159); |
| 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() { |
| CHECK_EQ(g_instance, this, base::NotFatalUntil::M159); |
| 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 kProgressiveAccessibilityPhase2 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(); |
| } |
| } |
| |
| void BrowserAccessibilityStateImpl::RecordPlatformClientHistograms( |
| ui::AXMode old_mode, |
| ui::AXMode new_mode) {} |
| |
| 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_; |
| } |
| |
| |
| 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)); |
| } |
| |
| |
| |
| void BrowserAccessibilityStateImpl::SetAXModeChangeAllowed(bool allowed) { |
| if (allow_ax_mode_changes_ == allowed) { |
| return; |
| } |
| allow_ax_mode_changes_ = allowed; |
| scoped_modes_for_process_.Recompute(MakePassKey()); |
| } |
| |
| 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); |
| } |
| } |
| |
| 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() { |
| 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( |
| WebContents* web_contents) { |
| if (web_contents && web_contents->GetAccessibilityMode().has_mode( |
| ui::AXMode::kNativeAdaptedWebContents)) { |
| return; |
| } |
| ++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 the WebContents is initially |
| // hidden. |
| if (web_contents->GetVisibility() == Visibility::HIDDEN) { |
| return; |
| } |
| |
| web_contents->SetAccessibilityMode(effective_mode); |
| } |
| |
| void BrowserAccessibilityStateImpl::OnWebContentsRevealed( |
| WebContentsImpl* web_contents) { |
| // Unconditionally cancel the disabler; even if |
| // kProgressiveAccessibilityPhase2 is not enabled. 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; |
| } |
| |
| // 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 a screen reader has been detected or if |
| // ProgressiveAccessibilityPhase2 is not enabled. The former limitation is 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 (ax_platform_.IsScreenReaderActive() || |
| !base::FeatureList::IsEnabled( |
| features::kProgressiveAccessibilityPhase2)) { |
| return; |
| } |
| |
| // Add `web_contents` to the list of the last five hidden WCs. |
| CHECK(!std::ranges::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)); |
| } |
| |
| |
| 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; |
| } |
| |
| // 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) { |
| // Strip kNativeAdaptedWebContents so it never pollutes process-wide UMA |
| old_mode.set_mode(ui::AXMode::kNativeAdaptedWebContents, false); |
| new_mode.set_mode(ui::AXMode::kNativeAdaptedWebContents, false); |
| |
| ui::RecordAccessibilityModeHistograms(ui::AXHistogramPrefix::kNone, new_mode, |
| old_mode); |
| RecordPlatformClientHistograms(old_mode, new_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()) { |
| // 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_ax_mode_changes_) { |
| // 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 |