blob: 54fed4867431919c91db8558194144491e15c92a [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/accessibility/android/accessibility_node_info_data_wrapper.h"
#include <algorithm>
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "chrome/grit/generated_resources.h"
#include "services/accessibility/android/android_accessibility_util.h"
#include "services/accessibility/android/ax_tree_source_android.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/platform/ax_android_constants.h"
#include "ui/base/l10n/l10n_util.h"
namespace ax::android {
namespace {
enum CollectionType { kGrid, kListWithCount, kListWithoutCount, kNone };
CollectionType GetCollectionType(
mojom::AccessibilityCollectionInfoData* collection_info) {
if (collection_info == nullptr) {
return CollectionType::kNone;
}
if (collection_info->row_count > 1 && collection_info->column_count > 1) {
return CollectionType::kGrid;
}
bool is_linear =
collection_info->row_count == 1 || collection_info->column_count == 1;
// CollectionInfo might be missing count information. ChromeVox doesn't expect
// a list without count information. We don't want to announce it as a list in
// that case.
bool has_both_count =
collection_info->row_count > 0 && collection_info->column_count > 0;
if (is_linear) {
if (has_both_count) {
return CollectionType::kListWithCount;
}
return CollectionType::kListWithoutCount;
}
return CollectionType::kNone;
}
} // namespace
using AXActionType = mojom::AccessibilityActionType;
using AXBooleanProperty = mojom::AccessibilityBooleanProperty;
using AXCollectionInfoData = mojom::AccessibilityCollectionInfoData;
using AXCollectionItemInfoData = mojom::AccessibilityCollectionItemInfoData;
using AXEventData = mojom::AccessibilityEventData;
using AXEventType = mojom::AccessibilityEventType;
using AXIntListProperty = mojom::AccessibilityIntListProperty;
using AXIntProperty = mojom::AccessibilityIntProperty;
using AXNodeInfoData = mojom::AccessibilityNodeInfoData;
using AXRangeInfoData = mojom::AccessibilityRangeInfoData;
using AXStringListProperty = mojom::AccessibilityStringListProperty;
using AXStringProperty = mojom::AccessibilityStringProperty;
constexpr mojom::AccessibilityStringProperty
AccessibilityNodeInfoDataWrapper::text_properties_[];
AccessibilityNodeInfoDataWrapper::AccessibilityNodeInfoDataWrapper(
AXTreeSourceAndroid* tree_source,
AXNodeInfoData* node)
: AccessibilityInfoDataWrapper(tree_source), node_ptr_(node) {}
AccessibilityNodeInfoDataWrapper::~AccessibilityNodeInfoDataWrapper() = default;
bool AccessibilityNodeInfoDataWrapper::IsNode() const {
return true;
}
mojom::AccessibilityNodeInfoData* AccessibilityNodeInfoDataWrapper::GetNode()
const {
return node_ptr_;
}
mojom::AccessibilityWindowInfoData*
AccessibilityNodeInfoDataWrapper::GetWindow() const {
return nullptr;
}
int32_t AccessibilityNodeInfoDataWrapper::GetId() const {
return node_ptr_->id;
}
const gfx::Rect AccessibilityNodeInfoDataWrapper::GetBounds() const {
return node_ptr_->bounds_in_screen;
}
bool AccessibilityNodeInfoDataWrapper::IsVisibleToUser() const {
return GetProperty(AXBooleanProperty::VISIBLE_TO_USER);
}
bool AccessibilityNodeInfoDataWrapper::IsWebNode() const {
if (is_web_node_.has_value()) {
return is_web_node_.value();
}
bool result = false;
ax::mojom::Role chrome_role = GetChromeRole();
if (chrome_role == ax::mojom::Role::kWebView ||
chrome_role == ax::mojom::Role::kRootWebArea) {
result = true;
} else if (AccessibilityInfoDataWrapper* parent = tree_source_->GetParent(
const_cast<AccessibilityNodeInfoDataWrapper*>(this))) {
result = parent->IsWebNode();
}
is_web_node_ = result;
return result;
}
bool AccessibilityNodeInfoDataWrapper::IsIgnored() const {
if (!tree_source_->UseFullFocusMode()) {
return !IsImportantInAndroid();
}
if (!IsImportantInAndroid() || !HasImportantProperty()) {
return true;
}
if (IsAccessibilityFocusableContainer()) {
return false;
}
if (!HasText()) {
return false; // A layout container with a11y importance.
}
return !HasAccessibilityFocusableText();
}
bool AccessibilityNodeInfoDataWrapper::IsImportantInAndroid() const {
// Virtual nodes are not enforced to be set importance. Here, they're always
// treated as important.
return node_ptr_->is_virtual_node ||
GetProperty(AXBooleanProperty::IMPORTANCE);
}
bool AccessibilityNodeInfoDataWrapper::IsFocusableInFullFocusMode() const {
if (!IsAccessibilityFocusableContainer() &&
!HasAccessibilityFocusableText()) {
return false;
}
ui::AXNodeData data;
PopulateAXRole(&data);
return ui::IsControl(data.role) || !ComputeAXName(true).empty();
}
bool AccessibilityNodeInfoDataWrapper::IsAccessibilityFocusableContainer()
const {
if (IsWebNode()) {
return GetProperty(AXBooleanProperty::SCREEN_READER_FOCUSABLE) ||
IsFocusable();
}
if (!IsImportantInAndroid() || (IsScrollableContainer() && !HasText())) {
return false;
}
return GetProperty(AXBooleanProperty::SCREEN_READER_FOCUSABLE) ||
IsFocusable() || IsClickable() || IsLongClickable() ||
IsToplevelScrollItem();
// TODO(hirokisato): probably check long clickable as well.
}
void AccessibilityNodeInfoDataWrapper::PopulateAXRole(
ui::AXNodeData* out_data) const {
std::string class_name;
if (GetProperty(AXStringProperty::CLASS_NAME, &class_name)) {
out_data->AddStringAttribute(ax::mojom::StringAttribute::kClassName,
class_name);
}
if (GetProperty(AXBooleanProperty::EDITABLE)) {
out_data->role = ax::mojom::Role::kTextField;
return;
}
if (HasCoveringSpan(AXStringProperty::TEXT, mojom::SpanType::URL) ||
HasCoveringSpan(AXStringProperty::CONTENT_DESCRIPTION,
mojom::SpanType::URL)) {
out_data->role = ax::mojom::Role::kLink;
return;
}
AXCollectionInfoData* collection_info;
switch (GetCollectionType(node_ptr_->collection_info.get())) {
case CollectionType::kGrid:
collection_info = node_ptr_->collection_info.get();
out_data->role = ax::mojom::Role::kGrid;
out_data->AddIntAttribute(ax::mojom::IntAttribute::kTableRowCount,
collection_info->row_count);
out_data->AddIntAttribute(ax::mojom::IntAttribute::kTableColumnCount,
collection_info->column_count);
return;
case CollectionType::kListWithCount:
collection_info = node_ptr_->collection_info.get();
out_data->AddIntAttribute(
ax::mojom::IntAttribute::kSetSize,
std::max(collection_info->row_count, collection_info->column_count));
out_data->role = ax::mojom::Role::kList;
return;
case CollectionType::kListWithoutCount:
out_data->role = ax::mojom::Role::kList;
return;
case CollectionType::kNone:
break;
}
if (node_ptr_->collection_item_info) {
AXCollectionItemInfoData* collection_item_info =
node_ptr_->collection_item_info.get();
if (collection_item_info->is_heading) {
out_data->role = ax::mojom::Role::kColumnHeader;
return;
}
// In order to properly resolve the role of this node, a collection item, we
// need additional information contained only in the CollectionInfo. The
// CollectionInfo should be an ancestor of this node.
collection_info = nullptr;
for (AccessibilityInfoDataWrapper* container =
const_cast<AccessibilityNodeInfoDataWrapper*>(this);
container;) {
if (!container || !container->IsNode()) {
break;
}
if (container->IsNode() && container->GetNode()->collection_info) {
collection_info = container->GetNode()->collection_info.get();
break;
}
container = tree_source_->GetParent(container);
}
switch (GetCollectionType(collection_info)) {
case CollectionType::kGrid:
out_data->role = ax::mojom::Role::kGridCell;
out_data->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowIndex,
collection_item_info->row_index);
out_data->AddIntAttribute(
ax::mojom::IntAttribute::kTableCellColumnIndex,
collection_item_info->column_index);
out_data->AddIntAttribute(ax::mojom::IntAttribute::kAriaCellRowIndex,
collection_item_info->row_index + 1);
out_data->AddIntAttribute(ax::mojom::IntAttribute::kAriaCellColumnIndex,
collection_item_info->column_index + 1);
return;
case CollectionType::kListWithCount:
if (collection_info->row_count == 1) {
out_data->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet,
collection_item_info->column_index);
} else if (collection_info->column_count == 1) {
out_data->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet,
collection_item_info->row_index);
}
out_data->role = ax::mojom::Role::kListItem;
return;
case CollectionType::kListWithoutCount:
out_data->role = ax::mojom::Role::kListItem;
return;
case CollectionType::kNone:
break;
}
}
if (GetProperty(AXBooleanProperty::HEADING)) {
out_data->role = ax::mojom::Role::kHeading;
return;
}
if (ax::mojom::Role chrome_role = GetChromeRole();
chrome_role != ax::mojom::Role::kNone) {
// The webView and rootWebArea roles differ between Android and Chrome. In
// particular, Android includes far fewer attributes which leads to
// undesirable behavior. Exclude their direct mapping.
out_data->role = (chrome_role != ax::mojom::Role::kWebView &&
chrome_role != ax::mojom::Role::kRootWebArea)
? chrome_role
: ax::mojom::Role::kGenericContainer;
return;
}
#define MAP_ROLE(android_class_name, chrome_role) \
if (class_name == android_class_name) { \
out_data->role = chrome_role; \
return; \
}
// These mappings were taken from accessibility utils (Android -> Chrome) and
// BrowserAccessibilityAndroid. They do not completely match the above two
// sources.
// EditText is excluded because it can be a container (b/150827734).
MAP_ROLE(ui::kAXAbsListViewClassname, ax::mojom::Role::kList);
MAP_ROLE(ui::kAXButtonClassname, ax::mojom::Role::kButton);
MAP_ROLE(ui::kAXCheckBoxClassname, ax::mojom::Role::kCheckBox);
MAP_ROLE(ui::kAXCheckedTextViewClassname, ax::mojom::Role::kStaticText);
MAP_ROLE(ui::kAXCompoundButtonClassname, ax::mojom::Role::kCheckBox);
MAP_ROLE(ui::kAXDialogClassname, ax::mojom::Role::kDialog);
MAP_ROLE(ui::kAXGridViewClassname, ax::mojom::Role::kTable);
MAP_ROLE(ui::kAXHorizontalScrollViewClassname, ax::mojom::Role::kScrollView);
MAP_ROLE(ui::kAXImageClassname, ax::mojom::Role::kImage);
MAP_ROLE(ui::kAXImageButtonClassname, ax::mojom::Role::kButton);
if (GetProperty(AXBooleanProperty::CLICKABLE)) {
MAP_ROLE(ui::kAXImageViewClassname, ax::mojom::Role::kButton);
} else {
MAP_ROLE(ui::kAXImageViewClassname, ax::mojom::Role::kImage);
}
MAP_ROLE(ui::kAXListViewClassname, ax::mojom::Role::kList);
MAP_ROLE(ui::kAXMenuItemClassname, ax::mojom::Role::kMenuItem);
MAP_ROLE(ui::kAXPagerClassname, ax::mojom::Role::kGroup);
MAP_ROLE(ui::kAXProgressBarClassname, ax::mojom::Role::kProgressIndicator);
MAP_ROLE(ui::kAXRadioButtonClassname, ax::mojom::Role::kRadioButton);
MAP_ROLE(ui::kAXRadioGroupClassname, ax::mojom::Role::kRadioGroup);
MAP_ROLE(ui::kAXScrollViewClassname, ax::mojom::Role::kScrollView);
MAP_ROLE(ui::kAXSeekBarClassname, ax::mojom::Role::kSlider);
MAP_ROLE(ui::kAXSpinnerClassname, ax::mojom::Role::kPopUpButton);
MAP_ROLE(ui::kAXSwitchClassname, ax::mojom::Role::kSwitch);
MAP_ROLE(ui::kAXTabWidgetClassname, ax::mojom::Role::kTabList);
MAP_ROLE(ui::kAXToggleButtonClassname, ax::mojom::Role::kToggleButton);
MAP_ROLE(ui::kAXViewClassname, ax::mojom::Role::kGenericContainer);
MAP_ROLE(ui::kAXViewGroupClassname, ax::mojom::Role::kGroup);
#undef MAP_ROLE
if (node_ptr_->collection_info) {
// Fallback for some RecyclerViews which doesn't correctly populate
// row/col counts.
out_data->role = ax::mojom::Role::kList;
return;
}
std::string text;
GetProperty(AXStringProperty::TEXT, &text);
std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>
children;
GetChildren(&children);
if (!text.empty() && children.empty()) {
out_data->role = ax::mojom::Role::kStaticText;
} else {
out_data->role = ax::mojom::Role::kGenericContainer;
}
}
void AccessibilityNodeInfoDataWrapper::PopulateAXState(
ui::AXNodeData* out_data) const {
#define MAP_STATE(android_boolean_property, chrome_state) \
if (GetProperty(android_boolean_property)) \
out_data->AddState(chrome_state);
// These mappings were taken from accessibility utils (Android -> Chrome) and
// BrowserAccessibilityAndroid. They do not completely match the above two
// sources.
MAP_STATE(AXBooleanProperty::EDITABLE, ax::mojom::State::kEditable);
MAP_STATE(AXBooleanProperty::MULTI_LINE, ax::mojom::State::kMultiline);
MAP_STATE(AXBooleanProperty::PASSWORD, ax::mojom::State::kProtected);
#undef MAP_STATE
const bool focusable = tree_source_->UseFullFocusMode()
? IsAccessibilityFocusableContainer()
: IsFocusable();
if (focusable) {
out_data->AddState(ax::mojom::State::kFocusable);
}
if (GetProperty(AXBooleanProperty::CHECKABLE)) {
const bool is_checked = GetProperty(AXBooleanProperty::CHECKED);
out_data->SetCheckedState(is_checked ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
}
if (!GetProperty(AXBooleanProperty::ENABLED)) {
out_data->SetRestriction(ax::mojom::Restriction::kDisabled);
}
if (!GetProperty(AXBooleanProperty::VISIBLE_TO_USER)) {
out_data->AddState(ax::mojom::State::kInvisible);
}
if (IsIgnored()) {
out_data->AddState(ax::mojom::State::kIgnored);
}
}
void AccessibilityNodeInfoDataWrapper::Serialize(
ui::AXNodeData* out_data) const {
AccessibilityInfoDataWrapper::Serialize(out_data);
bool is_node_tree_root = tree_source_->IsRootOfNodeTree(GetId());
// String properties that doesn't belong to any of existing chrome
// automation string properties are pushed into description.
// TODO(sahok): Refactor this to make clear the functionality(b/158633575).
std::vector<std::string> descriptions;
// String properties.
const std::string name = ComputeAXName(true);
if (!name.empty()) {
out_data->SetName(name);
}
// For a textField, the editable text is contained in the text property, and
// this should be set as the value instead of the name.
// This ensures that the edited text will be read out appropriately.
// When the edited text is empty, Android framework shows |hint_text| in
// the text field and |text| is also populated with |hint_text|.
// Prevent the duplicated output of |hint_text|.
if (GetProperty(AXBooleanProperty::EDITABLE) &&
!GetProperty(AXBooleanProperty::SHOWING_HINT_TEXT)) {
std::string text;
GetProperty(AXStringProperty::TEXT, &text);
if (!text.empty()) {
out_data->SetValue(text);
}
}
std::string role_description;
if (GetProperty(AXStringProperty::ROLE_DESCRIPTION, &role_description)) {
out_data->AddStringAttribute(ax::mojom::StringAttribute::kRoleDescription,
role_description);
}
if (is_node_tree_root) {
std::string package_name;
if (GetProperty(AXStringProperty::PACKAGE_NAME, &package_name)) {
const std::string& url =
base::StringPrintf("%s/%s", package_name.c_str(),
tree_source_->ax_tree_id().ToString().c_str());
out_data->AddStringAttribute(ax::mojom::StringAttribute::kUrl, url);
}
}
// If it exists, set tooltip value as on node.
std::string tooltip;
if (GetProperty(AXStringProperty::TOOLTIP, &tooltip)) {
out_data->AddStringAttribute(ax::mojom::StringAttribute::kTooltip, tooltip);
}
std::string state_description;
if (GetProperty(AXStringProperty::STATE_DESCRIPTION, &state_description)) {
// kValue (aria-valuetext) is supported on widgets with range_info. In this
// case, using kValue over kDescription is closer to the usage of
// stateDescription.
if (node_ptr_->range_info) {
out_data->AddStringAttribute(ax::mojom::StringAttribute::kValue,
state_description);
} else if (GetProperty(AXBooleanProperty::CHECKABLE)) {
out_data->AddStringAttribute(
ax::mojom::StringAttribute::kCheckedStateDescription,
state_description);
} else {
descriptions.push_back(state_description);
}
}
// Int properties.
int traversal_before = -1, traversal_after = -1;
if (GetProperty(AXIntProperty::TRAVERSAL_BEFORE, &traversal_before)) {
out_data->AddIntAttribute(ax::mojom::IntAttribute::kNextFocusId,
traversal_before);
}
if (GetProperty(AXIntProperty::TRAVERSAL_AFTER, &traversal_after)) {
out_data->AddIntAttribute(ax::mojom::IntAttribute::kPreviousFocusId,
traversal_after);
}
// Boolean properties.
PopulateAXState(out_data);
if (GetProperty(AXBooleanProperty::SCROLLABLE)) {
out_data->AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable, true);
}
if (IsClickable()) {
out_data->AddBoolAttribute(ax::mojom::BoolAttribute::kClickable, true);
}
if (IsLongClickable()) {
out_data->AddBoolAttribute(ax::mojom::BoolAttribute::kLongClickable, true);
out_data->AddAction(ax::mojom::Action::kLongClick);
}
if (GetProperty(AXBooleanProperty::SELECTED)) {
if (ui::IsSelectSupported(out_data->role)) {
out_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true);
} else {
descriptions.push_back(
l10n_util::GetStringUTF8(IDS_ARC_ACCESSIBILITY_SELECTED_STATUS));
}
}
if (GetProperty(AXBooleanProperty::SUPPORTS_TEXT_LOCATION)) {
out_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSupportsTextLocation,
true);
}
// All scrollable containers have the potential to have offscreen hidden
// nodes.
if (IsScrollableContainer()) {
out_data->AddBoolAttribute(
ax::mojom::BoolAttribute::kHasHiddenOffscreenNodes, true);
}
// Range info.
if (node_ptr_->range_info) {
AXRangeInfoData* range_info = node_ptr_->range_info.get();
out_data->AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
range_info->current);
out_data->AddFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange,
range_info->min);
out_data->AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange,
range_info->max);
}
// Integer properties.
int32_t val;
if (GetProperty(AXIntProperty::TEXT_SELECTION_START, &val) && val >= 0) {
out_data->AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, val);
}
if (GetProperty(AXIntProperty::TEXT_SELECTION_END, &val) && val >= 0) {
out_data->AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, val);
}
if (GetProperty(AXIntProperty::LIVE_REGION, &val) && val >= 0 &&
static_cast<mojom::AccessibilityLiveRegionType>(val) !=
mojom::AccessibilityLiveRegionType::NONE) {
const std::string& live_status = ToLiveStatusString(
static_cast<mojom::AccessibilityLiveRegionType>(val));
out_data->AddStringAttribute(ax::mojom::StringAttribute::kLiveStatus,
live_status);
out_data->AddStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus, live_status);
}
// Standard actions.
if (HasStandardAction(AXActionType::SCROLL_BACKWARD)) {
out_data->AddAction(ax::mojom::Action::kScrollBackward);
}
if (HasStandardAction(AXActionType::SCROLL_FORWARD)) {
out_data->AddAction(ax::mojom::Action::kScrollForward);
}
if (HasStandardAction(AXActionType::SCROLL_TO_POSITION)) {
out_data->AddAction(ax::mojom::Action::kScrollToPositionAtRowColumn);
}
if (HasStandardAction(AXActionType::EXPAND)) {
out_data->AddAction(ax::mojom::Action::kExpand);
out_data->AddState(ax::mojom::State::kCollapsed);
}
if (HasStandardAction(AXActionType::COLLAPSE)) {
out_data->AddAction(ax::mojom::Action::kCollapse);
out_data->AddState(ax::mojom::State::kExpanded);
}
if (node_ptr_->standard_actions) {
for (mojom::AccessibilityActionInAndroidPtr& android_action :
node_ptr_->standard_actions.value()) {
if (android_action->label.has_value()) {
const std::string& label = android_action->label.value();
const auto action_id =
static_cast<mojom::AccessibilityActionType>(android_action->id);
if (action_id == mojom::AccessibilityActionType::CLICK) {
out_data->AddStringAttribute(
ax::mojom::StringAttribute::kDoDefaultLabel, label);
}
if (action_id == mojom::AccessibilityActionType::LONG_CLICK) {
out_data->AddStringAttribute(
ax::mojom::StringAttribute::kLongClickLabel, label);
}
}
}
}
// Custom actions.
if (node_ptr_->custom_actions) {
std::vector<int32_t> custom_action_ids;
std::vector<std::string> custom_action_descriptions;
for (auto& action : node_ptr_->custom_actions.value()) {
custom_action_ids.push_back(action->id);
custom_action_descriptions.push_back(action->label.value());
}
out_data->AddAction(ax::mojom::Action::kCustomAction);
out_data->AddIntListAttribute(ax::mojom::IntListAttribute::kCustomActionIds,
custom_action_ids);
out_data->AddStringListAttribute(
ax::mojom::StringListAttribute::kCustomActionDescriptions,
custom_action_descriptions);
} else if (std::vector<int32_t> custom_action_ids;
GetProperty(AXIntListProperty::CUSTOM_ACTION_IDS_DEPRECATED,
&custom_action_ids)) {
std::vector<std::string> custom_action_descriptions;
CHECK(GetProperty(AXStringListProperty::CUSTOM_ACTION_DESCRIPTIONS,
&custom_action_descriptions));
DCHECK(!custom_action_ids.empty());
DCHECK_EQ(custom_action_ids.size(), custom_action_descriptions.size());
out_data->AddAction(ax::mojom::Action::kCustomAction);
out_data->AddIntListAttribute(ax::mojom::IntListAttribute::kCustomActionIds,
custom_action_ids);
out_data->AddStringListAttribute(
ax::mojom::StringListAttribute::kCustomActionDescriptions,
custom_action_descriptions);
}
if (!descriptions.empty()) {
out_data->AddStringAttribute(ax::mojom::StringAttribute::kDescription,
base::JoinString(descriptions, " "));
}
}
std::string AccessibilityNodeInfoDataWrapper::ComputeAXName(
bool do_recursive) const {
// TODO(hirokisato): Exposing all possible labels for a node, may result in
// too much being spoken. For ARC ++, this may result in divergent behaviour
// from Talkback.
std::string text;
std::string content_description;
std::string label;
GetProperty(AXStringProperty::CONTENT_DESCRIPTION, &content_description);
GetProperty(AXStringProperty::TEXT, &text);
int labeled_by = -1;
if (do_recursive && GetProperty(AXIntProperty::LABELED_BY, &labeled_by)) {
AccessibilityInfoDataWrapper* labeled_by_node =
tree_source_->GetFromId(labeled_by);
if (labeled_by_node && labeled_by_node->IsNode()) {
label = labeled_by_node->ComputeAXName(false);
}
}
// |hint_text| attribute in Android is often used as a placeholder text within
// textfields.
std::string hint_text;
GetProperty(AXStringProperty::HINT_TEXT, &hint_text);
std::vector<std::string> names;
// Append non empty properties to name attribute.
if (!content_description.empty()) {
names.push_back(content_description);
}
if (!label.empty()) {
names.push_back(label);
}
if (!text.empty() && !GetProperty(AXBooleanProperty::EDITABLE)) {
// EDITABLE is checked here, as EDITABLE field will have text set as value,
// this is done in Serialize() function.
names.push_back(text);
}
if (!hint_text.empty()) {
names.push_back(hint_text);
}
// If a node is accessibility focusable, but has no name, the name should be
// computed from its descendants.
if (names.empty() && tree_source_->UseFullFocusMode() &&
IsAccessibilityFocusableContainer()) {
ComputeNameFromContents(&names);
}
return base::JoinString(names, " ");
}
void AccessibilityNodeInfoDataWrapper::GetChildren(
std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>*
children) const {
if (!node_ptr_->int_list_properties) {
return;
}
const auto& it =
node_ptr_->int_list_properties->find(AXIntListProperty::CHILD_NODE_IDS);
if (it == node_ptr_->int_list_properties->end()) {
return;
}
for (const int32_t id : it->second) {
auto* child = tree_source_->GetFromId(id);
if (child != nullptr) {
children->push_back(child);
} else {
LOG(WARNING) << "Unexpected nullptr found while GetChildren";
}
}
}
int32_t AccessibilityNodeInfoDataWrapper::GetWindowId() const {
return node_ptr_->window_id;
}
bool AccessibilityNodeInfoDataWrapper::GetProperty(
AXBooleanProperty prop) const {
return ax::android::GetBooleanProperty(node_ptr_.get(), prop);
}
bool AccessibilityNodeInfoDataWrapper::GetProperty(AXIntProperty prop,
int32_t* out_value) const {
return ax::android::GetProperty(node_ptr_->int_properties, prop, out_value);
}
bool AccessibilityNodeInfoDataWrapper::HasProperty(
AXStringProperty prop) const {
return ax::android::HasProperty(node_ptr_->string_properties, prop);
}
bool AccessibilityNodeInfoDataWrapper::GetProperty(
AXStringProperty prop,
std::string* out_value) const {
return ax::android::GetProperty(node_ptr_->string_properties, prop,
out_value);
}
bool AccessibilityNodeInfoDataWrapper::GetProperty(
AXIntListProperty prop,
std::vector<int32_t>* out_value) const {
return ax::android::GetProperty(node_ptr_->int_list_properties, prop,
out_value);
}
bool AccessibilityNodeInfoDataWrapper::GetProperty(
AXStringListProperty prop,
std::vector<std::string>* out_value) const {
return ax::android::GetProperty(node_ptr_->string_list_properties, prop,
out_value);
}
bool AccessibilityNodeInfoDataWrapper::HasStandardAction(
AXActionType action) const {
if (node_ptr_->standard_actions) {
for (const auto& supported_action : node_ptr_->standard_actions.value()) {
if (static_cast<AXActionType>(supported_action->id) == action) {
return true;
}
}
return false;
}
if (!node_ptr_->int_list_properties) {
return false;
}
auto itr = node_ptr_->int_list_properties->find(
AXIntListProperty::STANDARD_ACTION_IDS_DEPRECATED);
if (itr == node_ptr_->int_list_properties->end()) {
return false;
}
for (const auto supported_action : itr->second) {
if (static_cast<AXActionType>(supported_action) == action) {
return true;
}
}
return false;
}
bool AccessibilityNodeInfoDataWrapper::HasCoveringSpan(
AXStringProperty prop,
mojom::SpanType span_type) const {
if (!node_ptr_->spannable_string_properties) {
return false;
}
std::string text;
GetProperty(prop, &text);
if (text.empty()) {
return false;
}
auto span_entries_it = node_ptr_->spannable_string_properties->find(prop);
if (span_entries_it == node_ptr_->spannable_string_properties->end()) {
return false;
}
for (const auto& entry : span_entries_it->second) {
if (entry->span_type != span_type) {
continue;
}
size_t span_size = entry->end - entry->start;
if (span_size == text.size()) {
return true;
}
}
return false;
}
bool AccessibilityNodeInfoDataWrapper::HasText() const {
if (!IsImportantInAndroid()) {
return false;
}
for (const auto it : text_properties_) {
if (HasNonEmptyStringProperty(node_ptr_.get(), it)) {
return true;
}
}
return false;
}
bool AccessibilityNodeInfoDataWrapper::HasAccessibilityFocusableText() const {
if (IsWebNode()) {
return HasText();
}
if (!IsImportantInAndroid() || !HasText()) {
return false;
}
// If any ancestor has a focusable property, the text is used by that node.
AccessibilityInfoDataWrapper* parent =
tree_source_->GetFirstImportantAncestor(
const_cast<AccessibilityNodeInfoDataWrapper*>(this));
while (parent && parent->IsNode()) {
if (parent->IsAccessibilityFocusableContainer()) {
return false;
}
parent = tree_source_->GetFirstImportantAncestor(parent);
}
return true;
}
void AccessibilityNodeInfoDataWrapper::ComputeNameFromContents(
std::vector<std::string>* names) const {
std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>
children;
GetChildren(&children);
for (AccessibilityInfoDataWrapper* child : children) {
static_cast<AccessibilityNodeInfoDataWrapper*>(child)
->ComputeNameFromContentsInternal(names);
}
}
void AccessibilityNodeInfoDataWrapper::ComputeNameFromContentsInternal(
std::vector<std::string>* names) const {
if (IsWebNode() || IsAccessibilityFocusableContainer()) {
return;
}
if (IsImportantInAndroid()) {
std::string name;
for (const auto it : text_properties_) {
if (GetProperty(it, &name) && !name.empty()) {
// Stop when we get a name for this subtree.
names->push_back(name);
return;
}
}
// TalkBack reads role description by default even when reading properties
// of descendant nodes. Let's append them here to fill the gap.
// This is not in |text_properties_| because when focusing on the node that
// has role_description, then ChromeVox selectively reads the role
// description if needed.
std::string role_description;
if (GetProperty(AXStringProperty::ROLE_DESCRIPTION, &role_description) &&
!role_description.empty()) {
names->push_back(role_description);
// don't early return here. subtree may contain more text.
}
}
// Otherwise, continue looking for a name in this subtree.
std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>
children;
GetChildren(&children);
for (AccessibilityInfoDataWrapper* child : children) {
static_cast<AccessibilityNodeInfoDataWrapper*>(child)
->ComputeNameFromContentsInternal(names);
}
}
bool AccessibilityNodeInfoDataWrapper::IsClickable() const {
return GetProperty(AXBooleanProperty::CLICKABLE) ||
HasStandardAction(AXActionType::CLICK);
}
bool AccessibilityNodeInfoDataWrapper::IsLongClickable() const {
return GetProperty(AXBooleanProperty::LONG_CLICKABLE) ||
HasStandardAction(AXActionType::LONG_CLICK);
}
bool AccessibilityNodeInfoDataWrapper::IsFocusable() const {
return GetProperty(AXBooleanProperty::FOCUSABLE) ||
HasStandardAction(AXActionType::FOCUS) ||
HasStandardAction(AXActionType::CLEAR_FOCUS);
}
bool AccessibilityNodeInfoDataWrapper::IsScrollableContainer() const {
if (GetProperty(AXBooleanProperty::SCROLLABLE)) {
return true;
}
ui::AXNodeData data;
PopulateAXRole(&data);
return data.role == ax::mojom::Role::kList ||
data.role == ax::mojom::Role::kGrid ||
data.role == ax::mojom::Role::kScrollView;
}
bool AccessibilityNodeInfoDataWrapper::IsToplevelScrollItem() const {
if (!IsVisibleToUser()) {
return false;
}
AccessibilityInfoDataWrapper* parent =
tree_source_->GetFirstImportantAncestor(
const_cast<AccessibilityNodeInfoDataWrapper*>(this));
if (!parent || !parent->IsNode()) {
return false;
}
return static_cast<AccessibilityNodeInfoDataWrapper*>(parent)
->IsScrollableContainer();
}
bool AccessibilityNodeInfoDataWrapper::HasImportantProperty() const {
if (!has_important_property_cache_.has_value()) {
has_important_property_cache_ = HasImportantPropertyInternal();
}
return *has_important_property_cache_;
}
bool AccessibilityNodeInfoDataWrapper::HasImportantPropertyInternal() const {
if (HasNonEmptyStringProperty(node_ptr_.get(),
AXStringProperty::CONTENT_DESCRIPTION) ||
HasNonEmptyStringProperty(node_ptr_.get(), AXStringProperty::TEXT) ||
HasNonEmptyStringProperty(node_ptr_.get(),
AXStringProperty::PANE_TITLE) ||
HasNonEmptyStringProperty(node_ptr_.get(), AXStringProperty::HINT_TEXT)) {
return true;
}
if (IsFocusable() || IsClickable() || IsLongClickable()) {
return true;
}
// These properties are sorted in the same order of mojom file.
if (GetProperty(AXBooleanProperty::CHECKABLE) ||
GetProperty(AXBooleanProperty::SELECTED) ||
GetProperty(AXBooleanProperty::EDITABLE)) {
return true;
}
ui::AXNodeData data;
PopulateAXRole(&data);
if (ui::IsControl(data.role)) {
return true;
}
// Check if any ancestor has an important property.
std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>
children;
GetChildren(&children);
for (AccessibilityInfoDataWrapper* child : children) {
if (static_cast<AccessibilityNodeInfoDataWrapper*>(child)
->HasImportantProperty()) {
return true;
}
}
return false;
}
ax::mojom::Role AccessibilityNodeInfoDataWrapper::GetChromeRole() const {
std::string chrome_role;
std::optional<ax::mojom::Role> result;
if (GetProperty(AXStringProperty::CHROME_ROLE, &chrome_role)) {
result = ui::MaybeParseAXEnum<ax::mojom::Role>(chrome_role.c_str());
}
return result.value_or(ax::mojom::Role::kNone);
}
} // namespace ax::android