Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(651)

Unified Diff: content/browser/accessibility/browser_accessibility_android.cc

Issue 15741009: Native Android accessibility. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: content/browser/accessibility/browser_accessibility_android.cc
diff --git a/content/browser/accessibility/browser_accessibility_android.cc b/content/browser/accessibility/browser_accessibility_android.cc
new file mode 100644
index 0000000000000000000000000000000000000000..cb00e2a932f3fc5051daee6fb79d67bffa6ae20d
--- /dev/null
+++ b/content/browser/accessibility/browser_accessibility_android.cc
@@ -0,0 +1,535 @@
+// Copyright (c) 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_android.h"
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_registrar.h"
+#include "base/android/jni_string.h"
+#include "base/utf_string_conversions.h"
+#include "content/browser/accessibility/browser_accessibility_manager_android.h"
+#include "content/common/accessibility_messages.h"
+#include "content/common/accessibility_node_data.h"
+#include "jni/BrowserAccessibility_jni.h"
+
+namespace content {
+
+// static
+BrowserAccessibility* BrowserAccessibility::Create() {
+ return new BrowserAccessibilityAndroid();
+}
+
+BrowserAccessibilityAndroid::BrowserAccessibilityAndroid() {
+ first_time_ = true;
+}
+
+bool BrowserAccessibilityAndroid::IsNative() const {
+ return true;
+}
+
+//
+// Actions, called from Java.
+//
+
+void BrowserAccessibilityAndroid::FocusJNI(JNIEnv* env, jobject obj) {
+ manager_->SetFocus(this, true);
+}
+
+void BrowserAccessibilityAndroid::ClickJNI(JNIEnv* env, jobject obj) {
+ manager_->DoDefaultAction(*this);
+}
+
+//
+// Const accessors, called from Java.
+//
+
+ScopedJavaLocalRef<jstring>
+BrowserAccessibilityAndroid::GetNameJNI(JNIEnv* env, jobject obj) const {
+ return base::android::ConvertUTF16ToJavaString(env, ComputeName());
+}
+
+ScopedJavaLocalRef<jobject>
+BrowserAccessibilityAndroid::GetAbsoluteRectJNI(JNIEnv* env, jobject obj) const {
+ gfx::Rect rect = GetLocalBoundsRect();
+
+ return Java_BrowserAccessibility_createRect(
+ env,
+ static_cast<int>(rect.x()),
+ static_cast<int>(rect.y()),
+ static_cast<int>(rect.right()),
+ static_cast<int>(rect.bottom()));
+}
+
+ScopedJavaLocalRef<jobject>
+BrowserAccessibilityAndroid::GetRectInParentJNI(JNIEnv* env, jobject obj) const {
+ gfx::Rect rect = GetLocalBoundsRect();
+ if (parent()) {
+ gfx::Rect parent_rect = parent()->GetLocalBoundsRect();
+ rect.Offset(-parent_rect.OffsetFromOrigin());
+ }
+ return Java_BrowserAccessibility_createRect(
+ env,
+ static_cast<int>(rect.x()),
+ static_cast<int>(rect.y()),
+ static_cast<int>(rect.right()),
+ static_cast<int>(rect.bottom()));
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsFocusableJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jboolean>(IsFocusable());
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsEditableTextJNI(JNIEnv* env, jobject obj) const {
+ return IsEditableText();
+}
+
+jint BrowserAccessibilityAndroid::GetParentJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jint>(parent()->renderer_id());
+}
+
+jint
+BrowserAccessibilityAndroid::GetChildCountJNI(JNIEnv* env, jobject obj) const {
+ if (IsLeaf())
+ return 0;
+ else
+ return static_cast<jint>(child_count());
+}
+
+jint BrowserAccessibilityAndroid::GetChildIdAtJNI(JNIEnv* env,
+ jobject obj,
+ jint child_index) const {
+ return static_cast<jint>(GetChild(child_index)->renderer_id());
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsCheckableJNI(JNIEnv* env, jobject obj) const {
+ bool checkable = false;
+ bool is_aria_pressed_defined;
+ bool is_mixed;
+ GetAriaTristate("aria-pressed", &is_aria_pressed_defined, &is_mixed);
+ if (role() == AccessibilityNodeData::ROLE_CHECKBOX ||
+ role() == AccessibilityNodeData::ROLE_RADIO_BUTTON ||
+ is_aria_pressed_defined) {
+ checkable = true;
+ }
+ if (HasState(AccessibilityNodeData::STATE_CHECKED))
+ checkable = true;
+ return static_cast<jboolean>(checkable);
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsCheckedJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jboolean>(
+ HasState(AccessibilityNodeData::STATE_CHECKED));
+}
+
+base::android::ScopedJavaLocalRef<jstring>
+BrowserAccessibilityAndroid::GetClassNameJNI(JNIEnv* env, jobject obj) const {
+ const char* class_name = NULL;
+
+ switch(role()) {
+ case AccessibilityNodeData::ROLE_EDITABLE_TEXT:
+ case AccessibilityNodeData::ROLE_SPIN_BUTTON:
+ case AccessibilityNodeData::ROLE_TEXTAREA:
+ case AccessibilityNodeData::ROLE_TEXT_FIELD:
+ class_name = "android.widget.EditText";
+ break;
+ case AccessibilityNodeData::ROLE_SLIDER:
+ class_name = "android.widget.SeekBar";
+ break;
+ case AccessibilityNodeData::ROLE_COMBO_BOX:
+ class_name = "android.widget.Spinner";
+ break;
+ case AccessibilityNodeData::ROLE_BUTTON:
+ case AccessibilityNodeData::ROLE_MENU_BUTTON:
+ case AccessibilityNodeData::ROLE_POPUP_BUTTON:
+ class_name = "android.widget.Button";
+ break;
+ case AccessibilityNodeData::ROLE_CHECKBOX:
+ class_name = "android.widget.CheckBox";
+ break;
+ case AccessibilityNodeData::ROLE_RADIO_BUTTON:
+ class_name = "android.widget.RadioButton";
+ break;
+ case AccessibilityNodeData::ROLE_TOGGLE_BUTTON:
+ class_name = "android.widget.ToggleButton";
+ break;
+ case AccessibilityNodeData::ROLE_CANVAS:
+ case AccessibilityNodeData::ROLE_IMAGE:
+ class_name = "android.widget.Image";
+ break;
+ case AccessibilityNodeData::ROLE_PROGRESS_INDICATOR:
+ class_name = "android.widget.ProgressBar";
+ break;
+ case AccessibilityNodeData::ROLE_TAB_LIST:
+ class_name = "android.widget.TabWidget";
+ break;
+ case AccessibilityNodeData::ROLE_GRID:
+ case AccessibilityNodeData::ROLE_TABLE:
+ class_name = "android.widget.GridView";
+ break;
+ case AccessibilityNodeData::ROLE_LIST:
+ case AccessibilityNodeData::ROLE_LISTBOX:
+ class_name = "android.widget.ListView";
+ break;
+ default:
+ class_name = "android.view.View";
+ break;
+ }
+
+ return base::android::ConvertUTF8ToJavaString(env, class_name);
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsEnabledJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jboolean>(
+ !HasState(AccessibilityNodeData::STATE_UNAVAILABLE));
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsFocusedJNI(JNIEnv* env, jobject obj) const {
+ return manager()->GetFocus(manager()->GetRoot()) == this;
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsPasswordJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jboolean>(
+ HasState(AccessibilityNodeData::STATE_PROTECTED));
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsScrollableJNI(JNIEnv* env, jobject obj) const {
+ int dummy;
+ bool scrollable = GetIntAttribute(
+ AccessibilityNodeData::ATTR_SCROLL_X_MAX, &dummy);
+ return static_cast<jboolean>(scrollable);
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsSelectedJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jboolean>(
+ HasState(AccessibilityNodeData::STATE_SELECTED));
+}
+
+jboolean
+BrowserAccessibilityAndroid::IsVisibleJNI(JNIEnv* env, jobject obj) const {
+ return static_cast<jboolean>(
+ !HasState(AccessibilityNodeData::STATE_INVISIBLE));
+}
+
+base::android::ScopedJavaLocalRef<jstring>
+BrowserAccessibilityAndroid::GetAriaLiveJNI(JNIEnv* env, jobject obj) const {
+ return base::android::ConvertUTF16ToJavaString(env, GetAriaLive());
+}
+
+jint BrowserAccessibilityAndroid::GetItemIndexJNI(JNIEnv* env, jobject obj) const {
+ int index = 0;
+ switch(role()) {
+ case AccessibilityNodeData::ROLE_LIST_ITEM:
+ case AccessibilityNodeData::ROLE_LISTBOX_OPTION:
+ index = index_in_parent();
+ break;
+ case AccessibilityNodeData::ROLE_SLIDER:
+ case AccessibilityNodeData::ROLE_PROGRESS_INDICATOR: {
+ float value_for_range;
+ if (GetFloatAttribute(
+ AccessibilityNodeData::ATTR_VALUE_FOR_RANGE, &value_for_range)) {
+ index = static_cast<int>(value_for_range);
+ }
+ break;
+ }
+ }
+ return static_cast<jint>(index);
+}
+
+jint
+BrowserAccessibilityAndroid::GetItemCountJNI(JNIEnv* env, jobject obj) const {
+ int count = 0;
+ switch(role()) {
+ case AccessibilityNodeData::ROLE_LIST:
+ case AccessibilityNodeData::ROLE_LISTBOX:
+ count = child_count();
+ break;
+ case AccessibilityNodeData::ROLE_SLIDER:
+ case AccessibilityNodeData::ROLE_PROGRESS_INDICATOR: {
+ float max_value_for_range;
+ if (GetFloatAttribute(AccessibilityNodeData::ATTR_MAX_VALUE_FOR_RANGE,
+ &max_value_for_range)) {
+ count = static_cast<int>(max_value_for_range);
+ }
+ break;
+ }
+ }
+ return static_cast<jint>(count);
+}
+
+jint
+BrowserAccessibilityAndroid::GetScrollXJNI(JNIEnv* env, jobject obj) const {
+ int value = 0;
+ GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X, &value);
+ return static_cast<jint>(value);
+}
+
+jint
+BrowserAccessibilityAndroid::GetScrollYJNI(JNIEnv* env, jobject obj) const {
+ int value = 0;
+ GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y, &value);
+ return static_cast<jint>(value);
+}
+
+jint
+BrowserAccessibilityAndroid::GetMaxScrollXJNI(JNIEnv* env, jobject obj) const {
+ int value = 0;
+ GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_X_MAX, &value);
+ return static_cast<jint>(value);
+}
+
+jint
+BrowserAccessibilityAndroid::GetMaxScrollYJNI(JNIEnv* env, jobject obj) const {
+ int value = 0;
+ GetIntAttribute(AccessibilityNodeData::ATTR_SCROLL_Y_MAX, &value);
+ return static_cast<jint>(value);
+}
+
+jboolean
+BrowserAccessibilityAndroid::GetClickableJNI(JNIEnv* env, jobject obj) const {
+ return (IsLeaf() && !ComputeName().empty());
+}
+
+jint BrowserAccessibilityAndroid::GetSelectionStartJNI(JNIEnv* env, jobject obj)
+ const {
+ int sel_start = 0;
+ GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_START, &sel_start);
+ return sel_start;
+}
+
+jint BrowserAccessibilityAndroid::GetSelectionEndJNI(JNIEnv* env, jobject obj)
+ const {
+ int sel_end = 0;
+ GetIntAttribute(AccessibilityNodeData::ATTR_TEXT_SEL_END, &sel_end);
+ return sel_end;
+}
+
+jint BrowserAccessibilityAndroid::GetEditableTextLengthJNI(
+ JNIEnv* env, jobject obj) const {
+ return value().length();
+}
+
+int BrowserAccessibilityAndroid::GetTextChangeFromIndexJNI(
+ JNIEnv* env, jobject obj) const {
+ size_t index = 0;
+ while (index < old_value_.length() &&
+ index < new_value_.length() &&
+ old_value_[index] == new_value_[index]) {
+ index++;
+ }
+ return index;
+}
+
+jint BrowserAccessibilityAndroid::GetTextChangeAddedCountJNI(
+ JNIEnv* env, jobject obj) const {
+ size_t old_len = old_value_.length();
+ size_t new_len = new_value_.length();
+ size_t left = 0;
+ while (left < old_len &&
+ left < new_len &&
+ old_value_[left] == new_value_[left]) {
+ left++;
+ }
+ size_t right = 0;
+ while (right < old_len &&
+ right < new_len &&
+ old_value_[old_len - right - 1] == new_value_[new_len - right - 1]) {
+ right++;
+ }
+ return (new_len - left - right);
+}
+
+jint BrowserAccessibilityAndroid::GetTextChangeRemovedCountJNI(
+ JNIEnv* env, jobject obj) const {
+ size_t old_len = old_value_.length();
+ size_t new_len = new_value_.length();
+ size_t left = 0;
+ while (left < old_len &&
+ left < new_len &&
+ old_value_[left] == new_value_[left]) {
+ left++;
+ }
+ size_t right = 0;
+ while (right < old_len &&
+ right < new_len &&
+ old_value_[old_len - right - 1] == new_value_[new_len - right - 1]) {
+ right++;
+ }
+ return (old_len - left - right);
+}
+
+base::android::ScopedJavaLocalRef<jstring>
+BrowserAccessibilityAndroid::GetTextChangeBeforeTextJNI(
+ JNIEnv* env, jobject obj) const {
+ return base::android::ConvertUTF16ToJavaString(env, old_value_);
+}
+
+string16 BrowserAccessibilityAndroid::ComputeName() const {
+ if (IsIframe() ||
+ role() == AccessibilityNodeData::ROLE_WEB_AREA) {
+ return string16();
+ }
+
+ string16 description;
+ GetStringAttribute(AccessibilityNodeData::ATTR_DESCRIPTION,
+ &description);
+
+ string16 text;
+ if (!name().empty())
+ text = name();
+ else if (!description.empty())
+ text = description;
+ else if (!value().empty())
+ text = value();
+
+ if (text.empty() && HasOnlyStaticTextChildren()) {
+ for (uint32 i = 0; i < child_count(); i++) {
+ BrowserAccessibility* child = GetChild(i);
+ text += static_cast<BrowserAccessibilityAndroid*>(child)->ComputeName();
+ }
+ }
+
+ switch(role()) {
+ case AccessibilityNodeData::ROLE_IMAGE_MAP_LINK:
+ case AccessibilityNodeData::ROLE_LINK:
+ case AccessibilityNodeData::ROLE_WEBCORE_LINK:
+ if (!text.empty())
+ text += ASCIIToUTF16(" ");
+ text += ASCIIToUTF16("Link");
+ break;
+ case AccessibilityNodeData::ROLE_HEADING:
+ // Only append "heading" if this node already has text.
+ if (!text.empty())
+ text += ASCIIToUTF16(" Heading");
+ break;
+ }
+
+ return text;
+}
+
+string16 BrowserAccessibilityAndroid::GetAriaLive() const {
+ string16 aria_live;
+ if (GetStringAttribute(AccessibilityNodeData::ATTR_CONTAINER_LIVE_STATUS,
+ &aria_live)) {
+ return aria_live;
+ }
+ return string16();
+}
+
+bool BrowserAccessibilityAndroid::HasFocusableChild() const {
+ for (uint32 i = 0; i < child_count(); i++) {
+ BrowserAccessibility* child = GetChild(i);
+ if (child->HasState(AccessibilityNodeData::STATE_FOCUSABLE))
+ return true;
+ if (static_cast<BrowserAccessibilityAndroid*>(child)->HasFocusableChild())
+ return true;
+ }
+ return false;
+}
+
+bool BrowserAccessibilityAndroid::HasOnlyStaticTextChildren() const {
+ for (uint32 i = 0; i < child_count(); i++) {
+ BrowserAccessibility* child = GetChild(i);
+ if (child->role() != AccessibilityNodeData::ROLE_STATIC_TEXT)
+ return false;
+ }
+ return true;
+}
+
+bool BrowserAccessibilityAndroid::IsIframe() const {
+ string16 html_tag;
+ GetStringAttribute(AccessibilityNodeData::ATTR_HTML_TAG, &html_tag);
+ return html_tag == ASCIIToUTF16("iframe");
+}
+
+bool BrowserAccessibilityAndroid::IsFocusable() const {
+ bool focusable = HasState(AccessibilityNodeData::STATE_FOCUSABLE);
+ if (IsIframe() ||
+ role() == AccessibilityNodeData::ROLE_WEB_AREA) {
+ focusable = false;
+ }
+ return focusable;
+}
+
+bool BrowserAccessibilityAndroid::IsLeaf() const {
+ if (child_count() == 0)
+ return true;
+
+ // Iframes are always allowed to contain children.
+ if (IsIframe() ||
+ role() == AccessibilityNodeData::ROLE_ROOT_WEB_AREA ||
+ role() == AccessibilityNodeData::ROLE_WEB_AREA) {
+ return false;
+ }
+
+ // If it has a focusable child, we definitely can't leave out children.
+ if (HasFocusableChild())
+ return false;
+
+ // Headings with text can drop their children.
+ string16 name = ComputeName();
+ if (role() == AccessibilityNodeData::ROLE_HEADING && !name.empty())
+ return true;
+
+ // Focusable nodes with text can drop their children.
+ if (HasState(AccessibilityNodeData::STATE_FOCUSABLE) && !name.empty())
+ return true;
+
+ // Nodes with only static text as children can drop their children.
+ if (HasOnlyStaticTextChildren())
+ return true;
+
+ return false;
+}
+
+void BrowserAccessibilityAndroid::PostInitialize() {
+ BrowserAccessibility::PostInitialize();
+
+ if (IsEditableText()) {
+ if (value_ != new_value_) {
+ old_value_ = new_value_;
+ new_value_ = value_;
+ }
+ }
+
+ if (role_ == AccessibilityNodeData::ROLE_ALERT && first_time_)
+ manager_->NotifyAccessibilityEvent(AccessibilityNotificationAlert, this);
+
+ string16 live;
+ if (GetStringAttribute(AccessibilityNodeData::ATTR_CONTAINER_LIVE_STATUS,
+ &live)) {
+ NotifyLiveRegionUpdate(live);
+ }
+
+ first_time_ = false;
+}
+
+void BrowserAccessibilityAndroid::NotifyLiveRegionUpdate(string16& aria_live) {
+ if (!EqualsASCII(aria_live, aria_strings::kAriaLivePolite) &&
+ !EqualsASCII(aria_live, aria_strings::kAriaLiveAssertive))
+ return;
+
+ string16 text = ComputeName();
+ if (cached_text_ != text) {
+ if (!text.empty()) {
+ manager_->NotifyAccessibilityEvent(AccessibilityNotificationObjectShow,
+ this);
+ }
+ cached_text_ = text;
+ }
+}
+
+bool RegisterBrowserAccessibility(JNIEnv* env) {
+ return RegisterNativesImpl(env);
+}
+
+} // namespace content

Powered by Google App Engine
This is Rietveld 408576698