| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/accessibility/android/android_accessibility_util.h" |
| |
| #include <optional> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/notreached.h" |
| #include "services/accessibility/android/accessibility_info_data_wrapper.h" |
| #include "services/accessibility/android/public/mojom/accessibility_helper.mojom-shared.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| |
| namespace ax::android { |
| |
| using AXBooleanProperty = mojom::AccessibilityBooleanProperty; |
| using AXEventIntProperty = mojom::AccessibilityEventIntProperty; |
| using AXIntProperty = mojom::AccessibilityIntProperty; |
| using AXNodeInfoData = mojom::AccessibilityNodeInfoData; |
| |
| std::optional<ax::mojom::Event> ToAXEvent( |
| mojom::AccessibilityEventType android_event_type, |
| AccessibilityInfoDataWrapper* source_node, |
| AccessibilityInfoDataWrapper* focused_node) { |
| switch (android_event_type) { |
| case mojom::AccessibilityEventType::VIEW_FOCUSED: |
| case mojom::AccessibilityEventType::VIEW_ACCESSIBILITY_FOCUSED: |
| return ax::mojom::Event::kFocus; |
| case mojom::AccessibilityEventType::VIEW_ACCESSIBILITY_FOCUS_CLEARED: |
| return ax::mojom::Event::kBlur; |
| case mojom::AccessibilityEventType::VIEW_CLICKED: |
| case mojom::AccessibilityEventType::VIEW_LONG_CLICKED: |
| return ax::mojom::Event::kClicked; |
| case mojom::AccessibilityEventType::VIEW_TEXT_CHANGED: |
| return std::nullopt; |
| case mojom::AccessibilityEventType::VIEW_TEXT_SELECTION_CHANGED: |
| return ax::mojom::Event::kTextSelectionChanged; |
| case mojom::AccessibilityEventType::WINDOW_STATE_CHANGED: { |
| if (focused_node) { |
| return ax::mojom::Event::kFocus; |
| } else { |
| return std::nullopt; |
| } |
| } |
| case mojom::AccessibilityEventType::WINDOW_CONTENT_CHANGED: |
| int live_region_type_int; |
| if (source_node && source_node->GetNode() && |
| GetProperty(source_node->GetNode()->int_properties, |
| AXIntProperty::LIVE_REGION, &live_region_type_int)) { |
| mojom::AccessibilityLiveRegionType live_region_type = |
| static_cast<mojom::AccessibilityLiveRegionType>( |
| live_region_type_int); |
| if (live_region_type != mojom::AccessibilityLiveRegionType::NONE) { |
| // Dispatch a kLiveRegionChanged event to ensure that all liveregions |
| // (inc. snackbar) will get announced. It is currently difficult to |
| // determine when liveregions need to be announced, in particular |
| // differentiaiting between when they first appear (vs text changed). |
| // This case is made evident with snackbar handling, which needs to be |
| // announced when it appears. |
| // TODO(b/187465133): Revisit this liveregion handling logic, once |
| // the talkback spec has been clarified. There is a proposal to write |
| // an API to expose attributes similar to aria-relevant, which will |
| // eventually allow liveregions to be handled similar to how it gets |
| // handled on the web. |
| return ax::mojom::Event::kLiveRegionChanged; |
| } |
| } |
| return std::nullopt; |
| case mojom::AccessibilityEventType::VIEW_HOVER_ENTER: |
| return ax::mojom::Event::kHover; |
| case mojom::AccessibilityEventType::ANNOUNCEMENT: { |
| // NOTE: Announcement event is handled in |
| // ArcAccessibilityHelperBridge::OnAccessibilityEvent. |
| NOTREACHED_IN_MIGRATION(); |
| break; |
| } |
| case mojom::AccessibilityEventType::VIEW_SCROLLED: |
| return ax::mojom::Event::kScrollPositionChanged; |
| case mojom::AccessibilityEventType::VIEW_SELECTED: { |
| // VIEW_SELECTED event is not selection event in Chrome. |
| // See the comment on AXTreeSourceAndroid::UpdateAndroidFocusedId. |
| if (source_node && source_node->IsNode() && |
| source_node->GetNode()->range_info) { |
| return std::nullopt; |
| } else { |
| return ax::mojom::Event::kFocus; |
| } |
| } |
| case mojom::AccessibilityEventType::INVALID_ENUM_VALUE: { |
| NOTREACHED_IN_MIGRATION(); |
| break; |
| } |
| case mojom::AccessibilityEventType::NOTIFICATION_STATE_CHANGED: |
| case mojom::AccessibilityEventType::VIEW_HOVER_EXIT: |
| case mojom::AccessibilityEventType::TOUCH_EXPLORATION_GESTURE_START: |
| case mojom::AccessibilityEventType::TOUCH_EXPLORATION_GESTURE_END: |
| case mojom::AccessibilityEventType:: |
| VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: |
| case mojom::AccessibilityEventType::GESTURE_DETECTION_START: |
| case mojom::AccessibilityEventType::GESTURE_DETECTION_END: |
| case mojom::AccessibilityEventType::TOUCH_INTERACTION_START: |
| case mojom::AccessibilityEventType::TOUCH_INTERACTION_END: |
| case mojom::AccessibilityEventType::WINDOWS_CHANGED: |
| case mojom::AccessibilityEventType::VIEW_CONTEXT_CLICKED: |
| case mojom::AccessibilityEventType::ASSIST_READING_CONTEXT: |
| return std::nullopt; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<mojom::AccessibilityActionType> ConvertToAndroidAction( |
| ax::mojom::Action action) { |
| switch (action) { |
| case ax::mojom::Action::kDoDefault: |
| return ax::android::mojom::AccessibilityActionType::CLICK; |
| case ax::mojom::Action::kFocus: |
| return ax::android::mojom::AccessibilityActionType::FOCUS; |
| case ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint: |
| return ax::android::mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS; |
| case ax::mojom::Action::kScrollToMakeVisible: |
| return ax::android::mojom::AccessibilityActionType::SHOW_ON_SCREEN; |
| case ax::mojom::Action::kScrollBackward: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_BACKWARD; |
| case ax::mojom::Action::kScrollForward: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_FORWARD; |
| case ax::mojom::Action::kScrollUp: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_UP; |
| case ax::mojom::Action::kScrollDown: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_DOWN; |
| case ax::mojom::Action::kScrollLeft: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_LEFT; |
| case ax::mojom::Action::kScrollRight: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_RIGHT; |
| case ax::mojom::Action::kScrollToPositionAtRowColumn: |
| return ax::android::mojom::AccessibilityActionType::SCROLL_TO_POSITION; |
| case ax::mojom::Action::kCustomAction: |
| return ax::android::mojom::AccessibilityActionType::CUSTOM_ACTION; |
| case ax::mojom::Action::kSetAccessibilityFocus: |
| return ax::android::mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS; |
| case ax::mojom::Action::kClearAccessibilityFocus: |
| return ax::android::mojom::AccessibilityActionType:: |
| CLEAR_ACCESSIBILITY_FOCUS; |
| case ax::mojom::Action::kGetTextLocation: |
| return ax::android::mojom::AccessibilityActionType::GET_TEXT_LOCATION; |
| case ax::mojom::Action::kShowTooltip: |
| return ax::android::mojom::AccessibilityActionType::SHOW_TOOLTIP; |
| case ax::mojom::Action::kHideTooltip: |
| return ax::android::mojom::AccessibilityActionType::HIDE_TOOLTIP; |
| case ax::mojom::Action::kCollapse: |
| return ax::android::mojom::AccessibilityActionType::COLLAPSE; |
| case ax::mojom::Action::kExpand: |
| return ax::android::mojom::AccessibilityActionType::EXPAND; |
| case ax::mojom::Action::kLongClick: |
| return ax::android::mojom::AccessibilityActionType::LONG_CLICK; |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| ax::mojom::Action ConvertToChromeAction( |
| const mojom::AccessibilityActionType action) { |
| switch (action) { |
| case ax::android::mojom::AccessibilityActionType::CLICK: |
| return ax::mojom::Action::kDoDefault; |
| case ax::android::mojom::AccessibilityActionType::FOCUS: |
| return ax::mojom::Action::kFocus; |
| case ax::android::mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS: |
| // TODO(hirokisato): there are multiple actions converted to |
| // ACCESSIBILITY_FOCUS. Consider if this is appropriate. |
| return ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint; |
| case ax::android::mojom::AccessibilityActionType::SHOW_ON_SCREEN: |
| return ax::mojom::Action::kScrollToMakeVisible; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_BACKWARD: |
| return ax::mojom::Action::kScrollBackward; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_FORWARD: |
| return ax::mojom::Action::kScrollForward; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_UP: |
| return ax::mojom::Action::kScrollUp; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_DOWN: |
| return ax::mojom::Action::kScrollDown; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_LEFT: |
| return ax::mojom::Action::kScrollLeft; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_RIGHT: |
| return ax::mojom::Action::kScrollRight; |
| case ax::android::mojom::AccessibilityActionType::CUSTOM_ACTION: |
| return ax::mojom::Action::kCustomAction; |
| case ax::android::mojom::AccessibilityActionType::CLEAR_ACCESSIBILITY_FOCUS: |
| return ax::mojom::Action::kClearAccessibilityFocus; |
| case ax::android::mojom::AccessibilityActionType::GET_TEXT_LOCATION: |
| return ax::mojom::Action::kGetTextLocation; |
| case ax::android::mojom::AccessibilityActionType::SHOW_TOOLTIP: |
| return ax::mojom::Action::kShowTooltip; |
| case ax::android::mojom::AccessibilityActionType::HIDE_TOOLTIP: |
| return ax::mojom::Action::kHideTooltip; |
| case ax::android::mojom::AccessibilityActionType::COLLAPSE: |
| return ax::mojom::Action::kCollapse; |
| case ax::android::mojom::AccessibilityActionType::EXPAND: |
| return ax::mojom::Action::kExpand; |
| case ax::android::mojom::AccessibilityActionType::LONG_CLICK: |
| return ax::mojom::Action::kLongClick; |
| case ax::android::mojom::AccessibilityActionType::SCROLL_TO_POSITION: |
| return ax::mojom::Action::kScrollToPositionAtRowColumn; |
| // Below are actions not mapped in ConvertToAndroidAction(). |
| case ax::android::mojom::AccessibilityActionType::CLEAR_FOCUS: |
| case ax::android::mojom::AccessibilityActionType::SELECT: |
| case ax::android::mojom::AccessibilityActionType::CLEAR_SELECTION: |
| case ax::android::mojom::AccessibilityActionType:: |
| NEXT_AT_MOVEMENT_GRANULARITY: |
| case ax::android::mojom::AccessibilityActionType:: |
| PREVIOUS_AT_MOVEMENT_GRANULARITY: |
| case ax::android::mojom::AccessibilityActionType::NEXT_HTML_ELEMENT: |
| case ax::android::mojom::AccessibilityActionType::PREVIOUS_HTML_ELEMENT: |
| case ax::android::mojom::AccessibilityActionType::COPY: |
| case ax::android::mojom::AccessibilityActionType::PASTE: |
| case ax::android::mojom::AccessibilityActionType::CUT: |
| case ax::android::mojom::AccessibilityActionType::SET_SELECTION: |
| case ax::android::mojom::AccessibilityActionType::DISMISS: |
| case ax::android::mojom::AccessibilityActionType::SET_TEXT: |
| case ax::android::mojom::AccessibilityActionType::CONTEXT_CLICK: |
| case ax::android::mojom::AccessibilityActionType::SET_PROGRESS: |
| return ax::mojom::Action::kNone; |
| case mojom::AccessibilityActionType::INVALID_ENUM_VALUE: |
| NOTREACHED_IN_MIGRATION(); |
| return ax::mojom::Action::kNone; |
| } |
| } |
| |
| AccessibilityInfoDataWrapper* GetSelectedNodeInfoFromAdapterViewEvent( |
| const mojom::AccessibilityEventData& event_data, |
| AccessibilityInfoDataWrapper* source_node) { |
| if (!source_node || !source_node->IsNode()) { |
| return nullptr; |
| } |
| |
| AXNodeInfoData* node_info = source_node->GetNode(); |
| if (!node_info) { |
| return nullptr; |
| } |
| |
| AccessibilityInfoDataWrapper* selected_node = source_node; |
| if (!node_info->collection_item_info) { |
| // The event source is not an item of AdapterView. If the event source is |
| // AdapterView, select the child. Otherwise, this is an unrelated event. |
| int item_count, from_index, current_item_index; |
| if (!GetProperty(event_data.int_properties, AXEventIntProperty::ITEM_COUNT, |
| &item_count) || |
| !GetProperty(event_data.int_properties, AXEventIntProperty::FROM_INDEX, |
| &from_index) || |
| !GetProperty(event_data.int_properties, |
| AXEventIntProperty::CURRENT_ITEM_INDEX, |
| ¤t_item_index)) { |
| return nullptr; |
| } |
| |
| int index = current_item_index - from_index; |
| if (index < 0) { |
| return nullptr; |
| } |
| |
| std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>> |
| children; |
| source_node->GetChildren(&children); |
| if (index >= static_cast<int>(children.size())) { |
| return nullptr; |
| } |
| |
| selected_node = children[index]; |
| } |
| |
| // Sometimes a collection item is wrapped by a non-focusable node. |
| // Find a node with focusable property. |
| while (selected_node && !GetBooleanProperty(selected_node->GetNode(), |
| AXBooleanProperty::FOCUSABLE)) { |
| std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>> |
| children; |
| selected_node->GetChildren(&children); |
| if (children.size() != 1) { |
| break; |
| } |
| selected_node = children[0]; |
| } |
| return selected_node; |
| } |
| |
| std::string ToLiveStatusString(mojom::AccessibilityLiveRegionType type) { |
| switch (type) { |
| case mojom::AccessibilityLiveRegionType::NONE: |
| return "off"; |
| case mojom::AccessibilityLiveRegionType::POLITE: |
| return "polite"; |
| case mojom::AccessibilityLiveRegionType::ASSERTIVE: |
| return "assertive"; |
| default: |
| NOTREACHED_IN_MIGRATION(); |
| } |
| return std::string(); // Placeholder. |
| } |
| |
| } // namespace ax::android |