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_TEXT_SELECTION_CHANGED: 172 Java_BrowserAccessibilityManager_handleTextSelectionChanged( 173 env, obj.obj(), node->GetId()); 174 break; 175 case ui::AX_EVENT_TEXT_CHANGED: 176 case ui::AX_EVENT_VALUE_CHANGED: 177 if (node->IsEditableText() && GetFocus(GetRoot()) == node) { 178 Java_BrowserAccessibilityManager_handleEditableTextChanged( 179 env, obj.obj(), node->GetId()); 180 } 181 break; 182 default: 183 // There are some notifications that aren't meaningful on Android. 184 // It's okay to skip them. 185 break; 186 } 187 } 188 189 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) { 190 return static_cast<jint>(GetRoot()->GetId()); 191 } 192 193 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid( 194 JNIEnv* env, jobject obj, jint id) { 195 return GetFromID(id) != NULL; 196 } 197 198 void BrowserAccessibilityManagerAndroid::HitTest( 199 JNIEnv* env, jobject obj, jint x, jint y) { 200 if (delegate()) 201 delegate()->AccessibilityHitTest(gfx::Point(x, y)); 202 } 203 204 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo( 205 JNIEnv* env, jobject obj, jobject info, jint id) { 206 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>( 207 GetFromID(id)); 208 if (!node) 209 return false; 210 211 if (node->GetParent()) { 212 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent( 213 env, obj, info, node->GetParent()->GetId()); 214 } 215 for (unsigned i = 0; i < node->PlatformChildCount(); ++i) { 216 Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild( 217 env, obj, info, node->InternalGetChild(i)->GetId()); 218 } 219 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes( 220 env, obj, info, 221 id, 222 node->IsCheckable(), 223 node->IsChecked(), 224 node->IsClickable(), 225 node->IsEnabled(), 226 node->IsFocusable(), 227 node->IsFocused(), 228 node->IsPassword(), 229 node->IsScrollable(), 230 node->IsSelected(), 231 node->IsVisibleToUser()); 232 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName( 233 env, obj, info, 234 base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj()); 235 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription( 236 env, obj, info, 237 base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj(), 238 node->IsLink()); 239 240 gfx::Rect absolute_rect = node->GetLocalBoundsRect(); 241 gfx::Rect parent_relative_rect = absolute_rect; 242 if (node->GetParent()) { 243 gfx::Rect parent_rect = node->GetParent()->GetLocalBoundsRect(); 244 parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin()); 245 } 246 bool is_root = node->GetParent() == NULL; 247 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation( 248 env, obj, info, 249 id, 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