chromium/ash/webui/eche_app_ui/accessibility_provider.cc


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

#include "ash/webui/eche_app_ui/accessibility_provider.h"

#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>

#include "ash/public/cpp/ash_web_view.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/system/eche/eche_tray.h"
#include "ash/webui/eche_app_ui/accessibility_tree_converter.h"
#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "content/public/browser/render_widget_host_view.h"
#include "services/accessibility/android/public/mojom/accessibility_helper.mojom-shared.h"
#include "ui/accessibility/aura/aura_window_properties.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/webview/webview.h"

namespace {
using AXActionType = ax::android::mojom::AccessibilityActionType;
using AXBooleanProperty = ax::android::mojom::AccessibilityBooleanProperty;
using AXCollectionInfoData =
    ax::android::mojom::AccessibilityCollectionInfoData;
using AXCollectionItemInfoData =
    ax::android::mojom::AccessibilityCollectionItemInfoData;
using AXEventData = ax::android::mojom::AccessibilityEventData;
using AXEventIntListProperty =
    ax::android::mojom::AccessibilityEventIntListProperty;
using AXEventIntProperty = ax::android::mojom::AccessibilityEventIntProperty;
using AXEventType = ax::android::mojom::AccessibilityEventType;
using AXIntListProperty = ax::android::mojom::AccessibilityIntListProperty;
using AXIntProperty = ax::android::mojom::AccessibilityIntProperty;
using AXNodeInfoData = ax::android::mojom::AccessibilityNodeInfoData;
using AXRangeInfoData = ax::android::mojom::AccessibilityRangeInfoData;
using AXStringProperty = ax::android::mojom::AccessibilityStringProperty;
using AXWindowBooleanProperty =
    ax::android::mojom::AccessibilityWindowBooleanProperty;
using AXWindowInfoData = ax::android::mojom::AccessibilityWindowInfoData;
using AXWindowIntProperty = ax::android::mojom::AccessibilityWindowIntProperty;
using AXWindowIntListProperty =
    ax::android::mojom::AccessibilityWindowIntListProperty;
using AXWindowStringProperty =
    ax::android::mojom::AccessibilityWindowStringProperty;
}  // namespace

