chromium/services/accessibility/android/auto_complete_handler.cc

// Copyright 2020 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/auto_complete_handler.h"

#include "base/memory/raw_ptr.h"
#include "services/accessibility/android/accessibility_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.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/platform/ax_android_constants.h"

namespace {

// The list value aria-autocomplete property.
constexpr char kAutoCompleteListAttribute[] = "list";

bool IsAutoComplete(ax::android::mojom::AccessibilityNodeInfoData* node) {
  if (!node || !node->string_properties) {
    return false;
  }

  auto it = node->string_properties->find(
      ax::android::mojom::AccessibilityStringProperty::CLASS_NAME);
  if (it == node->string_properties->end()) {
    return false;
  }

  return it->second == ui::kAXAutoCompleteTextViewClassname ||
         it->second == ui::kAXMultiAutoCompleteTextViewClassname;
}

int32_t GetAnchorId(ax::android::mojom::AccessibilityWindowInfoData* window) {
  if (!window) {
    return ui::kInvalidAXNodeID;
  }

  int32_t id;
  if (ax::android::GetProperty(
          window->int_properties,
          ax::android::mojom::AccessibilityWindowIntProperty::ANCHOR_NODE_ID,
          &id)) {
    return id;
  }
  return ui::kInvalidAXNodeID;
}

}  // namespace

namespace ax::android {

AutoCompleteHandler::AutoCompleteHandler(const int32_t editable_node_id)
    : anchored_node_id_(editable_node_id) {}

AutoCompleteHandler::~AutoCompleteHandler() = default;

// static
std::vector<AutoCompleteHandler::IdAndHandler>
AutoCompleteHandler::CreateIfNecessary(
    AXTreeSourceAndroid* tree_source,
    const mojom::AccessibilityEventData& event_data) {
  if (event_data.event_type !=
      mojom::AccessibilityEventType::WINDOW_CONTENT_CHANGED) {
    return {};
  }

  std::vector<IdAndHandler> results;

  // Check all updated nodes under the event source.
  std::vector<raw_ptr<AccessibilityInfoDataWrapper, VectorExperimental>>
      to_visit;
  to_visit.push_back(tree_source->GetFromId(event_data.source_id));
  while (!to_visit.empty()) {
    AccessibilityInfoDataWrapper* node_ptr = to_visit.back();
    to_visit.pop_back();
    if (!node_ptr) {
      continue;
    }

    const int32_t node_id = node_ptr->GetId();
    if (IsAutoComplete(node_ptr->GetNode())) {
      results.emplace_back(node_id,
                           std::make_unique<AutoCompleteHandler>(node_id));
    }

    node_ptr->GetChildren(&to_visit);
  }

  return results;
}

bool AutoCompleteHandler::PreDispatchEvent(
    AXTreeSourceAndroid* tree_source,
    const mojom::AccessibilityEventData& event_data) {
  if (event_data.event_type == mojom::AccessibilityEventType::WINDOWS_CHANGED) {
    // Check if a popup window anchoring the node exists.
    // The first window is the main window. Look for other windows.
    for (size_t i = 1; i < event_data.window_data->size(); ++i) {
      if (GetAnchorId(event_data.window_data->at(i).get()) !=
          anchored_node_id_) {
        continue;
      }

      int32_t window_id = event_data.window_data->at(i)->window_id;
      if (suggestion_window_id_ != window_id) {
        // Anchoring window has changed.
        suggestion_window_id_ = window_id;
        return true;
      } else {
        // Nothing related changed. No need to update.
        return false;
      }
    }

    if (suggestion_window_id_.has_value()) {
      // No popup window found. The window has disappeared.
      suggestion_window_id_.reset();
      selected_node_id_.reset();
      return true;
    }
    return false;
  } else if (event_data.event_type ==
             mojom::AccessibilityEventType::VIEW_SELECTED) {
    // VIEW_SELECTED event from the suggestion list is a user's selecting action
    // in a candidate.
    if (!suggestion_window_id_.has_value()) {
      return false;
    }

    AccessibilityInfoDataWrapper* source_node =
        tree_source->GetFromId(event_data.source_id);
    if (!source_node || source_node->GetWindowId() != suggestion_window_id_) {
      return false;
    }

    AccessibilityInfoDataWrapper* selected_node =
        GetSelectedNodeInfoFromAdapterViewEvent(event_data, source_node);
    if (!selected_node || selected_node->GetId() == selected_node_id_) {
      return false;
    }

    selected_node_id_ = selected_node->GetId();
    return true;
  }
  return false;
}

void AutoCompleteHandler::PostSerializeNode(ui::AXNodeData* out_data) const {
  DCHECK_EQ(out_data->role, ax::mojom::Role::kTextField);

  out_data->AddStringAttribute(ax::mojom::StringAttribute::kAutoComplete,
                               kAutoCompleteListAttribute);

  if (suggestion_window_id_.has_value()) {
    out_data->AddState(ax::mojom::State::kExpanded);
    out_data->AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds,
                                  {suggestion_window_id_.value()});
    if (selected_node_id_.has_value()) {
      out_data->AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
                                selected_node_id_.value());
    }
  } else {
    out_data->AddState(ax::mojom::State::kCollapsed);
  }
}

bool AutoCompleteHandler::ShouldDestroy(
    AXTreeSourceAndroid* tree_source) const {
  return tree_source->GetFromId(anchored_node_id_) == nullptr;
}

}  // namespace ax::android