blob: 57faf156add1ea7f3943ce0a891cb22cd219aaa6 [file]
// Copyright 2013 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_manager_android.h"
#include <optional>
#include <vector>
#include "base/android/android_info.h"
#include "base/check.h"
#include "base/debug/crash_logging.h"
#include "base/i18n/char_iterator.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility_android.h"
#include "content/browser/accessibility/web_contents_accessibility_android.h"
#include "content/common/features.h"
#include "content/public/common/content_features.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_event_generator.h"
#include "ui/accessibility/ax_position.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/platform/ax_android_constants.h"
#include "ui/accessibility/platform/ax_platform_tree_manager_delegate.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/accessibility/platform/one_shot_accessibility_tree_search.h"
namespace content {
namespace {
// Attempts to restore the original anchor container for a tree position.
//
// When resolving a Chrome tree position, `AsUnignoredSelectionPosition()` may
// descend into a deeply nested descendant. If the resolved position is at the
// start (offset 0) or end (offset child_count) of every intermediate container
// along that descent path, it is equivalent to a child offset on the
// `original_anchor`.
//
// For a (`descendant_container`, `descendant_child_offset`) position pair, this
// helper walks up through unignored ancestors as long as the child offset
// remains at the start or end boundary, re-anchoring the position on
// `original_anchor` (or its nearest unignored ancestor). Returns the converted
// position if reached, or std::nullopt otherwise.
std::optional<BrowserAccessibilityManagerAndroid::AndroidPosition>
TryReanchorToOriginalAnchor(const ui::AXNode* original_anchor,
BrowserAccessibilityAndroid* descendant_container,
int descendant_child_offset) {
if (!original_anchor || !descendant_container) {
return std::nullopt;
}
const ui::AXNode* original_unignored_anchor =
original_anchor->IsIgnored() ? original_anchor->GetUnignoredParent()
: original_anchor;
// Fast return: the descendant is the original anchor itself.
if (descendant_container->node() == original_unignored_anchor) {
return BrowserAccessibilityManagerAndroid::AndroidPosition{
descendant_container, descendant_child_offset,
ExtendedSelectionOffsetType::OFFSET_TYPE_CHILD};
}
BrowserAccessibilityAndroid* current_container = descendant_container;
int current_child_offset = descendant_child_offset;
const bool is_at_start = (current_child_offset == 0);
const bool is_at_end =
(current_child_offset ==
static_cast<int>(current_container->node()->GetUnignoredChildCount()));
if (!is_at_start && !is_at_end) {
return std::nullopt;
}
while (current_container->node() != original_unignored_anchor) {
const ui::AXNode* next_unignored_parent_node =
current_container->node()->GetUnignoredParent();
if (!next_unignored_parent_node) {
break;
}
BrowserAccessibilityAndroid* next_parent =
static_cast<BrowserAccessibilityAndroid*>(
current_container->manager()->GetFromAXNode(
next_unignored_parent_node));
if (!next_parent) {
break;
}
const bool is_intermediate_ancestor =
(next_unignored_parent_node != original_unignored_anchor);
// Intermediate ancestors along the descent path must strictly remain at
// the start (offset 0) or end (offset child_count) boundary. When
// reaching the final target `original_unignored_anchor`, any child index is
// accepted.
if (is_at_start) {
current_child_offset =
current_container->node()->GetUnignoredIndexInParent();
if (is_intermediate_ancestor && current_child_offset != 0) {
break;
}
} else {
// To stay at the end boundary, use the child offset after
// `current_container` in the `next_parent`.
current_child_offset =
current_container->node()->GetUnignoredIndexInParent() + 1;
if (is_intermediate_ancestor &&
current_child_offset !=
static_cast<int>(
next_unignored_parent_node->GetUnignoredChildCount())) {
break;
}
}
current_container = next_parent;
}
if (current_container->node() == original_unignored_anchor) {
return BrowserAccessibilityManagerAndroid::AndroidPosition{
current_container, current_child_offset,
ExtendedSelectionOffsetType::OFFSET_TYPE_CHILD};
}
return std::nullopt;
}
} // namespace
// static
ui::BrowserAccessibilityManager* BrowserAccessibilityManagerAndroid::Create(
const ui::AXTreeUpdate& initial_tree,
ui::AXNodeIdDelegate& node_id_delegate,
ui::AXPlatformTreeManagerDelegate* delegate) {
if (!delegate) {
return new BrowserAccessibilityManagerAndroid(initial_tree, nullptr,
node_id_delegate, nullptr);
}
WebContentsAccessibilityAndroid* wcax = nullptr;
if (delegate->AccessibilityIsRootFrame()) {
wcax = static_cast<WebContentsAccessibilityAndroid*>(
delegate->AccessibilityGetWebContentsAccessibility());
}
return new BrowserAccessibilityManagerAndroid(
initial_tree, wcax ? wcax->GetWeakPtr() : nullptr, node_id_delegate,
delegate);
}
// static
ui::BrowserAccessibilityManager* BrowserAccessibilityManagerAndroid::Create(
ui::AXNodeIdDelegate& node_id_delegate,
ui::AXPlatformTreeManagerDelegate* delegate) {
return BrowserAccessibilityManagerAndroid::Create(
BrowserAccessibilityManagerAndroid::GetEmptyDocument(), node_id_delegate,
delegate);
}
BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid(
const ui::AXTreeUpdate& initial_tree,
base::WeakPtr<WebContentsAccessibilityAndroid> web_contents_accessibility,
ui::AXNodeIdDelegate& node_id_delegate,
ui::AXPlatformTreeManagerDelegate* delegate)
: ui::BrowserAccessibilityManager(node_id_delegate, delegate),
web_contents_accessibility_(std::move(web_contents_accessibility)) {
if (base::FeatureList::IsEnabled(
features::kAccessibilityRequestScopedContentChangedEvents)) {
SetAccessibilityEventsCallbackForTesting(
base::BindRepeating(&BrowserAccessibilityManagerAndroid::
OnAccessibilityEventsProcessedForExperiment,
base::Unretained(this)));
SetLocationChangeCallbackForTesting(
base::BindRepeating(&BrowserAccessibilityManagerAndroid::
OnAccessibilityEventsProcessedForExperiment,
base::Unretained(this)));
}
// The Java layer handles the root scroll offset.
use_root_scroll_offsets_when_computing_bounds_ = false;
Initialize(initial_tree);
}
BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() =
default;
// static
ui::AXTreeUpdate BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
ui::AXNodeData empty_document;
empty_document.id = ui::kInitialEmptyDocumentRootNodeID;
empty_document.role = ax::mojom::Role::kRootWebArea;
empty_document.SetRestriction(ax::mojom::Restriction::kReadOnly);
ui::AXTreeUpdate update;
update.root_id = empty_document.id;
update.nodes.push_back(empty_document);
return update;
}
void BrowserAccessibilityManagerAndroid::ResetWebContentsAccessibility() {
web_contents_accessibility_.reset();
}
bool BrowserAccessibilityManagerAndroid::ShouldAllowImageDescriptions() {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
return (wcax && wcax->should_allow_image_descriptions()) ||
allow_image_descriptions_for_testing_;
}
ui::BrowserAccessibility* BrowserAccessibilityManagerAndroid::GetFocus() const {
// On Android, don't follow active descendant when focus is in a textfield,
// otherwise editable comboboxes such as the search field on google.com do
// not work with Talkback. See crbug.com/761501.
// TODO(accessibility) How does Talkback then read the active item?
// This fix came in crrev.com/c/647339 but said that a more comprehensive fix
// was landing in in crrev.com/c/642056, so is this override still necessary?
ui::AXNodeID focus_id = GetTreeData().focus_id;
ui::BrowserAccessibility* focus = GetFromID(focus_id);
if (focus && focus->IsAtomicTextField()) {
return focus;
}
return ui::BrowserAccessibilityManager::GetFocus();
}
ui::BrowserAccessibility*
BrowserAccessibilityManagerAndroid::GetAccessibilityFocus() const {
if (auto* wcax = GetWebContentsAXFromRootManager()) {
return wcax->GetAccessibilityFocus();
}
return nullptr;
}
ui::AXNode* BrowserAccessibilityManagerAndroid::RetargetForEvents(
ui::AXNode* node,
RetargetEventType type) const {
// TODO(crbug.com/40856596): Node should not be null. But this seems to be
// happening in the wild for reasons not yet determined. Because the only
// consequence of node being null is that we'll fail to fire an event on a
// non-existent object, the style guide's suggestion of using a CHECK
// temporarily seems a bit strong. Nonetheless we should get to the bottom of
// this. So we are temporarily using NOTREACHED in the hopes that ClusterFuzz
// will lead to a reliably-reproducible test case.
if (!node) {
NOTREACHED();
}
// Sometimes we get events on nodes in our internal accessibility tree
// that aren't exposed on Android. Get |updated| to point to the lowest
// ancestor that is exposed.
DUMP_WILL_BE_CHECK(node);
ui::BrowserAccessibility* wrapper = GetFromAXNode(node);
DUMP_WILL_BE_CHECK(wrapper);
ui::BrowserAccessibility* updated =
wrapper->PlatformGetLowestPlatformAncestor();
CHECK(updated, base::NotFatalUntil::M159);
switch (type) {
case RetargetEventType::RetargetEventTypeGenerated: {
// If the closest platform object is a password field, the event we're
// getting is doing something in the shadow dom, for example replacing a
// character with a dot after a short pause. On Android we don't want to
// fire an event for those changes, but we do want to make sure our
// internal state is correct, so we call OnDataChanged() and then return.
if (updated->IsPasswordField() && wrapper != updated) {
updated->OnDataChanged();
return nullptr;
}
break;
}
case RetargetEventType::RetargetEventTypeBlinkGeneral:
break;
case RetargetEventType::RetargetEventTypeBlinkHover: {
// If this node is uninteresting and just a wrapper around a sole
// interesting descendant, prefer that descendant instead.
const BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(updated);
const BrowserAccessibilityAndroid* sole_interesting_node =
android_node->GetSoleInterestingNodeFromSubtree();
if (sole_interesting_node) {
android_node = sole_interesting_node;
}
// Finally, if this node is still uninteresting, try to walk up to
// find an interesting parent.
while (android_node && !android_node->IsInterestingOnAndroid()) {
android_node = static_cast<BrowserAccessibilityAndroid*>(
android_node->PlatformGetParent());
}
updated = const_cast<BrowserAccessibilityAndroid*>(android_node);
break;
}
default:
NOTREACHED();
}
return updated ? updated->node() : nullptr;
}
void BrowserAccessibilityManagerAndroid::FireFocusEvent(ui::AXNode* node) {
ui::AXTreeManager::FireFocusEvent(node);
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
// When focusing a node on Android, we want to ensure that we clear the
// Java-side cache for the previously focused node as well.
if (ui::BrowserAccessibility* last_focused_node =
GetFromAXNode(GetLastFocusedNode())) {
BrowserAccessibilityAndroid* android_last_focused_node =
static_cast<BrowserAccessibilityAndroid*>(last_focused_node);
ClearNodeInfoCacheForGivenId(android_last_focused_node->GetUniqueId());
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(GetFromAXNode(node));
wcax->HandleFocusChanged(
android_node->GetUniqueId(),
android_node->manager()->GetBrowserAccessibilityRoot() == android_node);
}
void BrowserAccessibilityManagerAndroid::FireLocationChanged(
ui::BrowserAccessibility* node) {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
bool set_subtree_changed = !base::FeatureList::IsEnabled(
features::kAccessibilityRequestScopedContentChangedEvents);
wcax->HandleContentChanged(android_node->GetUniqueId(), set_subtree_changed);
}
void BrowserAccessibilityManagerAndroid::FireSourceEvent(
ax::mojom::Event event_type,
ui::BrowserAccessibility* node,
int action_request_id) {
ui::BrowserAccessibilityManager::FireSourceEvent(event_type, node,
action_request_id);
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
switch (event_type) {
case ax::mojom::Event::kClicked:
wcax->HandleClicked(android_node->GetUniqueId());
break;
case ax::mojom::Event::kEndOfTest:
wcax->HandleEndOfTestSignal();
break;
case ax::mojom::Event::kHover:
HandleHoverEvent(node);
break;
case ax::mojom::Event::kLoadComplete:
wcax->HandleInitialLoadComplete(
static_cast<BrowserAccessibilityAndroid*>(node)->GetUniqueId());
break;
case ax::mojom::Event::kScrolledToAnchor:
wcax->HandleScrolledToAnchor(android_node->GetUniqueId());
break;
default:
break;
}
}
void BrowserAccessibilityManagerAndroid::FireDocumentSelectionChangedEvent(
WebContentsAccessibilityAndroid* wcax) {
std::optional<SelectionRange> selection = GetSelectionRange();
const bool extended_selection_enabled =
base::FeatureList::IsEnabled(features::kAccessibilityExtendedSelection);
const bool expose_children_enabled = base::FeatureList::IsEnabled(
features::kAccessibilityExposeNonAtomicTextFieldChildren);
if (extended_selection_enabled) {
bool should_send_to_root = false;
if (expose_children_enabled) {
// Send the event to the root of the frame if selection should be
// cleared, or multiple nodes are selected, or
// a non-atomic text field. Atomic text fields will continue to receive
// their event on them, the rest should go to the root web area.
// Note that this is to support contenteditables, where the
// contenteditable root itself is a non-atomic text field, and its
// children may be editable.
should_send_to_root = !selection.has_value() ||
selection->focus.node != selection->anchor.node ||
!selection->focus.node->IsAtomicTextField();
} else {
// Send the event to the root of the frame if selection should be
// cleared, or multiple nodes are selected, or the node is not editable.
should_send_to_root = !selection.has_value() ||
selection->focus.node != selection->anchor.node ||
!selection->focus.node->IsTextField();
}
if (should_send_to_root) {
BrowserAccessibilityAndroid* android_root_object =
static_cast<BrowserAccessibilityAndroid*>(
GetFromAXNode(ax_tree()->root()));
ClearNodeInfoCacheForGivenId(android_root_object->GetUniqueId());
wcax->HandleTextSelectionChanged(android_root_object->GetUniqueId());
return;
}
} else if (!selection.has_value()) {
// If focus object does not exist and extended selection is not
// enabled, there is nothing more to do since previous selection node is
// not known here and can't be cleared.
return;
}
// Send event to the focus node.
CHECK(selection->focus.node);
wcax->HandleTextSelectionChanged(selection->focus.node->GetUniqueId());
}
bool isNodeLikelyKnownForExperiment(WebContentsAccessibilityAndroid* wcax,
int32_t unique_id) {
if (features::kPreventWindowContentChangesForNodesNotLikelyInAndroid.Get()) {
return wcax->IsNodeLikelyKnownByAndroidFrameworkForExperiment(unique_id);
}
return true;
}
void BrowserAccessibilityManagerAndroid::FireGeneratedEvent(
ui::AXEventGenerator::Event event_type,
const ui::AXNode* node) {
BrowserAccessibilityManager::FireGeneratedEvent(event_type, node);
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
ui::BrowserAccessibility* wrapper = GetFromAXNode(node);
CHECK(wrapper, base::NotFatalUntil::M159);
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(wrapper);
// Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
// the Android system that the accessibility hierarchy rooted at this
// node has changed.
if (event_type != ui::AXEventGenerator::Event::SUBTREE_CREATED) {
bool set_subtree_changed =
!base::FeatureList::IsEnabled(
features::kAccessibilityRequestScopedContentChangedEvents) ||
event_type == ui::AXEventGenerator::Event::CHILDREN_CHANGED;
wcax->HandleContentChanged(android_node->GetUniqueId(),
set_subtree_changed);
}
switch (event_type) {
case ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED: {
wcax->HandleActiveDescendantChanged(android_node->GetUniqueId());
break;
}
case ui::AXEventGenerator::Event::ALERT: {
wcax->HandlePaneOpened(android_node->GetUniqueId());
// ALERT events are only fired on the root node of the alert live region,
// but verify that `node` is in fact an atomic live region root.
if (base::FeatureList::IsEnabled(
features::kAccessibilityAtomicLiveRegions) &&
node->data().IsAtomicLiveRegionRoot()) {
wcax->HandleAtomicLiveRegionChanged(android_node->GetUniqueId());
}
break;
}
case ui::AXEventGenerator::Event::CHECKED_STATE_CHANGED:
wcax->HandleCheckStateChanged(android_node->GetUniqueId());
if (android_node->GetRole() == ax::mojom::Role::kToggleButton ||
android_node->GetRole() == ax::mojom::Role::kSwitch ||
android_node->GetRole() == ax::mojom::Role::kRadioButton) {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_STATE_DESCRIPTION);
}
}
break;
case ui::AXEventGenerator::Event::DEFAULT_ACTION_VERB_CHANGED:
wcax->HandleDefaultActionVerbChanged(android_node->GetUniqueId());
break;
case ui::AXEventGenerator::Event::DESCRIPTION_CHANGED: {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_UNDEFINED);
}
if (android_node->GetRole() == ax::mojom::Role::kDialog ||
android_node->GetRole() == ax::mojom::Role::kAlertDialog) {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_PANE_TITLE);
}
}
break;
}
case ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED: {
FireDocumentSelectionChangedEvent(wcax);
break;
}
case ui::AXEventGenerator::Event::EXPANDED: {
if (ui::IsComboBox(android_node->GetRole()) &&
GetFocus()->IsDescendantOf(android_node)) {
wcax->HandlePaneOpened(android_node->GetUniqueId());
}
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_EXPANDED);
}
break;
}
case ui::AXEventGenerator::Event::COLLAPSED: {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_EXPANDED);
}
break;
}
case ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED: {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_TEXT);
}
break;
}
case ui::AXEventGenerator::Event::INVALID_STATUS_CHANGED: {
if (base::FeatureList::IsEnabled(
features::kAccessibilityAriaInvalidAndErrorMessage)) {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_CONTENT_INVALID);
}
}
break;
}
case ui::AXEventGenerator::Event::LIVE_REGION_CHANGED: {
// When a change is made within a live region, this event is fired on the
// root node of that live region. For atomic live regions, we should begin
// at the root node and notify Android of every single node within the
// subtree of this atomic live region root.
if (base::FeatureList::IsEnabled(
features::kAccessibilityAtomicLiveRegions) &&
node->data().IsAtomicLiveRegionRoot()) {
wcax->HandleAtomicLiveRegionChanged(android_node->GetUniqueId());
}
break;
}
case ui::AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED: {
// This event is fired when an object appears in a live region.
if (base::FeatureList::IsEnabled(
features::kAccessibilityImproveLiveRegionAnnounce)) {
bool is_atomic = node->data().IsAtomicLiveRegionRoot() ||
node->data().IsContainedInAtomicLiveRegion();
// If kAccessibilityAtomicLiveRegions is enabled and our node is atomic,
// it will have been handled by the LIVE_REGION_CHANGED case above.
// Otherwise, fire a WINDOW_CONTENT_CHANGED event to inform the Android
// Framework of the individual node change.
if (!(is_atomic && base::FeatureList::IsEnabled(
features::kAccessibilityAtomicLiveRegions))) {
wcax->HandleLiveRegionNodeChanged(android_node->GetUniqueId());
}
}
// TODO(crbug.com/507858294): Remove TYPE_ANNOUNCE and new live region
// behavior flags once stability has been reached in several stable
// releases.
if (!base::FeatureList::IsEnabled(
features::kAccessibilityDeprecateTypeAnnounce)) {
// If we don't support WINDOW_CONTENT_CHANGED events BUT have not yet
// deprecated TYPE_ANNOUNCEMENT, we should fire a TYPE_ANNOUNCEMENT
// event which contains the text of the changed node.
std::u16string text = android_node->GetTextContentUTF16();
wcax->AnnounceLiveRegionText(text);
}
// If kAccessibilityImproveLiveRegionAnnounce is disabled and
// kAccessibilityDeprecateTypeAnnounce is enabled, we choose not to fire
// an event here. However, this should not happen in practice as we should
// not deprecate TYPE_ANNOUNCEMENT until we have landed its replacements.
break;
}
case ui::AXEventGenerator::Event::MENU_POPUP_START: {
wcax->HandleMenuOpened(android_node->GetUniqueId());
break;
}
case ui::AXEventGenerator::Event::NAME_CHANGED: {
// If this is a simple text element, also send an event to the framework.
if (ui::IsText(android_node->GetRole()) ||
android_node->IsAndroidTextView()) {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_TEXT);
}
}
// If the name of a dialog changes, its pane title also changes.
// Notify the Android framework about the pane title change.
if (ui::IsDialog(android_node->GetRole())) {
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_PANE_TITLE);
}
}
break;
}
case ui::AXEventGenerator::Event::RANGE_VALUE_CHANGED:
// TODO(crbug.com/557330338): CHECK-exclusion: Convert to a CHECK once we
// are confident it won't be triggered.
DCHECK(android_node->GetData().IsRangeValueSupported());
if ((android_node->GetRole() == ax::mojom::Role::kSpinButton &&
!android_node->IsTextField()) ||
android_node->GetRole() == ax::mojom::Role::kMeter ||
(android_node->IsSlider() &&
base::FeatureList::IsEnabled(
features::kAccessibilitySliderStateDescription))) {
// TalkBack expects SpinButtons (non-editable), Meter and Slider value
// to be changed via state description.
if (isNodeLikelyKnownForExperiment(wcax, android_node->GetUniqueId())) {
wcax->HandleWindowContentChange(
android_node->GetUniqueId(),
ANDROID_ACCESSIBILITY_EVENT_CONTENT_CHANGE_TYPE_STATE_DESCRIPTION);
}
} else if (android_node->IsSlider()) {
wcax->HandleSliderChanged(android_node->GetUniqueId());
}
break;
case ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED:
case ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED:
wcax->HandleScrollPositionChanged(android_node->GetUniqueId());
break;
case ui::AXEventGenerator::Event::SORT_CHANGED:
// TODO(crbug.com/465804174): Verify if removing aria-sort triggers this
// event.
wcax->HandleSortDirectionChanged(android_node->GetUniqueId());
break;
case ui::AXEventGenerator::Event::SUBTREE_CREATED: {
// When a dialog is shown, we will send a SUBTREE_CREATED event.
// When this happens, we want to generate a TYPE_WINDOW_STATE_CHANGED
// event and populate the node's paneTitle with the dialog description.
// Note that kAlertDialog is not included in this condition because the
// pane opened event is already handled for kAlertDialog by the ALERT
// event.
if (android_node->GetRole() == ax::mojom::Role::kDialog) {
wcax->HandlePaneOpened(android_node->GetUniqueId());
}
break;
}
case ui::AXEventGenerator::Event::VALUE_IN_TEXT_FIELD_CHANGED:
// Sometimes `RetargetForEvents` will walk up to the lowest platform leaf
// and fire the same event on that node. However, in some rare cases the
// leaf node might not be a text field. For example, in the unusual case
// when the text field is inside a button, the leaf node is the button not
// the text field.
if (android_node->IsTextField() && GetFocus() == wrapper) {
int32_t text_change_types =
ANDROID_ACCESSIBILITY_EVENT_TEXT_CHANGE_TYPE_UNDEFINED;
if (::features::IsAccessibilityTextChangeTypesEnabled()) {
if (android_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kHasComposition)) {
text_change_types |=
ANDROID_ACCESSIBILITY_EVENT_TEXT_CHANGE_TYPE_IN_COMPOSITION;
}
if (android_node->GetBoolAttribute(
ax::mojom::BoolAttribute::kTextSuggestionSelectedByIME)) {
text_change_types |=
ANDROID_ACCESSIBILITY_EVENT_TEXT_CHANGE_TYPE_CONVERSION_SUGGESTION_SELECTED_BY_IME;
}
if (android_node->GetIntAttribute(
ax::mojom::IntAttribute::kCommittedTextLength) > 0) {
text_change_types |=
ANDROID_ACCESSIBILITY_EVENT_TEXT_CHANGE_TYPE_COMMITTED_BY_IME;
}
}
wcax->HandleEditableTextChanged(android_node->GetUniqueId(),
text_change_types);
}
break;
case ui::AXEventGenerator::Event::VALUE_IN_SPIN_BUTTON_DECREMENTED:
case ui::AXEventGenerator::Event::VALUE_IN_SPIN_BUTTON_INCREMENTED:
if (GetFocus() == wrapper) {
wcax->HandleSpinButtonStepIntent(android_node->GetUniqueId());
}
break;
// Currently unused events on this platform.
case ui::AXEventGenerator::Event::NONE:
case ui::AXEventGenerator::Event::ACCESS_KEY_CHANGED:
case ui::AXEventGenerator::Event::ARIA_CURRENT_CHANGED:
case ui::AXEventGenerator::Event::ARIA_NOTIFICATIONS_POSTED:
case ui::AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED:
case ui::AXEventGenerator::Event::ATOMIC_CHANGED:
case ui::AXEventGenerator::Event::AUTO_COMPLETE_CHANGED:
case ui::AXEventGenerator::Event::AUTOFILL_AVAILABILITY_CHANGED:
case ui::AXEventGenerator::Event::BUSY_CHANGED:
case ui::AXEventGenerator::Event::CARET_BOUNDS_CHANGED:
case ui::AXEventGenerator::Event::CHECKED_STATE_DESCRIPTION_CHANGED:
case ui::AXEventGenerator::Event::CHILDREN_CHANGED:
case ui::AXEventGenerator::Event::CONTROLS_CHANGED:
case ui::AXEventGenerator::Event::DETAILS_CHANGED:
case ui::AXEventGenerator::Event::DESCRIBED_BY_CHANGED:
case ui::AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED:
case ui::AXEventGenerator::Event::EDITABLE_TEXT_CHANGED:
case ui::AXEventGenerator::Event::ENABLED_CHANGED:
case ui::AXEventGenerator::Event::FOCUS_CHANGED:
case ui::AXEventGenerator::Event::FLOW_FROM_CHANGED:
case ui::AXEventGenerator::Event::FLOW_TO_CHANGED:
case ui::AXEventGenerator::Event::GRAMMAR_MARKER_CHANGED:
case ui::AXEventGenerator::Event::HASPOPUP_CHANGED:
case ui::AXEventGenerator::Event::HIERARCHICAL_LEVEL_CHANGED:
case ui::AXEventGenerator::Event::HIGHLIGHT_MARKER_CHANGED:
case ui::AXEventGenerator::Event::IGNORED_CHANGED:
case ui::AXEventGenerator::Event::KEY_SHORTCUTS_CHANGED:
case ui::AXEventGenerator::Event::LABELED_BY_CHANGED:
case ui::AXEventGenerator::Event::LANGUAGE_CHANGED:
case ui::AXEventGenerator::Event::LAYOUT_INVALIDATED:
case ui::AXEventGenerator::Event::LIVE_REGION_CREATED:
case ui::AXEventGenerator::Event::LIVE_RELEVANT_CHANGED:
case ui::AXEventGenerator::Event::LIVE_STATUS_CHANGED:
case ui::AXEventGenerator::Event::MENU_POPUP_END:
case ui::AXEventGenerator::Event::MENU_ITEM_SELECTED:
case ui::AXEventGenerator::Event::MULTILINE_STATE_CHANGED:
case ui::AXEventGenerator::Event::MULTISELECTABLE_STATE_CHANGED:
case ui::AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED:
case ui::AXEventGenerator::Event::ORIENTATION_CHANGED:
case ui::AXEventGenerator::Event::PARENT_CHANGED:
case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED:
case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED:
case ui::AXEventGenerator::Event::RANGE_VALUE_MAX_CHANGED:
case ui::AXEventGenerator::Event::RANGE_VALUE_MIN_CHANGED:
case ui::AXEventGenerator::Event::RANGE_VALUE_STEP_CHANGED:
case ui::AXEventGenerator::Event::READONLY_CHANGED:
case ui::AXEventGenerator::Event::RELATED_NODE_CHANGED:
case ui::AXEventGenerator::Event::REQUIRED_STATE_CHANGED:
case ui::AXEventGenerator::Event::ROLE_CHANGED:
case ui::AXEventGenerator::Event::ROW_COUNT_CHANGED:
case ui::AXEventGenerator::Event::SELECTED_CHANGED:
case ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED:
case ui::AXEventGenerator::Event::SELECTED_VALUE_CHANGED:
case ui::AXEventGenerator::Event::SET_SIZE_CHANGED:
case ui::AXEventGenerator::Event::SPELLING_MARKER_CHANGED:
case ui::AXEventGenerator::Event::STATE_CHANGED:
case ui::AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED:
case ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED:
case ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED:
break;
}
}
void BrowserAccessibilityManagerAndroid::FireAriaNotificationEvent(
ui::BrowserAccessibility* node,
const std::string& announcement,
ax::mojom::AriaNotificationPriority priority_property,
ax::mojom::AriaNotificationInterrupt interrupt_property,
const std::string& type) {
CHECK(node, base::NotFatalUntil::M159);
auto* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
// TODO(aleventhal): If aria-notification becomes a web standard, a solution
// that doesn't use a forced announcement must be implemented.
if (!base::FeatureList::IsEnabled(
features::kAccessibilityDeprecateTypeAnnounce)) {
wcax->AnnounceLiveRegionText(base::UTF8ToUTF16(announcement));
}
}
void BrowserAccessibilityManagerAndroid::SendLocationChangeEvents(
const std::vector<ui::AXLocationChange>& changes) {
// Android is not very efficient at handling notifications, and location
// changes in particular are frequent and not time-critical. If a lot of
// nodes changed location, just send a single notification after a short
// delay (to batch them), rather than lots of individual notifications.
if (changes.size() > 3) {
auto* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
wcax->SendDelayedWindowContentChangedEvent();
return;
}
BrowserAccessibilityManager::SendLocationChangeEvents(changes);
}
bool BrowserAccessibilityManagerAndroid::NextAtGranularity(
int32_t granularity,
int32_t cursor_index,
BrowserAccessibilityAndroid* node,
int32_t* start_index,
int32_t* end_index) {
switch (granularity) {
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER: {
std::u16string text = node->GetTextContentUTF16();
if (cursor_index >= static_cast<int32_t>(text.length())) {
return false;
}
base::i18n::UTF16CharIterator iter(text);
while (!iter.end() &&
static_cast<int32_t>(iter.array_pos()) <= cursor_index) {
iter.Advance();
}
*end_index = iter.array_pos();
iter.Rewind();
*start_index = iter.array_pos();
break;
}
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE: {
std::vector<int32_t> starts;
std::vector<int32_t> ends;
node->GetGranularityBoundaries(granularity, &starts, &ends, 0);
if (starts.size() == 0) {
return false;
}
size_t index = 0;
while (index < starts.size() - 1 && starts[index] < cursor_index) {
index++;
}
if (starts[index] < cursor_index) {
return false;
}
*start_index = starts[index];
*end_index = ends[index];
break;
}
default:
NOTREACHED();
}
return true;
}
bool BrowserAccessibilityManagerAndroid::PreviousAtGranularity(
int32_t granularity,
int32_t cursor_index,
BrowserAccessibilityAndroid* node,
int32_t* start_index,
int32_t* end_index) {
switch (granularity) {
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER: {
if (cursor_index <= 0) {
return false;
}
std::u16string text = node->GetTextContentUTF16();
base::i18n::UTF16CharIterator iter(text);
int previous_index = 0;
while (!iter.end() &&
static_cast<int32_t>(iter.array_pos()) < cursor_index) {
previous_index = iter.array_pos();
iter.Advance();
}
*start_index = previous_index;
*end_index = iter.array_pos();
break;
}
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE: {
std::vector<int32_t> starts;
std::vector<int32_t> ends;
node->GetGranularityBoundaries(granularity, &starts, &ends, 0);
if (starts.size() == 0) {
return false;
}
size_t index = starts.size() - 1;
while (index > 0 && starts[index] >= cursor_index) {
index--;
}
if (starts[index] >= cursor_index) {
return false;
}
*start_index = starts[index];
*end_index = ends[index];
break;
}
default:
NOTREACHED();
}
return true;
}
void BrowserAccessibilityManagerAndroid::ClearNodeInfoCacheForGivenId(
int32_t unique_id) {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
// We do not need to clear a node more than once per atomic update.
if (!nodes_already_cleared_.emplace(unique_id).second) {
return;
}
wcax->ClearNodeInfoCacheForGivenId(unique_id);
}
bool BrowserAccessibilityManagerAndroid::OnHoverEvent(
const ui::MotionEventAndroid& event) {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
return wcax ? wcax->OnHoverEvent(event) : false;
}
void BrowserAccessibilityManagerAndroid::HandleHoverEvent(
ui::BrowserAccessibility* node) {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
if (android_node) {
wcax->HandleHover(android_node->GetUniqueId());
}
}
std::unique_ptr<ui::BrowserAccessibility>
BrowserAccessibilityManagerAndroid::CreateBrowserAccessibility(
ui::AXNode* node) {
return ui::BrowserAccessibility::Create(this, node);
}
void BrowserAccessibilityManagerAndroid::OnAtomicUpdateStarting(
ui::AXTree* tree,
const absl::flat_hash_set<ui::AXNodeID>& deleting_nodes,
const absl::flat_hash_set<ui::AXNodeID>& reparenting_nodes) {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (wcax) {
// This set needs to start fresh. This secondary cache is of requests to
// java to clear that primary cache of Android objects. The idea being only
// such such request is needed for each atomic update to the tree and node
// data.
nodes_already_cleared_.clear();
// Update the maximum number of nodes in the cache after each atomic update.
wcax->UpdateMaxNodesInCache();
for (ui::AXNodeID id : deleting_nodes) {
ui::BrowserAccessibility* wrapper = GetFromID(id);
if (!wrapper) {
continue;
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(wrapper);
ClearNodeInfoCacheForGivenId(android_node->GetUniqueId());
// When a node will be deleted, clear its parent from the cache as well,
// or the parent could erroneously report the cleared node as a child
// later on.
BrowserAccessibilityAndroid* parent_node =
static_cast<BrowserAccessibilityAndroid*>(
android_node->PlatformGetParent());
if (parent_node != nullptr) {
ClearNodeInfoCacheForGivenId(parent_node->GetUniqueId());
}
}
}
BrowserAccessibilityManager::OnAtomicUpdateStarting(tree, deleting_nodes,
reparenting_nodes);
}
void BrowserAccessibilityManagerAndroid::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
const std::vector<ui::AXTreeObserver::Change>& changes) {
BrowserAccessibilityManager::OnAtomicUpdateFinished(tree, root_changed,
changes);
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (wcax) {
// Reset content changed events counter every time we finish an atomic
// update.
wcax->ResetContentChangedEventsCounter();
// When the root changes, send the new root id and a navigate signal to
// Java.
if (root_changed) {
auto* root_manager = static_cast<BrowserAccessibilityManagerAndroid*>(
GetManagerForRootFrame());
CHECK(root_manager, base::NotFatalUntil::M159);
auto* root = static_cast<BrowserAccessibilityAndroid*>(
root_manager->GetBrowserAccessibilityRoot());
CHECK(root, base::NotFatalUntil::M159);
wcax->HandleNavigate(root->GetUniqueId());
}
}
// Invalidate java-side cache for structural generated events. This
// encompasses less nodes than `changes`, but includes unignored retargeted
// event targets that isn't in `changes`. Eventually, we should prefer
// invalidations of generated events to those in
// BrowserAccessibility::OnDataChanged.
for (const auto& targeted_event : event_generator()) {
BrowserAccessibilityAndroid* wrapper =
static_cast<BrowserAccessibilityAndroid*>(
GetFromID(targeted_event.node_id));
CHECK(wrapper);
auto event_type = targeted_event.event_params->event;
switch (event_type) {
case ui::AXEventGenerator::Event::CHILDREN_CHANGED:
case ui::AXEventGenerator::Event::PARENT_CHANGED:
// Structural changes in the unignored/platform tree requires the leaf
// cache be invalidated.
BrowserAccessibilityAndroid::ResetLeafCache();
ClearNodeInfoCacheForGivenId(wrapper->GetUniqueId());
break;
case ui::AXEventGenerator::Event::NAME_CHANGED:
case ui::AXEventGenerator::Event::ROLE_CHANGED:
case ui::AXEventGenerator::Event::STATE_CHANGED:
wrapper->EraseLeafCacheDataForNode();
break;
default:
break;
}
}
}
WebContentsAccessibilityAndroid*
BrowserAccessibilityManagerAndroid::GetWebContentsAXFromRootManager() const {
ui::BrowserAccessibility* parent_node =
GetParentNodeFromParentTreeAsBrowserAccessibility();
if (!parent_node) {
return web_contents_accessibility_.get();
}
auto* parent_manager =
static_cast<BrowserAccessibilityManagerAndroid*>(parent_node->manager());
return parent_manager->GetWebContentsAXFromRootManager();
}
std::u16string
BrowserAccessibilityManagerAndroid::GenerateAccessibilityNodeInfoString(
int32_t unique_id) {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return {};
}
return wcax->GenerateAccessibilityNodeInfoString(unique_id);
}
std::optional<std::vector<std::string>>
BrowserAccessibilityManagerAndroid::GetMetadataForTree() const {
return GetTreeData().metadata;
}
std::optional<BrowserAccessibilityManagerAndroid::SelectionRange>
BrowserAccessibilityManagerAndroid::GetSelectionRange() const {
ui::AXSelection selection = ax_tree()->GetSelection();
std::optional<AndroidPosition> anchor =
ConvertChromeSelectionPositionToAndroid(
selection.anchor_object_id, selection.anchor_offset,
selection.anchor_affinity, selection.is_backward);
if (!anchor.has_value()) {
return std::nullopt;
}
std::optional<AndroidPosition> focus =
ConvertChromeSelectionPositionToAndroid(
selection.focus_object_id, selection.focus_offset,
selection.focus_affinity, selection.is_backward);
if (!focus.has_value()) {
return std::nullopt;
}
SelectionRange selection_range;
selection_range.anchor = *anchor;
selection_range.focus = *focus;
return selection_range;
}
std::optional<BrowserAccessibilityManagerAndroid::AndroidPosition>
BrowserAccessibilityManagerAndroid::ConvertChromeSelectionPositionToAndroid(
ui::AXNodeID node_id,
int offset,
ax::mojom::TextAffinity affinity,
bool is_backward) const {
ui::AXNode* node = ax_tree()->GetFromId(node_id);
if (!node) {
return std::nullopt;
}
ui::AXNodePosition::AXPositionInstance position =
ui::AXNodePosition::CreatePosition(*node, offset, affinity);
const ui::AXNodePosition::AXPositionInstance original_position =
position->Clone();
// Move the position to the nearest unignored node. This node can be a
// descendant of the current node and Android specific logic in the rest of
// this function will not handle it.
// See `text-selection-inside-hidden-element.html` as an example.
position = position->AsUnignoredSelectionPosition(
is_backward ? ui::AXPositionAdjustmentBehavior::kMoveForward
: ui::AXPositionAdjustmentBehavior::kMoveBackward,
/*force_convert_leaf_to_text=*/false);
if (position->IsNullPosition()) {
return std::nullopt;
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(
GetFromAXNode(position->GetAnchor()));
if (!android_node) {
return std::nullopt;
}
BrowserAccessibilityAndroid* lowest_platform_ancestor =
static_cast<BrowserAccessibilityAndroid*>(
android_node->PlatformGetLowestPlatformAncestor());
if (!lowest_platform_ancestor) {
return std::nullopt;
}
// If a tree position's lowest platform ancestor is a text-selectable leaf on
// the platform with text content, convert it to a text position. This will
// result in more accurate representation of the position on Android as the
// child node are flattened.
if (position->IsTreePosition() && lowest_platform_ancestor->IsLeaf() &&
lowest_platform_ancestor->HasTextContent() &&
lowest_platform_ancestor->IsTextSelectable()) {
position = position->AsTextPosition();
}
if (position->IsTextPosition()) {
// Move Chrome position up to the lowest leaf in Android and perform the
// right adjustments for offset and affinity.
while (position->GetAnchor()->id() != lowest_platform_ancestor->GetId()) {
position = position->CreateParentPosition();
}
CHECK(position->IsTextPosition());
// If original position was a tree position and the resolved position is at
// the start or end of the node, try converting it to a child position on
// its unignored parent and re-anchoring to the original anchor. This will
// keep a closer parity with the original position and helps avoid a text
// position that Blink does not support, like a cross boundary selection.
if (original_position->IsTreePosition() && position->text_offset() == 0) {
if (ui::AXNode* parent_ax_node =
lowest_platform_ancestor->node()->GetUnignoredParent()) {
if (auto* parent_node = static_cast<BrowserAccessibilityAndroid*>(
GetFromAXNode(parent_ax_node))) {
int child_offset =
lowest_platform_ancestor->node()->GetUnignoredIndexInParent();
if (auto android_pos = TryReanchorToOriginalAnchor(
original_position->GetAnchor(), parent_node, child_offset)) {
return android_pos;
}
}
}
}
if (lowest_platform_ancestor->IsTextSelectable()) {
return AndroidPosition{lowest_platform_ancestor, position->text_offset(),
ExtendedSelectionOffsetType::OFFSET_TYPE_TEXT};
}
// A non-text-selectable node can only be represented as a child offset
// within its (unignored) parent. If it has no unignored parent (e.g.
// childless root), no valid Android position can be formed.
if (!lowest_platform_ancestor->node()->GetUnignoredParent()) {
return std::nullopt;
}
position = position->AsTreePosition();
}
// Since the parent of the target node may be ignored, find the target node
// in Android accessibility tree, then find its parent in Android and compute
// the offset based on that.
// The conversion below is lossy and should be improved by including affinity
// when Selection API supports it.
ui::AXNode* target_node = nullptr;
bool at_end_of_anchor = false;
int anchor_child_count = position->GetAnchor()->GetChildCount();
if (position->child_index() == ui::AXNodePosition::BEFORE_TEXT ||
anchor_child_count == 0) {
// A tree position with BEFORE_TEXT child index points to before the anchor
// node, hence the node itself is considered as target.
// A non BEFORE_TEXT child offset for a leaf node points to after the anchor
// point. Hence again the target is set to the anchor node, but keeping a
// note to select after it.
// TODO(crbug.com/443078007): Add test for both cases. The position is
// inside the container and not before or after the container, hence moving
// it before or after the `target_node` is not right.
target_node = position->GetAnchor();
at_end_of_anchor =
(position->child_index() != ui::AXNodePosition::BEFORE_TEXT);
} else if (position->child_index() < anchor_child_count) {
target_node =
position->GetAnchor()->GetChildAtIndex(position->child_index());
} else {
target_node =
position->GetAnchor()->GetChildAtIndex(position->child_index() - 1);
at_end_of_anchor = true;
}
CHECK(target_node);
// TODO(crbug.com/550089859): Remove when crash keys are not needed.
ui::AXNode* initial_target_node = target_node;
// If `target_node` is not in the Android accessibility tree (ignored or
// uninteresting), move it up to the lowest platform ancestor.
BrowserAccessibilityAndroid* target_android_node =
static_cast<BrowserAccessibilityAndroid*>(GetFromAXNode(target_node));
target_node =
target_android_node->PlatformGetLowestPlatformAncestor()->node();
CHECK(target_node);
offset = target_node->GetUnignoredIndexInParent();
if (at_end_of_anchor) {
offset++;
}
BrowserAccessibilityAndroid* parent_node =
static_cast<BrowserAccessibilityAndroid*>(
GetFromAXNode(target_node->GetUnignoredParent()));
// TODO(crbug.com/550089859): Update the comment below, when a contradicting
// example is found.
// `parent_node` cannot be null because tree positions only occur on anchors
// with children, resolving `target_node` to a child, and
// `GetUnignoredParent()` walks up to the nearest unignored ancestor (up to
// RootWebArea). Childless root nodes are treated as leaves by
// `AXNodePosition` and handled earlier in the text position branch.
if (!parent_node) {
SCOPED_CRASH_KEY_STRING256("ax", "original_pos",
original_position->ToString());
SCOPED_CRASH_KEY_STRING256("ax", "adjusted_pos", position->ToString());
SCOPED_CRASH_KEY_STRING256(
"ax", "initial_target_node",
initial_target_node->data().ToString(/*verbose=*/false));
SCOPED_CRASH_KEY_STRING256("ax", "final_target_node",
target_node->data().ToString(/*verbose=*/false));
// Since the entire accessibility tree can be large, collect the path of
// nodes from the initial target node to the root, and all children of the
// initial target node.
std::string ancestry;
ancestry.reserve(1024);
for (ui::AXNode* ancestor = initial_target_node; ancestor;
ancestor = ancestor->GetParent()) {
base::StrAppend(&ancestry,
{ancestor->data().ToString(/*verbose=*/false), "\n"});
}
SCOPED_CRASH_KEY_STRING1024("ax", "ancestry", ancestry);
std::string children;
children.reserve(1024);
for (const auto& child : initial_target_node->children()) {
base::StrAppend(&children,
{child->data().ToString(/*verbose=*/false), "\n"});
}
SCOPED_CRASH_KEY_STRING1024("ax", "children", children);
DUMP_WILL_BE_NOTREACHED();
return std::nullopt;
}
// If original position was a tree position and the resolved position is at
// the start or end boundary of intermediate ancestors, re-anchor it on the
// original anchor (or its nearest unignored ancestor).
if (original_position->IsTreePosition()) {
if (auto android_pos = TryReanchorToOriginalAnchor(
original_position->GetAnchor(), parent_node, offset)) {
return android_pos;
}
}
return AndroidPosition{parent_node, offset,
ExtendedSelectionOffsetType::OFFSET_TYPE_CHILD};
}
ui::BrowserAccessibility::AXPosition
BrowserAccessibilityManagerAndroid::ConvertAndroidSelectionPositionToChrome(
BrowserAccessibilityAndroid* node,
int32_t offset,
ExtendedSelectionOffsetType offset_type) {
if (offset_type == ExtendedSelectionOffsetType::OFFSET_TYPE_TEXT) {
ui::BrowserAccessibility::AXPosition position =
node->CreatePositionForSelectionAt(offset);
// If the node is an empty text-selectable container (e.g., an empty
// paragraph or heading treated as an Android TextView) with no text
// descendants, `CreatePositionForSelectionAt` returns a text position
// anchored at the container itself. Blink expects text positions only on
// text nodes or text fields, so convert it to a tree position.
if (position->IsTextPosition() &&
!position->GetAnchor()->data().IsTextField() &&
!position->GetAnchor()->IsText()) {
return position->AsTreePosition();
}
return position;
}
// When node 'c' is a child of node 'p' in the Android accessibility tree,
// their equivalent nodes in Chrome accessibility tree may not have the same
// relation and 'p' may not be an immediate parent ancestor. Therefore we need
// to find the child that Android is pointing to, and then create a position
// based on its direct parent in the Chrome tree.
const size_t child_count = node->PlatformChildCount();
// Since `node` is not text selectable, it is expected that `offset` would be
// a child index to point to "before a certain child", or equal to the number
// of children to point to "after the last child". Hence if there is no
// children, or `offset` is out of this range, it is invalid and ignored.
if (child_count == 0 || offset < 0 ||
static_cast<size_t>(offset) > child_count) {
return ui::AXNodePosition::CreateNullPosition();
}
bool at_end_of_anchor =
(static_cast<size_t>(offset) == node->PlatformChildCount());
if (at_end_of_anchor) {
offset--;
}
// Find the target node.
// Note: When the mapping from Android results in an ignored node, we default
// to a downstream adjustment (moving to the next unignored sibling). While an
// upstream adjustment would also be valid, we lack the affinity information
// from Android to make a more precise choice.
// The conversion below is lossy and should be improved by using affinity in
// all next cases when it is available in Selection API.
ui::BrowserAccessibility* target = node->PlatformGetChild(offset);
CHECK(target);
if (at_end_of_anchor) {
return ui::AXNodePosition::CreateTreePositionAtEndOfAnchor(*target->node())
->CreateParentPosition();
}
return ui::AXNodePosition::CreateTreePositionAtStartOfAnchor(*target->node())
->CreateParentPosition();
}
// TODO(crbug.com/485227837): Remove experiment's methods
void BrowserAccessibilityManagerAndroid::
OnAccessibilityEventsProcessedForExperiment() {
WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
if (!wcax) {
return;
}
if (base::FeatureList::IsEnabled(
features::kAccessibilityRequestScopedContentChangedEvents)) {
wcax->ValidateA11yCacheForExperiment();
}
}
} // namespace content