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
Create()47 BrowserAccessibility* BrowserAccessibility::Create() {
48 return new BrowserAccessibilityAndroid();
49 }
50
BrowserAccessibilityAndroid()51 BrowserAccessibilityAndroid::BrowserAccessibilityAndroid() {
52 first_time_ = true;
53 }
54
IsNative() const55 bool BrowserAccessibilityAndroid::IsNative() const {
56 return true;
57 }
58
PlatformIsLeaf() const59 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
IsCheckable() const90 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
IsChecked() const105 bool BrowserAccessibilityAndroid::IsChecked() const {
106 return HasState(blink::WebAXStateChecked);
107 }
108
IsClickable() const109 bool BrowserAccessibilityAndroid::IsClickable() const {
110 return (PlatformIsLeaf() && !GetText().empty());
111 }
112
IsCollection() const113 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
IsCollectionItem() const121 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
IsContentInvalid() const131 bool BrowserAccessibilityAndroid::IsContentInvalid() const {
132 std::string invalid;
133 return GetHtmlAttribute("aria-invalid", &invalid);
134 }
135
IsDismissable() const136 bool BrowserAccessibilityAndroid::IsDismissable() const {
137 return false; // No concept of "dismissable" on the web currently.
138 }
139
IsEnabled() const140 bool BrowserAccessibilityAndroid::IsEnabled() const {
141 return HasState(blink::WebAXStateEnabled);
142 }
143
IsFocusable() const144 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
IsFocused() const153 bool BrowserAccessibilityAndroid::IsFocused() const {
154 return manager()->GetFocus(manager()->GetRoot()) == this;
155 }
156
IsHeading() const157 bool BrowserAccessibilityAndroid::IsHeading() const {
158 return (role() == blink::WebAXRoleColumnHeader ||
159 role() == blink::WebAXRoleHeading ||
160 role() == blink::WebAXRoleRowHeader);
161 }
162
IsHierarchical() const163 bool BrowserAccessibilityAndroid::IsHierarchical() const {
164 return (role() == blink::WebAXRoleList ||
165 role() == blink::WebAXRoleTree);
166 }
167
IsMultiLine() const168 bool BrowserAccessibilityAndroid::IsMultiLine() const {
169 return role() == blink::WebAXRoleTextArea;
170 }
171
IsPassword() const172 bool BrowserAccessibilityAndroid::IsPassword() const {
173 return HasState(blink::WebAXStateProtected);
174 }
175
IsRangeType() const176 bool BrowserAccessibilityAndroid::IsRangeType() const {
177 return (role() == blink::WebAXRoleProgressIndicator ||
178 role() == blink::WebAXRoleScrollBar ||
179 role() == blink::WebAXRoleSlider);
180 }
181
IsScrollable() const182 bool BrowserAccessibilityAndroid::IsScrollable() const {
183 int dummy;
184 return GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &dummy);
185 }
186
IsSelected() const187 bool BrowserAccessibilityAndroid::IsSelected() const {
188 return HasState(blink::WebAXStateSelected);
189 }
190
IsVisibleToUser() const191 bool BrowserAccessibilityAndroid::IsVisibleToUser() const {
192 return !HasState(blink::WebAXStateInvisible);
193 }
194
CanOpenPopup() const195 bool BrowserAccessibilityAndroid::CanOpenPopup() const {
196 return HasState(blink::WebAXStateHaspopup);
197 }
198
GetClassName() const199 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
GetText() const258 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
GetItemIndex() const300 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
GetItemCount() const321 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
GetScrollX() const341 int BrowserAccessibilityAndroid::GetScrollX() const {
342 int value = 0;
343 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X, &value);
344 return value;
345 }
346
GetScrollY() const347 int BrowserAccessibilityAndroid::GetScrollY() const {
348 int value = 0;
349 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y, &value);
350 return value;
351 }
352
GetMaxScrollX() const353 int BrowserAccessibilityAndroid::GetMaxScrollX() const {
354 int value = 0;
355 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &value);
356 return value;
357 }
358
GetMaxScrollY() const359 int BrowserAccessibilityAndroid::GetMaxScrollY() const {
360 int value = 0;
361 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y_MAX, &value);
362 return value;
363 }
364
GetTextChangeFromIndex() const365 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
GetTextChangeAddedCount() const375 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
GetTextChangeRemovedCount() const393 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
GetTextChangeBeforeText() const411 base::string16 BrowserAccessibilityAndroid::GetTextChangeBeforeText() const {
412 return old_value_;
413 }
414
GetSelectionStart() const415 int BrowserAccessibilityAndroid::GetSelectionStart() const {
416 int sel_start = 0;
417 GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_START, &sel_start);
418 return sel_start;
419 }
420
GetSelectionEnd() const421 int BrowserAccessibilityAndroid::GetSelectionEnd() const {
422 int sel_end = 0;
423 GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_END, &sel_end);
424 return sel_end;
425 }
426
GetEditableTextLength() const427 int BrowserAccessibilityAndroid::GetEditableTextLength() const {
428 return value().length();
429 }
430
AndroidInputType() const431 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
AndroidLiveRegionType() const467 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
AndroidRangeType() const477 int BrowserAccessibilityAndroid::AndroidRangeType() const {
478 return ANDROID_VIEW_ACCESSIBILITY_RANGE_TYPE_FLOAT;
479 }
480
RowCount() const481 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
ColumnCount() const496 int BrowserAccessibilityAndroid::ColumnCount() const {
497 if (role() == blink::WebAXRoleGrid ||
498 role() == blink::WebAXRoleTable) {
499 return CountChildrenWithRole(blink::WebAXRoleColumn);
500 }
501 return 0;
502 }
503
RowIndex() const504 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
RowSpan() const514 int BrowserAccessibilityAndroid::RowSpan() const {
515 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_ROW_SPAN);
516 }
517
ColumnIndex() const518 int BrowserAccessibilityAndroid::ColumnIndex() const {
519 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_COLUMN_INDEX);
520 }
521
ColumnSpan() const522 int BrowserAccessibilityAndroid::ColumnSpan() const {
523 return GetIntAttribute(AccessibilityNodeData::ATTR_TABLE_CELL_COLUMN_SPAN);
524 }
525
RangeMin() const526 float BrowserAccessibilityAndroid::RangeMin() const {
527 return GetFloatAttribute(AccessibilityNodeData::ATTR_MIN_VALUE_FOR_RANGE);
528 }
529
RangeMax() const530 float BrowserAccessibilityAndroid::RangeMax() const {
531 return GetFloatAttribute(AccessibilityNodeData::ATTR_MAX_VALUE_FOR_RANGE);
532 }
533
RangeCurrentValue() const534 float BrowserAccessibilityAndroid::RangeCurrentValue() const {
535 return GetFloatAttribute(AccessibilityNodeData::ATTR_VALUE_FOR_RANGE);
536 }
537
HasFocusableChild() const538 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
HasOnlyStaticTextChildren() const551 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
IsIframe() const562 bool BrowserAccessibilityAndroid::IsIframe() const {
563 base::string16 html_tag = GetString16Attribute(
564 AccessibilityNodeData::ATTR_HTML_TAG);
565 return html_tag == ASCIIToUTF16("iframe");
566 }
567
PostInitialize()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
NotifyLiveRegionUpdate(base::string16 & aria_live)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
CountChildrenWithRole(blink::WebAXRole role) const606 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