1 // Copyright 2013 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 "content/browser/accessibility/browser_accessibility_android.h" 6 7 #include "base/strings/utf_string_conversions.h" 8 #include "content/browser/accessibility/browser_accessibility_manager_android.h" 9 #include "content/common/accessibility_messages.h" 10 #include "content/common/accessibility_node_data.h" 11 12 namespace { 13 14 // These are enums from android.text.InputType in Java: 15 enum { 16 ANDROID_TEXT_INPUTTYPE_TYPE_NULL = 0, 17 ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME = 0x4, 18 ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME_DATE = 0x14, 19 ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME_TIME = 0x24, 20 ANDROID_TEXT_INPUTTYPE_TYPE_NUMBER = 0x2, 21 ANDROID_TEXT_INPUTTYPE_TYPE_PHONE = 0x3, 22 ANDROID_TEXT_INPUTTYPE_TYPE_TEXT = 0x1, 23 ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_URI = 0x11, 24 ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_WEB_EDIT_TEXT = 0xa1, 25 ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_WEB_EMAIL = 0xd1, 26 ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_WEB_PASSWORD = 0xe1 27 }; 28 29 // These are enums from android.view.View in Java: 30 enum { 31 ANDROID_VIEW_VIEW_ACCESSIBILITY_LIVE_REGION_NONE = 0, 32 ANDROID_VIEW_VIEW_ACCESSIBILITY_LIVE_REGION_POLITE = 1, 33 ANDROID_VIEW_VIEW_ACCESSIBILITY_LIVE_REGION_ASSERTIVE = 2 34 }; 35 36 // These are enums from 37 // android.view.accessibility.AccessibilityNodeInfo.RangeInfo in Java: 38 enum { 39 ANDROID_VIEW_ACCESSIBILITY_RANGE_TYPE_FLOAT = 1 40 }; 41 42 } // namespace 43 44 namespace content { 45 46 // static 47 BrowserAccessibility* BrowserAccessibility::Create() { 48 return new BrowserAccessibilityAndroid(); 49 } 50 51 BrowserAccessibilityAndroid::BrowserAccessibilityAndroid() { 52 first_time_ = true; 53 } 54 55 bool BrowserAccessibilityAndroid::IsNative() const { 56 return true; 57 } 58 59 bool BrowserAccessibilityAndroid::PlatformIsLeaf() const { 60 if (child_count() == 0) 61 return true; 62 63 // Iframes are always allowed to contain children. 64 if (IsIframe() || 65 role() == blink::WebAXRoleRootWebArea || 66 role() == blink::WebAXRoleWebArea) { 67 return false; 68 } 69 70 // If it has a focusable child, we definitely can't leave out children. 71 if (HasFocusableChild()) 72 return false; 73 74 // Headings with text can drop their children. 75 base::string16 name = GetText(); 76 if (role() == blink::WebAXRoleHeading && !name.empty()) 77 return true; 78 79 // Focusable nodes with text can drop their children. 80 if (HasState(blink::WebAXStateFocusable) && !name.empty()) 81 return true; 82 83 // Nodes with only static text as children can drop their children. 84 if (HasOnlyStaticTextChildren()) 85 return true; 86 87 return BrowserAccessibility::PlatformIsLeaf(); 88 } 89 90 bool BrowserAccessibilityAndroid::IsCheckable() const { 91 bool checkable = false; 92 bool is_aria_pressed_defined; 93 bool is_mixed; 94 GetAriaTristate("aria-pressed", &is_aria_pressed_defined, &is_mixed); 95 if (role() == blink::WebAXRoleCheckBox || 96 role() == blink::WebAXRoleRadioButton || 97 is_aria_pressed_defined) { 98 checkable = true; 99 } 100 if (HasState(blink::WebAXStateChecked)) 101 checkable = true; 102 return checkable; 103 } 104 105 bool BrowserAccessibilityAndroid::IsChecked() const { 106 return HasState(blink::WebAXStateChecked); 107 } 108 109 bool BrowserAccessibilityAndroid::IsClickable() const { 110 return (PlatformIsLeaf() && !GetText().empty()); 111 } 112 113 bool BrowserAccessibilityAndroid::IsCollection() const { 114 return (role() == blink::WebAXRoleGrid || 115 role() == blink::WebAXRoleList || 116 role() == blink::WebAXRoleListBox || 117 role() == blink::WebAXRoleTable || 118 role() == blink::WebAXRoleTree); 119 } 120 121 bool BrowserAccessibilityAndroid::IsCollectionItem() const { 122 return (role() == blink::WebAXRoleCell || 123 role() == blink::WebAXRoleColumnHeader || 124 role() == blink::WebAXRoleDescriptionListTerm || 125 role() == blink::WebAXRoleListBoxOption || 126 role() == blink::WebAXRoleListItem || 127 role() == blink::WebAXRoleRowHeader || 128 role() == blink::WebAXRoleTreeItem); 129 } 130 131 bool BrowserAccessibilityAndroid::IsContentInvalid() const { 132 std::string invalid; 133 return GetHtmlAttribute("aria-invalid", &invalid); 134 } 135 136 bool BrowserAccessibilityAndroid::IsDismissable() const { 137 return false; // No concept of "dismissable" on the web currently. 138 } 139 140 bool BrowserAccessibilityAndroid::IsEnabled() const { 141 return HasState(blink::WebAXStateEnabled); 142 } 143 144 bool BrowserAccessibilityAndroid::IsFocusable() const { 145 bool focusable = HasState(blink::WebAXStateFocusable); 146 if (IsIframe() || 147 role() == blink::WebAXRoleWebArea) { 148 focusable = false; 149 } 150 return focusable; 151 } 152 153 bool BrowserAccessibilityAndroid::IsFocused() const { 154 return manager()->GetFocus(manager()->GetRoot()) == this; 155 } 156 157 bool BrowserAccessibilityAndroid::IsHeading() const { 158 return (role() == blink::WebAXRoleColumnHeader || 159 role() == blink::WebAXRoleHeading || 160 role() == blink::WebAXRoleRowHeader); 161 } 162 163 bool BrowserAccessibilityAndroid::IsHierarchical() const { 164 return (role() == blink::WebAXRoleList || 165 role() == blink::WebAXRoleTree); 166 } 167 168 bool BrowserAccessibilityAndroid::IsMultiLine() const { 169 return role() == blink::WebAXRoleTextArea; 170 } 171 172 bool BrowserAccessibilityAndroid::IsPassword() const { 173 return HasState(blink::WebAXStateProtected); 174 } 175 176 bool BrowserAccessibilityAndroid::IsRangeType() const { 177 return (role() == blink::WebAXRoleProgressIndicator || 178 role() == blink::WebAXRoleScrollBar || 179 role() == blink::WebAXRoleSlider); 180 } 181 182 bool BrowserAccessibilityAndroid::IsScrollable() const { 183 int dummy; 184 return GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &dummy); 185 } 186 187 bool BrowserAccessibilityAndroid::IsSelected() const { 188 return HasState(blink::WebAXStateSelected); 189 } 190 191 bool BrowserAccessibilityAndroid::IsVisibleToUser() const { 192 return !HasState(blink::WebAXStateInvisible); 193 } 194 195 bool BrowserAccessibilityAndroid::CanOpenPopup() const { 196 return HasState(blink::WebAXStateHaspopup); 197 } 198 199 const char* BrowserAccessibilityAndroid::GetClassName() const { 200 const char* class_name = NULL; 201 202 switch(role()) { 203 case blink::WebAXRoleEditableText: 204 case blink::WebAXRoleSpinButton: 205 case blink::WebAXRoleTextArea: 206 case blink::WebAXRoleTextField: 207 class_name = "android.widget.EditText"; 208 break; 209 case blink::WebAXRoleSlider: 210 class_name = "android.widget.SeekBar"; 211 break; 212 case blink::WebAXRoleComboBox: 213 class_name = "android.widget.Spinner"; 214 break; 215 case blink::WebAXRoleButton: 216 case blink::WebAXRoleMenuButton: 217 case blink::WebAXRolePopUpButton: 218 class_name = "android.widget.Button"; 219 break; 220 case blink::WebAXRoleCheckBox: 221 class_name = "android.widget.CheckBox"; 222 break; 223 case blink::WebAXRoleRadioButton: 224 class_name = "android.widget.RadioButton"; 225 break; 226 case blink::WebAXRoleToggleButton: 227 class_name = "android.widget.ToggleButton"; 228 break; 229 case blink::WebAXRoleCanvas: 230 case blink::WebAXRoleImage: 231 class_name = "android.widget.Image"; 232 break; 233 case blink::WebAXRoleProgressIndicator: 234 class_name = "android.widget.ProgressBar"; 235 break; 236 case blink::WebAXRoleTabList: 237 class_name = "android.widget.TabWidget"; 238 break; 239 case blink::WebAXRoleGrid: 240 case blink::WebAXRoleTable: 241 class_name = "android.widget.GridView"; 242 break; 243 case blink::WebAXRoleList: 244 case blink::WebAXRoleListBox: 245 class_name = "android.widget.ListView"; 246 break; 247 case blink::WebAXRoleDialog: 248 class_name = "android.app.Dialog"; 249 break; 250 default: 251 class_name = "android.view.View"; 252 break; 253 } 254 255 return class_name; 256 } 257 258 base::string16 BrowserAccessibilityAndroid::GetText() const { 259 if (IsIframe() || 260 role() == blink::WebAXRoleWebArea) { 261 return base::string16(); 262 } 263 264 base::string16 description = GetString16Attribute( 265 AccessibilityNodeData::ATTR_DESCRIPTION); 266 base::string16 text; 267 if (!name().empty()) 268 text = base::UTF8ToUTF16(name()); 269 else if (!description.empty()) 270 text = description; 271 else if (!value().empty()) 272 text = base::UTF8ToUTF16(value()); 273 274 // This is called from PlatformIsLeaf, so don't call PlatformChildCount 275 // from within this! 276 if (text.empty() && HasOnlyStaticTextChildren()) { 277 for (uint32 i = 0; i < child_count(); i++) { 278 BrowserAccessibility* child = children()[i]; 279 text += static_cast<BrowserAccessibilityAndroid*>(child)->GetText(); 280 } 281 } 282 283 switch(role()) { 284 case blink::WebAXRoleImageMapLink: 285 case blink::WebAXRoleLink: 286 if (!text.empty()) 287 text += ASCIIToUTF16(" "); 288 text += ASCIIToUTF16("Link"); 289 break; 290 case blink::WebAXRoleHeading: 291 // Only append "heading" if this node already has text. 292 if (!text.empty()) 293 text += ASCIIToUTF16(" Heading"); 294 break; 295 } 296 297 return text; 298 } 299 300 int BrowserAccessibilityAndroid::GetItemIndex() const { 301 int index = 0; 302 switch(role()) { 303 case blink::WebAXRoleListItem: 304 case blink::WebAXRoleListBoxOption: 305 case blink::WebAXRoleTreeItem: 306 index = index_in_parent(); 307 break; 308 case blink::WebAXRoleSlider: 309 case blink::WebAXRoleProgressIndicator: { 310 float value_for_range; 311 if (GetFloatAttribute( 312 AccessibilityNodeData::ATTR_VALUE_FOR_RANGE, &value_for_range)) { 313 index = static_cast<int>(value_for_range); 314 } 315 break; 316 } 317 } 318 return index; 319 } 320 321 int BrowserAccessibilityAndroid::GetItemCount() const { 322 int count = 0; 323 switch(role()) { 324 case blink::WebAXRoleList: 325 case blink::WebAXRoleListBox: 326 count = PlatformChildCount(); 327 break; 328 case blink::WebAXRoleSlider: 329 case blink::WebAXRoleProgressIndicator: { 330 float max_value_for_range; 331 if (GetFloatAttribute(AccessibilityNodeData::ATTR_MAX_VALUE_FOR_RANGE, 332 &max_value_for_range)) { 333 count = static_cast<int>(max_value_for_range); 334 } 335 break; 336 } 337 } 338 return count; 339 } 340 341 int BrowserAccessibilityAndroid::GetScrollX() const { 342 int value = 0; 343 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X, &value); 344 return value; 345 } 346 347 int BrowserAccessibilityAndroid::GetScrollY() const { 348 int value = 0; 349 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y, &value); 350 return value; 351 } 352 353 int BrowserAccessibilityAndroid::GetMaxScrollX() const { 354 int value = 0; 355 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &value); 356 return value; 357 } 358 359 int BrowserAccessibilityAndroid::GetMaxScrollY() const { 360 int value = 0; 361 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y_MAX, &value); 362 return value; 363 } 364 365 int BrowserAccessibilityAndroid::GetTextChangeFromIndex() const { 366 size_t index = 0; 367 while (index < old_value_.length() && 368 index < new_value_.length() && 369 old_value_[index] == new_value_[index]) { 370 index++; 371 } 372 return index; 373 } 374 375 int BrowserAccessibilityAndroid::GetTextChangeAddedCount() const { 376 size_t old_len = old_value_.length(); 377 size_t new_len = new_value_.length(); 378 size_t left = 0; 379 while (left < old_len && 380 left < new_len && 381 old_value_[left] == new_value_[left]) { 382 left++; 383 } 384 size_t right = 0; 385 while (right < old_len && 386 right < new_len && 387 old_value_[old_len - right - 1] == new_value_[new_len - right - 1]) { 388 right++; 389 } 390 return (new_len - left - right); 391 } 392 393 int BrowserAccessibilityAndroid::GetTextChangeRemovedCount() const { 394 size_t old_len = old_value_.length(); 395 size_t new_len = new_value_.length(); 396 size_t left = 0; 397 while (left < old_len && 398 left < new_len && 399 old_value_[left] == new_value_[left]) { 400 left++; 401 } 402 size_t right = 0; 403 while (right < old_len && 404 right < new_len && 405 old_value_[old_len - right - 1] == new_value_[new_len - right - 1]) { 406 right++; 407 } 408 return (old_len - left - right); 409 } 410 411 base::string16 BrowserAccessibilityAndroid::GetTextChangeBeforeText() const { 412 return old_value_; 413 } 414 415 int BrowserAccessibilityAndroid::GetSelectionStart() const { 416 int sel_start = 0; 417 GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_START, &sel_start); 418 return sel_start; 419 } 420 421 int BrowserAccessibilityAndroid::GetSelectionEnd() const { 422 int sel_end = 0; 423 GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_END, &sel_end); 424 return sel_end; 425 } 426 427 int BrowserAccessibilityAndroid::GetEditableTextLength() const { 428 return value().length(); 429 } 430 431 int BrowserAccessibilityAndroid::AndroidInputType() const { 432 std::string html_tag = GetStringAttribute( 433 AccessibilityNodeData::ATTR_HTML_TAG); 434 if (html_tag != "input") 435 return ANDROID_TEXT_INPUTTYPE_TYPE_NULL; 436 437 std::string type; 438 if (!GetHtmlAttribute("type", &type)) 439 return ANDROID_TEXT_INPUTTYPE_TYPE_TEXT; 440 441 if (type == "" || type == "text" || type == "search") 442 return ANDROID_TEXT_INPUTTYPE_TYPE_TEXT; 443 else if (type == "date") 444 return ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME_DATE; 445 else if (type == "datetime" || type == "datetime-local") 446 return ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME; 447 else if (type == "email") 448 return ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_WEB_EMAIL; 449 else if (type == "month") 450 return ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME_DATE; 451 else if (type == "number") 452 return ANDROID_TEXT_INPUTTYPE_TYPE_NUMBER; 453 else if (type == "password") 454 return ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_WEB_PASSWORD; 455 else if (type == "tel") 456 return ANDROID_TEXT_INPUTTYPE_TYPE_PHONE; 457 else if (type == "time") 458 return ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME_TIME; 459 else if (type == "url") 460 return ANDROID_TEXT_INPUTTYPE_TYPE_TEXT_URI; 461 else if (type == "week") 462 return ANDROID_TEXT_INPUTTYPE_TYPE_DATETIME; 463 464 return ANDROID_TEXT_INPUTTYPE_TYPE_NULL; 465 } 466 467 int BrowserAccessibilityAndroid::AndroidLiveRegionType() const { 468 std::string live = GetStringAttribute( 469 AccessibilityNodeData::ATTR_LIVE_STATUS); 470 if (live == "polite") 471 return ANDROID_VIEW_VIEW_ACCESSIBILITY_LIVE_REGION_POLITE; 472 else if (live == "assertive") 473 return ANDROID_VIEW_VIEW_ACCESSIBILITY_LIVE_REGION_ASSERTIVE; 474 return ANDROID_VIEW_VIEW_ACCESSIBILITY_LIVE_REGION_NONE; 475 } 476 477 int BrowserAccessibilityAndroid::AndroidRangeType() const { 478 return ANDROID_VIEW_ACCESSIBILITY_RANGE_TYPE_FLOAT; 479 } 480 481 int BrowserAccessibilityAndroid::RowCount() const { 482 if (role() == blink::WebAXRoleGrid || 483 role() == blink::WebAXRoleTable) { 484 return CountChildrenWithRole(blink::WebAXRoleRow); 485 } 486 487 if (role() == blink::WebAXRoleList || 488 role() == blink::WebAXRoleListBox || 489 role() == blink::WebAXRoleTree) { 490 return PlatformChildCount(); 491 } 492 493 return 0; 494 } 495 496 int BrowserAccessibilityAndroid::ColumnCount() const { 497 if (role() == blink::WebAXRoleGrid || 498 role() == blink::WebAXRoleTable) { 499 return CountChildrenWithRole(blink::WebAXRoleColumn); 500 } 501 return 0; 502 } 503 504 int BrowserAccessibilityAndroid::RowIndex() const { 505 if (role() == blink::WebAXRoleListItem || 506 role() == blink::WebAXRoleListBoxOption || 507 role() == blink::WebAXRoleTreeItem) { 508 return index_in_parent(); 509 } 510 511 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_ROW_INDEX); 512 } 513 514 int BrowserAccessibilityAndroid::RowSpan() const { 515 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_ROW_SPAN); 516 } 517 518 int BrowserAccessibilityAndroid::ColumnIndex() const { 519 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_COLUMN_INDEX); 520 } 521 522 int BrowserAccessibilityAndroid::ColumnSpan() const { 523 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_COLUMN_SPAN); 524 } 525 526 float BrowserAccessibilityAndroid::RangeMin() const { 527 return GetFloatAttribute(AccessibilityNodeData::ATTR_MIN_VALUE_FOR_RANGE); 528 } 529 530 float BrowserAccessibilityAndroid::RangeMax() const { 531 return GetFloatAttribute(AccessibilityNodeData::ATTR_MAX_VALUE_FOR_RANGE); 532 } 533 534 float BrowserAccessibilityAndroid::RangeCurrentValue() const { 535 return GetFloatAttribute(AccessibilityNodeData::ATTR_VALUE_FOR_RANGE); 536 } 537 538 bool BrowserAccessibilityAndroid::HasFocusableChild() const { 539 // This is called from PlatformIsLeaf, so don't call PlatformChildCount 540 // from within this! 541 for (uint32 i = 0; i < child_count(); i++) { 542 BrowserAccessibility* child = children()[i]; 543 if (child->HasState(blink::WebAXStateFocusable)) 544 return true; 545 if (static_cast<BrowserAccessibilityAndroid*>(child)->HasFocusableChild()) 546 return true; 547 } 548 return false; 549 } 550 551 bool BrowserAccessibilityAndroid::HasOnlyStaticTextChildren() const { 552 // This is called from PlatformIsLeaf, so don't call PlatformChildCount 553 // from within this! 554 for (uint32 i = 0; i < child_count(); i++) { 555 BrowserAccessibility* child = children()[i]; 556 if (child->role() != blink::WebAXRoleStaticText) 557 return false; 558 } 559 return true; 560 } 561 562 bool BrowserAccessibilityAndroid::IsIframe() const { 563 base::string16 html_tag = GetString16Attribute( 564 AccessibilityNodeData::ATTR_HTML_TAG); 565 return html_tag == ASCIIToUTF16("iframe"); 566 } 567 568 void BrowserAccessibilityAndroid::PostInitialize() { 569 BrowserAccessibility::PostInitialize(); 570 571 if (IsEditableText()) { 572 if (base::UTF8ToUTF16(value()) != new_value_) { 573 old_value_ = new_value_; 574 new_value_ = base::UTF8ToUTF16(value()); 575 } 576 } 577 578 if (role() == blink::WebAXRoleAlert && first_time_) 579 manager()->NotifyAccessibilityEvent(blink::WebAXEventAlert, this); 580 581 base::string16 live; 582 if (GetString16Attribute( 583 AccessibilityNodeData::ATTR_CONTAINER_LIVE_STATUS, &live)) { 584 NotifyLiveRegionUpdate(live); 585 } 586 587 first_time_ = false; 588 } 589 590 void BrowserAccessibilityAndroid::NotifyLiveRegionUpdate( 591 base::string16& aria_live) { 592 if (!EqualsASCII(aria_live, aria_strings::kAriaLivePolite) && 593 !EqualsASCII(aria_live, aria_strings::kAriaLiveAssertive)) 594 return; 595 596 base::string16 text = GetText(); 597 if (cached_text_ != text) { 598 if (!text.empty()) { 599 manager()->NotifyAccessibilityEvent(blink::WebAXEventShow, 600 this); 601 } 602 cached_text_ = text; 603 } 604 } 605 606 int BrowserAccessibilityAndroid::CountChildrenWithRole( 607 blink::WebAXRole role) const { 608 int count = 0; 609 for (uint32 i = 0; i < PlatformChildCount(); i++) { 610 if (PlatformGetChild(i)->role() == role) 611 count++; 612 } 613 return count; 614 } 615 616 } // namespace content 617