blob: e43165045a748bc5743c10c175856cf85aaa857f [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/renderer/accessibility/ax_tree_distiller.h"
#include <memory>
#include <queue>
#include <vector>
#include "base/containers/contains.h"
#include "base/strings/utf_string_conversions.h"
#include "content/renderer/accessibility/ax_tree_snapshotter_impl.h"
#include "content/renderer/render_frame_impl.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_tree.h"
namespace {
static constexpr int kMaxNodes = 5000;
// TODO: Consider moving this to AXNodeProperties.
static const ax::mojom::Role kContentRoles[]{
ax::mojom::Role::kHeading,
ax::mojom::Role::kParagraph,
};
// TODO: Consider moving this to AXNodeProperties.
static const ax::mojom::Role kRolesToSkip[]{
ax::mojom::Role::kAudio,
ax::mojom::Role::kBanner,
ax::mojom::Role::kButton,
ax::mojom::Role::kComplementary,
ax::mojom::Role::kContentInfo,
ax::mojom::Role::kFooter,
ax::mojom::Role::kFooterAsNonLandmark,
ax::mojom::Role::kHeader,
ax::mojom::Role::kHeaderAsNonLandmark,
ax::mojom::Role::kImage,
ax::mojom::Role::kLabelText,
ax::mojom::Role::kNavigation,
};
int32_t GetNumberOfChildParagraphs(const ui::AXNode* node) {
int n = 0;
for (auto iter = node->UnignoredChildrenBegin();
iter != node->UnignoredChildrenEnd(); ++iter) {
if (iter.get()->GetRole() == ax::mojom::Role::kParagraph)
n++;
}
return n;
}
// TODO(crbug.com/1266555): Replace this with a call to
// OneShotAccessibilityTreeSearch.
// The article node is the one which contains the most number of paragraphs.
// TODO(crbug.com/1266555): Ensure that this also includes the header node.
const ui::AXNode* GetArticleNode(const ui::AXNode* node) {
const ui::AXNode* article = nullptr;
int32_t max = 0;
std::queue<const ui::AXNode*> queue;
queue.push(node);
while (!queue.empty()) {
const ui::AXNode* popped = queue.front();
queue.pop();
int32_t n = GetNumberOfChildParagraphs(popped);
if (n > max) {
max = n;
article = popped;
}
if (!base::Contains(kContentRoles, node->GetRole())) {
for (auto iter = popped->UnignoredChildrenBegin();
iter != popped->UnignoredChildrenEnd(); ++iter) {
queue.push(iter.get());
}
}
}
return article;
}
// Recurse through the root node, searching for content nodes (any node whose
// role is in kContentRoles). Skip branches which begin with a node with role
// in kRolesToSkip. Once a content node is identified, add it to the vector
// |content_node_ids|, whose pointer is passed through the recursion.
void AddContentNodesToVector(const ui::AXNode* node,
std::vector<ui::AXNodeID>* content_node_ids) {
if (base::Contains(kContentRoles, node->GetRole())) {
content_node_ids->emplace_back(node->id());
return;
}
if (base::Contains(kRolesToSkip, node->GetRole()))
return;
for (auto iter = node->UnignoredChildrenBegin();
iter != node->UnignoredChildrenEnd(); ++iter) {
AddContentNodesToVector(iter.get(), content_node_ids);
}
}
} // namespace
namespace content {
AXTreeDistiller::AXTreeDistiller(RenderFrameImpl* render_frame)
: render_frame_(render_frame) {
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
if (features::IsReadAnythingWithScreen2xEnabled()) {
render_frame_->GetBrowserInterfaceBroker()->GetInterface(
main_content_extractor_.BindNewPipeAndPassReceiver());
}
#endif
}
AXTreeDistiller::~AXTreeDistiller() = default;
void AXTreeDistiller::Distill() {
SnapshotAXTree();
DistillAXTree();
// TODO(https://crbug.com/1278249): Move the call to a proper place.
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
if (features::IsReadAnythingWithScreen2xEnabled())
ScheduleScreen2xRun();
#endif
}
void AXTreeDistiller::SnapshotAXTree() {
// If snapshot_ is already cached, do nothing.
if (snapshot_)
return;
snapshot_ = std::make_unique<ui::AXTreeUpdate>();
// Get page contents (via snapshot of a11y tree) for reader generation.
// |ui::AXMode::kHTML| is needed for URL information.
// |ui::AXMode::kScreenReader| is needed for heading level information.
ui::AXMode ax_mode =
ui::AXMode::kWebContents | ui::AXMode::kHTML | ui::AXMode::kScreenReader;
AXTreeSnapshotterImpl snapshotter(render_frame_, ax_mode);
snapshotter.Snapshot(
/* exclude_offscreen= */ false, kMaxNodes,
/* timeout= */ {}, snapshot_.get());
}
void AXTreeDistiller::DistillAXTree() {
// If content_node_ids_ is already cached, do nothing.
if (content_node_ids_)
return;
content_node_ids_ = std::make_unique<std::vector<ui::AXNodeID>>();
DCHECK(snapshot_);
ui::AXTree tree;
bool success = tree.Unserialize(*snapshot_);
if (!success)
return;
const ui::AXNode* article_node = GetArticleNode(tree.root());
// If this page does not have an article node, this means it is not
// distillable.
if (!article_node) {
is_distillable_ = false;
return;
}
AddContentNodesToVector(article_node, content_node_ids_.get());
}
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
void AXTreeDistiller::ScheduleScreen2xRun() {
DCHECK(main_content_extractor_.is_bound());
DCHECK(snapshot_);
main_content_extractor_->ExtractMainContent(
*snapshot_, base::BindOnce(&AXTreeDistiller::ProcessScreen2xResult,
weak_ptr_factory_.GetWeakPtr()));
}
void AXTreeDistiller::ProcessScreen2xResult(
const std::vector<ui::AXNodeID>& content_node_ids) {
// TODO(https://crbug.com/1278249): Use |content_node_ids|.
}
#endif
} // namespace content