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].
Clamp(int val,int min,int 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
Create(const AccessibilityNodeData & src,BrowserAccessibilityDelegate * delegate,BrowserAccessibilityFactory * factory)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*
ToBrowserAccessibilityManagerAndroid()53 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
54 return static_cast<BrowserAccessibilityManagerAndroid*>(this);
55 }
56
BrowserAccessibilityManagerAndroid(ScopedJavaLocalRef<jobject> content_view_core,const AccessibilityNodeData & src,BrowserAccessibilityDelegate * delegate,BrowserAccessibilityFactory * factory)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
~BrowserAccessibilityManagerAndroid()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
GetEmptyDocument()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
SetContentViewCore(ScopedJavaLocalRef<jobject> content_view_core)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
NotifyAccessibilityEvent(blink::WebAXEvent event_type,BrowserAccessibility * node)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
GetRootId(JNIEnv * env,jobject obj)163 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
164 return static_cast<jint>(root_->renderer_id());
165 }
166
IsNodeValid(JNIEnv * env,jobject obj,jint id)167 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
168 JNIEnv* env, jobject obj, jint id) {
169 return GetFromRendererID(id) != NULL;
170 }
171
HitTest(JNIEnv * env,jobject obj,jint x,jint y)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
PopulateAccessibilityNodeInfo(JNIEnv * env,jobject obj,jobject info,jint id)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
PopulateAccessibilityEvent(JNIEnv * env,jobject obj,jobject event,jint id,jint event_type)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
Click(JNIEnv * env,jobject obj,jint id)364 void BrowserAccessibilityManagerAndroid::Click(
365 JNIEnv* env, jobject obj, jint id) {
366 BrowserAccessibility* node = GetFromRendererID(id);
367 if (node)
368 DoDefaultAction(*node);
369 }
370
Focus(JNIEnv * env,jobject obj,jint id)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
Blur(JNIEnv * env,jobject obj)378 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
379 SetFocus(root_, true);
380 }
381
FuzzyHitTest(int x,int y,BrowserAccessibility * start_node)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
FuzzyHitTestImpl(int x,int y,BrowserAccessibility * start_node,BrowserAccessibility ** nearest_candidate,int * nearest_distance)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
CalculateDistanceSquared(int x,int y,BrowserAccessibility * node)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
NotifyRootChanged()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
UseRootScrollOffsetsWhenComputingBounds()443 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
444 // The Java layer handles the root scroll offset.
445 return false;
446 }
447
RegisterBrowserAccessibilityManager(JNIEnv * env)448 bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
449 return RegisterNativesImpl(env);
450 }
451
452 } // namespace content
453