chromium/services/accessibility/features/automation_internal_bindings.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 "services/accessibility/features/automation_internal_bindings.h"

#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/values.h"
#include "gin/function_template.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "services/accessibility/assistive_technology_controller_impl.h"
#include "services/accessibility/automation_impl.h"
#include "services/accessibility/features/bindings_isolate_holder.h"
#include "services/accessibility/features/v8_utils.h"
#include "services/accessibility/public/mojom/accessibility_service.mojom.h"
#include "ui/accessibility/ax_event_generator.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/mojom/ax_event.mojom.h"
#include "ui/accessibility/platform/automation/automation_api_util.h"
#include "ui/accessibility/platform/automation/automation_v8_router.h"
#include "v8-function.h"
#include "v8-value.h"
#include "v8/include/v8-local-handle.h"
#include "v8/include/v8-template.h"

namespace ax {
namespace {

static const char kJSAutomationInternalV8Listeners[] =
    "automationInternalV8Listeners";
static const char kJSCallListeners[] = "callListeners";

}  // namespace
AutomationInternalBindings::AutomationInternalBindings(
    BindingsIsolateHolder* isolate_holder,
    mojo::PendingAssociatedReceiver<mojom::Automation> automation)
    : isolate_holder_(isolate_holder),
      automation_v8_bindings_(std::make_unique<ui::AutomationV8Bindings>(
          /*AutomationTreeManagerOwner=*/this,
          /*AutomationV8Router=*/this)) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  receiver_.Bind(std::move(automation));
}

AutomationInternalBindings::~AutomationInternalBindings() = default;

void AutomationInternalBindings::AddAutomationRoutesToTemplate(
    v8::Local<v8::ObjectTemplate>* object_template) {
  template_ = object_template;
  automation_v8_bindings_->AddV8Routes();
  template_ = nullptr;
}

ui::AutomationV8Bindings* AutomationInternalBindings::GetAutomationV8Bindings()
    const {
  DCHECK(automation_v8_bindings_);
  return automation_v8_bindings_.get();
}

void AutomationInternalBindings::NotifyTreeEventListenersChanged() {
  // This task is posted because we need to wait for any pending mutations
  // to be processed before sending the event.
  CHECK(base::SequencedTaskRunner::HasCurrentDefault());
  auto& main_runner = base::SequencedTaskRunner::GetCurrentDefault();

  // `this` is safe here because this object outlives
  // AutomationTreeManagerOwner, which in turn generates this kind of event.
  auto task = base::BindOnce(&AutomationInternalBindings::
                                 MaybeSendOnAllAutomationEventListenersRemoved,
                             base::Unretained(this));
  main_runner->PostTask(FROM_HERE, std::move(task));
}

void AutomationInternalBindings::ThrowInvalidArgumentsException(
    bool is_fatal) const {
  GetIsolate()->ThrowException(v8::String::NewFromUtf8Literal(
      GetIsolate(),
      "Invalid arguments to AutomationInternalBindings function"));
  if (!is_fatal)
    return;
  LOG(FATAL) << "Invalid arguments to AutomationInternalBindings function";
  // TODO(crbug.com/1357889): Could print a stack trace from JS, paralleling
  // AutomationInternalCustomBindings.
}

v8::Isolate* AutomationInternalBindings::GetIsolate() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(isolate_holder_);
  return isolate_holder_->GetIsolate();
}

v8::Local<v8::Context> AutomationInternalBindings::GetContext() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(isolate_holder_);
  return isolate_holder_->GetContext();
}

void AutomationInternalBindings::RouteHandlerFunction(
    const std::string& name,
    scoped_refptr<ui::V8HandlerFunctionWrapper> handler_function_wrapper) {
  DCHECK(template_);
  (*template_)
      ->Set(GetIsolate(), name.c_str(),
            gin::CreateFunctionTemplate(
                GetIsolate(),
                base::BindRepeating(&ui::V8HandlerFunctionWrapper::Run,
                                    handler_function_wrapper)));
}

ui::TreeChangeObserverFilter
AutomationInternalBindings::ParseTreeChangeObserverFilter(
    const std::string& filter) const {
  // TODO(b:327258691): Share const strings between c++ and js for event names.
  if (filter == "none") {
    return ui::TreeChangeObserverFilter::kNone;
  }
  if (filter == "noTreeChanges") {
    return ui::TreeChangeObserverFilter::kNoTreeChanges;
  }
  if (filter == "liveRegionTreeChanges") {
    return ui::TreeChangeObserverFilter::kLiveRegionTreeChanges;
  }
  if (filter == "textMarkerChanges") {
    return ui::TreeChangeObserverFilter::kTextMarkerChanges;
  }
  if (filter == "allTreeChanges") {
    return ui::TreeChangeObserverFilter::kAllTreeChanges;
  }

  return ui::TreeChangeObserverFilter::kAllTreeChanges;
}

