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 enum AndroidHtmlElementType {
     30   HTML_ELEMENT_TYPE_SECTION,
     31   HTML_ELEMENT_TYPE_LIST,
     32   HTML_ELEMENT_TYPE_CONTROL,
     33   HTML_ELEMENT_TYPE_ANY
     34 };
     35 
     36 // These are special unofficial strings sent from TalkBack/BrailleBack
     37 // to jump to certain categories of web elements.
     38 AndroidHtmlElementType HtmlElementTypeFromString(base::string16 element_type) {
     39   if (element_type == base::ASCIIToUTF16("SECTION"))
     40     return HTML_ELEMENT_TYPE_SECTION;
     41   else if (element_type == base::ASCIIToUTF16("LIST"))
     42     return HTML_ELEMENT_TYPE_LIST;
     43   else if (element_type == base::ASCIIToUTF16("CONTROL"))
     44     return HTML_ELEMENT_TYPE_CONTROL;
     45   else
     46     return HTML_ELEMENT_TYPE_ANY;
     47 }
     48 
     49 }  // anonymous namespace
     50 
     51 namespace content {
     52 
     53 namespace aria_strings {
     54   const char kAriaLivePolite[] = "polite";
     55   const char kAriaLiveAssertive[] = "assertive";
     56 }
     57 
     58 // static
     59 BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
     60     const ui::AXTreeUpdate& initial_tree,
     61     BrowserAccessibilityDelegate* delegate,
     62     BrowserAccessibilityFactory* factory) {
     63   return new BrowserAccessibilityManagerAndroid(
     64       ScopedJavaLocalRef<jobject>(), initial_tree, delegate, factory);
     65 }
     66 
     67 BrowserAccessibilityManagerAndroid*
     68 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
     69   return static_cast<BrowserAccessibilityManagerAndroid*>(this);
     70 }
     71 
     72 BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid(
     73     ScopedJavaLocalRef<jobject> content_view_core,
     74     const ui::AXTreeUpdate& initial_tree,
     75     BrowserAccessibilityDelegate* delegate,
     76     BrowserAccessibilityFactory* factory)
     77     : BrowserAccessibilityManager(initial_tree, delegate, factory) {
     78   SetContentViewCore(content_view_core);
     79 }
     80 
     81 BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() {
     82   JNIEnv* env = AttachCurrentThread();
     83   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
     84   if (obj.is_null())
     85     return;
     86 
     87   Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj());
     88 }
     89 
     90 // static
     91 ui::AXTreeUpdate BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
     92   ui::AXNodeData empty_document;
     93   empty_document.id = 0;
     94   empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA;
     95   empty_document.state = 1 << ui::AX_STATE_READ_ONLY;
     96 
     97   ui::AXTreeUpdate update;
     98   update.nodes.push_back(empty_document);
     99   return update;
    100 }
    101 
    102 void BrowserAccessibilityManagerAndroid::SetContentViewCore(
    103     ScopedJavaLocalRef<jobject> content_view_core) {
    104   if (content_view_core.is_null())
    105     return;
    106 
    107   JNIEnv* env = AttachCurrentThread();
    108   java_ref_ = JavaObjectWeakGlobalRef(
    109       env, Java_BrowserAccessibilityManager_create(
    110           env, reinterpret_cast<intptr_t>(this),
    111           content_view_core.obj()).obj());
    112 }
    113 
    114 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
    115     ui::AXEvent event_type,
    116     BrowserAccessibility* node) {
    117   JNIEnv* env = AttachCurrentThread();
    118   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
    119   if (obj.is_null())
    120     return;
    121 
    122   if (event_type == ui::AX_EVENT_HIDE)
    123     return;
    124 
    125   if (event_type == ui::AX_EVENT_HOVER) {
    126     HandleHoverEvent(node);
    127     return;
    128   }
    129 
    130   // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
    131   // the Android system that the accessibility hierarchy rooted at this
    132   // node has changed.
    133   Java_BrowserAccessibilityManager_handleContentChanged(
    134       env, obj.obj(), node->GetId());
    135 
    136   switch (event_type) {
    137     case ui::AX_EVENT_LOAD_COMPLETE:
    138       Java_BrowserAccessibilityManager_handlePageLoaded(
    139           env, obj.obj(), focus_->id());
    140       break;
    141     case ui::AX_EVENT_FOCUS:
    142       Java_BrowserAccessibilityManager_handleFocusChanged(
    143           env, obj.obj(), node->GetId());
    144       break;
    145     case ui::AX_EVENT_CHECKED_STATE_CHANGED:
    146       Java_BrowserAccessibilityManager_handleCheckStateChanged(
    147           env, obj.obj(), node->GetId());
    148       break;
    149     case ui::AX_EVENT_SCROLL_POSITION_CHANGED:
    150       Java_BrowserAccessibilityManager_handleScrollPositionChanged(
    151           env, obj.obj(), node->GetId());
    152       break;
    153     case ui::AX_EVENT_SCROLLED_TO_ANCHOR:
    154       Java_BrowserAccessibilityManager_handleScrolledToAnchor(
    155           env, obj.obj(), node->GetId());
    156       break;
    157     case ui::AX_EVENT_ALERT:
    158       // An alert is a special case of live region. Fall through to the
    159       // next case to handle it.
    160     case ui::AX_EVENT_SHOW: {
    161       // This event is fired when an object appears in a live region.
    162       // Speak its text.
    163       BrowserAccessibilityAndroid* android_node =
    164           static_cast<BrowserAccessibilityAndroid*>(node);
    165       Java_BrowserAccessibilityManager_announceLiveRegionText(
    166           env, obj.obj(),
    167           base::android::ConvertUTF16ToJavaString(
    168               env, android_node->GetText()).obj());
    169       break;
    170     }
    171     case ui::AX_EVENT_TEXT_SELECTION_CHANGED:
    172       Java_BrowserAccessibilityManager_handleTextSelectionChanged(
    173           env, obj.obj(), node->GetId());
    174       break;
    175     case ui::AX_EVENT_TEXT_CHANGED:
    176     case ui::AX_EVENT_VALUE_CHANGED:
    177       if (node->IsEditableText() && GetFocus(GetRoot()) == node) {
    178         Java_BrowserAccessibilityManager_handleEditableTextChanged(
    179             env, obj.obj(), node->GetId());
    180       }
    181       break;
    182     default:
    183       // There are some notifications that aren't meaningful on Android.
    184       // It's okay to skip them.
    185       break;
    186   }
    187 }
    188 
    189 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
    190   return static_cast<jint>(GetRoot()->GetId());
    191 }
    192 
    193 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
    194     JNIEnv* env, jobject obj, jint id) {
    195   return GetFromID(id) != NULL;
    196 }
    197 
    198 void BrowserAccessibilityManagerAndroid::HitTest(
    199     JNIEnv* env, jobject obj, jint x, jint y) {
    200   if (delegate())
    201     delegate()->AccessibilityHitTest(gfx::Point(x, y));
    202 }
    203 
    204 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
    205     JNIEnv* env, jobject obj, jobject info, jint id) {
    206   BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
    207       GetFromID(id));
    208   if (!node)
    209     return false;
    210 
    211   if (node->GetParent()) {
    212     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
    213         env, obj, info, node->GetParent()->GetId());
    214   }
    215   for (unsigned i = 0; i < node->PlatformChildCount(); ++i) {
    216     Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
    217         env, obj, info, node->InternalGetChild(i)->GetId());
    218   }
    219   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
    220       env, obj, info,
    221       id,
    222       node->IsCheckable(),
    223       node->IsChecked(),
    224       node->IsClickable(),
    225       node->IsEnabled(),
    226       node->IsFocusable(),
    227       node->IsFocused(),
    228       node->IsPassword(),
    229       node->IsScrollable(),
    230       node->IsSelected(),
    231       node->IsVisibleToUser());
    232   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName(
    233       env, obj, info,
    234       base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
    235   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription(
    236       env, obj, info,
    237       base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj(),
    238       node->IsLink());
    239 
    240   gfx::Rect absolute_rect = node->GetLocalBoundsRect();
    241   gfx::Rect parent_relative_rect = absolute_rect;
    242   if (node->GetParent()) {
    243     gfx::Rect parent_rect = node->GetParent()->GetLocalBoundsRect();
    244     parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin());
    245   }
    246   bool is_root = node->GetParent() == NULL;
    247   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
    248       env, obj, info,
    249       id,
    250       absolute_rect.x(), absolute_rect.y(),
    251       parent_relative_rect.x(), parent_relative_rect.y(),
    252       absolute_rect.width(), absolute_rect.height(),
    253       is_root);
    254 
    255   // New KitKat APIs
    256   Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes(
    257       env, obj, info,
    258       node->CanOpenPopup(),
    259       node->IsContentInvalid(),
    260       node->IsDismissable(),
    261       node->IsMultiLine(),
    262       node->AndroidInputType(),
    263       node->AndroidLiveRegionType());
    264   if (node->IsCollection()) {
    265     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo(
    266         env, obj, info,
    267         node->RowCount(),
    268         node->ColumnCount(),
    269         node->IsHierarchical());
    270   }
    271   if (node->IsCollectionItem() || node->IsHeading()) {
    272     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo(
    273         env, obj, info,
    274         node->RowIndex(),
    275         node->RowSpan(),
    276         node->ColumnIndex(),
    277         node->ColumnSpan(),
    278         node->IsHeading());
    279   }
    280   if (node->IsRangeType()) {
    281     Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo(
    282         env, obj, info,
    283         node->AndroidRangeType(),
    284         node->RangeMin(),
    285         node->RangeMax(),
    286         node->RangeCurrentValue());
    287   }
    288 
    289   return true;
    290 }
    291 
    292 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
    293     JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) {
    294   BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
    295       GetFromID(id));
    296   if (!node)
    297     return false;
    298 
    299   Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
    300       env, obj, event,
    301       node->IsChecked(),
    302       node->IsEnabled(),
    303       node->IsPassword(),
    304       node->IsScrollable());
    305   Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
    306       env, obj, event,
    307       base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
    308   Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
    309       env, obj, event,
    310       node->GetItemIndex(),
    311       node->GetItemCount());
    312   Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
    313       env, obj, event,
    314       node->GetScrollX(),
    315       node->GetScrollY(),
    316       node->GetMaxScrollX(),
    317       node->GetMaxScrollY());
    318 
    319   switch (event_type) {
    320     case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED:
    321       Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
    322           env, obj, event,
    323           node->GetTextChangeFromIndex(),
    324           node->GetTextChangeAddedCount(),
    325           node->GetTextChangeRemovedCount(),
    326           base::android::ConvertUTF16ToJavaString(
    327               env, node->GetTextChangeBeforeText()).obj(),
    328           base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    329       break;
    330     case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED:
    331       Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
    332           env, obj, event,
    333           node->GetSelectionStart(),
    334           node->GetSelectionEnd(),
    335           node->GetEditableTextLength(),
    336           base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj());
    337       break;
    338     default:
    339       break;
    340   }
    341 
    342   // Backwards-compatible fallback for new KitKat APIs.
    343   Java_BrowserAccessibilityManager_setAccessibilityEventKitKatAttributes(
    344       env, obj, event,
    345       node->CanOpenPopup(),
    346       node->IsContentInvalid(),
    347       node->IsDismissable(),
    348       node->IsMultiLine(),
    349       node->AndroidInputType(),
    350       node->AndroidLiveRegionType());
    351   if (node->IsCollection()) {
    352     Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo(
    353         env, obj, event,
    354         node->RowCount(),
    355         node->ColumnCount(),
    356         node->IsHierarchical());
    357   }
    358   if (node->IsHeading()) {
    359     Java_BrowserAccessibilityManager_setAccessibilityEventHeadingFlag(
    360         env, obj, event, true);
    361   }
    362   if (node->IsCollectionItem()) {
    363     Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo(
    364         env, obj, event,
    365         node->RowIndex(),
    366         node->RowSpan(),
    367         node->ColumnIndex(),
    368         node->ColumnSpan());
    369   }
    370   if (node->IsRangeType()) {
    371     Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo(
    372         env, obj, event,
    373         node->AndroidRangeType(),
    374         node->RangeMin(),
    375         node->RangeMax(),
    376         node->RangeCurrentValue());
    377   }
    378 
    379   return true;
    380 }
    381 
    382 void BrowserAccessibilityManagerAndroid::Click(
    383     JNIEnv* env, jobject obj, jint id) {
    384   BrowserAccessibility* node = GetFromID(id);
    385   if (node)
    386     DoDefaultAction(*node);
    387 }
    388 
    389 void BrowserAccessibilityManagerAndroid::Focus(
    390     JNIEnv* env, jobject obj, jint id) {
    391   BrowserAccessibility* node = GetFromID(id);
    392   if (node)
    393     SetFocus(node, true);
    394 }
    395 
    396 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
    397   SetFocus(GetRoot(), true);
    398 }
    399 
    400 void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible(
    401     JNIEnv* env, jobject obj, jint id) {
    402   BrowserAccessibility* node = GetFromID(id);
    403   if (node)
    404     ScrollToMakeVisible(*node, gfx::Rect(node->GetLocation().size()));
    405 }
    406 
    407 void BrowserAccessibilityManagerAndroid::HandleHoverEvent(
    408     BrowserAccessibility* node) {
    409   JNIEnv* env = AttachCurrentThread();
    410   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
    411   if (obj.is_null())
    412     return;
    413 
    414   BrowserAccessibilityAndroid* ancestor =
    415       static_cast<BrowserAccessibilityAndroid*>(node->GetParent());
    416   while (ancestor) {
    417     if (ancestor->PlatformIsLeaf() ||
    418         (ancestor->IsFocusable() && !ancestor->HasFocusableChild())) {
    419       node = ancestor;
    420       // Don't break - we want the highest ancestor that's focusable or a
    421       // leaf node.
    422     }
    423     ancestor = static_cast<BrowserAccessibilityAndroid*>(ancestor->GetParent());
    424   }
    425 
    426   Java_BrowserAccessibilityManager_handleHover(
    427       env, obj.obj(), node->GetId());
    428 }
    429 
    430 jint BrowserAccessibilityManagerAndroid::FindElementType(
    431     JNIEnv* env, jobject obj, jint start_id, jstring element_type_str,
    432     jboolean forwards) {
    433   BrowserAccessibility* node = GetFromID(start_id);
    434   if (!node)
    435     return 0;
    436 
    437   AndroidHtmlElementType element_type = HtmlElementTypeFromString(
    438       base::android::ConvertJavaStringToUTF16(env, element_type_str));
    439 
    440   node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node);
    441   while (node) {
    442     switch(element_type) {
    443       case HTML_ELEMENT_TYPE_SECTION:
    444         if (node->GetRole() == ui::AX_ROLE_ARTICLE ||
    445             node->GetRole() == ui::AX_ROLE_APPLICATION ||
    446             node->GetRole() == ui::AX_ROLE_BANNER ||
    447             node->GetRole() == ui::AX_ROLE_COMPLEMENTARY ||
    448             node->GetRole() == ui::AX_ROLE_CONTENT_INFO ||
    449             node->GetRole() == ui::AX_ROLE_HEADING ||
    450             node->GetRole() == ui::AX_ROLE_MAIN ||
    451             node->GetRole() == ui::AX_ROLE_NAVIGATION ||
    452             node->GetRole() == ui::AX_ROLE_SEARCH ||
    453             node->GetRole() == ui::AX_ROLE_REGION) {
    454           return node->GetId();
    455         }
    456         break;
    457       case HTML_ELEMENT_TYPE_LIST:
    458         if (node->GetRole() == ui::AX_ROLE_LIST ||
    459             node->GetRole() == ui::AX_ROLE_GRID ||
    460             node->GetRole() == ui::AX_ROLE_TABLE ||
    461             node->GetRole() == ui::AX_ROLE_TREE) {
    462           return node->GetId();
    463         }
    464         break;
    465       case HTML_ELEMENT_TYPE_CONTROL:
    466         if (static_cast<BrowserAccessibilityAndroid*>(node)->IsFocusable())
    467           return node->GetId();
    468         break;
    469       case HTML_ELEMENT_TYPE_ANY:
    470         // In theory, the API says that an accessibility service could
    471         // jump to an element by element name, like 'H1' or 'P'. This isn't
    472         // currently used by any accessibility service, and we think it's
    473         // better to keep them high-level like 'SECTION' or 'CONTROL', so we
    474         // just fall back on linear navigation when we don't recognize the
    475         // element type.
    476         if (static_cast<BrowserAccessibilityAndroid*>(node)->IsClickable())
    477           return node->GetId();
    478         break;
    479     }
    480 
    481     node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node);
    482   }
    483 
    484   return 0;
    485 }
    486 
    487 void BrowserAccessibilityManagerAndroid::OnRootChanged(ui::AXNode* new_root) {
    488   JNIEnv* env = AttachCurrentThread();
    489   ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
    490   if (obj.is_null())
    491     return;
    492 
    493   Java_BrowserAccessibilityManager_handleNavigate(env, obj.obj());
    494 }
    495 
    496 bool
    497 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
    498   // The Java layer handles the root scroll offset.
    499   return false;
    500 }
    501 
    502 bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
    503   return RegisterNativesImpl(env);
    504 }
    505 
    506 }  // namespace content
    507