blob: 43c74b4a4fa20599c32c4fa503d4e42f969950fa [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// 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_manager.h"
#include <stddef.h>
#include <algorithm>
#include <map>
#include <memory>
#include <utility>
#include "base/auto_reset.h"
#include "base/containers/adapters.h"
#include "base/logging.h"
#include "base/metrics/user_metrics.h"
#include "base/no_destructor.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/mojom/render_accessibility.mojom.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_language_detection.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/ax_tree_serializer.h"
#include "ui/base/buildflags.h"
#if defined(AX_FAIL_FAST_BUILD)
#include "base/command_line.h"
#include "content/public/browser/ax_inspect_factory.h"
#include "content/public/common/content_switches.h"
#endif
namespace content {
namespace {
// A function to call when focus changes, for testing only.
base::LazyInstance<base::RepeatingClosure>::DestructorAtExit
g_focus_change_callback_for_testing = LAZY_INSTANCE_INITIALIZER;
// If 2 or more tree updates can all be merged into others,
// process the whole set of tree updates, copying them to |dst|,
// and returning true. Otherwise, return false and |dst|
// is left unchanged.
//
// Merging tree updates helps minimize the overhead of calling
// Unserialize multiple times.
bool MergeTreeUpdates(const std::vector<ui::AXTreeUpdate>& src,
std::vector<ui::AXTreeUpdate>* dst) {
size_t merge_count = 0;
for (size_t i = 1; i < src.size(); i++) {
if (ui::TreeUpdatesCanBeMerged(src[i - 1], src[i]))
merge_count++;
}
// Doing a single merge isn't necessarily worth it because
// copying the tree updates takes time too so the total
// savings is less. But two more more merges is probably
// worth the overhead of copying.
if (merge_count < 2)
return false;
dst->resize(src.size() - merge_count);
(*dst)[0] = src[0];
size_t dst_index = 0;
for (size_t i = 1; i < src.size(); i++) {
if (ui::TreeUpdatesCanBeMerged(src[i - 1], src[i])) {
std::vector<ui::AXNodeData>& dst_nodes = (*dst)[dst_index].nodes;
const std::vector<ui::AXNodeData>& src_nodes = src[i].nodes;
dst_nodes.insert(dst_nodes.end(), src_nodes.begin(), src_nodes.end());
} else {
dst_index++;
(*dst)[dst_index] = src[i];
}
}
return true;
}
} // namespace
ui::AXTreeUpdate MakeAXTreeUpdate(
const ui::AXNodeData& node1,
const ui::AXNodeData& node2 /* = ui::AXNodeData() */,
const ui::AXNodeData& node3 /* = ui::AXNodeData() */,
const ui::AXNodeData& node4 /* = ui::AXNodeData() */,
const ui::AXNodeData& node5 /* = ui::AXNodeData() */,
const ui::AXNodeData& node6 /* = ui::AXNodeData() */,
const ui::AXNodeData& node7 /* = ui::AXNodeData() */,
const ui::AXNodeData& node8 /* = ui::AXNodeData() */,
const ui::AXNodeData& node9 /* = ui::AXNodeData() */,
const ui::AXNodeData& node10 /* = ui::AXNodeData() */,
const ui::AXNodeData& node11 /* = ui::AXNodeData() */,
const ui::AXNodeData& node12 /* = ui::AXNodeData() */,
const ui::AXNodeData& node13 /* = ui::AXNodeData() */,
const ui::AXNodeData& node14 /* = ui::AXNodeData() */) {
static base::NoDestructor<ui::AXNodeData> empty_data;
int32_t no_id = empty_data->id;
ui::AXTreeUpdate update;
ui::AXTreeData tree_data;
tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
tree_data.focused_tree_id = tree_data.tree_id;
tree_data.parent_tree_id = ui::AXTreeIDUnknown();
update.tree_data = tree_data;
update.has_tree_data = true;
update.root_id = node1.id;
update.nodes.push_back(node1);
if (node2.id != no_id)
update.nodes.push_back(node2);
if (node3.id != no_id)
update.nodes.push_back(node3);
if (node4.id != no_id)
update.nodes.push_back(node4);
if (node5.id != no_id)
update.nodes.push_back(node5);
if (node6.id != no_id)
update.nodes.push_back(node6);
if (node7.id != no_id)
update.nodes.push_back(node7);
if (node8.id != no_id)
update.nodes.push_back(node8);
if (node9.id != no_id)
update.nodes.push_back(node9);
if (node10.id != no_id)
update.nodes.push_back(node10);
if (node11.id != no_id)
update.nodes.push_back(node11);
if (node12.id != no_id)
update.nodes.push_back(node12);
if (node13.id != no_id)
update.nodes.push_back(node13);
if (node14.id != no_id)
update.nodes.push_back(node14);
return update;
}
BrowserAccessibilityFindInPageInfo::BrowserAccessibilityFindInPageInfo()
: request_id(-1),
match_index(-1),
start_id(-1),
start_offset(0),
end_id(-1),
end_offset(-1),
active_request_id(-1) {}
#if !BUILDFLAG(HAS_PLATFORM_ACCESSIBILITY_SUPPORT)
// static
BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
const ui::AXTreeUpdate& initial_tree,
BrowserAccessibilityDelegate* delegate) {
return new BrowserAccessibilityManager(initial_tree, delegate);
}
// static
BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
BrowserAccessibilityDelegate* delegate) {
return new BrowserAccessibilityManager(
BrowserAccessibilityManager::GetEmptyDocument(), delegate);
}
#endif
// static
BrowserAccessibilityManager* BrowserAccessibilityManager::FromID(
ui::AXTreeID ax_tree_id) {
DCHECK(ax_tree_id != ui::AXTreeIDUnknown());
return static_cast<BrowserAccessibilityManager*>(
ui::AXTreeManager::FromID(ax_tree_id));
}
BrowserAccessibilityManager::BrowserAccessibilityManager(
BrowserAccessibilityDelegate* delegate)
: AXPlatformTreeManager(ui::AXTreeIDUnknown(),
std::make_unique<ui::AXSerializableTree>()),
WebContentsObserver(delegate
? WebContents::FromRenderFrameHost(
delegate->AccessibilityRenderFrameHost())
: nullptr),
delegate_(delegate),
user_is_navigating_away_(false),
connected_to_parent_tree_node_(false),
device_scale_factor_(1.0f),
use_custom_device_scale_factor_for_testing_(false),
event_generator_(ax_tree()) {}
BrowserAccessibilityManager::BrowserAccessibilityManager(
const ui::AXTreeUpdate& initial_tree,
BrowserAccessibilityDelegate* delegate)
: AXPlatformTreeManager(ui::AXTreeIDUnknown(),
std::make_unique<ui::AXSerializableTree>()),
WebContentsObserver(delegate
? WebContents::FromRenderFrameHost(
delegate->AccessibilityRenderFrameHost())
: nullptr),
delegate_(delegate),
user_is_navigating_away_(false),
device_scale_factor_(1.0f),
use_custom_device_scale_factor_for_testing_(false),
event_generator_(ax_tree()) {
Initialize(initial_tree);
}
BrowserAccessibilityManager::~BrowserAccessibilityManager() {
// If the root's parent is in another accessibility tree but it wasn't
// previously connected, post the proper notifications on the parent.
BrowserAccessibility* parent = nullptr;
if (connected_to_parent_tree_node_)
parent = GetParentNodeFromParentTree();
// Fire any events that need to be fired when tree nodes get deleted. For
// example, events that fire every time "OnSubtreeWillBeDeleted" is called.
ax_tree()->Destroy();
delegate_ = nullptr; // Guard against reentrancy by screen reader.
if (last_focused_node_tree_id_ &&
ax_tree_id_ == *last_focused_node_tree_id_) {
SetLastFocusedNode(nullptr);
}
RemoveFromMap();
ParentConnectionChanged(parent);
}
void BrowserAccessibilityManager::Initialize(
const ui::AXTreeUpdate& initial_tree) {
if (!ax_tree()->Unserialize(initial_tree)) {
LOG(FATAL) << "No recovery is possible if the initial tree is broken: "
<< ax_tree()->error();
}
}
// A flag for use in tests to ensure events aren't suppressed or delayed.
// static
bool BrowserAccessibilityManager::never_suppress_or_delay_events_for_testing_ =
false;
// A flag to ensure that accessibility fatal errors crash immediately.
bool BrowserAccessibilityManager::is_fail_fast_mode_ = false;
// static
absl::optional<int32_t> BrowserAccessibilityManager::last_focused_node_id_ = {};
// static
absl::optional<ui::AXTreeID>
BrowserAccessibilityManager::last_focused_node_tree_id_ = {};
// static
ui::AXTreeUpdate BrowserAccessibilityManager::GetEmptyDocument() {
ui::AXNodeData empty_document;
empty_document.id = 1;
empty_document.role = ax::mojom::Role::kRootWebArea;
ui::AXTreeUpdate update;
update.root_id = empty_document.id;
update.nodes.push_back(empty_document);
return update;
}
void BrowserAccessibilityManager::FireFocusEventsIfNeeded() {
if (!CanFireEvents())
return;
BrowserAccessibility* focus = GetFocus();
// If |focus| is nullptr it means that we have no way of knowing where the
// focus is.
//
// One case when this would happen is when the current tree hasn't connected
// to its parent tree yet. That would mean that we have no way of getting to
// the top document which holds global focus information for the whole page.
//
// Note that if there is nothing focused on the page, then the focus should
// not be nullptr. The rootnode of the top document should be focused instead.
if (!focus)
return;
// Don't fire focus events if the window itself doesn't have focus.
// Bypass this check for some tests.
if (!never_suppress_or_delay_events_for_testing_ &&
!g_focus_change_callback_for_testing.Get()) {
if (delegate_ && !delegate_->AccessibilityViewHasFocus())
return;
}
// Wait until navigation is complete or stopped, before attempting to move the
// accessibility focus.
if (user_is_navigating_away_)
return;
BrowserAccessibility* last_focused_node = GetLastFocusedNode();
if (focus != last_focused_node)
FireFocusEvent(focus);
SetLastFocusedNode(focus);
}
bool BrowserAccessibilityManager::CanFireEvents() const {
// Delay events until it makes sense to fire them.
// Events that are generated while waiting until CanFireEvents() returns true
// are dropped by design. Any events after the page is ready for events will
// be relative to that initial tree.
// The current tree must have an AXTreeID.
if (ax_tree_id() == ui::AXTreeIDUnknown())
return false;
// Fire events only when the root of the tree is reachable, to avoid a bug
// in AppKit that gets stuck in an infinite loop trying to find the root,
// causing VoiceOver to get stuck announcing "Chrome is not responding".
BrowserAccessibilityManager* root_manager = GetRootManager();
if (!root_manager)
return false;
// Make sure that nodes can be traversed to the root.
const BrowserAccessibilityManager* ancestor_manager = this;
while (!ancestor_manager->IsRootTree()) {
BrowserAccessibility* host_node =
ancestor_manager->GetParentNodeFromParentTree();
if (!host_node)
return false; // Host node not ready yet.
ancestor_manager = host_node->manager();
}
// Do not fire events if a page is obscured by an interstitial page -- see
// crbug.com/730910.
// TODO(accessibility) Look into what happens if an interstitial page is only
// hiding an iframe.
if (root_manager->hidden_by_interstitial_page())
return false;
// Do not fire events when the page is frozen inside the back/forward cache.
// Rationale for the back/forward cache behavior:
// https://docs.google.com/document/d/1_jaEAXurfcvriwcNU-5u0h8GGioh0LelagUIIGFfiuU/
return !delegate_ || // Can be null in unit tests.
!delegate_->AccessibilityRenderFrameHost() ||
!delegate_->AccessibilityRenderFrameHost()->IsInBackForwardCache();
}
BrowserAccessibility* BrowserAccessibilityManager::RetargetForEvents(
BrowserAccessibility* node,
RetargetEventType type) const {
return node;
}
void BrowserAccessibilityManager::FireFocusEvent(BrowserAccessibility* node) {
if (g_focus_change_callback_for_testing.Get())
g_focus_change_callback_for_testing.Get().Run();
}
void BrowserAccessibilityManager::FireGeneratedEvent(
ui::AXEventGenerator::Event event_type,
BrowserAccessibility* node) {
if (!generated_event_callback_for_testing_.is_null()) {
generated_event_callback_for_testing_.Run(delegate(), event_type,
node->GetId());
}
}
BrowserAccessibility* BrowserAccessibilityManager::GetRoot() const {
ui::AXNode* root = GetRootAsAXNode();
return root ? GetFromAXNode(root) : nullptr;
}
BrowserAccessibility* BrowserAccessibilityManager::GetFromAXNode(
const ui::AXNode* node) const {
// TODO(benjamin.beaudry): Consider moving `GetFromId` to
// `AXPlatformTreeManager`.
if (!node)
return nullptr;
if (AXTreeManager* manager = node->GetManager()) {
return static_cast<BrowserAccessibilityManager*>(manager)->GetFromID(
node->id());
}
return GetFromID(node->id());
}
BrowserAccessibility* BrowserAccessibilityManager::GetFromID(int32_t id) const {
const auto iter = id_wrapper_map_.find(id);
if (iter != id_wrapper_map_.end()) {
DCHECK(iter->second);
return iter->second.get();
}
return nullptr;
}
BrowserAccessibility* BrowserAccessibilityManager::GetParentNodeFromParentTree()
const {
ui::AXNode* parent = GetParentNodeFromParentTreeAsAXNode();
if (!parent)
return nullptr;
// TODO(accessibility) Try to remove this redundant lookup. The call to
// GetParentNodeFromParentTreeAsAXNode() already retrieved the parent manager.
BrowserAccessibilityManager* parent_manager = GetParentManager();
DCHECK(parent_manager) << "Impossible to have null parent_manager if we "
"already have a parent AXNode.";
BrowserAccessibility* parent_node = parent_manager->GetFromAXNode(parent);
DCHECK_EQ(parent_node->manager(), parent_manager);
DCHECK_NE(parent_node->manager(), this);
return parent_node;
}
void BrowserAccessibilityManager::ParentConnectionChanged(
BrowserAccessibility* parent) {
if (!parent) {
connected_to_parent_tree_node_ = false;
return;
}
connected_to_parent_tree_node_ = true;
parent->OnDataChanged();
parent->UpdatePlatformAttributes();
BrowserAccessibilityManager* parent_manager = parent->manager();
parent = parent_manager->RetargetForEvents(
parent, RetargetEventType::RetargetEventTypeGenerated);
parent_manager->FireGeneratedEvent(
ui::AXEventGenerator::Event::CHILDREN_CHANGED, parent);
}
void BrowserAccessibilityManager::EnsureParentConnectionIfNotRootManager() {
BrowserAccessibility* parent = GetParentNodeFromParentTree();
if (parent) {
if (!connected_to_parent_tree_node_)
ParentConnectionChanged(parent);
SANITIZER_CHECK(!IsRootTree());
return;
}
if (connected_to_parent_tree_node_) {
connected_to_parent_tree_node_ = false;
// Two possible cases:
// 1. This manager was previously connected to a parent manager but now
// became the new root manager. One example where this can happen is portal
// activation.
// 2. The parent host node for this child tree was removed. Because the
// connection with the root has been severed, it will no longer be possible
// to fire events, as this BrowserAccessibilityManager is no longer tied to
// an existing document. Due to race conditions, in some cases, |this| is
// destroyed first, and this condition is not reached; while in other cases
// the parent node is destroyed first (this case).
DCHECK(IsRootTree() || !CanFireEvents());
}
}
BrowserAccessibility* BrowserAccessibilityManager::GetPopupRoot() const {
DCHECK_LE(popup_root_ids_.size(), 1u);
if (popup_root_ids_.size() == 1) {
BrowserAccessibility* node = GetFromID(*popup_root_ids_.begin());
if (node) {
DCHECK(node->GetRole() == ax::mojom::Role::kRootWebArea);
return node;
}
}
return nullptr;
}
const ui::AXTreeData& BrowserAccessibilityManager::GetTreeData() const {
return ax_tree()->data();
}
void BrowserAccessibilityManager::OnWindowFocused() {
if (IsRootTree())
FireFocusEventsIfNeeded();
}
void BrowserAccessibilityManager::OnWindowBlurred() {
if (IsRootTree())
SetLastFocusedNode(nullptr);
}
void BrowserAccessibilityManager::UserIsNavigatingAway() {
user_is_navigating_away_ = true;
}
void BrowserAccessibilityManager::UserIsReloading() {
user_is_navigating_away_ = true;
}
void BrowserAccessibilityManager::NavigationSucceeded() {
user_is_navigating_away_ = false;
// Do not call FireFocusEventsIfNeeded() yet -- wait until first call
// of OnAccessibilityEvents(), which will occur when kLoadStart is fired from
// the renderer, at which point there will be an AXTreeID().
}
void BrowserAccessibilityManager::NavigationFailed() {
user_is_navigating_away_ = false;
FireFocusEventsIfNeeded();
}
void BrowserAccessibilityManager::DidStopLoading() {
user_is_navigating_away_ = false;
FireFocusEventsIfNeeded();
}
bool BrowserAccessibilityManager::UseRootScrollOffsetsWhenComputingBounds() {
return use_root_scroll_offsets_when_computing_bounds_;
}
void BrowserAccessibilityManager ::
SetUseRootScrollOffsetsWhenComputingBoundsForTesting(bool use) {
use_root_scroll_offsets_when_computing_bounds_ = use;
}
bool BrowserAccessibilityManager::OnAccessibilityEvents(
const AXEventNotificationDetails& details) {
TRACE_EVENT0("accessibility",
"BrowserAccessibilityManager::OnAccessibilityEvents");
#if DCHECK_IS_ON()
base::AutoReset<bool> auto_reset(&in_on_accessibility_events_, true);
#endif // DCHECK_IS_ON()
// Update the cached device scale factor.
if (!use_custom_device_scale_factor_for_testing_)
UpdateDeviceScaleFactor();
// Optionally merge multiple tree updates into fewer updates.
const std::vector<ui::AXTreeUpdate>* tree_updates = &details.updates;
std::vector<ui::AXTreeUpdate> merged_tree_updates;
if (MergeTreeUpdates(details.updates, &merged_tree_updates))
tree_updates = &merged_tree_updates;
// Process all changes to the accessibility tree first.
for (const ui::AXTreeUpdate& tree_update : *tree_updates) {
if (!ax_tree()->Unserialize(tree_update)) {
// This is a fatal error, but if there is a delegate, it will handle the
// error result and recover by re-creating the manager. After a max
// threshold number of errors is reached, it will crash the browser.
if (!delegate_)
CHECK(false) << ax_tree()->error();
return false;
}
// It's a bug if we got an update containing more nodes than
// the size of the resulting tree. If Unserialize succeeded that
// means a node just got repeated or something harmless like that,
// but it should still be investigated and could be the sign of a
// performance issue.
DCHECK_LE(static_cast<int>(tree_update.nodes.size()), ax_tree()->size());
}
EnsureParentConnectionIfNotRootManager();
if (!CanFireEvents()) {
// TODO(accessibility) Change AXEventGenerator() to avoid doing any work
// and avoid queuing any events when CanFireEvents() is false.
for (const ui::AXEvent& event : details.events)
if (event.event_type != ax::mojom::Event::kLoadComplete)
defer_load_complete_event_ = true;
event_generator().ClearEvents();
return true;
}
BrowserAccessibilityManager* root_manager = GetRootManager();
DCHECK(root_manager) << "Cannot have detached document here, as "
"CanFireEvents() must return false in that case.";
#if defined(AX_FAIL_FAST_BUILD)
ui::AXTreeID parent_id = GetParentTreeID();
bool has_parent_id = parent_id != ui::AXTreeIDUnknown();
BrowserAccessibilityManager* parent_manager =
has_parent_id ? BrowserAccessibilityManager::FromID(parent_id) : nullptr;
if (IsRootTree()) {
CHECK(!has_parent_id) << "The root frame must be parentless, root url = "
<< GetTreeData().url << "\nSupposed parent = "
<< (parent_manager
? parent_manager->GetTreeData().url
: "[not in map for parent_tree_id]");
CHECK(!connected_to_parent_tree_node_)
<< "Root manager must not be connected to a parent tree node.";
} else {
CHECK(parent_manager) << "Non-root trees must have a parent manager to "
"reach this code, otherwise CanFireEvents() "
"should have returned false, has_parent_id = "
<< has_parent_id
<< "\nCurrent url = " << GetTreeData().url;
CHECK(connected_to_parent_tree_node_)
<< "Must be connected to parent tree node, otherwise could not reach "
"here, due to CanFireEvents() check above.";
}
#endif
// Allow derived classes to do event pre-processing.
BeforeAccessibilityEvents();
bool received_load_complete_event = false;
// If an earlier load complete event was suppressed, fire it now.
if (defer_load_complete_event_) {
received_load_complete_event = true;
defer_load_complete_event_ = false;
FireBlinkEvent(ax::mojom::Event::kLoadComplete, GetRoot(), -1);
}
// Fire any events related to changes to the tree that come from ancestors of
// the currently-focused node. We do this so that screen readers are made
// aware of changes in the tree which might be relevant to subsequent events
// on the focused node, such as the focused node being a descendant of a
// reparented node or a newly-shown dialog box.
BrowserAccessibility* focus = GetFocus();
std::vector<ui::AXEventGenerator::TargetedEvent> deferred_events;
for (const auto& targeted_event : event_generator()) {
BrowserAccessibility* event_target = GetFromID(targeted_event.node_id);
DCHECK(event_target) << "No event target for " << targeted_event.node_id;
event_target = RetargetForEvents(
event_target, RetargetEventType::RetargetEventTypeGenerated);
if (!event_target)
continue; // Drop the event if RetargetForEvents() returns nullptr.
if (!event_target->CanFireEvents())
continue;
// IsDescendantOf() also returns true in the case of equality.
if (focus && focus != event_target && focus->IsDescendantOf(event_target))
FireGeneratedEvent(targeted_event.event_params.event, event_target);
else
deferred_events.push_back(targeted_event);
}
// Screen readers might not process events related to the currently-focused
// node if they are not aware that node is now focused, so fire a focus event
// before firing any other events on that node. No focus event will be fired
// if the window itself isn't focused or if focus hasn't changed.
//
// We need to fire focus events specifically from the root manager, since we
// need the top document's delegate to check if its view has focus.
//
// If this manager is disconnected from the top document, then root_manager
// will be a null pointer and this code will not be reached.
root_manager->FireFocusEventsIfNeeded();
// Now fire all of the rest of the generated events we previously deferred.
for (const auto& targeted_event : deferred_events) {
BrowserAccessibility* event_target = GetFromID(targeted_event.node_id);
DCHECK(event_target) << "No event target for " << targeted_event.node_id;
event_target = RetargetForEvents(
event_target, RetargetEventType::RetargetEventTypeGenerated);
if (!event_target)
continue; // Drop the event if RetargetForEvents() returns nullptr.
if (!event_target->CanFireEvents())
continue;
FireGeneratedEvent(targeted_event.event_params.event, event_target);
}
event_generator().ClearEvents();
// Fire events from Blink.
for (const ui::AXEvent& event : details.events) {
// Fire the native event.
BrowserAccessibility* event_target = GetFromID(event.id);
DCHECK(event_target) << "No event target for " << event.id
<< " with event type " << event.event_type;
RetargetEventType type =
event.event_type == ax::mojom::Event::kHover
? RetargetEventType::RetargetEventTypeBlinkHover
: RetargetEventType::RetargetEventTypeBlinkGeneral;
BrowserAccessibility* retargeted = RetargetForEvents(event_target, type);
if (!retargeted)
continue; // Drop the event if RetargetForEvents() returns nullptr.
if (!retargeted->CanFireEvents())
continue;
if (event.event_type == ax::mojom::Event::kHover)
root_manager->CacheHitTestResult(event_target);
if (event.event_type == ax::mojom::Event::kLoadComplete) {
DCHECK_EQ(event_target, GetRoot());
DCHECK(event_target->IsPlatformDocument());
received_load_complete_event = true;
}
if (event.event_type == ax::mojom::Event::kLoadStart) {
DCHECK_EQ(event_target, GetRoot());
DCHECK(event_target->IsPlatformDocument());
// If we already have a load-complete event, the load-start event is no
// longer relevant. In addition, some code checks for the presence of
// the "busy" state when firing a platform load-start event. If the page
// is no longer loading, this state will have been removed and the check
// will fail.
if (received_load_complete_event)
continue; // Skip firing load start event.
}
FireBlinkEvent(event.event_type, retargeted, event.action_request_id);
}
if (received_load_complete_event) {
// Fire a focus event after the document has finished loading, but after all
// the platform independent events have already fired, e.g. kLayoutComplete.
// Some screen readers need a focus event in order to work properly.
FireFocusEventsIfNeeded();
// Perform the initial run of language detection.
ax_tree()->language_detection_manager->DetectLanguages();
ax_tree()->language_detection_manager->LabelLanguages();
// After initial language detection, enable language detection for future
// content updates in order to support dynamic content changes.
//
// If the LanguageDetectionDynamic feature flag is not enabled then this
// is a no-op.
ax_tree()->language_detection_manager->RegisterLanguageDetectionObserver();
}
// Allow derived classes to do event post-processing.
FinalizeAccessibilityEvents();
#if defined(AX_FAIL_FAST_BUILD)
// When running a debugging/sanitizer build with
// --force-renderer-accessibility, exercise the properties for every node, to
// ensure no crashes or assertions are triggered. This helpfully runs for all
// web tests on builder linux-blink-web-tests-force-accessibility-rel, as well
// as for some clusterfuzz runs.
static int g_max_ax_tree_exercise_iterations = 3; // Avoid timeouts.
static int count = 0;
if (GetRoot()->GetChildCount() > 0 &&
!GetRoot()->GetBoolAttribute(ax::mojom::BoolAttribute::kBusy) &&
++count <= g_max_ax_tree_exercise_iterations) {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(::switches::kForceRendererAccessibility)) {
std::unique_ptr<ui::AXTreeFormatter> formatter(
AXInspectFactory::CreatePlatformFormatter());
formatter->SetPropertyFilters({{"*", ui::AXPropertyFilter::ALLOW}});
std::string formatted_tree = formatter->Format(GetRoot());
VLOG(1) << "\n\n******** Formatted tree ********\n\n"
<< formatted_tree << "\n*********************************\n\n";
}
}
#endif
return true;
}
void BrowserAccessibilityManager::BeforeAccessibilityEvents() {}
void BrowserAccessibilityManager::FinalizeAccessibilityEvents() {}
void BrowserAccessibilityManager::OnLocationChanges(
const std::vector<blink::mojom::LocationChangesPtr>& changes) {
for (auto& change : changes) {
BrowserAccessibility* obj = GetFromID(change->id);
if (!obj)
continue;
ui::AXNode* node = obj->node();
node->SetLocation(change->new_location.offset_container_id,
change->new_location.bounds,
change->new_location.transform.get());
}
SendLocationChangeEvents(changes);
if (!location_change_callback_for_testing_.is_null())
location_change_callback_for_testing_.Run();
}
void BrowserAccessibilityManager::SendLocationChangeEvents(
const std::vector<blink::mojom::LocationChangesPtr>& changes) {
for (auto& change : changes) {
BrowserAccessibility* obj = GetFromID(change->id);
if (obj)
obj->OnLocationChanged();
}
}
void BrowserAccessibilityManager::OnFindInPageResult(int request_id,
int match_index,
int start_id,
int start_offset,
int end_id,
int end_offset) {
find_in_page_info_.request_id = request_id;
find_in_page_info_.match_index = match_index;
find_in_page_info_.start_id = start_id;
find_in_page_info_.start_offset = start_offset;
find_in_page_info_.end_id = end_id;
find_in_page_info_.end_offset = end_offset;
if (find_in_page_info_.active_request_id == request_id)
ActivateFindInPageResult(request_id);
}
void BrowserAccessibilityManager::ActivateFindInPageResult(int request_id) {
find_in_page_info_.active_request_id = request_id;
if (find_in_page_info_.request_id != request_id)
return;
BrowserAccessibility* node = GetFromID(find_in_page_info_.start_id);
if (!node)
return;
// If an ancestor of this node is a leaf node, or if this node is ignored,
// fire the notification on that.
node = node->PlatformGetLowestPlatformAncestor();
DCHECK(node);
// The "scrolled to anchor" notification is a great way to get a
// screen reader to jump directly to a specific location in a document.
FireBlinkEvent(ax::mojom::Event::kScrolledToAnchor, node,
/*action_request_id=*/-1);
}
BrowserAccessibility* BrowserAccessibilityManager::GetActiveDescendant(
BrowserAccessibility* node) const {
if (!node)
return nullptr;
ui::AXNodeID active_descendant_id;
BrowserAccessibility* active_descendant = nullptr;
if (node->GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
&active_descendant_id)) {
active_descendant = node->manager()->GetFromID(active_descendant_id);
}
if (node->GetRole() == ax::mojom::Role::kPopUpButton) {
BrowserAccessibility* child = node->InternalGetFirstChild();
if (child && child->GetRole() == ax::mojom::Role::kMenuListPopup &&
!child->IsInvisibleOrIgnored()) {
// The active descendant is found on the menu list popup, i.e. on the
// actual list and not on the button that opens it.
// If there is no active descendant, focus should stay on the button so
// that Windows screen readers would enable their virtual cursor.
// Do not expose an activedescendant in a hidden/collapsed list, as
// screen readers expect the focus event to go to the button itself.
// Note that the AX hierarchy in this case is strange -- the active
// option is the only visible option, and is inside an invisible list.
if (child->GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
&active_descendant_id)) {
active_descendant = child->manager()->GetFromID(active_descendant_id);
}
}
}
if (active_descendant && !active_descendant->IsInvisibleOrIgnored())
return active_descendant;
return node;
}
std::vector<BrowserAccessibility*> BrowserAccessibilityManager::GetAriaControls(
const BrowserAccessibility* focus) const {
if (!focus)
return {};
std::vector<BrowserAccessibility*> aria_control_nodes;
for (const auto& id :
focus->GetIntListAttribute(ax::mojom::IntListAttribute::kControlsIds)) {
if (focus->manager()->GetFromID(id))
aria_control_nodes.push_back(focus->manager()->GetFromID(id));
}
return aria_control_nodes;
}
bool BrowserAccessibilityManager::NativeViewHasFocus() {
BrowserAccessibilityDelegate* delegate = GetDelegateFromRootManager();
return delegate && delegate->AccessibilityViewHasFocus();
}
BrowserAccessibility* BrowserAccessibilityManager::GetFocus() const {
BrowserAccessibilityManager* root_manager = GetRootManager();
if (!root_manager) {
// We can't retrieved the globally focused object since we don't have access
// to the top document. If we return the focus in the current or a
// descendent tree, it might be wrong, since the top document might have
// another frame as the tree with the focus.
return nullptr;
}
ui::AXTreeID focused_tree_id = root_manager->GetTreeData().focused_tree_id;
BrowserAccessibilityManager* focused_manager = nullptr;
if (focused_tree_id != ui::AXTreeIDUnknown())
focused_manager = BrowserAccessibilityManager::FromID(focused_tree_id);
// BrowserAccessibilityManager::FromID(focused_tree_id) may return nullptr if
// the tree is not created or has been destroyed. In this case, we don't
// really know where the focus is, so we should return nullptr. However, due
// to a bug in RenderFrameHostImpl this is currently not possible.
//
// TODO(nektar): Fix All the issues identified in crbug.com/956748
if (!focused_manager)
return GetFocusFromThisOrDescendantFrame();
return focused_manager->GetFocusFromThisOrDescendantFrame();
}
BrowserAccessibility*
BrowserAccessibilityManager::GetFocusFromThisOrDescendantFrame() const {
ui::AXNodeID focus_id = GetTreeData().focus_id;
BrowserAccessibility* obj = GetFromID(focus_id);
// If nothing is focused, then the top document has the focus.
if (!obj)
return GetRoot();
if (obj->HasStringAttribute(ax::mojom::StringAttribute::kChildTreeId)) {
ui::AXTreeID child_tree_id = ui::AXTreeID::FromString(
obj->GetStringAttribute(ax::mojom::StringAttribute::kChildTreeId));
const BrowserAccessibilityManager* child_manager =
BrowserAccessibilityManager::FromID(child_tree_id);
if (child_manager)
return child_manager->GetFocusFromThisOrDescendantFrame();
}
return obj;
}
void BrowserAccessibilityManager::SetFocus(const BrowserAccessibility& node) {
if (!delegate_)
return;
base::RecordAction(
base::UserMetricsAction("Accessibility.NativeApi.SetFocus"));
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kFocus;
action_data.target_node_id = node.GetId();
if (!delegate_->AccessibilityViewHasFocus())
delegate_->AccessibilityViewSetFocus();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SetSequentialFocusNavigationStartingPoint(
const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action =
ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
// static
void BrowserAccessibilityManager::SetFocusChangeCallbackForTesting(
base::RepeatingClosure callback) {
g_focus_change_callback_for_testing.Get() = std::move(callback);
}
void BrowserAccessibilityManager::SetGeneratedEventCallbackForTesting(
const GeneratedEventCallbackForTesting& callback) {
generated_event_callback_for_testing_ = callback;
}
void BrowserAccessibilityManager::SetLocationChangeCallbackForTesting(
const base::RepeatingClosure& callback) {
location_change_callback_for_testing_ = callback;
}
// static
void BrowserAccessibilityManager::NeverSuppressOrDelayEventsForTesting() {
never_suppress_or_delay_events_for_testing_ = true;
}
void BrowserAccessibilityManager::Decrement(const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kDecrement;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::DoDefaultAction(
const BrowserAccessibility& node) {
if (!delegate_)
return;
base::RecordAction(
base::UserMetricsAction("Accessibility.NativeApi.DoDefault"));
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kDoDefault;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::GetImageData(const BrowserAccessibility& node,
const gfx::Size& max_size) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kGetImageData;
action_data.target_node_id = node.GetId();
action_data.target_rect = gfx::Rect(gfx::Point(), max_size);
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::Increment(const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kIncrement;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::ShowContextMenu(
const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kShowContextMenu;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SignalEndOfTest() {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kSignalEndOfTest;
delegate_->AccessibilityPerformAction(action_data);
}
void BrowserAccessibilityManager::Scroll(const BrowserAccessibility& node,
ax::mojom::Action scroll_action) {
if (!delegate_)
return;
switch (scroll_action) {
case ax::mojom::Action::kScrollBackward:
case ax::mojom::Action::kScrollForward:
case ax::mojom::Action::kScrollUp:
case ax::mojom::Action::kScrollDown:
case ax::mojom::Action::kScrollLeft:
case ax::mojom::Action::kScrollRight:
break;
default:
NOTREACHED() << "Cannot call Scroll with action=" << scroll_action;
}
ui::AXActionData action_data;
action_data.action = scroll_action;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::ScrollToMakeVisible(
const BrowserAccessibility& node,
gfx::Rect subfocus,
ax::mojom::ScrollAlignment horizontal_scroll_alignment,
ax::mojom::ScrollAlignment vertical_scroll_alignment,
ax::mojom::ScrollBehavior scroll_behavior) {
if (!delegate_)
return;
base::RecordAction(
base::UserMetricsAction("Accessibility.NativeApi.ScrollToMakeVisible"));
ui::AXActionData action_data;
action_data.target_node_id = node.GetId();
action_data.action = ax::mojom::Action::kScrollToMakeVisible;
action_data.target_rect = subfocus;
action_data.horizontal_scroll_alignment = horizontal_scroll_alignment;
action_data.vertical_scroll_alignment = vertical_scroll_alignment;
action_data.scroll_behavior = scroll_behavior;
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::ScrollToPoint(
const BrowserAccessibility& node,
gfx::Point point) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.target_node_id = node.GetId();
action_data.action = ax::mojom::Action::kScrollToPoint;
action_data.target_point = point;
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SetScrollOffset(
const BrowserAccessibility& node,
gfx::Point offset) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.target_node_id = node.GetId();
action_data.action = ax::mojom::Action::kSetScrollOffset;
action_data.target_point = offset;
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SetValue(const BrowserAccessibility& node,
const std::string& value) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.target_node_id = node.GetId();
action_data.action = ax::mojom::Action::kSetValue;
action_data.value = value;
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SetSelection(
const ui::AXActionData& action_data) {
if (!delegate_)
return;
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SetSelection(
const BrowserAccessibility::AXRange& range) {
if (!delegate_ || range.IsNull())
return;
ui::AXActionData action_data;
action_data.anchor_node_id = range.anchor()->anchor_id();
action_data.anchor_offset = range.anchor()->text_offset();
action_data.focus_node_id = range.focus()->anchor_id();
action_data.focus_offset = range.focus()->text_offset();
action_data.action = ax::mojom::Action::kSetSelection;
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::LoadInlineTextBoxes(
const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kLoadInlineTextBoxes;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::SetAccessibilityFocus(
const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kSetAccessibilityFocus;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::ClearAccessibilityFocus(
const BrowserAccessibility& node) {
if (!delegate_)
return;
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kClearAccessibilityFocus;
action_data.target_node_id = node.GetId();
delegate_->AccessibilityPerformAction(action_data);
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
void BrowserAccessibilityManager::HitTest(const gfx::Point& frame_point,
int request_id) const {
if (!delegate_)
return;
delegate_->AccessibilityHitTest(frame_point, ax::mojom::Event::kHover,
request_id,
/*opt_callback=*/{});
BrowserAccessibilityStateImpl::GetInstance()->OnAccessibilityApiUsage();
}
gfx::Rect BrowserAccessibilityManager::GetViewBoundsInScreenCoordinates()
const {
BrowserAccessibilityDelegate* delegate = GetDelegateFromRootManager();
if (delegate) {
gfx::Rect bounds = delegate->AccessibilityGetViewBounds();
// http://www.chromium.org/developers/design-documents/blink-coordinate-spaces
// The bounds returned by the delegate are always in device-independent
// pixels (DIPs), meaning physical pixels divided by device scale factor
// (DSF). However, Blink does not apply DSF when going from physical to
// screen pixels. In that case, we need to multiply DSF back in to get to
// Blink's notion of "screen pixels."
//
// TODO(vmpstr): This should return physical coordinates always to avoid
// confusion in the calling code. The calling code should be responsible
// for converting to whatever space necessary.
if (device_scale_factor() > 0.0 && device_scale_factor() != 1.0) {
bounds = ScaleToEnclosingRect(bounds, device_scale_factor());
}
return bounds;
}
return gfx::Rect();
}
// static
// Next object in tree using depth-first pre-order traversal.
BrowserAccessibility* BrowserAccessibilityManager::NextInTreeOrder(
const BrowserAccessibility* object) {
if (!object)
return nullptr;
if (object->PlatformChildCount())
return object->PlatformGetFirstChild();
while (object) {
BrowserAccessibility* sibling = object->PlatformGetNextSibling();
if (sibling)
return sibling;
object = object->PlatformGetParent();
}
return nullptr;
}
// static
// Next non-descendant object in tree using depth-first pre-order traversal.
BrowserAccessibility* BrowserAccessibilityManager::NextNonDescendantInTreeOrder(
const BrowserAccessibility* object) {
if (!object)
return nullptr;
while (object) {
BrowserAccessibility* sibling = object->PlatformGetNextSibling();
if (sibling)
return sibling;
object = object->PlatformGetParent();
}
return nullptr;
}
// static
// Previous object in tree using depth-first pre-order traversal.
BrowserAccessibility* BrowserAccessibilityManager::PreviousInTreeOrder(
const BrowserAccessibility* object,
bool can_wrap_to_last_element) {
if (!object)
return nullptr;
// For android, this needs to be handled carefully. If not, there is a chance
// of getting into infinite loop.
if (can_wrap_to_last_element && object->manager()->GetRoot() == object &&
object->PlatformChildCount() != 0) {
return object->PlatformDeepestLastChild();
}
BrowserAccessibility* sibling = object->PlatformGetPreviousSibling();
if (!sibling)
return object->PlatformGetParent();
if (sibling->PlatformChildCount())
return sibling->PlatformDeepestLastChild();
return sibling;
}
// static
BrowserAccessibility* BrowserAccessibilityManager::PreviousTextOnlyObject(
const BrowserAccessibility* object) {
BrowserAccessibility* previous_object = PreviousInTreeOrder(object, false);
while (previous_object && !previous_object->IsText())
previous_object = PreviousInTreeOrder(previous_object, false);
return previous_object;
}
// static
BrowserAccessibility* BrowserAccessibilityManager::NextTextOnlyObject(
const BrowserAccessibility* object) {
BrowserAccessibility* next_object = NextInTreeOrder(object);
while (next_object && !next_object->IsText())
next_object = NextInTreeOrder(next_object);
return next_object;
}
// static
bool BrowserAccessibilityManager::FindIndicesInCommonParent(
const BrowserAccessibility& object1,
const BrowserAccessibility& object2,
BrowserAccessibility** common_parent,
size_t* child_index1,
size_t* child_index2) {
DCHECK(common_parent && child_index1 && child_index2);
auto* ancestor1 = const_cast<BrowserAccessibility*>(&object1);
auto* ancestor2 = const_cast<BrowserAccessibility*>(&object2);
do {
*child_index1 = ancestor1->GetIndexInParent().value_or(0);
ancestor1 = ancestor1->PlatformGetParent();
} while (
ancestor1 &&
// |BrowserAccessibility::IsAncestorOf| returns true if objects are equal.
(ancestor1 == ancestor2 || !ancestor2->IsDescendantOf(ancestor1)));
if (!ancestor1)
return false;
do {
*child_index2 = ancestor2->GetIndexInParent().value();
ancestor2 = ancestor2->PlatformGetParent();
} while (ancestor1 != ancestor2);
*common_parent = ancestor1;
return true;
}
// static
ax::mojom::TreeOrder BrowserAccessibilityManager::CompareNodes(
const BrowserAccessibility& object1,
const BrowserAccessibility& object2) {
if (&object1 == &object2)
return ax::mojom::TreeOrder::kEqual;
BrowserAccessibility* common_parent;
size_t child_index1;
size_t child_index2;
if (FindIndicesInCommonParent(object1, object2, &common_parent, &child_index1,
&child_index2)) {
if (child_index1 < child_index2)
return ax::mojom::TreeOrder::kBefore;
if (child_index1 > child_index2)
return ax::mojom::TreeOrder::kAfter;
}
if (object2.IsDescendantOf(&object1))
return ax::mojom::TreeOrder::kBefore;
if (object1.IsDescendantOf(&object2))
return ax::mojom::TreeOrder::kAfter;
return ax::mojom::TreeOrder::kUndefined;
}
std::vector<const BrowserAccessibility*>
BrowserAccessibilityManager::FindTextOnlyObjectsInRange(
const BrowserAccessibility& start_object,
const BrowserAccessibility& end_object) {
std::vector<const BrowserAccessibility*> text_only_objects;
size_t child_index1 = 0;
size_t child_index2 = 0;
if (&start_object != &end_object) {
BrowserAccessibility* common_parent;
if (!FindIndicesInCommonParent(start_object, end_object, &common_parent,
&child_index1, &child_index2)) {
return text_only_objects;
}
DCHECK(common_parent);
// If the child indices are equal, one object is a descendant of the other.
DCHECK(child_index1 != child_index2 ||
start_object.IsDescendantOf(&end_object) ||
end_object.IsDescendantOf(&start_object));
}
const BrowserAccessibility* start_text_object = nullptr;
const BrowserAccessibility* end_text_object = nullptr;
if (&start_object == &end_object && start_object.IsAtomicTextField()) {
// We need to get to the shadow DOM that is inside the text control in order
// to find the text-only objects.
if (!start_object.InternalChildCount())
return text_only_objects;
start_text_object = start_object.InternalGetFirstChild();
end_text_object = start_object.InternalGetLastChild();
} else if (child_index1 <= child_index2 ||
end_object.IsDescendantOf(&start_object)) {
start_text_object = &start_object;
end_text_object = &end_object;
} else if (child_index1 > child_index2 ||
start_object.IsDescendantOf(&end_object)) {
start_text_object = &end_object;
end_text_object = &start_object;
}
// Pre-order traversal might leave some text-only objects behind if we don't
// start from the deepest children of the end object.
if (!end_text_object->IsLeaf())
end_text_object = end_text_object->PlatformDeepestLastChild();
if (!start_text_object->IsText())
start_text_object = NextTextOnlyObject(start_text_object);
if (!end_text_object->IsText())
end_text_object = PreviousTextOnlyObject(end_text_object);
if (!start_text_object || !end_text_object)
return text_only_objects;
while (start_text_object && start_text_object != end_text_object) {
text_only_objects.push_back(start_text_object);
start_text_object = NextTextOnlyObject(start_text_object);
}
text_only_objects.push_back(end_text_object);
return text_only_objects;
}
// static
std::u16string BrowserAccessibilityManager::GetTextForRange(
const BrowserAccessibility& start_object,
const BrowserAccessibility& end_object) {
return GetTextForRange(start_object, 0, end_object,
end_object.GetTextContentUTF16().length());
}
// static
std::u16string BrowserAccessibilityManager::GetTextForRange(
const BrowserAccessibility& start_object,
int start_offset,
const BrowserAccessibility& end_object,
int end_offset) {
DCHECK_GE(start_offset, 0);
DCHECK_GE(end_offset, 0);
if (&start_object == &end_object && start_object.IsAtomicTextField()) {
if (start_offset > end_offset)
std::swap(start_offset, end_offset);
if (start_offset >=
static_cast<int>(start_object.GetTextContentUTF16().length()) ||
end_offset >
static_cast<int>(start_object.GetTextContentUTF16().length())) {
return std::u16string();
}
return start_object.GetTextContentUTF16().substr(start_offset,
end_offset - start_offset);
}
std::vector<const BrowserAccessibility*> text_only_objects =
FindTextOnlyObjectsInRange(start_object, end_object);
if (text_only_objects.empty())
return std::u16string();
if (text_only_objects.size() == 1) {
// Be a little permissive with the start and end offsets.
if (start_offset > end_offset)
std::swap(start_offset, end_offset);
const BrowserAccessibility* text_object = text_only_objects[0];
if (start_offset <
static_cast<int>(text_object->GetTextContentUTF16().length()) &&
end_offset <=
static_cast<int>(text_object->GetTextContentUTF16().length())) {
return text_object->GetTextContentUTF16().substr(
start_offset, end_offset - start_offset);
}
return text_object->GetTextContentUTF16();
}
std::u16string text;
const BrowserAccessibility* start_text_object = text_only_objects[0];
// Figure out if the start and end positions have been reversed.
const BrowserAccessibility* first_object = &start_object;
if (!first_object->IsText())
first_object = NextTextOnlyObject(first_object);
if (!first_object || first_object != start_text_object)
std::swap(start_offset, end_offset);
if (start_offset <
static_cast<int>(start_text_object->GetTextContentUTF16().length())) {
text += start_text_object->GetTextContentUTF16().substr(start_offset);
} else {
text += start_text_object->GetTextContentUTF16();
}
for (size_t i = 1; i < text_only_objects.size() - 1; ++i) {
text += text_only_objects[i]->GetTextContentUTF16();
}
const BrowserAccessibility* end_text_object = text_only_objects.back();
if (end_offset <=
static_cast<int>(end_text_object->GetTextContentUTF16().length())) {
text += end_text_object->GetTextContentUTF16().substr(0, end_offset);
} else {
text += end_text_object->GetTextContentUTF16();
}
return text;
}
// static
gfx::Rect BrowserAccessibilityManager::GetRootFrameInnerTextRangeBoundsRect(
const BrowserAccessibility& start_object,
int start_offset,
const BrowserAccessibility& end_object,
int end_offset) {
DCHECK_GE(start_offset, 0);
DCHECK_GE(end_offset, 0);
if (&start_object == &end_object && start_object.IsAtomicTextField()) {
if (start_offset > end_offset)
std::swap(start_offset, end_offset);
if (start_offset >=
static_cast<int>(start_object.GetTextContentUTF16().length()) ||
end_offset >
static_cast<int>(start_object.GetTextContentUTF16().length())) {
return gfx::Rect();
}
return start_object.GetUnclippedRootFrameInnerTextRangeBoundsRect(
start_offset, end_offset);
}
gfx::Rect result;
const BrowserAccessibility* first = &start_object;
const BrowserAccessibility* last = &end_object;
switch (CompareNodes(*first, *last)) {
case ax::mojom::TreeOrder::kBefore:
case ax::mojom::TreeOrder::kEqual:
break;
case ax::mojom::TreeOrder::kAfter:
std::swap(first, last);
std::swap(start_offset, end_offset);
break;
default:
return gfx::Rect();
}
const BrowserAccessibility* current = first;
do {
if (current->IsText()) {
int len = static_cast<int>(current->GetTextContentUTF16().size());
int start_char_index = 0;
int end_char_index = len;
if (current == first)
start_char_index = start_offset;
if (current == last)
end_char_index = end_offset;
result.Union(current->GetUnclippedRootFrameInnerTextRangeBoundsRect(
start_char_index, end_char_index));
} else {
result.Union(current->GetClippedRootFrameBoundsRect());
}
if (current == last)
break;
current = NextInTreeOrder(current);
} while (current);
return result;
}
void BrowserAccessibilityManager::OnTreeDataChanged(
ui::AXTree* tree,
const ui::AXTreeData& old_data,
const ui::AXTreeData& new_data) {
DCHECK_EQ(ax_tree(), tree);
if (new_data.tree_id == ui::AXTreeIDUnknown() ||
new_data.tree_id == ax_tree_id_) {
return; // Tree ID hasn't changed.
}
// Either the tree that is being managed by this manager has just been
// created, or it has been destroyed and re-created.
connected_to_parent_tree_node_ = false;
// If the current focus is in the tree that has just been destroyed, then
// reset the focus to nullptr. It will be set to the current focus again the
// next time there is a focus event.
if (ax_tree_id_ != ui::AXTreeIDUnknown() &&
ax_tree_id_ == last_focused_node_tree_id_) {
SetLastFocusedNode(nullptr);
}
ui::AXTreeManager::OnTreeDataChanged(tree, old_data, new_data);
}
void BrowserAccessibilityManager::OnNodeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {
DCHECK(node);
if (BrowserAccessibility* wrapper = GetFromAXNode(node)) {
if (wrapper == GetLastFocusedNode())
SetLastFocusedNode(nullptr);
// We fire these here, immediately, to ensure we can send platform
// notifications prior to the actual destruction of the object.
if (node->GetRole() == ax::mojom::Role::kMenu)
FireGeneratedEvent(ui::AXEventGenerator::Event::MENU_POPUP_END, wrapper);
}
}
void BrowserAccessibilityManager::OnSubtreeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {}
void BrowserAccessibilityManager::OnNodeCreated(ui::AXTree* tree,
ui::AXNode* node) {
DCHECK(node);
id_wrapper_map_[node->id()] = BrowserAccessibility::Create(this, node);
if (tree->root() != node &&
node->GetRole() == ax::mojom::Role::kRootWebArea) {
popup_root_ids_.insert(node->id());
}
}
void BrowserAccessibilityManager::OnNodeDeleted(ui::AXTree* tree,
int32_t node_id) {
DCHECK_NE(node_id, ui::kInvalidAXNodeID);
id_wrapper_map_.erase(node_id);
popup_root_ids_.erase(node_id);
}
void BrowserAccessibilityManager::OnNodeReparented(ui::AXTree* tree,
ui::AXNode* node) {
DCHECK(node);
auto iter = id_wrapper_map_.find(node->id());
// TODO(crbug.com/1315661): This if statement ideally should never be entered.
// Identify why we are entering this code path and fix the root cause.
if (iter == id_wrapper_map_.end()) {
bool success;
std::tie(iter, success) = id_wrapper_map_.insert(
{node->id(), BrowserAccessibility::Create(this, node)});
DCHECK(success);
}
DCHECK(iter != id_wrapper_map_.end());
BrowserAccessibility* wrapper = iter->second.get();
wrapper->SetNode(*node);
}
void BrowserAccessibilityManager::OnRoleChanged(ui::AXTree* tree,
ui::AXNode* node,
ax::mojom::Role old_role,
ax::mojom::Role new_role) {
DCHECK(node);
if (tree->root() == node)
return;
if (new_role == ax::mojom::Role::kRootWebArea) {
popup_root_ids_.insert(node->id());
} else if (old_role == ax::mojom::Role::kRootWebArea) {
popup_root_ids_.erase(node->id());
}
}
void BrowserAccessibilityManager::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
const std::vector<ui::AXTreeObserver::Change>& changes) {
DCHECK_EQ(ax_tree(), tree);
if (root_changed)
connected_to_parent_tree_node_ = false;
// Calls OnDataChanged on newly created, reparented or changed nodes.
for (const auto& change : changes) {
ui::AXNode* node = change.node;
BrowserAccessibility* wrapper = GetFromAXNode(node);
if (wrapper)
wrapper->OnDataChanged();
}
}
ui::AXNode* BrowserAccessibilityManager::GetNodeFromTree(
const ui::AXTreeID tree_id,
const ui::AXNodeID node_id) const {
auto* manager = BrowserAccessibilityManager::FromID(tree_id);
CHECK(manager);
return manager->GetNodeFromTree(node_id);
}
ui::AXNode* BrowserAccessibilityManager::GetNodeFromTree(
const ui::AXNodeID node_id) const {
return ax_tree()->GetFromId(node_id);
}
ui::AXPlatformNode* BrowserAccessibilityManager::GetPlatformNodeFromTree(
const ui::AXNodeID node_id) const {
BrowserAccessibility* wrapper = GetFromID(node_id);
if (wrapper)
return wrapper->GetAXPlatformNode();
return nullptr;
}
ui::AXPlatformNode* BrowserAccessibilityManager::GetPlatformNodeFromTree(
const ui::AXNode& node) const {
return GetPlatformNodeFromTree(node.id());
}
ui::AXTreeID BrowserAccessibilityManager::GetTreeID() const {
return ax_tree_id_;
}
ui::AXTreeID BrowserAccessibilityManager::GetParentTreeID() const {
return GetTreeData().parent_tree_id;
}
ui::AXNode* BrowserAccessibilityManager::GetRootAsAXNode() const {
// ax_tree_ is nullptr after destruction.
if (!ax_tree())
return nullptr;
// ax_tree_->root() can be null during AXTreeObserver callbacks.
return ax_tree()->root();
}
ui::AXNode* BrowserAccessibilityManager::GetParentNodeFromParentTreeAsAXNode()
const {
BrowserAccessibilityManager* parent_manager = GetParentManager();
if (!parent_manager)
return nullptr;
DCHECK(GetRootAsAXNode());
std::set<int32_t> host_node_ids =
parent_manager->ax_tree()->GetNodeIdsForChildTreeId(ax_tree_id_);
if (host_node_ids.empty()) {
// Parent tree has host node but the change has not been serialized yet.
// For example, this could happen if an <iframe> or <portal> was added to
// the parent's DOM.
return nullptr;
}
CHECK_EQ(host_node_ids.size(), 1U)
<< "Multiple nodes cannot claim the same child tree ID.";
ui::AXNode* parent_node =
parent_manager->GetNodeFromTree(*(host_node_ids.begin()));
DCHECK(parent_node);
DCHECK_EQ(ax_tree_id_,
ui::AXTreeID::FromString(parent_node->GetStringAttribute(
ax::mojom::StringAttribute::kChildTreeId)))
<< "A node that hosts a child tree should expose its tree ID in its "
"`kChildTreeId` attribute.";
return parent_node;
}
void BrowserAccessibilityManager::WillBeRemovedFromMap() {
if (!ax_tree())
return;
ax_tree()->NotifyTreeManagerWillBeRemoved(ax_tree_id_);
}
BrowserAccessibilityManager* BrowserAccessibilityManager::GetRootManager()
const {
if (IsRootTree())
return const_cast<BrowserAccessibilityManager*>(this);
BrowserAccessibilityManager* parent_manager = GetParentManager();
if (!parent_manager) {
// This can occur when the child frame has an embedding token, but the
// parent element (e.g. <iframe>) does not yet know about the child.
// Attempting to change this to a DCHECK() will currently cause a number of
// tests to fail. Ideally, we would not need this if Blink always serialized
// the embedding token in the child tree owning element first, before
// serializing the child tree.
return nullptr;
}
return parent_manager->GetRootManager();
}
BrowserAccessibilityManager* BrowserAccessibilityManager::GetParentManager()
const {
ui::AXTreeID parent_tree_id = GetParentTreeID();
if (parent_tree_id == ui::AXTreeIDUnknown())
return nullptr; // Not connected yet.
DCHECK(!IsRootTree());
// This can still return null if the parent frame has not yet been serialized.
// We can't prevent a child frame from serializing before the parent frame
// does, because the child frame does not have access to the parent in the
// case of remote frames, aka Out-Of-Process Iframes, aka OOPIFs.
BrowserAccessibilityManager* parent =
BrowserAccessibilityManager::FromID(parent_tree_id);
#if DCHECK_IS_ON()
DCHECK(parent || !connected_to_parent_tree_node_);
// delegate_ is null during unit tests.
if (parent && delegate_ && delegate_->AccessibilityRenderFrameHost()) {
DCHECK(delegate_->AccessibilityRenderFrameHost()
->GetParentOrOuterDocumentOrEmbedder() ==
parent->delegate()->AccessibilityRenderFrameHost())
<< "RenderFrameHost parent should match BrowserAccessibilityManager's "
"parent's RenderFrameHost.";
}
#endif
return parent;
}
BrowserAccessibilityDelegate*
BrowserAccessibilityManager::GetDelegateFromRootManager() const {
BrowserAccessibilityManager* root_manager = GetRootManager();
if (root_manager)
return root_manager->delegate();
return nullptr;
}
bool BrowserAccessibilityManager::IsRootTree() const {
// delegate_ can be null in unit tests.
if (!delegate_)
return GetTreeData().parent_tree_id == ui::AXTreeIDUnknown();
bool is_root_tree = delegate_->AccessibilityIsMainFrame();
DCHECK(!is_root_tree || GetParentTreeID() == ui::AXTreeIDUnknown())
<< "Root tree has parent tree id of: " << GetParentTreeID();
return is_root_tree;
}
// static
void BrowserAccessibilityManager::SetLastFocusedNode(
BrowserAccessibility* node) {
if (node) {
DCHECK(node->manager());
last_focused_node_id_ = node->GetId();
last_focused_node_tree_id_ = node->manager()->ax_tree_id();
DCHECK(last_focused_node_tree_id_);
DCHECK(last_focused_node_tree_id_ != ui::AXTreeIDUnknown());
} else {
last_focused_node_id_.reset();
last_focused_node_tree_id_.reset();
}
}
// static
BrowserAccessibility* BrowserAccessibilityManager::GetLastFocusedNode() {
if (last_focused_node_id_) {
DCHECK(last_focused_node_tree_id_);
DCHECK(last_focused_node_tree_id_ != ui::AXTreeIDUnknown());
if (BrowserAccessibilityManager* last_focused_manager =
FromID(last_focused_node_tree_id_.value()))
return last_focused_manager->GetFromID(last_focused_node_id_.value());
}
return nullptr;
}
ui::AXTreeUpdate BrowserAccessibilityManager::SnapshotAXTreeForTesting() {
std::unique_ptr<ui::AXTreeSource<const ui::AXNode*>> tree_source(
ax_serializable_tree()->CreateTreeSource());
ui::AXTreeSerializer<const ui::AXNode*> serializer(tree_source.get());
ui::AXTreeUpdate update;
serializer.SerializeChanges(GetRootAsAXNode(), &update);
return update;
}
void BrowserAccessibilityManager::UseCustomDeviceScaleFactorForTesting(
float device_scale_factor) {
use_custom_device_scale_factor_for_testing_ = true;
device_scale_factor_ = device_scale_factor;
}
BrowserAccessibility* BrowserAccessibilityManager::CachingAsyncHitTest(
const gfx::Point& physical_pixel_point) const {
// TODO(crbug.com/1061323): By starting the hit test on the root frame,
// it allows for the possibility that we don't return a descendant as the
// hit test result, but AXPlatformNodeDelegate says that it's only supposed
// to return a descendant, so this isn't correctly fulfilling the contract.
// Unchecked it can even lead to an infinite loop.
BrowserAccessibilityManager* root_manager = GetRootManager();
if (root_manager && root_manager != this)
return root_manager->CachingAsyncHitTest(physical_pixel_point);
gfx::Point blink_screen_point = physical_pixel_point;
gfx::Rect screen_view_bounds = GetViewBoundsInScreenCoordinates();
if (delegate_) {
// Transform from screen to viewport to frame coordinates to pass to Blink.
// Note that page scale (pinch zoom) is independent of device scale factor
// (display DPI). Only the latter is affected by UseZoomForDSF.
// http://www.chromium.org/developers/design-documents/blink-coordinate-spaces
gfx::Point viewport_point =
blink_screen_point - screen_view_bounds.OffsetFromOrigin();
gfx::Point frame_point =
gfx::ScaleToRoundedPoint(viewport_point, 1.0f / page_scale_factor_);
// This triggers an asynchronous request to compute the true object that's
// under the point.
HitTest(frame_point, /*request_id=*/0);
// Unfortunately we still have to return an answer synchronously because
// the APIs were designed that way. The best case scenario is that the
// screen point is within the bounds of the last result we got from a
// call to AccessibilityHitTest - in that case, we can return that object!
if (last_hover_bounds_.Contains(blink_screen_point)) {
BrowserAccessibilityManager* manager =
BrowserAccessibilityManager::FromID(last_hover_ax_tree_id_);
if (manager) {
BrowserAccessibility* node = manager->GetFromID(last_hover_node_id_);
if (node)
return node;
}
}
}
// If that test failed we have to fall back on searching the accessibility
// tree locally for the best bounding box match. This is generally right
// for simple pages but wrong in cases of z-index, overflow, and other
// more complicated layouts. The hope is that if the user is moving the
// mouse, this fallback will only be used transiently, and the asynchronous
// result will be used for the next call.
return ApproximateHitTest(blink_screen_point);
}
BrowserAccessibility* BrowserAccessibilityManager::ApproximateHitTest(
const gfx::Point& blink_screen_point) const {
if (cached_node_rtree_)
return AXTreeHitTest(blink_screen_point);
return GetRoot()->ApproximateHitTest(blink_screen_point);
}
void BrowserAccessibilityManager::DetachFromParentManager() {
connected_to_parent_tree_node_ = false;
delegate_ = nullptr;
}
void BrowserAccessibilityManager::BuildAXTreeHitTestCache() {
auto* root = GetRoot();
if (!root)
return;
std::vector<const BrowserAccessibility*> storage;
BuildAXTreeHitTestCacheInternal(root, &storage);
// Use AXNodeID for this as nodes are unchanging with this cache.
cached_node_rtree_ = std::make_unique<cc::RTree<ui::AXNodeID>>();
cached_node_rtree_->Build(
storage,
[](const std::vector<const BrowserAccessibility*>& storage,
size_t index) {
return storage[index]->GetUnclippedRootFrameBoundsRect();
},
[](const std::vector<const BrowserAccessibility*>& storage,
size_t index) { return storage[index]->GetId(); });
}
void BrowserAccessibilityManager::BuildAXTreeHitTestCacheInternal(
const BrowserAccessibility* node,
std::vector<const BrowserAccessibility*>* storage) {
// Based on behavior in ApproximateHitTest() and node ordering in Blink:
// Generated backwards so that in the absence of any other information, we
// assume the object that occurs later in the tree is on top of one that comes
// before it.
auto range = node->PlatformChildren();
for (const auto& child : base::Reversed(range)) {
// Skip table columns because cells are only contained in rows,
// not columns.
if (child.GetRole() == ax::mojom::Role::kColumn)
continue;
BuildAXTreeHitTestCacheInternal(&child, storage);
}
storage->push_back(node);
}
BrowserAccessibility* BrowserAccessibilityManager::AXTreeHitTest(
const gfx::Point& blink_screen_point) const {
// TODO(crbug.com/1287526): assert that this gets called on a valid node. This
// should usually be the root node except for Paint Preview.
DCHECK(cached_node_rtree_);
std::vector<ui::AXNodeID> results;
std::vector<gfx::Rect> rects;
cached_node_rtree_->Search(
gfx::Rect(blink_screen_point.x(), blink_screen_point.y(), /*width=*/1,
/*height=*/1),
&results, &rects);
if (results.empty())
return nullptr;
// Find the tightest enclosing rect. Work backwards as leaf nodes come
// last and should be preferred.
auto rit = std::min_element(rects.rbegin(), rects.rend(),
[](const gfx::Rect& a, const gfx::Rect& b) {
return a.size().Area64() < b.size().Area64();
});
return GetFromID(results[std::distance(rects.begin(), rit.base()) - 1]);
}
void BrowserAccessibilityManager::CacheHitTestResult(
BrowserAccessibility* hit_test_result) const {
// Walk up to the highest ancestor that's a leaf node; we don't want to
// return a node that's hidden from the tree.
hit_test_result = hit_test_result->PlatformGetLowestPlatformAncestor();
last_hover_ax_tree_id_ = hit_test_result->manager()->ax_tree_id();
last_hover_node_id_ = hit_test_result->GetId();
last_hover_bounds_ = hit_test_result->GetClippedScreenBoundsRect();
}
void BrowserAccessibilityManager::DidActivatePortal(
WebContents* predecessor_contents,
base::TimeTicks activation_time) {
if (GetTreeData().loaded) {
FireGeneratedEvent(ui::AXEventGenerator::Event::PORTAL_ACTIVATED,
GetRoot());
}
}
void BrowserAccessibilityManager::SetPageScaleFactor(float page_scale_factor) {
page_scale_factor_ = page_scale_factor;
}
float BrowserAccessibilityManager::GetPageScaleFactor() const {
return page_scale_factor_;
}
void BrowserAccessibilityManager::CollectChangedNodesAndParentsForAtomicUpdate(
ui::AXTree* tree,
const std::vector<ui::AXTreeObserver::Change>& changes,
std::set<ui::AXPlatformNode*>* nodes_needing_update) {
// The nodes that need to be updated are all of the nodes that were changed,
// plus some parents.
for (const auto& change : changes) {
const ui::AXNode* changed_node = change.node;
DCHECK(changed_node);
BrowserAccessibility* obj = GetFromAXNode(changed_node);
if (obj)
nodes_needing_update->insert(obj->GetAXPlatformNode());
const ui::AXNode* parent = changed_node->GetUnignoredParent();
if (!parent)
continue;
// Update changed nodes' parents, including their hypertext:
// Any child that changes, whether text or not, can affect the parent's
// hypertext. Hypertext uses embedded object characters to represent
// child objects, and the AXHyperText caches relevant object at
// each embedded object character offset.
if (!changed_node->IsChildOfLeaf()) {
BrowserAccessibility* parent_obj = GetFromAXNode(parent);
if (parent_obj)
nodes_needing_update->insert(parent_obj->GetAXPlatformNode());
}
// When a node is editable, update the editable root too.
if (!changed_node->HasState(ax::mojom::State::kEditable))
continue;
const ui::AXNode* editable_root = changed_node;
while (editable_root->parent() &&
editable_root->parent()->HasState(ax::mojom::State::kEditable)) {
editable_root = editable_root->parent();
}
BrowserAccessibility* editable_root_obj = GetFromAXNode(editable_root);
if (editable_root_obj)
nodes_needing_update->insert(editable_root_obj->GetAXPlatformNode());
}
}
bool BrowserAccessibilityManager::ShouldFireEventForNode(
BrowserAccessibility* node) const {
node = RetargetForEvents(node, RetargetEventType::RetargetEventTypeGenerated);
if (!node || !node->CanFireEvents())
return false;
// If the root delegate isn't the main-frame, this may be a new frame that
// hasn't yet been swapped in or added to the frame tree. Suppress firing
// events until then.
BrowserAccessibilityDelegate* root_delegate = GetDelegateFromRootManager();
if (!root_delegate)
return false;
if (!root_delegate->AccessibilityIsMainFrame())
return false;
// Don't fire events when this document might be stale as the user has
// started navigating to a new document.
if (user_is_navigating_away_)
return false;
// Inline text boxes are an internal implementation detail, we don't
// expose them to the platform.
if (node->GetRole() == ax::mojom::Role::kInlineTextBox)
return false;
return true;
}
float BrowserAccessibilityManager::device_scale_factor() const {
return device_scale_factor_;
}
void BrowserAccessibilityManager::UpdateDeviceScaleFactor() {
if (delegate_)
device_scale_factor_ = delegate_->AccessibilityGetDeviceScaleFactor();
}
} // namespace content