blob: 2a0539a2deb762f37bc35f5a7bd2ccd2c9bcf7fc [file] [log] [blame]
// Copyright 2019 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 "fuchsia/engine/browser/accessibility_bridge.h"
#include <lib/sys/cpp/component_context.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include "base/fuchsia/fuchsia_logging.h"
#include "base/logging.h"
#include "fuchsia/engine/browser/ax_tree_converter.h"
#include "ui/accessibility/ax_action_data.h"
using fuchsia::accessibility::semantics::SemanticTree;
namespace {
constexpr uint32_t kSemanticNodeRootId = 0;
// TODO(https://crbug.com/973095): Update this value based on average and
// maximum sizes of serialized Semantic Nodes.
constexpr size_t kMaxNodesPerUpdate = 16;
} // namespace
AccessibilityBridge::AccessibilityBridge(
fuchsia::accessibility::semantics::SemanticsManagerPtr semantics_manager,
fuchsia::ui::views::ViewRef view_ref,
content::WebContents* web_contents)
: binding_(this), web_contents_(web_contents) {
DCHECK(web_contents_);
Observe(web_contents_);
tree_.AddObserver(this);
semantics_manager->RegisterViewForSemantics(
std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest());
tree_ptr_.set_error_handler([](zx_status_t status) {
ZX_LOG_IF(ERROR, status != ZX_ERR_PEER_CLOSED, status)
<< "Semantic Tree disconnected.";
});
}
AccessibilityBridge::~AccessibilityBridge() {
// Acknowledge to the SemanticsManager if any actions have not been handled
// upon destruction time.
for (auto& callback : pending_hit_test_callbacks_) {
fuchsia::accessibility::semantics::Hit hit;
hit.set_node_id(kSemanticNodeRootId);
callback.second(std::move(hit));
}
pending_hit_test_callbacks_.clear();
for (auto& callback : pending_accessibility_action_callbacks_)
callback.second(false);
pending_accessibility_action_callbacks_.clear();
}
void AccessibilityBridge::TryCommit() {
if (commit_inflight_ || to_send_.empty())
return;
SemanticUpdateOrDelete::Type current = to_send_.at(0).type;
int range_start = 0;
for (size_t i = 1; i < to_send_.size(); i++) {
if (to_send_.at(i).type == current &&
(i - range_start < kMaxNodesPerUpdate)) {
continue;
} else {
DispatchSemanticsMessages(range_start, i - range_start);
current = to_send_.at(i).type;
range_start = i;
}
}
DispatchSemanticsMessages(range_start, to_send_.size() - range_start);
tree_ptr_->CommitUpdates(
fit::bind_member(this, &AccessibilityBridge::OnCommitComplete));
commit_inflight_ = true;
to_send_.clear();
}
void AccessibilityBridge::DispatchSemanticsMessages(size_t start, size_t size) {
if (to_send_.at(start).type == SemanticUpdateOrDelete::Type::UPDATE) {
std::vector<fuchsia::accessibility::semantics::Node> updates;
for (size_t i = start; i < start + size; i++) {
DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::UPDATE);
updates.push_back(std::move(to_send_.at(i).update_node));
}
tree_ptr_->UpdateSemanticNodes(std::move(updates));
} else if (to_send_.at(start).type == SemanticUpdateOrDelete::Type::DELETE) {
std::vector<uint32_t> deletes;
for (size_t i = start; i < start + size; i++) {
DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::DELETE);
deletes.push_back(to_send_.at(i).id_to_delete);
}
tree_ptr_->DeleteSemanticNodes(deletes);
}
}
AccessibilityBridge::SemanticUpdateOrDelete::SemanticUpdateOrDelete(
AccessibilityBridge::SemanticUpdateOrDelete&& m)
: type(m.type),
update_node(std::move(m.update_node)),
id_to_delete(m.id_to_delete) {}
AccessibilityBridge::SemanticUpdateOrDelete::SemanticUpdateOrDelete(
Type type,
fuchsia::accessibility::semantics::Node node,
uint32_t id_to_delete)
: type(type), update_node(std::move(node)), id_to_delete(id_to_delete) {}
void AccessibilityBridge::OnCommitComplete() {
commit_inflight_ = false;
TryCommit();
}
uint32_t AccessibilityBridge::ConvertToFuchsiaNodeId(int32_t ax_node_id) {
if (ax_node_id == root_id_) {
// On the Fuchsia side, the root node is indicated by id
// |kSemanticNodeRootId|, which is 0.
return kSemanticNodeRootId;
} else {
// AXNode ids are signed, Semantic Node ids are unsigned.
return bit_cast<uint32_t>(ax_node_id);
}
}
void AccessibilityBridge::AccessibilityEventReceived(
const content::AXEventNotificationDetails& details) {
// Updates to AXTree must be applied first.
for (const ui::AXTreeUpdate& update : details.updates) {
tree_.Unserialize(update);
}
// Events to fire after tree has been updated.
for (const ui::AXEvent& event : details.events) {
if (event.event_type == ax::mojom::Event::kHitTestResult) {
if (pending_hit_test_callbacks_.find(event.action_request_id) !=
pending_hit_test_callbacks_.end()) {
fuchsia::accessibility::semantics::Hit hit;
hit.set_node_id(ConvertToFuchsiaNodeId(event.id));
// Run the pending callback with the hit.
pending_hit_test_callbacks_[event.action_request_id](std::move(hit));
pending_hit_test_callbacks_.erase(event.action_request_id);
}
}
// Run callbacks associated with other accessibility events.
if (pending_accessibility_action_callbacks_.find(event.id) !=
pending_accessibility_action_callbacks_.end()) {
pending_accessibility_action_callbacks_[event.id](true);
pending_accessibility_action_callbacks_.erase(event.id);
}
}
}
void AccessibilityBridge::OnAccessibilityActionRequested(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action,
OnAccessibilityActionRequestedCallback callback) {
ui::AXActionData action_data = ui::AXActionData();
// TODO(fxb/16501): Add more actions when they become available on the
// SemanticsManager side.
action_data.action = ax::mojom::Action::kDoDefault;
action_data.target_node_id = node_id;
pending_accessibility_action_callbacks_[node_id] = std::move(callback);
// Allow tests to bypass action handling to simulate the case that actions
// aren't handled in tests.
if (!handle_actions_for_test_) {
return;
}
web_contents_->GetMainFrame()->AccessibilityPerformAction(action_data);
}
void AccessibilityBridge::HitTest(fuchsia::math::PointF local_point,
HitTestCallback callback) {
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kHitTest;
gfx::Point point;
point.set_x(local_point.x);
point.set_y(local_point.y);
action_data.target_point = point;
action_data.hit_test_event_to_fire = ax::mojom::Event::kHitTestResult;
pending_hit_test_callbacks_[action_data.request_id] = std::move(callback);
web_contents_->GetMainFrame()->AccessibilityPerformAction(action_data);
}
void AccessibilityBridge::OnSemanticsModeChanged(
bool updates_enabled,
OnSemanticsModeChangedCallback callback) {
if (updates_enabled) {
// The first call to AccessibilityEventReceived after this call will be the
// entire semantic tree.
web_contents_->EnableWebContentsOnlyAccessibilityMode();
} else {
// The SemanticsManager will clear all state in this case, which is mirrored
// here.
to_send_.clear();
commit_inflight_ = false;
}
// Notify the SemanticsManager that semantics mode has been updated.
callback();
}
void AccessibilityBridge::DeleteSubtree(ui::AXTree* tree, ui::AXNode* node) {
DCHECK(tree);
DCHECK(node);
// When navigating, page 1, including the root, is deleted after page 2 has
// loaded. Since the root id is the same for page 1 and 2, page 2's root id
// ends up getting deleted. To handle this, the root will only be updated.
if (node->id() != root_id_) {
to_send_.push_back(
SemanticUpdateOrDelete(SemanticUpdateOrDelete::Type::DELETE, {},
ConvertToFuchsiaNodeId(node->id())));
}
for (ui::AXNode* child : node->children())
DeleteSubtree(tree, child);
}
void AccessibilityBridge::OnNodeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {
DeleteSubtree(tree, node);
}
void AccessibilityBridge::OnSubtreeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {
DeleteSubtree(tree, node);
}
void AccessibilityBridge::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
const std::vector<ui::AXTreeObserver::Change>& changes) {
root_id_ = tree_.root()->id();
for (const ui::AXTreeObserver::Change& change : changes) {
ui::AXNodeData ax_data;
switch (change.type) {
case ui::AXTreeObserver::NODE_CREATED:
case ui::AXTreeObserver::SUBTREE_CREATED:
case ui::AXTreeObserver::NODE_CHANGED:
ax_data = change.node->data();
if (change.node->id() == root_id_) {
ax_data.id = kSemanticNodeRootId;
}
to_send_.push_back(
SemanticUpdateOrDelete(SemanticUpdateOrDelete::Type::UPDATE,
AXNodeDataToSemanticNode(ax_data), 0));
break;
case ui::AXTreeObserver::NODE_REPARENTED:
case ui::AXTreeObserver::SUBTREE_REPARENTED:
DeleteSubtree(tree, change.node);
break;
}
}
TryCommit();
}