| // 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 |