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 18using base::android::AttachCurrentThread; 19using base::android::ScopedJavaLocalRef; 20 21namespace { 22 23// These are enums from android.view.accessibility.AccessibilityEvent in Java: 24enum { 25 ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED = 16, 26 ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192 27}; 28 29enum 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. 38AndroidHtmlElementType 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 51namespace content { 52 53namespace aria_strings { 54 const char kAriaLivePolite[] = "polite"; 55 const char kAriaLiveAssertive[] = "assertive"; 56} 57 58// static 59BrowserAccessibilityManager* 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 67BrowserAccessibilityManagerAndroid* 68BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() { 69 return static_cast<BrowserAccessibilityManagerAndroid*>(this); 70} 71 72BrowserAccessibilityManagerAndroid::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 81BrowserAccessibilityManagerAndroid::~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 91ui::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 102void 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 114void 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 190jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) { 191 return static_cast<jint>(GetRoot()->GetId()); 192} 193 194jboolean BrowserAccessibilityManagerAndroid::IsNodeValid( 195 JNIEnv* env, jobject obj, jint id) { 196 return GetFromID(id) != NULL; 197} 198 199void BrowserAccessibilityManagerAndroid::HitTest( 200 JNIEnv* env, jobject obj, jint x, jint y) { 201 if (delegate()) 202 delegate()->AccessibilityHitTest(gfx::Point(x, y)); 203} 204 205jboolean 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 292jboolean 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 382void BrowserAccessibilityManagerAndroid::Click( 383 JNIEnv* env, jobject obj, jint id) { 384 BrowserAccessibility* node = GetFromID(id); 385 if (node) 386 DoDefaultAction(*node); 387} 388 389void BrowserAccessibilityManagerAndroid::Focus( 390 JNIEnv* env, jobject obj, jint id) { 391 BrowserAccessibility* node = GetFromID(id); 392 if (node) 393 SetFocus(node, true); 394} 395 396void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) { 397 SetFocus(GetRoot(), true); 398} 399 400void 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 407void 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 430jint 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 487void 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 496bool 497BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() { 498 // The Java layer handles the root scroll offset. 499 return false; 500} 501 502bool RegisterBrowserAccessibilityManager(JNIEnv* env) { 503 return RegisterNativesImpl(env); 504} 505 506} // namespace content 507