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
29// Restricts |val| to the range [min, max].
30int Clamp(int val, int min, int max) {
31  return std::min(std::max(val, min), max);
32}
33
34}  // anonymous namespace
35
36namespace content {
37
38namespace aria_strings {
39  const char kAriaLivePolite[] = "polite";
40  const char kAriaLiveAssertive[] = "assertive";
41}
42
43// static
44BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
45    const AccessibilityNodeData& src,
46    BrowserAccessibilityDelegate* delegate,
47    BrowserAccessibilityFactory* factory) {
48  return new BrowserAccessibilityManagerAndroid(ScopedJavaLocalRef<jobject>(),
49                                                src, delegate, factory);
50}
51
52BrowserAccessibilityManagerAndroid::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
67BrowserAccessibilityManagerAndroid::~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
77AccessibilityNodeData 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
85void 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
149jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
150  return static_cast<jint>(root_->renderer_id());
151}
152
153jint 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
174jboolean 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
226jboolean 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
279void BrowserAccessibilityManagerAndroid::Click(
280    JNIEnv* env, jobject obj, jint id) {
281  BrowserAccessibility* node = GetFromRendererID(id);
282  if (node)
283    DoDefaultAction(*node);
284}
285
286void BrowserAccessibilityManagerAndroid::Focus(
287    JNIEnv* env, jobject obj, jint id) {
288  BrowserAccessibility* node = GetFromRendererID(id);
289  if (node)
290    SetFocus(node, true);
291}
292
293void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
294  SetFocus(root_, true);
295}
296
297BrowserAccessibility* 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
306void 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
340int 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
350void 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
359bool
360BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
361  // The Java layer handles the root scroll offset.
362  return false;
363}
364
365bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
366  return RegisterNativesImpl(env);
367}
368
369}  // namespace content
370