OLD | NEW |
(Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "ui/accessibility/platform/ax_snapshot_node_android_platform.h" |
| 6 |
| 7 #include <string> |
| 8 |
| 9 #include "base/logging.h" |
| 10 #include "base/memory/ptr_util.h" |
| 11 #include "base/strings/stringprintf.h" |
| 12 #include "base/strings/utf_string_conversions.h" |
| 13 #include "ui/accessibility/ax_node.h" |
| 14 #include "ui/accessibility/ax_serializable_tree.h" |
| 15 #include "ui/accessibility/platform/ax_android_constants.h" |
| 16 #include "ui/gfx/geometry/rect_conversions.h" |
| 17 #include "ui/gfx/transform.h" |
| 18 |
| 19 namespace ui { |
| 20 |
| 21 namespace { |
| 22 |
| 23 bool HasFocusableChild(const AXNode* node) { |
| 24 for (auto* child : node->children()) { |
| 25 if ((child->data().state & ui::AX_STATE_FOCUSABLE) != 0 || |
| 26 HasFocusableChild(child)) { |
| 27 return true; |
| 28 } |
| 29 } |
| 30 return false; |
| 31 } |
| 32 |
| 33 gfx::Rect RelativeToAbsoluteBounds(const AXNode* node, |
| 34 gfx::RectF bounds, |
| 35 const AXTree* tree) { |
| 36 const AXNode* current = node; |
| 37 while (current != nullptr) { |
| 38 if (current->data().transform) |
| 39 current->data().transform->TransformRect(&bounds); |
| 40 auto* container = tree->GetFromId(current->data().offset_container_id); |
| 41 if (!container) { |
| 42 if (current == tree->root()) |
| 43 container = current->parent(); |
| 44 else |
| 45 container = tree->root(); |
| 46 } |
| 47 if (!container || container == current) |
| 48 break; |
| 49 |
| 50 gfx::RectF container_bounds = container->data().location; |
| 51 bounds.Offset(container_bounds.x(), container_bounds.y()); |
| 52 current = container; |
| 53 } |
| 54 return gfx::ToEnclosingRect(bounds); |
| 55 } |
| 56 |
| 57 void FixEmptyBounds(const AXNode* node, gfx::RectF* bounds, const AXTree* tree); |
| 58 |
| 59 gfx::Rect GetPageBoundsRect(const AXNode* node, const AXTree* tree) { |
| 60 gfx::RectF bounds = node->data().location; |
| 61 FixEmptyBounds(node, &bounds, tree); |
| 62 return RelativeToAbsoluteBounds(node, bounds, tree); |
| 63 } |
| 64 |
| 65 void FixEmptyBounds(const AXNode* node, |
| 66 gfx::RectF* bounds, |
| 67 const AXTree* tree) { |
| 68 if (bounds->width() > 0 && bounds->height() > 0) |
| 69 return; |
| 70 for (auto* child : node->children()) { |
| 71 gfx::Rect child_bounds = GetPageBoundsRect(child, tree); |
| 72 if (child_bounds.width() == 0 || child_bounds.height() == 0) |
| 73 continue; |
| 74 if (bounds->width() == 0 || bounds->height() == 0) { |
| 75 *bounds = gfx::RectF(child_bounds); |
| 76 continue; |
| 77 } |
| 78 bounds->Union(gfx::RectF(child_bounds)); |
| 79 } |
| 80 } |
| 81 |
| 82 bool HasOnlyTextChildren(const AXNode* node) { |
| 83 for (auto* child : node->children()) { |
| 84 if (!child->IsTextNode()) |
| 85 return false; |
| 86 } |
| 87 return true; |
| 88 } |
| 89 |
| 90 // TODO(muyuanli): share with BrowserAccessibility. |
| 91 bool IsSimpleTextControl(AXRole role, uint32_t state) { |
| 92 switch (role) { |
| 93 case ui::AX_ROLE_COMBO_BOX: |
| 94 case ui::AX_ROLE_SEARCH_BOX: |
| 95 return true; |
| 96 case ui::AX_ROLE_TEXT_FIELD: |
| 97 return (state & ui::AX_STATE_RICHLY_EDITABLE) == 0; |
| 98 default: |
| 99 return false; |
| 100 } |
| 101 } |
| 102 |
| 103 bool IsRichTextEditable(const AXNode* node) { |
| 104 const AXNode* parent = node->parent(); |
| 105 return (node->data().state & ui::AX_STATE_RICHLY_EDITABLE) != 0 && |
| 106 (!parent || |
| 107 (parent->data().state & ui::AX_STATE_RICHLY_EDITABLE) == 0); |
| 108 } |
| 109 |
| 110 bool IsNativeTextControl(const AXNode* node) { |
| 111 const std::string& html_tag = |
| 112 node->data().GetStringAttribute(ui::AX_ATTR_HTML_TAG); |
| 113 if (html_tag == "input") { |
| 114 std::string input_type; |
| 115 if (!node->data().GetHtmlAttribute("type", &input_type)) |
| 116 return true; |
| 117 return input_type.empty() || input_type == "email" || |
| 118 input_type == "password" || input_type == "search" || |
| 119 input_type == "tel" || input_type == "text" || input_type == "url" || |
| 120 input_type == "number"; |
| 121 } |
| 122 return html_tag == "textarea"; |
| 123 } |
| 124 |
| 125 bool IsLeaf(const AXNode* node) { |
| 126 if (node->child_count() == 0) |
| 127 return true; |
| 128 |
| 129 if (IsNativeTextControl(node) || node->IsTextNode()) { |
| 130 return true; |
| 131 } |
| 132 |
| 133 switch (node->data().role) { |
| 134 case ui::AX_ROLE_IMAGE: |
| 135 case ui::AX_ROLE_METER: |
| 136 case ui::AX_ROLE_SCROLL_BAR: |
| 137 case ui::AX_ROLE_SLIDER: |
| 138 case ui::AX_ROLE_SPLITTER: |
| 139 case ui::AX_ROLE_PROGRESS_INDICATOR: |
| 140 case ui::AX_ROLE_DATE: |
| 141 case ui::AX_ROLE_DATE_TIME: |
| 142 case ui::AX_ROLE_INPUT_TIME: |
| 143 return true; |
| 144 default: |
| 145 return false; |
| 146 } |
| 147 } |
| 148 |
| 149 base::string16 GetInnerText(const AXNode* node) { |
| 150 if (node->IsTextNode()) { |
| 151 return node->data().GetString16Attribute(ui::AX_ATTR_NAME); |
| 152 } |
| 153 base::string16 text; |
| 154 for (auto* child : node->children()) { |
| 155 text += GetInnerText(child); |
| 156 } |
| 157 return text; |
| 158 } |
| 159 |
| 160 base::string16 GetValue(const AXNode* node, bool show_password) { |
| 161 base::string16 value = node->data().GetString16Attribute(ui::AX_ATTR_VALUE); |
| 162 |
| 163 if (value.empty() && |
| 164 (IsSimpleTextControl(node->data().role, node->data().state) || |
| 165 IsRichTextEditable(node)) && |
| 166 !IsNativeTextControl(node)) { |
| 167 value = GetInnerText(node); |
| 168 } |
| 169 |
| 170 if ((node->data().state & ui::AX_STATE_PROTECTED) != 0) { |
| 171 if (!show_password) { |
| 172 value = base::string16(value.size(), kSecurePasswordBullet); |
| 173 } |
| 174 } |
| 175 |
| 176 return value; |
| 177 } |
| 178 |
| 179 bool HasOnlyTextAndImageChildren(const AXNode* node) { |
| 180 for (auto* child : node->children()) { |
| 181 if (child->data().role != ui::AX_ROLE_STATIC_TEXT && |
| 182 child->data().role != ui::AX_ROLE_IMAGE) { |
| 183 return false; |
| 184 } |
| 185 } |
| 186 return true; |
| 187 } |
| 188 |
| 189 bool IsFocusable(const AXNode* node) { |
| 190 if (node->data().role == ui::AX_ROLE_IFRAME || |
| 191 node->data().role == ui::AX_ROLE_IFRAME_PRESENTATIONAL || |
| 192 (node->data().role == ui::AX_ROLE_ROOT_WEB_AREA && node->parent())) { |
| 193 return node->data().HasStringAttribute(ui::AX_ATTR_NAME); |
| 194 } |
| 195 return (node->data().state & ui::AX_STATE_FOCUSABLE) != 0; |
| 196 } |
| 197 |
| 198 base::string16 GetText(const AXNode* node, bool show_password) { |
| 199 if (node->data().role == ui::AX_ROLE_WEB_AREA || |
| 200 node->data().role == ui::AX_ROLE_IFRAME || |
| 201 node->data().role == ui::AX_ROLE_IFRAME_PRESENTATIONAL) { |
| 202 return base::string16(); |
| 203 } |
| 204 |
| 205 if (node->data().role == ui::AX_ROLE_LIST_ITEM && |
| 206 node->data().GetIntAttribute(ui::AX_ATTR_NAME_FROM) == |
| 207 ui::AX_NAME_FROM_CONTENTS) { |
| 208 if (node->child_count() > 0 && !HasOnlyTextChildren(node)) |
| 209 return base::string16(); |
| 210 } |
| 211 |
| 212 base::string16 value = GetValue(node, show_password); |
| 213 |
| 214 if (!value.empty()) { |
| 215 if ((node->data().state & ui::AX_STATE_EDITABLE) != 0) |
| 216 return value; |
| 217 |
| 218 switch (node->data().role) { |
| 219 case ui::AX_ROLE_COMBO_BOX: |
| 220 case ui::AX_ROLE_POP_UP_BUTTON: |
| 221 case ui::AX_ROLE_TEXT_FIELD: |
| 222 return value; |
| 223 default: |
| 224 break; |
| 225 } |
| 226 } |
| 227 |
| 228 if (node->data().role == ui::AX_ROLE_COLOR_WELL) { |
| 229 unsigned int color = static_cast<unsigned int>( |
| 230 node->data().GetIntAttribute(ui::AX_ATTR_COLOR_VALUE)); |
| 231 unsigned int red = color >> 16 & 0xFF; |
| 232 unsigned int green = color >> 8 & 0xFF; |
| 233 unsigned int blue = color >> 0 & 0xFF; |
| 234 return base::UTF8ToUTF16( |
| 235 base::StringPrintf("#%02X%02X%02X", red, green, blue)); |
| 236 } |
| 237 |
| 238 base::string16 text = node->data().GetString16Attribute(ui::AX_ATTR_NAME); |
| 239 base::string16 description = |
| 240 node->data().GetString16Attribute(ui::AX_ATTR_DESCRIPTION); |
| 241 if (!description.empty()) { |
| 242 if (!text.empty()) |
| 243 text += base::ASCIIToUTF16(" "); |
| 244 text += description; |
| 245 } |
| 246 |
| 247 if (text.empty()) |
| 248 text = value; |
| 249 |
| 250 if (node->data().role == ui::AX_ROLE_ROOT_WEB_AREA) |
| 251 return text; |
| 252 |
| 253 if (text.empty() && |
| 254 (HasOnlyTextChildren(node) || |
| 255 (IsFocusable(node) && HasOnlyTextAndImageChildren(node)))) { |
| 256 for (auto* child : node->children()) { |
| 257 text += GetText(child, show_password); |
| 258 } |
| 259 } |
| 260 |
| 261 if (text.empty() && (AXSnapshotNodeAndroid::AXRoleIsLink(node->data().role) || |
| 262 node->data().role == ui::AX_ROLE_IMAGE)) { |
| 263 base::string16 url = node->data().GetString16Attribute(ui::AX_ATTR_URL); |
| 264 text = AXSnapshotNodeAndroid::AXUrlBaseText(url); |
| 265 } |
| 266 return text; |
| 267 } |
| 268 |
| 269 } // namespace |
| 270 |
| 271 AXSnapshotNodeAndroid::AXSnapshotNodeAndroid() = default; |
| 272 AX_EXPORT AXSnapshotNodeAndroid::~AXSnapshotNodeAndroid() = default; |
| 273 |
| 274 // static |
| 275 AX_EXPORT std::unique_ptr<AXSnapshotNodeAndroid> AXSnapshotNodeAndroid::Create( |
| 276 const AXTreeUpdate& update, |
| 277 bool show_password) { |
| 278 auto tree = base::MakeUnique<ui::AXSerializableTree>(); |
| 279 if (!tree->Unserialize(update)) { |
| 280 LOG(FATAL) << tree->error(); |
| 281 } |
| 282 |
| 283 WalkAXTreeConfig config{ |
| 284 false, // should_select_leaf |
| 285 show_password // show_password |
| 286 }; |
| 287 return WalkAXTreeDepthFirst(tree->root(), gfx::Rect(), update, tree.get(), |
| 288 config); |
| 289 } |
| 290 |
| 291 // static |
| 292 AX_EXPORT bool AXSnapshotNodeAndroid::AXRoleIsLink(AXRole role) { |
| 293 return role == ui::AX_ROLE_LINK || role == ui::AX_ROLE_IMAGE_MAP_LINK; |
| 294 } |
| 295 |
| 296 // static |
| 297 AX_EXPORT base::string16 AXSnapshotNodeAndroid::AXUrlBaseText( |
| 298 base::string16 url) { |
| 299 // Given a url like http://foo.com/bar/baz.png, just return the |
| 300 // base text, e.g., "baz". |
| 301 int trailing_slashes = 0; |
| 302 while (url.size() - trailing_slashes > 0 && |
| 303 url[url.size() - trailing_slashes - 1] == '/') { |
| 304 trailing_slashes++; |
| 305 } |
| 306 if (trailing_slashes) |
| 307 url = url.substr(0, url.size() - trailing_slashes); |
| 308 size_t slash_index = url.rfind('/'); |
| 309 if (slash_index != std::string::npos) |
| 310 url = url.substr(slash_index + 1); |
| 311 size_t dot_index = url.rfind('.'); |
| 312 if (dot_index != std::string::npos) |
| 313 url = url.substr(0, dot_index); |
| 314 return url; |
| 315 } |
| 316 |
| 317 // static |
| 318 AX_EXPORT const char* AXSnapshotNodeAndroid::AXRoleToAndroidClassName( |
| 319 AXRole role, |
| 320 bool has_parent) { |
| 321 switch (role) { |
| 322 case ui::AX_ROLE_SEARCH_BOX: |
| 323 case ui::AX_ROLE_SPIN_BUTTON: |
| 324 case ui::AX_ROLE_TEXT_FIELD: |
| 325 return ui::kAXEditTextClassname; |
| 326 case ui::AX_ROLE_SLIDER: |
| 327 return ui::kAXSeekBarClassname; |
| 328 case ui::AX_ROLE_COLOR_WELL: |
| 329 case ui::AX_ROLE_COMBO_BOX: |
| 330 case ui::AX_ROLE_DATE: |
| 331 case ui::AX_ROLE_POP_UP_BUTTON: |
| 332 case ui::AX_ROLE_INPUT_TIME: |
| 333 return ui::kAXSpinnerClassname; |
| 334 case ui::AX_ROLE_BUTTON: |
| 335 case ui::AX_ROLE_MENU_BUTTON: |
| 336 return ui::kAXButtonClassname; |
| 337 case ui::AX_ROLE_CHECK_BOX: |
| 338 case ui::AX_ROLE_SWITCH: |
| 339 return ui::kAXCheckBoxClassname; |
| 340 case ui::AX_ROLE_RADIO_BUTTON: |
| 341 return ui::kAXRadioButtonClassname; |
| 342 case ui::AX_ROLE_TOGGLE_BUTTON: |
| 343 return ui::kAXToggleButtonClassname; |
| 344 case ui::AX_ROLE_CANVAS: |
| 345 case ui::AX_ROLE_IMAGE: |
| 346 case ui::AX_ROLE_SVG_ROOT: |
| 347 return ui::kAXImageClassname; |
| 348 case ui::AX_ROLE_METER: |
| 349 case ui::AX_ROLE_PROGRESS_INDICATOR: |
| 350 return ui::kAXProgressBarClassname; |
| 351 case ui::AX_ROLE_TAB_LIST: |
| 352 return ui::kAXTabWidgetClassname; |
| 353 case ui::AX_ROLE_GRID: |
| 354 case ui::AX_ROLE_TREE_GRID: |
| 355 case ui::AX_ROLE_TABLE: |
| 356 return ui::kAXGridViewClassname; |
| 357 case ui::AX_ROLE_LIST: |
| 358 case ui::AX_ROLE_LIST_BOX: |
| 359 case ui::AX_ROLE_DESCRIPTION_LIST: |
| 360 return ui::kAXListViewClassname; |
| 361 case ui::AX_ROLE_DIALOG: |
| 362 return ui::kAXDialogClassname; |
| 363 case ui::AX_ROLE_ROOT_WEB_AREA: |
| 364 return has_parent ? ui::kAXViewClassname : ui::kAXWebViewClassname; |
| 365 case ui::AX_ROLE_MENU_ITEM: |
| 366 case ui::AX_ROLE_MENU_ITEM_CHECK_BOX: |
| 367 case ui::AX_ROLE_MENU_ITEM_RADIO: |
| 368 return ui::kAXMenuItemClassname; |
| 369 default: |
| 370 return ui::kAXViewClassname; |
| 371 } |
| 372 } |
| 373 |
| 374 // static |
| 375 std::unique_ptr<AXSnapshotNodeAndroid> |
| 376 AXSnapshotNodeAndroid::WalkAXTreeDepthFirst( |
| 377 const AXNode* node, |
| 378 gfx::Rect rect, |
| 379 const ui::AXTreeUpdate& update, |
| 380 const AXTree* tree, |
| 381 AXSnapshotNodeAndroid::WalkAXTreeConfig& config) { |
| 382 auto result = |
| 383 std::unique_ptr<AXSnapshotNodeAndroid>(new AXSnapshotNodeAndroid()); |
| 384 result->text = GetText(node, config.show_password); |
| 385 result->class_name = AXSnapshotNodeAndroid::AXRoleToAndroidClassName( |
| 386 node->data().role, node->parent() != nullptr); |
| 387 |
| 388 result->text_size = -1.0; |
| 389 result->bgcolor = 0; |
| 390 result->color = 0; |
| 391 result->bold = 0; |
| 392 result->italic = 0; |
| 393 result->line_through = 0; |
| 394 result->underline = 0; |
| 395 |
| 396 if (node->data().HasFloatAttribute(ui::AX_ATTR_FONT_SIZE)) { |
| 397 gfx::RectF text_size_rect( |
| 398 0, 0, 1, node->data().GetFloatAttribute(ui::AX_ATTR_FONT_SIZE)); |
| 399 gfx::Rect scaled_text_size_rect = |
| 400 RelativeToAbsoluteBounds(node, text_size_rect, tree); |
| 401 result->text_size = scaled_text_size_rect.height(); |
| 402 |
| 403 const int text_style = node->data().GetIntAttribute(ui::AX_ATTR_TEXT_STYLE); |
| 404 result->color = node->data().GetIntAttribute(ui::AX_ATTR_COLOR); |
| 405 result->bgcolor = |
| 406 node->data().GetIntAttribute(ui::AX_ATTR_BACKGROUND_COLOR); |
| 407 result->bold = (text_style & ui::AX_TEXT_STYLE_BOLD) != 0; |
| 408 result->italic = (text_style & ui::AX_TEXT_STYLE_ITALIC) != 0; |
| 409 result->line_through = (text_style & ui::AX_TEXT_STYLE_LINE_THROUGH) != 0; |
| 410 result->underline = (text_style & ui::AX_TEXT_STYLE_UNDERLINE) != 0; |
| 411 } |
| 412 |
| 413 const gfx::Rect& absolute_rect = GetPageBoundsRect(node, tree); |
| 414 gfx::Rect parent_relative_rect = absolute_rect; |
| 415 bool is_root = node->parent() == nullptr; |
| 416 if (!is_root) { |
| 417 parent_relative_rect.Offset(-rect.OffsetFromOrigin()); |
| 418 } |
| 419 result->rect = gfx::Rect(parent_relative_rect.x(), parent_relative_rect.y(), |
| 420 absolute_rect.width(), absolute_rect.height()); |
| 421 result->has_selection = false; |
| 422 |
| 423 if (IsLeaf(node) && update.has_tree_data) { |
| 424 int start_selection = 0; |
| 425 int end_selection = 0; |
| 426 if (update.tree_data.sel_anchor_object_id == node->id()) { |
| 427 start_selection = update.tree_data.sel_anchor_offset; |
| 428 config.should_select_leaf = true; |
| 429 } |
| 430 |
| 431 if (config.should_select_leaf) { |
| 432 end_selection = |
| 433 static_cast<int32_t>(GetText(node, config.show_password).length()); |
| 434 } |
| 435 |
| 436 if (update.tree_data.sel_focus_object_id == node->id()) { |
| 437 end_selection = update.tree_data.sel_focus_offset; |
| 438 config.should_select_leaf = false; |
| 439 } |
| 440 if (end_selection > 0) { |
| 441 result->has_selection = true; |
| 442 result->start_selection = start_selection; |
| 443 result->end_selection = end_selection; |
| 444 } |
| 445 } |
| 446 |
| 447 for (auto* child : node->children()) { |
| 448 result->children.push_back( |
| 449 WalkAXTreeDepthFirst(child, absolute_rect, update, tree, config)); |
| 450 } |
| 451 |
| 452 return result; |
| 453 } |
| 454 |
| 455 } // namespace ui |
OLD | NEW |