namespace ash::eche_app {

AccessibilityProvider::AccessibilityProvider(
    std::unique_ptr<AccessibilityProviderProxy> proxy)
    : proxy_(std::move(proxy)) {
  // Register callback to know when accessibility should be enabled or disabled.
  proxy_->SetAccessibilityEnabledStateChangedCallback(base::BindRepeating(
      &AccessibilityProvider::OnAccessibilityEnabledStateChanged,
      weak_ptr_factory_.GetWeakPtr()));
  proxy_->SetExploreByTouchEnabledStateChangedCallback(base::BindRepeating(
      &AccessibilityProvider::OnExploreByTouchEnabledStateChanged,
      weak_ptr_factory_.GetWeakPtr()));
}
AccessibilityProvider::~AccessibilityProvider() = default;

void AccessibilityProvider::TrackView(AshWebView* view) {
  if (!base::FeatureList::IsEnabled(
          features::kEcheSWAProcessAndroidAccessibilityTree)) {
    // Don't track views if a11y tree is not enabled.
    return;
  }
  tree_source_ = std::make_unique<ax::android::AXTreeSourceAndroid>(
      this, std::make_unique<SerializationDelegate>(device_bounds_),
      view->GetNativeView() /*window*/);
  auto* child_view = view->GetViewByID(kAshWebViewChildWebViewId);
  CHECK(child_view);
  auto* webview = static_cast<views::WebView*>(child_view);
  webview->set_lock_child_ax_tree_id_override(true);
  webview->GetViewAccessibility().SetChildTreeID(
      tree_source_->ax_tree_id());
  auto* window_ptr =
      webview->web_contents()->GetRenderWidgetHostView()->GetNativeView();
  CHECK(window_ptr);
  // The render host view gets hit tests, set the hit test tree id.
  window_ptr->SetProperty(ui::kChildAXTreeID,
                          tree_source_->ax_tree_id().ToString());
  proxy_->OnViewTracked();
}

void AccessibilityProvider::HandleStreamClosed() {
  tree_source_.reset();
}

void AccessibilityProvider::HandleAccessibilityEventReceived(
    const std::vector<uint8_t>& serialized_proto) {
  if (!base::FeatureList::IsEnabled(
          features::kEcheSWAProcessAndroidAccessibilityTree)) {
    return;
  }

  if (serialized_proto.empty()) {
    return;
  }

  switch (GetFilterType()) {
    case ax::android::mojom::AccessibilityFilterType::ALL: {
      AccessibilityTreeConverter converter;
      // Parse the proto first so we can extract device screen size.
      proto::AccessibilityEventData proto_event_data;
      if (!converter.DeserializeProto(serialized_proto, &proto_event_data)) {
        return;
      }
      UpdateDeviceBounds(proto_event_data.display_info());
      auto mojom_event_data =
          converter.ConvertEventDataProtoToMojom(proto_event_data);
      if (mojom_event_data) {
        // Pass into correct tree. Currently there is only one.
        // Tree source will only be initialized once TrackView has been called.
        // Sometimes an event will come it right as the window is closed, which
        // can cause a crash with a check.
        if (tree_source_) {
          tree_source_->NotifyAccessibilityEvent(mojom_event_data.get());
        }
      }
    } break;
    case ax::android::mojom::AccessibilityFilterType::FOCUS:
      // TODO(francisjp): b/265817804
      break;
    case ax::android::mojom::AccessibilityFilterType::OFF:
      break;
    case ax::android::mojom::AccessibilityFilterType::INVALID_ENUM_VALUE:
      NOTREACHED();
  }
}

void AccessibilityProvider::SetAccessibilityObserver(
    mojo::PendingRemote<mojom::AccessibilityObserver> observer) {
  observer_remote_.reset();
  observer_remote_.Bind(std::move(observer));
}

void AccessibilityProvider::IsAccessibilityEnabled(
    IsAccessibilityEnabledCallback callback) {
  std::move(callback).Run(proxy_->IsAccessibilityEnabled());
}

bool AccessibilityProvider::UseFullFocusMode() const {
  return proxy_->UseFullFocusMode();
}

ax::android::mojom::AccessibilityFilterType
AccessibilityProvider::GetFilterType() {
  return proxy_->GetFilterType();
}

void AccessibilityProvider::UpdateDeviceBounds(
    const proto::Rect& device_bounds) {
  const int height = device_bounds.bottom() - device_bounds.top();
  const int width = device_bounds.right() - device_bounds.left();
  CHECK(height > 0);
  CHECK(width > 0);
  device_bounds_.set_size({width, height});
}

// TODO(b/296326746) The current implementation is a workaround for Select to
// Speak, and a proper fix is pending hit test logic for android trees.
void AccessibilityProvider::HandleHitTest(const ui::AXActionData& data) const {
  // A hit test may come in just after a user closes the window.
  if (!tree_source_ || !tree_source_->root_id().has_value()) {
    return;
  }

  auto* automation_router = extensions::AutomationEventRouter::GetInstance();
  ui::AXEvent event;
  event.action_request_id = data.request_id;
  event.event_from_action = data.action;
  event.event_intents = {};
  event.id = tree_source_->root_id().value();
  event.event_type = data.hit_test_event_to_fire;
  automation_router->DispatchAccessibilityEvents(tree_source_->ax_tree_id(), {},
                                                 data.target_point, {event});
}

void AccessibilityProvider::OnGetTextLocationDataResult(
    const ui::AXActionData& action,
    const std::optional<std::vector<uint8_t>>& serialized_text_location) const {
  std::optional<gfx::Rect> result_rect;
  // There was a rect returned. Parse and validate it.
  if (serialized_text_location.has_value()) {
    proto::Rect proto_rect;
    // Parse the rect.
    if (!proto_rect.ParseFromArray(serialized_text_location->data(),
                                   serialized_text_location->size())) {
      // Failed to parse the response. Fail the action.
      OnActionResult(action, false);
      return;
    }
    // Validate the rect.
    if (proto_rect.bottom() > proto_rect.top() &&
        proto_rect.right() > proto_rect.left()) {
      result_rect = OnGetTextLocationDataResultInternal(proto_rect);
    } else {
      // Rect was invalid. Fail the action.
      OnActionResult(action, false);
      return;
    }
  }
  if (!tree_source_) {
    // Simple return, there is no way to notify a
    // tree source that doesn't exist.
    return;
  }

  tree_source_->NotifyGetTextLocationDataResult(action, result_rect);
}

gfx::Rect AccessibilityProvider::OnGetTextLocationDataResultInternal(
    proto::Rect proto_rect) const {
  // TODO(francisjp): Determine if there is additional processing required.
  int height = proto_rect.bottom() - proto_rect.top();
  int width = proto_rect.right() - proto_rect.left();
  return gfx::Rect(proto_rect.top(), proto_rect.left(), width, height);
}

void AccessibilityProvider::OnAction(const ui::AXActionData& action) const {
  if (!tree_source_) {
    return;
  }
  if (!tree_source_->window_id().has_value()) {
    OnActionResult(action, false);
    return;
  }

  if (action.action == ax::mojom::Action::kHitTest) {
    HandleHitTest(action);
    return;
  }

  AccessibilityTreeConverter converter;
  auto proto_action =
      converter.ConvertActionDataToProto(action, *tree_source_->window_id());
  if (proto_action.has_value()) {
    size_t nbytes = proto_action->ByteSizeLong();
    std::vector<uint8_t> serialized_proto(nbytes);
    proto_action->SerializeToArray(serialized_proto.data(), nbytes);

    if (action.action == ax::mojom::Action::kGetTextLocation) {
      mojom::AccessibilityObserver::RefreshWithExtraDataCallback cb =
          base::BindOnce(&AccessibilityProvider::OnGetTextLocationDataResult,
                         weak_ptr_factory_.GetWeakPtr(), action);
      observer_remote_->RefreshWithExtraData(serialized_proto, std::move(cb));
    } else {
      mojom::AccessibilityObserver::PerformActionCallback cb =
          base::BindOnce(&AccessibilityProvider::OnActionResult,
                         weak_ptr_factory_.GetWeakPtr(), action);
      observer_remote_->PerformAction(serialized_proto, std::move(cb));
    }

  } else {
    OnActionResult(action, false);
  }
}

void AccessibilityProvider::Bind(
    mojo::PendingReceiver<mojom::AccessibilityProvider> receiver) {
  receiver_.reset();
  receiver_.Bind(std::move(receiver));
}

void AccessibilityProvider::OnActionResult(const ui::AXActionData& action,
                                           bool result) const {
  // Tree source could be null if the app is switched before the response comes
  // back.
  if (tree_source_) {
    tree_source_->NotifyActionResult(action, result);
  }
}

void AccessibilityProvider::OnAccessibilityEnabledStateChanged(bool enabled) {
  observer_remote_->EnableAccessibilityTreeStreaming(enabled);
}

void AccessibilityProvider::OnExploreByTouchEnabledStateChanged(bool enabled) {
  observer_remote_->EnableExploreByTouch(enabled);
}

// Serialization Delegate implementation

gfx::RectF
AccessibilityProvider::SerializationDelegate::ScaleAndroidPxToChromePx(
    const ax::android::AccessibilityInfoDataWrapper& node,
    aura::Window* window) const {
  gfx::Rect android_bounds = node.GetBounds();
  const auto* window_node = tree_source_->GetRoot();
  gfx::Rect window_res = window_node->GetWindow()->bounds_in_screen;
  const gfx::Rect chrome_window_bounds = window->GetBoundsInScreen();
  float x_scale =
      (float)chrome_window_bounds.width() / (float)device_bounds_->width();
  float y_scale =
      (float)chrome_window_bounds.height() / (float)device_bounds_->height();
  gfx::RectF chrome_bounds(android_bounds);
  const float chrome_dsf =
      window->GetToplevelWindow()->layer()->device_scale_factor();
  // Nodes need to be offset inside of their window.
  if (node.IsNode()) {
    chrome_bounds.Offset(-window_res.x(), -window_res.y());
  }
  chrome_bounds.Scale(x_scale * chrome_dsf, y_scale * chrome_dsf);
  return chrome_bounds;
}

void AccessibilityProvider::SerializationDelegate::PopulateBounds(
    const ax::android::AccessibilityInfoDataWrapper& node,
    ui::AXNodeData& out_data) const {
  gfx::RectF& out_bounds_px = out_data.relative_bounds.bounds;
  auto* window = tree_source_->window();
  out_bounds_px = ScaleAndroidPxToChromePx(node, window);
}

AccessibilityProvider::SerializationDelegate::SerializationDelegate(
    gfx::Rect& device_bounds)
    : device_bounds_(device_bounds) {}
}  // namespace ash::eche_app