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 content { 13 14 // static 15 BrowserAccessibility* BrowserAccessibility::Create() { 16 return new BrowserAccessibilityAndroid(); 17 } 18 19 BrowserAccessibilityAndroid::BrowserAccessibilityAndroid() { 20 first_time_ = true; 21 } 22 23 bool BrowserAccessibilityAndroid::IsNative() const { 24 return true; 25 } 26 27 bool BrowserAccessibilityAndroid::IsLeaf() const { 28 if (child_count() == 0) 29 return true; 30 31 // Iframes are always allowed to contain children. 32 if (IsIframe() || 33 role() == AccessibilityNodeData::ROLE_ROOT_WEB_AREA || 34 role() == AccessibilityNodeData::ROLE_WEB_AREA) { 35 return false; 36 } 37 38 // If it has a focusable child, we definitely can't leave out children. 39 if (HasFocusableChild()) 40 return false; 41 42 // Headings with text can drop their children. 43 string16 name = GetText(); 44 if (role() == AccessibilityNodeData::ROLE_HEADING && !name.empty()) 45 return true; 46 47 // Focusable nodes with text can drop their children. 48 if (HasState(AccessibilityNodeData::STATE_FOCUSABLE) && !name.empty()) 49 return true; 50 51 // Nodes with only static text as children can drop their children. 52 if (HasOnlyStaticTextChildren()) 53 return true; 54 55 return false; 56 } 57 58 bool BrowserAccessibilityAndroid::IsCheckable() const { 59 bool checkable = false; 60 bool is_aria_pressed_defined; 61 bool is_mixed; 62 GetAriaTristate("aria-pressed", &is_aria_pressed_defined, &is_mixed); 63 if (role() == AccessibilityNodeData::ROLE_CHECKBOX || 64 role() == AccessibilityNodeData::ROLE_RADIO_BUTTON || 65 is_aria_pressed_defined) { 66 checkable = true; 67 } 68 if (HasState(AccessibilityNodeData::STATE_CHECKED)) 69 checkable = true; 70 return checkable; 71 } 72 73 bool BrowserAccessibilityAndroid::IsChecked() const { 74 return HasState(AccessibilityNodeData::STATE_CHECKED); 75 } 76 77 bool BrowserAccessibilityAndroid::IsClickable() const { 78 return (IsLeaf() && !GetText().empty()); 79 } 80 81 bool BrowserAccessibilityAndroid::IsEnabled() const { 82 return !HasState(AccessibilityNodeData::STATE_UNAVAILABLE); 83 } 84 85 bool BrowserAccessibilityAndroid::IsFocusable() const { 86 bool focusable = HasState(AccessibilityNodeData::STATE_FOCUSABLE); 87 if (IsIframe() || 88 role() == AccessibilityNodeData::ROLE_WEB_AREA) { 89 focusable = false; 90 } 91 return focusable; 92 } 93 94 bool BrowserAccessibilityAndroid::IsFocused() const { 95 return manager()->GetFocus(manager()->GetRoot()) == this; 96 } 97 98 bool BrowserAccessibilityAndroid::IsPassword() const { 99 return HasState(AccessibilityNodeData::STATE_PROTECTED); 100 } 101 102 bool BrowserAccessibilityAndroid::IsScrollable() const { 103 int dummy; 104 return GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &dummy); 105 } 106 107 bool BrowserAccessibilityAndroid::IsSelected() const { 108 return HasState(AccessibilityNodeData::STATE_SELECTED); 109 } 110 111 bool BrowserAccessibilityAndroid::IsVisibleToUser() const { 112 return !HasState(AccessibilityNodeData::STATE_INVISIBLE); 113 } 114 115 const char* BrowserAccessibilityAndroid::GetClassName() const { 116 const char* class_name = NULL; 117 118 switch(role()) { 119 case AccessibilityNodeData::ROLE_EDITABLE_TEXT: 120 case AccessibilityNodeData::ROLE_SPIN_BUTTON: 121 case AccessibilityNodeData::ROLE_TEXTAREA: 122 case AccessibilityNodeData::ROLE_TEXT_FIELD: 123 class_name = "android.widget.EditText"; 124 break; 125 case AccessibilityNodeData::ROLE_SLIDER: 126 class_name = "android.widget.SeekBar"; 127 break; 128 case AccessibilityNodeData::ROLE_COMBO_BOX: 129 class_name = "android.widget.Spinner"; 130 break; 131 case AccessibilityNodeData::ROLE_BUTTON: 132 case AccessibilityNodeData::ROLE_MENU_BUTTON: 133 case AccessibilityNodeData::ROLE_POPUP_BUTTON: 134 class_name = "android.widget.Button"; 135 break; 136 case AccessibilityNodeData::ROLE_CHECKBOX: 137 class_name = "android.widget.CheckBox"; 138 break; 139 case AccessibilityNodeData::ROLE_RADIO_BUTTON: 140 class_name = "android.widget.RadioButton"; 141 break; 142 case AccessibilityNodeData::ROLE_TOGGLE_BUTTON: 143 class_name = "android.widget.ToggleButton"; 144 break; 145 case AccessibilityNodeData::ROLE_CANVAS: 146 case AccessibilityNodeData::ROLE_IMAGE: 147 class_name = "android.widget.Image"; 148 break; 149 case AccessibilityNodeData::ROLE_PROGRESS_INDICATOR: 150 class_name = "android.widget.ProgressBar"; 151 break; 152 case AccessibilityNodeData::ROLE_TAB_LIST: 153 class_name = "android.widget.TabWidget"; 154 break; 155 case AccessibilityNodeData::ROLE_GRID: 156 case AccessibilityNodeData::ROLE_TABLE: 157 class_name = "android.widget.GridView"; 158 break; 159 case AccessibilityNodeData::ROLE_LIST: 160 case AccessibilityNodeData::ROLE_LISTBOX: 161 class_name = "android.widget.ListView"; 162 break; 163 default: 164 class_name = "android.view.View"; 165 break; 166 } 167 168 return class_name; 169 } 170 171 string16 BrowserAccessibilityAndroid::GetText() const { 172 if (IsIframe() || 173 role() == AccessibilityNodeData::ROLE_WEB_AREA) { 174 return string16(); 175 } 176 177 string16 description; 178 GetStringAttribute(AccessibilityNodeData::ATTR_DESCRIPTION, &description); 179 180 string16 text; 181 if (!name().empty()) 182 text = name(); 183 else if (!description.empty()) 184 text = description; 185 else if (!value().empty()) 186 text = value(); 187 188 if (text.empty() && HasOnlyStaticTextChildren()) { 189 for (uint32 i = 0; i < child_count(); i++) { 190 BrowserAccessibility* child = GetChild(i); 191 text += static_cast<BrowserAccessibilityAndroid*>(child)->GetText(); 192 } 193 } 194 195 switch(role()) { 196 case AccessibilityNodeData::ROLE_IMAGE_MAP_LINK: 197 case AccessibilityNodeData::ROLE_LINK: 198 case AccessibilityNodeData::ROLE_WEBCORE_LINK: 199 if (!text.empty()) 200 text += ASCIIToUTF16(" "); 201 text += ASCIIToUTF16("Link"); 202 break; 203 case AccessibilityNodeData::ROLE_HEADING: 204 // Only append "heading" if this node already has text. 205 if (!text.empty()) 206 text += ASCIIToUTF16(" Heading"); 207 break; 208 } 209 210 return text; 211 } 212 213 int BrowserAccessibilityAndroid::GetItemIndex() const { 214 int index = 0; 215 switch(role()) { 216 case AccessibilityNodeData::ROLE_LIST_ITEM: 217 case AccessibilityNodeData::ROLE_LISTBOX_OPTION: 218 index = index_in_parent(); 219 break; 220 case AccessibilityNodeData::ROLE_SLIDER: 221 case AccessibilityNodeData::ROLE_PROGRESS_INDICATOR: { 222 float value_for_range; 223 if (GetFloatAttribute( 224 AccessibilityNodeData::ATTR_VALUE_FOR_RANGE, &value_for_range)) { 225 index = static_cast<int>(value_for_range); 226 } 227 break; 228 } 229 } 230 return index; 231 } 232 233 int BrowserAccessibilityAndroid::GetItemCount() const { 234 int count = 0; 235 switch(role()) { 236 case AccessibilityNodeData::ROLE_LIST: 237 case AccessibilityNodeData::ROLE_LISTBOX: 238 count = child_count(); 239 break; 240 case AccessibilityNodeData::ROLE_SLIDER: 241 case AccessibilityNodeData::ROLE_PROGRESS_INDICATOR: { 242 float max_value_for_range; 243 if (GetFloatAttribute(AccessibilityNodeData::ATTR_MAX_VALUE_FOR_RANGE, 244 &max_value_for_range)) { 245 count = static_cast<int>(max_value_for_range); 246 } 247 break; 248 } 249 } 250 return count; 251 } 252 253 int BrowserAccessibilityAndroid::GetScrollX() const { 254 int value = 0; 255 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X, &value); 256 return value; 257 } 258 259 int BrowserAccessibilityAndroid::GetScrollY() const { 260 int value = 0; 261 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y, &value); 262 return value; 263 } 264 265 int BrowserAccessibilityAndroid::GetMaxScrollX() const { 266 int value = 0; 267 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &value); 268 return value; 269 } 270 271 int BrowserAccessibilityAndroid::GetMaxScrollY() const { 272 int value = 0; 273 GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y_MAX, &value); 274 return value; 275 } 276 277 int BrowserAccessibilityAndroid::GetTextChangeFromIndex() const { 278 size_t index = 0; 279 while (index < old_value_.length() && 280 index < new_value_.length() && 281 old_value_[index] == new_value_[index]) { 282 index++; 283 } 284 return index; 285 } 286 287 int BrowserAccessibilityAndroid::GetTextChangeAddedCount() const { 288 size_t old_len = old_value_.length(); 289 size_t new_len = new_value_.length(); 290 size_t left = 0; 291 while (left < old_len && 292 left < new_len && 293 old_value_[left] == new_value_[left]) { 294 left++; 295 } 296 size_t right = 0; 297 while (right < old_len && 298 right < new_len && 299 old_value_[old_len - right - 1] == new_value_[new_len - right - 1]) { 300 right++; 301 } 302 return (new_len - left - right); 303 } 304 305 int BrowserAccessibilityAndroid::GetTextChangeRemovedCount() const { 306 size_t old_len = old_value_.length(); 307 size_t new_len = new_value_.length(); 308 size_t left = 0; 309 while (left < old_len && 310 left < new_len && 311 old_value_[left] == new_value_[left]) { 312 left++; 313 } 314 size_t right = 0; 315 while (right < old_len && 316 right < new_len && 317 old_value_[old_len - right - 1] == new_value_[new_len - right - 1]) { 318 right++; 319 } 320 return (old_len - left - right); 321 } 322 323 string16 BrowserAccessibilityAndroid::GetTextChangeBeforeText() const { 324 return old_value_; 325 } 326 327 int BrowserAccessibilityAndroid::GetSelectionStart() const { 328 int sel_start = 0; 329 GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_START, &sel_start); 330 return sel_start; 331 } 332 333 int BrowserAccessibilityAndroid::GetSelectionEnd() const { 334 int sel_end = 0; 335 GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_END, &sel_end); 336 return sel_end; 337 } 338 339 int BrowserAccessibilityAndroid::GetEditableTextLength() const { 340 return value().length(); 341 } 342 343 bool BrowserAccessibilityAndroid::HasFocusableChild() const { 344 for (uint32 i = 0; i < child_count(); i++) { 345 BrowserAccessibility* child = GetChild(i); 346 if (child->HasState(AccessibilityNodeData::STATE_FOCUSABLE)) 347 return true; 348 if (static_cast<BrowserAccessibilityAndroid*>(child)->HasFocusableChild()) 349 return true; 350 } 351 return false; 352 } 353 354 bool BrowserAccessibilityAndroid::HasOnlyStaticTextChildren() const { 355 for (uint32 i = 0; i < child_count(); i++) { 356 BrowserAccessibility* child = GetChild(i); 357 if (child->role() != AccessibilityNodeData::ROLE_STATIC_TEXT) 358 return false; 359 } 360 return true; 361 } 362 363 bool BrowserAccessibilityAndroid::IsIframe() const { 364 string16 html_tag; 365 GetStringAttribute(AccessibilityNodeData::ATTR_HTML_TAG, &html_tag); 366 return html_tag == ASCIIToUTF16("iframe"); 367 } 368 369 void BrowserAccessibilityAndroid::PostInitialize() { 370 BrowserAccessibility::PostInitialize(); 371 372 if (IsEditableText()) { 373 if (value_ != new_value_) { 374 old_value_ = new_value_; 375 new_value_ = value_; 376 } 377 } 378 379 if (role_ == AccessibilityNodeData::ROLE_ALERT && first_time_) 380 manager_->NotifyAccessibilityEvent(AccessibilityNotificationAlert, this); 381 382 string16 live; 383 if (GetStringAttribute(AccessibilityNodeData::ATTR_CONTAINER_LIVE_STATUS, 384 &live)) { 385 NotifyLiveRegionUpdate(live); 386 } 387 388 first_time_ = false; 389 } 390 391 void BrowserAccessibilityAndroid::NotifyLiveRegionUpdate(string16& aria_live) { 392 if (!EqualsASCII(aria_live, aria_strings::kAriaLivePolite) && 393 !EqualsASCII(aria_live, aria_strings::kAriaLiveAssertive)) 394 return; 395 396 string16 text = GetText(); 397 if (cached_text_ != text) { 398 if (!text.empty()) { 399 manager_->NotifyAccessibilityEvent(AccessibilityNotificationObjectShow, 400 this); 401 } 402 cached_text_ = text; 403 } 404 } 405 406 } // namespace content 407