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

// Copyright 2022 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/accessibility_bridge_fuchsia_impl.h"

#include "base/strings/stringprintf.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/platform/fuchsia/accessibility_bridge_fuchsia_registry.h"
#include "ui/accessibility/platform/fuchsia/ax_platform_node_fuchsia.h"
#include "ui/accessibility/platform/fuchsia/semantic_provider_impl.h"
#include "ui/gfx/geometry/rect_conversions.h"

namespace ui {
namespace {

// Error allowed for each edge when converting from gfx::RectF to gfx::Rect.
constexpr float kRectConversionError = 0.5;

std::optional<ax::mojom::Action> ConvertAction(
    fuchsia_accessibility_semantics::Action fuchsia_action) {
  switch (fuchsia_action) {
    case fuchsia_accessibility_semantics::Action::kDefault:
      return ax::mojom::Action::kDoDefault;
    case fuchsia_accessibility_semantics::Action::kDecrement:
      return ax::mojom::Action::kDecrement;
    case fuchsia_accessibility_semantics::Action::kIncrement:
      return ax::mojom::Action::kIncrement;
    case fuchsia_accessibility_semantics::Action::kShowOnScreen:
      return ax::mojom::Action::kScrollToMakeVisible;
    case fuchsia_accessibility_semantics::Action::kSecondary:
      LOG(WARNING) << "SECONDARY action not supported";
      return {};
    case fuchsia_accessibility_semantics::Action::kSetFocus:
      return ax::mojom::Action::kFocus;
    case fuchsia_accessibility_semantics::Action::kSetValue:
      return ax::mojom::Action::kSetValue;
    default:
      LOG(WARNING)
          << "Unknown fuchsia_accessibility_semantics::Action with value "
          << static_cast<int>(fuchsia_action);
      return {};
  }
}

}  // namespace

AccessibilityBridgeFuchsiaImpl::AccessibilityBridgeFuchsiaImpl(
    aura::Window* window,
    fuchsia_ui_views::ViewRef view_ref,
    base::RepeatingCallback<void(bool)> on_semantics_enabled,
    OnConnectionClosedCallback on_connection_closed,
    inspect::Node inspect_node)
    : root_window_(window),
      on_semantics_enabled_(std::move(on_semantics_enabled)),
      on_connection_closed_(std::move(on_connection_closed)),
      inspect_node_(std::move(inspect_node)) {
  semantic_provider_ = std::make_unique<AXFuchsiaSemanticProviderImpl>(
      std::move(view_ref), this);

  AccessibilityBridgeFuchsiaRegistry* registry =
      AccessibilityBridgeFuchsiaRegistry::GetInstance();
  DCHECK(registry);
  if (root_window_)
    registry->RegisterAccessibilityBridge(root_window_, this);
}

AccessibilityBridgeFuchsiaImpl::~AccessibilityBridgeFuchsiaImpl() {
  AccessibilityBridgeFuchsiaRegistry* registry =
      AccessibilityBridgeFuchsiaRegistry::GetInstance();
  DCHECK(registry);
  if (root_window_)
    registry->UnregisterAccessibilityBridge(root_window_);
}

uint32_t AccessibilityBridgeFuchsiaImpl::MaybeToFuchsiaRootID(
    uint32_t node_id) {
  // If |node_id| refers to the root, then map to fuchsia ID 0. Otherwise,
  // use |node_id| directly.
  if (root_node_id_ && node_id == *root_node_id_)
    return AXFuchsiaSemanticProvider::kFuchsiaRootNodeId;

  return node_id;
}

void AccessibilityBridgeFuchsiaImpl::UpdateNode(
    fuchsia_accessibility_semantics::Node node) {
  DCHECK(node.node_id().has_value());

  node.node_id(MaybeToFuchsiaRootID(node.node_id().value()));

  if (node.container_id()) {
    node.container_id(MaybeToFuchsiaRootID(node.container_id().value()));
  }

  semantic_provider_->Update(std::move(node));
}

void AccessibilityBridgeFuchsiaImpl::DeleteNode(uint32_t node_id) {
  uint32_t fuchsia_node_id = MaybeToFuchsiaRootID(node_id);
  semantic_provider_->Delete(fuchsia_node_id);

  // If we have deleted the root node, we should also clear root_node_id_.
  if (fuchsia_node_id == AXFuchsiaSemanticProvider::kFuchsiaRootNodeId)
    root_node_id_.reset();
}

void AccessibilityBridgeFuchsiaImpl::OnAccessibilityHitTestResult(
    int hit_test_request_id,
    std::optional<uint32_t> result) {
  auto it = pending_hit_test_completers_.find(hit_test_request_id);
  if (it == pending_hit_test_completers_.end()) {
    return;
  }

  fuchsia_accessibility_semantics::Hit hit;
  if (result)
    hit.node_id(MaybeToFuchsiaRootID(*result));

  std::move(it->second).Run(std::move(hit));
  pending_hit_test_completers_.erase(it);
}

float AccessibilityBridgeFuchsiaImpl::GetDeviceScaleFactor() {
  return semantic_provider_->GetPixelScale();
}

void AccessibilityBridgeFuchsiaImpl::SetRootID(uint32_t root_node_id) {
  // If the root has changed, then we should delete the old root.
  //
  // If the old root was already deleted, then this operation will be a NOOP.
  // If the old root was NOT yet deleted, then this operation will delete it.
  // When the accessibility bridge client tries to delete it later (using its
  // AXUniqueID), the accessibility bridge will no longer map its id to
  // kFuchsiaRootNodeId. So, that deletion will also be a NOOP.
  //
  // In either case, this operation is safe, and the end state is that the old
  // root node is deleted.
  if (root_node_id_)
    semantic_provider_->Delete(AXFuchsiaSemanticProvider::kFuchsiaRootNodeId);

  root_node_id_ = root_node_id;
}

bool AccessibilityBridgeFuchsiaImpl::OnAccessibilityAction(
    uint32_t node_id,
    fuchsia_accessibility_semantics::Action action) {
  // If the action was requested on the root, translate to the root node's
  // unique ID.
  if (node_id == AXFuchsiaSemanticProvider::kFuchsiaRootNodeId) {
    if (!root_node_id_)
      return false;

    node_id = *root_node_id_;
  }

  AXPlatformNode* ax_platform_node =
      AXPlatformNodeFuchsia::GetFromUniqueId(node_id);
  AXPlatformNodeFuchsia* ax_platform_node_fuchsia =
      static_cast<AXPlatformNodeFuchsia*>(ax_platform_node);

  if (!ax_platform_node_fuchsia)
    return false;

  AXActionData action_data = AXActionData();

  // The requested action is not supported.
  std::optional<ax::mojom::Action> ax_action = ConvertAction(action);
  if (!ax_action)
    return false;

  action_data.action = *ax_action;
  action_data.target_node_id = node_id;

  if (action == fuchsia_accessibility_semantics::Action::kShowOnScreen) {
    // The scroll-to-make-visible action expects coordinates in the local
    // coordinate space of |node|. So, we need to translate node's bounds to the
    // origin.
    gfx::Rect local_bounds = gfx::ToEnclosedRectIgnoringError(
        ax_platform_node_fuchsia->GetData().relative_bounds.bounds,
        kRectConversionError);
    local_bounds = gfx::Rect(local_bounds.size());

    action_data.target_rect = local_bounds;
    action_data.horizontal_scroll_alignment =
        ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
    action_data.vertical_scroll_alignment =
        ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
    action_data.scroll_behavior = ax::mojom::ScrollBehavior::kScrollIfVisible;
  }

  ax_platform_node_fuchsia->PerformAction(action_data);
  return true;
}

void AccessibilityBridgeFuchsiaImpl::OnHitTest(
    fuchsia_math::PointF point,
    AXFuchsiaSemanticProvider::Delegate::HitTestCallback callback) {
  AXPlatformNodeFuchsia* ax_platform_node_fuchsia = nullptr;

  if (root_node_id_) {
    // Target the root node.
    AXPlatformNode* ax_platform_node =
        AXPlatformNodeFuchsia::GetFromUniqueId(*root_node_id_);
    ax_platform_node_fuchsia =
        static_cast<AXPlatformNodeFuchsia*>(ax_platform_node);
  }

  if (!ax_platform_node_fuchsia) {
    fuchsia_accessibility_semantics::Hit hit;
    std::move(callback).Run(std::move(hit));
    return;
  }

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kHitTest;
  gfx::Point target_point;
  target_point.set_x(point.x());
  target_point.set_y(point.y());
  action_data.target_point = target_point;
  action_data.hit_test_event_to_fire = ax::mojom::Event::kHitTestResult;
  action_data.request_id = next_hittest_request_id_++;

  pending_hit_test_completers_.insert_or_assign(action_data.request_id,
                                                std::move(callback));

  ax_platform_node_fuchsia->PerformAction(std::move(action_data));
}

void AccessibilityBridgeFuchsiaImpl::OnSemanticsEnabled(bool enabled) {
  if (on_semantics_enabled_)
    on_semantics_enabled_.Run(enabled);
}

bool AccessibilityBridgeFuchsiaImpl::OnSemanticsManagerConnectionClosed(
    zx_status_t status) {
  if (on_connection_closed_)
    return on_connection_closed_.Run(status);

  // If the user does not specify a callback, then we can assume no attempt to
  // reconnect should be made.
  return false;
}

void AccessibilityBridgeFuchsiaImpl::set_semantic_provider_for_test(
    std::unique_ptr<AXFuchsiaSemanticProvider> semantic_provider) {
  semantic_provider_ = std::move(semantic_provider);
}

inspect::Node AccessibilityBridgeFuchsiaImpl::GetInspectNode() {
  return inspect_node_.CreateChild(
      base::StringPrintf("AXTree-%d", next_inspect_tree_number_++));
}

void AccessibilityBridgeFuchsiaImpl::SetPixelScale(float pixel_scale) {
  semantic_provider_->SetPixelScale(pixel_scale);
}

}  // namespace ui