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_manager_android.h" 6 7 #include <cmath> 8 9 #include "base/android/jni_android.h" 10 #include "base/android/jni_string.h" 11 #include "base/strings/string_number_conversions.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "base/values.h" 14 #include "content/browser/accessibility/browser_accessibility_android.h" 15 #include "content/common/accessibility_messages.h" 16 #include "jni/BrowserAccessibilityManager_jni.h" 17 18 using base::android::AttachCurrentThread; 19 using base::android::ScopedJavaLocalRef; 20 21 namespace { 22 23 // These are enums from android.view.accessibility.AccessibilityEvent in Java: 24 enum { 25 ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED = 16, 26 ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192 27 }; 28 29 enum AndroidHtmlElementType { 30 HTML_ELEMENT_TYPE_SECTION, 31 HTML_ELEMENT_TYPE_LIST, 32 HTML_ELEMENT_TYPE_CONTROL, 33 HTML_ELEMENT_TYPE_ANY 34 }; 35 36 // These are special unofficial strings sent from TalkBack/BrailleBack 37 // to jump to certain categories of web elements. 38 AndroidHtmlElementType HtmlElementTypeFromString(base::string16 element_type) { 39 if (element_type == base::ASCIIToUTF16("SECTION")) 40 return HTML_ELEMENT_TYPE_SECTION; 41 else if (element_type == base::ASCIIToUTF16("LIST")) 42 return HTML_ELEMENT_TYPE_LIST; 43 else if (element_type == base::ASCIIToUTF16("CONTROL")) 44 return HTML_ELEMENT_TYPE_CONTROL; 45 else 46 return HTML_ELEMENT_TYPE_ANY; 47 } 48 49 } // anonymous namespace 50 51 namespace content { 52 53 namespace aria_strings { 54 const char kAriaLivePolite[] = "polite"; 55 const char kAriaLiveAssertive[] = "assertive"; 56 } 57 58 // static 59 BrowserAccessibilityManager* BrowserAccessibilityManager::Create( 60 const ui::AXTreeUpdate& initial_tree, 61 BrowserAccessibilityDelegate* delegate, 62 BrowserAccessibilityFactory* factory) { 63 return new BrowserAccessibilityManagerAndroid( 64 ScopedJavaLocalRef<jobject>(), initial_tree, delegate, factory); 65 } 66 67 BrowserAccessibilityManagerAndroid* 68 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() { 69 return static_cast<BrowserAccessibilityManagerAndroid*>(this); 70 } 71 72 BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid( 73 ScopedJavaLocalRef<jobject> content_view_core, 74 const ui::AXTreeUpdate& initial_tree, 75 BrowserAccessibilityDelegate* delegate, 76 BrowserAccessibilityFactory* factory) 77 : BrowserAccessibilityManager(initial_tree, delegate, factory) { 78 SetContentViewCore(content_view_core); 79 } 80 81 BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() { 82 JNIEnv* env = AttachCurrentThread(); 83 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); 84 if (obj.is_null()) 85 return; 86 87 Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj()); 88 } 89 90 // static 91 ui::AXTreeUpdate BrowserAccessibilityManagerAndroid::GetEmptyDocument() { 92 ui::AXNodeData empty_document; 93 empty_document.id = 0; 94 empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA; 95 empty_document.state = 1 << ui::AX_STATE_READ_ONLY; 96 97 ui::AXTreeUpdate update; 98 update.nodes.push_back(empty_document); 99 return update; 100 } 101 102 void BrowserAccessibilityManagerAndroid::SetContentViewCore( 103 ScopedJavaLocalRef<jobject> content_view_core) { 104 if (content_view_core.is_null()) 105 return; 106 107 JNIEnv* env = AttachCurrentThread(); 108 java_ref_ = JavaObjectWeakGlobalRef( 109 env, Java_BrowserAccessibilityManager_create( 110 env, reinterpret_cast<intptr_t>(this), 111 content_view_core.obj()).obj()); 112 } 113 114 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent( 115 ui::AXEvent event_type, 116 BrowserAccessibility* node) { 117 JNIEnv* env = AttachCurrentThread(); 118 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); 119 if (obj.is_null()) 120 return; 121 122 if (event_type == ui::AX_EVENT_HIDE) 123 return; 124 125 if (event_type == ui::AX_EVENT_HOVER) { 126 HandleHoverEvent(node); 127 return; 128 } 129 130 // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify 131 // the Android system that the accessibility hierarchy rooted at this 132 // node has changed. 133 Java_BrowserAccessibilityManager_handleContentChanged( 134 env, obj.obj(), node->GetId()); 135 136 switch (event_type) { 137 case ui::AX_EVENT_LOAD_COMPLETE: 138 Java_BrowserAccessibilityManager_handlePageLoaded( 139 env, obj.obj(), focus_->id()); 140 break; 141 case ui::AX_EVENT_FOCUS: 142 Java_BrowserAccessibilityManager_handleFocusChanged( 143 env, obj.obj(), node->GetId()); 144 break; 145 case ui::AX_EVENT_CHECKED_STATE_CHANGED: 146 Java_BrowserAccessibilityManager_handleCheckStateChanged( 147 env, obj.obj(), node->GetId()); 148 break; 149 case ui::AX_EVENT_SCROLL_POSITION_CHANGED: 150 Java_BrowserAccessibilityManager_handleScrollPositionChanged( 151 env, obj.obj(), node->GetId()); 152 break; 153 case ui::AX_EVENT_SCROLLED_TO_ANCHOR: 154 Java_BrowserAccessibilityManager_handleScrolledToAnchor( 155 env, obj.obj(), node->GetId()); 156 break; 157 case ui::AX_EVENT_ALERT: 158 // An alert is a special case of live region. Fall through to the 159 // next case to handle it. 160 case ui::AX_EVENT_SHOW: { 161 // This event is fired when an object appears in a live region. 162 // Speak its text. 163 BrowserAccessibilityAndroid* android_node = 164 static_cast<BrowserAccessibilityAndroid*>(node); 165 Java_BrowserAccessibilityManager_announceLiveRegionText( 166 env, obj.obj(), 167 base::android::ConvertUTF16ToJavaString( 168 env, android_node->GetText()).obj()); 169 break; 170 } 171 case ui::AX_EVENT_SELECTED_TEXT_CHANGED: 172 Java_BrowserAccessibilityManager_handleTextSelectionChanged( 173 env, obj.obj(), node->GetId()); 174 break; 175 case ui::AX_EVENT_CHILDREN_CHANGED: 176 case ui::AX_EVENT_TEXT_CHANGED: 177 case ui::AX_EVENT_VALUE_CHANGED: 178 if (node->IsEditableText()) { 179 Java_BrowserAccessibilityManager_handleEditableTextChanged( 180 env, obj.obj(), node->GetId()); 181 } 182 break; 183 default: 184 // There are some notifications that aren't meaningful on Android. 185 // It's okay to skip them. 186 break; 187 } 188 } 189 190 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) { 191 return static_cast<jint>(GetRoot()->GetId()); 192 } 193 194 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid( 195 JNIEnv* env, jobject obj, jint id) { 196 return GetFromID(id) != NULL; 197 } 198 199 void BrowserAccessibilityManagerAndroid::HitTest( 200 JNIEnv* env, jobject obj, jint x, jint y) { 201 if (delegate()) 202 delegate()->AccessibilityHitTest(gfx::Point(x, y)); 203 } 204 205 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo( 206 JNIEnv* env, jobject obj, jobject info, jint id) { 207 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>( 208 GetFromID(id)); 209 if (!node) 210 return false; 211 212 if (node->GetParent()) { 213 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent( 214 env, obj, info, node->GetParent()->GetId()); 215 } 216 for (unsigned i = 0; i < node->PlatformChildCount(); ++i) { 217 Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild( 218 env, obj, info, node->InternalGetChild(i)->GetId()); 219 } 220 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes( 221 env, obj, info, 222 id, 223 node->IsCheckable(), 224 node->IsChecked(), 225 node->IsClickable(), 226 node->IsEnabled(), 227 node->IsFocusable(), 228 node->IsFocused(), 229 node->IsPassword(), 230 node->IsScrollable(), 231 node->IsSelected(), 232 node->IsVisibleToUser()); 233 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName( 234 env, obj, info, 235 base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj()); 236 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription( 237 env, obj, info, 238 base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj(), 239 node->IsLink()); 240 241 gfx::Rect absolute_rect = node->GetLocalBoundsRect(); 242 gfx::Rect parent_relative_rect = absolute_rect; 243 if (node->GetParent()) { 244 gfx::Rect parent_rect = node->GetParent()->GetLocalBoundsRect(); 245 parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin()); 246 } 247 bool is_root = node->GetParent() == NULL; 248 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation( 249 env, obj, info, 250 absolute_rect.x(), absolute_rect.y(), 251 parent_relative_rect.x(), parent_relative_rect.y(), 252 absolute_rect.width(), absolute_rect.height(), 253 is_root); 254 255 // New KitKat APIs 256 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes( 257 env, obj, info, 258 node->CanOpenPopup(), 259 node->IsContentInvalid(), 260 node->IsDismissable(), 261 node->IsMultiLine(), 262 node->AndroidInputType(), 263 node->AndroidLiveRegionType()); 264 if (node->IsCollection()) { 265 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo( 266 env, obj, info, 267 node->RowCount(), 268 node->ColumnCount(), 269 node->IsHierarchical()); 270 } 271 if (node->IsCollectionItem() || node->IsHeading()) { 272 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo( 273 env, obj, info, 274 node->RowIndex(), 275 node->RowSpan(), 276 node->ColumnIndex(), 277 node->ColumnSpan(), 278 node->IsHeading()); 279 } 280 if (node->IsRangeType()) { 281 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo( 282 env, obj, info, 283 node->AndroidRangeType(), 284 node->RangeMin(), 285 node->RangeMax(), 286 node->RangeCurrentValue()); 287 } 288 289 return true; 290 } 291 292 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent( 293 JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) { 294 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>( 295 GetFromID(id)); 296 if (!node) 297 return false; 298 299 Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes( 300 env, obj, event, 301 node->IsChecked(), 302 node->IsEnabled(), 303 node->IsPassword(), 304 node->IsScrollable()); 305 Java_BrowserAccessibilityManager_setAccessibilityEventClassName( 306 env, obj, event, 307 base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj()); 308 Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes( 309 env, obj, event, 310 node->GetItemIndex(), 311 node->GetItemCount()); 312 Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes( 313 env, obj, event, 314 node->GetScrollX(), 315 node->GetScrollY(), 316 node->GetMaxScrollX(), 317 node->GetMaxScrollY()); 318 319 switch (event_type) { 320 case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED: 321 Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs( 322 env, obj, event, 323 node->GetTextChangeFromIndex(), 324 node->GetTextChangeAddedCount(), 325 node->GetTextChangeRemovedCount(), 326 base::android::ConvertUTF16ToJavaString( 327 env, node->GetTextChangeBeforeText()).obj(), 328 base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj()); 329 break; 330 case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED: 331 Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs( 332 env, obj, event, 333 node->GetSelectionStart(), 334 node->GetSelectionEnd(), 335 node->GetEditableTextLength(), 336 base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj()); 337 break; 338 default: 339 break; 340 } 341 342 // Backwards-compatible fallback for new KitKat APIs. 343 Java_BrowserAccessibilityManager_setAccessibilityEventKitKatAttributes( 344 env, obj, event, 345 node->CanOpenPopup(), 346 node->IsContentInvalid(), 347 node->IsDismissable(), 348 node->IsMultiLine(), 349 node->AndroidInputType(), 350 node->AndroidLiveRegionType()); 351 if (node->IsCollection()) { 352 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo( 353 env, obj, event, 354 node->RowCount(), 355 node->ColumnCount(), 356 node->IsHierarchical()); 357 } 358 if (node->IsHeading()) { 359 Java_BrowserAccessibilityManager_setAccessibilityEventHeadingFlag( 360 env, obj, event, true); 361 } 362 if (node->IsCollectionItem()) { 363 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo( 364 env, obj, event, 365 node->RowIndex(), 366 node->RowSpan(), 367 node->ColumnIndex(), 368 node->ColumnSpan()); 369 } 370 if (node->IsRangeType()) { 371 Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo( 372 env, obj, event, 373 node->AndroidRangeType(), 374 node->RangeMin(), 375 node->RangeMax(), 376 node->RangeCurrentValue()); 377 } 378 379 return true; 380 } 381 382 void BrowserAccessibilityManagerAndroid::Click( 383 JNIEnv* env, jobject obj, jint id) { 384 BrowserAccessibility* node = GetFromID(id); 385 if (node) 386 DoDefaultAction(*node); 387 } 388 389 void BrowserAccessibilityManagerAndroid::Focus( 390 JNIEnv* env, jobject obj, jint id) { 391 BrowserAccessibility* node = GetFromID(id); 392 if (node) 393 SetFocus(node, true); 394 } 395 396 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) { 397 SetFocus(GetRoot(), true); 398 } 399 400 void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible( 401 JNIEnv* env, jobject obj, jint id) { 402 BrowserAccessibility* node = GetFromID(id); 403 if (node) 404 ScrollToMakeVisible(*node, gfx::Rect(node->GetLocation().size())); 405 } 406 407 void BrowserAccessibilityManagerAndroid::HandleHoverEvent( 408 BrowserAccessibility* node) { 409 JNIEnv* env = AttachCurrentThread(); 410 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); 411 if (obj.is_null()) 412 return; 413 414 BrowserAccessibilityAndroid* ancestor = 415 static_cast<BrowserAccessibilityAndroid*>(node->GetParent()); 416 while (ancestor) { 417 if (ancestor->PlatformIsLeaf() || 418 (ancestor->IsFocusable() && !ancestor->HasFocusableChild())) { 419 node = ancestor; 420 // Don't break - we want the highest ancestor that's focusable or a 421 // leaf node. 422 } 423 ancestor = static_cast<BrowserAccessibilityAndroid*>(ancestor->GetParent()); 424 } 425 426 Java_BrowserAccessibilityManager_handleHover( 427 env, obj.obj(), node->GetId()); 428 } 429 430 jint BrowserAccessibilityManagerAndroid::FindElementType( 431 JNIEnv* env, jobject obj, jint start_id, jstring element_type_str, 432 jboolean forwards) { 433 BrowserAccessibility* node = GetFromID(start_id); 434 if (!node) 435 return 0; 436 437 AndroidHtmlElementType element_type = HtmlElementTypeFromString( 438 base::android::ConvertJavaStringToUTF16(env, element_type_str)); 439 440 node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node); 441 while (node) { 442 switch(element_type) { 443 case HTML_ELEMENT_TYPE_SECTION: 444 if (node->GetRole() == ui::AX_ROLE_ARTICLE || 445 node->GetRole() == ui::AX_ROLE_APPLICATION || 446 node->GetRole() == ui::AX_ROLE_BANNER || 447 node->GetRole() == ui::AX_ROLE_COMPLEMENTARY || 448 node->GetRole() == ui::AX_ROLE_CONTENT_INFO || 449 node->GetRole() == ui::AX_ROLE_HEADING || 450 node->GetRole() == ui::AX_ROLE_MAIN || 451 node->GetRole() == ui::AX_ROLE_NAVIGATION || 452 node->GetRole() == ui::AX_ROLE_SEARCH || 453 node->GetRole() == ui::AX_ROLE_REGION) { 454 return node->GetId(); 455 } 456 break; 457 case HTML_ELEMENT_TYPE_LIST: 458 if (node->GetRole() == ui::AX_ROLE_LIST || 459 node->GetRole() == ui::AX_ROLE_GRID || 460 node->GetRole() == ui::AX_ROLE_TABLE || 461 node->GetRole() == ui::AX_ROLE_TREE) { 462 return node->GetId(); 463 } 464 break; 465 case HTML_ELEMENT_TYPE_CONTROL: 466 if (static_cast<BrowserAccessibilityAndroid*>(node)->IsFocusable()) 467 return node->GetId(); 468 break; 469 case HTML_ELEMENT_TYPE_ANY: 470 // In theory, the API says that an accessibility service could 471 // jump to an element by element name, like 'H1' or 'P'. This isn't 472 // currently used by any accessibility service, and we think it's 473 // better to keep them high-level like 'SECTION' or 'CONTROL', so we 474 // just fall back on linear navigation when we don't recognize the 475 // element type. 476 if (static_cast<BrowserAccessibilityAndroid*>(node)->IsClickable()) 477 return node->GetId(); 478 break; 479 } 480 481 node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node); 482 } 483 484 return 0; 485 } 486 487 void BrowserAccessibilityManagerAndroid::OnRootChanged(ui::AXNode* new_root) { 488 JNIEnv* env = AttachCurrentThread(); 489 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); 490 if (obj.is_null()) 491 return; 492 493 Java_BrowserAccessibilityManager_handleNavigate(env, obj.obj()); 494 } 495 496 bool 497 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() { 498 // The Java layer handles the root scroll offset. 499 return false; 500 } 501 502 bool RegisterBrowserAccessibilityManager(JNIEnv* env) { 503 return RegisterNativesImpl(env); 504 } 505 506 } // namespace content 507