// 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 }; // Restricts |val| to the range [min, max]. int Clamp(int val, int min, int max) { return std::min(std::max(val, min), max); } } // anonymous namespace namespace content { namespace aria_strings { const char kAriaLivePolite[] = "polite"; const char kAriaLiveAssertive[] = "assertive"; } // static BrowserAccessibilityManager* BrowserAccessibilityManager::Create( const AccessibilityNodeData& src, BrowserAccessibilityDelegate* delegate, BrowserAccessibilityFactory* factory) { return new BrowserAccessibilityManagerAndroid(ScopedJavaLocalRef(), src, delegate, factory); } BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid( ScopedJavaLocalRef content_view_core, const AccessibilityNodeData& src, BrowserAccessibilityDelegate* delegate, BrowserAccessibilityFactory* factory) : BrowserAccessibilityManager(src, delegate, factory) { 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()); } BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef obj = java_ref_.get(env); if (obj.is_null()) return; Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj()); } // static AccessibilityNodeData BrowserAccessibilityManagerAndroid::GetEmptyDocument() { AccessibilityNodeData empty_document; empty_document.id = 0; empty_document.role = AccessibilityNodeData::ROLE_ROOT_WEB_AREA; empty_document.state = 1 << AccessibilityNodeData::STATE_READONLY; return empty_document; } void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent( int type, BrowserAccessibility* node) { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef obj = java_ref_.get(env); if (obj.is_null()) return; switch (type) { case AccessibilityNotificationLoadComplete: Java_BrowserAccessibilityManager_handlePageLoaded( env, obj.obj(), focus_->renderer_id()); break; case AccessibilityNotificationFocusChanged: Java_BrowserAccessibilityManager_handleFocusChanged( env, obj.obj(), node->renderer_id()); break; case AccessibilityNotificationCheckStateChanged: Java_BrowserAccessibilityManager_handleCheckStateChanged( env, obj.obj(), node->renderer_id()); break; case AccessibilityNotificationScrolledToAnchor: Java_BrowserAccessibilityManager_handleScrolledToAnchor( env, obj.obj(), node->renderer_id()); break; case AccessibilityNotificationAlert: // An alert is a special case of live region. Fall through to the // next case to handle it. case AccessibilityNotificationObjectShow: { // 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 AccessibilityNotificationSelectedTextChanged: Java_BrowserAccessibilityManager_handleTextSelectionChanged( env, obj.obj(), node->renderer_id()); break; case AccessibilityNotificationChildrenChanged: case AccessibilityNotificationTextChanged: case AccessibilityNotificationValueChanged: if (node->IsEditableText()) { Java_BrowserAccessibilityManager_handleEditableTextChanged( env, obj.obj(), node->renderer_id()); } else { Java_BrowserAccessibilityManager_handleContentChanged( env, obj.obj(), node->renderer_id()); } 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(root_->renderer_id()); } jint BrowserAccessibilityManagerAndroid::HitTest( JNIEnv* env, jobject obj, jint x, jint y) { BrowserAccessibilityAndroid* result = static_cast( root_->BrowserAccessibilityForPoint(gfx::Point(x, y))); if (!result) return root_->renderer_id(); if (result->IsFocusable()) return result->renderer_id(); // Examine the children of |result| to find the nearest accessibility focus // candidate BrowserAccessibility* nearest_node = FuzzyHitTest(x, y, result); if (nearest_node) return nearest_node->renderer_id(); return root_->renderer_id(); } jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo( JNIEnv* env, jobject obj, jobject info, jint id) { BrowserAccessibilityAndroid* node = static_cast( GetFromRendererID(id)); if (!node) return false; if (node->parent()) { Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent( env, obj, info, node->parent()->renderer_id()); } if (!node->IsLeaf()) { for (unsigned i = 0; i < node->child_count(); ++i) { Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild( env, obj, info, node->children()[i]->renderer_id()); } } 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_setAccessibilityNodeInfoStringAttributes( env, obj, info, base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj(), base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj()); gfx::Rect absolute_rect = node->GetLocalBoundsRect(); gfx::Rect parent_relative_rect = absolute_rect; if (node->parent()) { gfx::Rect parent_rect = node->parent()->GetLocalBoundsRect(); parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin()); } bool is_root = node->parent() == NULL; Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation( env, obj, info, absolute_rect.x(), absolute_rect.y(), parent_relative_rect.x(), parent_relative_rect.y(), absolute_rect.width(), absolute_rect.height(), is_root); return true; } jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent( JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) { BrowserAccessibilityAndroid* node = static_cast( GetFromRendererID(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; } return true; } void BrowserAccessibilityManagerAndroid::Click( JNIEnv* env, jobject obj, jint id) { BrowserAccessibility* node = GetFromRendererID(id); if (node) DoDefaultAction(*node); } void BrowserAccessibilityManagerAndroid::Focus( JNIEnv* env, jobject obj, jint id) { BrowserAccessibility* node = GetFromRendererID(id); if (node) SetFocus(node, true); } void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) { SetFocus(root_, true); } BrowserAccessibility* BrowserAccessibilityManagerAndroid::FuzzyHitTest( int x, int y, BrowserAccessibility* start_node) { BrowserAccessibility* nearest_node = NULL; int min_distance = INT_MAX; FuzzyHitTestImpl(x, y, start_node, &nearest_node, &min_distance); return nearest_node; } // static void BrowserAccessibilityManagerAndroid::FuzzyHitTestImpl( int x, int y, BrowserAccessibility* start_node, BrowserAccessibility** nearest_candidate, int* nearest_distance) { BrowserAccessibilityAndroid* node = static_cast(start_node); int distance = CalculateDistanceSquared(x, y, node); if (node->IsFocusable()) { if (distance < *nearest_distance) { *nearest_candidate = node; *nearest_distance = distance; } // Don't examine any more children of focusable node // TODO(aboxhall): what about focusable children? return; } if (!node->GetText().empty()) { if (distance < *nearest_distance) { *nearest_candidate = node; *nearest_distance = distance; } return; } if (!node->IsLeaf()) { for (uint32 i = 0; i < node->child_count(); i++) { BrowserAccessibility* child = node->GetChild(i); FuzzyHitTestImpl(x, y, child, nearest_candidate, nearest_distance); } } } // static int BrowserAccessibilityManagerAndroid::CalculateDistanceSquared( int x, int y, BrowserAccessibility* node) { gfx::Rect node_bounds = node->GetLocalBoundsRect(); int nearest_x = Clamp(x, node_bounds.x(), node_bounds.right()); int nearest_y = Clamp(y, node_bounds.y(), node_bounds.bottom()); int dx = std::abs(x - nearest_x); int dy = std::abs(y - nearest_y); return dx * dx + dy * dy; } void BrowserAccessibilityManagerAndroid::NotifyRootChanged() { 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