• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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