| // Copyright 2017 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 "ui/accessibility/platform/ax_snapshot_node_android_platform.h" |
| |
| #include <string> |
| |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/accessibility/ax_node.h" |
| #include "ui/accessibility/ax_serializable_tree.h" |
| #include "ui/accessibility/platform/ax_android_constants.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/transform.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| bool HasFocusableChild(const AXNode* node) { |
| for (auto* child : node->children()) { |
| if ((child->data().state & ui::AX_STATE_FOCUSABLE) != 0 || |
| HasFocusableChild(child)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool HasOnlyTextChildren(const AXNode* node) { |
| for (auto* child : node->children()) { |
| if (!child->IsTextNode()) |
| return false; |
| } |
| return true; |
| } |
| |
| // TODO(muyuanli): share with BrowserAccessibility. |
| bool IsSimpleTextControl(AXRole role, uint32_t state) { |
| switch (role) { |
| case ui::AX_ROLE_COMBO_BOX: |
| case ui::AX_ROLE_SEARCH_BOX: |
| return true; |
| case ui::AX_ROLE_TEXT_FIELD: |
| return (state & ui::AX_STATE_RICHLY_EDITABLE) == 0; |
| default: |
| return false; |
| } |
| } |
| |
| bool IsRichTextEditable(const AXNode* node) { |
| const AXNode* parent = node->parent(); |
| return (node->data().state & ui::AX_STATE_RICHLY_EDITABLE) != 0 && |
| (!parent || |
| (parent->data().state & ui::AX_STATE_RICHLY_EDITABLE) == 0); |
| } |
| |
| bool IsNativeTextControl(const AXNode* node) { |
| const std::string& html_tag = |
| node->data().GetStringAttribute(ui::AX_ATTR_HTML_TAG); |
| if (html_tag == "input") { |
| std::string input_type; |
| if (!node->data().GetHtmlAttribute("type", &input_type)) |
| return true; |
| return input_type.empty() || input_type == "email" || |
| input_type == "password" || input_type == "search" || |
| input_type == "tel" || input_type == "text" || input_type == "url" || |
| input_type == "number"; |
| } |
| return html_tag == "textarea"; |
| } |
| |
| bool IsLeaf(const AXNode* node) { |
| if (node->child_count() == 0) |
| return true; |
| |
| if (IsNativeTextControl(node) || node->IsTextNode()) { |
| return true; |
| } |
| |
| switch (node->data().role) { |
| case ui::AX_ROLE_IMAGE: |
| case ui::AX_ROLE_METER: |
| case ui::AX_ROLE_SCROLL_BAR: |
| case ui::AX_ROLE_SLIDER: |
| case ui::AX_ROLE_SPLITTER: |
| case ui::AX_ROLE_PROGRESS_INDICATOR: |
| case ui::AX_ROLE_DATE: |
| case ui::AX_ROLE_DATE_TIME: |
| case ui::AX_ROLE_INPUT_TIME: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| base::string16 GetInnerText(const AXNode* node) { |
| if (node->IsTextNode()) { |
| return node->data().GetString16Attribute(ui::AX_ATTR_NAME); |
| } |
| base::string16 text; |
| for (auto* child : node->children()) { |
| text += GetInnerText(child); |
| } |
| return text; |
| } |
| |
| base::string16 GetValue(const AXNode* node, bool show_password) { |
| base::string16 value = node->data().GetString16Attribute(ui::AX_ATTR_VALUE); |
| |
| if (value.empty() && |
| (IsSimpleTextControl(node->data().role, node->data().state) || |
| IsRichTextEditable(node)) && |
| !IsNativeTextControl(node)) { |
| value = GetInnerText(node); |
| } |
| |
| if ((node->data().state & ui::AX_STATE_PROTECTED) != 0) { |
| if (!show_password) { |
| value = base::string16(value.size(), kSecurePasswordBullet); |
| } |
| } |
| |
| return value; |
| } |
| |
| bool HasOnlyTextAndImageChildren(const AXNode* node) { |
| for (auto* child : node->children()) { |
| if (child->data().role != ui::AX_ROLE_STATIC_TEXT && |
| child->data().role != ui::AX_ROLE_IMAGE) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool IsFocusable(const AXNode* node) { |
| if (node->data().role == ui::AX_ROLE_IFRAME || |
| node->data().role == ui::AX_ROLE_IFRAME_PRESENTATIONAL || |
| (node->data().role == ui::AX_ROLE_ROOT_WEB_AREA && node->parent())) { |
| return node->data().HasStringAttribute(ui::AX_ATTR_NAME); |
| } |
| return (node->data().state & ui::AX_STATE_FOCUSABLE) != 0; |
| } |
| |
| base::string16 GetText(const AXNode* node, bool show_password) { |
| if (node->data().role == ui::AX_ROLE_WEB_AREA || |
| node->data().role == ui::AX_ROLE_IFRAME || |
| node->data().role == ui::AX_ROLE_IFRAME_PRESENTATIONAL) { |
| return base::string16(); |
| } |
| |
| if (node->data().role == ui::AX_ROLE_LIST_ITEM && |
| node->data().GetIntAttribute(ui::AX_ATTR_NAME_FROM) == |
| ui::AX_NAME_FROM_CONTENTS) { |
| if (node->child_count() > 0 && !HasOnlyTextChildren(node)) |
| return base::string16(); |
| } |
| |
| base::string16 value = GetValue(node, show_password); |
| |
| if (!value.empty()) { |
| if ((node->data().state & ui::AX_STATE_EDITABLE) != 0) |
| return value; |
| |
| switch (node->data().role) { |
| case ui::AX_ROLE_COMBO_BOX: |
| case ui::AX_ROLE_POP_UP_BUTTON: |
| case ui::AX_ROLE_TEXT_FIELD: |
| return value; |
| default: |
| break; |
| } |
| } |
| |
| if (node->data().role == ui::AX_ROLE_COLOR_WELL) { |
| unsigned int color = static_cast<unsigned int>( |
| node->data().GetIntAttribute(ui::AX_ATTR_COLOR_VALUE)); |
| unsigned int red = color >> 16 & 0xFF; |
| unsigned int green = color >> 8 & 0xFF; |
| unsigned int blue = color >> 0 & 0xFF; |
| return base::UTF8ToUTF16( |
| base::StringPrintf("#%02X%02X%02X", red, green, blue)); |
| } |
| |
| base::string16 text = node->data().GetString16Attribute(ui::AX_ATTR_NAME); |
| base::string16 description = |
| node->data().GetString16Attribute(ui::AX_ATTR_DESCRIPTION); |
| if (!description.empty()) { |
| if (!text.empty()) |
| text += base::ASCIIToUTF16(" "); |
| text += description; |
| } |
| |
| if (text.empty()) |
| text = value; |
| |
| if (node->data().role == ui::AX_ROLE_ROOT_WEB_AREA) |
| return text; |
| |
| if (text.empty() && |
| (HasOnlyTextChildren(node) || |
| (IsFocusable(node) && HasOnlyTextAndImageChildren(node)))) { |
| for (auto* child : node->children()) { |
| text += GetText(child, show_password); |
| } |
| } |
| |
| if (text.empty() && (AXSnapshotNodeAndroid::AXRoleIsLink(node->data().role) || |
| node->data().role == ui::AX_ROLE_IMAGE)) { |
| base::string16 url = node->data().GetString16Attribute(ui::AX_ATTR_URL); |
| text = AXSnapshotNodeAndroid::AXUrlBaseText(url); |
| } |
| return text; |
| } |
| |
| } // namespace |
| |
| AXSnapshotNodeAndroid::AXSnapshotNodeAndroid() = default; |
| AX_EXPORT AXSnapshotNodeAndroid::~AXSnapshotNodeAndroid() = default; |
| |
| // static |
| AX_EXPORT std::unique_ptr<AXSnapshotNodeAndroid> AXSnapshotNodeAndroid::Create( |
| const AXTreeUpdate& update, |
| bool show_password) { |
| auto tree = base::MakeUnique<ui::AXSerializableTree>(); |
| if (!tree->Unserialize(update)) { |
| LOG(FATAL) << tree->error(); |
| } |
| |
| WalkAXTreeConfig config{ |
| false, // should_select_leaf |
| show_password // show_password |
| }; |
| return WalkAXTreeDepthFirst(tree->root(), gfx::Rect(), update, tree.get(), |
| config); |
| } |
| |
| // static |
| AX_EXPORT bool AXSnapshotNodeAndroid::AXRoleIsLink(AXRole role) { |
| return role == ui::AX_ROLE_LINK || role == ui::AX_ROLE_IMAGE_MAP_LINK; |
| } |
| |
| // static |
| AX_EXPORT base::string16 AXSnapshotNodeAndroid::AXUrlBaseText( |
| base::string16 url) { |
| // Given a url like http://foo.com/bar/baz.png, just return the |
| // base text, e.g., "baz". |
| int trailing_slashes = 0; |
| while (url.size() - trailing_slashes > 0 && |
| url[url.size() - trailing_slashes - 1] == '/') { |
| trailing_slashes++; |
| } |
| if (trailing_slashes) |
| url = url.substr(0, url.size() - trailing_slashes); |
| size_t slash_index = url.rfind('/'); |
| if (slash_index != std::string::npos) |
| url = url.substr(slash_index + 1); |
| size_t dot_index = url.rfind('.'); |
| if (dot_index != std::string::npos) |
| url = url.substr(0, dot_index); |
| return url; |
| } |
| |
| // static |
| AX_EXPORT const char* AXSnapshotNodeAndroid::AXRoleToAndroidClassName( |
| AXRole role, |
| bool has_parent) { |
| switch (role) { |
| case ui::AX_ROLE_SEARCH_BOX: |
| case ui::AX_ROLE_SPIN_BUTTON: |
| case ui::AX_ROLE_TEXT_FIELD: |
| return ui::kAXEditTextClassname; |
| case ui::AX_ROLE_SLIDER: |
| return ui::kAXSeekBarClassname; |
| case ui::AX_ROLE_COLOR_WELL: |
| case ui::AX_ROLE_COMBO_BOX: |
| case ui::AX_ROLE_DATE: |
| case ui::AX_ROLE_POP_UP_BUTTON: |
| case ui::AX_ROLE_INPUT_TIME: |
| return ui::kAXSpinnerClassname; |
| case ui::AX_ROLE_BUTTON: |
| case ui::AX_ROLE_MENU_BUTTON: |
| return ui::kAXButtonClassname; |
| case ui::AX_ROLE_CHECK_BOX: |
| case ui::AX_ROLE_SWITCH: |
| return ui::kAXCheckBoxClassname; |
| case ui::AX_ROLE_RADIO_BUTTON: |
| return ui::kAXRadioButtonClassname; |
| case ui::AX_ROLE_TOGGLE_BUTTON: |
| return ui::kAXToggleButtonClassname; |
| case ui::AX_ROLE_CANVAS: |
| case ui::AX_ROLE_IMAGE: |
| case ui::AX_ROLE_SVG_ROOT: |
| return ui::kAXImageClassname; |
| case ui::AX_ROLE_METER: |
| case ui::AX_ROLE_PROGRESS_INDICATOR: |
| return ui::kAXProgressBarClassname; |
| case ui::AX_ROLE_TAB_LIST: |
| return ui::kAXTabWidgetClassname; |
| case ui::AX_ROLE_GRID: |
| case ui::AX_ROLE_TREE_GRID: |
| case ui::AX_ROLE_TABLE: |
| return ui::kAXGridViewClassname; |
| case ui::AX_ROLE_LIST: |
| case ui::AX_ROLE_LIST_BOX: |
| case ui::AX_ROLE_DESCRIPTION_LIST: |
| return ui::kAXListViewClassname; |
| case ui::AX_ROLE_DIALOG: |
| return ui::kAXDialogClassname; |
| case ui::AX_ROLE_ROOT_WEB_AREA: |
| return has_parent ? ui::kAXViewClassname : ui::kAXWebViewClassname; |
| case ui::AX_ROLE_MENU_ITEM: |
| case ui::AX_ROLE_MENU_ITEM_CHECK_BOX: |
| case ui::AX_ROLE_MENU_ITEM_RADIO: |
| return ui::kAXMenuItemClassname; |
| default: |
| return ui::kAXViewClassname; |
| } |
| } |
| |
| // static |
| std::unique_ptr<AXSnapshotNodeAndroid> |
| AXSnapshotNodeAndroid::WalkAXTreeDepthFirst( |
| const AXNode* node, |
| gfx::Rect rect, |
| const ui::AXTreeUpdate& update, |
| const AXTree* tree, |
| AXSnapshotNodeAndroid::WalkAXTreeConfig& config) { |
| auto result = |
| std::unique_ptr<AXSnapshotNodeAndroid>(new AXSnapshotNodeAndroid()); |
| result->text = GetText(node, config.show_password); |
| result->class_name = AXSnapshotNodeAndroid::AXRoleToAndroidClassName( |
| node->data().role, node->parent() != nullptr); |
| |
| result->text_size = -1.0; |
| result->bgcolor = 0; |
| result->color = 0; |
| result->bold = 0; |
| result->italic = 0; |
| result->line_through = 0; |
| result->underline = 0; |
| |
| if (node->data().HasFloatAttribute(ui::AX_ATTR_FONT_SIZE)) { |
| gfx::RectF text_size_rect( |
| 0, 0, 1, node->data().GetFloatAttribute(ui::AX_ATTR_FONT_SIZE)); |
| gfx::Rect scaled_text_size_rect = |
| gfx::ToEnclosingRect(tree->RelativeToTreeBounds(node, text_size_rect)); |
| result->text_size = scaled_text_size_rect.height(); |
| |
| const int text_style = node->data().GetIntAttribute(ui::AX_ATTR_TEXT_STYLE); |
| result->color = node->data().GetIntAttribute(ui::AX_ATTR_COLOR); |
| result->bgcolor = |
| node->data().GetIntAttribute(ui::AX_ATTR_BACKGROUND_COLOR); |
| result->bold = (text_style & ui::AX_TEXT_STYLE_BOLD) != 0; |
| result->italic = (text_style & ui::AX_TEXT_STYLE_ITALIC) != 0; |
| result->line_through = (text_style & ui::AX_TEXT_STYLE_LINE_THROUGH) != 0; |
| result->underline = (text_style & ui::AX_TEXT_STYLE_UNDERLINE) != 0; |
| } |
| |
| const gfx::Rect& absolute_rect = |
| gfx::ToEnclosingRect(tree->GetTreeBounds(node)); |
| gfx::Rect parent_relative_rect = absolute_rect; |
| bool is_root = node->parent() == nullptr; |
| if (!is_root) { |
| parent_relative_rect.Offset(-rect.OffsetFromOrigin()); |
| } |
| result->rect = gfx::Rect(parent_relative_rect.x(), parent_relative_rect.y(), |
| absolute_rect.width(), absolute_rect.height()); |
| result->has_selection = false; |
| |
| if (IsLeaf(node) && update.has_tree_data) { |
| int start_selection = 0; |
| int end_selection = 0; |
| if (update.tree_data.sel_anchor_object_id == node->id()) { |
| start_selection = update.tree_data.sel_anchor_offset; |
| config.should_select_leaf = true; |
| } |
| |
| if (config.should_select_leaf) { |
| end_selection = |
| static_cast<int32_t>(GetText(node, config.show_password).length()); |
| } |
| |
| if (update.tree_data.sel_focus_object_id == node->id()) { |
| end_selection = update.tree_data.sel_focus_offset; |
| config.should_select_leaf = false; |
| } |
| if (end_selection > 0) { |
| result->has_selection = true; |
| result->start_selection = start_selection; |
| result->end_selection = end_selection; |
| } |
| } |
| |
| for (auto* child : node->children()) { |
| result->children.push_back( |
| WalkAXTreeDepthFirst(child, absolute_rect, update, tree, config)); |
| } |
| |
| return result; |
| } |
| |
| } // namespace ui |