chromium/services/accessibility/android/pane_title_handler.cc

// Copyright 2024 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/android/pane_title_handler.h"

#include <cstdint>
#include <memory>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "services/accessibility/android/accessibility_info_data_wrapper.h"
#include "services/accessibility/android/accessibility_node_info_data_wrapper.h"
#include "services/accessibility/android/android_accessibility_util.h"
#include "services/accessibility/android/ax_tree_source_android.h"
#include "services/accessibility/android/public/mojom/accessibility_helper.mojom-forward.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/gfx/geometry/rect.h"

namespace ax::android {

namespace {

class PaneTitleProviderNode : public AccessibilityInfoDataWrapper {
 public:
  PaneTitleProviderNode(AXTreeSourceAndroid* tree_source,
                        int32_t id,
                        std::string name)
      : AccessibilityInfoDataWrapper(tree_source), id_(id), name_(name) {}

  PaneTitleProviderNode(const PaneTitleProviderNode&) = delete;
  PaneTitleProviderNode& operator=(const PaneTitleProviderNode&) = delete;

  // AccessibilityInfoDataWrapper overrides.
  bool IsNode() const override { return false; }
  mojom::AccessibilityNodeInfoData* GetNode() const override { return nullptr; }
  mojom::AccessibilityWindowInfoData* GetWindow() const override {
    return nullptr;
  }
  int32_t GetId() const override { return id_; }
  const gfx::Rect GetBounds() const override { return gfx::Rect(0, 0, 1, 1); }
  bool IsVisibleToUser() const override { return false; }
  bool IsWebNode() const override { return false; }
  bool IsIgnored() const override { return false; }
  bool IsImportantInAndroid() const override { return true; }
  bool IsFocusableInFullFocusMode() const override { return false; }
  bool IsAccessibilityFocusableContainer() const override { return false; }
  void PopulateAXRole(ui::AXNodeData* out_data) const override {
    out_data->role = ax::mojom::Role::kGenericContainer;
  }
  void PopulateAXState(ui::AXNodeData* out_data) const override {}
  void Serialize(ui::AXNodeData* out_data) const override {
    AccessibilityInfoDataWrapper::Serialize(out_data);

    out_data->SetName(ComputeAXName(false));

    out_data->AddStringAttribute(ax::mojom::StringAttribute::kLiveStatus,
                                 "polite");
    out_data->AddStringAttribute(
        ax::mojom::StringAttribute::kContainerLiveStatus, "polite");

    out_data->AddState(ax::mojom::State::kInvisible);
  }
  std::string ComputeAXName(bool do_recursive) const override { return name_; }
  void GetChildren(
      std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>*
          children) const override {}
  int32_t GetWindowId() const override {
    DUMP_WILL_BE_NOTREACHED();
    return -1;
  }

 private:
  const int32_t id_;
  const std::string name_;
};

std::optional<std::string> GetPaneTitle(AccessibilityInfoDataWrapper* node) {
  if (!node || !node->GetNode()) {
    return std::nullopt;
  }
  std::string pane_title;
  if (!GetProperty(node->GetNode()->string_properties,
                   mojom::AccessibilityStringProperty::PANE_TITLE,
                   &pane_title) ||
      pane_title.empty()) {
    return std::nullopt;
  }
  return pane_title;
}

}  // namespace

// static
std::optional<std::pair<int32_t, std::unique_ptr<PaneTitleHandler>>>
PaneTitleHandler::CreateIfNecessary(
    AXTreeSourceAndroid* tree_source,
    const mojom::AccessibilityEventData& event_data) {
  // Creates a handler on PANE_APPEARED event, which is a subtype of
  // WINDOW_STATE_CHANGED. pant title attribute may be added before the event,
  // but triggering on event allows us to only need to check the event source,
  // not the entire tree.
  if (event_data.event_type !=
      mojom::AccessibilityEventType::WINDOW_STATE_CHANGED) {
    return std::nullopt;
  }

  if (!event_data.int_list_properties) {
    return std::nullopt;
  }
  const auto& itr = event_data.int_list_properties->find(
      mojom::AccessibilityEventIntListProperty::CONTENT_CHANGE_TYPES);
  if (itr == event_data.int_list_properties->end() ||
      base::ranges::find(
          itr->second,
          static_cast<int32_t>(mojom::ContentChangeType::PANE_APPEARED)) ==
          itr->second.end()) {
    return std::nullopt;
  }

  auto* source_node = tree_source->GetFromId(event_data.source_id);
  std::optional<std::string> pane_title = GetPaneTitle(source_node);
  if (!pane_title) {
    return std::nullopt;
  }

  // hook on the root and the virtual node is added as its child.
  auto* root_node = tree_source->GetRoot();
  CHECK(root_node);

  // Setting a quite large ID here assuming that when normal ids practically
  // never hit this number.
  static int32_t next_virtual_node_id = 1'000'000'000;
  if (tree_source->GetFromId(next_virtual_node_id)) {
    LOG(ERROR) << "Virtual ID Conflict. Not adding a pane title handler.";
    return std::nullopt;
  }

  return std::make_pair(
      root_node->GetId(),
      std::make_unique<PaneTitleHandler>(next_virtual_node_id++,
                                         source_node->GetId(), *pane_title));
}

bool PaneTitleHandler::PreDispatchEvent(
    AXTreeSourceAndroid* tree_source,
    const mojom::AccessibilityEventData& event_data) {
  auto* source_node = tree_source->GetFromId(pane_node_id_);
  std::optional<std::string> pane_title = GetPaneTitle(source_node);
  name_ = pane_title.value_or(base::EmptyString());

  tree_source->SetVirtualNode(
      tree_source->GetRoot()->GetId(),
      std::make_unique<PaneTitleProviderNode>(
          tree_source, virtual_node_id_,
          creation_done_ ? name_ : base::EmptyString()));
  creation_done_ = true;

  return true;
}

void PaneTitleHandler::PostSerializeNode(ui::AXNodeData* out_data) const {}

bool PaneTitleHandler::ShouldDestroy(AXTreeSourceAndroid* tree_source) const {
  auto* pane_node = tree_source->GetFromId(pane_node_id_);
  std::optional<std::string> pane_title = GetPaneTitle(pane_node);
  return !pane_title;
}

}  // namespace ax::android