| // Copyright 2017 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 "extensions/renderer/api/automation/automation_ax_tree_wrapper.h" |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/no_destructor.h" |
| #include "components/crash/core/common/crash_key.h" |
| #include "extensions/common/extension_messages.h" |
| #include "extensions/renderer/api/automation/automation_api_util.h" |
| #include "extensions/renderer/api/automation/automation_internal_custom_bindings.h" |
| #include "ui/accessibility/ax_language_detection.h" |
| #include "ui/accessibility/ax_node_position.h" |
| |
| namespace extensions { |
| |
| // Multiroot tree lookup. |
| // Represents an app node. |
| struct AppNodeInfo { |
| ui::AXTreeID tree_id; |
| int32_t node_id; |
| }; |
| |
| // These maps support moving from a node to a descendant tree node via an app id |
| // (and vice versa). |
| std::map<std::string, std::pair<ui::AXTreeID, int32_t>>& |
| GetAppIDToParentTreeNodeMap() { |
| static base::NoDestructor< |
| std::map<std::string, std::pair<ui::AXTreeID, int32_t>>> |
| app_id_to_tree_node_map; |
| return *app_id_to_tree_node_map; |
| } |
| |
| std::map<std::string, std::vector<AppNodeInfo>>& GetAppIDToTreeNodeMap() { |
| static base::NoDestructor<std::map<std::string, std::vector<AppNodeInfo>>> |
| app_id_to_tree_node_map; |
| return *app_id_to_tree_node_map; |
| } |
| |
| AutomationAXTreeWrapper::AutomationAXTreeWrapper( |
| ui::AXTreeID tree_id, |
| AutomationInternalCustomBindings* owner) |
| : ui::AXTreeManager(tree_id, std::make_unique<ui::AXTree>()), |
| owner_(owner), |
| event_generator_(ax_tree()) {} |
| |
| AutomationAXTreeWrapper::~AutomationAXTreeWrapper() = default; |
| |
| // static |
| AutomationAXTreeWrapper* AutomationAXTreeWrapper::GetParentOfTreeId( |
| ui::AXTreeID tree_id) { |
| std::map<ui::AXTreeID, AutomationAXTreeWrapper*>& child_tree_id_reverse_map = |
| GetChildTreeIDReverseMap(); |
| const auto& iter = child_tree_id_reverse_map.find(tree_id); |
| if (iter != child_tree_id_reverse_map.end()) |
| return iter->second; |
| |
| return nullptr; |
| } |
| |
| bool AutomationAXTreeWrapper::OnAccessibilityEvents( |
| const ExtensionMsg_AccessibilityEventBundleParams& event_bundle, |
| bool is_active_profile) { |
| TRACE_EVENT0("accessibility", |
| "AutomationAXTreeWrapper::OnAccessibilityEvents"); |
| |
| absl::optional<gfx::Rect> previous_accessibility_focused_global_bounds = |
| owner_->GetAccessibilityFocusedLocation(); |
| |
| std::map<ui::AXTreeID, AutomationAXTreeWrapper*>& child_tree_id_reverse_map = |
| GetChildTreeIDReverseMap(); |
| const auto& child_tree_ids = ax_tree_->GetAllChildTreeIds(); |
| |
| // Invalidate any reverse child tree id mappings. Note that it is possible |
| // there are no entries in this map for a given child tree to |this|, if this |
| // is the first event from |this| tree or if |this| was destroyed and (and |
| // then reset). |
| base::EraseIf(child_tree_id_reverse_map, [child_tree_ids](auto& pair) { |
| return child_tree_ids.count(pair.first); |
| }); |
| |
| // Unserialize all incoming data. |
| for (const auto& update : event_bundle.updates) { |
| deleted_node_ids_.clear(); |
| did_send_tree_change_during_unserialization_ = false; |
| |
| if (!ax_tree_->Unserialize(update)) { |
| static crash_reporter::CrashKeyString<4> crash_key( |
| "ax-tree-wrapper-unserialize-failed"); |
| crash_key.Set("yes"); |
| event_generator_.ClearEvents(); |
| return false; |
| } |
| |
| if (is_active_profile) { |
| owner_->SendNodesRemovedEvent(ax_tree(), deleted_node_ids_); |
| |
| if (update.nodes.size() && did_send_tree_change_during_unserialization_) { |
| owner_->SendTreeChangeEvent( |
| api::automation::TREE_CHANGE_TYPE_SUBTREEUPDATEEND, ax_tree(), |
| ax_tree_->root()); |
| } |
| } |
| } |
| |
| // Refresh child tree id mappings. |
| for (const ui::AXTreeID& tree_id : ax_tree_->GetAllChildTreeIds()) { |
| DCHECK(!base::Contains(child_tree_id_reverse_map, tree_id)); |
| child_tree_id_reverse_map.insert(std::make_pair(tree_id, this)); |
| } |
| |
| // Exit early if this isn't the active profile. |
| if (!is_active_profile) { |
| event_generator_.ClearEvents(); |
| return true; |
| } |
| |
| // Perform language detection first thing if we see a load complete event. |
| // We have to run *before* we send the load complete event to javascript |
| // otherwise code which runs immediately on load complete will not be able |
| // to see the results of language detection. |
| // |
| // Currently language detection only runs once for initial load complete, any |
| // content loaded after this will not have language detection performed for |
| // it. |
| for (const auto& event : event_bundle.events) { |
| if (event.event_type == ax::mojom::Event::kLoadComplete) { |
| ax_tree_->language_detection_manager->DetectLanguages(); |
| ax_tree_->language_detection_manager->LabelLanguages(); |
| |
| // After initial language detection, enable language detection for future |
| // content updates in order to support dynamic content changes. |
| // |
| // If the LanguageDetectionDynamic feature flag is not enabled then this |
| // is a no-op. |
| ax_tree_->language_detection_manager->RegisterLanguageDetectionObserver(); |
| |
| break; |
| } |
| } |
| |
| // Send all blur and focus events first. |
| owner_->MaybeSendFocusAndBlur(this, event_bundle); |
| |
| // Send auto-generated AXEventGenerator events. |
| for (const auto& targeted_event : event_generator_) { |
| if (ShouldIgnoreGeneratedEvent(targeted_event.event_params.event)) |
| continue; |
| ui::AXEvent generated_event; |
| generated_event.id = targeted_event.node_id; |
| generated_event.event_from = targeted_event.event_params.event_from; |
| generated_event.event_from_action = |
| targeted_event.event_params.event_from_action; |
| generated_event.event_intents = targeted_event.event_params.event_intents; |
| owner_->SendAutomationEvent(event_bundle.tree_id, |
| event_bundle.mouse_location, generated_event, |
| targeted_event.event_params.event); |
| } |
| event_generator_.ClearEvents(); |
| |
| for (const auto& event : event_bundle.events) { |
| if (event.event_type == ax::mojom::Event::kFocus || |
| event.event_type == ax::mojom::Event::kBlur) |
| continue; |
| |
| // Send some events directly. |
| if (!ShouldIgnoreAXEvent(event.event_type)) { |
| owner_->SendAutomationEvent(event_bundle.tree_id, |
| event_bundle.mouse_location, event); |
| } |
| } |
| |
| if (previous_accessibility_focused_global_bounds.has_value() && |
| previous_accessibility_focused_global_bounds != |
| owner_->GetAccessibilityFocusedLocation()) { |
| owner_->SendAccessibilityFocusedLocationChange(event_bundle.mouse_location); |
| } |
| |
| return true; |
| } |
| |
| bool AutomationAXTreeWrapper::IsDesktopTree() const { |
| return ax_tree_->root() |
| ? ax_tree_->root()->GetRole() == ax::mojom::Role::kDesktop |
| : false; |
| } |
| |
| bool AutomationAXTreeWrapper::HasDeviceScaleFactor() const { |
| return ax_tree_->root() ? |
| // These are views-backed trees. |
| ax_tree_->root()->GetRole() != ax::mojom::Role::kDesktop && |
| ax_tree_->root()->GetRole() != ax::mojom::Role::kClient |
| : true; |
| } |
| |
| bool AutomationAXTreeWrapper::IsInFocusChain(int32_t node_id) { |
| if (ax_tree_->data().focus_id != node_id) |
| return false; |
| |
| if (IsDesktopTree()) |
| return true; |
| |
| AutomationAXTreeWrapper* descendant_tree = this; |
| ui::AXTreeID descendant_tree_id = GetTreeID(); |
| AutomationAXTreeWrapper* ancestor_tree = descendant_tree; |
| bool found = true; |
| while ((ancestor_tree = ancestor_tree->GetParentTree())) { |
| int32_t ancestor_tree_focus_id = ancestor_tree->ax_tree()->data().focus_id; |
| ui::AXNode* ancestor_tree_focused_node = |
| ancestor_tree->ax_tree()->GetFromId(ancestor_tree_focus_id); |
| if (!ancestor_tree_focused_node) |
| return false; |
| |
| if (ancestor_tree_focused_node->HasStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeNodeAppId)) { |
| // |ancestor_tree_focused_node| points to a tree with multiple roots as |
| // its child tree node. Ensure the node points back to |
| // |ancestor_tree_focused_node| as its parent. |
| ui::AXNode* parent_node = descendant_tree->GetParentTreeNodeForAppID( |
| ancestor_tree_focused_node->GetStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeNodeAppId), |
| owner_); |
| if (parent_node != ancestor_tree_focused_node) |
| return false; |
| } else if (ui::AXTreeID::FromString( |
| ancestor_tree_focused_node->GetStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeId)) != |
| descendant_tree_id && |
| ancestor_tree->ax_tree()->data().focused_tree_id != |
| descendant_tree_id) { |
| // Surprisingly, an ancestor frame can "skip" a child frame to point to a |
| // descendant granchild, so we have to scan upwards. |
| found = false; |
| continue; |
| } |
| |
| found = true; |
| |
| if (ancestor_tree->IsDesktopTree()) |
| return true; |
| |
| descendant_tree_id = ancestor_tree->GetTreeID(); |
| descendant_tree = ancestor_tree; |
| } |
| |
| // We can end up here if the tree is detached from any desktop. This can |
| // occur in tabs-only mode. This is also the codepath for frames with inner |
| // focus, but which are not focused by ancestor frames. |
| return found; |
| } |
| |
| ui::AXTree::Selection AutomationAXTreeWrapper::GetUnignoredSelection() { |
| return ax_tree_->GetUnignoredSelection(); |
| } |
| |
| ui::AXNode* AutomationAXTreeWrapper::GetUnignoredNodeFromId(int32_t id) { |
| ui::AXNode* node = ax_tree_->GetFromId(id); |
| return (node && !node->IsIgnored()) ? node : nullptr; |
| } |
| |
| void AutomationAXTreeWrapper::SetAccessibilityFocus(int32_t node_id) { |
| accessibility_focused_id_ = node_id; |
| } |
| |
| ui::AXNode* AutomationAXTreeWrapper::GetAccessibilityFocusedNode() { |
| return accessibility_focused_id_ == ui::kInvalidAXNodeID |
| ? nullptr |
| : ax_tree_->GetFromId(accessibility_focused_id_); |
| } |
| |
| AutomationAXTreeWrapper* AutomationAXTreeWrapper::GetParentTree() { |
| // Explicit parent tree from this tree's data. |
| auto* ret = GetParentOfTreeId(ax_tree_->data().tree_id); |
| |
| // If this tree has multiple roots, and no explicit parent tree, fallback to |
| // any node with a parent tree node app id to find a parent tree. |
| return ret ? ret : GetParentTreeFromAnyAppID(); |
| } |
| |
| AutomationAXTreeWrapper* |
| AutomationAXTreeWrapper::GetTreeWrapperWithUnignoredRoot() { |
| // The desktop is always unignored. |
| if (IsDesktopTree()) |
| return this; |
| |
| // Keep following these parent node id links upwards, since we want to ignore |
| // these roots for the api in js. |
| AutomationAXTreeWrapper* current = this; |
| AutomationAXTreeWrapper* parent = this; |
| while ((parent = current->GetParentTreeFromAnyAppID())) |
| current = parent; |
| |
| return current; |
| } |
| |
| AutomationAXTreeWrapper* AutomationAXTreeWrapper::GetParentTreeFromAnyAppID() { |
| for (const std::string& app_id : all_tree_node_app_ids_) { |
| auto* wrapper = GetParentTreeWrapperForAppID(app_id, owner_); |
| if (wrapper) |
| return wrapper; |
| } |
| |
| return nullptr; |
| } |
| |
| void AutomationAXTreeWrapper::EventListenerAdded( |
| api::automation::EventType event_type, |
| ui::AXNode* node) { |
| node_id_to_events_[node->id()].insert(event_type); |
| } |
| |
| void AutomationAXTreeWrapper::EventListenerRemoved( |
| api::automation::EventType event_type, |
| ui::AXNode* node) { |
| auto it = node_id_to_events_.find(node->id()); |
| if (it != node_id_to_events_.end()) { |
| it->second.erase(event_type); |
| if (it->second.empty()) |
| node_id_to_events_.erase(it); |
| } |
| } |
| |
| bool AutomationAXTreeWrapper::HasEventListener( |
| api::automation::EventType event_type, |
| ui::AXNode* node) { |
| auto it = node_id_to_events_.find(node->id()); |
| if (it == node_id_to_events_.end()) |
| return false; |
| |
| return it->second.count(event_type); |
| } |
| |
| size_t AutomationAXTreeWrapper::EventListenerCount() const { |
| return node_id_to_events_.size(); |
| } |
| |
| // static |
| std::map<ui::AXTreeID, AutomationAXTreeWrapper*>& |
| AutomationAXTreeWrapper::GetChildTreeIDReverseMap() { |
| static base::NoDestructor<std::map<ui::AXTreeID, AutomationAXTreeWrapper*>> |
| child_tree_id_reverse_map; |
| return *child_tree_id_reverse_map; |
| } |
| |
| // static |
| ui::AXNode* AutomationAXTreeWrapper::GetParentTreeNodeForAppID( |
| const std::string& app_id, |
| const AutomationInternalCustomBindings* owner) { |
| auto& map = GetAppIDToParentTreeNodeMap(); |
| auto it = map.find(app_id); |
| if (it == map.end()) |
| return nullptr; |
| |
| AutomationAXTreeWrapper* wrapper = |
| owner->GetAutomationAXTreeWrapperFromTreeID(it->second.first); |
| if (!wrapper) |
| return nullptr; |
| |
| return wrapper->ax_tree()->GetFromId(it->second.second); |
| } |
| |
| // static |
| AutomationAXTreeWrapper* AutomationAXTreeWrapper::GetParentTreeWrapperForAppID( |
| const std::string& app_id, |
| const AutomationInternalCustomBindings* owner) { |
| auto& map = GetAppIDToParentTreeNodeMap(); |
| auto it = map.find(app_id); |
| if (it == map.end()) |
| return nullptr; |
| |
| return owner->GetAutomationAXTreeWrapperFromTreeID(it->second.first); |
| } |
| |
| // static |
| std::vector<ui::AXNode*> AutomationAXTreeWrapper::GetChildTreeNodesForAppID( |
| const std::string& app_id, |
| const AutomationInternalCustomBindings* owner) { |
| auto& map = GetAppIDToTreeNodeMap(); |
| auto it = map.find(app_id); |
| if (it == map.end()) |
| return std::vector<ui::AXNode*>(); |
| |
| std::vector<ui::AXNode*> nodes; |
| for (const AppNodeInfo& app_node_info : it->second) { |
| AutomationAXTreeWrapper* wrapper = |
| owner->GetAutomationAXTreeWrapperFromTreeID(app_node_info.tree_id); |
| if (!wrapper) |
| continue; |
| |
| nodes.push_back(wrapper->ax_tree()->GetFromId(app_node_info.node_id)); |
| } |
| |
| return nodes; |
| } |
| |
| void AutomationAXTreeWrapper::OnNodeDataChanged( |
| ui::AXTree* tree, |
| const ui::AXNodeData& old_node_data, |
| const ui::AXNodeData& new_node_data) { |
| if (old_node_data.GetStringAttribute(ax::mojom::StringAttribute::kName) != |
| new_node_data.GetStringAttribute(ax::mojom::StringAttribute::kName)) |
| text_changed_node_ids_.push_back(new_node_data.id); |
| } |
| |
| void AutomationAXTreeWrapper::OnStringAttributeChanged( |
| ui::AXTree* tree, |
| ui::AXNode* node, |
| ax::mojom::StringAttribute attr, |
| const std::string& old_value, |
| const std::string& new_value) { |
| if (attr == ax::mojom::StringAttribute::kChildTreeNodeAppId) { |
| if (new_value.empty()) { |
| GetAppIDToParentTreeNodeMap().erase(old_value); |
| } else { |
| GetAppIDToParentTreeNodeMap()[new_value] = {tree->GetAXTreeID(), |
| node->data().id}; |
| } |
| } |
| |
| if (attr == ax::mojom::StringAttribute::kAppId) { |
| if (new_value.empty()) { |
| auto it = GetAppIDToTreeNodeMap().find(old_value); |
| if (it != GetAppIDToTreeNodeMap().end()) { |
| base::EraseIf(it->second, [node](const AppNodeInfo& app_node_info) { |
| return app_node_info.node_id == node->id(); |
| }); |
| if (it->second.empty()) { |
| GetAppIDToTreeNodeMap().erase(old_value); |
| all_tree_node_app_ids_.erase(old_value); |
| } |
| } |
| } else { |
| GetAppIDToTreeNodeMap()[new_value].push_back( |
| {tree->GetAXTreeID(), node->data().id}); |
| all_tree_node_app_ids_.insert(new_value); |
| } |
| } |
| } |
| |
| void AutomationAXTreeWrapper::OnNodeWillBeDeleted(ui::AXTree* tree, |
| ui::AXNode* node) { |
| did_send_tree_change_during_unserialization_ |= owner_->SendTreeChangeEvent( |
| api::automation::TREE_CHANGE_TYPE_NODEREMOVED, tree, node); |
| deleted_node_ids_.push_back(node->id()); |
| node_id_to_events_.erase(node->id()); |
| |
| if (node->HasStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeNodeAppId)) { |
| GetAppIDToParentTreeNodeMap().erase(node->GetStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeNodeAppId)); |
| } |
| |
| if (node->HasStringAttribute(ax::mojom::StringAttribute::kAppId)) { |
| const std::string& app_id = |
| node->GetStringAttribute(ax::mojom::StringAttribute::kAppId); |
| auto it = GetAppIDToTreeNodeMap().find(app_id); |
| if (it != GetAppIDToTreeNodeMap().end()) { |
| base::EraseIf(it->second, [node](const AppNodeInfo& app_node_info) { |
| return app_node_info.node_id == node->id(); |
| }); |
| |
| if (it->second.empty()) { |
| GetAppIDToTreeNodeMap().erase(app_id); |
| all_tree_node_app_ids_.erase(app_id); |
| } |
| } |
| } |
| } |
| |
| void AutomationAXTreeWrapper::OnNodeCreated(ui::AXTree* tree, |
| ui::AXNode* node) { |
| if (node->HasStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeNodeAppId)) { |
| GetAppIDToParentTreeNodeMap()[node->GetStringAttribute( |
| ax::mojom::StringAttribute::kChildTreeNodeAppId)] = { |
| node->tree()->GetAXTreeID(), node->id()}; |
| } |
| |
| if (node->HasStringAttribute(ax::mojom::StringAttribute::kAppId)) { |
| const std::string& app_id = |
| node->GetStringAttribute(ax::mojom::StringAttribute::kAppId); |
| GetAppIDToTreeNodeMap()[app_id].push_back( |
| {node->tree()->GetAXTreeID(), node->id()}); |
| all_tree_node_app_ids_.insert(app_id); |
| } |
| } |
| |
| void AutomationAXTreeWrapper::OnAtomicUpdateFinished( |
| ui::AXTree* tree, |
| bool root_changed, |
| const std::vector<ui::AXTreeObserver::Change>& changes) { |
| DCHECK_EQ(ax_tree(), tree); |
| for (const auto& change : changes) { |
| ui::AXNode* node = change.node; |
| switch (change.type) { |
| case NODE_CREATED: |
| did_send_tree_change_during_unserialization_ |= |
| owner_->SendTreeChangeEvent( |
| api::automation::TREE_CHANGE_TYPE_NODECREATED, tree, node); |
| break; |
| case SUBTREE_CREATED: |
| did_send_tree_change_during_unserialization_ |= |
| owner_->SendTreeChangeEvent( |
| api::automation::TREE_CHANGE_TYPE_SUBTREECREATED, tree, node); |
| break; |
| case NODE_CHANGED: |
| did_send_tree_change_during_unserialization_ |= |
| owner_->SendTreeChangeEvent( |
| api::automation::TREE_CHANGE_TYPE_NODECHANGED, tree, node); |
| break; |
| // Unhandled. |
| case NODE_REPARENTED: |
| case SUBTREE_REPARENTED: |
| break; |
| } |
| } |
| |
| for (int id : text_changed_node_ids_) { |
| did_send_tree_change_during_unserialization_ |= owner_->SendTreeChangeEvent( |
| api::automation::TREE_CHANGE_TYPE_TEXTCHANGED, tree, |
| tree->GetFromId(id)); |
| } |
| text_changed_node_ids_.clear(); |
| } |
| |
| void AutomationAXTreeWrapper::OnIgnoredChanged(ui::AXTree* tree, |
| ui::AXNode* node, |
| bool is_ignored_new_value) { |
| owner_->SendTreeChangeEvent( |
| is_ignored_new_value ? api::automation::TREE_CHANGE_TYPE_NODEREMOVED |
| : api::automation::TREE_CHANGE_TYPE_NODECREATED, |
| tree, node); |
| } |
| |
| bool AutomationAXTreeWrapper::IsTreeIgnored() { |
| // Check the hosting nodes within the parenting trees for ignored host nodes. |
| AutomationAXTreeWrapper* tree = this; |
| while (tree) { |
| ui::AXNode* host = owner_->GetHostInParentTree(&tree); |
| if (!host) |
| break; |
| |
| // This catches things like aria hidden on an iframe. |
| if (host->data().HasState(ax::mojom::State::kInvisible)) |
| return true; |
| } |
| return false; |
| } |
| |
| ui::AXNode* AutomationAXTreeWrapper::GetNodeFromTree( |
| const ui::AXTreeID tree_id, |
| const ui::AXNodeID node_id) const { |
| AutomationAXTreeWrapper* tree_wrapper = |
| owner_->GetAutomationAXTreeWrapperFromTreeID(tree_id); |
| return tree_wrapper ? tree_wrapper->GetNodeFromTree(node_id) : nullptr; |
| } |
| |
| ui::AXNode* AutomationAXTreeWrapper::GetNodeFromTree( |
| const ui::AXNodeID node_id) const { |
| return ax_tree_->GetFromId(node_id); |
| } |
| |
| ui::AXTreeID AutomationAXTreeWrapper::GetTreeID() const { |
| return ax_tree_id_; |
| } |
| |
| ui::AXTreeID AutomationAXTreeWrapper::GetParentTreeID() const { |
| AutomationAXTreeWrapper* parent_tree = GetParentOfTreeId(ax_tree_id_); |
| return parent_tree ? parent_tree->GetTreeID() : ui::AXTreeIDUnknown(); |
| } |
| |
| ui::AXNode* AutomationAXTreeWrapper::GetRootAsAXNode() const { |
| return ax_tree_->root(); |
| } |
| |
| ui::AXNode* AutomationAXTreeWrapper::GetParentNodeFromParentTreeAsAXNode() |
| const { |
| AutomationAXTreeWrapper* wrapper = const_cast<AutomationAXTreeWrapper*>(this); |
| return owner_->GetParent(ax_tree_->root(), &wrapper, |
| /* should_use_app_id = */ true, |
| /* requires_unignored = */ false); |
| } |
| |
| } // namespace extensions |