std::string AutomationInternalBindings::GetMarkerTypeString(
    ax::mojom::MarkerType type) const {
  // TODO(crbug.com/1357889): Implement based on Automation API.
  return ui::ToString(type);
}

std::string AutomationInternalBindings::GetFocusedStateString() const {
  // TODO(crbug.com/1357889): Implement based on Automation API.
  return "focused";
}

std::string AutomationInternalBindings::GetOffscreenStateString() const {
  // TODO(crbug.com/1357889): Implement based on Automation API.
  return "offscreen";
}

std::string
AutomationInternalBindings::GetLocalizedStringForImageAnnotationStatus(
    ax::mojom::ImageAnnotationStatus status) const {
  // TODO(crbug.com/1357889): Implement based on Automation API.
  return ui::ToString(status);
}

std::string AutomationInternalBindings::GetTreeChangeTypeString(
    ax::mojom::Mutation change_type) const {
  // TODO(b:327258691): Share const strings between c++ and js for event names.
  switch (change_type) {
    case ax::mojom::Mutation::kNone:
      return "none";
    case ax::mojom::Mutation::kNodeCreated:
      return "nodeCreated";
    case ax::mojom::Mutation::kSubtreeCreated:
      return "subtreeCreated";
    case ax::mojom::Mutation::kNodeChanged:
      return "nodeChanged";
    case ax::mojom::Mutation::kTextChanged:
      return "textChanged";
    case ax::mojom::Mutation::kNodeRemoved:
      return "nodeRemoved";
    case ax::mojom::Mutation::kSubtreeUpdateEnd:
      return "subtreeUpdateEnd";
  }
}

std::string AutomationInternalBindings::GetEventTypeString(
    const std::tuple<ax::mojom::Event, ui::AXEventGenerator::Event>& event_type)
    const {
  // TODO(b:327258691): Share const strings between c++ and js for event names.
  const ui::AXEventGenerator::Event& generated_event = std::get<1>(event_type);

  // Resolve the proper event based on generated or non-generated event sources.
  if (generated_event != ui::AXEventGenerator::Event::NONE &&
      !ui::ShouldIgnoreGeneratedEventForAutomation(generated_event)) {
    return ui::ToString(generated_event);
  }

  const ax::mojom::Event& event = std::get<0>(event_type);
  if (event != ax::mojom::Event::kNone &&
      !ui::ShouldIgnoreAXEventForAutomation(event)) {
    return ui::ToString(event);
  }
  return "";
}

void AutomationInternalBindings::DispatchEvent(
    const std::string& event_name,
    const base::Value::List& event_args) const {
  v8::HandleScope handle_scope(GetIsolate());
  v8::Local<v8::Context> context = GetContext();
  v8::Context::Scope context_scope(context);

  // Here, a helper function defined in js is invoked to retrieve the event
  // object that contains all the listeners. Then, we dispatch the event to all
  // listeners.
  v8::Local<v8::String> func_name =
      v8::String::NewFromUtf8(GetIsolate(), kJSAutomationInternalV8Listeners)
          .ToLocalChecked();
  v8::Local<v8::Value> func_value =
      context->Global()->Get(context, func_name).ToLocalChecked();
  v8::Local<v8::Function> get_event_listeners_from_name_function =
      v8::Local<v8::Function>::Cast(func_value);

  // Prepare the argument for the 'getEventListenersFromName' call
  v8::Local<v8::Value> args[] = {
      v8::String::NewFromUtf8(GetIsolate(), event_name.c_str())
          .ToLocalChecked()};

  // Call the 'getEventListenersFromName' function using the event that needs to
  // be dispatched. The return value is an object containing all listeners to
  // that event.
  v8::Local<v8::Value> result = get_event_listeners_from_name_function
                                    ->Call(context, context->Global(), 1, args)
                                    .ToLocalChecked();
  CHECK(result->IsObject()) << "Unknown event type: " << event_name;

  v8::Local<v8::Object> event_listeners_object =
      v8::Local<v8::Object>::Cast(result);

  v8::Local<v8::String> method_name =
      v8::String::NewFromUtf8(GetIsolate(), kJSCallListeners).ToLocalChecked();
  v8::Local<v8::Value> method_value =
      event_listeners_object->Get(context, method_name).ToLocalChecked();
  v8::Local<v8::Function> method = v8::Local<v8::Function>::Cast(method_value);

  std::vector<v8::Local<v8::Value>> js_event_args;
  for (const base::Value& value : event_args) {
    v8::Local<v8::Value> js_event_arg =
        V8ValueConverter ::GetInstance()->ConvertToV8Value(value, context);
    js_event_args.push_back(js_event_arg);
  }

  v8::MaybeLocal<v8::Value> unused_result =
      method->Call(context, event_listeners_object, js_event_args.size(),
                   js_event_args.data());
  CHECK(!unused_result.IsEmpty());
}

}  // namespace ax