| // 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 <cmath> |
| |
| #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<jobject>(), |
| src, delegate, factory); |
| } |
| |
| BrowserAccessibilityManagerAndroid* |
| BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() { |
| return static_cast<BrowserAccessibilityManagerAndroid*>(this); |
| } |
| |
| BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid( |
| ScopedJavaLocalRef<jobject> content_view_core, |
| const AccessibilityNodeData& src, |
| BrowserAccessibilityDelegate* delegate, |
| BrowserAccessibilityFactory* factory) |
| : BrowserAccessibilityManager(src, delegate, factory) { |
| SetContentViewCore(content_view_core); |
| } |
| |
| BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> 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 = WebKit::WebAXRoleRootWebArea; |
| empty_document.state = 1 << WebKit::WebAXStateReadonly; |
| return empty_document; |
| } |
| |
| void BrowserAccessibilityManagerAndroid::SetContentViewCore( |
| ScopedJavaLocalRef<jobject> content_view_core) { |
| if (content_view_core.is_null()) |
| return; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| java_ref_ = JavaObjectWeakGlobalRef( |
| env, Java_BrowserAccessibilityManager_create( |
| env, reinterpret_cast<jint>(this), content_view_core.obj()).obj()); |
| } |
| |
| void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent( |
| WebKit::WebAXEvent event_type, |
| BrowserAccessibility* node) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); |
| if (obj.is_null()) |
| 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->renderer_id()); |
| |
| switch (event_type) { |
| case WebKit::WebAXEventLoadComplete: |
| Java_BrowserAccessibilityManager_handlePageLoaded( |
| env, obj.obj(), focus_->renderer_id()); |
| break; |
| case WebKit::WebAXEventFocus: |
| Java_BrowserAccessibilityManager_handleFocusChanged( |
| env, obj.obj(), node->renderer_id()); |
| break; |
| case WebKit::WebAXEventCheckedStateChanged: |
| Java_BrowserAccessibilityManager_handleCheckStateChanged( |
| env, obj.obj(), node->renderer_id()); |
| break; |
| case WebKit::WebAXEventScrolledToAnchor: |
| Java_BrowserAccessibilityManager_handleScrolledToAnchor( |
| env, obj.obj(), node->renderer_id()); |
| break; |
| case WebKit::WebAXEventAlert: |
| // An alert is a special case of live region. Fall through to the |
| // next case to handle it. |
| case WebKit::WebAXEventShow: { |
| // This event is fired when an object appears in a live region. |
| // Speak its text. |
| BrowserAccessibilityAndroid* android_node = |
| static_cast<BrowserAccessibilityAndroid*>(node); |
| Java_BrowserAccessibilityManager_announceLiveRegionText( |
| env, obj.obj(), |
| base::android::ConvertUTF16ToJavaString( |
| env, android_node->GetText()).obj()); |
| break; |
| } |
| case WebKit::WebAXEventSelectedTextChanged: |
| Java_BrowserAccessibilityManager_handleTextSelectionChanged( |
| env, obj.obj(), node->renderer_id()); |
| break; |
| case WebKit::WebAXEventChildrenChanged: |
| case WebKit::WebAXEventTextChanged: |
| case WebKit::WebAXEventValueChanged: |
| if (node->IsEditableText()) { |
| Java_BrowserAccessibilityManager_handleEditableTextChanged( |
| 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<jint>(root_->renderer_id()); |
| } |
| |
| jint BrowserAccessibilityManagerAndroid::HitTest( |
| JNIEnv* env, jobject obj, jint x, jint y) { |
| BrowserAccessibilityAndroid* result = |
| static_cast<BrowserAccessibilityAndroid*>( |
| 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<BrowserAccessibilityAndroid*>( |
| GetFromRendererID(id)); |
| if (!node) |
| return false; |
| |
| if (node->parent()) { |
| Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent( |
| env, obj, info, node->parent()->renderer_id()); |
| } |
| for (unsigned i = 0; i < node->PlatformChildCount(); ++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<BrowserAccessibilityAndroid*>( |
| 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<BrowserAccessibilityAndroid*>(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; |
| } |
| |
| for (uint32 i = 0; i < node->PlatformChildCount(); i++) { |
| BrowserAccessibility* child = node->PlatformGetChild(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<jobject> 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 |