Home | History | Annotate | Download | only in accessibility
      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 // Restricts |val| to the range [min, max].
     30 int Clamp(int val, int min, int max) {
     31   return std::min(std::max(val, min), max);
     32 }
     33 
     34 }  // anonymous namespace
     35 
     36 namespace content {
     37 
     38 namespace aria_strings {
     39   const char kAriaLivePolite[] = "polite";
     40   const char kAriaLiveAssertive[] = "assertive";
     41 }
     42 
     43 // static
     44 BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
     45     const AccessibilityNodeData& src,
     46     BrowserAccessibilityDelegate* delegate,
     47     BrowserAccessibilityFactory* factory) {
     48   return new BrowserAccessibilityManagerAndroid(ScopedJavaLocalRef<jobject>(),
     49                                                 src, delegate, factory);
     50 }
     51 
     52 BrowserAccessibilityManagerAndroid*
     53 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
     54   return static_cast<BrowserAccessibilityManagerAndroid*>(this);
     55 }
     56 
     57 BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid(
     58     ScopedJavaLocalRef<jobject> content_view_core,
     59     const AccessibilityNodeData& src,
     60     BrowserAccessibilityDelegate* delegate,
     61     BrowserAccessibilityFactory* factory)
     62     : BrowserAccessibilityManager(src, delegate, factory) {
     63   SetContentViewCore(content_view_core);
     64 }
     65 
     66 BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() {
     67   JNIEnv* env = AttachCurrentThread();
     68   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
     69   if (obj.is_null())
     70     return;
     71 
     72   Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj());
     73 }
     74 
     75 // static
     76 AccessibilityNodeData BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
     77   AccessibilityNodeData empty_document;
     78   empty_document.id = 0;
     79   empty_document.role = blink::WebAXRoleRootWebArea;
     80   empty_document.state = 1 << blink::WebAXStateReadonly;
     81   return empty_document;
     82 }
     83 
     84 void BrowserAccessibilityManagerAndroid::SetContentViewCore(
     85     ScopedJavaLocalRef<jobject> content_view_core) {
     86   if (content_view_core.is_null())
     87     return;
     88 
     89   JNIEnv* env = AttachCurrentThread();
     90   java_ref_ = JavaObjectWeakGlobalRef(
     91       env, Java_BrowserAccessibilityManager_create(
     92           env, reinterpret_cast<intptr_t>(this),
     93           content_view_core.obj()).obj());
     94 }
     95 
     96 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
     97     blink::WebAXEvent event_type,
     98     BrowserAccessibility* node) {
     99   JNIEnv* env = AttachCurrentThread();
    100   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
    101   if (obj.is_null())
    102     return;
    103 
    104   if (event_type == blink::WebAXEventHide)
    105     return;
    106 
    107   // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
    108   // the Android system that the accessibility hierarchy rooted at this
    109   // node has changed.
    110   Java_BrowserAccessibilityManager_handleContentChanged(
    111       env, obj.obj(), node->renderer_id());
    112 
    113   switch (event_type) {
    114     case blink::WebAXEventLoadComplete:
    115       Java_BrowserAccessibilityManager_handlePageLoaded(
    116           env, obj.obj(), focus_->renderer_id());
    117       break;
    118     case blink::WebAXEventFocus:
    119       Java_BrowserAccessibilityManager_handleFocusChanged(
    120           env, obj.obj(), node->renderer_id());
    121       break;
    122     case blink::WebAXEventCheckedStateChanged:
    123       Java_BrowserAccessibilityManager_handleCheckStateChanged(
    124           env, obj.obj(), node->renderer_id());
    125       break;
    126     case blink::WebAXEventScrolledToAnchor:
    127       Java_BrowserAccessibilityManager_handleScrolledToAnchor(
    128           env, obj.obj(), node->renderer_id());
    129       break;
    130     case blink::WebAXEventAlert:
    131       // An alert is a special case of live region. Fall through to the
    132       // next case to handle it.
    133     case blink::WebAXEventShow: {
    134       // This event is fired when an object appears in a live region.
    135       // Speak its text.
    136       BrowserAccessibilityAndroid* android_node =
    137           static_cast<BrowserAccessibilityAndroid*>(node);
    138       Java_BrowserAccessibilityManager_announceLiveRegionText(
    139           env, obj.obj(),
    140           base::android::ConvertUTF16ToJavaString(
    141               env, android_node->GetText()).obj());
    142       break;
    143     }
    144     case blink::WebAXEventSelectedTextChanged:
    145       Java_BrowserAccessibilityManager_handleTextSelectionChanged(
    146           env, obj.obj(), node->renderer_id());
    147       break;
    148     case blink::WebAXEventChildrenChanged:
    149     case blink::WebAXEventTextChanged:
    150     case blink::WebAXEventValueChanged:
    151       if (node->IsEditableText()) {
    152         Java_BrowserAccessibilityManager_handleEditableTextChanged(
    153             env, obj.obj(), node->renderer_id());
    154       }
    155       break;
    156     default:
    157       // There are some notifications that aren't meaningful on Android.
    158       // It's okay to skip them.
    159       break;
    160   }
    161 }
    162 
    163 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
    164   return static_cast<jint>(root_->renderer_id());
    165 }
    166 
    167 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
    168     JNIEnv* env, jobject obj, jint id) {
    169   return GetFromRendererID(id) != NULL;
    170 }
    171 
    172 jint BrowserAccessibilityManagerAndroid::HitTest(
    173     JNIEnv* env, jobject obj, jint x, jint y) {
    174   BrowserAccessibilityAndroid* result =
    175       static_cast<BrowserAccessibilityAndroid*>(
    176           root_->BrowserAccessibilityForPoint(gfx::Point(x, y)));
    177 
    178   if (!result)
    179     return root_->renderer_id();
    180 
    181   if (result->IsFocusable())
    182     return result->renderer_id();
    183 
    184   // Examine the children of |result| to find the nearest accessibility focus
    185   // candidate
    186   BrowserAccessibility* nearest_node = FuzzyHitTest(x, y, result);
    187   if (nearest_node)
    188     return nearest_node->renderer_id();
    189 
    190   return root_->renderer_id();
    191 }
    192 
    193 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
    194     JNIEnv* env, jobject obj, jobject info, jint id) {
    195   BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
    196       GetFromRendererID(id));
    197   if (!node)
    198     return false;
    199 
    200   if (node->parent()) {
    201     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
    202         env, obj, info, node->parent()->renderer_id());
    203   }
    204   for (unsigned i = 0; i < node->PlatformChildCount(); ++i) {
    205     Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
    206         env, obj, info, node->children()[i]->renderer_id());
    207   }
    208   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
    209       env, obj, info,
    210       id,
    211       node->IsCheckable(),
    212       node->IsChecked(),
    213       node->IsClickable(),
    214       node->IsEnabled(),
    215       node->IsFocusable(),
    216       node->IsFocused(),
    217       node->IsPassword(),
    218       node->IsScrollable(),
    219       node->IsSelected(),
    220       node->IsVisibleToUser());
    221   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoStringAttributes(
    222       env, obj, info,
    223       base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj(),
    224       base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    225 
    226   gfx::Rect absolute_rect = node->GetLocalBoundsRect();
    227   gfx::Rect parent_relative_rect = absolute_rect;
    228   if (node->parent()) {
    229     gfx::Rect parent_rect = node->parent()->GetLocalBoundsRect();
    230     parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin());
    231   }
    232   bool is_root = node->parent() == NULL;
    233   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
    234       env, obj, info,
    235       absolute_rect.x(), absolute_rect.y(),
    236       parent_relative_rect.x(), parent_relative_rect.y(),
    237       absolute_rect.width(), absolute_rect.height(),
    238       is_root);
    239 
    240   // New KitKat APIs
    241   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes(
    242       env, obj, info,
    243       node->CanOpenPopup(),
    244       node->IsContentInvalid(),
    245       node->IsDismissable(),
    246       node->IsMultiLine(),
    247       node->AndroidInputType(),
    248       node->AndroidLiveRegionType());
    249   if (node->IsCollection()) {
    250     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo(
    251         env, obj, info,
    252         node->RowCount(),
    253         node->ColumnCount(),
    254         node->IsHierarchical());
    255   }
    256   if (node->IsCollectionItem() || node->IsHeading()) {
    257     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo(
    258         env, obj, info,
    259         node->RowIndex(),
    260         node->RowSpan(),
    261         node->ColumnIndex(),
    262         node->ColumnSpan(),
    263         node->IsHeading());
    264   }
    265   if (node->IsRangeType()) {
    266     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo(
    267         env, obj, info,
    268         node->AndroidRangeType(),
    269         node->RangeMin(),
    270         node->RangeMax(),
    271         node->RangeCurrentValue());
    272   }
    273 
    274   return true;
    275 }
    276 
    277 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
    278     JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) {
    279   BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
    280       GetFromRendererID(id));
    281   if (!node)
    282     return false;
    283 
    284   Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
    285       env, obj, event,
    286       node->IsChecked(),
    287       node->IsEnabled(),
    288       node->IsPassword(),
    289       node->IsScrollable());
    290   Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
    291       env, obj, event,
    292       base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
    293   Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
    294       env, obj, event,
    295       node->GetItemIndex(),
    296       node->GetItemCount());
    297   Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
    298       env, obj, event,
    299       node->GetScrollX(),
    300       node->GetScrollY(),
    301       node->GetMaxScrollX(),
    302       node->GetMaxScrollY());
    303 
    304   switch (event_type) {
    305     case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED:
    306       Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
    307           env, obj, event,
    308           node->GetTextChangeFromIndex(),
    309           node->GetTextChangeAddedCount(),
    310           node->GetTextChangeRemovedCount(),
    311           base::android::ConvertUTF16ToJavaString(
    312               env, node->GetTextChangeBeforeText()).obj(),
    313           base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    314       break;
    315     case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED:
    316       Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
    317           env, obj, event,
    318           node->GetSelectionStart(),
    319           node->GetSelectionEnd(),
    320           node->GetEditableTextLength(),
    321           base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    322       break;
    323     default:
    324       break;
    325   }
    326 
    327   // Backwards-compatible fallback for new KitKat APIs.
    328   Java_BrowserAccessibilityManager_setAccessibilityEventKitKatAttributes(
    329       env, obj, event,
    330       node->CanOpenPopup(),
    331       node->IsContentInvalid(),
    332       node->IsDismissable(),
    333       node->IsMultiLine(),
    334       node->AndroidInputType(),
    335       node->AndroidLiveRegionType());
    336   if (node->IsCollection()) {
    337     Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo(
    338         env, obj, event,
    339         node->RowCount(),
    340         node->ColumnCount(),
    341         node->IsHierarchical());
    342   }
    343   if (node->IsCollectionItem() || node->IsHeading()) {
    344     Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo(
    345         env, obj, event,
    346         node->RowIndex(),
    347         node->RowSpan(),
    348         node->ColumnIndex(),
    349         node->ColumnSpan(),
    350         node->IsHeading());
    351   }
    352   if (node->IsRangeType()) {
    353     Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo(
    354         env, obj, event,
    355         node->AndroidRangeType(),
    356         node->RangeMin(),
    357         node->RangeMax(),
    358         node->RangeCurrentValue());
    359   }
    360 
    361   return true;
    362 }
    363 
    364 void BrowserAccessibilityManagerAndroid::Click(
    365     JNIEnv* env, jobject obj, jint id) {
    366   BrowserAccessibility* node = GetFromRendererID(id);
    367   if (node)
    368     DoDefaultAction(*node);
    369 }
    370 
    371 void BrowserAccessibilityManagerAndroid::Focus(
    372     JNIEnv* env, jobject obj, jint id) {
    373   BrowserAccessibility* node = GetFromRendererID(id);
    374   if (node)
    375     SetFocus(node, true);
    376 }
    377 
    378 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
    379   SetFocus(root_, true);
    380 }
    381 
    382 BrowserAccessibility* BrowserAccessibilityManagerAndroid::FuzzyHitTest(
    383     int x, int y, BrowserAccessibility* start_node) {
    384   BrowserAccessibility* nearest_node = NULL;
    385   int min_distance = INT_MAX;
    386   FuzzyHitTestImpl(x, y, start_node, &nearest_node, &min_distance);
    387   return nearest_node;
    388 }
    389 
    390 // static
    391 void BrowserAccessibilityManagerAndroid::FuzzyHitTestImpl(
    392     int x, int y, BrowserAccessibility* start_node,
    393     BrowserAccessibility** nearest_candidate, int* nearest_distance) {
    394   BrowserAccessibilityAndroid* node =
    395       static_cast<BrowserAccessibilityAndroid*>(start_node);
    396   int distance = CalculateDistanceSquared(x, y, node);
    397 
    398   if (node->IsFocusable()) {
    399     if (distance < *nearest_distance) {
    400       *nearest_candidate = node;
    401       *nearest_distance = distance;
    402     }
    403     // Don't examine any more children of focusable node
    404     // TODO(aboxhall): what about focusable children?
    405     return;
    406   }
    407 
    408   if (!node->GetText().empty()) {
    409     if (distance < *nearest_distance) {
    410       *nearest_candidate = node;
    411       *nearest_distance = distance;
    412     }
    413     return;
    414   }
    415 
    416   for (uint32 i = 0; i < node->PlatformChildCount(); i++) {
    417     BrowserAccessibility* child = node->PlatformGetChild(i);
    418     FuzzyHitTestImpl(x, y, child, nearest_candidate, nearest_distance);
    419   }
    420 }
    421 
    422 // static
    423 int BrowserAccessibilityManagerAndroid::CalculateDistanceSquared(
    424     int x, int y, BrowserAccessibility* node) {
    425   gfx::Rect node_bounds = node->GetLocalBoundsRect();
    426   int nearest_x = Clamp(x, node_bounds.x(), node_bounds.right());
    427   int nearest_y = Clamp(y, node_bounds.y(), node_bounds.bottom());
    428   int dx = std::abs(x - nearest_x);
    429   int dy = std::abs(y - nearest_y);
    430   return dx * dx + dy * dy;
    431 }
    432 
    433 void BrowserAccessibilityManagerAndroid::NotifyRootChanged() {
    434   JNIEnv* env = AttachCurrentThread();
    435   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
    436   if (obj.is_null())
    437     return;
    438 
    439   Java_BrowserAccessibilityManager_handleNavigate(env, obj.obj());
    440 }
    441 
    442 bool
    443 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
    444   // The Java layer handles the root scroll offset.
    445   return false;
    446 }
    447 
    448 bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
    449   return RegisterNativesImpl(env);
    450 }
    451 
    452 }  // namespace content
    453