| // Copyright 2017 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/ax_tree_source_android.h" |
| |
| #include <memory> |
| #include <stack> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/containers/adapters.h" |
| #include "base/dcheck_is_on.h" |
| #include "base/memory/raw_ptr.h" |
| #include "services/accessibility/android/accessibility_node_info_data_wrapper.h" |
| #include "services/accessibility/android/accessibility_window_info_data_wrapper.h" |
| #include "services/accessibility/android/android_accessibility_util.h" |
| #include "services/accessibility/android/auto_complete_handler.h" |
| #include "services/accessibility/android/drawer_layout_handler.h" |
| #include "services/accessibility/android/pane_title_handler.h" |
| #include "services/accessibility/android/public/mojom/accessibility_helper.mojom-shared.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_tree_source_checker.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace ax::android { |
| |
| using AXBooleanProperty = mojom::AccessibilityBooleanProperty; |
| using AXEventData = mojom::AccessibilityEventData; |
| using AXEventType = mojom::AccessibilityEventType; |
| using AXIntProperty = mojom::AccessibilityIntProperty; |
| using AXIntListProperty = mojom::AccessibilityIntListProperty; |
| using AXNodeInfoData = mojom::AccessibilityNodeInfoData; |
| using AXWindowBooleanProperty = mojom::AccessibilityWindowBooleanProperty; |
| using AXWindowInfoData = mojom::AccessibilityWindowInfoData; |
| using AXWindowIntListProperty = mojom::AccessibilityWindowIntListProperty; |
| |
| // TODO(hirokisato): Enable AXTreeAndroidSerializer's |crash_on_error| once |
| // Android becomes able to send reliable trees. |
| AXTreeSourceAndroid::AXTreeSourceAndroid( |
| Delegate* delegate, |
| std::unique_ptr<SerializationDelegate> serialization_delegate, |
| aura::Window* window) |
| : current_tree_serializer_( |
| new AXTreeAndroidSerializer(this, DCHECK_IS_ON())), |
| is_notification_(false), |
| is_input_method_window_(false), |
| window_(window), |
| delegate_(delegate), |
| serialization_delegate_(std::move(serialization_delegate)) { |
| CHECK(serialization_delegate_); |
| serialization_delegate_->BindTree(this); |
| } |
| |
| AXTreeSourceAndroid::~AXTreeSourceAndroid() { |
| Reset(); |
| } |
| |
| void AXTreeSourceAndroid::NotifyAccessibilityEvent(AXEventData* event_data) { |
| root_id_.reset(); |
| DCHECK(event_data); |
| |
| NotifyAccessibilityEventInternal(*event_data); |
| |
| // Clear maps in order to prevent invalid access from dead pointers. |
| tree_map_.clear(); |
| parent_map_.clear(); |
| computed_bounds_.clear(); |
| } |
| |
| void AXTreeSourceAndroid::NotifyActionResult(const ui::AXActionData& data, |
| bool result) { |
| GetAutomationEventRouter()->DispatchActionResult(data, result); |
| } |
| |
| void AXTreeSourceAndroid::NotifyGetTextLocationDataResult( |
| const ui::AXActionData& data, |
| const std::optional<gfx::Rect>& rect) { |
| GetAutomationEventRouter()->DispatchGetTextLocationDataResult(data, rect); |
| } |
| |
| bool AXTreeSourceAndroid::UseFullFocusMode() const { |
| return delegate_->UseFullFocusMode(); |
| } |
| |
| void AXTreeSourceAndroid::InvalidateTree() { |
| current_tree_serializer_->Reset(); |
| } |
| |
| bool AXTreeSourceAndroid::IsRootOfNodeTree(int32_t id) const { |
| const auto& node_it = tree_map_.find(id); |
| if (node_it == tree_map_.end()) { |
| return false; |
| } |
| |
| if (!node_it->second->IsNode()) { |
| return false; |
| } |
| |
| const auto& parent_it = parent_map_.find(id); |
| if (parent_it == parent_map_.end()) { |
| return true; |
| } |
| |
| const auto& parent_tree_it = tree_map_.find(parent_it->second); |
| CHECK(parent_tree_it != tree_map_.end()); |
| return !parent_tree_it->second->IsNode(); |
| } |
| |
| void AXTreeSourceAndroid::SetVirtualNode( |
| int32_t parent_id, |
| std::unique_ptr<AccessibilityInfoDataWrapper> child) { |
| auto* parent_node = GetFromId(parent_id); |
| // TODO support any node as a parent, not limiting to a window. |
| CHECK(parent_node); |
| CHECK(parent_node->GetWindow()); |
| |
| int32_t node_id = child->GetId(); |
| tree_map_[node_id] = std::move(child); |
| parent_map_[node_id] = parent_node->GetId(); |
| static_cast<AccessibilityWindowInfoDataWrapper*>(parent_node) |
| ->AddVirtualChild(node_id); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceAndroid::GetFirstImportantAncestor( |
| AccessibilityInfoDataWrapper* info_data) const { |
| AccessibilityInfoDataWrapper* parent = GetParent(info_data); |
| while (parent && parent->IsNode() && !parent->IsImportantInAndroid()) { |
| parent = GetParent(parent); |
| } |
| return parent; |
| } |
| |
| AccessibilityInfoDataWrapper* |
| AXTreeSourceAndroid::GetFirstAccessibilityFocusableAncestor( |
| AccessibilityInfoDataWrapper* info_data) const { |
| AccessibilityInfoDataWrapper* node = info_data; |
| while (node && !node->IsAccessibilityFocusableContainer()) { |
| node = GetParent(node); |
| } |
| return node; |
| } |
| |
| bool AXTreeSourceAndroid::GetTreeData(ui::AXTreeData* data) const { |
| data->tree_id = ax_tree_id(); |
| if (android_focused_id_.has_value()) { |
| data->focus_id = *android_focused_id_; |
| } |
| return true; |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceAndroid::GetRoot() const { |
| return root_id_.has_value() ? GetFromId(*root_id_) : nullptr; |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceAndroid::GetFromId(int32_t id) const { |
| auto it = tree_map_.find(id); |
| if (it == tree_map_.end()) { |
| return nullptr; |
| } |
| return it->second.get(); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceAndroid::GetParent( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (!info_data) { |
| return nullptr; |
| } |
| auto it = parent_map_.find(info_data->GetId()); |
| if (it != parent_map_.end()) { |
| return GetFromId(it->second); |
| } |
| return nullptr; |
| } |
| |
| void AXTreeSourceAndroid::SerializeNode(AccessibilityInfoDataWrapper* info_data, |
| ui::AXNodeData* out_data) const { |
| if (!info_data) { |
| return; |
| } |
| |
| info_data->Serialize(out_data); |
| |
| const auto& itr = hooks_.find(info_data->GetId()); |
| if (itr != hooks_.end()) { |
| itr->second->PostSerializeNode(out_data); |
| } |
| } |
| |
| void AXTreeSourceAndroid::BuildNodeMap(const AXEventData& event_data) { |
| // Prepare the wrapper objects of mojom data from Android. |
| CHECK(event_data.window_data); |
| root_id_ = event_data.window_data->at(0)->window_id; |
| for (const auto& window_ptr : *event_data.window_data) { |
| int32_t window_id = window_ptr->window_id; |
| int32_t root_node_id = window_ptr->root_node_id; |
| AXWindowInfoData* window = window_ptr.get(); |
| if (root_node_id) { |
| parent_map_[root_node_id] = window_id; |
| } |
| |
| tree_map_[window_id] = |
| std::make_unique<AccessibilityWindowInfoDataWrapper>(this, window); |
| |
| std::vector<int32_t> children; |
| if (GetProperty(window->int_list_properties, |
| AXWindowIntListProperty::CHILD_WINDOW_IDS, &children)) { |
| for (const int32_t child : children) { |
| DCHECK(child != root_id_); |
| parent_map_[child] = window_id; |
| } |
| } |
| } |
| |
| for (const auto& node_ptr : event_data.node_data) { |
| int32_t node_id = node_ptr->id; |
| AXNodeInfoData* node = node_ptr.get(); |
| tree_map_[node_id] = |
| std::make_unique<AccessibilityNodeInfoDataWrapper>(this, node); |
| |
| std::vector<int32_t> children; |
| if (GetProperty(node_ptr.get()->int_list_properties, |
| AXIntListProperty::CHILD_NODE_IDS, &children)) { |
| for (const int32_t child : children) { |
| parent_map_[child] = node_id; |
| } |
| } |
| } |
| } |
| |
| void AXTreeSourceAndroid::NotifyAccessibilityEventInternal( |
| const AXEventData& event_data) { |
| if (window_id_ != event_data.window_id) { |
| // Root window id is changed. Resetting variables that depends on id. |
| android_focused_id_.reset(); |
| hooks_.clear(); |
| window_id_ = event_data.window_id; |
| } |
| is_notification_ = event_data.notification_key.has_value(); |
| if (is_notification_) { |
| notification_key_ = event_data.notification_key; |
| } |
| is_input_method_window_ = event_data.is_input_method_window; |
| |
| BuildNodeMap(event_data); |
| |
| // Compute each node's bounds, based on its descendants. |
| // Assuming |nodeData| is in pre-order, compute cached bounds in post-order to |
| // avoid an O(n^2) amount of work as the computed bounds uses descendant |
| // bounds. |
| for (int i = event_data.node_data.size() - 1; i >= 0; --i) { |
| int32_t id = event_data.node_data[i]->id; |
| computed_bounds_[id] = ComputeEnclosingBounds(tree_map_[id].get()); |
| } |
| for (int i = event_data.window_data->size() - 1; i >= 0; --i) { |
| int32_t id = event_data.window_data->at(i)->window_id; |
| computed_bounds_[id] = ComputeEnclosingBounds(tree_map_[id].get()); |
| } |
| |
| if (!UpdateAndroidFocusedId(event_data)) { |
| // Exit this function if the focused node doesn't exist nor isn't visible. |
| return; |
| } |
| |
| AccessibilityInfoDataWrapper* const source_node = |
| GetFromId(event_data.source_id); |
| |
| std::vector<int32_t> update_ids = ProcessHooksOnEvent(event_data); |
| |
| // Prep the event and send it to automation. |
| AccessibilityInfoDataWrapper* focused_node = |
| android_focused_id_.has_value() ? GetFromId(*android_focused_id_) |
| : nullptr; |
| std::vector<ui::AXEvent> events; |
| const std::optional<ax::mojom::Event> event_type = |
| ToAXEvent(event_data.event_type, source_node, focused_node); |
| if (event_type) { |
| ui::AXEvent event; |
| event.event_type = event_type.value(); |
| event.id = event_data.source_id; |
| |
| int event_from_action; |
| if (GetProperty(event_data.int_properties, |
| ax::android::mojom::AccessibilityEventIntProperty::ACTION, |
| &event_from_action)) { |
| event.event_from = ax::mojom::EventFrom::kAction; |
| |
| event.event_from_action = ConvertToChromeAction( |
| static_cast<mojom::AccessibilityActionType>(event_from_action)); |
| } |
| |
| events.push_back(std::move(event)); |
| } |
| |
| if (event_data.event_type == AXEventType::WINDOW_STATE_CHANGED) { |
| // On event type of WINDOW_STATE_CHANGED, update the entire tree so that |
| // window location is correctly calculated. |
| update_ids.push_back(*root_id_); |
| } else if (!UseFullFocusMode()) { |
| update_ids.push_back(event_data.source_id); |
| } else { |
| // Otherwise, update subtree under the event source. |
| // If the event source is ignored, it's possible that the name is used by |
| // ancestor. |
| AccessibilityInfoDataWrapper* parent = |
| GetFirstAccessibilityFocusableAncestor(source_node); |
| update_ids.push_back(parent ? parent->GetId() : event_data.source_id); |
| } |
| |
| for (const int32_t update_id : update_ids) { |
| current_tree_serializer_->MarkSubtreeDirty(update_id); |
| } |
| |
| // Serialize updates in the reverse order of |update_ids|. |
| // Updates from Android event first as this contains the entire tree |
| // information, including focus. |
| std::vector<ui::AXTreeUpdate> updates; |
| for (const int32_t update_id : base::Reversed(update_ids)) { |
| AccessibilityInfoDataWrapper* update_root = GetFromId(update_id); |
| if (!update_root) { |
| LOG(ERROR) << "Update root node doesn't exist, id=" << update_id; |
| continue; |
| } |
| ui::AXTreeUpdate update; |
| if (!current_tree_serializer_->SerializeChanges(update_root, &update)) { |
| std::string error_string; |
| ui::AXTreeSourceChecker<AccessibilityInfoDataWrapper*> checker(this); |
| checker.CheckAndGetErrorString(&error_string); |
| |
| LOG(ERROR) << "Unable to serialize accessibility event\n" |
| << "Error: " << error_string << "\n" |
| << "Update: " << update.ToString(); |
| } else { |
| updates.push_back(std::move(update)); |
| } |
| } |
| |
| GetAutomationEventRouter()->DispatchAccessibilityEvents( |
| ax_tree_id(), std::move(updates), gfx::Point(), std::move(events)); |
| } |
| |
| extensions::AutomationEventRouterInterface* |
| AXTreeSourceAndroid::GetAutomationEventRouter() const { |
| if (automation_event_router_for_test_) { |
| return automation_event_router_for_test_; |
| } |
| |
| return extensions::AutomationEventRouter::GetInstance(); |
| } |
| |
| gfx::Rect AXTreeSourceAndroid::ComputeEnclosingBounds( |
| AccessibilityInfoDataWrapper* info_data) const { |
| DCHECK(info_data); |
| gfx::Rect computed_bounds; |
| // Exit early if the node or window is invisible. |
| if (!info_data->IsVisibleToUser()) { |
| return computed_bounds; |
| } |
| |
| ComputeEnclosingBoundsInternal(info_data, &computed_bounds); |
| return computed_bounds; |
| } |
| |
| void AXTreeSourceAndroid::ComputeEnclosingBoundsInternal( |
| AccessibilityInfoDataWrapper* info_data, |
| gfx::Rect* computed_bounds) const { |
| DCHECK(computed_bounds); |
| auto cached_bounds = computed_bounds_.find(info_data->GetId()); |
| if (cached_bounds != computed_bounds_.end()) { |
| computed_bounds->Union(cached_bounds->second); |
| return; |
| } |
| |
| if (!info_data->IsVisibleToUser()) { |
| return; |
| } |
| // Only consider nodes that can possibly be accessibility focused. |
| if (info_data->IsFocusableInFullFocusMode()) { |
| computed_bounds->Union(info_data->GetBounds()); |
| } |
| |
| // NOTE: |AXTreeSourceAndroid::GetChildren| depends on ComputeEnclosingBounds. |
| // To get children, directly call wrapper's GetChildren here. |
| std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>> |
| children; |
| info_data->GetChildren(&children); |
| for (AccessibilityInfoDataWrapper* child : children) { |
| ComputeEnclosingBoundsInternal(child, computed_bounds); |
| } |
| return; |
| } |
| |
| AccessibilityInfoDataWrapper* |
| AXTreeSourceAndroid::FindFirstFocusableNodeInFullFocusMode( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (!info_data) { |
| return nullptr; |
| } |
| |
| if (info_data->IsVisibleToUser() && info_data->IsFocusableInFullFocusMode()) { |
| return info_data; |
| } |
| |
| for (AccessibilityInfoDataWrapper* child : GetChildren(info_data)) { |
| AccessibilityInfoDataWrapper* candidate = |
| FindFirstFocusableNodeInFullFocusMode(child); |
| if (candidate) { |
| return candidate; |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| bool AXTreeSourceAndroid::UpdateAndroidFocusedId( |
| const AXEventData& event_data) { |
| AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id); |
| if (source_node) { |
| AccessibilityInfoDataWrapper* source_window = |
| GetFromId(source_node->GetWindowId()); |
| if (!source_window || |
| !GetBooleanProperty(source_window->GetWindow(), |
| AXWindowBooleanProperty::FOCUSED)) { |
| // Don't update focus in this task for events from non-focused window. |
| return true; |
| } |
| } |
| |
| if (event_data.event_type == AXEventType::VIEW_FOCUSED) { |
| if (source_node && source_node->IsVisibleToUser() && |
| GetBooleanProperty(source_node->GetNode(), |
| AXBooleanProperty::FOCUSED)) { |
| // Sometimes Android sets focus on unfocusable node, e.g. ListView. |
| AccessibilityInfoDataWrapper* adjusted_node = |
| UseFullFocusMode() |
| ? FindFirstFocusableNodeInFullFocusMode(source_node) |
| : source_node; |
| if (adjusted_node) { |
| android_focused_id_ = adjusted_node->GetId(); |
| } |
| } |
| } else if (event_data.event_type == AXEventType::VIEW_ACCESSIBILITY_FOCUSED && |
| UseFullFocusMode()) { |
| if (source_node && source_node->IsVisibleToUser()) { |
| android_focused_id_ = source_node->GetId(); |
| } |
| } else if (event_data.event_type == |
| AXEventType::VIEW_ACCESSIBILITY_FOCUS_CLEARED && |
| UseFullFocusMode()) { |
| int event_from_action; |
| GetProperty(event_data.int_properties, |
| mojom::AccessibilityEventIntProperty::ACTION, |
| &event_from_action); |
| const mojom::AccessibilityActionType action = |
| static_cast<mojom::AccessibilityActionType>(event_from_action); |
| if (action != mojom::AccessibilityActionType::FOCUS && |
| action != mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS) { |
| android_focused_id_.reset(); |
| } |
| } else if (event_data.event_type == AXEventType::VIEW_SELECTED) { |
| // In Android, VIEW_SELECTED event is dispatched in the two cases below: |
| // 1. Changing a value in ProgressBar or TimePicker in Android P. |
| // 2. Selecting an item in the context of an AdapterView. |
| if (!source_node || !source_node->IsNode()) { |
| return false; |
| } |
| |
| AXNodeInfoData* node_info = source_node->GetNode(); |
| DCHECK(node_info); |
| |
| bool is_range_change = !node_info->range_info.is_null(); |
| if (!is_range_change) { |
| AccessibilityInfoDataWrapper* selected_node = |
| GetSelectedNodeInfoFromAdapterViewEvent(event_data, source_node); |
| if (!selected_node || !selected_node->IsVisibleToUser()) { |
| return false; |
| } |
| |
| android_focused_id_ = selected_node->GetId(); |
| } |
| } else if (event_data.event_type == AXEventType::WINDOW_STATE_CHANGED) { |
| // When accessibility window changed, a11y event of WINDOW_CONTENT_CHANGED |
| // is fired from Android multiple times. |
| // The event of WINDOW_STATE_CHANGED is fired only once for each window |
| // change and use it as a trigger to move the a11y focus to the first node. |
| AccessibilityInfoDataWrapper* new_focus = nullptr; |
| |
| // If the current window has ever been visited in the current task, try |
| // focus on the last focus node in this window. |
| // We do it for WINDOW_STATE_CHANGED event from a window or a root node. |
| bool from_root_or_window = (source_node && !source_node->IsNode()) || |
| IsRootOfNodeTree(event_data.source_id); |
| if (from_root_or_window) { |
| auto itr = window_id_to_last_focus_node_id_.find(event_data.window_id); |
| if (itr != window_id_to_last_focus_node_id_.end()) { |
| new_focus = GetFromId(itr->second); |
| } |
| } else if (UseFullFocusMode()) { |
| // Otherwise, try focus on the first focusable node. |
| new_focus = FindFirstFocusableNodeInFullFocusMode( |
| GetFromId(event_data.source_id)); |
| } |
| |
| if (new_focus) { |
| android_focused_id_ = new_focus->GetId(); |
| } |
| } |
| |
| if (!android_focused_id_ || !GetFromId(*android_focused_id_)) { |
| // Because we only handle events from the focused window, let's reset the |
| // focus to the root window. |
| AccessibilityInfoDataWrapper* root = GetRoot(); |
| CHECK(root); |
| android_focused_id_ = root_id_; |
| } |
| |
| if (android_focused_id_.has_value()) { |
| window_id_to_last_focus_node_id_[event_data.window_id] = |
| *android_focused_id_; |
| } else { |
| window_id_to_last_focus_node_id_.erase(event_data.window_id); |
| } |
| |
| AccessibilityInfoDataWrapper* focused_node = |
| android_focused_id_.has_value() ? GetFromId(*android_focused_id_) |
| : nullptr; |
| |
| // Ensure that the focused node correctly gets focus. |
| while (focused_node && focused_node->IsIgnored()) { |
| AccessibilityInfoDataWrapper* parent = GetParent(focused_node); |
| if (parent) { |
| android_focused_id_ = parent->GetId(); |
| focused_node = parent; |
| } else { |
| // Unable to find the appropriate focus. Removing the focused node. |
| android_focused_id_.reset(); |
| focused_node = nullptr; |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| std::vector<int32_t> AXTreeSourceAndroid::ProcessHooksOnEvent( |
| const AXEventData& event_data) { |
| base::EraseIf(hooks_, [this](const auto& it) { |
| return it.second->ShouldDestroy(this); |
| }); |
| |
| std::vector<int32_t> serialization_needed_ids; |
| for (const auto& modifier : hooks_) { |
| if (modifier.second->PreDispatchEvent(this, event_data)) { |
| serialization_needed_ids.push_back(modifier.first); |
| } |
| } |
| |
| // Add new hook implementations if necessary. |
| auto drawer_layout_hook = |
| DrawerLayoutHandler::CreateIfNecessary(this, event_data); |
| if (drawer_layout_hook.has_value()) { |
| hooks_.insert(std::move(*drawer_layout_hook)); |
| } |
| |
| auto auto_complete_hooks = |
| AutoCompleteHandler::CreateIfNecessary(this, event_data); |
| for (auto& modifier : auto_complete_hooks) { |
| if (hooks_.count(modifier.first) == 0) { |
| hooks_.insert(std::move(modifier)); |
| } |
| } |
| |
| auto pane_title_hook = PaneTitleHandler::CreateIfNecessary(this, event_data); |
| if (pane_title_hook) { |
| hooks_.insert(std::move(*pane_title_hook)); |
| } |
| |
| return serialization_needed_ids; |
| } |
| |
| void AXTreeSourceAndroid::Reset() { |
| tree_map_.clear(); |
| parent_map_.clear(); |
| computed_bounds_.clear(); |
| current_tree_serializer_ = std::make_unique<AXTreeAndroidSerializer>(this); |
| root_id_.reset(); |
| window_id_.reset(); |
| android_focused_id_.reset(); |
| extensions::AutomationEventRouterInterface* router = |
| GetAutomationEventRouter(); |
| if (!router) { |
| return; |
| } |
| |
| router->DispatchTreeDestroyedEvent(ax_tree_id()); |
| } |
| |
| bool AXTreeSourceAndroid::NeedReorder( |
| AccessibilityInfoDataWrapper* left, |
| AccessibilityInfoDataWrapper* right) const { |
| auto left_bounds = ComputeEnclosingBounds(left); |
| auto right_bounds = ComputeEnclosingBounds(right); |
| return !CompareBounds(left_bounds, right_bounds) && |
| CompareBounds(left->GetBounds(), right->GetBounds()); |
| } |
| |
| bool AXTreeSourceAndroid::CompareBounds(const gfx::Rect& left, |
| const gfx::Rect& right) const { |
| if (left.IsEmpty() || right.IsEmpty()) { |
| return true; |
| } |
| |
| // Non-intersecting vertical check. |
| if (left.bottom() <= right.y()) { |
| return true; |
| } |
| |
| if (left.y() >= right.bottom()) { |
| return false; |
| } |
| |
| // Vertically overlapping. Left one comes first. |
| // TODO consider right-to-left |
| int left_difference = left.x() - right.x(); |
| if (left_difference != 0) { |
| return left_difference < 0; |
| } |
| |
| // Top to bottom. |
| int top_difference = left.y() - right.y(); |
| if (top_difference != 0) { |
| return top_difference < 0; |
| } |
| |
| // Larger to smaller. |
| int height_difference = left.height() - right.height(); |
| if (height_difference != 0) { |
| return height_difference > 0; |
| } |
| |
| int width_difference = left.width() - right.width(); |
| if (width_difference != 0) { |
| return width_difference > 0; |
| } |
| |
| // The rects are equal. Respect the original order. |
| return true; |
| } |
| |
| int32_t AXTreeSourceAndroid::GetId( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (!info_data) { |
| return ui::kInvalidAXNodeID; |
| } |
| return info_data->GetId(); |
| } |
| |
| void AXTreeSourceAndroid::CacheChildrenIfNeeded( |
| AccessibilityInfoDataWrapper* info_data) { |
| ComputeAndCacheChildren(info_data); |
| } |
| |
| size_t AXTreeSourceAndroid::GetChildCount( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (!info_data) { |
| return 0; |
| } |
| DCHECK(info_data->cached_children_); |
| return info_data->cached_children_->size(); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceAndroid::ChildAt( |
| AccessibilityInfoDataWrapper* info_data, |
| size_t i) const { |
| DCHECK(info_data->cached_children_); |
| DCHECK(i >= 0 && i < info_data->cached_children_->size()); |
| return (*info_data->cached_children_)[i]; |
| } |
| |
| // We don't need to handle cache clearing here, because each |
| // AccessibilityInfoDataWrapper is created during |
| // AXTreeSourceAndroid::NotifyAccessibilityEvent(), and destructed at the end of |
| // it that method. |
| void AXTreeSourceAndroid::ClearChildCache( |
| AccessibilityInfoDataWrapper* info_data) {} |
| |
| bool AXTreeSourceAndroid::IsIgnored( |
| AccessibilityInfoDataWrapper* info_data) const { |
| return false; |
| } |
| |
| bool AXTreeSourceAndroid::IsEqual( |
| AccessibilityInfoDataWrapper* info_data1, |
| AccessibilityInfoDataWrapper* info_data2) const { |
| if (!info_data1 || !info_data2) { |
| return false; |
| } |
| return info_data1->GetId() == info_data2->GetId(); |
| } |
| |
| AccessibilityInfoDataWrapper* AXTreeSourceAndroid::GetNull() const { |
| return nullptr; |
| } |
| |
| void AXTreeSourceAndroid::PerformAction(const ui::AXActionData& data) { |
| delegate_->OnAction(data); |
| } |
| |
| std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>& |
| AXTreeSourceAndroid::GetChildren( |
| AccessibilityInfoDataWrapper* info_data) const { |
| DCHECK(info_data); |
| ComputeAndCacheChildren(info_data); |
| return info_data->cached_children_.value(); |
| } |
| |
| void AXTreeSourceAndroid::ComputeAndCacheChildren( |
| AccessibilityInfoDataWrapper* info_data) const { |
| if (info_data->cached_children_) { |
| return; |
| } |
| |
| std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>& |
| children = info_data->cached_children_.emplace(); |
| |
| info_data->GetChildren(&children); |
| if (children.size() < 2) { |
| return; |
| } |
| |
| // We sort output nodes only in full focus mode. |
| if (!UseFullFocusMode() || info_data->IsWebNode()) { |
| return; |
| } |
| |
| // Also don't sort for virtual nodes (e.g. WebView). |
| for (const AccessibilityInfoDataWrapper* child : children) { |
| if (child->IsWebNode()) { |
| return; |
| } |
| } |
| |
| // This is a kind of bubble sort, but we reorder nodes only when the original |
| // node bounds (which is from node->GetBounds()) and child enclosing bounds |
| // (which is from ComputeEnclosingBounds()) are different. |
| // This algorithm takes O(N^2) time, but we practically don't expect that |
| // there's a node that contains hundreds of child nodes that require |
| // reordering. |
| // |
| // The concept here is taken from Android accessibility's similar logic in |
| // com.google.android.accessibility.utils.traversal.ReorderedChildrenIterator. |
| // |
| // Note that NeedReorder method is not transitive, so we cannot sort with it. |
| // For example, consider bounds below: |
| // a = (0,11)-(10x10) |
| // b = (20,5)-(10x10) |
| // c = (40,0)-(10x10) |
| // Here, NeedReorder(a, b) = false, NeedReorder(b, c) = false, but |
| // NeedReorder(a, c) = true. |
| |
| for (int i = children.size() - 2; i >= 0; i--) { |
| auto original_bounds = children.at(i)->GetBounds(); |
| auto enclosing_bounds = ComputeEnclosingBounds(children.at(i)); |
| if (original_bounds == enclosing_bounds) { |
| continue; |
| } |
| |
| // move the current node to be visited later if necessary. |
| for (size_t j = i; j + 1 < children.size() && |
| NeedReorder(children.at(j), children.at(j + 1)); |
| j++) { |
| std::swap(children.at(j), children.at(j + 1)); |
| } |
| } |
| } |
| |
| } // namespace ax::android |