Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / browser / accessibility / browser_accessibility_manager_android.cc
blob0772b1e71ff79260e77ec4b018ec029630af59a8
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"
7 #include <cmath>
9 #include "base/android/jni_android.h"
10 #include "base/android/jni_string.h"
11 #include "base/i18n/char_iterator.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/values.h"
15 #include "content/browser/accessibility/browser_accessibility_android.h"
16 #include "content/common/accessibility_messages.h"
17 #include "jni/BrowserAccessibilityManager_jni.h"
18 #include "ui/accessibility/ax_text_utils.h"
20 using base::android::AttachCurrentThread;
21 using base::android::ScopedJavaLocalRef;
23 namespace {
25 enum AndroidHtmlElementType {
26 HTML_ELEMENT_TYPE_SECTION,
27 HTML_ELEMENT_TYPE_LIST,
28 HTML_ELEMENT_TYPE_CONTROL,
29 HTML_ELEMENT_TYPE_ANY
32 // These are special unofficial strings sent from TalkBack/BrailleBack
33 // to jump to certain categories of web elements.
34 AndroidHtmlElementType HtmlElementTypeFromString(base::string16 element_type) {
35 if (element_type == base::ASCIIToUTF16("SECTION"))
36 return HTML_ELEMENT_TYPE_SECTION;
37 else if (element_type == base::ASCIIToUTF16("LIST"))
38 return HTML_ELEMENT_TYPE_LIST;
39 else if (element_type == base::ASCIIToUTF16("CONTROL"))
40 return HTML_ELEMENT_TYPE_CONTROL;
41 else
42 return HTML_ELEMENT_TYPE_ANY;
45 } // anonymous namespace
47 namespace content {
49 namespace aria_strings {
50 const char kAriaLivePolite[] = "polite";
51 const char kAriaLiveAssertive[] = "assertive";
54 // static
55 BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
56 const ui::AXTreeUpdate& initial_tree,
57 BrowserAccessibilityDelegate* delegate,
58 BrowserAccessibilityFactory* factory) {
59 return new BrowserAccessibilityManagerAndroid(
60 ScopedJavaLocalRef<jobject>(), initial_tree, delegate, factory);
63 BrowserAccessibilityManagerAndroid*
64 BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
65 return static_cast<BrowserAccessibilityManagerAndroid*>(this);
68 BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid(
69 ScopedJavaLocalRef<jobject> content_view_core,
70 const ui::AXTreeUpdate& initial_tree,
71 BrowserAccessibilityDelegate* delegate,
72 BrowserAccessibilityFactory* factory)
73 : BrowserAccessibilityManager(delegate, factory) {
74 SetContentViewCore(content_view_core);
75 Initialize(initial_tree);
78 BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() {
79 JNIEnv* env = AttachCurrentThread();
80 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
81 if (obj.is_null())
82 return;
84 Java_BrowserAccessibilityManager_onNativeObjectDestroyed(env, obj.obj());
87 // static
88 ui::AXTreeUpdate BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
89 ui::AXNodeData empty_document;
90 empty_document.id = 0;
91 empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA;
92 empty_document.state = 1 << ui::AX_STATE_READ_ONLY;
94 ui::AXTreeUpdate update;
95 update.nodes.push_back(empty_document);
96 return update;
99 void BrowserAccessibilityManagerAndroid::SetContentViewCore(
100 ScopedJavaLocalRef<jobject> content_view_core) {
101 if (content_view_core.is_null())
102 return;
104 JNIEnv* env = AttachCurrentThread();
105 java_ref_ = JavaObjectWeakGlobalRef(
106 env, Java_BrowserAccessibilityManager_create(
107 env, reinterpret_cast<intptr_t>(this),
108 content_view_core.obj()).obj());
111 void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
112 ui::AXEvent event_type,
113 BrowserAccessibility* node) {
114 JNIEnv* env = AttachCurrentThread();
115 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
116 if (obj.is_null())
117 return;
119 BrowserAccessibilityAndroid* android_node =
120 static_cast<BrowserAccessibilityAndroid*>(node);
122 if (event_type == ui::AX_EVENT_HIDE)
123 return;
125 if (event_type == ui::AX_EVENT_TREE_CHANGED)
126 return;
128 if (event_type == ui::AX_EVENT_HOVER) {
129 HandleHoverEvent(node);
130 return;
133 // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
134 // the Android system that the accessibility hierarchy rooted at this
135 // node has changed.
136 Java_BrowserAccessibilityManager_handleContentChanged(
137 env, obj.obj(), node->GetId());
139 switch (event_type) {
140 case ui::AX_EVENT_LOAD_COMPLETE:
141 Java_BrowserAccessibilityManager_handlePageLoaded(
142 env, obj.obj(), focus_->id());
143 break;
144 case ui::AX_EVENT_FOCUS:
145 Java_BrowserAccessibilityManager_handleFocusChanged(
146 env, obj.obj(), node->GetId());
147 break;
148 case ui::AX_EVENT_CHECKED_STATE_CHANGED:
149 Java_BrowserAccessibilityManager_handleCheckStateChanged(
150 env, obj.obj(), node->GetId());
151 break;
152 case ui::AX_EVENT_SCROLL_POSITION_CHANGED:
153 Java_BrowserAccessibilityManager_handleScrollPositionChanged(
154 env, obj.obj(), node->GetId());
155 break;
156 case ui::AX_EVENT_SCROLLED_TO_ANCHOR:
157 Java_BrowserAccessibilityManager_handleScrolledToAnchor(
158 env, obj.obj(), node->GetId());
159 break;
160 case ui::AX_EVENT_ALERT:
161 // An alert is a special case of live region. Fall through to the
162 // next case to handle it.
163 case ui::AX_EVENT_SHOW: {
164 // This event is fired when an object appears in a live region.
165 // Speak its text.
166 Java_BrowserAccessibilityManager_announceLiveRegionText(
167 env, obj.obj(),
168 base::android::ConvertUTF16ToJavaString(
169 env, android_node->GetText()).obj());
170 break;
172 case ui::AX_EVENT_TEXT_SELECTION_CHANGED:
173 Java_BrowserAccessibilityManager_handleTextSelectionChanged(
174 env, obj.obj(), node->GetId());
175 break;
176 case ui::AX_EVENT_TEXT_CHANGED:
177 case ui::AX_EVENT_VALUE_CHANGED:
178 if (node->IsEditableText() && GetFocus(GetRoot()) == node) {
179 Java_BrowserAccessibilityManager_handleEditableTextChanged(
180 env, obj.obj(), node->GetId());
181 } else if (android_node->IsSlider()) {
182 Java_BrowserAccessibilityManager_handleSliderChanged(
183 env, obj.obj(), node->GetId());
185 break;
186 default:
187 // There are some notifications that aren't meaningful on Android.
188 // It's okay to skip them.
189 break;
193 jint BrowserAccessibilityManagerAndroid::GetRootId(JNIEnv* env, jobject obj) {
194 return static_cast<jint>(GetRoot()->GetId());
197 jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
198 JNIEnv* env, jobject obj, jint id) {
199 return GetFromID(id) != NULL;
202 void BrowserAccessibilityManagerAndroid::HitTest(
203 JNIEnv* env, jobject obj, jint x, jint y) {
204 if (delegate())
205 delegate()->AccessibilityHitTest(gfx::Point(x, y));
208 jboolean BrowserAccessibilityManagerAndroid::IsEditableText(
209 JNIEnv* env, jobject obj, jint id) {
210 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
211 GetFromID(id));
212 if (!node)
213 return false;
215 return node->IsEditableText();
218 jint BrowserAccessibilityManagerAndroid::GetEditableTextSelectionStart(
219 JNIEnv* env, jobject obj, jint id) {
220 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
221 GetFromID(id));
222 if (!node)
223 return false;
225 return node->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_START);
228 jint BrowserAccessibilityManagerAndroid::GetEditableTextSelectionEnd(
229 JNIEnv* env, jobject obj, jint id) {
230 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
231 GetFromID(id));
232 if (!node)
233 return false;
235 return node->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_END);
238 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
239 JNIEnv* env, jobject obj, jobject info, jint id) {
240 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
241 GetFromID(id));
242 if (!node)
243 return false;
245 if (node->GetParent()) {
246 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
247 env, obj, info, node->GetParent()->GetId());
249 for (unsigned i = 0; i < node->PlatformChildCount(); ++i) {
250 Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
251 env, obj, info, node->InternalGetChild(i)->GetId());
253 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
254 env, obj, info,
256 node->IsCheckable(),
257 node->IsChecked(),
258 node->IsClickable(),
259 node->IsEnabled(),
260 node->IsFocusable(),
261 node->IsFocused(),
262 node->IsPassword(),
263 node->IsScrollable(),
264 node->IsSelected(),
265 node->IsVisibleToUser());
266 Java_BrowserAccessibilityManager_addAccessibilityNodeInfoActions(
267 env, obj, info,
269 node->CanScrollForward(),
270 node->CanScrollBackward(),
271 node->IsClickable(),
272 node->IsEditableText(),
273 node->IsEnabled(),
274 node->IsFocusable(),
275 node->IsFocused());
276 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName(
277 env, obj, info,
278 base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
279 if (!node->IsPassword() ||
280 Java_BrowserAccessibilityManager_shouldExposePasswordText(env, obj)) {
281 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoContentDescription(
282 env, obj, info,
283 base::android::ConvertUTF16ToJavaString(env, node->GetText()).obj(),
284 node->IsLink());
287 gfx::Rect absolute_rect = node->GetLocalBoundsRect();
288 gfx::Rect parent_relative_rect = absolute_rect;
289 if (node->GetParent()) {
290 gfx::Rect parent_rect = node->GetParent()->GetLocalBoundsRect();
291 parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin());
293 bool is_root = node->GetParent() == NULL;
294 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
295 env, obj, info,
297 absolute_rect.x(), absolute_rect.y(),
298 parent_relative_rect.x(), parent_relative_rect.y(),
299 absolute_rect.width(), absolute_rect.height(),
300 is_root);
302 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLollipopAttributes(
303 env, obj, info,
304 node->CanOpenPopup(),
305 node->IsContentInvalid(),
306 node->IsDismissable(),
307 node->IsMultiLine(),
308 node->AndroidInputType(),
309 node->AndroidLiveRegionType());
310 if (node->IsCollection()) {
311 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo(
312 env, obj, info,
313 node->RowCount(),
314 node->ColumnCount(),
315 node->IsHierarchical());
317 if (node->IsCollectionItem() || node->IsHeading()) {
318 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo(
319 env, obj, info,
320 node->RowIndex(),
321 node->RowSpan(),
322 node->ColumnIndex(),
323 node->ColumnSpan(),
324 node->IsHeading());
326 if (node->IsRangeType()) {
327 Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo(
328 env, obj, info,
329 node->AndroidRangeType(),
330 node->RangeMin(),
331 node->RangeMax(),
332 node->RangeCurrentValue());
335 return true;
338 jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
339 JNIEnv* env, jobject obj, jobject event, jint id, jint event_type) {
340 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
341 GetFromID(id));
342 if (!node)
343 return false;
345 Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
346 env, obj, event,
347 node->IsChecked(),
348 node->IsEnabled(),
349 node->IsPassword(),
350 node->IsScrollable());
351 Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
352 env, obj, event,
353 base::android::ConvertUTF8ToJavaString(env, node->GetClassName()).obj());
354 Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
355 env, obj, event,
356 node->GetItemIndex(),
357 node->GetItemCount());
358 Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
359 env, obj, event,
360 node->GetScrollX(),
361 node->GetScrollY(),
362 node->GetMaxScrollX(),
363 node->GetMaxScrollY());
365 switch (event_type) {
366 case ANDROID_ACCESSIBILITY_EVENT_TEXT_CHANGED: {
367 base::string16 before_text, text;
368 if (!node->IsPassword() ||
369 Java_BrowserAccessibilityManager_shouldExposePasswordText(env, obj)) {
370 before_text = node->GetTextChangeBeforeText();
371 text = node->GetText();
373 Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
374 env, obj, event,
375 node->GetTextChangeFromIndex(),
376 node->GetTextChangeAddedCount(),
377 node->GetTextChangeRemovedCount(),
378 base::android::ConvertUTF16ToJavaString(
379 env, before_text).obj(),
380 base::android::ConvertUTF16ToJavaString(env, text).obj());
381 break;
383 case ANDROID_ACCESSIBILITY_EVENT_TEXT_SELECTION_CHANGED: {
384 base::string16 text;
385 if (!node->IsPassword() ||
386 Java_BrowserAccessibilityManager_shouldExposePasswordText(env, obj)) {
387 text = node->GetText();
389 Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
390 env, obj, event,
391 node->GetSelectionStart(),
392 node->GetSelectionEnd(),
393 node->GetEditableTextLength(),
394 base::android::ConvertUTF16ToJavaString(env, text).obj());
395 break;
397 default:
398 break;
401 Java_BrowserAccessibilityManager_setAccessibilityEventLollipopAttributes(
402 env, obj, event,
403 node->CanOpenPopup(),
404 node->IsContentInvalid(),
405 node->IsDismissable(),
406 node->IsMultiLine(),
407 node->AndroidInputType(),
408 node->AndroidLiveRegionType());
409 if (node->IsCollection()) {
410 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo(
411 env, obj, event,
412 node->RowCount(),
413 node->ColumnCount(),
414 node->IsHierarchical());
416 if (node->IsHeading()) {
417 Java_BrowserAccessibilityManager_setAccessibilityEventHeadingFlag(
418 env, obj, event, true);
420 if (node->IsCollectionItem()) {
421 Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo(
422 env, obj, event,
423 node->RowIndex(),
424 node->RowSpan(),
425 node->ColumnIndex(),
426 node->ColumnSpan());
428 if (node->IsRangeType()) {
429 Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo(
430 env, obj, event,
431 node->AndroidRangeType(),
432 node->RangeMin(),
433 node->RangeMax(),
434 node->RangeCurrentValue());
437 return true;
440 void BrowserAccessibilityManagerAndroid::Click(
441 JNIEnv* env, jobject obj, jint id) {
442 BrowserAccessibility* node = GetFromID(id);
443 if (node)
444 DoDefaultAction(*node);
447 void BrowserAccessibilityManagerAndroid::Focus(
448 JNIEnv* env, jobject obj, jint id) {
449 BrowserAccessibility* node = GetFromID(id);
450 if (node)
451 SetFocus(node, true);
454 void BrowserAccessibilityManagerAndroid::Blur(JNIEnv* env, jobject obj) {
455 SetFocus(GetRoot(), true);
458 void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible(
459 JNIEnv* env, jobject obj, jint id) {
460 BrowserAccessibility* node = GetFromID(id);
461 if (node)
462 ScrollToMakeVisible(*node, gfx::Rect(node->GetLocation().size()));
465 void BrowserAccessibilityManagerAndroid::SetTextFieldValue(
466 JNIEnv* env, jobject obj, jint id, jstring value) {
467 BrowserAccessibility* node = GetFromID(id);
468 if (node) {
469 BrowserAccessibilityManager::SetValue(
470 *node, base::android::ConvertJavaStringToUTF16(env, value));
474 void BrowserAccessibilityManagerAndroid::SetSelection(
475 JNIEnv* env, jobject obj, jint id, jint start, jint end) {
476 BrowserAccessibility* node = GetFromID(id);
477 if (node)
478 SetTextSelection(*node, start, end);
481 jboolean BrowserAccessibilityManagerAndroid::AdjustSlider(
482 JNIEnv* env, jobject obj, jint id, jboolean increment) {
483 BrowserAccessibility* node = GetFromID(id);
484 if (!node)
485 return false;
487 BrowserAccessibilityAndroid* android_node =
488 static_cast<BrowserAccessibilityAndroid*>(node);
490 if (!android_node->IsSlider() || !android_node->IsEnabled())
491 return false;
493 float value = node->GetFloatAttribute(ui::AX_ATTR_VALUE_FOR_RANGE);
494 float min = node->GetFloatAttribute(ui::AX_ATTR_MIN_VALUE_FOR_RANGE);
495 float max = node->GetFloatAttribute(ui::AX_ATTR_MAX_VALUE_FOR_RANGE);
496 if (max <= min)
497 return false;
499 // To behave similarly to an Android SeekBar, move by an increment of
500 // approximately 20%.
501 float original_value = value;
502 float delta = (max - min) / 5.0f;
503 value += (increment ? delta : -delta);
504 value = std::max(std::min(value, max), min);
505 if (value != original_value) {
506 BrowserAccessibilityManager::SetValue(
507 *node, base::UTF8ToUTF16(base::DoubleToString(value)));
508 return true;
510 return false;
513 void BrowserAccessibilityManagerAndroid::HandleHoverEvent(
514 BrowserAccessibility* node) {
515 JNIEnv* env = AttachCurrentThread();
516 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
517 if (obj.is_null())
518 return;
520 BrowserAccessibilityAndroid* ancestor =
521 static_cast<BrowserAccessibilityAndroid*>(node->GetParent());
522 while (ancestor && ancestor != GetRoot()) {
523 if (ancestor->PlatformIsLeaf() ||
524 (ancestor->IsFocusable() && !ancestor->HasFocusableChild())) {
525 node = ancestor;
526 // Don't break - we want the highest ancestor that's focusable or a
527 // leaf node.
529 ancestor = static_cast<BrowserAccessibilityAndroid*>(ancestor->GetParent());
532 Java_BrowserAccessibilityManager_handleHover(
533 env, obj.obj(), node->GetId());
536 jint BrowserAccessibilityManagerAndroid::FindElementType(
537 JNIEnv* env, jobject obj, jint start_id, jstring element_type_str,
538 jboolean forwards) {
539 BrowserAccessibility* node = GetFromID(start_id);
540 if (!node)
541 return 0;
543 AndroidHtmlElementType element_type = HtmlElementTypeFromString(
544 base::android::ConvertJavaStringToUTF16(env, element_type_str));
546 node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node);
547 while (node) {
548 switch(element_type) {
549 case HTML_ELEMENT_TYPE_SECTION:
550 if (node->GetRole() == ui::AX_ROLE_ARTICLE ||
551 node->GetRole() == ui::AX_ROLE_APPLICATION ||
552 node->GetRole() == ui::AX_ROLE_BANNER ||
553 node->GetRole() == ui::AX_ROLE_COMPLEMENTARY ||
554 node->GetRole() == ui::AX_ROLE_CONTENT_INFO ||
555 node->GetRole() == ui::AX_ROLE_HEADING ||
556 node->GetRole() == ui::AX_ROLE_MAIN ||
557 node->GetRole() == ui::AX_ROLE_NAVIGATION ||
558 node->GetRole() == ui::AX_ROLE_SEARCH ||
559 node->GetRole() == ui::AX_ROLE_REGION) {
560 return node->GetId();
562 break;
563 case HTML_ELEMENT_TYPE_LIST:
564 if (node->GetRole() == ui::AX_ROLE_LIST ||
565 node->GetRole() == ui::AX_ROLE_GRID ||
566 node->GetRole() == ui::AX_ROLE_TABLE ||
567 node->GetRole() == ui::AX_ROLE_TREE) {
568 return node->GetId();
570 break;
571 case HTML_ELEMENT_TYPE_CONTROL:
572 if (static_cast<BrowserAccessibilityAndroid*>(node)->IsFocusable())
573 return node->GetId();
574 break;
575 case HTML_ELEMENT_TYPE_ANY:
576 // In theory, the API says that an accessibility service could
577 // jump to an element by element name, like 'H1' or 'P'. This isn't
578 // currently used by any accessibility service, and we think it's
579 // better to keep them high-level like 'SECTION' or 'CONTROL', so we
580 // just fall back on linear navigation when we don't recognize the
581 // element type.
582 if (static_cast<BrowserAccessibilityAndroid*>(node)->IsClickable())
583 return node->GetId();
584 break;
587 node = forwards ? NextInTreeOrder(node) : PreviousInTreeOrder(node);
590 return 0;
593 jboolean BrowserAccessibilityManagerAndroid::NextAtGranularity(
594 JNIEnv* env, jobject obj, jint granularity, jboolean extend_selection,
595 jint id, jint cursor_index) {
596 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
597 GetFromID(id));
598 if (!node)
599 return false;
601 jint start_index = -1;
602 int end_index = -1;
603 if (NextAtGranularity(granularity, cursor_index, node,
604 &start_index, &end_index)) {
605 base::string16 text;
606 if (!node->IsPassword() ||
607 Java_BrowserAccessibilityManager_shouldExposePasswordText(env, obj)) {
608 text = node->GetText();
610 Java_BrowserAccessibilityManager_finishGranularityMove(
611 env, obj, base::android::ConvertUTF16ToJavaString(
612 env, text).obj(),
613 extend_selection, start_index, end_index, true);
614 return true;
616 return false;
619 jboolean BrowserAccessibilityManagerAndroid::PreviousAtGranularity(
620 JNIEnv* env, jobject obj, jint granularity, jboolean extend_selection,
621 jint id, jint cursor_index) {
622 BrowserAccessibilityAndroid* node = static_cast<BrowserAccessibilityAndroid*>(
623 GetFromID(id));
624 if (!node)
625 return false;
627 jint start_index = -1;
628 int end_index = -1;
629 if (PreviousAtGranularity(granularity, cursor_index, node,
630 &start_index, &end_index)) {
631 Java_BrowserAccessibilityManager_finishGranularityMove(
632 env, obj, base::android::ConvertUTF16ToJavaString(
633 env, node->GetText()).obj(),
634 extend_selection, start_index, end_index, false);
635 return true;
637 return false;
640 bool BrowserAccessibilityManagerAndroid::NextAtGranularity(
641 int32 granularity, int32 cursor_index,
642 BrowserAccessibilityAndroid* node, int32* start_index, int32* end_index) {
643 switch (granularity) {
644 case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER: {
645 base::string16 text = node->GetText();
646 if (cursor_index >= static_cast<int32>(text.length()))
647 return false;
648 base::i18n::UTF16CharIterator iter(text.data(), text.size());
649 while (!iter.end() && iter.array_pos() <= cursor_index)
650 iter.Advance();
651 *start_index = iter.array_pos();
652 *end_index = iter.array_pos();
653 break;
655 case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
656 case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE: {
657 std::vector<int32> starts;
658 std::vector<int32> ends;
659 node->GetGranularityBoundaries(granularity, &starts, &ends, 0);
660 if (starts.size() == 0)
661 return false;
663 size_t index = 0;
664 while (index < starts.size() - 1 && starts[index] < cursor_index)
665 index++;
667 if (starts[index] < cursor_index)
668 return false;
670 *start_index = starts[index];
671 *end_index = ends[index];
672 break;
674 default:
675 NOTREACHED();
678 return true;
681 bool BrowserAccessibilityManagerAndroid::PreviousAtGranularity(
682 int32 granularity, int32 cursor_index,
683 BrowserAccessibilityAndroid* node, int32* start_index, int32* end_index) {
684 switch (granularity) {
685 case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER: {
686 if (cursor_index <= 0)
687 return false;
688 base::string16 text = node->GetText();
689 base::i18n::UTF16CharIterator iter(text.data(), text.size());
690 int previous_index = 0;
691 while (!iter.end() && iter.array_pos() < cursor_index) {
692 previous_index = iter.array_pos();
693 iter.Advance();
695 *start_index = previous_index;
696 *end_index = previous_index;
697 break;
699 case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
700 case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE: {
701 std::vector<int32> starts;
702 std::vector<int32> ends;
703 node->GetGranularityBoundaries(granularity, &starts, &ends, 0);
704 if (starts.size() == 0)
705 return false;
707 size_t index = starts.size() - 1;
708 while (index > 0 && starts[index] >= cursor_index)
709 index--;
711 if (starts[index] >= cursor_index)
712 return false;
714 *start_index = starts[index];
715 *end_index = ends[index];
716 break;
718 default:
719 NOTREACHED();
722 return true;
725 void BrowserAccessibilityManagerAndroid::SetAccessibilityFocus(
726 JNIEnv* env, jobject obj, jint id) {
727 if (delegate_)
728 delegate_->AccessibilitySetAccessibilityFocus(id);
731 void BrowserAccessibilityManagerAndroid::OnAtomicUpdateFinished(
732 bool root_changed,
733 const std::vector<ui::AXTreeDelegate::Change>& changes) {
734 if (root_changed) {
735 JNIEnv* env = AttachCurrentThread();
736 ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
737 if (obj.is_null())
738 return;
740 Java_BrowserAccessibilityManager_handleNavigate(env, obj.obj());
744 bool
745 BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
746 // The Java layer handles the root scroll offset.
747 return false;
750 bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
751 return RegisterNativesImpl(env);
754 } // namespace content