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.
HtmlElementTypeFromString(base::string16 element_type)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
Create(const ui::AXTreeUpdate & initial_tree,BrowserAccessibilityDelegate * delegate,BrowserAccessibilityFactory * factory)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*
ToBrowserAccessibilityManagerAndroid()68 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
69 return static_cast<BrowserAccessibilityManagerAndroid*>(this);
70 }
71
BrowserAccessibilityManagerAndroid(ScopedJavaLocalRef<jobject> content_view_core,const ui::AXTreeUpdate & initial_tree,BrowserAccessibilityDelegate * delegate,BrowserAccessibilityFactory * factory)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
~BrowserAccessibilityManagerAndroid()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
GetEmptyDocument()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
SetContentViewCore(ScopedJavaLocalRef<jobject> content_view_core)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
NotifyAccessibilityEvent(ui::AXEvent event_type,BrowserAccessibility * node)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
GetRootId(JNIEnv * env,jobject obj)189 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
190 return static_cast<jint>(GetRoot()->GetId());
191 }
192
IsNodeValid(JNIEnv * env,jobject obj,jint id)193 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
194 JNIEnv* env, jobject obj, jint id) {
195 return GetFromID(id) != NULL;
196 }
197
HitTest(JNIEnv * env,jobject obj,jint x,jint y)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
PopulateAccessibilityNodeInfo(JNIEnv * env,jobject obj,jobject info,jint id)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
PopulateAccessibilityEvent(JNIEnv * env,jobject obj,jobject event,jint id,jint event_type)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
Click(JNIEnv * env,jobject obj,jint id)382 void BrowserAccessibilityManagerAndroid::Click(
383 JNIEnv* env, jobject obj, jint id) {
384 BrowserAccessibility* node = GetFromID(id);
385 if (node)
386 DoDefaultAction(*node);
387 }
388
Focus(JNIEnv * env,jobject obj,jint id)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
Blur(JNIEnv * env,jobject obj)396 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
397 SetFocus(GetRoot(), true);
398 }
399
ScrollToMakeNodeVisible(JNIEnv * env,jobject obj,jint id)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
HandleHoverEvent(BrowserAccessibility * node)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
FindElementType(JNIEnv * env,jobject obj,jint start_id,jstring element_type_str,jboolean forwards)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
OnRootChanged(ui::AXNode * new_root)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
UseRootScrollOffsetsWhenComputingBounds()497 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
498 // The Java layer handles the root scroll offset.
499 return false;
500 }
501
RegisterBrowserAccessibilityManager(JNIEnv * env)502 bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
503 return RegisterNativesImpl(env);
504 }
505
506 } // namespace content
507