Files
BrowserOS/patches/nxtscape/browserOS-API.patch
2025-07-17 17:57:18 -07:00

2742 lines
97 KiB
Diff

From fd332e09d563fe667f00c541a889d2a4b41dd200 Mon Sep 17 00:00:00 2001
From: Nikhil Sonti <nikhilsv92@gmail.com>
Date: Mon, 7 Jul 2025 15:38:54 -0700
Subject: [PATCH 1/2] browserOS API
---
chrome/browser/extensions/BUILD.gn | 8 +
.../api/browser_os/browser_os_api.cc | 760 ++++++++++++++++++
.../api/browser_os/browser_os_api.h | 196 +++++
.../api/browser_os/browser_os_api_helpers.cc | 63 ++
.../api/browser_os/browser_os_api_helpers.h | 44 +
.../api/browser_os/browser_os_api_utils.cc | 378 +++++++++
.../api/browser_os/browser_os_api_utils.h | 62 ++
.../browser_os_snapshot_processor.cc | 679 ++++++++++++++++
.../browser_os_snapshot_processor.h | 89 ++
.../chrome_extensions_browser_api_provider.cc | 8 +
.../common/extensions/api/_api_features.json | 28 +
.../extensions/api/_permission_features.json | 4 +
chrome/common/extensions/api/api_sources.gni | 1 +
chrome/common/extensions/api/browser_os.idl | 194 +++++
.../permissions/chrome_api_permissions.cc | 1 +
.../extension_function_histogram_value.h | 12 +
.../common/mojom/api_permission_id.mojom | 1 +
.../histograms/metadata/extensions/enums.xml | 10 +
18 files changed, 2538 insertions(+)
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_api.cc
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_api.h
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h
create mode 100644 chrome/common/extensions/api/browser_os.idl
diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn
index d50ffdfbcce34..bbb5a16ccd5d0 100644
--- a/chrome/browser/extensions/BUILD.gn
+++ b/chrome/browser/extensions/BUILD.gn
@@ -516,6 +516,14 @@ source_set("extensions") {
"api/bookmark_manager_private/bookmark_manager_private_api.h",
"api/bookmarks/bookmarks_api.cc",
"api/bookmarks/bookmarks_api.h",
+ "api/browser_os/browser_os_api.cc",
+ "api/browser_os/browser_os_api.h",
+ "api/browser_os/browser_os_api_helpers.cc",
+ "api/browser_os/browser_os_api_helpers.h",
+ "api/browser_os/browser_os_api_utils.cc",
+ "api/browser_os/browser_os_api_utils.h",
+ "api/browser_os/browser_os_snapshot_processor.cc",
+ "api/browser_os/browser_os_snapshot_processor.h",
"api/chrome_device_permissions_prompt.h",
"api/chrome_extensions_api_client.cc",
"api/chrome_extensions_api_client.h",
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api.cc b/chrome/browser/extensions/api/browser_os/browser_os_api.cc
new file mode 100644
index 0000000000000..c39e67d88e1de
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api.cc
@@ -0,0 +1,760 @@
+// Copyright 2024 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/browser/extensions/api/browser_os/browser_os_api.h"
+
+#include <set>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "base/functional/bind.h"
+#include "base/json/json_writer.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/base64.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h"
+#include "chrome/browser/extensions/extension_tab_util.h"
+#include "chrome/browser/extensions/window_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/extensions/api/browser_os.h"
+#include "content/browser/renderer_host/render_widget_host_impl.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_widget_host.h"
+#include "content/public/browser/render_widget_host_view.h"
+#include "content/public/browser/web_contents.h"
+#include "third_party/blink/public/common/input/web_input_event.h"
+#include "third_party/blink/public/common/input/web_mouse_event.h"
+#include "ui/accessibility/ax_action_data.h"
+#include "ui/accessibility/ax_enum_util.h"
+#include "ui/accessibility/ax_mode.h"
+#include "ui/accessibility/ax_node_data.h"
+#include "ui/accessibility/ax_role_properties.h"
+#include "ui/accessibility/ax_tree_update.h"
+#include "ui/base/ime/ime_text_span.h"
+#include "ui/events/base_event_utils.h"
+#include "ui/events/keycodes/dom/dom_code.h"
+#include "ui/events/keycodes/dom/dom_key.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+#include "ui/gfx/geometry/point_f.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/rect_f.h"
+#include "ui/gfx/range/range.h"
+#include "ui/gfx/codec/png_codec.h"
+#include "ui/gfx/image/image.h"
+#include "ui/snapshot/snapshot.h"
+
+namespace extensions {
+namespace api {
+
+// Static member initialization
+uint32_t BrowserOSGetInteractiveSnapshotFunction::next_snapshot_id_ = 1;
+
+// Constructor and destructor implementations
+BrowserOSGetInteractiveSnapshotFunction::BrowserOSGetInteractiveSnapshotFunction() = default;
+BrowserOSGetInteractiveSnapshotFunction::~BrowserOSGetInteractiveSnapshotFunction() = default;
+
+ExtensionFunction::ResponseAction BrowserOSGetAccessibilityTreeFunction::Run() {
+ std::optional<browser_os::GetAccessibilityTree::Params> params =
+ browser_os::GetAccessibilityTree::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Enable accessibility if needed
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ // Request accessibility tree snapshot
+ // Use WebContents with extended properties to get a full tree
+ web_contents->RequestAXTreeSnapshot(
+ base::BindOnce(
+ &BrowserOSGetAccessibilityTreeFunction::OnAccessibilityTreeReceived,
+ this),
+ ui::AXMode(ui::AXMode::kWebContents | ui::AXMode::kExtendedProperties |
+ ui::AXMode::kInlineTextBoxes),
+ /* max_nodes= */ 0, // No limit
+ /* timeout= */ base::TimeDelta(),
+ content::WebContents::AXTreeSnapshotPolicy::kAll);
+
+ return RespondLater();
+}
+
+void BrowserOSGetAccessibilityTreeFunction::OnAccessibilityTreeReceived(
+ ui::AXTreeUpdate& tree_update) {
+ browser_os::AccessibilityTree result;
+ result.root_id = tree_update.root_id;
+
+ // Convert AX nodes to API format
+ base::Value::Dict nodes;
+ for (const auto& node_data : tree_update.nodes) {
+ browser_os::AccessibilityNode node;
+ node.id = node_data.id;
+ node.role = ui::ToString(node_data.role);
+
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
+ node.name =
+ node_data.GetStringAttribute(ax::mojom::StringAttribute::kName);
+ }
+
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kValue)) {
+ node.value =
+ node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue);
+ }
+
+ // Add child IDs
+ if (!node_data.child_ids.empty()) {
+ node.child_ids.emplace();
+ for (int32_t child_id : node_data.child_ids) {
+ node.child_ids->push_back(child_id);
+ }
+ }
+
+ // Add basic attributes
+ base::Value::Dict attributes;
+ if (node_data.HasBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
+ attributes.Set("selected",
+ node_data.GetBoolAttribute(ax::mojom::BoolAttribute::kSelected));
+ }
+ // TODO: Add focused attribute when available
+ if (node_data.HasIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)) {
+ attributes.Set("level",
+ node_data.GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel));
+ }
+ if (!attributes.empty()) {
+ browser_os::AccessibilityNode::Attributes attr;
+ attr.additional_properties = std::move(attributes);
+ node.attributes = std::move(attr);
+ }
+
+ // Convert to dictionary
+ nodes.Set(base::NumberToString(node_data.id), node.ToValue());
+ }
+
+ result.nodes.additional_properties = std::move(nodes);
+
+ Respond(ArgumentList(
+ browser_os::GetAccessibilityTree::Results::Create(result)));
+}
+
+// Implementation of BrowserOSGetInteractiveSnapshotFunction
+
+ExtensionFunction::ResponseAction BrowserOSGetInteractiveSnapshotFunction::Run() {
+ std::optional<browser_os::GetInteractiveSnapshot::Params> params =
+ browser_os::GetInteractiveSnapshot::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Note: We don't need to get scale factors here!
+ // The accessibility tree provides bounds in CSS pixels (logical pixels),
+ // which is the correct coordinate space for ForwardMouseEvent.
+ // The browser and renderer handle device pixel ratio conversion internally.
+
+ // Store tab ID for mapping
+ tab_id_ = tab_info->tab_id;
+
+ // Get viewport size
+ content::RenderWidgetHostView* rwhv = web_contents->GetRenderWidgetHostView();
+ if (rwhv) {
+ viewport_size_ = rwhv->GetVisibleViewportSize();
+ LOG(INFO) << "Viewport size: " << viewport_size_.ToString();
+ }
+
+ // Request accessibility tree snapshot
+ web_contents->RequestAXTreeSnapshot(
+ base::BindOnce(
+ &BrowserOSGetInteractiveSnapshotFunction::OnAccessibilityTreeReceived,
+ this),
+ ui::AXMode(ui::AXMode::kWebContents | ui::AXMode::kExtendedProperties |
+ ui::AXMode::kInlineTextBoxes),
+ /* max_nodes= */ 0, // No limit
+ /* timeout= */ base::TimeDelta(),
+ content::WebContents::AXTreeSnapshotPolicy::kAll);
+
+ return RespondLater();
+}
+
+void BrowserOSGetInteractiveSnapshotFunction::OnAccessibilityTreeReceived(
+ ui::AXTreeUpdate& tree_update) {
+ // Simple API layer - just delegates to the processor
+ SnapshotProcessor::ProcessAccessibilityTree(
+ tree_update,
+ tab_id_,
+ next_snapshot_id_++,
+ viewport_size_,
+ base::BindOnce(
+ &BrowserOSGetInteractiveSnapshotFunction::OnSnapshotProcessed,
+ base::WrapRefCounted(this)));
+}
+
+void BrowserOSGetInteractiveSnapshotFunction::OnSnapshotProcessed(
+ SnapshotProcessingResult result) {
+ Respond(ArgumentList(
+ browser_os::GetInteractiveSnapshot::Results::Create(result.snapshot)));
+}
+
+// Implementation of BrowserOSClickFunction
+
+ExtensionFunction::ResponseAction BrowserOSClickFunction::Run() {
+ std::optional<browser_os::Click::Params> params =
+ browser_os::Click::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+ int tab_id = tab_info->tab_id;
+
+ // Look up the AX node ID from our nodeId
+ auto tab_it = GetNodeIdMappings().find(tab_id);
+ if (tab_it == GetNodeIdMappings().end()) {
+ return RespondNow(Error("No snapshot data for this tab"));
+ }
+
+ auto node_it = tab_it->second.find(params->node_id);
+ if (node_it == tab_it->second.end()) {
+ return RespondNow(Error("Node ID not found"));
+ }
+
+ const NodeInfo& node_info = node_it->second;
+
+ // Calculate click point (center of the element)
+ gfx::PointF click_point(
+ node_info.bounds.x() + node_info.bounds.width() / 2.0f,
+ node_info.bounds.y() + node_info.bounds.height() / 2.0f);
+
+ // Perform the click
+ PerformClick(web_contents, click_point);
+
+ return RespondNow(NoArguments());
+}
+
+// Implementation of BrowserOSInputTextFunction
+
+ExtensionFunction::ResponseAction BrowserOSInputTextFunction::Run() {
+ std::optional<browser_os::InputText::Params> params =
+ browser_os::InputText::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+ int tab_id = tab_info->tab_id;
+
+ // Look up the AX node ID from our nodeId
+ auto tab_it = GetNodeIdMappings().find(tab_id);
+ if (tab_it == GetNodeIdMappings().end()) {
+ return RespondNow(Error("No snapshot data for this tab"));
+ }
+
+ auto node_it = tab_it->second.find(params->node_id);
+ if (node_it == tab_it->second.end()) {
+ return RespondNow(Error("Node ID not found"));
+ }
+
+ const NodeInfo& node_info = node_it->second;
+
+
+ // First, click on the element to focus it
+ gfx::PointF click_point(
+ node_info.bounds.x() + node_info.bounds.width() / 2.0f,
+ node_info.bounds.y() + node_info.bounds.height() / 2.0f);
+
+ PerformClick(web_contents, click_point);
+
+ // Get render widget host for text input
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh) {
+ return RespondNow(Error("No render widget host"));
+ }
+
+ // Convert text to UTF16
+ std::u16string text16 = base::UTF8ToUTF16(params->text);
+
+ // Add a small delay to ensure the element is focused after click
+ // Then send the text using ImeCommitText
+ base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(
+ [](content::RenderWidgetHost* rwh, const std::u16string& text) {
+ if (!rwh)
+ return;
+
+ content::RenderWidgetHostImpl* rwhi =
+ static_cast<content::RenderWidgetHostImpl*>(rwh);
+
+ // Ensure the widget has focus
+ rwhi->Focus();
+
+ // Try multiple approaches to input text
+ // 1. First try ImeSetComposition to simulate typing
+ rwhi->ImeSetComposition(text,
+ std::vector<ui::ImeTextSpan>(),
+ gfx::Range::InvalidRange(),
+ text.length(), // selection_start at end
+ text.length()); // selection_end at end
+
+ // 2. Then commit the text
+ rwhi->ImeCommitText(text,
+ std::vector<ui::ImeTextSpan>(),
+ gfx::Range::InvalidRange(),
+ 0); // relative_cursor_pos = 0 means after the text
+
+ // 3. Finish composing to ensure text is committed
+ rwhi->ImeFinishComposingText(false);
+
+ },
+ rwh, text16),
+ base::Milliseconds(100)); // Increase delay to 100ms for better focus handling
+
+
+ return RespondNow(NoArguments());
+}
+
+// Implementation of BrowserOSClearFunction
+
+ExtensionFunction::ResponseAction BrowserOSClearFunction::Run() {
+ std::optional<browser_os::Clear::Params> params =
+ browser_os::Clear::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+ int tab_id = tab_info->tab_id;
+
+ // Look up the AX node ID from our nodeId
+ auto tab_it = GetNodeIdMappings().find(tab_id);
+ if (tab_it == GetNodeIdMappings().end()) {
+ return RespondNow(Error("No snapshot data for this tab"));
+ }
+
+ auto node_it = tab_it->second.find(params->node_id);
+ if (node_it == tab_it->second.end()) {
+ return RespondNow(Error("Node ID not found"));
+ }
+
+ const NodeInfo& node_info = node_it->second;
+
+ // First, click on the element to focus it
+ gfx::PointF click_point(
+ node_info.bounds.x() + node_info.bounds.width() / 2.0f,
+ node_info.bounds.y() + node_info.bounds.height() / 2.0f);
+
+ PerformClick(web_contents, click_point);
+
+ // Get render widget host for keyboard events
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh) {
+ return RespondNow(Error("No render widget host"));
+ }
+
+ // Use JavaScript to clear the field, similar to how Puppeteer does it
+ rfh->ExecuteJavaScriptForTests(
+ u"(function() {"
+ u" var activeElement = document.activeElement;"
+ u" if (activeElement) {"
+ u" if (activeElement.value !== undefined) {"
+ u" activeElement.value = '';"
+ u" }"
+ u" if (activeElement.textContent !== undefined && activeElement.isContentEditable) {"
+ u" activeElement.textContent = '';"
+ u" }"
+ u" activeElement.dispatchEvent(new Event('input', {bubbles: true}));"
+ u" activeElement.dispatchEvent(new Event('change', {bubbles: true}));"
+ u" }"
+ u"})();",
+ base::NullCallback(),
+ /*honor_js_content_settings=*/false);
+
+ return RespondNow(NoArguments());
+}
+
+// Implementation of BrowserOSGetPageLoadStatusFunction
+
+ExtensionFunction::ResponseAction BrowserOSGetPageLoadStatusFunction::Run() {
+ std::optional<browser_os::GetPageLoadStatus::Params> params =
+ browser_os::GetPageLoadStatus::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Get the primary main frame
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ // Build the status object
+ browser_os::PageLoadStatus status;
+
+ // Check if any resources are still loading
+ status.is_resources_loading = web_contents->IsLoading();
+
+ // Check if DOMContentLoaded has fired
+ status.is_dom_content_loaded = rfh->IsDOMContentLoaded();
+
+ // Check if onload has completed (all resources loaded)
+ status.is_page_complete = rfh->IsDocumentOnLoadCompletedInMainFrame();
+
+ return RespondNow(ArgumentList(
+ browser_os::GetPageLoadStatus::Results::Create(status)));
+}
+
+// Implementation of BrowserOSScrollUpFunction
+
+ExtensionFunction::ResponseAction BrowserOSScrollUpFunction::Run() {
+ std::optional<browser_os::ScrollUp::Params> params =
+ browser_os::ScrollUp::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Get viewport height to scroll by approximately one page
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh) {
+ return RespondNow(Error("No render widget host"));
+ }
+
+ content::RenderWidgetHostView* rwhv = rwh->GetView();
+ if (!rwhv) {
+ return RespondNow(Error("No render widget host view"));
+ }
+
+ gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+ int scroll_amount = viewport_bounds.height() * 0.9; // 90% of viewport height
+
+ // Perform scroll up (negative delta_y)
+ PerformScroll(web_contents, 0, -scroll_amount, true);
+
+ return RespondNow(NoArguments());
+}
+
+// Implementation of BrowserOSScrollDownFunction
+
+ExtensionFunction::ResponseAction BrowserOSScrollDownFunction::Run() {
+ std::optional<browser_os::ScrollDown::Params> params =
+ browser_os::ScrollDown::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Get viewport height to scroll by approximately one page
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh) {
+ return RespondNow(Error("No render widget host"));
+ }
+
+ content::RenderWidgetHostView* rwhv = rwh->GetView();
+ if (!rwhv) {
+ return RespondNow(Error("No render widget host view"));
+ }
+
+ gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+ int scroll_amount = viewport_bounds.height() * 0.9; // 90% of viewport height
+
+ // Perform scroll down (positive delta_y)
+ PerformScroll(web_contents, 0, scroll_amount, true);
+
+ return RespondNow(NoArguments());
+}
+
+// Implementation of BrowserOSScrollToNodeFunction
+
+ExtensionFunction::ResponseAction BrowserOSScrollToNodeFunction::Run() {
+ std::optional<browser_os::ScrollToNode::Params> params =
+ browser_os::ScrollToNode::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+ int tab_id = tab_info->tab_id;
+
+ // Look up the AX node ID from our nodeId
+ auto tab_it = GetNodeIdMappings().find(tab_id);
+ if (tab_it == GetNodeIdMappings().end()) {
+ return RespondNow(Error("No snapshot data for this tab"));
+ }
+
+ auto node_it = tab_it->second.find(params->node_id);
+ if (node_it == tab_it->second.end()) {
+ return RespondNow(Error("Node ID not found"));
+ }
+
+ const NodeInfo& node_info = node_it->second;
+
+ // Get viewport bounds to check if node is already in view
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh) {
+ return RespondNow(Error("No render widget host"));
+ }
+
+ content::RenderWidgetHostView* rwhv = rwh->GetView();
+ if (!rwhv) {
+ return RespondNow(Error("No render widget host view"));
+ }
+
+ gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+
+ // Check if the node is already visible in the viewport
+ // We consider it visible if any part of it is within the viewport
+ bool is_in_view = false;
+ if (node_info.bounds.y() < viewport_bounds.height() &&
+ node_info.bounds.bottom() > 0 &&
+ node_info.bounds.x() < viewport_bounds.width() &&
+ node_info.bounds.right() > 0) {
+ is_in_view = true;
+ }
+
+ if (!is_in_view) {
+ // Use accessibility action to scroll
+ if (rfh) {
+ ui::AXActionData action_data;
+ action_data.action = ax::mojom::Action::kScrollToMakeVisible;
+ action_data.target_node_id = node_info.ax_node_id;
+ action_data.horizontal_scroll_alignment = ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
+ action_data.vertical_scroll_alignment = ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
+ action_data.scroll_behavior = ax::mojom::ScrollBehavior::kScrollIfVisible;
+
+ rfh->AccessibilityPerformAction(action_data);
+ }
+ }
+
+ return RespondNow(ArgumentList(
+ browser_os::ScrollToNode::Results::Create(!is_in_view)));
+}
+
+// Implementation of BrowserOSSendKeysFunction
+
+ExtensionFunction::ResponseAction BrowserOSSendKeysFunction::Run() {
+ std::optional<browser_os::SendKeys::Params> params =
+ browser_os::SendKeys::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Validate the key - use a simple check instead of std::set to avoid exit-time destructor
+ const std::string& key = params->key;
+ bool is_supported = (key == "Enter" || key == "Delete" || key == "Backspace" ||
+ key == "Tab" || key == "Escape" || key == "ArrowUp" ||
+ key == "ArrowDown" || key == "ArrowLeft" || key == "ArrowRight" ||
+ key == "Home" || key == "End" || key == "PageUp" || key == "PageDown");
+
+ if (!is_supported) {
+ return RespondNow(Error("Unsupported key: " + params->key));
+ }
+
+ // Send the key
+ SendSpecialKey(web_contents, params->key);
+
+ return RespondNow(NoArguments());
+}
+
+// Implementation of BrowserOSCaptureScreenshotFunction
+
+ExtensionFunction::ResponseAction BrowserOSCaptureScreenshotFunction::Run() {
+ std::optional<browser_os::CaptureScreenshot::Params> params =
+ browser_os::CaptureScreenshot::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+
+ // Get the target tab
+ std::string error_message;
+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(),
+ include_incognito_information(),
+ &error_message);
+ if (!tab_info) {
+ return RespondNow(Error(error_message));
+ }
+
+ content::WebContents* web_contents = tab_info->web_contents;
+
+ // Get the render widget host view
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh) {
+ return RespondNow(Error("No render frame"));
+ }
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh) {
+ return RespondNow(Error("No render widget host"));
+ }
+
+ content::RenderWidgetHostView* rwhv = rwh->GetView();
+ if (!rwhv) {
+ return RespondNow(Error("No render widget host view"));
+ }
+
+ // Get the view bounds to determine the size
+ gfx::Rect view_bounds = rwhv->GetViewBounds();
+
+ // Create a reasonable thumbnail size (e.g., 256x256 or proportional)
+ const int kMaxThumbnailSize = 1024; // 512;//256;
+ gfx::Size thumbnail_size = view_bounds.size();
+
+ // Scale down proportionally
+ if (thumbnail_size.width() > kMaxThumbnailSize ||
+ thumbnail_size.height() > kMaxThumbnailSize) {
+ float scale = std::min(
+ static_cast<float>(kMaxThumbnailSize) / thumbnail_size.width(),
+ static_cast<float>(kMaxThumbnailSize) / thumbnail_size.height());
+ thumbnail_size = gfx::ScaleToFlooredSize(thumbnail_size, scale);
+ }
+
+ // For macOS, we need to use a different approach since GrabWindowSnapshot
+ // expects a window, not a view. Let's use CopyFromSurface instead.
+ content::RenderWidgetHostImpl* rwhi =
+ static_cast<content::RenderWidgetHostImpl*>(rwh);
+
+ // Request a copy of the surface
+ rwhi->GetView()->CopyFromSurface(
+ gfx::Rect(), // Empty rect means copy entire surface
+ thumbnail_size,
+ base::BindOnce(&BrowserOSCaptureScreenshotFunction::OnScreenshotCaptured,
+ this));
+
+ return RespondLater();
+}
+
+void BrowserOSCaptureScreenshotFunction::OnScreenshotCaptured(
+ const SkBitmap& bitmap) {
+ if (bitmap.empty()) {
+ Respond(Error("Failed to capture screenshot"));
+ return;
+ }
+
+ // Convert bitmap to PNG
+ auto png_data = gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false);
+ if (!png_data.has_value()) {
+ Respond(Error("Failed to encode screenshot"));
+ return;
+ }
+
+ // Convert to base64 data URL
+ std::string base64_data = base::Base64Encode(png_data.value());
+
+ std::string data_url = "data:image/png;base64," + base64_data;
+
+ Respond(ArgumentList(
+ browser_os::CaptureScreenshot::Results::Create(data_url)));
+}
+
+} // namespace api
+} // namespace extensions
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api.h b/chrome/browser/extensions/api/browser_os/browser_os_api.h
new file mode 100644
index 0000000000000..58acc663f0170
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api.h
@@ -0,0 +1,196 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_H_
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_H_
+
+#include <cstdint>
+
+#include "base/values.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h"
+#include "extensions/browser/extension_function.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace content {
+class WebContents;
+}
+
+namespace ui {
+struct AXTreeUpdate;
+}
+
+namespace extensions {
+namespace api {
+
+
+class BrowserOSGetAccessibilityTreeFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.getAccessibilityTree",
+ BROWSER_OS_GETACCESSIBILITYTREE)
+
+ BrowserOSGetAccessibilityTreeFunction() = default;
+
+ protected:
+ ~BrowserOSGetAccessibilityTreeFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+
+ private:
+ void OnAccessibilityTreeReceived(ui::AXTreeUpdate& tree_update);
+};
+
+class BrowserOSGetInteractiveSnapshotFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.getInteractiveSnapshot",
+ BROWSER_OS_GETINTERACTIVESNAPSHOT)
+
+ BrowserOSGetInteractiveSnapshotFunction();
+
+ protected:
+ ~BrowserOSGetInteractiveSnapshotFunction() override;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+
+ private:
+ void OnAccessibilityTreeReceived(ui::AXTreeUpdate& tree_update);
+ void OnSnapshotProcessed(SnapshotProcessingResult result);
+
+ // Counter for snapshot IDs
+ static uint32_t next_snapshot_id_;
+
+ // Tab ID for storing mappings
+ int tab_id_ = -1;
+
+ // Viewport size for checking visibility
+ gfx::Size viewport_size_;
+};
+
+class BrowserOSClickFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.click", BROWSER_OS_CLICK)
+
+ BrowserOSClickFunction() = default;
+
+ protected:
+ ~BrowserOSClickFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSInputTextFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.inputText", BROWSER_OS_INPUTTEXT)
+
+ BrowserOSInputTextFunction() = default;
+
+ protected:
+ ~BrowserOSInputTextFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSClearFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.clear", BROWSER_OS_CLEAR)
+
+ BrowserOSClearFunction() = default;
+
+ protected:
+ ~BrowserOSClearFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSGetPageLoadStatusFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.getPageLoadStatus",
+ BROWSER_OS_GETPAGELOADSTATUS)
+
+ BrowserOSGetPageLoadStatusFunction() = default;
+
+ protected:
+ ~BrowserOSGetPageLoadStatusFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSScrollUpFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.scrollUp", BROWSER_OS_SCROLLUP)
+
+ BrowserOSScrollUpFunction() = default;
+
+ protected:
+ ~BrowserOSScrollUpFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSScrollDownFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.scrollDown", BROWSER_OS_SCROLLDOWN)
+
+ BrowserOSScrollDownFunction() = default;
+
+ protected:
+ ~BrowserOSScrollDownFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSScrollToNodeFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.scrollToNode", BROWSER_OS_SCROLLTONODE)
+
+ BrowserOSScrollToNodeFunction() = default;
+
+ protected:
+ ~BrowserOSScrollToNodeFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSSendKeysFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.sendKeys", BROWSER_OS_SENDKEYS)
+
+ BrowserOSSendKeysFunction() = default;
+
+ protected:
+ ~BrowserOSSendKeysFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+};
+
+class BrowserOSCaptureScreenshotFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("browserOS.captureScreenshot", BROWSER_OS_CAPTURESCREENSHOT)
+
+ BrowserOSCaptureScreenshotFunction() = default;
+
+ protected:
+ ~BrowserOSCaptureScreenshotFunction() override = default;
+
+ // ExtensionFunction:
+ ResponseAction Run() override;
+
+ private:
+ void OnScreenshotCaptured(const SkBitmap& bitmap);
+};
+
+} // namespace api
+} // namespace extensions
+
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_H_
\ No newline at end of file
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc
new file mode 100644
index 0000000000000..e66c5004440ae
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc
@@ -0,0 +1,63 @@
+// Copyright 2024 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/browser/extensions/api/browser_os/browser_os_api_helpers.h"
+
+#include "chrome/browser/extensions/extension_tab_util.h"
+#include "chrome/browser/extensions/window_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "content/public/browser/web_contents.h"
+
+namespace extensions {
+namespace api {
+
+std::optional<TabInfo> GetTabFromOptionalId(
+ std::optional<int> tab_id_param,
+ content::BrowserContext* browser_context,
+ bool include_incognito_information,
+ std::string* error_message) {
+ content::WebContents* web_contents = nullptr;
+ int tab_id = -1;
+
+ if (tab_id_param) {
+ // Get specific tab by ID
+ WindowController* controller = nullptr;
+ int tab_index = -1;
+ if (!ExtensionTabUtil::GetTabById(*tab_id_param, browser_context,
+ include_incognito_information,
+ &controller, &web_contents,
+ &tab_index)) {
+ if (error_message) {
+ *error_message = "Tab not found";
+ }
+ return std::nullopt;
+ }
+ tab_id = *tab_id_param;
+ } else {
+ // Get active tab
+ Browser* browser = chrome::FindLastActive();
+ if (!browser) {
+ if (error_message) {
+ *error_message = "No active browser";
+ }
+ return std::nullopt;
+ }
+
+ web_contents = browser->tab_strip_model()->GetActiveWebContents();
+ if (!web_contents) {
+ if (error_message) {
+ *error_message = "No active tab";
+ }
+ return std::nullopt;
+ }
+ tab_id = ExtensionTabUtil::GetTabId(web_contents);
+ }
+
+ return TabInfo(web_contents, tab_id);
+}
+
+} // namespace api
+} // namespace extensions
\ No newline at end of file
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
new file mode 100644
index 0000000000000..142874b3d0374
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
@@ -0,0 +1,44 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
+
+#include <optional>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+
+namespace content {
+class BrowserContext;
+class WebContents;
+} // namespace content
+
+namespace extensions {
+
+class WindowController;
+
+namespace api {
+
+// Result structure for tab retrieval
+struct TabInfo {
+ raw_ptr<content::WebContents> web_contents;
+ int tab_id;
+
+ TabInfo(content::WebContents* wc, int id)
+ : web_contents(wc), tab_id(id) {}
+};
+
+// Helper to get WebContents and tab ID from optional tab_id parameter
+// Returns nullptr if tab is not found, with error message set
+std::optional<TabInfo> GetTabFromOptionalId(
+ std::optional<int> tab_id_param,
+ content::BrowserContext* browser_context,
+ bool include_incognito_information,
+ std::string* error_message);
+
+} // namespace api
+} // namespace extensions
+
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
\ No newline at end of file
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
new file mode 100644
index 0000000000000..0d83f4da95ba9
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
@@ -0,0 +1,378 @@
+// Copyright 2024 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/browser/extensions/api/browser_os/browser_os_api_utils.h"
+
+#include "base/hash/hash.h"
+#include "base/no_destructor.h"
+#include "base/strings/string_number_conversions.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_widget_host.h"
+#include "content/public/browser/render_widget_host_view.h"
+#include "content/public/browser/web_contents.h"
+#include "components/input/native_web_keyboard_event.h"
+#include "third_party/blink/public/common/input/web_input_event.h"
+#include "third_party/blink/public/common/input/web_keyboard_event.h"
+#include "third_party/blink/public/common/input/web_mouse_event.h"
+#include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
+#include "ui/accessibility/ax_role_properties.h"
+#include "ui/events/base_event_utils.h"
+#include "ui/events/keycodes/dom/dom_code.h"
+#include "ui/events/keycodes/dom/dom_key.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+#include "ui/gfx/geometry/point_f.h"
+
+namespace extensions {
+namespace api {
+
+// Global node ID mappings storage
+// Use NoDestructor to avoid exit-time destructor
+std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>&
+GetNodeIdMappings() {
+ static base::NoDestructor<std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>>
+ g_node_id_mappings;
+ return *g_node_id_mappings;
+}
+
+// Helper to create and dispatch mouse events for clicking
+void PerformClick(content::WebContents* web_contents,
+ const gfx::PointF& point) {
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh)
+ return;
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh)
+ return;
+
+ content::RenderWidgetHostView* rwhv = rwh->GetView();
+ if (!rwhv)
+ return;
+
+ // Get viewport bounds for screen position calculation
+ gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+ gfx::PointF viewport_origin(viewport_bounds.x(), viewport_bounds.y());
+
+ // The coordinates are already in widget space (CSS pixels)
+ gfx::PointF widget_point = point;
+
+ // Create mouse down event
+ blink::WebMouseEvent mouse_down;
+ mouse_down.SetType(blink::WebInputEvent::Type::kMouseDown);
+ mouse_down.button = blink::WebPointerProperties::Button::kLeft;
+ mouse_down.click_count = 1;
+ mouse_down.SetPositionInWidget(widget_point.x(), widget_point.y());
+ mouse_down.SetPositionInScreen(widget_point.x() + viewport_origin.x(),
+ widget_point.y() + viewport_origin.y());
+ mouse_down.SetTimeStamp(ui::EventTimeForNow());
+ mouse_down.SetModifiers(blink::WebInputEvent::kLeftButtonDown);
+
+ // Create mouse up event
+ blink::WebMouseEvent mouse_up;
+ mouse_up.SetType(blink::WebInputEvent::Type::kMouseUp);
+ mouse_up.button = blink::WebPointerProperties::Button::kLeft;
+ mouse_up.click_count = 1;
+ mouse_up.SetPositionInWidget(widget_point.x(), widget_point.y());
+ mouse_up.SetPositionInScreen(widget_point.x() + viewport_origin.x(),
+ widget_point.y() + viewport_origin.y());
+ mouse_up.SetTimeStamp(ui::EventTimeForNow());
+
+ // Send the events
+ rwh->ForwardMouseEvent(mouse_down);
+ rwh->ForwardMouseEvent(mouse_up);
+}
+
+// Helper to determine if a node is interactive (clickable/typeable/selectable)
+browser_os::InteractiveNodeType GetInteractiveNodeType(
+ const ui::AXNodeData& node_data) {
+
+ // Skip invisible or ignored nodes early
+ if (node_data.IsInvisibleOrIgnored()) {
+ return browser_os::InteractiveNodeType::kOther;
+ }
+
+ // Use built-in IsTextField() and related methods for typeable elements
+ if (node_data.IsTextField() ||
+ node_data.IsPasswordField() ||
+ node_data.IsAtomicTextField() ||
+ node_data.IsNonAtomicTextField() ||
+ node_data.IsSpinnerTextField()) {
+ return browser_os::InteractiveNodeType::kTypeable;
+ }
+
+ // Use built-in IsSelectable() for selectable elements
+ if (node_data.IsSelectable()) {
+ return browser_os::InteractiveNodeType::kSelectable;
+ }
+
+ // Use built-in IsClickable() method
+ if (node_data.IsClickable()) {
+ return browser_os::InteractiveNodeType::kClickable;
+ }
+
+ // Additional check for combobox and list options which might not be caught by IsSelectable
+ using Role = ax::mojom::Role;
+ if (node_data.role == Role::kComboBoxSelect ||
+ node_data.role == Role::kComboBoxMenuButton ||
+ node_data.role == Role::kComboBoxGrouping ||
+ node_data.role == Role::kListBox ||
+ node_data.role == Role::kListBoxOption ||
+ node_data.role == Role::kMenuListOption ||
+ node_data.role == Role::kMenuItem ||
+ node_data.role == Role::kMenuItemCheckBox ||
+ node_data.role == Role::kMenuItemRadio) {
+ return browser_os::InteractiveNodeType::kSelectable;
+ }
+
+ return browser_os::InteractiveNodeType::kOther;
+}
+
+// Helper to compute branch path hash for an AX node
+uint64_t ComputeBranchPath(const ui::AXNodeData& node_data,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
+ std::unordered_map<int32_t, uint64_t>& path_cache) {
+ // Check cache first
+ auto it = path_cache.find(node_data.id);
+ if (it != path_cache.end()) {
+ return it->second;
+ }
+
+ // Build path from root to this node
+ std::vector<int32_t> path;
+ int32_t current_id = node_data.id;
+
+ while (current_id != 0) {
+ path.push_back(current_id);
+
+ // Find parent
+ bool found_parent = false;
+ for (const auto& [id, data] : node_map) {
+ if (std::find(data.child_ids.begin(), data.child_ids.end(), current_id) !=
+ data.child_ids.end()) {
+ current_id = id;
+ found_parent = true;
+ break;
+ }
+ }
+
+ if (!found_parent) {
+ break;
+ }
+ }
+
+ // Compute hash from path
+ std::string path_str;
+ for (auto path_it = path.rbegin(); path_it != path.rend(); ++path_it) {
+ path_str += base::NumberToString(*path_it) + "/";
+ }
+
+ uint64_t hash = base::PersistentHash(path_str);
+ path_cache[node_data.id] = hash;
+ return hash;
+}
+
+// Helper to get the HTML tag name from AX role
+std::string GetTagFromRole(ax::mojom::Role role) {
+ switch (role) {
+ case ax::mojom::Role::kButton:
+ return "button";
+ case ax::mojom::Role::kLink:
+ return "a";
+ case ax::mojom::Role::kTextField:
+ case ax::mojom::Role::kSearchBox:
+ return "input";
+ case ax::mojom::Role::kTextFieldWithComboBox:
+ return "input";
+ case ax::mojom::Role::kComboBoxSelect:
+ return "select";
+ case ax::mojom::Role::kCheckBox:
+ return "input";
+ case ax::mojom::Role::kRadioButton:
+ return "input";
+ case ax::mojom::Role::kImage:
+ return "img";
+ case ax::mojom::Role::kHeading:
+ return "h1"; // Could be h1-h6
+ case ax::mojom::Role::kParagraph:
+ return "p";
+ case ax::mojom::Role::kListItem:
+ return "li";
+ case ax::mojom::Role::kList:
+ return "ul";
+ case ax::mojom::Role::kForm:
+ return "form";
+ case ax::mojom::Role::kTable:
+ return "table";
+ default:
+ return "div";
+ }
+}
+
+// Helper to perform scroll actions using mouse wheel events
+void PerformScroll(content::WebContents* web_contents,
+ int delta_x,
+ int delta_y,
+ bool precise) {
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh)
+ return;
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh)
+ return;
+
+ content::RenderWidgetHostView* rwhv = rwh->GetView();
+ if (!rwhv)
+ return;
+
+ // Get viewport bounds and center point
+ gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+ gfx::PointF center_point(viewport_bounds.width() / 2.0f,
+ viewport_bounds.height() / 2.0f);
+
+ // Create mouse wheel event
+ blink::WebMouseWheelEvent wheel_event;
+ wheel_event.SetType(blink::WebInputEvent::Type::kMouseWheel);
+ wheel_event.SetPositionInWidget(center_point.x(), center_point.y());
+ wheel_event.SetPositionInScreen(center_point.x() + viewport_bounds.x(),
+ center_point.y() + viewport_bounds.y());
+ wheel_event.SetTimeStamp(ui::EventTimeForNow());
+
+ // Set the scroll deltas
+ wheel_event.delta_x = delta_x;
+ wheel_event.delta_y = delta_y;
+
+ // Set wheel tick values (120 = one notch)
+ wheel_event.wheel_ticks_x = delta_x / 120.0f;
+ wheel_event.wheel_ticks_y = delta_y / 120.0f;
+
+ // Phase information for smooth scrolling
+ wheel_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
+
+ // Precise scrolling for touchpad, non-precise for mouse wheel
+ if (precise) {
+ // For precise scrolling, deltas are in pixels
+ wheel_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel;
+ } else {
+ // For non-precise scrolling, deltas are in lines
+ wheel_event.delta_units = ui::ScrollGranularity::kScrollByLine;
+ }
+
+ // Send the wheel event
+ rwh->ForwardWheelEvent(wheel_event);
+
+ // Send phase ended event for smooth scrolling
+ wheel_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
+ wheel_event.delta_x = 0;
+ wheel_event.delta_y = 0;
+ wheel_event.wheel_ticks_x = 0;
+ wheel_event.wheel_ticks_y = 0;
+ rwh->ForwardWheelEvent(wheel_event);
+}
+
+// Helper to send special key events
+void SendSpecialKey(content::WebContents* web_contents,
+ const std::string& key) {
+ content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+ if (!rfh)
+ return;
+
+ content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+ if (!rwh)
+ return;
+
+ // Map key names to Windows key codes and DOM codes/keys
+ ui::KeyboardCode windows_key_code;
+ ui::DomCode dom_code;
+ ui::DomKey dom_key;
+
+ // Use if-else chain to avoid static initialization
+ if (key == "Enter") {
+ windows_key_code = ui::VKEY_RETURN;
+ dom_code = ui::DomCode::ENTER;
+ dom_key = ui::DomKey::ENTER;
+ } else if (key == "Delete") {
+ windows_key_code = ui::VKEY_DELETE;
+ dom_code = ui::DomCode::DEL;
+ dom_key = ui::DomKey::DEL;
+ } else if (key == "Backspace") {
+ windows_key_code = ui::VKEY_BACK;
+ dom_code = ui::DomCode::BACKSPACE;
+ dom_key = ui::DomKey::BACKSPACE;
+ } else if (key == "Tab") {
+ windows_key_code = ui::VKEY_TAB;
+ dom_code = ui::DomCode::TAB;
+ dom_key = ui::DomKey::TAB;
+ } else if (key == "Escape") {
+ windows_key_code = ui::VKEY_ESCAPE;
+ dom_code = ui::DomCode::ESCAPE;
+ dom_key = ui::DomKey::ESCAPE;
+ } else if (key == "ArrowUp") {
+ windows_key_code = ui::VKEY_UP;
+ dom_code = ui::DomCode::ARROW_UP;
+ dom_key = ui::DomKey::ARROW_UP;
+ } else if (key == "ArrowDown") {
+ windows_key_code = ui::VKEY_DOWN;
+ dom_code = ui::DomCode::ARROW_DOWN;
+ dom_key = ui::DomKey::ARROW_DOWN;
+ } else if (key == "ArrowLeft") {
+ windows_key_code = ui::VKEY_LEFT;
+ dom_code = ui::DomCode::ARROW_LEFT;
+ dom_key = ui::DomKey::ARROW_LEFT;
+ } else if (key == "ArrowRight") {
+ windows_key_code = ui::VKEY_RIGHT;
+ dom_code = ui::DomCode::ARROW_RIGHT;
+ dom_key = ui::DomKey::ARROW_RIGHT;
+ } else if (key == "Home") {
+ windows_key_code = ui::VKEY_HOME;
+ dom_code = ui::DomCode::HOME;
+ dom_key = ui::DomKey::HOME;
+ } else if (key == "End") {
+ windows_key_code = ui::VKEY_END;
+ dom_code = ui::DomCode::END;
+ dom_key = ui::DomKey::END;
+ } else if (key == "PageUp") {
+ windows_key_code = ui::VKEY_PRIOR;
+ dom_code = ui::DomCode::PAGE_UP;
+ dom_key = ui::DomKey::PAGE_UP;
+ } else if (key == "PageDown") {
+ windows_key_code = ui::VKEY_NEXT;
+ dom_code = ui::DomCode::PAGE_DOWN;
+ dom_key = ui::DomKey::PAGE_DOWN;
+ } else {
+ return; // Unsupported key
+ }
+
+ // Create keyboard event
+ input::NativeWebKeyboardEvent key_down(
+ blink::WebInputEvent::Type::kKeyDown,
+ blink::WebInputEvent::kNoModifiers,
+ ui::EventTimeForNow());
+
+ key_down.windows_key_code = windows_key_code;
+ key_down.native_key_code = windows_key_code;
+ key_down.dom_code = static_cast<int>(dom_code);
+ key_down.dom_key = static_cast<int>(dom_key);
+
+ // Send key down
+ rwh->ForwardKeyboardEvent(key_down);
+
+ // For most keys, also send key up
+ if (key != "Tab") { // Tab usually doesn't need key up for focus change
+ input::NativeWebKeyboardEvent key_up(
+ blink::WebInputEvent::Type::kKeyUp,
+ blink::WebInputEvent::kNoModifiers,
+ ui::EventTimeForNow());
+
+ key_up.windows_key_code = windows_key_code;
+ key_up.native_key_code = windows_key_code;
+ key_up.dom_code = static_cast<int>(dom_code);
+ key_up.dom_key = static_cast<int>(dom_key);
+
+ rwh->ForwardKeyboardEvent(key_up);
+ }
+}
+
+} // namespace api
+} // namespace extensions
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
new file mode 100644
index 0000000000000..2070807877455
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
@@ -0,0 +1,62 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_UTILS_H_
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_UTILS_H_
+
+#include <string>
+#include <unordered_map>
+
+#include "chrome/common/extensions/api/browser_os.h"
+#include "ui/accessibility/ax_node_data.h"
+#include "ui/gfx/geometry/rect_f.h"
+
+namespace content {
+class RenderWidgetHost;
+class WebContents;
+} // namespace content
+
+namespace extensions {
+namespace api {
+
+// Stores mapping information for a node
+struct NodeInfo {
+ int32_t ax_node_id;
+ gfx::RectF bounds; // Absolute bounds in CSS pixels
+};
+
+// Global node ID mappings storage
+std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>&
+GetNodeIdMappings();
+
+// Helper to create and dispatch mouse events for clicking
+void PerformClick(content::WebContents* web_contents,
+ const gfx::PointF& point);
+
+// Helper to determine if a node is interactive (clickable/typable)
+browser_os::InteractiveNodeType GetInteractiveNodeType(
+ const ui::AXNodeData& node_data);
+
+// Helper to compute branch path hash for an AX node
+uint64_t ComputeBranchPath(const ui::AXNodeData& node_data,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
+ std::unordered_map<int32_t, uint64_t>& path_cache);
+
+// Helper to get the HTML tag name from AX role
+std::string GetTagFromRole(ax::mojom::Role role);
+
+// Helper to perform scroll actions using mouse wheel events
+void PerformScroll(content::WebContents* web_contents,
+ int delta_x,
+ int delta_y,
+ bool precise = false);
+
+// Helper to send special key events
+void SendSpecialKey(content::WebContents* web_contents,
+ const std::string& key);
+
+} // namespace api
+} // namespace extensions
+
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_UTILS_H_
\ No newline at end of file
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
new file mode 100644
index 0000000000000..c84fe62d9e76d
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
@@ -0,0 +1,679 @@
+// Copyright 2024 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/browser/extensions/api/browser_os/browser_os_snapshot_processor.h"
+
+#include <algorithm>
+#include <atomic>
+#include <cctype>
+#include <functional>
+#include <future>
+#include <memory>
+#include <queue>
+#include <sstream>
+#include <unordered_set>
+#include <utility>
+
+#include "base/functional/bind.h"
+#include "base/logging.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/string_util.h"
+#include "base/task/thread_pool.h"
+#include "base/time/time.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
+#include "ui/accessibility/ax_enum_util.h"
+#include "ui/accessibility/ax_node_data.h"
+#include "ui/accessibility/ax_tree_id.h"
+#include "ui/accessibility/ax_tree_update.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/rect_conversions.h"
+#include "ui/gfx/geometry/rect_f.h"
+#include "ui/gfx/geometry/transform.h"
+
+namespace extensions {
+namespace api {
+
+// Helper to compute absolute bounds from relative bounds by walking up the tree
+// If bounds_cache is provided, it will be used to cache computed bounds
+static gfx::RectF ComputeAbsoluteBoundsFromRelative(
+ const ui::AXNodeData& node_data,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
+ std::unordered_map<int32_t, gfx::RectF>* bounds_cache = nullptr) {
+ // Check cache first if provided
+ if (bounds_cache) {
+ auto cache_it = bounds_cache->find(node_data.id);
+ if (cache_it != bounds_cache->end()) {
+ return cache_it->second;
+ }
+ }
+ // Compute absolute bounds by walking up the tree
+ gfx::RectF absolute_bounds = node_data.relative_bounds.bounds;
+ gfx::Transform accumulated_transform;
+
+ // Apply this node's transform if it has one
+ if (node_data.relative_bounds.transform) {
+ accumulated_transform = *node_data.relative_bounds.transform;
+ }
+
+ // Walk up the tree to compute absolute position
+ int32_t current_container_id = node_data.relative_bounds.offset_container_id;
+ int walk_depth = 0;
+
+ while (current_container_id >= 0 && walk_depth < 100) { // Prevent infinite loops
+ auto container_it = node_map.find(current_container_id);
+ if (container_it == node_map.end()) {
+ break;
+ }
+
+ const ui::AXNodeData& container = container_it->second;
+
+ // Offset by container's position
+ absolute_bounds.Offset(container.relative_bounds.bounds.x(),
+ container.relative_bounds.bounds.y());
+
+ // Apply container's transform if any
+ if (container.relative_bounds.transform) {
+ gfx::Transform container_transform = *container.relative_bounds.transform;
+ container_transform.PostConcat(accumulated_transform);
+ accumulated_transform = container_transform;
+ }
+
+ // Account for scroll offset if container has it
+ if (container.HasIntAttribute(ax::mojom::IntAttribute::kScrollX) ||
+ container.HasIntAttribute(ax::mojom::IntAttribute::kScrollY)) {
+ int scroll_x = container.GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
+ int scroll_y = container.GetIntAttribute(ax::mojom::IntAttribute::kScrollY);
+ absolute_bounds.Offset(-scroll_x, -scroll_y);
+ }
+
+ // Move to next container
+ current_container_id = container.relative_bounds.offset_container_id;
+ walk_depth++;
+ }
+
+ // Apply accumulated transform
+ if (!accumulated_transform.IsIdentity()) {
+ absolute_bounds = accumulated_transform.MapRect(absolute_bounds);
+ }
+
+ // Store in cache if provided
+ if (bounds_cache) {
+ (*bounds_cache)[node_data.id] = absolute_bounds;
+ }
+
+ return absolute_bounds;
+}
+
+// ProcessedNode implementation
+SnapshotProcessor::ProcessedNode::ProcessedNode()
+ : node_data(nullptr), node_id(0) {}
+
+SnapshotProcessor::ProcessedNode::ProcessedNode(const ProcessedNode&) = default;
+SnapshotProcessor::ProcessedNode::ProcessedNode(ProcessedNode&&) = default;
+SnapshotProcessor::ProcessedNode&
+SnapshotProcessor::ProcessedNode::operator=(const ProcessedNode&) = default;
+SnapshotProcessor::ProcessedNode&
+SnapshotProcessor::ProcessedNode::operator=(ProcessedNode&&) = default;
+SnapshotProcessor::ProcessedNode::~ProcessedNode() = default;
+
+
+namespace {
+
+// Check if a node should create a section (container roles)
+[[maybe_unused]]
+bool IsContainer(ax::mojom::Role role) {
+ return role == ax::mojom::Role::kMain ||
+ role == ax::mojom::Role::kArticle ||
+ role == ax::mojom::Role::kSection ||
+ role == ax::mojom::Role::kNavigation ||
+ role == ax::mojom::Role::kForm ||
+ role == ax::mojom::Role::kDialog ||
+ role == ax::mojom::Role::kSearch ||
+ role == ax::mojom::Role::kRegion ||
+ role == ax::mojom::Role::kBanner || // header
+ role == ax::mojom::Role::kContentInfo || // footer
+ role == ax::mojom::Role::kComplementary || // aside
+ role == ax::mojom::Role::kHeading ||
+ role == ax::mojom::Role::kList ||
+ role == ax::mojom::Role::kListItem || // product cards
+ role == ax::mojom::Role::kGrid ||
+ role == ax::mojom::Role::kTable;
+}
+
+// Get a readable name for the section
+[[maybe_unused]]
+std::string GetSectionName(const ui::AXNodeData& node) {
+ std::string name;
+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
+ name = node.GetStringAttribute(ax::mojom::StringAttribute::kName);
+ }
+
+ // Add role description
+ std::string role_desc;
+ switch (node.role) {
+ case ax::mojom::Role::kNavigation:
+ role_desc = "navigation";
+ break;
+ case ax::mojom::Role::kMain:
+ role_desc = "main";
+ break;
+ case ax::mojom::Role::kBanner:
+ role_desc = "header";
+ break;
+ case ax::mojom::Role::kContentInfo:
+ role_desc = "footer";
+ break;
+ case ax::mojom::Role::kForm:
+ role_desc = "form";
+ break;
+ case ax::mojom::Role::kList:
+ role_desc = "list";
+ break;
+ case ax::mojom::Role::kListItem:
+ role_desc = "listitem";
+ break;
+ case ax::mojom::Role::kHeading:
+ role_desc = "heading";
+ break;
+ default:
+ role_desc = "section"; // Generic fallback
+ }
+
+ if (!name.empty()) {
+ return name + " (" + role_desc + ")";
+ } else {
+ // Capitalize first letter
+ if (!role_desc.empty()) {
+ role_desc[0] = std::toupper(role_desc[0]);
+ }
+ return role_desc;
+ }
+}
+
+// Helper to sanitize strings to ensure valid UTF-8 by keeping only printable ASCII
+std::string SanitizeStringForOutput(const std::string& input) {
+ std::string output;
+ output.reserve(input.size());
+
+ for (char c : input) {
+ // Only include printable ASCII and whitespace
+ if ((c >= 32 && c <= 126) || c == '\t' || c == '\n') {
+ output.push_back(c);
+ } else {
+ output.push_back(' '); // Replace non-printable with space
+ }
+ }
+
+ return output;
+}
+
+// Helper to determine if a node should be skipped for the interactive snapshot
+bool ShouldSkipNode(const ui::AXNodeData& node_data) {
+ // Skip invisible or ignored nodes
+ if (node_data.IsInvisibleOrIgnored()) {
+ return true;
+ }
+
+ // Get the interactive type and skip if it's not interactive
+ browser_os::InteractiveNodeType node_type = GetInteractiveNodeType(node_data);
+ if (node_type == browser_os::InteractiveNodeType::kOther) {
+ return true;
+ }
+
+ return false;
+}
+
+} // namespace
+
+// Internal structure for managing async processing
+struct SnapshotProcessor::ProcessingContext
+ : public base::RefCountedThreadSafe<ProcessingContext> {
+ browser_os::InteractiveSnapshot snapshot;
+ std::unordered_map<int32_t, ui::AXNodeData> node_map;
+ std::unordered_map<int32_t, int32_t> parent_map; // child_id -> parent_id
+ std::unordered_map<int32_t, std::vector<int32_t>> children_map; // parent_id -> child_ids
+ int tab_id;
+ base::TimeTicks start_time;
+ size_t total_nodes;
+ size_t processed_batches;
+ size_t total_batches;
+ gfx::Rect viewport_bounds;
+ base::OnceCallback<void(SnapshotProcessingResult)> callback;
+
+ private:
+ friend class base::RefCountedThreadSafe<ProcessingContext>;
+ ~ProcessingContext() = default;
+};
+
+// Helper to collect text from a node's subtree
+std::string CollectTextFromNode(
+ int32_t node_id,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
+ int max_chars = 200) {
+
+ auto node_it = node_map.find(node_id);
+ if (node_it == node_map.end()) {
+ return "";
+ }
+
+ std::vector<std::string> text_parts;
+
+ // BFS to collect text from this node and its children
+ std::queue<int32_t> queue;
+ queue.push(node_id);
+ int chars_collected = 0;
+
+ while (!queue.empty() && chars_collected < max_chars) {
+ int32_t current_id = queue.front();
+ queue.pop();
+
+ auto current_it = node_map.find(current_id);
+ if (current_it == node_map.end()) continue;
+
+ const ui::AXNodeData& current = current_it->second;
+
+ // Collect text from this node
+ if (current.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
+ std::string text = current.GetStringAttribute(ax::mojom::StringAttribute::kName);
+ text = std::string(base::TrimWhitespaceASCII(text, base::TRIM_ALL));
+ if (!text.empty()) {
+ std::string clean_text = SanitizeStringForOutput(text);
+ if (!clean_text.empty()) {
+ text_parts.push_back(clean_text);
+ chars_collected += clean_text.length();
+ }
+ }
+ }
+
+ // Add children to queue
+ for (int32_t child_id : current.child_ids) {
+ queue.push(child_id);
+ }
+ }
+
+ std::string result = base::JoinString(text_parts, " ");
+ if (result.length() > static_cast<size_t>(max_chars)) {
+ result = result.substr(0, max_chars - 3) + "...";
+ }
+ return result;
+}
+
+// Helper to build path using offset_container_id and return depth
+std::pair<std::string, int> BuildPathAndDepth(
+ int32_t node_id,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map) {
+
+ std::vector<std::string> path_parts;
+ int32_t current_id = node_id;
+ int depth = 0;
+ const int max_depth = 10;
+
+ while (current_id >= 0 && depth < max_depth) {
+ auto node_it = node_map.find(current_id);
+ if (node_it == node_map.end()) break;
+
+ const ui::AXNodeData& node = node_it->second;
+
+ // Just append the role
+ path_parts.push_back(ui::ToString(node.role));
+
+ // Move to offset container
+ current_id = node.relative_bounds.offset_container_id;
+ depth++;
+ }
+
+ // Reverse to get top-down path
+ std::reverse(path_parts.begin(), path_parts.end());
+ return std::make_pair(base::JoinString(path_parts, " > "), depth);
+}
+
+// Helper to populate all attributes for a node
+void PopulateNodeAttributes(
+ const ui::AXNodeData& node_data,
+ std::unordered_map<std::string, std::string>& attributes) {
+
+ // Add role as string
+ attributes["role"] = ui::ToString(node_data.role);
+
+ // Add value attribute for inputs
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kValue)) {
+ std::string value = node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue);
+ attributes["value"] = SanitizeStringForOutput(value);
+ }
+
+ // Add HTML tag if available
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kHtmlTag)) {
+ attributes["html-tag"] = node_data.GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
+ }
+
+ // Add role description
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription)) {
+ std::string role_desc = node_data.GetStringAttribute(ax::mojom::StringAttribute::kRoleDescription);
+ attributes["role-description"] = SanitizeStringForOutput(role_desc);
+ }
+
+ // Add input type
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kInputType)) {
+ std::string input_type = node_data.GetStringAttribute(ax::mojom::StringAttribute::kInputType);
+ attributes["input-type"] = SanitizeStringForOutput(input_type);
+ }
+
+ // Add tooltip
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kTooltip)) {
+ std::string tooltip = node_data.GetStringAttribute(ax::mojom::StringAttribute::kTooltip);
+ attributes["tooltip"] = SanitizeStringForOutput(tooltip);
+ }
+
+ // Add placeholder for input fields
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kPlaceholder)) {
+ std::string placeholder = node_data.GetStringAttribute(ax::mojom::StringAttribute::kPlaceholder);
+ attributes["placeholder"] = SanitizeStringForOutput(placeholder);
+ }
+
+ // Add description for more context
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kDescription)) {
+ std::string description = node_data.GetStringAttribute(ax::mojom::StringAttribute::kDescription);
+ attributes["description"] = SanitizeStringForOutput(description);
+ }
+
+ // Add URL for links
+ // if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) {
+ // std::string url = node_data.GetStringAttribute(ax::mojom::StringAttribute::kUrl);
+ // attributes["url"] = SanitizeStringForOutput(url);
+ // }
+
+ // Add checked state description
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kCheckedStateDescription)) {
+ std::string checked_desc = node_data.GetStringAttribute(ax::mojom::StringAttribute::kCheckedStateDescription);
+ attributes["checked-state"] = SanitizeStringForOutput(checked_desc);
+ }
+
+ // Add autocomplete hint
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete)) {
+ std::string autocomplete = node_data.GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete);
+ attributes["autocomplete"] = SanitizeStringForOutput(autocomplete);
+ }
+
+ // Add HTML ID for form associations
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kHtmlId)) {
+ std::string html_id = node_data.GetStringAttribute(ax::mojom::StringAttribute::kHtmlId);
+ attributes["id"] = SanitizeStringForOutput(html_id);
+ }
+}
+
+// Process a batch of nodes
+std::vector<SnapshotProcessor::ProcessedNode> SnapshotProcessor::ProcessNodeBatch(
+ const std::vector<ui::AXNodeData>& nodes_to_process,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
+ uint32_t start_node_id,
+ const gfx::Rect& doc_viewport_bounds) {
+ std::vector<ProcessedNode> results;
+ results.reserve(nodes_to_process.size());
+
+ // Local caches for this batch
+ std::unordered_map<int32_t, gfx::RectF> bounds_cache;
+ std::unordered_map<int32_t, uint64_t> path_cache;
+
+ uint32_t current_node_id = start_node_id;
+
+ for (const auto& node_data : nodes_to_process) {
+ // Skip invisible, ignored, or non-interactive elements
+ if (ShouldSkipNode(node_data)) {
+ continue;
+ }
+
+ // Double-check invisibility (already done in ShouldSkipNode, but being explicit)
+ if (node_data.IsInvisibleOrIgnored()) {
+ continue;
+ }
+
+ // Get the interactive node type
+ browser_os::InteractiveNodeType node_type = GetInteractiveNodeType(node_data);
+
+ ProcessedNode data;
+ data.node_data = &node_data;
+ data.node_id = current_node_id++;
+ data.node_type = node_type;
+
+ // Get accessible name
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
+ std::string name = node_data.GetStringAttribute(ax::mojom::StringAttribute::kName);
+ data.name = SanitizeStringForOutput(name);
+ }
+
+ // Compute absolute bounds with caching
+ data.absolute_bounds = ComputeAbsoluteBoundsFromRelative(
+ node_data, node_map, &bounds_cache);
+
+ // Populate all attributes using helper function
+ PopulateNodeAttributes(node_data, data.attributes);
+
+ // Add context from parent node
+ int32_t parent_id = node_data.relative_bounds.offset_container_id;
+ if (parent_id >= 0) {
+ std::string context = CollectTextFromNode(parent_id, node_map, 200);
+ if (!context.empty()) {
+ data.attributes["context"] = context;
+ }
+ }
+
+ // Add path and depth using offset_container_id chain
+ auto [path, depth] = BuildPathAndDepth(node_data.id, node_map);
+ if (!path.empty()) {
+ data.attributes["path"] = path;
+ }
+ data.attributes["depth"] = std::to_string(depth);
+
+ // Check if node is in viewport
+ bool in_viewport = false;
+ if (!doc_viewport_bounds.IsEmpty()) {
+ // Convert absolute bounds to integer rect for intersection test
+ gfx::Rect node_rect = gfx::ToEnclosingRect(data.absolute_bounds);
+ in_viewport = doc_viewport_bounds.Intersects(node_rect);
+ }
+ data.attributes["in_viewport"] = in_viewport ? "true" : "false";
+
+ results.push_back(std::move(data));
+ }
+
+ return results;
+}
+
+// Helper to handle batch processing results
+void SnapshotProcessor::OnBatchProcessed(
+ scoped_refptr<ProcessingContext> context,
+ std::vector<ProcessedNode> batch_results) {
+ // Process batch results
+ for (const auto& node_data : batch_results) {
+ // Store mapping from our nodeId to AX node ID and bounds
+ NodeInfo info;
+ info.ax_node_id = node_data.node_data->id;
+ info.bounds = node_data.absolute_bounds;
+ GetNodeIdMappings()[context->tab_id][node_data.node_id] = info;
+
+ // Log the mapping for debugging
+ VLOG(2) << "Node ID Mapping: Interactive nodeId=" << node_data.node_id
+ << " -> AX node ID=" << info.ax_node_id
+ << " (name: " << node_data.name << ")";
+
+ // Create interactive node
+ browser_os::InteractiveNode interactive_node;
+ interactive_node.node_id = node_data.node_id;
+ interactive_node.type = node_data.node_type;
+ interactive_node.name = node_data.name;
+
+ // Set the bounding rectangle
+ browser_os::Rect rect;
+ rect.x = node_data.absolute_bounds.x();
+ rect.y = node_data.absolute_bounds.y();
+ rect.width = node_data.absolute_bounds.width();
+ rect.height = node_data.absolute_bounds.height();
+ interactive_node.rect = std::move(rect);
+
+ // Create attributes dictionary by iterating over all key-value pairs
+ if (!node_data.attributes.empty()) {
+ browser_os::InteractiveNode::Attributes attributes;
+
+ // Iterate over all attributes and add them to the dictionary
+ for (const auto& [key, value] : node_data.attributes) {
+ attributes.additional_properties.Set(key, value);
+ }
+
+ interactive_node.attributes = std::move(attributes);
+ }
+
+ context->snapshot.elements.push_back(std::move(interactive_node));
+ }
+
+ context->processed_batches++;
+
+ // Check if all batches are complete
+ if (context->processed_batches == context->total_batches) {
+ // Sort elements by node_id to maintain consistent ordering
+ std::sort(context->snapshot.elements.begin(),
+ context->snapshot.elements.end(),
+ [](const browser_os::InteractiveNode& a,
+ const browser_os::InteractiveNode& b) {
+ return a.node_id < b.node_id;
+ });
+
+ // Leave hierarchical_structure empty for now as requested
+ context->snapshot.hierarchical_structure = "";
+
+ base::TimeDelta processing_time = base::TimeTicks::Now() - context->start_time;
+ LOG(INFO) << "[PERF] Interactive snapshot processed in "
+ << processing_time.InMilliseconds() << " ms"
+ << " (nodes: " << context->snapshot.elements.size() << ")";
+
+ // Set processing time in the snapshot
+ context->snapshot.processing_time_ms = processing_time.InMilliseconds();
+
+ SnapshotProcessingResult result;
+ result.snapshot = std::move(context->snapshot);
+ result.nodes_processed = context->total_nodes;
+ result.processing_time_ms = processing_time.InMilliseconds();
+
+ // Run callback (context will be deleted when last ref is released)
+ std::move(context->callback).Run(std::move(result));
+ }
+}
+
+// Main processing function
+void SnapshotProcessor::ProcessAccessibilityTree(
+ const ui::AXTreeUpdate& tree_update,
+ int tab_id,
+ uint32_t snapshot_id,
+ const gfx::Size& viewport_size,
+ base::OnceCallback<void(SnapshotProcessingResult)> callback) {
+ base::TimeTicks start_time = base::TimeTicks::Now();
+
+
+ // Build node ID map, parent map and children map for efficient lookup
+ std::unordered_map<int32_t, ui::AXNodeData> node_map;
+ std::unordered_map<int32_t, int32_t> parent_map;
+ std::unordered_map<int32_t, std::vector<int32_t>> children_map;
+
+ for (const auto& node : tree_update.nodes) {
+ node_map[node.id] = node;
+ // Build parent and children relationships
+ for (int32_t child_id : node.child_ids) {
+ parent_map[child_id] = node.id;
+ children_map[node.id].push_back(child_id);
+ }
+ }
+
+ // Prepare processing context using RefCounted
+ auto context = base::MakeRefCounted<ProcessingContext>();
+ context->snapshot.snapshot_id = snapshot_id;
+ context->snapshot.timestamp = base::Time::Now().InMillisecondsFSinceUnixEpoch();
+ context->tab_id = tab_id;
+ context->node_map = std::move(node_map);
+ context->parent_map = std::move(parent_map);
+ context->children_map = std::move(children_map);
+ context->start_time = start_time;
+
+ // Convert viewport size to document viewport bounds
+ // Find the root node and get its scroll offset
+ gfx::Rect doc_viewport_bounds;
+ if (!viewport_size.IsEmpty() && tree_update.has_tree_data && tree_update.root_id != 0) {
+ auto root_it = node_map.find(tree_update.root_id);
+ if (root_it != node_map.end()) {
+ const ui::AXNodeData& root_node = root_it->second;
+ int scroll_x = root_node.GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
+ int scroll_y = root_node.GetIntAttribute(ax::mojom::IntAttribute::kScrollY);
+
+ // Create viewport in document coordinates
+ // Position is based on scroll offset, size is the visible viewport size
+ doc_viewport_bounds = gfx::Rect(scroll_x, scroll_y,
+ viewport_size.width(),
+ viewport_size.height());
+
+ LOG(INFO) << "Viewport size: " << viewport_size.ToString();
+ LOG(INFO) << "Root scroll offset: (" << scroll_x << ", " << scroll_y << ")";
+ LOG(INFO) << "Document viewport bounds: " << doc_viewport_bounds.ToString();
+ }
+ }
+
+ context->viewport_bounds = doc_viewport_bounds;
+ context->callback = std::move(callback);
+ context->processed_batches = 0;
+
+ // Clear previous mappings for this tab
+ GetNodeIdMappings()[tab_id].clear();
+
+ // Collect all nodes to process and filter
+ std::vector<ui::AXNodeData> nodes_to_process;
+ for (const auto& node : tree_update.nodes) {
+ // Skip invisible, ignored, or non-interactive nodes
+ if (ShouldSkipNode(node)) {
+ continue;
+ }
+ nodes_to_process.push_back(node);
+ }
+
+ context->total_nodes = nodes_to_process.size();
+
+ // Handle empty case
+ if (nodes_to_process.empty()) {
+ base::TimeDelta processing_time = base::TimeTicks::Now() - start_time;
+ context->snapshot.processing_time_ms = processing_time.InMilliseconds();
+
+ SnapshotProcessingResult result;
+ result.snapshot = std::move(context->snapshot);
+ result.nodes_processed = 0;
+ result.processing_time_ms = processing_time.InMilliseconds();
+ std::move(context->callback).Run(std::move(result));
+ return;
+ }
+
+ // Process nodes in batches using ThreadPool
+ const size_t batch_size = 100; // Process 100 nodes per batch
+ size_t num_batches = (nodes_to_process.size() + batch_size - 1) / batch_size;
+ context->total_batches = num_batches;
+
+ for (size_t i = 0; i < nodes_to_process.size(); i += batch_size) {
+ size_t end = std::min(i + batch_size, nodes_to_process.size());
+ std::vector<ui::AXNodeData> batch(
+ std::make_move_iterator(nodes_to_process.begin() + i),
+ std::make_move_iterator(nodes_to_process.begin() + end));
+ uint32_t start_node_id = i + 1; // Node IDs start at 1
+
+ // Post task to ThreadPool and handle result on UI thread
+ base::ThreadPool::PostTaskAndReplyWithResult(
+ FROM_HERE,
+ {base::TaskPriority::USER_VISIBLE},
+ base::BindOnce(&SnapshotProcessor::ProcessNodeBatch,
+ std::move(batch),
+ context->node_map,
+ start_node_id,
+ context->viewport_bounds),
+ base::BindOnce(&SnapshotProcessor::OnBatchProcessed,
+ context));
+ }
+}
+
+
+} // namespace api
+} // namespace extensions
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h
new file mode 100644
index 0000000000000..5e1114c40fe89
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h
@@ -0,0 +1,89 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_SNAPSHOT_PROCESSOR_H_
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_SNAPSHOT_PROCESSOR_H_
+
+#include <cstdint>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "base/functional/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "chrome/common/extensions/api/browser_os.h"
+#include "ui/gfx/geometry/rect_f.h"
+
+namespace ui {
+struct AXNodeData;
+struct AXTreeUpdate;
+} // namespace ui
+
+namespace extensions {
+namespace api {
+
+// Result of snapshot processing
+struct SnapshotProcessingResult {
+ browser_os::InteractiveSnapshot snapshot;
+ int nodes_processed = 0;
+ int64_t processing_time_ms = 0;
+};
+
+// Processes accessibility trees into interactive snapshots with parallel processing
+class SnapshotProcessor {
+ public:
+ // Structure to hold data for a processed node
+ struct ProcessedNode {
+ ProcessedNode();
+ ProcessedNode(const ProcessedNode&);
+ ProcessedNode(ProcessedNode&&);
+ ProcessedNode& operator=(const ProcessedNode&);
+ ProcessedNode& operator=(ProcessedNode&&);
+ ~ProcessedNode();
+
+ raw_ptr<const ui::AXNodeData> node_data;
+ uint32_t node_id;
+ browser_os::InteractiveNodeType node_type;
+ std::string name;
+ gfx::RectF absolute_bounds;
+ // All attributes stored as key-value pairs
+ std::unordered_map<std::string, std::string> attributes;
+ };
+
+ SnapshotProcessor() = default;
+ ~SnapshotProcessor() = default;
+
+ // Main processing function - handles all threading internally
+ // This function processes the accessibility tree into an interactive snapshot
+ // using parallel processing on the thread pool.
+ static void ProcessAccessibilityTree(
+ const ui::AXTreeUpdate& tree_update,
+ int tab_id,
+ uint32_t snapshot_id,
+ const gfx::Size& viewport_size,
+ base::OnceCallback<void(SnapshotProcessingResult)> callback);
+
+ // Process a batch of nodes (exposed for testing)
+ static std::vector<ProcessedNode> ProcessNodeBatch(
+ const std::vector<ui::AXNodeData>& nodes_to_process,
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
+ uint32_t start_node_id,
+ const gfx::Rect& doc_viewport_bounds);
+
+ private:
+ // Internal processing context
+ struct ProcessingContext;
+
+ // Batch processing callback
+ static void OnBatchProcessed(scoped_refptr<ProcessingContext> context,
+ std::vector<ProcessedNode> batch_results);
+
+ SnapshotProcessor(const SnapshotProcessor&) = delete;
+ SnapshotProcessor& operator=(const SnapshotProcessor&) = delete;
+};
+
+} // namespace api
+} // namespace extensions
+
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_SNAPSHOT_PROCESSOR_H_
\ No newline at end of file
diff --git a/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc b/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc
index 9c73fc6067b2f..6b3227c786686 100644
--- a/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc
+++ b/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/extensions/chrome_extensions_browser_api_provider.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_api.h"
#include "chrome/browser/extensions/api/generated_api_registration.h"
#include "extensions/browser/extension_function_registry.h"
#include "extensions/buildflags/buildflags.h"
@@ -21,6 +22,13 @@ void ChromeExtensionsBrowserAPIProvider::RegisterExtensionFunctions(
// Commands
registry->RegisterFunction<GetAllCommandsFunction>();
+ // Browser OS API
+ registry->RegisterFunction<api::BrowserOSGetAccessibilityTreeFunction>();
+ registry->RegisterFunction<api::BrowserOSGetInteractiveSnapshotFunction>();
+ registry->RegisterFunction<api::BrowserOSClickFunction>();
+ registry->RegisterFunction<api::BrowserOSInputTextFunction>();
+ registry->RegisterFunction<api::BrowserOSClearFunction>();
+
// Generated APIs from Chrome.
api::ChromeGeneratedFunctionRegistry::RegisterAll(registry);
}
diff --git a/chrome/common/extensions/api/_api_features.json b/chrome/common/extensions/api/_api_features.json
index 846a910323217..e78e1125f61cd 100644
--- a/chrome/common/extensions/api/_api_features.json
+++ b/chrome/common/extensions/api/_api_features.json
@@ -179,6 +179,34 @@
],
"contexts": ["privileged_extension"]
}],
+ "browserOS": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
+ "browserOS.getAccessibilityTree": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
+ "browserOS.getInteractiveSnapshot": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
+ "browserOS.getPageStructure": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
+ "browserOS.click": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
+ "browserOS.inputText": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
+ "browserOS.clear": {
+ "dependencies": ["permission:browserOS"],
+ "contexts": ["privileged_extension"]
+ },
"browsingData": {
"dependencies": ["permission:browsingData"],
"contexts": ["privileged_extension"]
diff --git a/chrome/common/extensions/api/_permission_features.json b/chrome/common/extensions/api/_permission_features.json
index 93ae24f9a0972..01742801dc6b5 100644
--- a/chrome/common/extensions/api/_permission_features.json
+++ b/chrome/common/extensions/api/_permission_features.json
@@ -91,6 +91,10 @@
"extension_types": ["extension", "legacy_packaged_app", "platform_app"],
"location": "component"
},
+ "browserOS": {
+ "channel": "stable",
+ "extension_types": ["extension", "platform_app"]
+ },
"browsingData": {
"channel": "stable",
"extension_types": ["extension", "legacy_packaged_app"]
diff --git a/chrome/common/extensions/api/api_sources.gni b/chrome/common/extensions/api/api_sources.gni
index cb1b525b39038..c57c8b74785f0 100644
--- a/chrome/common/extensions/api/api_sources.gni
+++ b/chrome/common/extensions/api/api_sources.gni
@@ -48,6 +48,7 @@ if (enable_extensions) {
"bookmark_manager_private.json",
"bookmarks.json",
"braille_display_private.idl",
+ "browser_os.idl",
"chrome_web_view_internal.json",
"command_line_private.json",
"content_settings.json",
diff --git a/chrome/common/extensions/api/browser_os.idl b/chrome/common/extensions/api/browser_os.idl
new file mode 100644
index 0000000000000..f088a5d1041f0
--- /dev/null
+++ b/chrome/common/extensions/api/browser_os.idl
@@ -0,0 +1,194 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// browserOS API for accessing system-level browser functionality
+namespace browserOS {
+ dictionary AccessibilityNode {
+ long id;
+ DOMString role;
+ DOMString? name;
+ DOMString? value;
+ object? attributes;
+ long[]? childIds;
+ };
+
+ dictionary AccessibilityTree {
+ // The ID of the root node
+ long rootId;
+
+ // Map of node IDs to AccessibilityNode objects
+ object nodes;
+ };
+
+ // Interactive element types
+ enum InteractiveNodeType {
+ clickable,
+ typeable,
+ selectable,
+ other
+ };
+
+ // Rectangle bounds
+ dictionary Rect {
+ double x;
+ double y;
+ double width;
+ double height;
+ };
+
+ // Interactive node in the snapshot
+ dictionary InteractiveNode {
+ long nodeId;
+ InteractiveNodeType type;
+ DOMString? name;
+ // Bounding rectangle of the node
+ Rect? rect;
+ // Flexible attributes dictionary for extensibility
+ // Can include: tag, axValue, htmlTag, role, context, path, and any future attributes
+ object? attributes;
+ };
+
+ // Snapshot of interactive elements
+ dictionary InteractiveSnapshot {
+ long snapshotId;
+ double timestamp;
+ InteractiveNode[] elements;
+ // Hierarchical text representation with context
+ DOMString? hierarchicalStructure;
+ // Performance metrics
+ long processingTimeMs;
+ };
+
+ // Options for getInteractiveSnapshot
+ dictionary InteractiveSnapshotOptions {
+ boolean? viewportOnly;
+ };
+
+ // Page load status information
+ dictionary PageLoadStatus {
+ boolean isResourcesLoading;
+ boolean isDOMContentLoaded;
+ boolean isPageComplete;
+ };
+
+
+ callback GetAccessibilityTreeCallback = void(AccessibilityTree tree);
+ callback GetInteractiveSnapshotCallback = void(InteractiveSnapshot snapshot);
+ callback ClickCallback = void();
+ callback InputTextCallback = void();
+ callback ClearCallback = void();
+ callback GetPageLoadStatusCallback = void(PageLoadStatus status);
+ callback ScrollCallback = void();
+ callback ScrollToNodeCallback = void(boolean scrolled);
+ callback SendKeysCallback = void();
+ callback CaptureScreenshotCallback = void(DOMString dataUrl);
+
+ interface Functions {
+ // Gets the full accessibility tree for a tab
+ // |tabId|: The tab to get the accessibility tree for. Defaults to active tab.
+ // |callback|: Called with the accessibility tree data.
+ static void getAccessibilityTree(
+ optional long tabId,
+ GetAccessibilityTreeCallback callback);
+
+ // Gets a snapshot of interactive elements on the page
+ // |tabId|: The tab to get the snapshot for. Defaults to active tab.
+ // |options|: Options for the snapshot.
+ // |callback|: Called with the interactive snapshot data.
+ static void getInteractiveSnapshot(
+ optional long tabId,
+ optional InteractiveSnapshotOptions options,
+ GetInteractiveSnapshotCallback callback);
+
+
+ // Clicks on an element by its nodeId from the interactive snapshot
+ // |tabId|: The tab containing the element. Defaults to active tab.
+ // |nodeId|: The nodeId from the interactive snapshot.
+ // |callback|: Called when the click is complete.
+ static void click(
+ optional long tabId,
+ long nodeId,
+ ClickCallback callback);
+
+ // Inputs text into an element by its nodeId
+ // |tabId|: The tab containing the element. Defaults to active tab.
+ // |nodeId|: The nodeId from the interactive snapshot.
+ // |text|: The text to input.
+ // |callback|: Called when the input is complete.
+ static void inputText(
+ optional long tabId,
+ long nodeId,
+ DOMString text,
+ InputTextCallback callback);
+
+ // Clears the content of an input element by its nodeId
+ // |tabId|: The tab containing the element. Defaults to active tab.
+ // |nodeId|: The nodeId from the interactive snapshot.
+ // |callback|: Called when the clear is complete.
+ static void clear(
+ optional long tabId,
+ long nodeId,
+ ClearCallback callback);
+
+ // Gets the page load status for a tab
+ // |tabId|: The tab to check. Defaults to active tab.
+ // |callback|: Called with the page load status.
+ static void getPageLoadStatus(
+ optional long tabId,
+ GetPageLoadStatusCallback callback);
+
+ // Scrolls the page up by approximately one viewport height
+ // |tabId|: The tab to scroll. Defaults to active tab.
+ // |callback|: Called when the scroll is complete.
+ static void scrollUp(
+ optional long tabId,
+ ScrollCallback callback);
+
+ // Scrolls the page down by approximately one viewport height
+ // |tabId|: The tab to scroll. Defaults to active tab.
+ // |callback|: Called when the scroll is complete.
+ static void scrollDown(
+ optional long tabId,
+ ScrollCallback callback);
+
+ // Scrolls the page to bring the specified node into view
+ // |tabId|: The tab to scroll. Defaults to active tab.
+ // |nodeId|: The node ID from getInteractiveSnapshot to scroll to.
+ // |callback|: Called with whether scrolling was needed (false if already in view).
+ static void scrollToNode(
+ optional long tabId,
+ long nodeId,
+ ScrollToNodeCallback callback);
+
+ // Sends special key events to the active element in a tab
+ // |tabId|: The tab to send keys to. Defaults to active tab.
+ // |key|: The special key to send. Supported keys:
+ // - "Enter": Submit forms, activate buttons, insert line break
+ // - "Delete": Delete character after cursor
+ // - "Backspace": Delete character before cursor
+ // - "Tab": Move focus to next element
+ // - "Escape": Cancel operations, close dialogs
+ // - "ArrowUp": Move cursor/selection up
+ // - "ArrowDown": Move cursor/selection down
+ // - "ArrowLeft": Move cursor/selection left
+ // - "ArrowRight": Move cursor/selection right
+ // - "Home": Move to beginning of line/document
+ // - "End": Move to end of line/document
+ // - "PageUp": Scroll up one page
+ // - "PageDown": Scroll down one page
+ // |callback|: Called when the key has been sent.
+ static void sendKeys(
+ optional long tabId,
+ DOMString key,
+ SendKeysCallback callback);
+
+ // Captures a screenshot of the tab as a thumbnail
+ // |tabId|: The tab to capture. Defaults to active tab.
+ // |callback|: Called with the screenshot as a data URL.
+ static void captureScreenshot(
+ optional long tabId,
+ CaptureScreenshotCallback callback);
+ };
+};
+
diff --git a/chrome/common/extensions/permissions/chrome_api_permissions.cc b/chrome/common/extensions/permissions/chrome_api_permissions.cc
index 7eba27856109e..141f5f93d7213 100644
--- a/chrome/common/extensions/permissions/chrome_api_permissions.cc
+++ b/chrome/common/extensions/permissions/chrome_api_permissions.cc
@@ -69,6 +69,7 @@ constexpr APIPermissionInfo::InitInfo permissions_to_register[] = {
{APIPermissionID::kBookmark, "bookmarks"},
{APIPermissionID::kBrailleDisplayPrivate, "brailleDisplayPrivate",
APIPermissionInfo::kFlagCannotBeOptional},
+ {APIPermissionID::kBrowserOS, "browserOS"},
{APIPermissionID::kBrowsingData, "browsingData",
APIPermissionInfo::kFlagDoesNotRequireManagedSessionFullLoginWarning},
{APIPermissionID::kCertificateProvider, "certificateProvider",
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index daced4aed4d50..880d5f4812347 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1997,6 +1997,18 @@ enum HistogramValue {
EXPERIMENTALACTOR_STARTTASK = 1934,
EXPERIMENTALACTOR_EXECUTEACTION = 1935,
EXPERIMENTALACTOR_STOPTASK = 1936,
+ BROWSER_OS_GETACCESSIBILITYTREE = 1937,
+ BROWSER_OS_GETINTERACTIVESNAPSHOT = 1938,
+ BROWSER_OS_CLICK = 1939,
+ BROWSER_OS_INPUTTEXT = 1940,
+ BROWSER_OS_CLEAR = 1941,
+ BROWSER_OS_GETPAGELOADSTATUS = 1942,
+ BROWSER_OS_SCROLLUP = 1943,
+ BROWSER_OS_SCROLLDOWN = 1944,
+ BROWSER_OS_SCROLLTONODE = 1945,
+ BROWSER_OS_SENDKEYS = 1946,
+ BROWSER_OS_GETPAGESTRUCTURE = 1947,
+ BROWSER_OS_CAPTURESCREENSHOT = 1948,
// Last entry: Add new entries above, then run:
// tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY
diff --git a/extensions/common/mojom/api_permission_id.mojom b/extensions/common/mojom/api_permission_id.mojom
index 96be0882a86cb..6e99ceae68be8 100644
--- a/extensions/common/mojom/api_permission_id.mojom
+++ b/extensions/common/mojom/api_permission_id.mojom
@@ -287,6 +287,7 @@ enum APIPermissionID {
kExperimentalAiData = 260,
kOmniboxDirectInput = 261,
kExperimentalActor = 262,
+ kBrowserOS = 263,
// Add new entries at the end of the enum and be sure to update the
// "ExtensionPermission3" enum in
diff --git a/tools/metrics/histograms/metadata/extensions/enums.xml b/tools/metrics/histograms/metadata/extensions/enums.xml
index 3eea7f9d144a7..7af6e83dedf57 100644
--- a/tools/metrics/histograms/metadata/extensions/enums.xml
+++ b/tools/metrics/histograms/metadata/extensions/enums.xml
@@ -2819,6 +2819,16 @@ Called by update_extension_histograms.py.-->
<int value="1934" label="EXPERIMENTALACTOR_STARTTASK"/>
<int value="1935" label="EXPERIMENTALACTOR_EXECUTEACTION"/>
<int value="1936" label="EXPERIMENTALACTOR_STOPTASK"/>
+ <int value="1937" label="BROWSER_OS_GETACCESSIBILITYTREE"/>
+ <int value="1938" label="BROWSER_OS_GETINTERACTIVESNAPSHOT"/>
+ <int value="1939" label="BROWSER_OS_CLICK"/>
+ <int value="1940" label="BROWSER_OS_INPUTTEXT"/>
+ <int value="1941" label="BROWSER_OS_CLEAR"/>
+ <int value="1942" label="BROWSER_OS_GETPAGELOADSTATUS"/>
+ <int value="1943" label="BROWSER_OS_SCROLLUP"/>
+ <int value="1944" label="BROWSER_OS_SCROLLDOWN"/>
+ <int value="1945" label="BROWSER_OS_SCROLLTONODE"/>
+ <int value="1946" label="BROWSER_OS_SENDKEYS"/>
</enum>
<!-- LINT.ThenChange(//extensions/browser/extension_function_histogram_value.h:HistogramValue) -->
--
2.49.0