blob: 3f7121b56d738c3ce28a7fc38afc1c5534ee0aca [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/accessibility/browser_accessibility_manager_android.h"
#include <stddef.h>
#include <cmath>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/i18n/char_iterator.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "content/browser/accessibility/browser_accessibility_android.h"
#include "content/browser/accessibility/one_shot_accessibility_tree_search.h"
#include "content/common/accessibility_messages.h"
#include "jni/BrowserAccessibilityManager_jni.h"
#include "ui/accessibility/ax_text_utils.h"
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace content {
namespace {
using SearchKeyToPredicateMap =
base::hash_map<base::string16, AccessibilityMatchPredicate>;
base::LazyInstance<SearchKeyToPredicateMap> g_search_key_to_predicate_map =
LAZY_INSTANCE_INITIALIZER;
base::LazyInstance<base::string16> g_all_search_keys =
LAZY_INSTANCE_INITIALIZER;
bool SectionPredicate(
BrowserAccessibility* start, BrowserAccessibility* node) {
switch (node->GetRole()) {
case ui::AX_ROLE_ARTICLE:
case ui::AX_ROLE_APPLICATION:
case ui::AX_ROLE_BANNER:
case ui::AX_ROLE_COMPLEMENTARY:
case ui::AX_ROLE_CONTENT_INFO:
case ui::AX_ROLE_HEADING:
case ui::AX_ROLE_MAIN:
case ui::AX_ROLE_NAVIGATION:
case ui::AX_ROLE_SEARCH:
case ui::AX_ROLE_REGION:
return true;
default:
return false;
}
}
bool AllInterestingNodesPredicate(
BrowserAccessibility* start, BrowserAccessibility* node) {
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
return android_node->IsInterestingOnAndroid();
}
void AddToPredicateMap(const char* search_key_ascii,
AccessibilityMatchPredicate predicate) {
base::string16 search_key_utf16 = base::ASCIIToUTF16(search_key_ascii);
g_search_key_to_predicate_map.Get()[search_key_utf16] = predicate;
if (!g_all_search_keys.Get().empty())
g_all_search_keys.Get() += base::ASCIIToUTF16(",");
g_all_search_keys.Get() += search_key_utf16;
}
// These are special unofficial strings sent from TalkBack/BrailleBack
// to jump to certain categories of web elements.
void InitSearchKeyToPredicateMapIfNeeded() {
if (!g_search_key_to_predicate_map.Get().empty())
return;
AddToPredicateMap("ARTICLE", AccessibilityArticlePredicate);
AddToPredicateMap("BUTTON", AccessibilityButtonPredicate);
AddToPredicateMap("CHECKBOX", AccessibilityCheckboxPredicate);
AddToPredicateMap("COMBOBOX", AccessibilityComboboxPredicate);
AddToPredicateMap("CONTROL", AccessibilityControlPredicate);
AddToPredicateMap("FOCUSABLE", AccessibilityFocusablePredicate);
AddToPredicateMap("FRAME", AccessibilityFramePredicate);
AddToPredicateMap("GRAPHIC", AccessibilityGraphicPredicate);
AddToPredicateMap("H1", AccessibilityH1Predicate);
AddToPredicateMap("H2", AccessibilityH2Predicate);
AddToPredicateMap("H3", AccessibilityH3Predicate);
AddToPredicateMap("H4", AccessibilityH4Predicate);
AddToPredicateMap("H5", AccessibilityH5Predicate);
AddToPredicateMap("H6", AccessibilityH6Predicate);
AddToPredicateMap("HEADING", AccessibilityHeadingPredicate);
AddToPredicateMap("LANDMARK", AccessibilityLandmarkPredicate);
AddToPredicateMap("LINK", AccessibilityLinkPredicate);
AddToPredicateMap("LIST", AccessibilityListPredicate);
AddToPredicateMap("LIST_ITEM", AccessibilityListItemPredicate);
AddToPredicateMap("MAIN", AccessibilityMainPredicate);
AddToPredicateMap("MEDIA", AccessibilityMediaPredicate);
AddToPredicateMap("RADIO", AccessibilityRadioButtonPredicate);
AddToPredicateMap("SECTION", SectionPredicate);
AddToPredicateMap("TABLE", AccessibilityTablePredicate);
AddToPredicateMap("TEXT_FIELD", AccessibilityTextfieldPredicate);
AddToPredicateMap("UNVISITED_LINK", AccessibilityUnvisitedLinkPredicate);
AddToPredicateMap("VISITED_LINK", AccessibilityVisitedLinkPredicate);
}
AccessibilityMatchPredicate PredicateForSearchKey(
const base::string16& element_type) {
InitSearchKeyToPredicateMapIfNeeded();
const auto& iter = g_search_key_to_predicate_map.Get().find(element_type);
if (iter != g_search_key_to_predicate_map.Get().end())
return iter->second;
// If we don't recognize the selector, return any element that a
// screen reader should navigate to.
return AllInterestingNodesPredicate;
}
} // anonymous namespace
namespace aria_strings {
const char kAriaLivePolite[] = "polite";
const char kAriaLiveAssertive[] = "assertive";
}
// static
BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
const ui::AXTreeUpdate& initial_tree,
BrowserAccessibilityDelegate* delegate,
BrowserAccessibilityFactory* factory) {
return new BrowserAccessibilityManagerAndroid(
ScopedJavaLocalRef<jobject>(), initial_tree, delegate, factory);
}
BrowserAccessibilityManagerAndroid*
BrowserAccessibilityManager::ToBrowserAccessibilityManagerAndroid() {
return static_cast<BrowserAccessibilityManagerAndroid*>(this);
}
BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid(
ScopedJavaLocalRef<jobject> content_view_core,
const ui::AXTreeUpdate& initial_tree,
BrowserAccessibilityDelegate* delegate,
BrowserAccessibilityFactory* factory)
: BrowserAccessibilityManager(delegate, factory),
prune_tree_for_screen_reader_(true) {
Initialize(initial_tree);
SetContentViewCore(content_view_core);
}
BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager();
if (obj.is_null())
return;
Java_BrowserAccessibilityManager_onNativeObjectDestroyed(
env, obj, reinterpret_cast<intptr_t>(this));
}
// static
ui::AXTreeUpdate
BrowserAccessibilityManagerAndroid::GetEmptyDocument() {
ui::AXNodeData empty_document;
empty_document.id = 0;
empty_document.role = ui::AX_ROLE_ROOT_WEB_AREA;
empty_document.state = 1 << ui::AX_STATE_READ_ONLY;
ui::AXTreeUpdate update;
update.root_id = empty_document.id;
update.nodes.push_back(empty_document);
return update;
}
void BrowserAccessibilityManagerAndroid::SetContentViewCore(
ScopedJavaLocalRef<jobject> content_view_core) {
if (content_view_core.is_null())
return;
JNIEnv* env = AttachCurrentThread();
java_ref_ = JavaObjectWeakGlobalRef(
env, Java_BrowserAccessibilityManager_create(
env, reinterpret_cast<intptr_t>(this), content_view_core)
.obj());
}
bool BrowserAccessibilityManagerAndroid::ShouldExposePasswordText() {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager();
if (obj.is_null())
return false;
return Java_BrowserAccessibilityManager_shouldExposePasswordText(env, obj);
}
BrowserAccessibility* BrowserAccessibilityManagerAndroid::GetFocus() {
BrowserAccessibility* focus = BrowserAccessibilityManager::GetFocus();
return GetActiveDescendant(focus);
}
void BrowserAccessibilityManagerAndroid::NotifyAccessibilityEvent(
BrowserAccessibilityEvent::Source source,
ui::AXEvent event_type,
BrowserAccessibility* node) {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager();
if (obj.is_null())
return;
if (event_type == ui::AX_EVENT_HIDE)
return;
if (event_type == ui::AX_EVENT_TREE_CHANGED)
return;
// Layout changes are handled in OnLocationChanges and
// SendLocationChangeEvents.
if (event_type == ui::AX_EVENT_LAYOUT_COMPLETE)
return;
if (event_type == ui::AX_EVENT_HOVER) {
HandleHoverEvent(node);
return;
}
// Sometimes we get events on nodes in our internal accessibility tree
// that aren't exposed on Android. Update |node| to point to the highest
// ancestor that's a leaf node.
node = node->GetClosestPlatformObject();
// Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
// the Android system that the accessibility hierarchy rooted at this
// node has changed.
Java_BrowserAccessibilityManager_handleContentChanged(env, obj,
node->unique_id());
// Ignore load complete events on iframes.
if (event_type == ui::AX_EVENT_LOAD_COMPLETE &&
node->manager() != GetRootManager()) {
return;
}
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
switch (event_type) {
case ui::AX_EVENT_LOAD_COMPLETE:
Java_BrowserAccessibilityManager_handlePageLoaded(
env, obj, GetFocus()->unique_id());
break;
case ui::AX_EVENT_FOCUS:
Java_BrowserAccessibilityManager_handleFocusChanged(env, obj,
node->unique_id());
break;
case ui::AX_EVENT_CHECKED_STATE_CHANGED:
Java_BrowserAccessibilityManager_handleCheckStateChanged(
env, obj, node->unique_id());
break;
case ui::AX_EVENT_CLICKED:
Java_BrowserAccessibilityManager_handleClicked(env, obj,
node->unique_id());
break;
case ui::AX_EVENT_SCROLL_POSITION_CHANGED:
Java_BrowserAccessibilityManager_handleScrollPositionChanged(
env, obj, node->unique_id());
break;
case ui::AX_EVENT_SCROLLED_TO_ANCHOR:
Java_BrowserAccessibilityManager_handleScrolledToAnchor(
env, obj, node->unique_id());
break;
case ui::AX_EVENT_ALERT:
// An alert is a special case of live region. Fall through to the
// next case to handle it.
case ui::AX_EVENT_SHOW: {
// This event is fired when an object appears in a live region.
// Speak its text.
Java_BrowserAccessibilityManager_announceLiveRegionText(
env, obj, base::android::ConvertUTF16ToJavaString(
env, android_node->GetText()));
break;
}
case ui::AX_EVENT_TEXT_SELECTION_CHANGED:
Java_BrowserAccessibilityManager_handleTextSelectionChanged(
env, obj, node->unique_id());
break;
case ui::AX_EVENT_TEXT_CHANGED:
case ui::AX_EVENT_VALUE_CHANGED:
if (android_node->IsEditableText() && GetFocus() == node) {
Java_BrowserAccessibilityManager_handleEditableTextChanged(
env, obj, node->unique_id());
} else if (android_node->IsSlider()) {
Java_BrowserAccessibilityManager_handleSliderChanged(env, obj,
node->unique_id());
}
break;
default:
// There are some notifications that aren't meaningful on Android.
// It's okay to skip them.
break;
}
}
void BrowserAccessibilityManagerAndroid::SendLocationChangeEvents(
const std::vector<AccessibilityHostMsg_LocationChangeParams>& params) {
// Android is not very efficient at handling notifications, and location
// changes in particular are frequent and not time-critical. If a lot of
// nodes changed location, just send a single notification after a short
// delay (to batch them), rather than lots of individual notifications.
if (params.size() > 3) {
ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager();
JNIEnv* env = AttachCurrentThread();
if (obj.is_null())
return;
Java_BrowserAccessibilityManager_sendDelayedWindowContentChangedEvent(env,
obj);
return;
}
BrowserAccessibilityManager::SendLocationChangeEvents(params);
}
base::android::ScopedJavaLocalRef<jstring>
BrowserAccessibilityManagerAndroid::GetSupportedHtmlElementTypes(
JNIEnv* env,
const JavaParamRef<jobject>& obj) {
InitSearchKeyToPredicateMapIfNeeded();
return base::android::ConvertUTF16ToJavaString(env, g_all_search_keys.Get());
}
jint BrowserAccessibilityManagerAndroid::GetRootId(
JNIEnv* env,
const JavaParamRef<jobject>& obj) {
if (GetRoot())
return static_cast<jint>(GetRoot()->unique_id());
else
return -1;
}
jboolean BrowserAccessibilityManagerAndroid::IsNodeValid(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
return GetFromUniqueID(id) != NULL;
}
void BrowserAccessibilityManagerAndroid::HitTest(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint x,
jint y) {
BrowserAccessibilityManager::HitTest(gfx::Point(x, y));
}
jboolean BrowserAccessibilityManagerAndroid::IsEditableText(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
return node->IsEditableText();
}
jboolean BrowserAccessibilityManagerAndroid::IsFocused(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
return node->IsFocused();
}
jint BrowserAccessibilityManagerAndroid::GetEditableTextSelectionStart(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
return node->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_START);
}
jint BrowserAccessibilityManagerAndroid::GetEditableTextSelectionEnd(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
return node->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_END);
}
jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityNodeInfo(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
const JavaParamRef<jobject>& info,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
if (node->GetParent()) {
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent(
env, obj, info, node->GetParent()->unique_id());
}
for (unsigned i = 0; i < node->PlatformChildCount(); ++i) {
Java_BrowserAccessibilityManager_addAccessibilityNodeInfoChild(
env, obj, info, node->PlatformGetChild(i)->unique_id());
}
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoBooleanAttributes(
env, obj, info,
id,
node->IsCheckable(),
node->IsChecked(),
node->IsClickable(),
node->IsEnabled(),
node->IsFocusable(),
node->IsFocused(),
node->IsPassword(),
node->IsScrollable(),
node->IsSelected(),
node->IsVisibleToUser());
Java_BrowserAccessibilityManager_addAccessibilityNodeInfoActions(
env, obj, info,
id,
node->CanScrollForward(),
node->CanScrollBackward(),
node->CanScrollUp(),
node->CanScrollDown(),
node->CanScrollLeft(),
node->CanScrollRight(),
node->IsClickable(),
node->IsEditableText(),
node->IsEnabled(),
node->IsFocusable(),
node->IsFocused(),
node->IsCollapsed(),
node->IsExpanded(),
node->HasNonEmptyValue());
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoClassName(
env, obj, info,
base::android::ConvertUTF8ToJavaString(env, node->GetClassName()));
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoText(
env, obj, info,
base::android::ConvertUTF16ToJavaString(env, node->GetText()),
node->IsLink(), node->IsEditableText());
base::string16 element_id;
if (node->GetHtmlAttribute("id", &element_id)) {
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoViewIdResourceName(
env, obj, info,
base::android::ConvertUTF16ToJavaString(env, element_id));
}
gfx::Rect absolute_rect = node->GetPageBoundsRect();
gfx::Rect parent_relative_rect = absolute_rect;
if (node->GetParent()) {
gfx::Rect parent_rect = node->GetParent()->GetPageBoundsRect();
parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin());
}
bool is_root = node->GetParent() == NULL;
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLocation(
env, obj, info,
id,
absolute_rect.x(), absolute_rect.y(),
parent_relative_rect.x(), parent_relative_rect.y(),
absolute_rect.width(), absolute_rect.height(),
is_root);
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoKitKatAttributes(
env, obj, info, is_root, node->IsEditableText(),
base::android::ConvertUTF16ToJavaString(env, node->GetRoleDescription()));
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoLollipopAttributes(
env, obj, info,
node->CanOpenPopup(),
node->IsContentInvalid(),
node->IsDismissable(),
node->IsMultiLine(),
node->AndroidInputType(),
node->AndroidLiveRegionType());
if (node->IsCollection()) {
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionInfo(
env, obj, info,
node->RowCount(),
node->ColumnCount(),
node->IsHierarchical());
}
if (node->IsCollectionItem() || node->IsHeading()) {
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoCollectionItemInfo(
env, obj, info,
node->RowIndex(),
node->RowSpan(),
node->ColumnIndex(),
node->ColumnSpan(),
node->IsHeading());
}
if (node->IsRangeType()) {
Java_BrowserAccessibilityManager_setAccessibilityNodeInfoRangeInfo(
env, obj, info,
node->AndroidRangeType(),
node->RangeMin(),
node->RangeMax(),
node->RangeCurrentValue());
}
return true;
}
jboolean BrowserAccessibilityManagerAndroid::PopulateAccessibilityEvent(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
const JavaParamRef<jobject>& event,
jint id,
jint event_type) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
Java_BrowserAccessibilityManager_setAccessibilityEventBooleanAttributes(
env, obj, event,
node->IsChecked(),
node->IsEnabled(),
node->IsPassword(),
node->IsScrollable());
Java_BrowserAccessibilityManager_setAccessibilityEventClassName(
env, obj, event,
base::android::ConvertUTF8ToJavaString(env, node->GetClassName()));
Java_BrowserAccessibilityManager_setAccessibilityEventListAttributes(
env, obj, event,
node->GetItemIndex(),
node->GetItemCount());
Java_BrowserAccessibilityManager_setAccessibilityEventScrollAttributes(
env, obj, event,
node->GetScrollX(),
node->GetScrollY(),
node->GetMaxScrollX(),
node->GetMaxScrollY());
switch (event_type) {
case ANDROID_ACCESSIBILITY_EVENT_TEXT_CHANGED: {
base::string16 before_text = node->GetTextChangeBeforeText();
base::string16 text = node->GetText();
Java_BrowserAccessibilityManager_setAccessibilityEventTextChangedAttrs(
env, obj, event, node->GetTextChangeFromIndex(),
node->GetTextChangeAddedCount(), node->GetTextChangeRemovedCount(),
base::android::ConvertUTF16ToJavaString(env, before_text),
base::android::ConvertUTF16ToJavaString(env, text));
break;
}
case ANDROID_ACCESSIBILITY_EVENT_TEXT_SELECTION_CHANGED: {
base::string16 text = node->GetText();
Java_BrowserAccessibilityManager_setAccessibilityEventSelectionAttrs(
env, obj, event, node->GetSelectionStart(), node->GetSelectionEnd(),
node->GetEditableTextLength(),
base::android::ConvertUTF16ToJavaString(env, text));
break;
}
default:
break;
}
Java_BrowserAccessibilityManager_setAccessibilityEventLollipopAttributes(
env, obj, event,
node->CanOpenPopup(),
node->IsContentInvalid(),
node->IsDismissable(),
node->IsMultiLine(),
node->AndroidInputType(),
node->AndroidLiveRegionType());
if (node->IsCollection()) {
Java_BrowserAccessibilityManager_setAccessibilityEventCollectionInfo(
env, obj, event,
node->RowCount(),
node->ColumnCount(),
node->IsHierarchical());
}
if (node->IsHeading()) {
Java_BrowserAccessibilityManager_setAccessibilityEventHeadingFlag(
env, obj, event, true);
}
if (node->IsCollectionItem()) {
Java_BrowserAccessibilityManager_setAccessibilityEventCollectionItemInfo(
env, obj, event,
node->RowIndex(),
node->RowSpan(),
node->ColumnIndex(),
node->ColumnSpan());
}
if (node->IsRangeType()) {
Java_BrowserAccessibilityManager_setAccessibilityEventRangeInfo(
env, obj, event,
node->AndroidRangeType(),
node->RangeMin(),
node->RangeMax(),
node->RangeCurrentValue());
}
return true;
}
void BrowserAccessibilityManagerAndroid::Click(JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (node)
node->manager()->DoDefaultAction(*node);
}
void BrowserAccessibilityManagerAndroid::Focus(JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (node)
node->manager()->SetFocus(*node);
}
void BrowserAccessibilityManagerAndroid::Blur(
JNIEnv* env,
const JavaParamRef<jobject>& obj) {
SetFocus(*GetRoot());
}
void BrowserAccessibilityManagerAndroid::ScrollToMakeNodeVisible(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (node)
node->manager()->ScrollToMakeVisible(
*node, gfx::Rect(node->GetFrameBoundsRect().size()));
}
void BrowserAccessibilityManagerAndroid::SetTextFieldValue(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id,
const JavaParamRef<jstring>& value) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (node) {
node->manager()->SetValue(
*node, base::android::ConvertJavaStringToUTF16(env, value));
}
}
void BrowserAccessibilityManagerAndroid::SetSelection(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id,
jint start,
jint end) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (node)
node->manager()->SetTextSelection(*node, start, end);
}
jboolean BrowserAccessibilityManagerAndroid::AdjustSlider(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id,
jboolean increment) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
if (!android_node->IsSlider() || !android_node->IsEnabled())
return false;
float value = node->GetFloatAttribute(ui::AX_ATTR_VALUE_FOR_RANGE);
float min = node->GetFloatAttribute(ui::AX_ATTR_MIN_VALUE_FOR_RANGE);
float max = node->GetFloatAttribute(ui::AX_ATTR_MAX_VALUE_FOR_RANGE);
if (max <= min)
return false;
// To behave similarly to an Android SeekBar, move by an increment of
// approximately 20%.
float original_value = value;
float delta = (max - min) / 5.0f;
value += (increment ? delta : -delta);
value = std::max(std::min(value, max), min);
if (value != original_value) {
node->manager()->SetValue(
*node, base::UTF8ToUTF16(base::DoubleToString(value)));
return true;
}
return false;
}
void BrowserAccessibilityManagerAndroid::ShowContextMenu(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (node)
node->manager()->ShowContextMenu(*node);
}
void BrowserAccessibilityManagerAndroid::HandleHoverEvent(
BrowserAccessibility* node) {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager();
if (obj.is_null())
return;
// First walk up to the nearest platform node, in case this node isn't
// even exposed on the platform.
node = node->GetClosestPlatformObject();
// If this node is uninteresting and just a wrapper around a sole
// interesting descendant, prefer that descendant instead.
const BrowserAccessibilityAndroid* android_node =
static_cast<BrowserAccessibilityAndroid*>(node);
const BrowserAccessibilityAndroid* sole_interesting_node =
android_node->GetSoleInterestingNodeFromSubtree();
if (sole_interesting_node)
android_node = sole_interesting_node;
// Finally, if this node is still uninteresting, try to walk up to
// find an interesting parent.
while (android_node && !android_node->IsInterestingOnAndroid()) {
android_node = static_cast<BrowserAccessibilityAndroid*>(
android_node->GetParent());
}
if (android_node) {
Java_BrowserAccessibilityManager_handleHover(
env, obj, android_node->unique_id());
}
}
jint BrowserAccessibilityManagerAndroid::FindElementType(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint start_id,
const JavaParamRef<jstring>& element_type_str,
jboolean forwards) {
BrowserAccessibilityAndroid* start_node = GetFromUniqueID(start_id);
if (!start_node)
return 0;
BrowserAccessibilityManager* root_manager = GetRootManager();
if (!root_manager)
return 0;
BrowserAccessibility* root = root_manager->GetRoot();
if (!root)
return 0;
AccessibilityMatchPredicate predicate = PredicateForSearchKey(
base::android::ConvertJavaStringToUTF16(env, element_type_str));
OneShotAccessibilityTreeSearch tree_search(root);
tree_search.SetStartNode(start_node);
tree_search.SetDirection(
forwards ?
OneShotAccessibilityTreeSearch::FORWARDS :
OneShotAccessibilityTreeSearch::BACKWARDS);
tree_search.SetResultLimit(1);
tree_search.SetImmediateDescendantsOnly(false);
tree_search.SetVisibleOnly(false);
tree_search.AddPredicate(predicate);
if (tree_search.CountMatches() == 0)
return 0;
return tree_search.GetMatchAtIndex(0)->unique_id();
}
jboolean BrowserAccessibilityManagerAndroid::NextAtGranularity(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint granularity,
jboolean extend_selection,
jint id,
jint cursor_index) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
jint start_index = -1;
int end_index = -1;
if (NextAtGranularity(granularity, cursor_index, node,
&start_index, &end_index)) {
base::string16 text = node->GetText();
Java_BrowserAccessibilityManager_finishGranularityMove(
env, obj, base::android::ConvertUTF16ToJavaString(env, text),
extend_selection, start_index, end_index, true);
return true;
}
return false;
}
jboolean BrowserAccessibilityManagerAndroid::PreviousAtGranularity(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint granularity,
jboolean extend_selection,
jint id,
jint cursor_index) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
jint start_index = -1;
int end_index = -1;
if (PreviousAtGranularity(granularity, cursor_index, node,
&start_index, &end_index)) {
Java_BrowserAccessibilityManager_finishGranularityMove(
env, obj, base::android::ConvertUTF16ToJavaString(env, node->GetText()),
extend_selection, start_index, end_index, false);
return true;
}
return false;
}
bool BrowserAccessibilityManagerAndroid::NextAtGranularity(
int32_t granularity,
int32_t cursor_index,
BrowserAccessibilityAndroid* node,
int32_t* start_index,
int32_t* end_index) {
switch (granularity) {
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER: {
base::string16 text = node->GetText();
if (cursor_index >= static_cast<int32_t>(text.length()))
return false;
base::i18n::UTF16CharIterator iter(text.data(), text.size());
while (!iter.end() && iter.array_pos() <= cursor_index)
iter.Advance();
*start_index = iter.array_pos();
*end_index = iter.array_pos();
break;
}
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE: {
std::vector<int32_t> starts;
std::vector<int32_t> ends;
node->GetGranularityBoundaries(granularity, &starts, &ends, 0);
if (starts.size() == 0)
return false;
size_t index = 0;
while (index < starts.size() - 1 && starts[index] < cursor_index)
index++;
if (starts[index] < cursor_index)
return false;
*start_index = starts[index];
*end_index = ends[index];
break;
}
default:
NOTREACHED();
}
return true;
}
bool BrowserAccessibilityManagerAndroid::PreviousAtGranularity(
int32_t granularity,
int32_t cursor_index,
BrowserAccessibilityAndroid* node,
int32_t* start_index,
int32_t* end_index) {
switch (granularity) {
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER: {
if (cursor_index <= 0)
return false;
base::string16 text = node->GetText();
base::i18n::UTF16CharIterator iter(text.data(), text.size());
int previous_index = 0;
while (!iter.end() && iter.array_pos() < cursor_index) {
previous_index = iter.array_pos();
iter.Advance();
}
*start_index = previous_index;
*end_index = previous_index;
break;
}
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE: {
std::vector<int32_t> starts;
std::vector<int32_t> ends;
node->GetGranularityBoundaries(granularity, &starts, &ends, 0);
if (starts.size() == 0)
return false;
size_t index = starts.size() - 1;
while (index > 0 && starts[index] >= cursor_index)
index--;
if (starts[index] >= cursor_index)
return false;
*start_index = starts[index];
*end_index = ends[index];
break;
}
default:
NOTREACHED();
}
return true;
}
void BrowserAccessibilityManagerAndroid::SetAccessibilityFocus(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return;
node->manager()->SetAccessibilityFocus(*node);
}
bool BrowserAccessibilityManagerAndroid::IsSlider(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
return node->GetRole() == ui::AX_ROLE_SLIDER;
}
bool BrowserAccessibilityManagerAndroid::Scroll(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jint id,
int direction) {
BrowserAccessibilityAndroid* node = GetFromUniqueID(id);
if (!node)
return false;
return node->Scroll(direction);
}
void BrowserAccessibilityManagerAndroid::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
const std::vector<ui::AXTreeDelegate::Change>& changes) {
BrowserAccessibilityManager::OnAtomicUpdateFinished(
tree, root_changed, changes);
if (root_changed) {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager();
if (obj.is_null())
return;
Java_BrowserAccessibilityManager_handleNavigate(env, obj);
}
}
bool
BrowserAccessibilityManagerAndroid::UseRootScrollOffsetsWhenComputingBounds() {
// The Java layer handles the root scroll offset.
return false;
}
BrowserAccessibilityAndroid*
BrowserAccessibilityManagerAndroid::GetFromUniqueID(int32_t unique_id) {
return static_cast<BrowserAccessibilityAndroid*>(
BrowserAccessibility::GetFromUniqueID(unique_id));
}
ScopedJavaLocalRef<jobject>
BrowserAccessibilityManagerAndroid::GetJavaRefFromRootManager() {
BrowserAccessibilityManagerAndroid* root_manager =
static_cast<BrowserAccessibilityManagerAndroid*>(
GetRootManager());
if (!root_manager)
return ScopedJavaLocalRef<jobject>();
JNIEnv* env = AttachCurrentThread();
return root_manager->java_ref().get(env);
}
bool RegisterBrowserAccessibilityManager(JNIEnv* env) {
return RegisterNativesImpl(env);
}
} // namespace content