chromium/ui/accessibility/platform/fuchsia/semantic_provider_impl.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/platform/fuchsia/semantic_provider_impl.h"

#include <fidl/fuchsia.ui.gfx/cpp/fidl.h>
#include <lib/async/default.h>
#include <lib/sys/cpp/component_context.h>

#include "base/check.h"
#include "base/check_op.h"
#include "base/fuchsia/fuchsia_component_connect.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "ui/gfx/geometry/transform.h"

namespace ui {
namespace {

using fuchsia_accessibility_semantics::Node;

constexpr size_t kMaxOperationsPerBatch = 16;

SemanticTreeEventHandler::SemanticTreeEventHandler(
    base::OnceCallback<void(fidl::UnbindInfo)> on_fidl_error_callback)
    : on_fidl_error_callback_(std::move(on_fidl_error_callback)) {}
SemanticTreeEventHandler::~SemanticTreeEventHandler() = default;

void SemanticTreeEventHandler::on_fidl_error(fidl::UnbindInfo error) {
  std::move(on_fidl_error_callback_).Run(error);
}

}  // namespace

AXFuchsiaSemanticProviderImpl::Batch::Batch(Type type) : type_(type) {}
AXFuchsiaSemanticProviderImpl::Batch::Batch(Batch&& other) = default;
AXFuchsiaSemanticProviderImpl::Batch::~Batch() = default;

bool AXFuchsiaSemanticProviderImpl::Batch::IsFull() const {
  return (
      (type_ == Type::kUpdate && updates_.size() >= kMaxOperationsPerBatch) ||
      (type_ == Type::kDelete &&
       delete_node_ids_.size() >= kMaxOperationsPerBatch));
}

void AXFuchsiaSemanticProviderImpl::Batch::Append(
    fuchsia_accessibility_semantics::Node node) {
  DCHECK_EQ(type_, Type::kUpdate);
  DCHECK(!IsFull());
  updates_.push_back(std::move(node));
}

void AXFuchsiaSemanticProviderImpl::Batch::AppendDeletion(
    uint32_t delete_node_id) {
  DCHECK_EQ(type_, Type::kDelete);
  DCHECK(!IsFull());
  delete_node_ids_.push_back(delete_node_id);
}

void AXFuchsiaSemanticProviderImpl::Batch::Apply(
    fidl::Client<fuchsia_accessibility_semantics::SemanticTree>*
        semantic_tree) {
  if (type_ == Type::kUpdate && !updates_.empty()) {
    auto result = (*semantic_tree)->UpdateSemanticNodes(std::move(updates_));
    LOG_IF(ERROR, result.is_error())
        << base::FidlMethodResultErrorMessage(result, "UpdateSemanticNodes");
  } else if (type_ == Type::kDelete && !delete_node_ids_.empty()) {
    auto result =
        (*semantic_tree)->DeleteSemanticNodes(std::move(delete_node_ids_));
    LOG_IF(ERROR, result.is_error())
        << base::FidlMethodResultErrorMessage(result, "DeleteSemanticNodes");
  }
}

AXFuchsiaSemanticProviderImpl::NodeInfo ::NodeInfo() = default;
AXFuchsiaSemanticProviderImpl::NodeInfo ::~NodeInfo() = default;

AXFuchsiaSemanticProviderImpl::Delegate::Delegate() = default;
AXFuchsiaSemanticProviderImpl::Delegate::~Delegate() = default;

AXFuchsiaSemanticProviderImpl::AXFuchsiaSemanticProviderImpl(
    fuchsia_ui_views::ViewRef view_ref,
    Delegate* delegate)
    : delegate_(delegate) {
  DCHECK(delegate_);

  auto semantics_manager_client_end = base::fuchsia_component::Connect<
      fuchsia_accessibility_semantics::SemanticsManager>();
  // TODO(crbug.com/40263576): Create a path for gracefully failing to connect
  // to SemanticsManager instead of CHECKing.
  CHECK(semantics_manager_client_end.is_ok())
      << base::FidlConnectionErrorMessage(semantics_manager_client_end);
  fidl::Client semantics_manager(
      std::move(semantics_manager_client_end.value()),
      async_get_default_dispatcher());

  auto semantic_listener_endpoints = fidl::CreateEndpoints<
      fuchsia_accessibility_semantics::SemanticListener>();
  ZX_CHECK(semantic_listener_endpoints.is_ok(),
           semantic_listener_endpoints.status_value());
  semantic_listener_binding_.emplace(
      async_get_default_dispatcher(),
      std::move(semantic_listener_endpoints->server), this,
      base::FidlBindingClosureWarningLogger(
          "fuchsia.accessibility.semantics.SemanticListener"));

  semantic_tree_event_handler_.emplace(base::BindOnce(
      [](AXFuchsiaSemanticProviderImpl* semantic_provider,
         fidl::UnbindInfo info) {
        ZX_LOG(ERROR, info.status()) << "SemanticListener disconnected";
        semantic_provider->delegate_->OnSemanticsManagerConnectionClosed(
            info.status());
        semantic_provider->semantic_updates_enabled_ = false;
      },
      this));

  auto semantic_tree_endpoints =
      fidl::CreateEndpoints<fuchsia_accessibility_semantics::SemanticTree>();
  ZX_CHECK(semantic_tree_endpoints.is_ok(),
           semantic_tree_endpoints.status_value());
  semantic_tree_.Bind(std::move(semantic_tree_endpoints->client),
                      async_get_default_dispatcher(),
                      &semantic_tree_event_handler_.value());

  auto result = semantics_manager->RegisterViewForSemantics({{
      .view_ref = std::move(view_ref),
      .listener = std::move(semantic_listener_endpoints->client),
      .semantic_tree_request = std::move(semantic_tree_endpoints->server),
  }});
  if (result.is_error()) {
    ZX_LOG(ERROR, result.error_value().status())
        << "Error calling RegisterViewForSemantics()";
  }
}

AXFuchsiaSemanticProviderImpl::~AXFuchsiaSemanticProviderImpl() = default;

bool AXFuchsiaSemanticProviderImpl::Update(
    fuchsia_accessibility_semantics::Node node) {
  if (!semantic_updates_enabled())
    return false;

  DCHECK(node.node_id().has_value());

  // If the updated node is the root, we need to account for the pixel scale in
  // its transform.
  //
  // Otherwise, we need to update our connectivity book-keeping.
  if (node.node_id() == kFuchsiaRootNodeId) {
    gfx::Transform transform;
    transform.PostScale(1 / pixel_scale_, 1 / pixel_scale_);

    // Convert to fuchsia's transform type.
    std::array<float, 16> mat = {};
    transform.GetColMajorF(mat.data());
    fuchsia_ui_gfx::Mat4 mat4{std::move(mat)};
    // The root node will never have an offset container, so its transform will
    // always be the identity matrix. Thus, we can safely overwrite it here.
    node.node_to_container_transform(std::move(mat4));
  } else {
    auto found_not_reachable = not_reachable_.find(node.node_id().value());
    const bool is_not_reachable = found_not_reachable != not_reachable_.end();
    const std::optional<uint32_t> parent_node_id =
        GetParentForNode(node.node_id().value());
    if (is_not_reachable && parent_node_id) {
      // Connection parent -> |node| exists now.
      not_reachable_.erase(found_not_reachable);
      nodes_[node.node_id().value()].parents.insert(*parent_node_id);
    } else if (!parent_node_id) {
      // No node or multiple nodes points to this one, so it is not reachable.
      if (!is_not_reachable)
        not_reachable_[node.node_id().value()] = {};
    }
  }

  // If the node is not present in the map, the list of children will be empty
  // so this is a no-op in the call below.
  std::vector<uint32_t>& children = nodes_[node.node_id().value()].children;

  // Before updating the node, update the list of children to be not reachable,
  // in case the new list of children change.
  MarkChildrenAsNotReachable(children, node.node_id().value());
  children = node.child_ids().value_or(std::vector<uint32_t>());
  MarkChildrenAsReachable(children, node.node_id().value());

  Batch& batch = GetCurrentUnfilledBatch(Batch::Type::kUpdate);
  batch.Append(std::move(node));
  TryToCommit();
  return true;
}

void AXFuchsiaSemanticProviderImpl::TryToCommit() {
  // Don't send out updates while the tree is mid-mutation.
  if (commit_inflight_ || batches_.empty())
    return;

  // If a tree has nodes but no root, wait until the root is present or all
  // nodes are deleted.
  if (!nodes_.empty() && nodes_.find(kFuchsiaRootNodeId) == nodes_.end())
    return;

  if (!not_reachable_.empty())
    return;

  for (auto& batch : batches_) {
    batch.Apply(&semantic_tree_);
  }

  batches_.clear();
  semantic_tree_->CommitUpdates().Then(
      fit::bind_member(this, &AXFuchsiaSemanticProviderImpl::OnCommitComplete));
  commit_inflight_ = true;
}

bool AXFuchsiaSemanticProviderImpl::Delete(uint32_t node_id) {
  if (!semantic_updates_enabled())
    return false;

  auto it = nodes_.find(node_id);
  if (it == nodes_.end())
    return false;

  if (it->second.parents.empty()) {
    // No node points to this one, so it is safe to remove it from the tree.
    not_reachable_.erase(node_id);
  } else {
    not_reachable_[node_id] =
        it->second
            .parents;  // Zero or more parents can be pointing to this node.
  }
  MarkChildrenAsNotReachable(it->second.children, node_id);

  nodes_.erase(it);

  Batch& batch = GetCurrentUnfilledBatch(Batch::Type::kDelete);
  batch.AppendDeletion(node_id);
  TryToCommit();
  return true;
}

void AXFuchsiaSemanticProviderImpl::SendEvent(
    fuchsia_accessibility_semantics::SemanticEvent event) {
  semantic_tree_->SendSemanticEvent({{.semantic_event = std::move(event)}})
      .Then(
          [](fidl::Result<
              fuchsia_accessibility_semantics::SemanticTree::SendSemanticEvent>&
                 result) {
            ZX_LOG_IF(ERROR, result.is_error(), result.error_value().status());
          });
}

bool AXFuchsiaSemanticProviderImpl::HasPendingUpdates() const {
  return commit_inflight_ || !batches_.empty();
}

bool AXFuchsiaSemanticProviderImpl::Clear() {
  if (!semantic_updates_enabled())
    return false;

  batches_.clear();
  not_reachable_.clear();
  nodes_.clear();
  Batch& batch = GetCurrentUnfilledBatch(Batch::Type::kDelete);
  batch.AppendDeletion(kFuchsiaRootNodeId);
  TryToCommit();
  return true;
}

void AXFuchsiaSemanticProviderImpl::OnAccessibilityActionRequested(
    AXFuchsiaSemanticProviderImpl::OnAccessibilityActionRequestedRequest&
        request,
    AXFuchsiaSemanticProviderImpl::OnAccessibilityActionRequestedCompleter::
        Sync& completer) {
  if (delegate_->OnAccessibilityAction(request.node_id(), request.action())) {
    completer.Reply(true);
    return;
  }

  // The action was not handled.
  completer.Reply(false);
}

void AXFuchsiaSemanticProviderImpl::HitTest(
    AXFuchsiaSemanticProviderImpl::HitTestRequest& request,
    AXFuchsiaSemanticProviderImpl::HitTestCompleter::Sync& completer) {
  delegate_->OnHitTest(
      {{
          .x = request.local_point().x() * pixel_scale_,
          .y = request.local_point().y() * pixel_scale_,
      }},
      base::BindOnce(
          [](HitTestCompleter::Async async_completer,
             const fidl::Response<
                 fuchsia_accessibility_semantics::SemanticListener::HitTest>&
                 result) { async_completer.Reply(result); },
          completer.ToAsync()));
  return;
}

void AXFuchsiaSemanticProviderImpl::OnSemanticsModeChanged(
    AXFuchsiaSemanticProviderImpl::OnSemanticsModeChangedRequest& request,
    AXFuchsiaSemanticProviderImpl::OnSemanticsModeChangedCompleter::Sync&
        completer) {
  if (semantic_updates_enabled_ != request.updates_enabled()) {
    delegate_->OnSemanticsEnabled(request.updates_enabled());
  }

  semantic_updates_enabled_ = request.updates_enabled();
  completer.Reply();
}

void AXFuchsiaSemanticProviderImpl::MarkChildrenAsNotReachable(
    const std::vector<uint32_t>& child_ids,
    uint32_t parent_id) {
  for (const uint32_t child_id : child_ids) {
    const auto it = nodes_.find(child_id);
    if (it != nodes_.end()) {
      it->second.parents.erase(parent_id);
      if (it->second.parents.empty())
        not_reachable_[child_id] = {};
      else
        not_reachable_.erase(child_id);
    } else {
      auto not_reachable_it = not_reachable_.find(child_id);
      // Child id is no longer in the regular map, deletes it also from
      // not_reachable_ if no parent points to it anymore.
      if (not_reachable_it != not_reachable_.end()) {
        not_reachable_it->second.erase(parent_id);
        if (not_reachable_it->second.empty())
          not_reachable_.erase(not_reachable_it);
      }
    }
  }
}

void AXFuchsiaSemanticProviderImpl::MarkChildrenAsReachable(
    const std::vector<uint32_t>& child_ids,
    uint32_t parent_id) {
  for (const uint32_t child_id : child_ids) {
    auto it = nodes_.find(child_id);
    if (it == nodes_.end())
      not_reachable_[child_id].insert(parent_id);
    else {
      it->second.parents.insert(parent_id);
      if (it->second.parents.size() == 1)
        not_reachable_.erase(child_id);
      else
        not_reachable_[child_id].insert(parent_id);
    }
  }
}

std::optional<uint32_t> AXFuchsiaSemanticProviderImpl::GetParentForNode(
    const uint32_t node_id) {
  const auto it = nodes_.find(node_id);
  if (it != nodes_.end()) {
    if (it->second.parents.size() == 1)
      return *it->second.parents.begin();
    else
      return std::nullopt;
  }

  const auto not_reachable_it = not_reachable_.find(node_id);
  if (not_reachable_it != not_reachable_.end()) {
    if (not_reachable_it->second.size() == 1)
      return *not_reachable_it->second.begin();
    else
      return std::nullopt;
  }

  return std::nullopt;
}

AXFuchsiaSemanticProviderImpl::Batch&
AXFuchsiaSemanticProviderImpl::GetCurrentUnfilledBatch(Batch::Type type) {
  if (batches_.empty() || batches_.back().type() != type ||
      batches_.back().IsFull())
    batches_.emplace_back(type);

  return batches_.back();
}

void AXFuchsiaSemanticProviderImpl::OnCommitComplete(
    fidl::Result<fuchsia_accessibility_semantics::SemanticTree::CommitUpdates>&
        result) {
  ZX_LOG_IF(ERROR, result.is_error(), result.error_value().status());
  commit_inflight_ = false;
  TryToCommit();
}

float AXFuchsiaSemanticProviderImpl::GetPixelScale() const {
  return pixel_scale_;
}

void AXFuchsiaSemanticProviderImpl::SetPixelScale(float pixel_scale) {
  pixel_scale_ = pixel_scale;

  // If the root node exists, then we need to update its transform to reflect
  // the new pixel scale.
  if (nodes_.find(kFuchsiaRootNodeId) == nodes_.end())
    return;

  // We need to fill the `child_ids` field to prevent Update() from trampling
  // our connectivity bookkeeping. Update() will handle setting the
  // `node_to_container_transform` field.
  Update({{
      .node_id = kFuchsiaRootNodeId,
      .child_ids = nodes_[kFuchsiaRootNodeId].children,
  }});
}

}  // namespace ui