[go: nahoru, domu]

blob: 505149384a0669d0da741ae893d4047d34ba19bd [file] [log] [blame]
// 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 "chrome/renderer/accessibility/ax_tree_distiller.h"
#include <memory>
#include <queue>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/strings/utf_string_conversions.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "services/metrics/public/cpp/mojo_ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_tree.h"
namespace {
// TODO: Consider moving this to AXNodeProperties.
static const ax::mojom::Role kContentRoles[]{
ax::mojom::Role::kHeading, ax::mojom::Role::kParagraph,
ax::mojom::Role::kNote, ax::mojom::Role::kImage,
ax::mojom::Role::kFigcaption};
// 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::kLabelText,
ax::mojom::Role::kNavigation,
};
// Find all of the main and article nodes. Also, include unignored heading nodes
// which lie outside of the main and article node.
// TODO(crbug.com/1266555): Replace this with a call to
// OneShotAccessibilityTreeSearch.
void GetContentRootNodes(const ui::AXNode* root,
std::vector<const ui::AXNode*>* content_root_nodes) {
if (!root) {
return;
}
std::queue<const ui::AXNode*> queue;
queue.push(root);
bool has_main_or_heading = false;
while (!queue.empty()) {
const ui::AXNode* node = queue.front();
queue.pop();
// If a main or article node is found, add it to the list of content root
// nodes and continue. Do not explore children for nested article nodes.
if (node->GetRole() == ax::mojom::Role::kMain ||
node->GetRole() == ax::mojom::Role::kArticle) {
content_root_nodes->push_back(node);
has_main_or_heading = true;
continue;
}
// If a heading node is found, add it to the list of content root nodes,
// too. It may be removed later if the tree doesn't contain a main or
// article node.
if (node->GetRole() == ax::mojom::Role::kHeading) {
content_root_nodes->push_back(node);
continue;
}
for (auto iter = node->UnignoredChildrenBegin();
iter != node->UnignoredChildrenEnd(); ++iter) {
queue.push(iter.get());
}
}
if (!has_main_or_heading) {
content_root_nodes->clear();
}
}
// 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) {
const auto& role = node->GetRole();
if (base::Contains(kContentRoles, role)) {
// TODO(1464340): Remove when flag is no longer necessary. Skip these roles
// if the flag is not enabled.
if (!features::IsReadAnythingImagesViaAlgorithmEnabled() &&
(role == ax::mojom::Role::kFigcaption ||
role == ax::mojom::Role::kImage)) {
return;
}
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
AXTreeDistiller::AXTreeDistiller(
OnAXTreeDistilledCallback on_ax_tree_distilled_callback)
: on_ax_tree_distilled_callback_(on_ax_tree_distilled_callback) {
// TODO(crbug.com/1450930): Use a global ukm recorder instance instead.
mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
content::RenderThread::Get()->BindHostReceiver(
factory.BindNewPipeAndPassReceiver());
ukm_recorder_ = ukm::MojoUkmRecorder::Create(*factory);
}
AXTreeDistiller::~AXTreeDistiller() = default;
void AXTreeDistiller::Distill(const ui::AXTree& tree,
const ui::AXTreeUpdate& snapshot,
const ukm::SourceId ukm_source_id) {
base::TimeTicks start_time = base::TimeTicks::Now();
std::vector<ui::AXNodeID> content_node_ids;
if (features::IsReadAnythingWithAlgorithmEnabled()) {
// Try with the algorithm first.
DistillViaAlgorithm(tree, ukm_source_id, &content_node_ids);
}
// If Read Anything with Screen 2x is enabled and the main content extractor
// is bound, kick off Screen 2x run, which distills the AXTree in the
// utility process using ML.
if (features::IsReadAnythingWithScreen2xEnabled() &&
main_content_extractor_.is_bound()) {
DistillViaScreen2x(tree, snapshot, ukm_source_id, start_time,
&content_node_ids);
return;
}
// Ensure we still callback if Screen2x is not available.
on_ax_tree_distilled_callback_.Run(tree.GetAXTreeID(), content_node_ids);
}
void AXTreeDistiller::DistillViaAlgorithm(
const ui::AXTree& tree,
const ukm::SourceId ukm_source_id,
std::vector<ui::AXNodeID>* content_node_ids) {
base::TimeTicks start_time = base::TimeTicks::Now();
std::vector<const ui::AXNode*> content_root_nodes;
GetContentRootNodes(tree.root(), &content_root_nodes);
for (const ui::AXNode* content_root_node : content_root_nodes) {
AddContentNodesToVector(content_root_node, content_node_ids);
}
RecordRulesMetrics(ukm_source_id, base::TimeTicks::Now() - start_time,
!content_node_ids->empty());
}
void AXTreeDistiller::RecordRulesMetrics(ukm::SourceId ukm_source_id,
base::TimeDelta elapsed_time,
bool success) {
if (success) {
base::UmaHistogramTimes(
"Accessibility.ReadAnything.RulesDistillationTime.Success",
elapsed_time);
ukm::builders::Accessibility_ReadAnything(ukm_source_id)
.SetRulesDistillationTime_Success(elapsed_time.InMilliseconds())
.Record(ukm_recorder_.get());
} else {
base::UmaHistogramTimes(
"Accessibility.ReadAnything.RulesDistillationTime.Failure",
elapsed_time);
ukm::builders::Accessibility_ReadAnything(ukm_source_id)
.SetRulesDistillationTime_Failure(elapsed_time.InMilliseconds())
.Record(ukm_recorder_.get());
}
}
void AXTreeDistiller::DistillViaScreen2x(
const ui::AXTree& tree,
const ui::AXTreeUpdate& snapshot,
const ukm::SourceId ukm_source_id,
base::TimeTicks start_time,
std::vector<ui::AXNodeID>* content_node_ids_algorithm) {
DCHECK(main_content_extractor_.is_bound());
// Make a copy of |content_node_ids_algorithm| rather than sending a pointer.
main_content_extractor_->ExtractMainContent(
snapshot, ukm_source_id,
base::BindOnce(&AXTreeDistiller::ProcessScreen2xResult,
weak_ptr_factory_.GetWeakPtr(), tree.GetAXTreeID(),
ukm_source_id, start_time, *content_node_ids_algorithm));
}
void AXTreeDistiller::ProcessScreen2xResult(
const ui::AXTreeID& tree_id,
const ukm::SourceId ukm_source_id,
base::TimeTicks start_time,
std::vector<ui::AXNodeID> content_node_ids_algorithm,
const std::vector<ui::AXNodeID>& content_node_ids_screen2x) {
// Merge the results from the algorithm and from screen2x.
for (ui::AXNodeID content_node_id_screen2x : content_node_ids_screen2x) {
if (!base::Contains(content_node_ids_algorithm, content_node_id_screen2x)) {
content_node_ids_algorithm.push_back(content_node_id_screen2x);
}
}
RecordMergedMetrics(ukm_source_id, base::TimeTicks::Now() - start_time,
!content_node_ids_algorithm.empty());
on_ax_tree_distilled_callback_.Run(tree_id, content_node_ids_algorithm);
// TODO(crbug.com/1266555): If no content nodes were identified, and
// there is a selection, try sending Screen2x a partial tree just containing
// the selected nodes.
}
void AXTreeDistiller::ScreenAIServiceReady(content::RenderFrame* render_frame) {
if (main_content_extractor_.is_bound() || !render_frame) {
return;
}
render_frame->GetBrowserInterfaceBroker()->GetInterface(
main_content_extractor_.BindNewPipeAndPassReceiver());
main_content_extractor_.set_disconnect_handler(
base::BindOnce(&AXTreeDistiller::OnMainContentExtractorDisconnected,
weak_ptr_factory_.GetWeakPtr()));
}
void AXTreeDistiller::OnMainContentExtractorDisconnected() {
on_ax_tree_distilled_callback_.Run(ui::AXTreeIDUnknown(),
std::vector<ui::AXNodeID>());
}
void AXTreeDistiller::RecordMergedMetrics(ukm::SourceId ukm_source_id,
base::TimeDelta elapsed_time,
bool success) {
if (success) {
base::UmaHistogramTimes(
"Accessibility.ReadAnything.MergedDistillationTime.Success",
elapsed_time);
ukm::builders::Accessibility_ReadAnything(ukm_source_id)
.SetMergedDistillationTime_Success(elapsed_time.InMilliseconds())
.Record(ukm_recorder_.get());
} else {
base::UmaHistogramTimes(
"Accessibility.ReadAnything.MergedDistillationTime.Failure",
elapsed_time);
ukm::builders::Accessibility_ReadAnything(ukm_source_id)
.SetMergedDistillationTime_Failure(elapsed_time.InMilliseconds())
.Record(ukm_recorder_.get());
}
}