blob: 0b0c3b10520e37412662b3a1678c799b3347bdf3 [file] [log] [blame]
// 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,
&current_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