| // 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 "base/i18n/char_iterator.h" |
| #include "content/browser/accessibility/browser_accessibility_android.h" |
| #include "content/browser/accessibility/web_contents_accessibility_android.h" |
| #include "content/common/accessibility_messages.h" |
| #include "content/public/common/use_zoom_for_dsf_policy.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| |
| namespace content { |
| |
| // static |
| BrowserAccessibilityManager* BrowserAccessibilityManager::Create( |
| const ui::AXTreeUpdate& initial_tree, |
| BrowserAccessibilityDelegate* delegate, |
| BrowserAccessibilityFactory* factory) { |
| return new BrowserAccessibilityManagerAndroid(initial_tree, nullptr, delegate, |
| factory); |
| } |
| |
| BrowserAccessibilityManagerAndroid::BrowserAccessibilityManagerAndroid( |
| const ui::AXTreeUpdate& initial_tree, |
| WebContentsAccessibilityAndroid* web_contents_accessibility, |
| BrowserAccessibilityDelegate* delegate, |
| BrowserAccessibilityFactory* factory) |
| : BrowserAccessibilityManager(delegate, factory), |
| web_contents_accessibility_(web_contents_accessibility), |
| prune_tree_for_screen_reader_(true) { |
| if (web_contents_accessibility) |
| web_contents_accessibility->set_root_manager(this); |
| Initialize(initial_tree); |
| } |
| |
| BrowserAccessibilityManagerAndroid::~BrowserAccessibilityManagerAndroid() { |
| if (web_contents_accessibility_) |
| web_contents_accessibility_->set_root_manager(nullptr); |
| } |
| |
| // static |
| ui::AXTreeUpdate BrowserAccessibilityManagerAndroid::GetEmptyDocument() { |
| ui::AXNodeData empty_document; |
| empty_document.id = 0; |
| empty_document.role = ax::mojom::Role::kRootWebArea; |
| empty_document.SetRestriction(ax::mojom::Restriction::kReadOnly); |
| ui::AXTreeUpdate update; |
| update.root_id = empty_document.id; |
| update.nodes.push_back(empty_document); |
| return update; |
| } |
| |
| bool BrowserAccessibilityManagerAndroid::ShouldRespectDisplayedPasswordText() { |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| return wcax ? wcax->ShouldRespectDisplayedPasswordText() : false; |
| } |
| |
| bool BrowserAccessibilityManagerAndroid::ShouldExposePasswordText() { |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| return wcax ? wcax->ShouldExposePasswordText() : false; |
| } |
| |
| BrowserAccessibility* BrowserAccessibilityManagerAndroid::GetFocus() const { |
| BrowserAccessibility* focus = BrowserAccessibilityManager::GetFocus(); |
| if (focus && !focus->IsPlainTextField()) |
| return GetActiveDescendant(focus); |
| return focus; |
| } |
| |
| void BrowserAccessibilityManagerAndroid::FireFocusEvent( |
| BrowserAccessibility* node) { |
| BrowserAccessibilityManager::FireFocusEvent(node); |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| return; |
| BrowserAccessibilityAndroid* android_node = |
| static_cast<BrowserAccessibilityAndroid*>(node); |
| android_node->ResetContentInvalidTimer(); |
| wcax->HandleFocusChanged(android_node->unique_id()); |
| } |
| |
| void BrowserAccessibilityManagerAndroid::FireLocationChanged( |
| BrowserAccessibility* node) { |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| return; |
| |
| BrowserAccessibilityAndroid* android_node = |
| static_cast<BrowserAccessibilityAndroid*>(node); |
| wcax->HandleContentChanged(android_node->unique_id()); |
| } |
| |
| void BrowserAccessibilityManagerAndroid::FireBlinkEvent( |
| ax::mojom::Event event_type, |
| BrowserAccessibility* node) { |
| BrowserAccessibilityManager::FireBlinkEvent(event_type, node); |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| 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(); |
| BrowserAccessibilityAndroid* android_node = |
| static_cast<BrowserAccessibilityAndroid*>(node); |
| |
| switch (event_type) { |
| case ax::mojom::Event::kHover: |
| HandleHoverEvent(node); |
| break; |
| case ax::mojom::Event::kScrolledToAnchor: |
| wcax->HandleScrolledToAnchor(android_node->unique_id()); |
| break; |
| case ax::mojom::Event::kClicked: |
| wcax->HandleClicked(android_node->unique_id()); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void BrowserAccessibilityManagerAndroid::FireGeneratedEvent( |
| ui::AXEventGenerator::Event event_type, |
| BrowserAccessibility* node) { |
| BrowserAccessibilityManager::FireGeneratedEvent(event_type, node); |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| 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. |
| if (event_type != ui::AXEventGenerator::Event::SUBTREE_CREATED) |
| wcax->HandleContentChanged(android_node->unique_id()); |
| |
| switch (event_type) { |
| case ui::AXEventGenerator::Event::LOAD_COMPLETE: |
| if (node->manager() == GetRootManager()) { |
| auto* android_focused = |
| static_cast<BrowserAccessibilityAndroid*>(GetFocus()); |
| if (android_focused) |
| wcax->HandlePageLoaded(android_focused->unique_id()); |
| } |
| break; |
| case ui::AXEventGenerator::Event::CHECKED_STATE_CHANGED: |
| wcax->HandleCheckStateChanged(android_node->unique_id()); |
| break; |
| case ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED: |
| case ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED: |
| wcax->HandleScrollPositionChanged(android_node->unique_id()); |
| break; |
| case ui::AXEventGenerator::Event::ALERT: |
| // An alert is a special case of live region. Fall through to the |
| // next case to handle it. |
| case ui::AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED: { |
| // This event is fired when an object appears in a live region. |
| // Speak its text. |
| base::string16 text = android_node->GetInnerText(); |
| wcax->AnnounceLiveRegionText(text); |
| break; |
| } |
| case ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED: { |
| int32_t focus_id = ax_tree()->GetUnignoredSelection().focus_object_id; |
| BrowserAccessibility* focus_object = GetFromID(focus_id); |
| if (focus_object) { |
| BrowserAccessibilityAndroid* android_focus_object = |
| static_cast<BrowserAccessibilityAndroid*>(focus_object); |
| wcax->HandleTextSelectionChanged(android_focus_object->unique_id()); |
| } |
| break; |
| } |
| case ui::AXEventGenerator::Event::VALUE_CHANGED: |
| if (android_node->IsEditableText() && GetFocus() == node) { |
| wcax->HandleEditableTextChanged(android_node->unique_id()); |
| } else if (android_node->IsSlider()) { |
| wcax->HandleSliderChanged(android_node->unique_id()); |
| } |
| break; |
| case ui::AXEventGenerator::Event::ACCESS_KEY_CHANGED: |
| case ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED: |
| case ui::AXEventGenerator::Event::ATOMIC_CHANGED: |
| case ui::AXEventGenerator::Event::BUSY_CHANGED: |
| case ui::AXEventGenerator::Event::AUTO_COMPLETE_CHANGED: |
| case ui::AXEventGenerator::Event::CHILDREN_CHANGED: |
| case ui::AXEventGenerator::Event::CLASS_NAME_CHANGED: |
| case ui::AXEventGenerator::Event::COLLAPSED: |
| case ui::AXEventGenerator::Event::CONTROLS_CHANGED: |
| case ui::AXEventGenerator::Event::DESCRIBED_BY_CHANGED: |
| case ui::AXEventGenerator::Event::DESCRIPTION_CHANGED: |
| case ui::AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED: |
| case ui::AXEventGenerator::Event::DROPEFFECT_CHANGED: |
| case ui::AXEventGenerator::Event::EXPANDED: |
| case ui::AXEventGenerator::Event::ENABLED_CHANGED: |
| case ui::AXEventGenerator::Event::FOCUS_CHANGED: |
| case ui::AXEventGenerator::Event::FLOW_FROM_CHANGED: |
| case ui::AXEventGenerator::Event::FLOW_TO_CHANGED: |
| case ui::AXEventGenerator::Event::GRABBED_CHANGED: |
| case ui::AXEventGenerator::Event::HASPOPUP_CHANGED: |
| case ui::AXEventGenerator::Event::HIERARCHICAL_LEVEL_CHANGED: |
| case ui::AXEventGenerator::Event::IGNORED_CHANGED: |
| case ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED: |
| case ui::AXEventGenerator::Event::INVALID_STATUS_CHANGED: |
| case ui::AXEventGenerator::Event::KEY_SHORTCUTS_CHANGED: |
| case ui::AXEventGenerator::Event::LABELED_BY_CHANGED: |
| case ui::AXEventGenerator::Event::LANGUAGE_CHANGED: |
| case ui::AXEventGenerator::Event::LAYOUT_INVALIDATED: |
| case ui::AXEventGenerator::Event::LIVE_REGION_CHANGED: |
| case ui::AXEventGenerator::Event::LIVE_REGION_CREATED: |
| case ui::AXEventGenerator::Event::LIVE_RELEVANT_CHANGED: |
| case ui::AXEventGenerator::Event::LIVE_STATUS_CHANGED: |
| case ui::AXEventGenerator::Event::LOAD_START: |
| case ui::AXEventGenerator::Event::MENU_ITEM_SELECTED: |
| case ui::AXEventGenerator::Event::MULTILINE_STATE_CHANGED: |
| case ui::AXEventGenerator::Event::MULTISELECTABLE_STATE_CHANGED: |
| case ui::AXEventGenerator::Event::NAME_CHANGED: |
| case ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED: |
| case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED: |
| case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED: |
| case ui::AXEventGenerator::Event::READONLY_CHANGED: |
| case ui::AXEventGenerator::Event::RELATED_NODE_CHANGED: |
| case ui::AXEventGenerator::Event::REQUIRED_STATE_CHANGED: |
| case ui::AXEventGenerator::Event::ROLE_CHANGED: |
| case ui::AXEventGenerator::Event::ROW_COUNT_CHANGED: |
| case ui::AXEventGenerator::Event::SELECTED_CHANGED: |
| case ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED: |
| case ui::AXEventGenerator::Event::SET_SIZE_CHANGED: |
| case ui::AXEventGenerator::Event::SORT_CHANGED: |
| case ui::AXEventGenerator::Event::STATE_CHANGED: |
| case ui::AXEventGenerator::Event::SUBTREE_CREATED: |
| case ui::AXEventGenerator::Event::VALUE_MAX_CHANGED: |
| case ui::AXEventGenerator::Event::VALUE_MIN_CHANGED: |
| case ui::AXEventGenerator::Event::VALUE_STEP_CHANGED: |
| // 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) { |
| auto* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| return; |
| wcax->SendDelayedWindowContentChangedEvent(); |
| return; |
| } |
| BrowserAccessibilityManager::SendLocationChangeEvents(params); |
| } |
| |
| 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->GetInnerText(); |
| 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->GetInnerText(); |
| 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; |
| } |
| |
| bool BrowserAccessibilityManagerAndroid::OnHoverEvent( |
| const ui::MotionEventAndroid& event) { |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| return wcax ? wcax->OnHoverEvent(event) : false; |
| } |
| |
| void BrowserAccessibilityManagerAndroid::HandleHoverEvent( |
| BrowserAccessibility* node) { |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| 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) |
| wcax->HandleHover(android_node->unique_id()); |
| } |
| |
| gfx::Rect BrowserAccessibilityManagerAndroid::GetViewBounds() { |
| // We have to take the device scale factor into account on Android. |
| BrowserAccessibilityDelegate* delegate = GetDelegateFromRootManager(); |
| if (delegate) { |
| gfx::Rect bounds = delegate->AccessibilityGetViewBounds(); |
| if (IsUseZoomForDSFEnabled() && device_scale_factor() > 0.0 && |
| device_scale_factor() != 1.0) |
| bounds = ScaleToEnclosingRect(bounds, device_scale_factor()); |
| return bounds; |
| } |
| return gfx::Rect(); |
| } |
| |
| void BrowserAccessibilityManagerAndroid::OnAtomicUpdateFinished( |
| ui::AXTree* tree, |
| bool root_changed, |
| const std::vector<ui::AXTreeObserver::Change>& changes) { |
| BrowserAccessibilityManager::OnAtomicUpdateFinished(tree, root_changed, |
| changes); |
| |
| if (root_changed) { |
| WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager(); |
| if (!wcax) |
| return; |
| wcax->HandleNavigate(); |
| } |
| } |
| |
| bool BrowserAccessibilityManagerAndroid:: |
| UseRootScrollOffsetsWhenComputingBounds() { |
| // The Java layer handles the root scroll offset. |
| return false; |
| } |
| |
| WebContentsAccessibilityAndroid* |
| BrowserAccessibilityManagerAndroid::GetWebContentsAXFromRootManager() { |
| BrowserAccessibility* parent_node = GetParentNodeFromParentTree(); |
| if (!parent_node) |
| return web_contents_accessibility_; |
| |
| auto* parent_manager = |
| static_cast<BrowserAccessibilityManagerAndroid*>(parent_node->manager()); |
| return parent_manager->GetWebContentsAXFromRootManager(); |
| } |
| |
| } // namespace content |