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.
5 #include "content/browser/accessibility/browser_accessibility_manager_android.h"
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"
18 using base::android::AttachCurrentThread
;
19 using base::android::ScopedJavaLocalRef
;
23 // These are enums from android.view.accessibility.AccessibilityEvent in Java:
25 ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED
= 16,
26 ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED
= 8192
29 enum AndroidHtmlElementType
{
30 HTML_ELEMENT_TYPE_SECTION
,
31 HTML_ELEMENT_TYPE_LIST
,
32 HTML_ELEMENT_TYPE_CONTROL
,
36 // Restricts |val| to the range [min, max].
37 int Clamp(int val
, int min
, int max
) {
38 return std::min(std::max(val
, min
), max
);
41 // These are special unofficial strings sent from TalkBack/BrailleBack
42 // to jump to certain categories of web elements.
43 AndroidHtmlElementType
HtmlElementTypeFromString(base::string16 element_type
) {
44 if (element_type
== base::ASCIIToUTF16("SECTION"))
45 return HTML_ELEMENT_TYPE_SECTION
;
46 else if (element_type
== base::ASCIIToUTF16("LIST"))
47 return HTML_ELEMENT_TYPE_LIST
;
48 else if (element_type
== base::ASCIIToUTF16("CONTROL"))
49 return HTML_ELEMENT_TYPE_CONTROL
;
51 return HTML_ELEMENT_TYPE_ANY
;
54 } // anonymous namespace
58 namespace aria_strings
{
59 const char kAriaLivePolite
[] = "polite";
60 const char kAriaLiveAssertive
[] = "assertive";
64 BrowserAccessibilityManager
* BrowserAccessibilityManager::Create(
65 const ui::AXTreeUpdate
& initial_tree
,
66 BrowserAccessibilityDelegate
* delegate
,
67 BrowserAccessibilityFactory
* factory
) {
68 return new BrowserAccessibilityManagerAndroid(
69 ScopedJavaLocalRef
<jobject
>(), initial_tree
, delegate
, factory
);
72 BrowserAccessibilityManagerAndroid
*
73 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
74 return static_cast<BrowserAccessibilityManagerAndroid
*>(this);
77 BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid(
78 ScopedJavaLocalRef
<jobject
> content_view_core
,
79 const ui::AXTreeUpdate
& initial_tree
,
80 BrowserAccessibilityDelegate
* delegate
,
81 BrowserAccessibilityFactory
* factory
)
82 : BrowserAccessibilityManager(initial_tree
, delegate
, factory
) {
83 SetContentViewCore(content_view_core
);
86 BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() {
87 JNIEnv
* env
= AttachCurrentThread();
88 ScopedJavaLocalRef
<jobject
> obj
= java_ref_
.get(env
);
92 Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env
, obj
.obj());
96 ui::AXTreeUpdate
BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
97 ui::AXNodeData empty_document
;
98 empty_document
.id
= 0;
99 empty_document
.role
= ui::AX_ROLE_ROOT_WEB_AREA
;
100 empty_document
.state
= 1 << ui::AX_STATE_READ_ONLY
;
102 ui::AXTreeUpdate update
;
103 update
.nodes
.push_back(empty_document
);
107 void BrowserAccessibilityManagerAndroid::SetContentViewCore(
108 ScopedJavaLocalRef
<jobject
> content_view_core
) {
109 if (content_view_core
.is_null())
112 JNIEnv
* env
= AttachCurrentThread();
113 java_ref_
= JavaObjectWeakGlobalRef(
114 env
, Java_BrowserAccessibilityManager_create(
115 env
, reinterpret_cast<intptr_t>(this),
116 content_view_core
.obj()).obj());
119 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
120 ui::AXEvent event_type
,
121 BrowserAccessibility
* node
) {
122 JNIEnv
* env
= AttachCurrentThread();
123 ScopedJavaLocalRef
<jobject
> obj
= java_ref_
.get(env
);
127 if (event_type
== ui::AX_EVENT_HIDE
)
130 // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
131 // the Android system that the accessibility hierarchy rooted at this
133 Java_BrowserAccessibilityManager_handleContentChanged(
134 env
, obj
.obj(), node
->GetId());
136 switch (event_type
) {
137 case ui::AX_EVENT_LOAD_COMPLETE
:
138 Java_BrowserAccessibilityManager_handlePageLoaded(
139 env
, obj
.obj(), focus_
->id());
141 case ui::AX_EVENT_FOCUS
:
142 Java_BrowserAccessibilityManager_handleFocusChanged(
143 env
, obj
.obj(), node
->GetId());
145 case ui::AX_EVENT_CHECKED_STATE_CHANGED
:
146 Java_BrowserAccessibilityManager_handleCheckStateChanged(
147 env
, obj
.obj(), node
->GetId());
149 case ui::AX_EVENT_SCROLL_POSITION_CHANGED
:
150 Java_BrowserAccessibilityManager_handleScrollPositionChanged(
151 env
, obj
.obj(), node
->GetId());
153 case ui::AX_EVENT_SCROLLED_TO_ANCHOR
:
154 Java_BrowserAccessibilityManager_handleScrolledToAnchor(
155 env
, obj
.obj(), node
->GetId());
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.
163 BrowserAccessibilityAndroid
* android_node
=
164 static_cast<BrowserAccessibilityAndroid
*>(node
);
165 Java_BrowserAccessibilityManager_announceLiveRegionText(
167 base::android::ConvertUTF16ToJavaString(
168 env
, android_node
->GetText()).obj());
171 case ui::AX_EVENT_SELECTED_TEXT_CHANGED
:
172 Java_BrowserAccessibilityManager_handleTextSelectionChanged(
173 env
, obj
.obj(), node
->GetId());
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());
184 // There are some notifications that aren't meaningful on Android.
185 // It's okay to skip them.
190 jint
BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv
* env
, jobject obj
) {
191 return static_cast<jint
>(GetRoot()->GetId());
194 jboolean
BrowserAccessibilityManagerAndroid::IsNodeValid(
195 JNIEnv
* env
, jobject obj
, jint id
) {
196 return GetFromID(id
) != NULL
;
199 jint
BrowserAccessibilityManagerAndroid::HitTest(
200 JNIEnv
* env
, jobject obj
, jint x
, jint y
) {
201 BrowserAccessibilityAndroid
* result
=
202 static_cast<BrowserAccessibilityAndroid
*>(
203 GetRoot()->BrowserAccessibilityForPoint(gfx::Point(x
, y
)));
206 return GetRoot()->GetId();
208 if (result
->IsFocusable())
209 return result
->GetId();
211 // Examine the children of |result| to find the nearest accessibility focus
213 BrowserAccessibility
* nearest_node
= FuzzyHitTest(x
, y
, result
);
215 return nearest_node
->GetId();
217 return GetRoot()->GetId();
220 jboolean
BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
221 JNIEnv
* env
, jobject obj
, jobject info
, jint id
) {
222 BrowserAccessibilityAndroid
* node
= static_cast<BrowserAccessibilityAndroid
*>(
227 if (node
->GetParent()) {
228 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
229 env
, obj
, info
, node
->GetParent()->GetId());
231 for (unsigned i
= 0; i
< node
->PlatformChildCount(); ++i
) {
232 Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
233 env
, obj
, info
, node
->InternalGetChild(i
)->GetId());
235 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
245 node
->IsScrollable(),
247 node
->IsVisibleToUser());
248 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName(
250 base::android::ConvertUTF8ToJavaString(env
, node
->GetClassName()).obj());
251 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription(
253 base::android::ConvertUTF16ToJavaString(env
, node
->GetText()).obj(),
256 gfx::Rect absolute_rect
= node
->GetLocalBoundsRect();
257 gfx::Rect parent_relative_rect
= absolute_rect
;
258 if (node
->GetParent()) {
259 gfx::Rect parent_rect
= node
->GetParent()->GetLocalBoundsRect();
260 parent_relative_rect
.Offset(-parent_rect
.OffsetFromOrigin());
262 bool is_root
= node
->GetParent() == NULL
;
263 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
265 absolute_rect
.x(), absolute_rect
.y(),
266 parent_relative_rect
.x(), parent_relative_rect
.y(),
267 absolute_rect
.width(), absolute_rect
.height(),
271 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes(
273 node
->CanOpenPopup(),
274 node
->IsContentInvalid(),
275 node
->IsDismissable(),
277 node
->AndroidInputType(),
278 node
->AndroidLiveRegionType());
279 if (node
->IsCollection()) {
280 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo(
284 node
->IsHierarchical());
286 if (node
->IsCollectionItem() || node
->IsHeading()) {
287 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo(
295 if (node
->IsRangeType()) {
296 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo(
298 node
->AndroidRangeType(),
301 node
->RangeCurrentValue());
307 jboolean
BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
308 JNIEnv
* env
, jobject obj
, jobject event
, jint id
, jint event_type
) {
309 BrowserAccessibilityAndroid
* node
= static_cast<BrowserAccessibilityAndroid
*>(
314 Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
319 node
->IsScrollable());
320 Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
322 base::android::ConvertUTF8ToJavaString(env
, node
->GetClassName()).obj());
323 Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
325 node
->GetItemIndex(),
326 node
->GetItemCount());
327 Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
331 node
->GetMaxScrollX(),
332 node
->GetMaxScrollY());
334 switch (event_type
) {
335 case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED
:
336 Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
338 node
->GetTextChangeFromIndex(),
339 node
->GetTextChangeAddedCount(),
340 node
->GetTextChangeRemovedCount(),
341 base::android::ConvertUTF16ToJavaString(
342 env
, node
->GetTextChangeBeforeText()).obj(),
343 base::android::ConvertUTF16ToJavaString(env
, node
->GetText()).obj());
345 case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED
:
346 Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
348 node
->GetSelectionStart(),
349 node
->GetSelectionEnd(),
350 node
->GetEditableTextLength(),
351 base::android::ConvertUTF16ToJavaString(env
, node
->GetText()).obj());
357 // Backwards-compatible fallback for new KitKat APIs.
358 Java_BrowserAccessibilityManager_setAccessibilityEventKitKatAttributes(
360 node
->CanOpenPopup(),
361 node
->IsContentInvalid(),
362 node
->IsDismissable(),
364 node
->AndroidInputType(),
365 node
->AndroidLiveRegionType());
366 if (node
->IsCollection()) {
367 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo(
371 node
->IsHierarchical());
373 if (node
->IsCollectionItem() || node
->IsHeading()) {
374 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo(
382 if (node
->IsRangeType()) {
383 Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo(
385 node
->AndroidRangeType(),
388 node
->RangeCurrentValue());
394 void BrowserAccessibilityManagerAndroid::Click(
395 JNIEnv
* env
, jobject obj
, jint id
) {
396 BrowserAccessibility
* node
= GetFromID(id
);
398 DoDefaultAction(*node
);
401 void BrowserAccessibilityManagerAndroid::Focus(
402 JNIEnv
* env
, jobject obj
, jint id
) {
403 BrowserAccessibility
* node
= GetFromID(id
);
405 SetFocus(node
, true);
408 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv
* env
, jobject obj
) {
409 SetFocus(GetRoot(), true);
412 void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible(
413 JNIEnv
* env
, jobject obj
, jint id
) {
414 BrowserAccessibility
* node
= GetFromID(id
);
416 ScrollToMakeVisible(*node
, gfx::Rect(node
->GetLocation().size()));
419 BrowserAccessibility
* BrowserAccessibilityManagerAndroid::FuzzyHitTest(
420 int x
, int y
, BrowserAccessibility
* start_node
) {
421 BrowserAccessibility
* nearest_node
= NULL
;
422 int min_distance
= INT_MAX
;
423 FuzzyHitTestImpl(x
, y
, start_node
, &nearest_node
, &min_distance
);
428 void BrowserAccessibilityManagerAndroid::FuzzyHitTestImpl(
429 int x
, int y
, BrowserAccessibility
* start_node
,
430 BrowserAccessibility
** nearest_candidate
, int* nearest_distance
) {
431 BrowserAccessibilityAndroid
* node
=
432 static_cast<BrowserAccessibilityAndroid
*>(start_node
);
433 int distance
= CalculateDistanceSquared(x
, y
, node
);
435 if (node
->IsFocusable()) {
436 if (distance
< *nearest_distance
) {
437 *nearest_candidate
= node
;
438 *nearest_distance
= distance
;
440 // Don't examine any more children of focusable node
441 // TODO(aboxhall): what about focusable children?
445 if (!node
->GetText().empty()) {
446 if (distance
< *nearest_distance
) {
447 *nearest_candidate
= node
;
448 *nearest_distance
= distance
;
453 for (uint32 i
= 0; i
< node
->PlatformChildCount(); i
++) {
454 BrowserAccessibility
* child
= node
->PlatformGetChild(i
);
455 FuzzyHitTestImpl(x
, y
, child
, nearest_candidate
, nearest_distance
);
460 int BrowserAccessibilityManagerAndroid::CalculateDistanceSquared(
461 int x
, int y
, BrowserAccessibility
* node
) {
462 gfx::Rect node_bounds
= node
->GetLocalBoundsRect();
463 int nearest_x
= Clamp(x
, node_bounds
.x(), node_bounds
.right());
464 int nearest_y
= Clamp(y
, node_bounds
.y(), node_bounds
.bottom());
465 int dx
= std::abs(x
- nearest_x
);
466 int dy
= std::abs(y
- nearest_y
);
467 return dx
* dx
+ dy
* dy
;
470 jint
BrowserAccessibilityManagerAndroid::FindElementType(
471 JNIEnv
* env
, jobject obj
, jint start_id
, jstring element_type_str
,
473 BrowserAccessibility
* node
= GetFromID(start_id
);
477 AndroidHtmlElementType element_type
= HtmlElementTypeFromString(
478 base::android::ConvertJavaStringToUTF16(env
, element_type_str
));
480 node
= forwards
? NextInTreeOrder(node
) : PreviousInTreeOrder(node
);
482 switch(element_type
) {
483 case HTML_ELEMENT_TYPE_SECTION
:
484 if (node
->GetRole() == ui::AX_ROLE_ARTICLE
||
485 node
->GetRole() == ui::AX_ROLE_APPLICATION
||
486 node
->GetRole() == ui::AX_ROLE_BANNER
||
487 node
->GetRole() == ui::AX_ROLE_COMPLEMENTARY
||
488 node
->GetRole() == ui::AX_ROLE_CONTENT_INFO
||
489 node
->GetRole() == ui::AX_ROLE_HEADING
||
490 node
->GetRole() == ui::AX_ROLE_MAIN
||
491 node
->GetRole() == ui::AX_ROLE_NAVIGATION
||
492 node
->GetRole() == ui::AX_ROLE_SEARCH
||
493 node
->GetRole() == ui::AX_ROLE_REGION
) {
494 return node
->GetId();
497 case HTML_ELEMENT_TYPE_LIST
:
498 if (node
->GetRole() == ui::AX_ROLE_LIST
||
499 node
->GetRole() == ui::AX_ROLE_GRID
||
500 node
->GetRole() == ui::AX_ROLE_TABLE
||
501 node
->GetRole() == ui::AX_ROLE_TREE
) {
502 return node
->GetId();
505 case HTML_ELEMENT_TYPE_CONTROL
:
506 if (static_cast<BrowserAccessibilityAndroid
*>(node
)->IsFocusable())
507 return node
->GetId();
509 case HTML_ELEMENT_TYPE_ANY
:
510 // In theory, the API says that an accessibility service could
511 // jump to an element by element name, like 'H1' or 'P'. This isn't
512 // currently used by any accessibility service, and we think it's
513 // better to keep them high-level like 'SECTION' or 'CONTROL', so we
514 // just fall back on linear navigation when we don't recognize the
516 if (static_cast<BrowserAccessibilityAndroid
*>(node
)->IsClickable())
517 return node
->GetId();
521 node
= forwards
? NextInTreeOrder(node
) : PreviousInTreeOrder(node
);
527 void BrowserAccessibilityManagerAndroid::OnRootChanged(ui::AXNode
* new_root
) {
528 JNIEnv
* env
= AttachCurrentThread();
529 ScopedJavaLocalRef
<jobject
> obj
= java_ref_
.get(env
);
533 Java_BrowserAccessibilityManager_handleNavigate(env
, obj
.obj());
537 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
538 // The Java layer handles the root scroll offset.
542 bool RegisterBrowserAccessibilityManager(JNIEnv
* env
) {
543 return RegisterNativesImpl(env
);
546 } // namespace content