| // Copyright 2012 The Chromium Authors |
| // 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 <string> |
| #include <utility> |
| |
| #include "base/debug/crash_logging.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "build/build_config.h" |
| #include "content/public/renderer/render_thread.h" |
| #include "content/renderer/accessibility/annotations/ax_annotators_manager.h" |
| #include "content/renderer/accessibility/ax_action_target_factory.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 "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/web/web_disallow_transition_scope.h" |
| #include "third_party/blink/public/web/web_document.h" |
| #include "third_party/blink/public/web/web_page_popup.h" |
| #include "third_party/blink/public/web/web_settings.h" |
| #include "third_party/blink/public/web/web_view.h" |
| #include "ui/accessibility/ax_event_intent.h" |
| #include "ui/accessibility/ax_mode_histogram_logger.h" |
| #include "ui/accessibility/ax_tree_id.h" |
| #include "ui/gfx/geometry/vector2d_conversions.h" |
| |
| using blink::WebAXContext; |
| using blink::WebAXObject; |
| using blink::WebDocument; |
| using blink::WebView; |
| |
| namespace { |
| |
| // The minimum amount of time that should be spent in serializing code in order |
| // to report the elapsed time as a URL-keyed metric. |
| constexpr base::TimeDelta kMinSerializationTimeToSend = base::Milliseconds(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 base::TimeDelta kMinUKMDelay = base::Seconds(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* const ax_mode_crash_key = base::debug::AllocateCrashKeyString( |
| "ax_mode", base::debug::CrashKeySize::Size64); |
| base::debug::SetCrashKeyString(ax_mode_crash_key, mode.ToString()); |
| } |
| |
| } // namespace |
| |
| namespace content { |
| |
| RenderAccessibilityImpl::RenderAccessibilityImpl( |
| RenderAccessibilityManager* const render_accessibility_manager, |
| RenderFrameImpl* const render_frame) |
| : RenderFrameObserver(render_frame), |
| render_accessibility_manager_(render_accessibility_manager), |
| render_frame_(render_frame), |
| ukm_timer_(std::make_unique<base::ElapsedTimer>()), |
| last_ukm_source_id_(ukm::kInvalidSourceId) { |
| mojo::Remote<ukm::mojom::UkmRecorderFactory> factory; |
| content::RenderThread::Get()->BindHostReceiver( |
| factory.BindNewPipeAndPassReceiver()); |
| ukm_recorder_ = ukm::MojoUkmRecorder::Create(*factory); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Password values are only passed through on Android. |
| render_frame_->GetWebView() |
| ->GetSettings() |
| ->SetAccessibilityPasswordValuesEnabled(true); |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID) |
| // aria-modal currently prunes the accessibility tree on Mac and Android only. |
| render_frame_->GetWebView()->GetSettings()->SetAriaModalPrunesAXTree(true); |
| #endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Do not ignore SVG grouping (<g>) elements on ChromeOS, which is needed so |
| // Select-to-Speak can read SVG text nodes in natural reading order. |
| render_frame_->GetWebView() |
| ->GetSettings() |
| ->SetAccessibilityIncludeSvgGElement(true); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| ax_annotators_manager_ = std::make_unique<AXAnnotatorsManager>(this); |
| } |
| |
| RenderAccessibilityImpl::~RenderAccessibilityImpl() { |
| if (ax_context_) { |
| // Accessibility has been turned off for this frame. Destruction of this |
| // instance's WebAXContext will destroy the document's AXObjectCache if |
| // there are no other active contexts on the document. If there are other |
| // active contexts, the serializer must be reset to ensure that the full |
| // tree is serialized should accessibility be once again turned on for this |
| // frame. |
| ax_context_->ResetSerializer(); |
| } |
| } |
| |
| void RenderAccessibilityImpl::DidCreateNewDocument() { |
| const WebDocument& document = GetMainDocument(); |
| DCHECK(!document.IsNull()); |
| ax_context_ = std::make_unique<WebAXContext>(document, accessibility_mode_); |
| |
| // Set reset token which will be returned to browser in the next IPC, so that |
| // RenderFrameHostImpl can discard stale data, when the token does not match |
| // the expected token. |
| ax_context_->SetSerializationResetToken(*reset_token_); |
| |
| ScheduleImmediateAXUpdate(); |
| } |
| |
| void RenderAccessibilityImpl::DidCommitProvisionalLoad( |
| ui::PageTransition transition) { |
| MaybeSendUKM(); |
| slowest_serialization_time_ = base::TimeDelta(); |
| ukm_timer_ = std::make_unique<base::ElapsedTimer>(); |
| |
| ax_annotators_manager_->CancelAnnotations(); |
| page_language_.clear(); |
| |
| // New document has started. Do not expect to receive the ACK for a |
| // serialization sent by the old document. |
| ax_context_->OnSerializationCancelled(); |
| weak_factory_for_pending_events_.InvalidateWeakPtrs(); |
| |
| loading_stage_ = LoadingStage::kPreload; |
| } |
| |
| void RenderAccessibilityImpl::NotifyAccessibilityModeChange( |
| const ui::AXMode& mode) { |
| CHECK(reset_token_); |
| ui::AXMode old_mode = accessibility_mode_; |
| DCHECK(!mode.is_mode_off()) |
| << "Should not be reached when turning a11y off; rather, the " |
| "RenderAccessibilityImpl should be destroyed."; |
| |
| if (old_mode == mode) { |
| DCHECK(ax_context_); |
| NOTREACHED() << "Do not call AccessibilityModeChanged unless it changes."; |
| } |
| |
| accessibility_mode_ = mode; |
| |
| bool was_on = !old_mode.is_mode_off(); |
| |
| DCHECK_EQ(was_on, !!ax_context_); |
| |
| SetAccessibilityCrashKey(mode); |
| |
| if (ax_context_) { |
| ax_context_->SetAXMode(mode); |
| } else { |
| DidCreateNewDocument(); |
| } |
| |
| DCHECK(ax_context_); |
| DCHECK_EQ(accessibility_mode_, ax_context_->GetAXMode()); |
| |
| // Log individual mode flags transitioning to the set state, as well as usage |
| // of named bundles of node flags. |
| ui::RecordAccessibilityModeHistograms(ui::AXHistogramPrefix::kBlink, |
| accessibility_mode_, old_mode); |
| |
| // Build (or rebuild) the accessibility tree with the new mode. |
| if (was_on) { |
| ax_context_->MarkDocumentDirty(); |
| } |
| |
| // Update AXAnnotators based on a changed accessibility mode. |
| ax_annotators_manager_->AccessibilityModeChanged(old_mode, mode); |
| |
| // Fire a load complete event so that any ATs present can treat the page as |
| // fresh and newly loaded. |
| FireLoadCompleteIfLoaded(); |
| } |
| |
| // Token to return this token in the next IPC, so that RenderFrameHostImpl |
| // can discard stale data, when the token does not match the expected token. |
| void RenderAccessibilityImpl::set_reset_token(uint32_t reset_token) { |
| CHECK(reset_token); |
| reset_token_ = reset_token; |
| if (ax_context_) { |
| ax_context_->SetSerializationResetToken(reset_token); |
| } |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| void RenderAccessibilityImpl::FireLayoutComplete() { |
| if (ax_context_) { |
| ax_context_->AddEventToSerializationQueue( |
| ui::AXEvent(ComputeRoot().AxID(), ax::mojom::Event::kLayoutComplete), |
| true); |
| } |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| void RenderAccessibilityImpl::FireLoadCompleteIfLoaded() { |
| if (GetMainDocument().IsLoaded() && |
| GetMainDocument().GetFrame()->GetEmbeddingToken()) { |
| DCHECK(ax_context_); |
| ax_context_->FireLoadCompleteIfLoaded(); |
| } |
| } |
| |
| // This function expects the |point| passed by parameter to be relative to the |
| // page viewport, always. This means that when the position is within a popup, |
| // the |point| should still be relative to the web page's viewport. |
| void RenderAccessibilityImpl::HitTest( |
| const gfx::Point& point, |
| ax::mojom::Event event_to_fire, |
| int request_id, |
| blink::mojom::RenderAccessibility::HitTestCallback callback) { |
| const WebDocument& document = GetMainDocument(); |
| DCHECK(!document.IsNull()); |
| DCHECK(ax_context_); |
| ax_context_->UpdateAXForAllDocuments(); |
| |
| WebAXObject ax_object; |
| auto root_obj = WebAXObject::FromWebDocument(document); |
| 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; |
| } |
| |
| DCHECK(ax_object.IsIncludedInTree()); |
| |
| // If the result was in the same frame, return the result. |
| ui::AXNodeData data; |
| ax_object.Serialize(&data, ax_context_->GetAXMode()); |
| std::optional<ui::AXTreeID> child_tree_id = data.GetChildTreeID(); |
| gfx::Point transformed_point = point; |
| if (child_tree_id) { |
| // The result may be in a child frame. Reply so that the. |
| // The client can do a hit test on the child frame recursively. |
| // If it's a remote frame or a stitched child tree, also transform the point |
| // into the child frame's coordinate system. (See |
| // ax::mojom::Action::kStitchedChildTree for more information on the latter |
| // case.) |
| blink::WebFrame* child_frame = |
| blink::WebFrame::FromFrameOwnerElement(ax_object.GetNode()); |
| |
| if (!child_frame || child_frame->IsWebRemoteFrame()) { |
| // Remote frames and stitched child trees 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. |
| gfx::Rect 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_->GetWebView(); |
| gfx::PointF viewport_offset = web_view->VisualViewportOffset(); |
| transformed_point += |
| gfx::Vector2d(viewport_offset.x(), viewport_offset.y()) - |
| rect.OffsetFromOrigin(); |
| } |
| |
| if (child_frame) { |
| std::move(callback).Run(blink::mojom::HitTestResponse::New( |
| ui::AXTreeIDUnknown(), child_frame->GetFrameToken(), |
| transformed_point, ax_object.AxID())); |
| return; |
| } |
| |
| // The tree is not coming from Web content. It has been stitched in on the |
| // browser side from other sources, e.g. OCR results. Fall through so that |
| // we would respond with the hosting node and the browser will handle the |
| // hit test in the stitched child tree. |
| |
| } else { |
| // 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; |
| // Marking dirty ensures that a lifecycle update will be scheduled. |
| MarkWebAXObjectDirty(ax_object); |
| HandleAXEvent(ui::AXEvent( |
| ax_object.AxID(), event_to_fire, ax::mojom::EventFrom::kAction, |
| ax::mojom::Action::kHitTest, intents, request_id)); |
| } |
| } |
| |
| // Reply with the result. |
| const auto& frame_token = render_frame_->GetWebFrame()->GetFrameToken(); |
| std::move(callback).Run(blink::mojom::HitTestResponse::New( |
| child_tree_id.value_or(ui::AXTreeIDUnknown()), frame_token, |
| transformed_point, ax_object.AxID())); |
| } |
| |
| void RenderAccessibilityImpl::PerformAction(const ui::AXActionData& data) { |
| if (!ax_context_) { |
| return; |
| } |
| // Update layout and AX first before attempting to perform the action. |
| ax_context_->UpdateAXForAllDocuments(); |
| |
| WebDocument document = GetMainDocument(); |
| if (document.IsNull()) { |
| return; |
| } |
| |
| // Schedule the next serialization to come immediately after the action is |
| // complete, even if the document is still loading. |
| // Do this scheduling now, because in some cases performing the action |
| // could cause script to run that destroys the frame, which destroys |this|, |
| // and ax_context_ is no longer at a valid memory address. |
| ScheduleImmediateAXUpdate(); |
| |
| // TODO: think about how to handle this without holding onto a plugin tree |
| // source. |
| std::unique_ptr<ui::AXActionTarget> target = |
| AXActionTargetFactory::CreateFromNodeIdOrRole( |
| document, plugin_action_target_adapter_, data.target_node_id, |
| data.target_role); |
| std::unique_ptr<ui::AXActionTarget> anchor = |
| AXActionTargetFactory::CreateFromNodeIdOrRole( |
| document, plugin_action_target_adapter_, data.anchor_node_id); |
| std::unique_ptr<ui::AXActionTarget> focus = |
| AXActionTargetFactory::CreateFromNodeIdOrRole( |
| document, plugin_action_target_adapter_, data.focus_node_id); |
| |
| ax_annotators_manager_->PerformAction(data.action); |
| |
| // Important: keep this reconciled with AXObject::PerformAction(). |
| // Actions shouldn't be handled in both places. |
| switch (data.action) { |
| case ax::mojom::Action::kGetImageData: |
| OnGetImageData(target.get(), data.target_rect.size()); |
| break; |
| case ax::mojom::Action::kLoadInlineTextBoxes: |
| OnLoadInlineTextBoxes(target.get()); |
| break; |
| case ax::mojom::Action::kSetSelection: |
| anchor->SetSelection(anchor.get(), data.anchor_offset, focus.get(), |
| data.focus_offset); |
| 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::kBlur: |
| case ax::mojom::Action::kClearAccessibilityFocus: |
| case ax::mojom::Action::kCollapse: |
| case ax::mojom::Action::kDecrement: |
| case ax::mojom::Action::kDoDefault: |
| case ax::mojom::Action::kExpand: |
| case ax::mojom::Action::kIncrement: |
| case ax::mojom::Action::kScrollToPoint: |
| case ax::mojom::Action::kScrollToPositionAtRowColumn: |
| case ax::mojom::Action::kFocus: |
| case ax::mojom::Action::kSetAccessibilityFocus: |
| case ax::mojom::Action::kSetScrollOffset: |
| case ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint: |
| case ax::mojom::Action::kSetValue: |
| case ax::mojom::Action::kShowContextMenu: |
| 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: |
| case ax::mojom::Action::kStitchChildTree: |
| target->PerformAction(data); |
| break; |
| case ax::mojom::Action::kCustomAction: |
| case ax::mojom::Action::kHitTest: |
| case ax::mojom::Action::kReplaceSelectedText: |
| case ax::mojom::Action::kNone: |
| NOTREACHED(); |
| case ax::mojom::Action::kGetTextLocation: |
| break; |
| case ax::mojom::Action::kAnnotatePageImages: |
| // Handle this in AXAnnotatorsManager. |
| break; |
| case ax::mojom::Action::kSignalEndOfTest: |
| HandleAXEvent( |
| ui::AXEvent(ComputeRoot().AxID(), ax::mojom::Event::kEndOfTest)); |
| break; |
| case ax::mojom::Action::kShowTooltip: |
| case ax::mojom::Action::kHideTooltip: |
| case ax::mojom::Action::kInternalInvalidateTree: |
| case ax::mojom::Action::kResumeMedia: |
| case ax::mojom::Action::kStartDuckingMedia: |
| case ax::mojom::Action::kStopDuckingMedia: |
| case ax::mojom::Action::kSuspendMedia: |
| case ax::mojom::Action::kLongClick: |
| break; |
| } |
| } |
| |
| void RenderAccessibilityImpl::Reset(uint32_t reset_token) { |
| DCHECK(ax_context_); |
| DCHECK(!accessibility_mode_.is_mode_off()); |
| ax_context_->ResetSerializer(); |
| set_reset_token(reset_token); |
| FireLoadCompleteIfLoaded(); |
| } |
| |
| void RenderAccessibilityImpl::MarkWebAXObjectDirty( |
| const WebAXObject& obj, |
| ax::mojom::EventFrom event_from, |
| ax::mojom::Action event_from_action, |
| std::vector<ui::AXEventIntent> event_intents, |
| ax::mojom::Event event_type) { |
| DCHECK(obj.IsIncludedInTree()) |
| << "Cannot serialize unincluded object: " << obj.ToString().Utf8(); |
| |
| obj.AddDirtyObjectToSerializationQueue(event_from, event_from_action, |
| event_intents); |
| } |
| |
| void RenderAccessibilityImpl::HandleAXEvent(const ui::AXEvent& event) { |
| DCHECK(ax_context_); |
| |
| // All events sent to AXObjectCache from RAI need immediate serialization. |
| ax_context_->AddEventToSerializationQueue(event, /* immediate */ true); |
| } |
| |
| // TODO(accessibility): When legacy mode is deleted, calls to this function may |
| // be replaced with ax_context_->ScheduleImmediateSerialization() |
| void RenderAccessibilityImpl::ScheduleImmediateAXUpdate() { |
| DCHECK(ax_context_); |
| ax_context_->ScheduleImmediateSerialization(); |
| } |
| |
| bool RenderAccessibilityImpl::HasActiveDocument() const { |
| DCHECK(ax_context_); |
| return ax_context_->HasActiveDocument(); |
| } |
| |
| ui::AXMode RenderAccessibilityImpl::GetAXMode() const { |
| return accessibility_mode_; |
| } |
| |
| WebDocument RenderAccessibilityImpl::GetMainDocument() const { |
| if (render_frame_ && render_frame_->GetWebFrame()) |
| return render_frame_->GetWebFrame()->GetDocument(); |
| return WebDocument(); |
| } |
| |
| std::string RenderAccessibilityImpl::GetLanguage() { |
| return page_language_; |
| } |
| |
| bool RenderAccessibilityImpl::SendAccessibilitySerialization( |
| std::vector<ui::AXTreeUpdate> updates, |
| std::vector<ui::AXEvent> events, |
| ui::AXLocationAndScrollUpdates location_and_scroll_updates, |
| bool had_load_complete_messages) { |
| if (had_load_complete_messages) { |
| loading_stage_ = LoadingStage::kLoadCompleted; |
| } |
| |
| TRACE_EVENT0( |
| "accessibility", |
| loading_stage_ == LoadingStage::kPostLoad |
| ? "RenderAccessibilityImpl::SendPendingAccessibilityEvents" |
| : "RenderAccessibilityImpl::SendPendingAccessibilityEventsLoading"); |
| base::ElapsedTimer timer; |
| |
| DCHECK(!accessibility_mode_.is_mode_off()); |
| DCHECK(ax_context_); |
| DCHECK(ax_context_->IsSerializationInFlight()); |
| DCHECK(ax_context_->HasActiveDocument()); |
| |
| CHECK(render_frame_); |
| CHECK(render_frame_->in_frame_tree()); |
| |
| // Don't send accessibility events for frames that don't yet have an tree id |
| // as doing so will cause the browser to discard that message and all |
| // subsequent ones. |
| // TODO(crbug.com/40190596): There are some cases where no content is |
| // currently rendered, due to an iframe returning 204 or window.stop() being |
| // called. In these cases there will never be an AXTreeID as there is no |
| // commit, which will prevent accessibility updates from ever being sent even |
| // if the rendering is fixed. See also other TODOs related to 1231184 in this |
| // file. |
| DCHECK(render_frame_->GetWebFrame()->GetAXTreeID().token()); |
| |
| WebDocument document = GetMainDocument(); |
| CHECK(!document.IsNull()); |
| |
| // Don't serialize child trees without an embedding token. These are |
| // unrendered child frames. This prevents a situation where child trees can't |
| // be linked to their parent, leading to a dangerous situation for some |
| // platforms, where events are fired on objects not connected to the root. For |
| // example, on Mac, this can lead to a lockup in AppKit. |
| DCHECK(document.GetFrame()->GetEmbeddingToken()); |
| |
| WebAXObject root = ComputeRoot(); |
| #if DCHECK_IS_ON() |
| // Never causes a document lifecycle change during serialization, |
| // because the assumption is that layout is in a safe, stable state. |
| // (Skip if image_annotation_debugging_ is enabled because it adds |
| // style attributes to images, affecting the document lifecycle |
| // during accessibility.) |
| std::unique_ptr<blink::WebDisallowTransitionScope> disallow; |
| if (!image_annotation_debugging_) { |
| disallow = std::make_unique<blink::WebDisallowTransitionScope>(&document); |
| } |
| #endif |
| |
| // Save the page language. |
| page_language_ = root.Language().Utf8(); |
| |
| #if DCHECK_IS_ON() |
| // Protect against lifecycle changes in the popup document, if any. |
| WebDocument popup_document = GetPopupDocument(); |
| std::optional<blink::WebDisallowTransitionScope> disallow2; |
| if (!popup_document.IsNull()) { |
| disallow2.emplace(&popup_document); |
| } |
| #endif |
| |
| ui::AXUpdatesAndEvents updates_and_events; |
| updates_and_events.updates = std::move(updates); |
| updates_and_events.events = std::move(events); |
| |
| for (auto& update : updates_and_events.updates) { |
| ax_annotators_manager_->Annotate(document, &update, |
| had_load_complete_messages); |
| } |
| |
| ax_annotators_manager_->AddDebuggingAttributes(updates_and_events.updates); |
| |
| CHECK(!weak_factory_for_pending_events_.HasWeakPtrs()); |
| CHECK(reset_token_); |
| render_accessibility_manager_->HandleAXEvents( |
| updates_and_events, location_and_scroll_updates, *reset_token_, |
| base::BindOnce(&RenderAccessibilityImpl::OnSerializationReceived, |
| weak_factory_for_pending_events_.GetWeakPtr())); |
| |
| // Measure the amount of time spent in this function. Keep track of the |
| // maximum within a time interval so we can upload UKM. |
| base::TimeDelta elapsed_time_ms = timer.Elapsed(); |
| if (elapsed_time_ms > slowest_serialization_time_) { |
| last_ukm_source_id_ = document.GetUkmSourceId(); |
| slowest_serialization_time_ = elapsed_time_ms; |
| } |
| // Also log the time taken in this function to track serialization |
| // performance. |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "Accessibility.Performance.SendPendingAccessibilityEvents2", |
| elapsed_time_ms, base::Microseconds(1), base::Seconds(1), 50); |
| |
| if (loading_stage_ == LoadingStage::kPostLoad) { |
| // Track serialization after document load in order to measure the |
| // contribution of serialization to interaction latency. |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "Accessibility.Performance.SendPendingAccessibilityEvents.PostLoad2", |
| elapsed_time_ms, base::Microseconds(1), base::Seconds(1), 50); |
| } |
| |
| if (loading_stage_ == LoadingStage::kLoadCompleted) { |
| loading_stage_ = LoadingStage::kPostLoad; |
| } |
| |
| if (ukm_timer_->Elapsed() >= kMinUKMDelay) { |
| MaybeSendUKM(); |
| } |
| |
| return true; |
| } |
| |
| void RenderAccessibilityImpl::OnSerializationReceived() { |
| DCHECK(ax_context_); |
| ax_context_->OnSerializationReceived(); |
| } |
| |
| void RenderAccessibilityImpl::OnLoadInlineTextBoxes( |
| const ui::AXActionTarget* target) { |
| const BlinkAXActionTarget* blink_target = |
| BlinkAXActionTarget::FromAXActionTarget(target); |
| if (!blink_target) { |
| return; |
| } |
| const WebAXObject& obj = blink_target->WebAXObject(); |
| |
| DCHECK(ax_context_); |
| obj.OnLoadInlineTextBoxes(); |
| |
| // 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(); |
| obj.SetImageAsDataNodeId(max_size); |
| |
| const WebDocument& document = GetMainDocument(); |
| if (document.IsNull()) { |
| return; |
| } |
| |
| MarkWebAXObjectDirty(obj); |
| HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kImageFrameUpdated, |
| ax::mojom::EventFrom::kAction, |
| ax::mojom::Action::kGetImageData)); |
| } |
| |
| void RenderAccessibilityImpl::OnDestruct() { |
| render_frame_ = nullptr; |
| delete this; |
| } |
| |
| blink::WebDocument RenderAccessibilityImpl::GetPopupDocument() { |
| blink::WebPagePopup* popup = render_frame_->GetWebView()->GetPagePopup(); |
| if (popup) |
| return popup->GetDocument(); |
| return WebDocument(); |
| } |
| |
| WebAXObject RenderAccessibilityImpl::ComputeRoot() { |
| DCHECK(render_frame_); |
| DCHECK(render_frame_->GetWebFrame()); |
| return WebAXObject::FromWebDocument(GetMainDocument()); |
| } |
| |
| void RenderAccessibilityImpl::ConnectionClosed() { |
| // This can happen when a navigation occurs with a serialization is in flight. |
| // There is nothing special to do here. |
| ax_context_->OnSerializationCancelled(); |
| } |
| |
| void RenderAccessibilityImpl::SetPluginAXTreeActionTargetAdapter( |
| PluginAXTreeActionTargetAdapter* adapter) { |
| plugin_action_target_adapter_ = adapter; |
| } |
| |
| void RenderAccessibilityImpl::MaybeSendUKM() { |
| if (slowest_serialization_time_ < kMinSerializationTimeToSend) |
| return; |
| |
| ukm::builders::Accessibility_Renderer(last_ukm_source_id_) |
| .SetCpuTime_SendPendingAccessibilityEvents( |
| slowest_serialization_time_.InMilliseconds()) |
| .Record(ukm_recorder_.get()); |
| ResetUKMData(); |
| } |
| |
| void RenderAccessibilityImpl::ResetUKMData() { |
| slowest_serialization_time_ = base::TimeDelta(); |
| ukm_timer_ = std::make_unique<base::ElapsedTimer>(); |
| last_ukm_source_id_ = ukm::kInvalidSourceId; |
| } |
| |
| } // namespace content |