| // 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/feature_list.h" |
| #include "base/i18n/char_iterator.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "content/app/strings/grit/content_strings.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 "content/public/common/content_features.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 { |
| |
| // IMPORTANT! |
| // These values are written to logs. Do not renumber or delete |
| // existing items; add new entries to the end of the list. |
| // |
| // Note: The string names for these enums must correspond with the names of |
| // constants from AccessibilityEvent and AccessibilityServiceInfo, defined |
| // below. For example, UMA_EVENT_ANNOUNCEMENT corresponds to |
| // ACCESSIBILITYEVENT_TYPE_ANNOUNCEMENT via the macro |
| // EVENT_TYPE_HISTOGRAM(event_type_mask, ANNOUNCEMENT). |
| enum { |
| UMA_CAPABILITY_CAN_CONTROL_MAGNIFICATION = 0, |
| UMA_CAPABILITY_CAN_PERFORM_GESTURES = 1, |
| UMA_CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 2, |
| UMA_CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = 3, |
| UMA_CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 4, |
| UMA_CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT = 5, |
| UMA_EVENT_ANNOUNCEMENT = 6, |
| UMA_EVENT_ASSIST_READING_CONTEXT = 7, |
| UMA_EVENT_GESTURE_DETECTION_END = 8, |
| UMA_EVENT_GESTURE_DETECTION_START = 9, |
| UMA_EVENT_NOTIFICATION_STATE_CHANGED = 10, |
| UMA_EVENT_TOUCH_EXPLORATION_GESTURE_END = 11, |
| UMA_EVENT_TOUCH_EXPLORATION_GESTURE_START = 12, |
| UMA_EVENT_TOUCH_INTERACTION_END = 13, |
| UMA_EVENT_TOUCH_INTERACTION_START = 14, |
| UMA_EVENT_VIEW_ACCESSIBILITY_FOCUSED = 15, |
| UMA_EVENT_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 16, |
| UMA_EVENT_VIEW_CLICKED = 17, |
| UMA_EVENT_VIEW_CONTEXT_CLICKED = 18, |
| UMA_EVENT_VIEW_FOCUSED = 19, |
| UMA_EVENT_VIEW_HOVER_ENTER = 20, |
| UMA_EVENT_VIEW_HOVER_EXIT = 21, |
| UMA_EVENT_VIEW_LONG_CLICKED = 22, |
| UMA_EVENT_VIEW_SCROLLED = 23, |
| UMA_EVENT_VIEW_SELECTED = 24, |
| UMA_EVENT_VIEW_TEXT_CHANGED = 25, |
| UMA_EVENT_VIEW_TEXT_SELECTION_CHANGED = 26, |
| UMA_EVENT_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 27, |
| UMA_EVENT_WINDOWS_CHANGED = 28, |
| UMA_EVENT_WINDOW_CONTENT_CHANGED = 29, |
| UMA_EVENT_WINDOW_STATE_CHANGED = 30, |
| UMA_FEEDBACK_AUDIBLE = 31, |
| UMA_FEEDBACK_BRAILLE = 32, |
| UMA_FEEDBACK_GENERIC = 33, |
| UMA_FEEDBACK_HAPTIC = 34, |
| UMA_FEEDBACK_SPOKEN = 35, |
| UMA_FEEDBACK_VISUAL = 36, |
| UMA_FLAG_FORCE_DIRECT_BOOT_AWARE = 37, |
| UMA_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 38, |
| UMA_FLAG_REPORT_VIEW_IDS = 39, |
| UMA_FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 40, |
| UMA_FLAG_REQUEST_FILTER_KEY_EVENTS = 41, |
| UMA_FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 42, |
| UMA_FLAG_RETRIEVE_INTERACTIVE_WINDOWS = 43, |
| |
| // This must always be the last enum. It's okay for its value to |
| // increase, but none of the other enum values may change. |
| UMA_ACCESSIBILITYSERVICEINFO_MAX |
| }; |
| |
| // These are constants from |
| // android.view.accessibility.AccessibilityEvent in Java. |
| // |
| // If you add a new constant, add a new UMA enum above and add a line |
| // to CollectStats(), below. |
| enum { |
| ACCESSIBILITYEVENT_TYPE_VIEW_CLICKED = 0x00000001, |
| ACCESSIBILITYEVENT_TYPE_VIEW_LONG_CLICKED = 0x00000002, |
| ACCESSIBILITYEVENT_TYPE_VIEW_SELECTED = 0x00000004, |
| ACCESSIBILITYEVENT_TYPE_VIEW_FOCUSED = 0x00000008, |
| ACCESSIBILITYEVENT_TYPE_VIEW_TEXT_CHANGED = 0x00000010, |
| ACCESSIBILITYEVENT_TYPE_WINDOW_STATE_CHANGED = 0x00000020, |
| ACCESSIBILITYEVENT_TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040, |
| ACCESSIBILITYEVENT_TYPE_VIEW_HOVER_ENTER = 0x00000080, |
| ACCESSIBILITYEVENT_TYPE_VIEW_HOVER_EXIT = 0x00000100, |
| ACCESSIBILITYEVENT_TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200, |
| ACCESSIBILITYEVENT_TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400, |
| ACCESSIBILITYEVENT_TYPE_WINDOW_CONTENT_CHANGED = 0x00000800, |
| ACCESSIBILITYEVENT_TYPE_VIEW_SCROLLED = 0x00001000, |
| ACCESSIBILITYEVENT_TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000, |
| ACCESSIBILITYEVENT_TYPE_ANNOUNCEMENT = 0x00004000, |
| ACCESSIBILITYEVENT_TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000, |
| ACCESSIBILITYEVENT_TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000, |
| ACCESSIBILITYEVENT_TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = |
| 0x00020000, |
| ACCESSIBILITYEVENT_TYPE_GESTURE_DETECTION_START = 0x00040000, |
| ACCESSIBILITYEVENT_TYPE_GESTURE_DETECTION_END = 0x00080000, |
| ACCESSIBILITYEVENT_TYPE_TOUCH_INTERACTION_START = 0x00100000, |
| ACCESSIBILITYEVENT_TYPE_TOUCH_INTERACTION_END = 0x00200000, |
| ACCESSIBILITYEVENT_TYPE_WINDOWS_CHANGED = 0x00400000, |
| ACCESSIBILITYEVENT_TYPE_VIEW_CONTEXT_CLICKED = 0x00800000, |
| ACCESSIBILITYEVENT_TYPE_ASSIST_READING_CONTEXT = 0x01000000, |
| }; |
| |
| // These are constants from |
| // android.accessibilityservice.AccessibilityServiceInfo in Java: |
| // |
| // If you add a new constant, add a new UMA enum above and add a line |
| // to CollectStats(), below. |
| enum { |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT = 0x00000001, |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = |
| 0x00000002, |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = |
| 0x00000004, |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = |
| 0x00000008, |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_CAN_CONTROL_MAGNIFICATION = 0x00000010, |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_CAN_PERFORM_GESTURES = 0x00000020, |
| ACCESSIBILITYSERVICEINFO_FEEDBACK_SPOKEN = 0x0000001, |
| ACCESSIBILITYSERVICEINFO_FEEDBACK_HAPTIC = 0x0000002, |
| ACCESSIBILITYSERVICEINFO_FEEDBACK_AUDIBLE = 0x0000004, |
| ACCESSIBILITYSERVICEINFO_FEEDBACK_VISUAL = 0x0000008, |
| ACCESSIBILITYSERVICEINFO_FEEDBACK_GENERIC = 0x0000010, |
| ACCESSIBILITYSERVICEINFO_FEEDBACK_BRAILLE = 0x0000020, |
| ACCESSIBILITYSERVICEINFO_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x0000002, |
| ACCESSIBILITYSERVICEINFO_FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 0x0000004, |
| ACCESSIBILITYSERVICEINFO_FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000008, |
| ACCESSIBILITYSERVICEINFO_FLAG_REPORT_VIEW_IDS = 0x00000010, |
| ACCESSIBILITYSERVICEINFO_FLAG_REQUEST_FILTER_KEY_EVENTS = 0x00000020, |
| ACCESSIBILITYSERVICEINFO_FLAG_RETRIEVE_INTERACTIVE_WINDOWS = 0x00000040, |
| ACCESSIBILITYSERVICEINFO_FLAG_FORCE_DIRECT_BOOT_AWARE = 0x00010000, |
| }; |
| |
| // These macros simplify recording a histogram based on information we get |
| // from an AccessibilityService. There are four bitmasks of information |
| // we get from each AccessibilityService, and for each possible bit mask |
| // corresponding to one flag, we want to check if that flag is set, and if |
| // so, record the corresponding histogram. |
| // |
| // Doing this with macros reduces the chance for human error by |
| // recording the wrong histogram for the wrong flag. |
| // |
| // These macros are used by CollectStats(), below. |
| #define EVENT_TYPE_HISTOGRAM(event_type_mask, event_type) \ |
| if (event_type_mask & ACCESSIBILITYEVENT_TYPE_##event_type) \ |
| UMA_HISTOGRAM_ENUMERATION("Accessibility.AndroidServiceInfo", \ |
| UMA_EVENT_##event_type, \ |
| UMA_ACCESSIBILITYSERVICEINFO_MAX) |
| #define FLAGS_HISTOGRAM(flags_mask, flag) \ |
| if (flags_mask & ACCESSIBILITYSERVICEINFO_FLAG_##flag) \ |
| UMA_HISTOGRAM_ENUMERATION("Accessibility.AndroidServiceInfo", \ |
| UMA_FLAG_##flag, UMA_ACCESSIBILITYSERVICEINFO_MAX) |
| #define FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, feedback_type) \ |
| if (feedback_type_mask & ACCESSIBILITYSERVICEINFO_FEEDBACK_##feedback_type) \ |
| UMA_HISTOGRAM_ENUMERATION("Accessibility.AndroidServiceInfo", \ |
| UMA_FEEDBACK_##feedback_type, \ |
| UMA_ACCESSIBILITYSERVICEINFO_MAX) |
| #define CAPABILITY_TYPE_HISTOGRAM(capability_type_mask, capability_type) \ |
| if (capability_type_mask & \ |
| ACCESSIBILITYSERVICEINFO_CAPABILITY_##capability_type) \ |
| UMA_HISTOGRAM_ENUMERATION("Accessibility.AndroidServiceInfo", \ |
| UMA_CAPABILITY_##capability_type, \ |
| UMA_ACCESSIBILITYSERVICEINFO_MAX) |
| |
| using SearchKeyToPredicateMap = |
| base::hash_map<base::string16, AccessibilityMatchPredicate>; |
| base::LazyInstance<SearchKeyToPredicateMap>::DestructorAtExit |
| g_search_key_to_predicate_map = LAZY_INSTANCE_INITIALIZER; |
| base::LazyInstance<base::string16>::DestructorAtExit 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; |
| } |
| |
| // The element in the document for which we may be displaying an autofill popup. |
| int32_t g_element_hosting_autofill_popup_unique_id = -1; |
| |
| // The element in the document that is the next element after |
| // |g_element_hosting_autofill_popup_unique_id|. |
| int32_t g_element_after_element_hosting_autofill_popup_unique_id = -1; |
| |
| // Autofill popup will not be part of the |AXTree| that is sent by renderer. |
| // Hence, we need a proxy |AXNode| to represent the autofill popup. |
| BrowserAccessibility* g_autofill_popup_proxy_node = nullptr; |
| ui::AXNode* g_autofill_popup_proxy_node_ax_node = nullptr; |
| |
| void DeleteAutofillPopupProxy() { |
| if (g_autofill_popup_proxy_node) { |
| g_autofill_popup_proxy_node->Destroy(); |
| delete g_autofill_popup_proxy_node_ax_node; |
| g_autofill_popup_proxy_node = nullptr; |
| } |
| } |
| |
| } // 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); |
| |
| // TODO(dmazzoni): get rid of the use of this class by |
| // AXTreeSnapshotCallback so that content_view_core is always valid. |
| if (!content_view_core.is_null()) |
| CollectStats(); |
| } |
| |
| BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager(); |
| if (obj.is_null()) |
| return; |
| |
| // Clean up autofill popup proxy node in case the popup was not dismissed. |
| DeleteAutofillPopupProxy(); |
| |
| 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.AddState(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::ShouldRespectDisplayedPasswordText() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager(); |
| if (obj.is_null()) |
| return false; |
| |
| return Java_BrowserAccessibilityManager_shouldRespectDisplayedPasswordText( |
| env, 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. |
| BrowserAccessibility* original_node = node; |
| node = node->GetClosestPlatformObject(); |
| BrowserAccessibilityAndroid* android_node = |
| static_cast<BrowserAccessibilityAndroid*>(node); |
| |
| // If the closest platform object is a password field, the event we're |
| // getting is doing something in the shadow dom, for example replacing a |
| // character with a dot after a short pause. On Android we don't want to |
| // fire an event for those changes, but we do want to make sure our internal |
| // state is correct, so we call OnDataChanged() and then return. |
| if (android_node->IsPassword() && original_node != node) { |
| android_node->OnDataChanged(); |
| return; |
| } |
| |
| // 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; |
| } |
| |
| 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->PlatformGetParent()) { |
| Java_BrowserAccessibilityManager_setAccessibilityNodeInfoParent( |
| env, obj, info, node->PlatformGetParent()->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::android::ConvertUTF16ToJavaString( |
| env, node->GetInheritedString16Attribute(ui::AX_ATTR_LANGUAGE))); |
| 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->PlatformGetParent()) { |
| gfx::Rect parent_rect = node->PlatformGetParent()->GetPageBoundsRect(); |
| parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin()); |
| } |
| bool is_root = node->PlatformGetParent() == 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()), |
| node->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_START), |
| node->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_END)); |
| |
| 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()->SetSelection(AXPlatformRange(node->CreatePositionAt(start), |
| node->CreatePositionAt(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->PlatformGetParent()); |
| } |
| |
| 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; |
| |
| int32_t element_id = tree_search.GetMatchAtIndex(0)->unique_id(); |
| |
| // Navigate forwards to the autofill popup's proxy node if focus is currently |
| // on the element hosting the autofill popup. Once within the popup, a back |
| // press will navigate back to the element hosting the popup. If user swipes |
| // past last suggestion in the popup, or swipes left from the first suggestion |
| // in the popup, we will navigate to the element that is the next element in |
| // the document after the element hosting the popup. |
| if (forwards && start_id == g_element_hosting_autofill_popup_unique_id && |
| g_autofill_popup_proxy_node) { |
| g_element_after_element_hosting_autofill_popup_unique_id = element_id; |
| return g_autofill_popup_proxy_node->unique_id(); |
| } |
| |
| return element_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) |
| 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; |
| } |
| |
| void BrowserAccessibilityManagerAndroid::OnAutofillPopupDisplayed( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj) { |
| if (!base::FeatureList::IsEnabled(features::kAndroidAutofillAccessibility)) |
| return; |
| |
| BrowserAccessibility* current_focus = GetFocus(); |
| if (current_focus == nullptr) { |
| return; |
| } |
| |
| DeleteAutofillPopupProxy(); |
| |
| g_autofill_popup_proxy_node = BrowserAccessibility::Create(); |
| g_autofill_popup_proxy_node_ax_node = new ui::AXNode(nullptr, -1, -1); |
| ui::AXNodeData ax_node_data; |
| ax_node_data.role = ui::AX_ROLE_MENU; |
| ax_node_data.SetName("Autofill"); |
| ax_node_data.AddState(ui::AX_STATE_READ_ONLY); |
| ax_node_data.AddState(ui::AX_STATE_FOCUSABLE); |
| ax_node_data.AddState(ui::AX_STATE_SELECTABLE); |
| g_autofill_popup_proxy_node_ax_node->SetData(ax_node_data); |
| g_autofill_popup_proxy_node->Init(this, g_autofill_popup_proxy_node_ax_node); |
| |
| g_element_hosting_autofill_popup_unique_id = current_focus->unique_id(); |
| } |
| |
| void BrowserAccessibilityManagerAndroid::OnAutofillPopupDismissed( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj) { |
| g_element_hosting_autofill_popup_unique_id = -1; |
| g_element_after_element_hosting_autofill_popup_unique_id = -1; |
| DeleteAutofillPopupProxy(); |
| } |
| |
| jint BrowserAccessibilityManagerAndroid:: |
| GetIdForElementAfterElementHostingAutofillPopup( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj) { |
| if (!base::FeatureList::IsEnabled(features::kAndroidAutofillAccessibility) || |
| g_element_after_element_hosting_autofill_popup_unique_id == -1 || |
| GetFromUniqueID( |
| g_element_after_element_hosting_autofill_popup_unique_id) == nullptr) |
| return 0; |
| |
| return g_element_after_element_hosting_autofill_popup_unique_id; |
| } |
| |
| jboolean BrowserAccessibilityManagerAndroid::IsAutofillPopupNode( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| jint id) { |
| return g_autofill_popup_proxy_node && |
| g_autofill_popup_proxy_node->unique_id() == id; |
| } |
| |
| 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); |
| } |
| |
| void BrowserAccessibilityManagerAndroid::CollectStats() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = GetJavaRefFromRootManager(); |
| if (obj.is_null()) |
| return; |
| |
| int event_type_mask = |
| Java_BrowserAccessibilityManager_getAccessibilityServiceEventTypeMask( |
| env, obj); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, ANNOUNCEMENT); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, ASSIST_READING_CONTEXT); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, GESTURE_DETECTION_END); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, GESTURE_DETECTION_START); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, NOTIFICATION_STATE_CHANGED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, TOUCH_EXPLORATION_GESTURE_END); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, TOUCH_EXPLORATION_GESTURE_START); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, TOUCH_INTERACTION_END); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, TOUCH_INTERACTION_START); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_ACCESSIBILITY_FOCUSED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_ACCESSIBILITY_FOCUS_CLEARED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_CLICKED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_CONTEXT_CLICKED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_FOCUSED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_HOVER_ENTER); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_HOVER_EXIT); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_LONG_CLICKED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_SCROLLED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_SELECTED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_TEXT_CHANGED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, VIEW_TEXT_SELECTION_CHANGED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, |
| VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, WINDOWS_CHANGED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, WINDOW_CONTENT_CHANGED); |
| EVENT_TYPE_HISTOGRAM(event_type_mask, WINDOW_STATE_CHANGED); |
| |
| int feedback_type_mask = |
| Java_BrowserAccessibilityManager_getAccessibilityServiceFeedbackTypeMask( |
| env, obj); |
| FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, SPOKEN); |
| FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, HAPTIC); |
| FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, AUDIBLE); |
| FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, VISUAL); |
| FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, GENERIC); |
| FEEDBACK_TYPE_HISTOGRAM(feedback_type_mask, BRAILLE); |
| |
| int flags_mask = |
| Java_BrowserAccessibilityManager_getAccessibilityServiceFlagsMask(env, |
| obj); |
| FLAGS_HISTOGRAM(flags_mask, INCLUDE_NOT_IMPORTANT_VIEWS); |
| FLAGS_HISTOGRAM(flags_mask, REQUEST_TOUCH_EXPLORATION_MODE); |
| FLAGS_HISTOGRAM(flags_mask, REQUEST_ENHANCED_WEB_ACCESSIBILITY); |
| FLAGS_HISTOGRAM(flags_mask, REPORT_VIEW_IDS); |
| FLAGS_HISTOGRAM(flags_mask, REQUEST_FILTER_KEY_EVENTS); |
| FLAGS_HISTOGRAM(flags_mask, RETRIEVE_INTERACTIVE_WINDOWS); |
| FLAGS_HISTOGRAM(flags_mask, FORCE_DIRECT_BOOT_AWARE); |
| |
| int capabilities_mask = |
| Java_BrowserAccessibilityManager_getAccessibilityServiceCapabilitiesMask( |
| env, obj); |
| CAPABILITY_TYPE_HISTOGRAM(capabilities_mask, CAN_RETRIEVE_WINDOW_CONTENT); |
| CAPABILITY_TYPE_HISTOGRAM(capabilities_mask, CAN_REQUEST_TOUCH_EXPLORATION); |
| CAPABILITY_TYPE_HISTOGRAM(capabilities_mask, |
| CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY); |
| CAPABILITY_TYPE_HISTOGRAM(capabilities_mask, CAN_REQUEST_FILTER_KEY_EVENTS); |
| CAPABILITY_TYPE_HISTOGRAM(capabilities_mask, CAN_CONTROL_MAGNIFICATION); |
| CAPABILITY_TYPE_HISTOGRAM(capabilities_mask, CAN_PERFORM_GESTURES); |
| } |
| |
| bool RegisterBrowserAccessibilityManager(JNIEnv* env) { |
| return RegisterNativesImpl(env); |
| } |
| |
| } // namespace content |