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::BrowserAccessibilityManagerAndroid(
     53     ScopedJavaLocalRef<jobject> content_view_core,
     54     const AccessibilityNodeData& src,
     55     BrowserAccessibilityDelegate* delegate,
     56     BrowserAccessibilityFactory* factory)
     57     : BrowserAccessibilityManager(src, delegate, factory) {
     58   if (content_view_core.is_null())
     59     return;
     60 
     61   JNIEnv* env = AttachCurrentThread();
     62   java_ref_ = JavaObjectWeakGlobalRef(
     63       env, Java_BrowserAccessibilityManager_create(
     64           env, reinterpret_cast<jint>(this), content_view_core.obj()).obj());
     65 }
     66 
     67 BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() {
     68   JNIEnv* env = AttachCurrentThread();
     69   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
     70   if (obj.is_null())
     71     return;
     72 
     73   Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj());
     74 }
     75 
     76 // static
     77 AccessibilityNodeData BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
     78   AccessibilityNodeData empty_document;
     79   empty_document.id = 0;
     80   empty_document.role = AccessibilityNodeData::ROLE_ROOT_WEB_AREA;
     81   empty_document.state = 1 << AccessibilityNodeData::STATE_READONLY;
     82   return empty_document;
     83 }
     84 
     85 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
     86     int type,
     87     BrowserAccessibility* node) {
     88   JNIEnv* env = AttachCurrentThread();
     89   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
     90   if (obj.is_null())
     91     return;
     92 
     93   // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
     94   // the Android system that the accessibility hierarchy rooted at this
     95   // node has changed.
     96   Java_BrowserAccessibilityManager_handleContentChanged(
     97       env, obj.obj(), node->renderer_id());
     98 
     99   switch (type) {
    100     case AccessibilityNotificationLoadComplete:
    101       Java_BrowserAccessibilityManager_handlePageLoaded(
    102           env, obj.obj(), focus_->renderer_id());
    103       break;
    104     case AccessibilityNotificationFocusChanged:
    105       Java_BrowserAccessibilityManager_handleFocusChanged(
    106           env, obj.obj(), node->renderer_id());
    107       break;
    108     case AccessibilityNotificationCheckStateChanged:
    109       Java_BrowserAccessibilityManager_handleCheckStateChanged(
    110           env, obj.obj(), node->renderer_id());
    111       break;
    112     case AccessibilityNotificationScrolledToAnchor:
    113       Java_BrowserAccessibilityManager_handleScrolledToAnchor(
    114           env, obj.obj(), node->renderer_id());
    115       break;
    116     case AccessibilityNotificationAlert:
    117       // An alert is a special case of live region. Fall through to the
    118       // next case to handle it.
    119     case AccessibilityNotificationObjectShow: {
    120       // This event is fired when an object appears in a live region.
    121       // Speak its text.
    122       BrowserAccessibilityAndroid* android_node =
    123           static_cast<BrowserAccessibilityAndroid*>(node);
    124       Java_BrowserAccessibilityManager_announceLiveRegionText(
    125           env, obj.obj(),
    126           base::android::ConvertUTF16ToJavaString(
    127               env, android_node->GetText()).obj());
    128       break;
    129     }
    130     case AccessibilityNotificationSelectedTextChanged:
    131       Java_BrowserAccessibilityManager_handleTextSelectionChanged(
    132           env, obj.obj(), node->renderer_id());
    133       break;
    134     case AccessibilityNotificationChildrenChanged:
    135     case AccessibilityNotificationTextChanged:
    136     case AccessibilityNotificationValueChanged:
    137       if (node->IsEditableText()) {
    138         Java_BrowserAccessibilityManager_handleEditableTextChanged(
    139             env, obj.obj(), node->renderer_id());
    140       }
    141       break;
    142     default:
    143       // There are some notifications that aren't meaningful on Android.
    144       // It's okay to skip them.
    145       break;
    146   }
    147 }
    148 
    149 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
    150   return static_cast<jint>(root_->renderer_id());
    151 }
    152 
    153 jint BrowserAccessibilityManagerAndroid::HitTest(
    154     JNIEnv* env, jobject obj, jint x, jint y) {
    155   BrowserAccessibilityAndroid* result =
    156       static_cast<BrowserAccessibilityAndroid*>(
    157           root_->BrowserAccessibilityForPoint(gfx::Point(x, y)));
    158 
    159   if (!result)
    160     return root_->renderer_id();
    161 
    162   if (result->IsFocusable())
    163     return result->renderer_id();
    164 
    165   // Examine the children of |result| to find the nearest accessibility focus
    166   // candidate
    167   BrowserAccessibility* nearest_node = FuzzyHitTest(x, y, result);
    168   if (nearest_node)
    169     return nearest_node->renderer_id();
    170 
    171   return root_->renderer_id();
    172 }
    173 
    174 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
    175     JNIEnv* env, jobject obj, jobject info, jint id) {
    176   BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
    177       GetFromRendererID(id));
    178   if (!node)
    179     return false;
    180 
    181   if (node->parent()) {
    182     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
    183         env, obj, info, node->parent()->renderer_id());
    184   }
    185   if (!node->IsLeaf()) {
    186     for (unsigned i = 0; i < node->child_count(); ++i) {
    187       Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
    188           env, obj, info, node->children()[i]->renderer_id());
    189     }
    190   }
    191   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
    192       env, obj, info,
    193       id,
    194       node->IsCheckable(),
    195       node->IsChecked(),
    196       node->IsClickable(),
    197       node->IsEnabled(),
    198       node->IsFocusable(),
    199       node->IsFocused(),
    200       node->IsPassword(),
    201       node->IsScrollable(),
    202       node->IsSelected(),
    203       node->IsVisibleToUser());
    204   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoStringAttributes(
    205       env, obj, info,
    206       base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj(),
    207       base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    208 
    209   gfx::Rect absolute_rect = node->GetLocalBoundsRect();
    210   gfx::Rect parent_relative_rect = absolute_rect;
    211   if (node->parent()) {
    212     gfx::Rect parent_rect = node->parent()->GetLocalBoundsRect();
    213     parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin());
    214   }
    215   bool is_root = node->parent() == NULL;
    216   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
    217       env, obj, info,
    218       absolute_rect.x(), absolute_rect.y(),
    219       parent_relative_rect.x(), parent_relative_rect.y(),
    220       absolute_rect.width(), absolute_rect.height(),
    221       is_root);
    222 
    223   return true;
    224 }
    225 
    226 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
    227     JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) {
    228   BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
    229       GetFromRendererID(id));
    230   if (!node)
    231     return false;
    232 
    233   Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
    234       env, obj, event,
    235       node->IsChecked(),
    236       node->IsEnabled(),
    237       node->IsPassword(),
    238       node->IsScrollable());
    239   Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
    240       env, obj, event,
    241       base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
    242   Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
    243       env, obj, event,
    244       node->GetItemIndex(),
    245       node->GetItemCount());
    246   Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
    247       env, obj, event,
    248       node->GetScrollX(),
    249       node->GetScrollY(),
    250       node->GetMaxScrollX(),
    251       node->GetMaxScrollY());
    252 
    253   switch (event_type) {
    254     case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED:
    255       Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
    256           env, obj, event,
    257           node->GetTextChangeFromIndex(),
    258           node->GetTextChangeAddedCount(),
    259           node->GetTextChangeRemovedCount(),
    260           base::android::ConvertUTF16ToJavaString(
    261               env, node->GetTextChangeBeforeText()).obj(),
    262           base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    263       break;
    264     case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED:
    265       Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
    266           env, obj, event,
    267           node->GetSelectionStart(),
    268           node->GetSelectionEnd(),
    269           node->GetEditableTextLength(),
    270           base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    271       break;
    272     default:
    273       break;
    274   }
    275 
    276   return true;
    277 }
    278 
    279 void BrowserAccessibilityManagerAndroid::Click(
    280     JNIEnv* env, jobject obj, jint id) {
    281   BrowserAccessibility* node = GetFromRendererID(id);
    282   if (node)
    283     DoDefaultAction(*node);
    284 }
    285 
    286 void BrowserAccessibilityManagerAndroid::Focus(
    287     JNIEnv* env, jobject obj, jint id) {
    288   BrowserAccessibility* node = GetFromRendererID(id);
    289   if (node)
    290     SetFocus(node, true);
    291 }
    292 
    293 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
    294   SetFocus(root_, true);
    295 }
    296 
    297 BrowserAccessibility* BrowserAccessibilityManagerAndroid::FuzzyHitTest(
    298     int x, int y, BrowserAccessibility* start_node) {
    299   BrowserAccessibility* nearest_node = NULL;
    300   int min_distance = INT_MAX;
    301   FuzzyHitTestImpl(x, y, start_node, &nearest_node, &min_distance);
    302   return nearest_node;
    303 }
    304 
    305 // static
    306 void BrowserAccessibilityManagerAndroid::FuzzyHitTestImpl(
    307     int x, int y, BrowserAccessibility* start_node,
    308     BrowserAccessibility** nearest_candidate, int* nearest_distance) {
    309   BrowserAccessibilityAndroid* node =
    310       static_cast<BrowserAccessibilityAndroid*>(start_node);
    311   int distance = CalculateDistanceSquared(x, y, node);
    312 
    313   if (node->IsFocusable()) {
    314     if (distance < *nearest_distance) {
    315       *nearest_candidate = node;
    316       *nearest_distance = distance;
    317     }
    318     // Don't examine any more children of focusable node
    319     // TODO(aboxhall): what about focusable children?
    320     return;
    321   }
    322 
    323   if (!node->GetText().empty()) {
    324     if (distance < *nearest_distance) {
    325       *nearest_candidate = node;
    326       *nearest_distance = distance;
    327     }
    328     return;
    329   }
    330 
    331   if (!node->IsLeaf()) {
    332     for (uint32 i = 0; i < node->child_count(); i++) {
    333       BrowserAccessibility* child = node->GetChild(i);
    334       FuzzyHitTestImpl(x, y, child, nearest_candidate, nearest_distance);
    335     }
    336   }
    337 }
    338 
    339 // static
    340 int BrowserAccessibilityManagerAndroid::CalculateDistanceSquared(
    341     int x, int y, BrowserAccessibility* node) {
    342   gfx::Rect node_bounds = node->GetLocalBoundsRect();
    343   int nearest_x = Clamp(x, node_bounds.x(), node_bounds.right());
    344   int nearest_y = Clamp(y, node_bounds.y(), node_bounds.bottom());
    345   int dx = std::abs(x - nearest_x);
    346   int dy = std::abs(y - nearest_y);
    347   return dx * dx + dy * dy;
    348 }
    349 
    350 void BrowserAccessibilityManagerAndroid::NotifyRootChanged() {
    351   JNIEnv* env = AttachCurrentThread();
    352   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
    353   if (obj.is_null())
    354     return;
    355 
    356   Java_BrowserAccessibilityManager_handleNavigate(env, obj.obj());
    357 }
    358 
    359 bool
    360 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
    361   // The Java layer handles the root scroll offset.
    362   return false;
    363 }
    364 
    365 bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
    366   return RegisterNativesImpl(env);
    367 }
    368 
    369 }  // namespace content
    370