blob: fbf256507573da24f734ec7b2c4533784f5b7a65 [file] [log] [blame]
// Copyright (c) 2012 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/render_accessibility_impl.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <set>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/containers/queue.h"
#include "base/debug/crash_logging.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "content/public/renderer/render_thread.h"
#include "content/renderer/accessibility/ax_action_target_factory.h"
#include "content/renderer/accessibility/ax_image_annotator.h"
#include "content/renderer/accessibility/blink_ax_action_target.h"
#include "content/renderer/accessibility/render_accessibility_manager.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_frame_proxy.h"
#include "content/renderer/render_view_impl.h"
#include "services/image_annotation/public/mojom/image_annotation.mojom.h"
#include "services/metrics/public/cpp/mojo_ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_float_rect.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_input_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_settings.h"
#include "third_party/blink/public/web/web_view.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
using blink::WebAXContext;
using blink::WebAXObject;
using blink::WebDocument;
using blink::WebElement;
using blink::WebFloatRect;
using blink::WebLocalFrame;
using blink::WebNode;
using blink::WebRect;
using blink::WebSettings;
using blink::WebView;
namespace {
// The amount of time, in milliseconds, to wait before sending accessibility
// events that are deferred rather than being sent right away. As one
// example this is used during initial page load.
constexpr int kDelayForDeferredEvents = 350;
// The minimum amount of time in milliseconds that should be spent
// in serializing code in order to report the elapsed time as a URL-keyed
// metric.
constexpr int kMinSerializationTimeToSendInMS = 100;
// When URL-keyed metrics for the amount of time spent in serializing code
// are sent, the minimum amount of time to wait, in seconds, before
// sending metrics. Metrics may also be sent once per page transition.
constexpr int kMinUKMDelayInSeconds = 300;
void SetAccessibilityCrashKey(ui::AXMode mode) {
// Add a crash key with the ax_mode, to enable searching for top crashes that
// occur when accessibility is turned on. This adds it for each renderer,
// process, and elsewhere the same key is added for the browser process.
// Note: in theory multiple renderers in the same process might not have the
// same mode. As an example, kLabelImages could be enabled for just one
// renderer. The presence if a mode flag means in a crash report means at
// least one renderer in the same process had that flag.
// Examples of when multiple renderers could share the same process:
// 1) Android, 2) When many tabs are open.
static auto* ax_mode_crash_key = base::debug::AllocateCrashKeyString(
"ax_mode", base::debug::CrashKeySize::Size64);
if (ax_mode_crash_key)
base::debug::SetCrashKeyString(ax_mode_crash_key, mode.ToString());
}
}
namespace content {
// Cap the number of nodes returned in an accessibility
// tree snapshot to avoid outrageous memory or bandwidth
// usage.
const size_t kMaxSnapshotNodeCount = 5000;
AXTreeSnapshotterImpl::AXTreeSnapshotterImpl(RenderFrameImpl* render_frame)
: render_frame_(render_frame) {
DCHECK(render_frame->GetWebFrame());
blink::WebDocument document_ = render_frame->GetWebFrame()->GetDocument();
context_ = std::make_unique<WebAXContext>(document_);
}
AXTreeSnapshotterImpl::~AXTreeSnapshotterImpl() = default;
void AXTreeSnapshotterImpl::Snapshot(ui::AXMode ax_mode,
size_t max_node_count,
ui::AXTreeUpdate* response) {
// Get a snapshot of the accessibility tree as an AXContentNodeData.
AXContentTreeUpdate content_tree;
SnapshotContentTree(ax_mode, max_node_count, &content_tree);
// As a sanity check, node_id_to_clear and event_from should be uninitialized
// if this is a full tree snapshot. They'd only be set to something if
// this was indeed a partial update to the tree (which we don't want).
DCHECK_EQ(0, content_tree.node_id_to_clear);
DCHECK_EQ(ax::mojom::EventFrom::kNone, content_tree.event_from);
// We now have a complete serialization of the accessibility tree, but it
// includes a few fields we don't want to export outside of content/,
// so copy it into a more generic ui::AXTreeUpdate instead.
response->root_id = content_tree.root_id;
response->nodes.resize(content_tree.nodes.size());
response->node_id_to_clear = content_tree.node_id_to_clear;
response->event_from = content_tree.event_from;
// AXNodeData is a superclass of AXContentNodeData, so we can convert
// just by assigning.
response->nodes.assign(content_tree.nodes.begin(), content_tree.nodes.end());
}
void AXTreeSnapshotterImpl::SnapshotContentTree(ui::AXMode ax_mode,
size_t max_node_count,
AXContentTreeUpdate* response) {
WebAXObject root = context_->Root();
if (!root.UpdateLayoutAndCheckValidity())
return;
BlinkAXTreeSource tree_source(render_frame_, ax_mode);
tree_source.SetRoot(root);
ScopedFreezeBlinkAXTreeSource freeze(&tree_source);
// The serializer returns an AXContentTreeUpdate, which can store a complete
// or a partial accessibility tree. AXTreeSerializer is stateful, but the
// first time you serialize from a brand-new tree you're guaranteed to get a
// complete tree.
BlinkAXTreeSerializer serializer(&tree_source);
if (max_node_count)
serializer.set_max_node_count(max_node_count);
if (serializer.SerializeChanges(root, response))
return;
// It's possible for the page to fail to serialize the first time due to
// aria-owns rearranging the page while it's being scanned. Try a second
// time.
*response = AXContentTreeUpdate();
if (serializer.SerializeChanges(root, response))
return;
// It failed again. Clear the response object because it might have errors.
*response = AXContentTreeUpdate();
LOG(WARNING) << "Unable to serialize accessibility tree.";
}
// static
void RenderAccessibilityImpl::SnapshotAccessibilityTree(
RenderFrameImpl* render_frame,
AXContentTreeUpdate* response,
ui::AXMode ax_mode) {
TRACE_EVENT0("accessibility",
"RenderAccessibilityImpl::SnapshotAccessibilityTree");
DCHECK(render_frame);
DCHECK(response);
if (!render_frame->GetWebFrame())
return;
AXTreeSnapshotterImpl snapshotter(render_frame);
snapshotter.SnapshotContentTree(ax_mode, kMaxSnapshotNodeCount, response);
}
RenderAccessibilityImpl::RenderAccessibilityImpl(
RenderAccessibilityManager* const render_accessibility_manager,
RenderFrameImpl* const render_frame,
ui::AXMode mode)
: RenderFrameObserver(render_frame),
render_accessibility_manager_(render_accessibility_manager),
render_frame_(render_frame),
tree_source_(std::make_unique<BlinkAXTreeSource>(render_frame, mode)),
serializer_(std::make_unique<BlinkAXTreeSerializer>(tree_source_.get())),
plugin_tree_source_(nullptr),
last_scroll_offset_(gfx::Size()),
event_schedule_status_(EventScheduleStatus::kNotWaiting),
reset_token_(0),
ukm_timer_(std::make_unique<base::ElapsedTimer>()),
last_ukm_source_id_(ukm::kInvalidSourceId) {
mojo::PendingRemote<ukm::mojom::UkmRecorderInterface> recorder;
content::RenderThread::Get()->BindHostReceiver(
recorder.InitWithNewPipeAndPassReceiver());
ukm_recorder_ = std::make_unique<ukm::MojoUkmRecorder>(std::move(recorder));
WebView* web_view = render_frame_->GetRenderView()->GetWebView();
WebSettings* settings = web_view->GetSettings();
SetAccessibilityCrashKey(mode);
#if defined(OS_ANDROID)
// Password values are only passed through on Android.
settings->SetAccessibilityPasswordValuesEnabled(true);
#endif
#if !defined(OS_ANDROID)
// Inline text boxes can be enabled globally on all except Android.
// On Android they can be requested for just a specific node.
if (mode.has_mode(ui::AXMode::kInlineTextBoxes))
settings->SetInlineTextBoxAccessibilityEnabled(true);
#endif
#if defined(OS_MACOSX)
// aria-modal currently prunes the accessibility tree on Mac only.
settings->SetAriaModalPrunesAXTree(true);
#endif
const WebDocument& document = GetMainDocument();
if (!document.IsNull()) {
ax_context_ = std::make_unique<WebAXContext>(document);
StartOrStopLabelingImages(ui::AXMode(), mode);
// It's possible that the webview has already loaded a webpage without
// accessibility being enabled. Initialize the browser's cached
// accessibility tree by sending it a notification.
WebAXObject root_object = WebAXObject::FromWebDocument(document);
HandleAXEvent(
ui::AXEvent(root_object.AxID(), ax::mojom::Event::kLayoutComplete));
}
image_annotation_debugging_ =
base::CommandLine::ForCurrentProcess()->HasSwitch(
::switches::kEnableExperimentalAccessibilityLabelsDebugging);
}
RenderAccessibilityImpl::~RenderAccessibilityImpl() = default;
void RenderAccessibilityImpl::DidCreateNewDocument() {
const WebDocument& document = GetMainDocument();
if (!document.IsNull())
ax_context_ = std::make_unique<WebAXContext>(document);
}
void RenderAccessibilityImpl::DidCommitProvisionalLoad(
ui::PageTransition transition) {
has_injected_stylesheet_ = false;
// If we have events scheduled, but not sent, cancel them
CancelScheduledEvents();
// Defer events during initial page load.
event_schedule_mode_ = EventScheduleMode::kDeferEvents;
MaybeSendUKM();
slowest_serialization_ms_ = 0;
ukm_timer_ = std::make_unique<base::ElapsedTimer>();
// Remove the image annotator if the page is loading and it was added for
// the one-shot image annotation (i.e. AXMode for image annotation is not
// set).
if (!ax_image_annotator_ ||
GetAccessibilityMode().has_mode(ui::AXMode::kLabelImages)) {
return;
}
tree_source_->RemoveImageAnnotator();
ax_image_annotator_->Destroy();
ax_image_annotator_.release();
page_language_.clear();
}
void RenderAccessibilityImpl::AccessibilityModeChanged(const ui::AXMode& mode) {
ui::AXMode old_mode = GetAccessibilityMode();
if (old_mode == mode)
return;
tree_source_->SetAccessibilityMode(mode);
SetAccessibilityCrashKey(mode);
#if !defined(OS_ANDROID)
// Inline text boxes can be enabled globally on all except Android.
// On Android they can be requested for just a specific node.
RenderView* render_view = render_frame_->GetRenderView();
if (render_view) {
WebView* web_view = render_view->GetWebView();
if (web_view) {
WebSettings* settings = web_view->GetSettings();
if (settings) {
if (mode.has_mode(ui::AXMode::kInlineTextBoxes)) {
settings->SetInlineTextBoxAccessibilityEnabled(true);
tree_source_->GetRoot().UpdateLayoutAndCheckValidity();
tree_source_->GetRoot().LoadInlineTextBoxes();
} else {
settings->SetInlineTextBoxAccessibilityEnabled(false);
}
}
}
}
#endif // !defined(OS_ANDROID)
serializer_->Reset();
const WebDocument& document = GetMainDocument();
if (!document.IsNull()) {
StartOrStopLabelingImages(old_mode, mode);
// If there are any events in flight, |HandleAXEvent| will refuse to process
// our new event.
pending_events_.clear();
auto root_object = WebAXObject::FromWebDocument(document);
ax::mojom::Event event = root_object.IsLoaded()
? ax::mojom::Event::kLoadComplete
: ax::mojom::Event::kLayoutComplete;
HandleAXEvent(ui::AXEvent(root_object.AxID(), event));
}
}
void RenderAccessibilityImpl::HitTest(
const gfx::Point& point,
ax::mojom::Event event_to_fire,
int request_id,
mojom::RenderAccessibility::HitTestCallback callback) {
WebAXObject ax_object;
const WebDocument& document = GetMainDocument();
if (!document.IsNull()) {
auto root_obj = WebAXObject::FromWebDocument(document);
if (root_obj.UpdateLayoutAndCheckValidity())
ax_object = root_obj.HitTest(point);
}
// Return if no attached accessibility object was found for the main document.
if (ax_object.IsDetached()) {
std::move(callback).Run(/*hit_test_response=*/nullptr);
return;
}
// If the result was in the same frame, return the result.
AXContentNodeData data;
ScopedFreezeBlinkAXTreeSource freeze(tree_source_.get());
tree_source_->SerializeNode(ax_object, &data);
if (data.child_routing_id == MSG_ROUTING_NONE) {
// Optionally fire an event, if requested to. This is a good fit for
// features like touch exploration on Android, Chrome OS, and
// possibly other platforms - if the user explore a particular point,
// we fire a hover event on the nearest object under the point.
//
// Avoid using this mechanism to fire a particular sentinel event
// and then listen for that event to associate it with the hit test
// request. Instead, the mojo reply should be used directly.
if (event_to_fire != ax::mojom::Event::kNone) {
const std::vector<ui::AXEventIntent> intents;
HandleAXEvent(ui::AXEvent(ax_object.AxID(), event_to_fire,
ax::mojom::EventFrom::kAction, intents,
request_id));
}
// Reply with the result.
const auto& frame_token = render_frame_->GetWebFrame()->GetFrameToken();
std::move(callback).Run(
mojom::HitTestResponse::New(frame_token, point, ax_object.AxID()));
return;
}
// The result was in a child frame. Reply so that the
// client can do a hit test on the child frame recursively.
// If it's a remote frame, transform the point into the child frame's
// coordinate system.
gfx::Point transformed_point = point;
blink::WebFrame* child_frame =
blink::WebFrame::FromFrameOwnerElement(ax_object.GetNode());
DCHECK(child_frame);
if (child_frame->IsWebRemoteFrame()) {
// Remote frames don't have access to the information from the visual
// viewport regarding the visual viewport offset, so we adjust the
// coordinates before sending them to the remote renderer.
WebRect rect = ax_object.GetBoundsInFrameCoordinates();
// The following transformation of the input point is naive, but works
// fairly well. It will fail with CSS transforms that rotate or shear.
// https://crbug.com/981959.
WebView* web_view = render_frame_->GetRenderView()->GetWebView();
gfx::PointF viewport_offset = web_view->VisualViewportOffset();
transformed_point +=
gfx::Vector2d(viewport_offset.x(), viewport_offset.y()) -
gfx::Rect(rect).OffsetFromOrigin();
}
std::move(callback).Run(mojom::HitTestResponse::New(
child_frame->GetFrameToken(), transformed_point, ax_object.AxID()));
}
void RenderAccessibilityImpl::PerformAction(const ui::AXActionData& data) {
const WebDocument& document = GetMainDocument();
if (document.IsNull())
return;
auto root = WebAXObject::FromWebDocument(document);
if (!root.UpdateLayoutAndCheckValidity())
return;
// If an action was requested, we no longer want to defer events.
event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately;
std::unique_ptr<ui::AXActionTarget> target =
AXActionTargetFactory::CreateFromNodeId(document, plugin_tree_source_,
data.target_node_id);
std::unique_ptr<ui::AXActionTarget> anchor =
AXActionTargetFactory::CreateFromNodeId(document, plugin_tree_source_,
data.anchor_node_id);
std::unique_ptr<ui::AXActionTarget> focus =
AXActionTargetFactory::CreateFromNodeId(document, plugin_tree_source_,
data.focus_node_id);
switch (data.action) {
case ax::mojom::Action::kBlur:
root.Focus();
break;
case ax::mojom::Action::kClearAccessibilityFocus:
target->ClearAccessibilityFocus();
break;
case ax::mojom::Action::kDecrement:
target->Decrement();
break;
case ax::mojom::Action::kDoDefault:
target->Click();
break;
case ax::mojom::Action::kGetImageData:
OnGetImageData(target.get(), data.target_rect.size());
break;
case ax::mojom::Action::kIncrement:
target->Increment();
break;
case ax::mojom::Action::kScrollToMakeVisible:
target->ScrollToMakeVisibleWithSubFocus(
data.target_rect, data.horizontal_scroll_alignment,
data.vertical_scroll_alignment, data.scroll_behavior);
break;
case ax::mojom::Action::kScrollToPoint:
target->ScrollToGlobalPoint(data.target_point);
break;
case ax::mojom::Action::kLoadInlineTextBoxes:
OnLoadInlineTextBoxes(target.get());
break;
case ax::mojom::Action::kFocus:
target->Focus();
break;
case ax::mojom::Action::kSetAccessibilityFocus:
target->SetAccessibilityFocus();
break;
case ax::mojom::Action::kSetScrollOffset:
target->SetScrollOffset(data.target_point);
break;
case ax::mojom::Action::kSetSelection:
anchor->SetSelection(anchor.get(), data.anchor_offset, focus.get(),
data.focus_offset);
HandleAXEvent(
ui::AXEvent(root.AxID(), ax::mojom::Event::kLayoutComplete));
break;
case ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint:
target->SetSequentialFocusNavigationStartingPoint();
break;
case ax::mojom::Action::kSetValue:
target->SetValue(data.value);
break;
case ax::mojom::Action::kShowContextMenu:
target->ShowContextMenu();
break;
case ax::mojom::Action::kScrollBackward:
case ax::mojom::Action::kScrollForward:
case ax::mojom::Action::kScrollUp:
case ax::mojom::Action::kScrollDown:
case ax::mojom::Action::kScrollLeft:
case ax::mojom::Action::kScrollRight:
Scroll(target.get(), data.action);
break;
case ax::mojom::Action::kCustomAction:
case ax::mojom::Action::kCollapse:
case ax::mojom::Action::kExpand:
case ax::mojom::Action::kHitTest:
case ax::mojom::Action::kReplaceSelectedText:
case ax::mojom::Action::kNone:
NOTREACHED();
break;
case ax::mojom::Action::kGetTextLocation:
break;
case ax::mojom::Action::kAnnotatePageImages:
// Ensure we aren't already labeling images, in which case this should
// not change.
if (!ax_image_annotator_) {
CreateAXImageAnnotator();
// Walk the tree to discover images, and mark them dirty so that
// they get added to the annotator.
MarkAllAXObjectsDirty(ax::mojom::Role::kImage);
}
break;
case ax::mojom::Action::kSignalEndOfTest:
// Wait for 100ms to allow pending events to come in
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
HandleAXEvent(ui::AXEvent(root.AxID(), ax::mojom::Event::kEndOfTest));
break;
case ax::mojom::Action::kShowTooltip:
case ax::mojom::Action::kHideTooltip:
case ax::mojom::Action::kInternalInvalidateTree:
break;
}
}
void RenderAccessibilityImpl::Reset(int32_t reset_token) {
reset_token_ = reset_token;
serializer_->Reset();
pending_events_.clear();
const WebDocument& document = GetMainDocument();
if (!document.IsNull()) {
// Tree-only mode gets used by the automation extension API which requires a
// load complete event to invoke listener callbacks.
auto root_object = WebAXObject::FromWebDocument(document);
ax::mojom::Event event = root_object.IsLoaded()
? ax::mojom::Event::kLoadComplete
: ax::mojom::Event::kLayoutComplete;
HandleAXEvent(ui::AXEvent(root_object.AxID(), event));
}
}
void RenderAccessibilityImpl::HandleWebAccessibilityEvent(
const ui::AXEvent& event) {
HandleAXEvent(event);
}
void RenderAccessibilityImpl::MarkWebAXObjectDirty(const WebAXObject& obj,
bool subtree) {
DirtyObject dirty_object;
dirty_object.obj = obj;
dirty_object.event_from = ax::mojom::EventFrom::kAction;
dirty_objects_.push_back(dirty_object);
if (subtree)
serializer_->InvalidateSubtree(obj);
ScheduleSendPendingAccessibilityEvents();
}
void RenderAccessibilityImpl::HandleAXEvent(const ui::AXEvent& event) {
const WebDocument& document = GetMainDocument();
if (document.IsNull())
return;
auto obj = WebAXObject::FromWebDocumentByID(document, event.id);
if (obj.IsDetached())
return;
if (document.GetFrame()) {
gfx::Size scroll_offset = document.GetFrame()->GetScrollOffset();
if (scroll_offset != last_scroll_offset_) {
// Make sure the browser is always aware of the scroll position of
// the root document element by posting a generic notification that
// will update it.
// TODO(dmazzoni): remove this as soon as
// https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
last_scroll_offset_ = scroll_offset;
auto root_object = WebAXObject::FromWebDocument(document);
if (!obj.Equals(root_object)) {
HandleAXEvent(ui::AXEvent(root_object.AxID(),
ax::mojom::Event::kLayoutComplete,
event.event_from));
}
}
}
#if defined(OS_ANDROID)
// Force the newly focused node to be re-serialized so we include its
// inline text boxes.
if (event.event_type == ax::mojom::Event::kFocus)
serializer_->InvalidateSubtree(obj);
#endif
// If a select tag is opened or closed, all the children must be updated
// because their visibility may have changed.
if (obj.Role() == ax::mojom::Role::kMenuListPopup &&
event.event_type == ax::mojom::Event::kChildrenChanged) {
WebAXObject popup_like_object = obj.ParentObject();
if (!popup_like_object.IsDetached()) {
serializer_->InvalidateSubtree(popup_like_object);
HandleAXEvent(ui::AXEvent(popup_like_object.AxID(),
ax::mojom::Event::kChildrenChanged));
}
}
// Discard duplicate accessibility events.
for (const ui::AXEvent& pending_event : pending_events_) {
if (pending_event.id == event.id &&
pending_event.event_type == event.event_type) {
return;
}
}
pending_events_.push_back(event);
// Once we get the first load, we should no longer defer events.
if (event.event_type == ax::mojom::Event::kLoadComplete)
event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately;
ScheduleSendPendingAccessibilityEvents();
}
bool RenderAccessibilityImpl::ShouldSerializeNodeForEvent(
const WebAXObject& obj,
const ui::AXEvent& event) const {
if (obj.IsDetached())
return false;
if (event.event_type == ax::mojom::Event::kTextSelectionChanged &&
!obj.IsNativeTextControl()) {
// Selection changes on non-native text controls cause no change to the
// control node's data.
//
// Selection offsets exposed via kTextSelStart and kTextSelEnd are only used
// for plain text controls, (input of a text field type, and textarea). Rich
// editable areas, such as contenteditables, use AXTreeData.
//
// TODO(nektar): Remove kTextSelStart and kTextSelEnd from the renderer.
return false;
}
return true;
}
void RenderAccessibilityImpl::ScheduleSendPendingAccessibilityEvents(
bool scheduling_from_task) {
// Don't send accessibility events for frames that are not in the frame tree
// yet (i.e., provisional frames used for remote-to-local navigations, which
// haven't committed yet). Doing so might trigger layout, which may not work
// correctly for those frames. The events should be sent once such a frame
// commits.
if (!render_frame_ || !render_frame_->in_frame_tree())
return;
switch (event_schedule_status_) {
case EventScheduleStatus::kScheduledDeferred:
if (event_schedule_mode_ ==
EventScheduleMode::kProcessEventsImmediately) {
// Cancel scheduled deferred events so we can schedule events to be
// sent immediately.
CancelScheduledEvents();
break;
}
// We have already scheduled a task to send pending events.
return;
case EventScheduleStatus::kScheduledImmediate:
// The send pending events task have been scheduled, but has not started.
return;
case EventScheduleStatus::kWaitingForAck:
// Events have been sent, wait for ack.
return;
case EventScheduleStatus::kNotWaiting:
// Once the events have been handled, we schedule the pending events from
// that task. In this case, there would be a weak ptr still in use.
if (!scheduling_from_task &&
weak_factory_for_pending_events_.HasWeakPtrs())
return;
break;
}
base::TimeDelta delay = base::TimeDelta::FromMilliseconds(0);
switch (event_schedule_mode_) {
case EventScheduleMode::kDeferEvents:
event_schedule_status_ = EventScheduleStatus::kScheduledDeferred;
// During page load, process changes on a delay so that they occur in
// larger batches, which helps improve efficiency of page loads.
delay = base::TimeDelta::FromMilliseconds(kDelayForDeferredEvents);
break;
case EventScheduleMode::kProcessEventsImmediately:
event_schedule_status_ = EventScheduleStatus::kScheduledImmediate;
delay = base::TimeDelta::FromMilliseconds(0);
break;
}
// When no accessibility events are in-flight post a task to send
// the events to the browser. We use PostTask so that we can queue
// up additional events.
render_frame_->GetTaskRunner(blink::TaskType::kInternalDefault)
->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&RenderAccessibilityImpl::SendPendingAccessibilityEvents,
weak_factory_for_pending_events_.GetWeakPtr()),
delay);
}
int RenderAccessibilityImpl::GenerateAXID() {
WebAXObject root = tree_source_->GetRoot();
return root.GenerateAXID();
}
void RenderAccessibilityImpl::SetPluginTreeSource(
PluginAXTreeSource* plugin_tree_source) {
plugin_tree_source_ = plugin_tree_source;
plugin_serializer_.reset(new PluginAXTreeSerializer(plugin_tree_source_));
OnPluginRootNodeUpdated();
}
void RenderAccessibilityImpl::OnPluginRootNodeUpdated() {
// Search the accessibility tree for plugin's root object and post a
// children changed notification on it to force it to update the
// plugin accessibility tree.
WebAXObject obj = GetPluginRoot();
if (obj.IsNull())
return;
HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kChildrenChanged));
}
void RenderAccessibilityImpl::ShowPluginContextMenu() {
// Search the accessibility tree for plugin's root object and invoke
// ShowContextMenu() on it to show context menu for plugin.
WebAXObject obj = GetPluginRoot();
if (obj.IsNull())
return;
const WebDocument& document = GetMainDocument();
if (document.IsNull())
return;
std::unique_ptr<ui::AXActionTarget> target =
AXActionTargetFactory::CreateFromNodeId(document, plugin_tree_source_,
obj.AxID());
target->ShowContextMenu();
}
WebDocument RenderAccessibilityImpl::GetMainDocument() {
if (render_frame_ && render_frame_->GetWebFrame())
return render_frame_->GetWebFrame()->GetDocument();
return WebDocument();
}
std::string RenderAccessibilityImpl::GetLanguage() {
return page_language_;
}
void RenderAccessibilityImpl::SendPendingAccessibilityEvents() {
TRACE_EVENT0("accessibility",
"RenderAccessibilityImpl::SendPendingAccessibilityEvents");
base::ElapsedTimer timer;
// Clear status here in case we return early.
event_schedule_status_ = EventScheduleStatus::kNotWaiting;
WebDocument document = GetMainDocument();
if (document.IsNull())
return;
if (pending_events_.empty() && dirty_objects_.empty())
return;
// Make a copy of the events, because it's possible that
// actions inside this loop will cause more events to be
// queued up.
std::vector<ui::AXEvent> src_events = pending_events_;
pending_events_.clear();
// The serialized list of updates and events to send to the browser.
std::vector<AXContentTreeUpdate> updates;
std::vector<ui::AXEvent> events;
// Keep track of nodes in the tree that need to be updated.
std::vector<DirtyObject> dirty_objects = dirty_objects_;
dirty_objects_.clear();
// If there's a layout complete message, we need to send location changes.
bool had_layout_complete_messages = false;
// If there's a load complete message, we need to change the event schedule
// mode.
bool had_load_complete_messages = false;
ScopedFreezeBlinkAXTreeSource freeze(tree_source_.get());
// Save the page language.
WebAXObject root = tree_source_->GetRoot();
page_language_ = root.Language().Utf8();
// Loop over each event and generate an updated event message.
for (ui::AXEvent& event : src_events) {
if (event.event_type == ax::mojom::Event::kLayoutComplete)
had_layout_complete_messages = true;
if (event.event_type == ax::mojom::Event::kLoadComplete)
had_load_complete_messages = true;
auto obj = WebAXObject::FromWebDocumentByID(document, event.id);
// Make sure the object still exists.
if (!obj.UpdateLayoutAndCheckValidity())
continue;
// Make sure it's a descendant of our root node - exceptions include the
// scroll area that's the parent of the main document (we ignore it), and
// possibly nodes attached to a different document.
if (!tree_source_->IsInTree(obj))
continue;
// If it's ignored, find the first ancestor that's not ignored.
//
// Note that "IsDetached()" also calls "IsNull()". Additionally,
// "ParentObject()" always gets the first ancestor that is included in tree
// (ignored or unignored), so it will never return objects that are not
// included in the tree at all.
if (!obj.AccessibilityIsIncludedInTree())
obj = obj.ParentObject();
for (; !obj.IsDetached() && obj.AccessibilityIsIgnored();
obj = obj.ParentObject()) {
// There are 3 states of nodes that we care about here.
// (x) Unignored, included in tree
// [x] Ignored, included in tree
// <x> Ignored, excluded from tree
//
// Consider the following tree :
// ++(0) Role::kRootWebArea
// ++++<1> Role::kIgnored
// ++++++[2] Role::kGenericContainer <body>
// ++++++++[3] Role::kGenericContainer with 'visibility: hidden'
//
// If we modify [3] to be 'visibility: visible', we will receive
// Event::kChildrenChanged here for the Ignored parent [2].
// We must re-serialize the Unignored parent node (0) due to this
// change, but we must also re-serialize [2] since its children
// have changed. <1> was never part of the ax tree, and therefore
// does not need to be serialized.
// Note that [3] will be serialized to (3) during :
// |AXTreeSerializer<>::SerializeChangedNodes| when node [2] is
// being serialized, since it will detect the Ignored state had
// changed.
//
// Similarly, during Event::kTextChanged, if any Ignored,
// but included in tree ancestor uses NameFrom::kContents,
// they must also be re-serialized in case the name changed.
if (ShouldSerializeNodeForEvent(obj, event)) {
DirtyObject dirty_object;
dirty_object.obj = obj;
dirty_object.event_from = event.event_from;
dirty_object.event_intents = event.event_intents;
dirty_objects.push_back(dirty_object);
}
}
events.push_back(event);
VLOG(1) << "Accessibility event: " << ui::ToString(event.event_type)
<< " on node id " << event.id;
// Some events don't cause any changes to their associated objects.
if (ShouldSerializeNodeForEvent(obj, event)) {
DirtyObject dirty_object;
dirty_object.obj = obj;
dirty_object.event_from = event.event_from;
dirty_object.event_intents = event.event_intents;
dirty_objects.push_back(dirty_object);
}
}
// Popups have a document lifecycle managed separately from the main document
// but we need to return a combined accessibility tree for both.
// We ensured layout validity for the main document in the loop above; if a
// popup is open, do the same for it.
WebDocument popup_document = GetPopupDocument();
if (!popup_document.IsNull()) {
WebAXObject popup_root_obj = WebAXObject::FromWebDocument(popup_document);
if (!popup_root_obj.UpdateLayoutAndCheckValidity()) {
// If a popup is open but we can't ensure its validity, return without
// sending an update bundle, the same as we would for a node in the main
// document.
return;
}
}
// Keep track of if the host node for a plugin has been invalidated,
// because if so, the plugin subtree will need to be re-serialized.
bool invalidate_plugin_subtree = false;
if (plugin_tree_source_ && !plugin_host_node_.IsDetached()) {
invalidate_plugin_subtree = !serializer_->IsInClientTree(plugin_host_node_);
}
// Now serialize all dirty objects. Keep track of IDs serialized
// so we don't have to serialize the same node twice.
std::set<int32_t> already_serialized_ids;
for (size_t i = 0; i < dirty_objects.size(); i++) {
auto obj = dirty_objects[i].obj;
// Dirty objects can be added using MarkWebAXObjectDirty(obj) from other
// parts of the code as well, so we need to ensure the object still exists.
if (!obj.UpdateLayoutAndCheckValidity())
continue;
// If the object in question is not included in the tree, get the
// nearest ancestor that is (ParentObject() will do this for us).
// Otherwise this can lead to the serializer doing extra work because
// the object won't be in |already_serialized_ids|.
if (!obj.AccessibilityIsIncludedInTree()) {
obj = obj.ParentObject();
if (obj.IsDetached())
continue;
}
if (already_serialized_ids.find(obj.AxID()) != already_serialized_ids.end())
continue;
AXContentTreeUpdate update;
update.event_from = dirty_objects[i].event_from;
update.event_intents = dirty_objects[i].event_intents;
// If there's a plugin, force the tree data to be generated in every
// message so the plugin can merge its own tree data changes.
if (plugin_tree_source_)
update.has_tree_data = true;
if (!serializer_->SerializeChanges(obj, &update)) {
VLOG(1) << "Failed to serialize one accessibility event.";
continue;
}
if (update.node_id_to_clear > 0)
invalidate_plugin_subtree = true;
if (plugin_tree_source_)
AddPluginTreeToUpdate(&update, invalidate_plugin_subtree);
// For each node in the update, set the location in our map from
// ids to locations.
for (auto& node : update.nodes) {
ui::AXRelativeBounds& dst = locations_[node.id];
dst = node.relative_bounds;
already_serialized_ids.insert(node.id);
}
updates.push_back(update);
VLOG(1) << "Accessibility tree update:\n" << update.ToString();
}
event_schedule_status_ = EventScheduleStatus::kWaitingForAck;
render_accessibility_manager_->HandleAccessibilityEvents(
updates, events, reset_token_,
base::BindOnce(&RenderAccessibilityImpl::OnAccessibilityEventsHandled,
weak_factory_for_pending_events_.GetWeakPtr()));
reset_token_ = 0;
if (had_layout_complete_messages)
SendLocationChanges();
if (had_load_complete_messages) {
has_injected_stylesheet_ = false;
event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately;
}
if (image_annotation_debugging_)
AddImageAnnotationDebuggingAttributes(updates);
// Measure the amount of time spent in this function. Keep track of the
// maximum within a time interval so we can upload UKM.
int elapsed_time_ms = timer.Elapsed().InMilliseconds();
if (elapsed_time_ms > slowest_serialization_ms_) {
last_ukm_source_id_ = document.GetUkmSourceId();
last_ukm_url_ = document.CanonicalUrlForSharing().GetString().Utf8();
slowest_serialization_ms_ = elapsed_time_ms;
}
if (ukm_timer_->Elapsed().InSeconds() >= kMinUKMDelayInSeconds)
MaybeSendUKM();
}
void RenderAccessibilityImpl::SendLocationChanges() {
TRACE_EVENT0("accessibility", "RenderAccessibilityImpl::SendLocationChanges");
std::vector<mojom::LocationChangesPtr> changes;
// Update layout on the root of the tree.
WebAXObject root = tree_source_->GetRoot();
if (!root.UpdateLayoutAndCheckValidity())
return;
// Do a breadth-first explore of the whole blink AX tree.
std::unordered_map<int, ui::AXRelativeBounds> new_locations;
base::queue<WebAXObject> objs_to_explore;
objs_to_explore.push(root);
while (objs_to_explore.size()) {
WebAXObject obj = objs_to_explore.front();
objs_to_explore.pop();
// See if we had a previous location. If not, this whole subtree must
// be new, so don't continue to explore this branch.
int id = obj.AxID();
auto iter = locations_.find(id);
if (iter == locations_.end())
continue;
// If the location has changed, append it to the IPC message.
WebAXObject offset_container;
WebFloatRect bounds_in_container;
SkMatrix44 container_transform;
obj.GetRelativeBounds(offset_container, bounds_in_container,
container_transform);
ui::AXRelativeBounds new_location;
new_location.offset_container_id = offset_container.AxID();
new_location.bounds = bounds_in_container;
if (!container_transform.isIdentity())
new_location.transform = base::WrapUnique(
new gfx::Transform(container_transform));
if (iter->second != new_location)
changes.push_back(mojom::LocationChanges::New(id, new_location));
// Save the new location.
new_locations[id] = new_location;
// Explore children of this object.
std::vector<WebAXObject> children;
tree_source_->GetChildren(obj, &children);
for (WebAXObject& child : children)
objs_to_explore.push(child);
}
locations_.swap(new_locations);
render_accessibility_manager_->HandleLocationChanges(std::move(changes));
}
void RenderAccessibilityImpl::OnAccessibilityEventsHandled() {
DCHECK_EQ(event_schedule_status_, EventScheduleStatus::kWaitingForAck);
event_schedule_status_ = EventScheduleStatus::kNotWaiting;
switch (event_schedule_mode_) {
case EventScheduleMode::kDeferEvents:
ScheduleSendPendingAccessibilityEvents(true);
break;
case EventScheduleMode::kProcessEventsImmediately:
SendPendingAccessibilityEvents();
break;
}
}
void RenderAccessibilityImpl::OnLoadInlineTextBoxes(
const ui::AXActionTarget* target) {
const BlinkAXActionTarget* blink_target =
BlinkAXActionTarget::FromAXActionTarget(target);
if (!blink_target)
return;
const WebAXObject& obj = blink_target->WebAXObject();
ScopedFreezeBlinkAXTreeSource freeze(tree_source_.get());
if (tree_source_->ShouldLoadInlineTextBoxes(obj))
return;
tree_source_->SetLoadInlineTextBoxesForId(obj.AxID());
const WebDocument& document = GetMainDocument();
if (document.IsNull())
return;
// This object may not be a leaf node. Force the whole subtree to be
// re-serialized.
serializer_->InvalidateSubtree(obj);
// Explicitly send a tree change update event now.
HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kTreeChanged));
}
void RenderAccessibilityImpl::OnGetImageData(const ui::AXActionTarget* target,
const gfx::Size& max_size) {
const BlinkAXActionTarget* blink_target =
BlinkAXActionTarget::FromAXActionTarget(target);
if (!blink_target)
return;
const WebAXObject& obj = blink_target->WebAXObject();
ScopedFreezeBlinkAXTreeSource freeze(tree_source_.get());
if (tree_source_->image_data_node_id() == obj.AxID())
return;
tree_source_->set_image_data_node_id(obj.AxID());
tree_source_->set_max_image_data_size(max_size);
const WebDocument& document = GetMainDocument();
if (document.IsNull())
return;
serializer_->InvalidateSubtree(obj);
HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kImageFrameUpdated));
}
void RenderAccessibilityImpl::OnDestruct() {
render_frame_ = nullptr;
delete this;
}
void RenderAccessibilityImpl::AddPluginTreeToUpdate(
AXContentTreeUpdate* update,
bool invalidate_plugin_subtree) {
const WebDocument& document = GetMainDocument();
if (invalidate_plugin_subtree)
plugin_serializer_->Reset();
for (size_t i = 0; i < update->nodes.size(); ++i) {
if (update->nodes[i].role == ax::mojom::Role::kEmbeddedObject) {
plugin_host_node_ =
WebAXObject::FromWebDocumentByID(document, update->nodes[i].id);
const ui::AXNode* root = plugin_tree_source_->GetRoot();
update->nodes[i].child_ids.push_back(root->id());
ui::AXTreeUpdate plugin_update;
plugin_serializer_->SerializeChanges(root, &plugin_update);
// We have to copy the updated nodes using a loop because we're
// converting from a generic ui::AXNodeData to a vector of its
// content-specific subclass AXContentNodeData.
size_t old_count = update->nodes.size();
size_t new_count = plugin_update.nodes.size();
update->nodes.resize(old_count + new_count);
for (size_t j = 0; j < new_count; ++j)
update->nodes[old_count + j] = plugin_update.nodes[j];
break;
}
}
if (plugin_tree_source_->GetTreeData(&update->tree_data))
update->has_tree_data = true;
}
void RenderAccessibilityImpl::CreateAXImageAnnotator() {
if (!render_frame_)
return;
mojo::PendingRemote<image_annotation::mojom::Annotator> annotator;
render_frame_->GetBrowserInterfaceBroker()->GetInterface(
annotator.InitWithNewPipeAndPassReceiver());
ax_image_annotator_ =
std::make_unique<AXImageAnnotator>(this, std::move(annotator));
tree_source_->AddImageAnnotator(ax_image_annotator_.get());
}
void RenderAccessibilityImpl::StartOrStopLabelingImages(ui::AXMode old_mode,
ui::AXMode new_mode) {
if (!render_frame_)
return;
if (!old_mode.has_mode(ui::AXMode::kLabelImages) &&
new_mode.has_mode(ui::AXMode::kLabelImages)) {
CreateAXImageAnnotator();
} else if (old_mode.has_mode(ui::AXMode::kLabelImages) &&
!new_mode.has_mode(ui::AXMode::kLabelImages)) {
tree_source_->RemoveImageAnnotator();
ax_image_annotator_->Destroy();
ax_image_annotator_.release();
}
}
void RenderAccessibilityImpl::MarkAllAXObjectsDirty(ax::mojom::Role role) {
ScopedFreezeBlinkAXTreeSource freeze(tree_source_.get());
base::queue<WebAXObject> objs_to_explore;
objs_to_explore.push(tree_source_->GetRoot());
while (objs_to_explore.size()) {
WebAXObject obj = objs_to_explore.front();
objs_to_explore.pop();
if (obj.Role() == role)
MarkWebAXObjectDirty(obj, /* subtree */ false);
std::vector<blink::WebAXObject> children;
tree_source_->GetChildren(obj, &children);
for (WebAXObject& child : children)
objs_to_explore.push(child);
}
}
void RenderAccessibilityImpl::Scroll(const ui::AXActionTarget* target,
ax::mojom::Action scroll_action) {
gfx::Rect bounds = target->GetRelativeBounds();
if (bounds.IsEmpty())
return;
gfx::Point initial = target->GetScrollOffset();
gfx::Point min = target->MinimumScrollOffset();
gfx::Point max = target->MaximumScrollOffset();
// TODO(anastasi): This 4/5ths came from the Android implementation, revisit
// to find the appropriate modifier to keep enough context onscreen after
// scrolling.
int page_x = std::max((int)(bounds.width() * 4 / 5), 1);
int page_y = std::max((int)(bounds.height() * 4 / 5), 1);
// Forward/backward defaults to down/up unless it can only be scrolled
// horizontally.
if (scroll_action == ax::mojom::Action::kScrollForward)
scroll_action = max.y() > min.y() ? ax::mojom::Action::kScrollDown
: ax::mojom::Action::kScrollRight;
if (scroll_action == ax::mojom::Action::kScrollBackward)
scroll_action = max.y() > min.y() ? ax::mojom::Action::kScrollUp
: ax::mojom::Action::kScrollLeft;
int x = initial.x();
int y = initial.y();
switch (scroll_action) {
case ax::mojom::Action::kScrollUp:
if (initial.y() == min.y())
return;
y = std::max(initial.y() - page_y, min.y());
break;
case ax::mojom::Action::kScrollDown:
if (initial.y() == max.y())
return;
y = std::min(initial.y() + page_y, max.y());
break;
case ax::mojom::Action::kScrollLeft:
if (initial.x() == min.x())
return;
x = std::max(initial.x() - page_x, min.x());
break;
case ax::mojom::Action::kScrollRight:
if (initial.x() == max.x())
return;
x = std::min(initial.x() + page_x, max.x());
break;
default:
NOTREACHED();
}
target->SetScrollOffset(gfx::Point(x, y));
}
void RenderAccessibilityImpl::AddImageAnnotationDebuggingAttributes(
const std::vector<AXContentTreeUpdate>& updates) {
DCHECK(image_annotation_debugging_);
for (auto& update : updates) {
for (auto& node : update.nodes) {
if (!node.HasIntAttribute(
ax::mojom::IntAttribute::kImageAnnotationStatus))
continue;
ax::mojom::ImageAnnotationStatus status = node.GetImageAnnotationStatus();
bool should_set_attributes = false;
switch (status) {
case ax::mojom::ImageAnnotationStatus::kNone:
case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme:
case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation:
break;
case ax::mojom::ImageAnnotationStatus::kAnnotationPending:
case ax::mojom::ImageAnnotationStatus::kAnnotationAdult:
case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty:
case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed:
case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded:
should_set_attributes = true;
break;
}
if (!should_set_attributes)
continue;
WebDocument document = GetMainDocument();
if (document.IsNull())
continue;
WebAXObject obj = WebAXObject::FromWebDocumentByID(document, node.id);
if (obj.IsDetached())
continue;
if (!has_injected_stylesheet_) {
document.InsertStyleSheet(
"[imageannotation=annotationPending] { outline: 3px solid #9ff; } "
"[imageannotation=annotationSucceeded] { outline: 3px solid #3c3; "
"} "
"[imageannotation=annotationEmpty] { outline: 3px solid #ee6; } "
"[imageannotation=annotationAdult] { outline: 3px solid #f90; } "
"[imageannotation=annotationProcessFailed] { outline: 3px solid "
"#c00; } ");
has_injected_stylesheet_ = true;
}
WebNode web_node = obj.GetNode();
if (web_node.IsNull() || !web_node.IsElementNode())
continue;
WebElement element = web_node.To<WebElement>();
std::string status_str = ui::ToString(status);
if (element.GetAttribute("imageannotation").Utf8() != status_str)
element.SetAttribute("imageannotation",
blink::WebString::FromUTF8(status_str));
std::string title = "%" + status_str;
std::string annotation =
node.GetStringAttribute(ax::mojom::StringAttribute::kImageAnnotation);
if (!annotation.empty())
title = title + ": " + annotation;
if (element.GetAttribute("title").Utf8() != title) {
element.SetAttribute("title", blink::WebString::FromUTF8(title));
}
}
}
}
blink::WebDocument RenderAccessibilityImpl::GetPopupDocument() {
blink::WebPagePopup* popup =
render_frame_->GetRenderView()->GetWebView()->GetPagePopup();
if (popup)
return popup->GetDocument();
return WebDocument();
}
WebAXObject RenderAccessibilityImpl::GetPluginRoot() {
ScopedFreezeBlinkAXTreeSource freeze(tree_source_.get());
WebAXObject root = tree_source_->GetRoot();
if (!root.UpdateLayoutAndCheckValidity())
return WebAXObject();
base::queue<WebAXObject> objs_to_explore;
objs_to_explore.push(root);
while (objs_to_explore.size()) {
WebAXObject obj = objs_to_explore.front();
objs_to_explore.pop();
WebNode node = obj.GetNode();
if (!node.IsNull() && node.IsElementNode()) {
WebElement element = node.To<WebElement>();
if (element.HasHTMLTagName("embed")) {
return obj;
}
}
// Explore children of this object.
std::vector<WebAXObject> children;
tree_source_->GetChildren(obj, &children);
for (const auto& child : children)
objs_to_explore.push(child);
}
return WebAXObject();
}
void RenderAccessibilityImpl::CancelScheduledEvents() {
switch (event_schedule_status_) {
case EventScheduleStatus::kScheduledDeferred:
case EventScheduleStatus::kScheduledImmediate: // Fallthrough
weak_factory_for_pending_events_.InvalidateWeakPtrs();
event_schedule_status_ = EventScheduleStatus::kNotWaiting;
break;
case EventScheduleStatus::kWaitingForAck:
case EventScheduleStatus::kNotWaiting: // Fallthrough
break;
}
}
void RenderAccessibilityImpl::MaybeSendUKM() {
if (slowest_serialization_ms_ < kMinSerializationTimeToSendInMS)
return;
ukm::builders::Accessibility_Renderer(last_ukm_source_id_)
.SetCpuTime_SendPendingAccessibilityEvents(slowest_serialization_ms_)
.Record(ukm_recorder_.get());
ResetUKMData();
}
void RenderAccessibilityImpl::ResetUKMData() {
slowest_serialization_ms_ = 0;
ukm_timer_ = std::make_unique<base::ElapsedTimer>();
last_ukm_source_id_ = ukm::kInvalidSourceId;
last_ukm_url_ = "";
}
RenderAccessibilityImpl::DirtyObject::DirtyObject() = default;
RenderAccessibilityImpl::DirtyObject::DirtyObject(const DirtyObject& other) =
default;
RenderAccessibilityImpl::DirtyObject::~DirtyObject() = default;
} // namespace content