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::AXNodeData
& src
,
66 BrowserAccessibilityDelegate
* delegate
,
67 BrowserAccessibilityFactory
* factory
) {
68 return new BrowserAccessibilityManagerAndroid(ScopedJavaLocalRef
<jobject
>(),
69 src
, 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::AXNodeData
& src
,
80 BrowserAccessibilityDelegate
* delegate
,
81 BrowserAccessibilityFactory
* factory
)
82 : BrowserAccessibilityManager(src
, 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::AXNodeData
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
;
101 return empty_document
;
104 void BrowserAccessibilityManagerAndroid::SetContentViewCore(
105 ScopedJavaLocalRef
<jobject
> content_view_core
) {
106 if (content_view_core
.is_null())
109 JNIEnv
* env
= AttachCurrentThread();
110 java_ref_
= JavaObjectWeakGlobalRef(
111 env
, Java_BrowserAccessibilityManager_create(
112 env
, reinterpret_cast<intptr_t>(this),
113 content_view_core
.obj()).obj());
116 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
117 ui::AXEvent event_type
,
118 BrowserAccessibility
* node
) {
119 JNIEnv
* env
= AttachCurrentThread();
120 ScopedJavaLocalRef
<jobject
> obj
= java_ref_
.get(env
);
124 if (event_type
== ui::AX_EVENT_HIDE
)
127 // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
128 // the Android system that the accessibility hierarchy rooted at this
130 Java_BrowserAccessibilityManager_handleContentChanged(
131 env
, obj
.obj(), node
->renderer_id());
133 switch (event_type
) {
134 case ui::AX_EVENT_LOAD_COMPLETE
:
135 Java_BrowserAccessibilityManager_handlePageLoaded(
136 env
, obj
.obj(), focus_
->renderer_id());
138 case ui::AX_EVENT_FOCUS
:
139 Java_BrowserAccessibilityManager_handleFocusChanged(
140 env
, obj
.obj(), node
->renderer_id());
142 case ui::AX_EVENT_CHECKED_STATE_CHANGED
:
143 Java_BrowserAccessibilityManager_handleCheckStateChanged(
144 env
, obj
.obj(), node
->renderer_id());
146 case ui::AX_EVENT_SCROLL_POSITION_CHANGED
:
147 Java_BrowserAccessibilityManager_handleScrollPositionChanged(
148 env
, obj
.obj(), node
->renderer_id());
150 case ui::AX_EVENT_SCROLLED_TO_ANCHOR
:
151 Java_BrowserAccessibilityManager_handleScrolledToAnchor(
152 env
, obj
.obj(), node
->renderer_id());
154 case ui::AX_EVENT_ALERT
:
155 // An alert is a special case of live region. Fall through to the
156 // next case to handle it.
157 case ui::AX_EVENT_SHOW
: {
158 // This event is fired when an object appears in a live region.
160 BrowserAccessibilityAndroid
* android_node
=
161 static_cast<BrowserAccessibilityAndroid
*>(node
);
162 Java_BrowserAccessibilityManager_announceLiveRegionText(
164 base::android::ConvertUTF16ToJavaString(
165 env
, android_node
->GetText()).obj());
168 case ui::AX_EVENT_SELECTED_TEXT_CHANGED
:
169 Java_BrowserAccessibilityManager_handleTextSelectionChanged(
170 env
, obj
.obj(), node
->renderer_id());
172 case ui::AX_EVENT_CHILDREN_CHANGED
:
173 case ui::AX_EVENT_TEXT_CHANGED
:
174 case ui::AX_EVENT_VALUE_CHANGED
:
175 if (node
->IsEditableText()) {
176 Java_BrowserAccessibilityManager_handleEditableTextChanged(
177 env
, obj
.obj(), node
->renderer_id());
181 // There are some notifications that aren't meaningful on Android.
182 // It's okay to skip them.
187 jint
BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv
* env
, jobject obj
) {
188 return static_cast<jint
>(root_
->renderer_id());
191 jboolean
BrowserAccessibilityManagerAndroid::IsNodeValid(
192 JNIEnv
* env
, jobject obj
, jint id
) {
193 return GetFromRendererID(id
) != NULL
;
196 jint
BrowserAccessibilityManagerAndroid::HitTest(
197 JNIEnv
* env
, jobject obj
, jint x
, jint y
) {
198 BrowserAccessibilityAndroid
* result
=
199 static_cast<BrowserAccessibilityAndroid
*>(
200 root_
->BrowserAccessibilityForPoint(gfx::Point(x
, y
)));
203 return root_
->renderer_id();
205 if (result
->IsFocusable())
206 return result
->renderer_id();
208 // Examine the children of |result| to find the nearest accessibility focus
210 BrowserAccessibility
* nearest_node
= FuzzyHitTest(x
, y
, result
);
212 return nearest_node
->renderer_id();
214 return root_
->renderer_id();
217 jboolean
BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
218 JNIEnv
* env
, jobject obj
, jobject info
, jint id
) {
219 BrowserAccessibilityAndroid
* node
= static_cast<BrowserAccessibilityAndroid
*>(
220 GetFromRendererID(id
));
224 if (node
->parent()) {
225 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
226 env
, obj
, info
, node
->parent()->renderer_id());
228 for (unsigned i
= 0; i
< node
->PlatformChildCount(); ++i
) {
229 Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
230 env
, obj
, info
, node
->children()[i
]->renderer_id());
232 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
242 node
->IsScrollable(),
244 node
->IsVisibleToUser());
245 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName(
247 base::android::ConvertUTF8ToJavaString(env
, node
->GetClassName()).obj());
248 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription(
250 base::android::ConvertUTF16ToJavaString(env
, node
->GetText()).obj(),
253 gfx::Rect absolute_rect
= node
->GetLocalBoundsRect();
254 gfx::Rect parent_relative_rect
= absolute_rect
;
255 if (node
->parent()) {
256 gfx::Rect parent_rect
= node
->parent()->GetLocalBoundsRect();
257 parent_relative_rect
.Offset(-parent_rect
.OffsetFromOrigin());
259 bool is_root
= node
->parent() == NULL
;
260 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
262 absolute_rect
.x(), absolute_rect
.y(),
263 parent_relative_rect
.x(), parent_relative_rect
.y(),
264 absolute_rect
.width(), absolute_rect
.height(),
268 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes(
270 node
->CanOpenPopup(),
271 node
->IsContentInvalid(),
272 node
->IsDismissable(),
274 node
->AndroidInputType(),
275 node
->AndroidLiveRegionType());
276 if (node
->IsCollection()) {
277 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo(
281 node
->IsHierarchical());
283 if (node
->IsCollectionItem() || node
->IsHeading()) {
284 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo(
292 if (node
->IsRangeType()) {
293 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo(
295 node
->AndroidRangeType(),
298 node
->RangeCurrentValue());
304 jboolean
BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
305 JNIEnv
* env
, jobject obj
, jobject event
, jint id
, jint event_type
) {
306 BrowserAccessibilityAndroid
* node
= static_cast<BrowserAccessibilityAndroid
*>(
307 GetFromRendererID(id
));
311 Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
316 node
->IsScrollable());
317 Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
319 base::android::ConvertUTF8ToJavaString(env
, node
->GetClassName()).obj());
320 Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
322 node
->GetItemIndex(),
323 node
->GetItemCount());
324 Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
328 node
->GetMaxScrollX(),
329 node
->GetMaxScrollY());
331 switch (event_type
) {
332 case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_CHANGED
:
333 Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
335 node
->GetTextChangeFromIndex(),
336 node
->GetTextChangeAddedCount(),
337 node
->GetTextChangeRemovedCount(),
338 base::android::ConvertUTF16ToJavaString(
339 env
, node
->GetTextChangeBeforeText()).obj(),
340 base::android::ConvertUTF16ToJavaString(env
, node
->GetText()).obj());
342 case ANDROID_ACCESSIBILITY_EVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED
:
343 Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
345 node
->GetSelectionStart(),
346 node
->GetSelectionEnd(),
347 node
->GetEditableTextLength(),
348 base::android::ConvertUTF16ToJavaString(env
, node
->GetText()).obj());
354 // Backwards-compatible fallback for new KitKat APIs.
355 Java_BrowserAccessibilityManager_setAccessibilityEventKitKatAttributes(
357 node
->CanOpenPopup(),
358 node
->IsContentInvalid(),
359 node
->IsDismissable(),
361 node
->AndroidInputType(),
362 node
->AndroidLiveRegionType());
363 if (node
->IsCollection()) {
364 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo(
368 node
->IsHierarchical());
370 if (node
->IsCollectionItem() || node
->IsHeading()) {
371 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo(
379 if (node
->IsRangeType()) {
380 Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo(
382 node
->AndroidRangeType(),
385 node
->RangeCurrentValue());
391 void BrowserAccessibilityManagerAndroid::Click(
392 JNIEnv
* env
, jobject obj
, jint id
) {
393 BrowserAccessibility
* node
= GetFromRendererID(id
);
395 DoDefaultAction(*node
);
398 void BrowserAccessibilityManagerAndroid::Focus(
399 JNIEnv
* env
, jobject obj
, jint id
) {
400 BrowserAccessibility
* node
= GetFromRendererID(id
);
402 SetFocus(node
, true);
405 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv
* env
, jobject obj
) {
406 SetFocus(root_
, true);
409 void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible(
410 JNIEnv
* env
, jobject obj
, jint id
) {
411 BrowserAccessibility
* node
= GetFromRendererID(id
);
413 ScrollToMakeVisible(*node
, gfx::Rect(node
->location().size()));
416 BrowserAccessibility
* BrowserAccessibilityManagerAndroid::FuzzyHitTest(
417 int x
, int y
, BrowserAccessibility
* start_node
) {
418 BrowserAccessibility
* nearest_node
= NULL
;
419 int min_distance
= INT_MAX
;
420 FuzzyHitTestImpl(x
, y
, start_node
, &nearest_node
, &min_distance
);
425 void BrowserAccessibilityManagerAndroid::FuzzyHitTestImpl(
426 int x
, int y
, BrowserAccessibility
* start_node
,
427 BrowserAccessibility
** nearest_candidate
, int* nearest_distance
) {
428 BrowserAccessibilityAndroid
* node
=
429 static_cast<BrowserAccessibilityAndroid
*>(start_node
);
430 int distance
= CalculateDistanceSquared(x
, y
, node
);
432 if (node
->IsFocusable()) {
433 if (distance
< *nearest_distance
) {
434 *nearest_candidate
= node
;
435 *nearest_distance
= distance
;
437 // Don't examine any more children of focusable node
438 // TODO(aboxhall): what about focusable children?
442 if (!node
->GetText().empty()) {
443 if (distance
< *nearest_distance
) {
444 *nearest_candidate
= node
;
445 *nearest_distance
= distance
;
450 for (uint32 i
= 0; i
< node
->PlatformChildCount(); i
++) {
451 BrowserAccessibility
* child
= node
->PlatformGetChild(i
);
452 FuzzyHitTestImpl(x
, y
, child
, nearest_candidate
, nearest_distance
);
457 int BrowserAccessibilityManagerAndroid::CalculateDistanceSquared(
458 int x
, int y
, BrowserAccessibility
* node
) {
459 gfx::Rect node_bounds
= node
->GetLocalBoundsRect();
460 int nearest_x
= Clamp(x
, node_bounds
.x(), node_bounds
.right());
461 int nearest_y
= Clamp(y
, node_bounds
.y(), node_bounds
.bottom());
462 int dx
= std::abs(x
- nearest_x
);
463 int dy
= std::abs(y
- nearest_y
);
464 return dx
* dx
+ dy
* dy
;
467 jint
BrowserAccessibilityManagerAndroid::FindElementType(
468 JNIEnv
* env
, jobject obj
, jint start_id
, jstring element_type_str
,
470 BrowserAccessibility
* node
= GetFromRendererID(start_id
);
474 AndroidHtmlElementType element_type
= HtmlElementTypeFromString(
475 base::android::ConvertJavaStringToUTF16(env
, element_type_str
));
477 node
= forwards
? NextInTreeOrder(node
) : PreviousInTreeOrder(node
);
479 switch(element_type
) {
480 case HTML_ELEMENT_TYPE_SECTION
:
481 if (node
->role() == ui::AX_ROLE_ARTICLE
||
482 node
->role() == ui::AX_ROLE_APPLICATION
||
483 node
->role() == ui::AX_ROLE_BANNER
||
484 node
->role() == ui::AX_ROLE_COMPLEMENTARY
||
485 node
->role() == ui::AX_ROLE_CONTENT_INFO
||
486 node
->role() == ui::AX_ROLE_HEADING
||
487 node
->role() == ui::AX_ROLE_MAIN
||
488 node
->role() == ui::AX_ROLE_NAVIGATION
||
489 node
->role() == ui::AX_ROLE_SEARCH
||
490 node
->role() == ui::AX_ROLE_REGION
) {
491 return node
->renderer_id();
494 case HTML_ELEMENT_TYPE_LIST
:
495 if (node
->role() == ui::AX_ROLE_LIST
||
496 node
->role() == ui::AX_ROLE_GRID
||
497 node
->role() == ui::AX_ROLE_TABLE
||
498 node
->role() == ui::AX_ROLE_TREE
) {
499 return node
->renderer_id();
502 case HTML_ELEMENT_TYPE_CONTROL
:
503 if (static_cast<BrowserAccessibilityAndroid
*>(node
)->IsFocusable())
504 return node
->renderer_id();
506 case HTML_ELEMENT_TYPE_ANY
:
507 // In theory, the API says that an accessibility service could
508 // jump to an element by element name, like 'H1' or 'P'. This isn't
509 // currently used by any accessibility service, and we think it's
510 // better to keep them high-level like 'SECTION' or 'CONTROL', so we
511 // just fall back on linear navigation when we don't recognize the
513 if (static_cast<BrowserAccessibilityAndroid
*>(node
)->IsClickable())
514 return node
->renderer_id();
518 node
= forwards
? NextInTreeOrder(node
) : PreviousInTreeOrder(node
);
524 void BrowserAccessibilityManagerAndroid::NotifyRootChanged() {
525 JNIEnv
* env
= AttachCurrentThread();
526 ScopedJavaLocalRef
<jobject
> obj
= java_ref_
.get(env
);
530 Java_BrowserAccessibilityManager_handleNavigate(env
, obj
.obj());
534 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
535 // The Java layer handles the root scroll offset.
539 bool RegisterBrowserAccessibilityManager(JNIEnv
* env
) {
540 return RegisterNativesImpl(env
);
543 } // namespace content