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
18using base::android::AttachCurrentThread;
19using base::android::ScopedJavaLocalRef;
20
21namespace {
22
23// These are enums from android.view.accessibility.AccessibilityEvent in Java:
24enum {
25  ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED = 16,
26  ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192
27};
28
29enum 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.
38AndroidHtmlElementType 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
51namespace content {
52
53namespace aria_strings {
54  const char kAriaLivePolite[] = "polite";
55  const char kAriaLiveAssertive[] = "assertive";
56}
57
58// static
59BrowserAccessibilityManager* 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
67BrowserAccessibilityManagerAndroid*
68BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
69  return static_cast<BrowserAccessibilityManagerAndroid*>(this);
70}
71
72BrowserAccessibilityManagerAndroid::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
81BrowserAccessibilityManagerAndroid::~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
91ui::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
102void 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
114void 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_SELECTED_TEXT_CHANGED:
172      Java_BrowserAccessibilityManager_handleTextSelectionChanged(
173          env, obj.obj(), node->GetId());
174      break;
175    case ui::AX_EVENT_CHILDREN_CHANGED:
176    case ui::AX_EVENT_TEXT_CHANGED:
177    case ui::AX_EVENT_VALUE_CHANGED:
178      if (node->IsEditableText()) {
179        Java_BrowserAccessibilityManager_handleEditableTextChanged(
180            env, obj.obj(), node->GetId());
181      }
182      break;
183    default:
184      // There are some notifications that aren't meaningful on Android.
185      // It's okay to skip them.
186      break;
187  }
188}
189
190jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
191  return static_cast<jint>(GetRoot()->GetId());
192}
193
194jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
195    JNIEnv* env, jobject obj, jint id) {
196  return GetFromID(id) != NULL;
197}
198
199void BrowserAccessibilityManagerAndroid::HitTest(
200    JNIEnv* env, jobject obj, jint x, jint y) {
201  if (delegate())
202    delegate()->AccessibilityHitTest(gfx::Point(x, y));
203}
204
205jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
206    JNIEnv* env, jobject obj, jobject info, jint id) {
207  BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
208      GetFromID(id));
209  if (!node)
210    return false;
211
212  if (node->GetParent()) {
213    Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
214        env, obj, info, node->GetParent()->GetId());
215  }
216  for (unsigned i = 0; i < node->PlatformChildCount(); ++i) {
217    Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
218        env, obj, info, node->InternalGetChild(i)->GetId());
219  }
220  Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
221      env, obj, info,
222      id,
223      node->IsCheckable(),
224      node->IsChecked(),
225      node->IsClickable(),
226      node->IsEnabled(),
227      node->IsFocusable(),
228      node->IsFocused(),
229      node->IsPassword(),
230      node->IsScrollable(),
231      node->IsSelected(),
232      node->IsVisibleToUser());
233  Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName(
234      env, obj, info,
235      base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
236  Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription(
237      env, obj, info,
238      base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj(),
239      node->IsLink());
240
241  gfx::Rect absolute_rect = node->GetLocalBoundsRect();
242  gfx::Rect parent_relative_rect = absolute_rect;
243  if (node->GetParent()) {
244    gfx::Rect parent_rect = node->GetParent()->GetLocalBoundsRect();
245    parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin());
246  }
247  bool is_root = node->GetParent() == NULL;
248  Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
249      env, obj, info,
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
292jboolean 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
382void BrowserAccessibilityManagerAndroid::Click(
383    JNIEnv* env, jobject obj, jint id) {
384  BrowserAccessibility* node = GetFromID(id);
385  if (node)
386    DoDefaultAction(*node);
387}
388
389void BrowserAccessibilityManagerAndroid::Focus(
390    JNIEnv* env, jobject obj, jint id) {
391  BrowserAccessibility* node = GetFromID(id);
392  if (node)
393    SetFocus(node, true);
394}
395
396void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
397  SetFocus(GetRoot(), true);
398}
399
400void 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
407void 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
430jint 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
487void 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
496bool
497BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
498  // The Java layer handles the root scroll offset.
499  return false;
500}
501
502bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
503  return RegisterNativesImpl(env);
504}
505
506}  // namespace content
507