// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/accessibility/browser_accessibility_manager_android.h" #include #include "base/android/jni_android.h" #include "base/android/jni_string.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "content/browser/accessibility/browser_accessibility_android.h" #include "content/common/accessibility_messages.h" #include "jni/BrowserAccessibilityManager_jni.h" using base::android::AttachCurrentThread; using base::android::ScopedJavaLocalRef; namespace { // These are enums from android.view.accessibility.AccessibilityEvent in Java: enum { ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED = 16, ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192 }; enum AndroidHtmlElementType { HTML_ELEMENT_TYPE_SECTION, HTML_ELEMENT_TYPE_LIST, HTML_ELEMENT_TYPE_CONTROL, HTML_ELEMENT_TYPE_ANY }; // These are special unofficial strings sent from TalkBack/BrailleBack // to jump to certain categories of web elements. AndroidHtmlElementType HtmlElementTypeFromString(base::string16 element_type) { if (element_type == base::ASCIIToUTF16("SECTION")) return HTML_ELEMENT_TYPE_SECTION; else if (element_type == base::ASCIIToUTF16("LIST")) return HTML_ELEMENT_TYPE_LIST; else if (element_type == base::ASCIIToUTF16("CONTROL")) return HTML_ELEMENT_TYPE_CONTROL; else return HTML_ELEMENT_TYPE_ANY; } } // anonymous namespace namespace content { namespace aria_strings { const char kAriaLivePolite[] = "polite"; const char kAriaLiveAssertive[] = "assertive"; } // static BrowserAccessibilityManager* BrowserAccessibilityManager::Create( const ui::AXTreeUpdate& initial_tree, BrowserAccessibilityDelegate* delegate, BrowserAccessibilityFactory* factory) { return new BrowserAccessibilityManagerAndroid( ScopedJavaLocalRef(), initial_tree, delegate, factory); } BrowserAccessibilityManagerAndroid* BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() { return static_cast(this); } BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid( ScopedJavaLocalRef content_view_core, const ui::AXTreeUpdate& initial_tree, BrowserAccessibilityDelegate* delegate, BrowserAccessibilityFactory* factory) : BrowserAccessibilityManager(delegate, factory) { SetContentViewCore(content_view_core); Initialize(initial_tree); } BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef obj = java_ref_.get(env); if (obj.is_null()) return; Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj()); } // static ui::AXTreeUpdate BrowserAccessibilityManagerAndroid::GetEmptyDocument() { ui::AXNodeData empty_document; empty_document.id = 0; empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA; empty_document.state = 1 << ui::AX_STATE_READ_ONLY; ui::AXTreeUpdate update; update.nodes.push_back(empty_document); return update; } void BrowserAccessibilityManagerAndroid::SetContentViewCore( ScopedJavaLocalRef content_view_core) { if (content_view_core.is_null()) return; JNIEnv* env = AttachCurrentThread(); java_ref_ = JavaObjectWeakGlobalRef( env, Java_BrowserAccessibilityManager_create( env, reinterpret_cast(this), content_view_core.obj()).obj()); } void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent( ui::AXEvent event_type, BrowserAccessibility* node) { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef obj = java_ref_.get(env); if (obj.is_null()) return; if (event_type == ui::AX_EVENT_HIDE) return; if (event_type == ui::AX_EVENT_HOVER) { HandleHoverEvent(node); return; } // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify // the Android system that the accessibility hierarchy rooted at this // node has changed. Java_BrowserAccessibilityManager_handleContentChanged( env, obj.obj(), node->GetId()); switch (event_type) { case ui::AX_EVENT_LOAD_COMPLETE: Java_BrowserAccessibilityManager_handlePageLoaded( env, obj.obj(), focus_->id()); break; case ui::AX_EVENT_FOCUS: Java_BrowserAccessibilityManager_handleFocusChanged( env, obj.obj(), node->GetId()); break; case ui::AX_EVENT_CHECKED_STATE_CHANGED: Java_BrowserAccessibilityManager_handleCheckStateChanged( env, obj.obj(), node->GetId()); break; case ui::AX_EVENT_SCROLL_POSITION_CHANGED: Java_BrowserAccessibilityManager_handleScrollPositionChanged( env, obj.obj(), node->GetId()); break; case ui::AX_EVENT_SCROLLED_TO_ANCHOR: Java_BrowserAccessibilityManager_handleScrolledToAnchor( env, obj.obj(), node->GetId()); break; case ui::AX_EVENT_ALERT: // An alert is a special case of live region. Fall through to the // next case to handle it. case ui::AX_EVENT_SHOW: { // This event is fired when an object appears in a live region. // Speak its text. BrowserAccessibilityAndroid* android_node = static_cast(node); Java_BrowserAccessibilityManager_announceLiveRegionText( env, obj.obj(), base::android::ConvertUTF16ToJavaString( env, android_node->GetText()).obj()); break; } case ui::AX_EVENT_TEXT_SELECTION_CHANGED: Java_BrowserAccessibilityManager_handleTextSelectionChanged( env, obj.obj(), node->GetId()); break; case ui::AX_EVENT_CHILDREN_CHANGED: case ui::AX_EVENT_TEXT_CHANGED: case ui::AX_EVENT_VALUE_CHANGED: if (node->IsEditableText()) { Java_BrowserAccessibilityManager_handleEditableTextChanged( env, obj.obj(), node->GetId()); } break; default: // There are some notifications that aren't meaningful on Android. // It's okay to skip them. break; } } jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) { return static_cast(GetRoot()->GetId()); } jboolean BrowserAccessibilityManagerAndroid::IsNodeValid( JNIEnv* env, jobject obj, jint id) { return GetFromID(id) != NULL; } void BrowserAccessibilityManagerAndroid::HitTest( JNIEnv* env, jobject obj, jint x, jint y) { if (delegate()) delegate()->AccessibilityHitTest(gfx::Point(x, y)); } jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo( JNIEnv* env, jobject obj, jobject info, jint id) { BrowserAccessibilityAndroid* node = static_cast( GetFromID(id)); if (!node) return false; if (node->GetParent()) { Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent( env, obj, info, node->GetParent()->GetId()); } for (unsigned i = 0; i < node->PlatformChildCount(); ++i) { Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild( env, obj, info, node->InternalGetChild(i)->GetId()); } Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes( env, obj, info, id, node->IsCheckable(), node->IsChecked(), node->IsClickable(), node->IsEnabled(), node->IsFocusable(), node->IsFocused(), node->IsPassword(), node->IsScrollable(), node->IsSelected(), node->IsVisibleToUser()); Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName( env, obj, info, base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj()); Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription( env, obj, info, base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj(), node->IsLink()); gfx::Rect absolute_rect = node->GetLocalBoundsRect(); gfx::Rect parent_relative_rect = absolute_rect; if (node->GetParent()) { gfx::Rect parent_rect = node->GetParent()->GetLocalBoundsRect(); parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin()); } bool is_root = node->GetParent() == NULL; Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation( env, obj, info, id, absolute_rect.x(), absolute_rect.y(), parent_relative_rect.x(), parent_relative_rect.y(), absolute_rect.width(), absolute_rect.height(), is_root); // New KitKat APIs Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes( env, obj, info, node->CanOpenPopup(), node->IsContentInvalid(), node->IsDismissable(), node->IsMultiLine(), node->AndroidInputType(), node->AndroidLiveRegionType()); if (node->IsCollection()) { Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo( env, obj, info, node->RowCount(), node->ColumnCount(), node->IsHierarchical()); } if (node->IsCollectionItem() || node->IsHeading()) { Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo( env, obj, info, node->RowIndex(), node->RowSpan(), node->ColumnIndex(), node->ColumnSpan(), node->IsHeading()); } if (node->IsRangeType()) { Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo( env, obj, info, node->AndroidRangeType(), node->RangeMin(), node->RangeMax(), node->RangeCurrentValue()); } return true; } jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent( JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) { BrowserAccessibilityAndroid* node = static_cast( GetFromID(id)); if (!node) return false; Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes( env, obj, event, node->IsChecked(), node->IsEnabled(), node->IsPassword(), node->IsScrollable()); Java_BrowserAccessibilityManager_setAccessibilityEventClassName( env, obj, event, base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj()); Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes( env, obj, event, node->GetItemIndex(), node->GetItemCount()); Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes( env, obj, event, node->GetScrollX(), node->GetScrollY(), node->GetMaxScrollX(), node->GetMaxScrollY()); switch (event_type) { case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED: Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs( env, obj, event, node->GetTextChangeFromIndex(), node->GetTextChangeAddedCount(), node->GetTextChangeRemovedCount(), base::android::ConvertUTF16ToJavaString( env, node->GetTextChangeBeforeText()).obj(), base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj()); break; case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED: Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs( env, obj, event, node->GetSelectionStart(), node->GetSelectionEnd(), node->GetEditableTextLength(), base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj()); break; default: break; } // Backwards-compatible fallback for new KitKat APIs. Java_BrowserAccessibilityManager_setAccessibilityEventKitKatAttributes( env, obj, event, node->CanOpenPopup(), node->IsContentInvalid(), node->IsDismissable(), node->IsMultiLine(), node->AndroidInputType(), node->AndroidLiveRegionType()); if (node->IsCollection()) { Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo( env, obj, event, node->RowCount(), node->ColumnCount(), node->IsHierarchical()); } if (node->IsHeading()) { Java_BrowserAccessibilityManager_setAccessibilityEventHeadingFlag( env, obj, event, true); } if (node->IsCollectionItem()) { Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo( env, obj, event, node->RowIndex(), node->RowSpan(), node->ColumnIndex(), node->ColumnSpan()); } if (node->IsRangeType()) { Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo( env, obj, event, node->AndroidRangeType(), node->RangeMin(), node->RangeMax(), node->RangeCurrentValue()); } return true; } void BrowserAccessibilityManagerAndroid::Click( JNIEnv* env, jobject obj, jint id) { BrowserAccessibility* node = GetFromID(id); if (node) DoDefaultAction(*node); } void BrowserAccessibilityManagerAndroid::Focus( JNIEnv* env, jobject obj, jint id) { BrowserAccessibility* node = GetFromID(id); if (node) SetFocus(node, true); } void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) { SetFocus(GetRoot(), true); } void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible( JNIEnv* env, jobject obj, jint id) { BrowserAccessibility* node = GetFromID(id); if (node) ScrollToMakeVisible(*node, gfx::Rect(node->GetLocation().size())); } void BrowserAccessibilityManagerAndroid::HandleHoverEvent( BrowserAccessibility* node) { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef obj = java_ref_.get(env); if (obj.is_null()) return; BrowserAccessibilityAndroid* ancestor = static_cast(node->GetParent()); while (ancestor && ancestor != GetRoot()) { if (ancestor->PlatformIsLeaf() || (ancestor->IsFocusable() && !ancestor->HasFocusableChild())) { node = ancestor; // Don't break - we want the highest ancestor that's focusable or a // leaf node. } ancestor = static_cast(ancestor->GetParent()); } Java_BrowserAccessibilityManager_handleHover( env, obj.obj(), node->GetId()); } jint BrowserAccessibilityManagerAndroid::FindElementType( JNIEnv* env, jobject obj, jint start_id, jstring element_type_str, jboolean forwards) { BrowserAccessibility* node = GetFromID(start_id); if (!node) return 0; AndroidHtmlElementType element_type = HtmlElementTypeFromString( base::android::ConvertJavaStringToUTF16(env, element_type_str)); node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node); while (node) { switch(element_type) { case HTML_ELEMENT_TYPE_SECTION: if (node->GetRole() == ui::AX_ROLE_ARTICLE || node->GetRole() == ui::AX_ROLE_APPLICATION || node->GetRole() == ui::AX_ROLE_BANNER || node->GetRole() == ui::AX_ROLE_COMPLEMENTARY || node->GetRole() == ui::AX_ROLE_CONTENT_INFO || node->GetRole() == ui::AX_ROLE_HEADING || node->GetRole() == ui::AX_ROLE_MAIN || node->GetRole() == ui::AX_ROLE_NAVIGATION || node->GetRole() == ui::AX_ROLE_SEARCH || node->GetRole() == ui::AX_ROLE_REGION) { return node->GetId(); } break; case HTML_ELEMENT_TYPE_LIST: if (node->GetRole() == ui::AX_ROLE_LIST || node->GetRole() == ui::AX_ROLE_GRID || node->GetRole() == ui::AX_ROLE_TABLE || node->GetRole() == ui::AX_ROLE_TREE) { return node->GetId(); } break; case HTML_ELEMENT_TYPE_CONTROL: if (static_cast(node)->IsFocusable()) return node->GetId(); break; case HTML_ELEMENT_TYPE_ANY: // In theory, the API says that an accessibility service could // jump to an element by element name, like 'H1' or 'P'. This isn't // currently used by any accessibility service, and we think it's // better to keep them high-level like 'SECTION' or 'CONTROL', so we // just fall back on linear navigation when we don't recognize the // element type. if (static_cast(node)->IsClickable()) return node->GetId(); break; } node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node); } return 0; } void BrowserAccessibilityManagerAndroid::OnRootChanged(ui::AXNode* new_root) { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef obj = java_ref_.get(env); if (obj.is_null()) return; Java_BrowserAccessibilityManager_handleNavigate(env, obj.obj()); } bool BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() { // The Java layer handles the root scroll offset. return false; } bool RegisterBrowserAccessibilityManager(JNIEnv* env) { return RegisterNativesImpl(env); } } // namespace content