blob: a8343d0ff3e8f748b8d4b2b145d43741921b3029 [file]
// Copyright 2025 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/devtools/protocol/browser_handler_android.h"
#include <set>
#include <vector>
#include "base/functional/bind.h"
#include "chrome/browser/android/devtools_manager_delegate_android.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "ui/base/base_window.h"
#include "ui/gfx/geometry/rect.h"
using protocol::Response;
namespace {
static constexpr char kNotImplemented[] = "Not implemented";
struct ResolvedWindowState {
std::string name;
bool is_minimized;
};
ResolvedWindowState ResolveWindowState(ui::BaseWindow* window) {
bool is_minimized = window->IsMinimized();
bool is_maximized = window->IsMaximized();
// A backgrounded fixed-size Android task is not minimized in the CDP sense.
if (is_minimized) {
ui::WindowResizePrecheckResult resize_precheck_result;
if (!window->CanResize(resize_precheck_result) &&
resize_precheck_result !=
ui::WindowResizePrecheckResult::kAndroidNoActivity) {
is_minimized = false;
is_maximized = true;
}
}
return {
BrowserHandlerAndroid::ComputeWindowStateString(
window->IsFullscreen(), is_maximized, is_minimized),
is_minimized,
};
}
// CDP window-state mutations are only observable for resizable Android tasks.
// Fixed-size tasks continue to report maximized even when backgrounded.
Response CheckCanChangeWindow(ui::BaseWindow* window, bool is_pending) {
ui::WindowResizePrecheckResult resize_precheck_result;
if (window->CanResize(resize_precheck_result) ||
(is_pending && resize_precheck_result ==
ui::WindowResizePrecheckResult::kAndroidNoActivity)) {
// A DevTools-created Android task can be addressed before its Activity is
// attached. ChromeAndroidTask queues state and bounds changes in this case.
return Response::Success();
}
return Response::ServerError(
"Window state or bounds cannot be changed in the current Android "
"configuration");
}
} // namespace
// static
ui::BaseWindow* BrowserHandlerAndroid::FindBrowserWindowById(int window_id) {
for (BrowserWindowInterface* bwi : GetAllBrowserWindowInterfaces()) {
if (bwi->GetSessionID().id() == window_id) {
return bwi->GetWindow();
}
}
return nullptr;
}
// static
std::unique_ptr<protocol::Browser::Bounds>
BrowserHandlerAndroid::BuildBrowserWindowBounds(ui::BaseWindow* window) {
ResolvedWindowState window_state = ResolveWindowState(window);
const gfx::Rect bounds = window_state.is_minimized
? window->GetRestoredBounds()
: window->GetBounds();
return protocol::Browser::Bounds::Create()
.SetLeft(bounds.x())
.SetTop(bounds.y())
.SetWidth(bounds.width())
.SetHeight(bounds.height())
.SetWindowState(window_state.name)
.Build();
}
BrowserHandlerAndroid::BrowserHandlerAndroid(
protocol::UberDispatcher* dispatcher,
const std::string& target_id)
: target_id_(target_id) {
CHECK(dispatcher);
protocol::Browser::Dispatcher::wire(dispatcher, this);
}
BrowserHandlerAndroid::~BrowserHandlerAndroid() = default;
void BrowserHandlerAndroid::TrackBrowserWindow(
BrowserWindowInterface* browser_window) {
CHECK(browser_window);
const SessionID& window_id = browser_window->GetSessionID();
CHECK(window_id.is_valid());
auto tracked_window = tracked_browser_windows_.find(window_id.id());
if (tracked_window != tracked_browser_windows_.end() &&
tracked_window->second != browser_window) {
window_close_subscriptions_.erase(tracked_window->second);
}
tracked_browser_windows_.insert_or_assign(window_id.id(), browser_window);
if (!window_close_subscriptions_.contains(browser_window)) {
window_close_subscriptions_.insert_or_assign(
browser_window,
browser_window->RegisterBrowserDidClose(
base::BindRepeating(&BrowserHandlerAndroid::OnBrowserWindowClosed,
base::Unretained(this))));
}
}
Response BrowserHandlerAndroid::BuildBoundsForWindowId(
int window_id,
std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
auto [window, _] = FindWindowById(window_id);
if (!window) {
return Response::ServerError("Browser window not found");
}
*out_bounds = BuildBrowserWindowBounds(window);
return Response::Success();
}
BrowserHandlerAndroid::WindowLookupResult BrowserHandlerAndroid::FindWindowById(
int window_id) {
if (ui::BaseWindow* window = FindBrowserWindowById(window_id)) {
auto tracked_window = tracked_browser_windows_.find(window_id);
if (tracked_window != tracked_browser_windows_.end()) {
BrowserWindowInterface* browser_window = tracked_window->second;
tracked_browser_windows_.erase(tracked_window);
window_close_subscriptions_.erase(browser_window);
}
return {window, false};
}
auto tracked_window = tracked_browser_windows_.find(window_id);
if (tracked_window != tracked_browser_windows_.end()) {
return {tracked_window->second->GetWindow(), true};
}
return {nullptr, false};
}
void BrowserHandlerAndroid::OnBrowserWindowClosed(
BrowserWindowInterface* browser_window) {
auto tracked_window =
tracked_browser_windows_.find(browser_window->GetSessionID().id());
if (tracked_window != tracked_browser_windows_.end() &&
tracked_window->second == browser_window) {
tracked_browser_windows_.erase(tracked_window);
}
window_close_subscriptions_.erase(browser_window);
}
// static
std::optional<int> BrowserHandlerAndroid::ResolveWindowIdForWebContents(
content::WebContents* web_contents) {
// Prefer the tab's own window id (covers pending ids).
TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
if (tab) {
SessionID wid = tab->GetWindowId();
if (wid.is_valid()) {
return wid.id();
}
}
// Fallback: use the owning TabModel's session id.
for (TabModel* model : TabModelList::models()) {
for (int i = 0; i < model->GetTabCount(); ++i) {
TabAndroid* model_tab = model->GetTabAt(i);
if (model_tab && model_tab->web_contents() == web_contents) {
return model->GetSessionId().id();
}
}
}
return std::nullopt;
}
// static
std::string BrowserHandlerAndroid::ComputeWindowStateString(bool is_fullscreen,
bool is_maximized,
bool is_minimized) {
if (is_fullscreen) {
return protocol::Browser::WindowStateEnum::Fullscreen;
}
if (is_maximized) {
return protocol::Browser::WindowStateEnum::Maximized;
}
if (is_minimized) {
return protocol::Browser::WindowStateEnum::Minimized;
}
return protocol::Browser::WindowStateEnum::Normal;
}
Response BrowserHandlerAndroid::GetWindowForTarget(
std::optional<std::string> target_id,
int* out_window_id,
std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
auto host =
content::DevToolsAgentHost::GetForId(target_id.value_or(target_id_));
if (!host) {
return Response::ServerError("No matching target");
}
content::WebContents* web_contents = host->GetWebContents();
if (!web_contents) {
return Response::ServerError("No web contents in the target");
}
std::optional<int> window_id = ResolveWindowIdForWebContents(web_contents);
if (!window_id.has_value()) {
return Response::ServerError("Browser window not found");
}
*out_window_id = window_id.value();
return BuildBoundsForWindowId(window_id.value(), out_bounds);
}
Response BrowserHandlerAndroid::GetWindowBounds(
int window_id,
std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
return BuildBoundsForWindowId(window_id, out_bounds);
}
Response BrowserHandlerAndroid::Close() {
return Response::MethodNotFound(kNotImplemented);
}
Response BrowserHandlerAndroid::SetWindowBounds(
int window_id,
std::unique_ptr<protocol::Browser::Bounds> window_bounds) {
auto [window, is_pending] = FindWindowById(window_id);
if (!window) {
return Response::ServerError("Browser window not found");
}
gfx::Rect bounds = window->GetBounds();
const bool set_bounds = window_bounds->HasLeft() || window_bounds->HasTop() ||
window_bounds->HasWidth() ||
window_bounds->HasHeight();
if (set_bounds) {
bounds.set_x(window_bounds->GetLeft(bounds.x()));
bounds.set_y(window_bounds->GetTop(bounds.y()));
bounds.set_width(window_bounds->GetWidth(bounds.width()));
bounds.set_height(window_bounds->GetHeight(bounds.height()));
}
const std::string window_state =
window_bounds->GetWindowState(protocol::Browser::WindowStateEnum::Normal);
if (set_bounds &&
window_state != protocol::Browser::WindowStateEnum::Normal) {
return Response::InvalidParams(
"The 'minimized', 'maximized' and 'fullscreen' states cannot be "
"combined with 'left', 'top', 'width' or 'height'");
}
if (window_state == protocol::Browser::WindowStateEnum::Fullscreen) {
return Response::ServerError("Fullscreen not supported on Android");
}
const std::string current_state = ResolveWindowState(window).name;
if (window_state == protocol::Browser::WindowStateEnum::Maximized) {
if (current_state == protocol::Browser::WindowStateEnum::Maximized) {
return Response::Success();
}
if (current_state == protocol::Browser::WindowStateEnum::Minimized ||
current_state == protocol::Browser::WindowStateEnum::Fullscreen) {
return Response::ServerError(
"To maximize a minimized or fullscreen "
"window, restore it to normal state first.");
}
Response can_change = CheckCanChangeWindow(window, is_pending);
if (!can_change.IsSuccess()) {
return can_change;
}
window->Maximize();
return Response::Success();
}
if (window_state == protocol::Browser::WindowStateEnum::Minimized) {
if (current_state == protocol::Browser::WindowStateEnum::Minimized) {
return Response::Success();
}
if (current_state == protocol::Browser::WindowStateEnum::Fullscreen) {
return Response::ServerError(
"To minimize a fullscreen window, restore it to normal "
"state first.");
}
Response can_change = CheckCanChangeWindow(window, is_pending);
if (!can_change.IsSuccess()) {
return can_change;
}
window->Minimize();
return Response::Success();
}
if (window_state != protocol::Browser::WindowStateEnum::Normal) {
return Response::InvalidParams("Invalid windowState: " + window_state);
}
if (current_state == protocol::Browser::WindowStateEnum::Fullscreen) {
return Response::ServerError("Cannot exit fullscreen on Android");
}
if (set_bounds) {
Response can_change = CheckCanChangeWindow(window, is_pending);
if (!can_change.IsSuccess()) {
return can_change;
}
// Android's task implementation handles restoring a maximized/minimized
// freeform window as part of applying explicit bounds.
window->SetBounds(bounds);
} else if (current_state == protocol::Browser::WindowStateEnum::Minimized ||
current_state == protocol::Browser::WindowStateEnum::Maximized) {
Response can_change = CheckCanChangeWindow(window, is_pending);
if (!can_change.IsSuccess()) {
return can_change;
}
window->Restore();
}
return Response::Success();
}
Response BrowserHandlerAndroid::SetContentsSize(int window_id,
std::optional<int> width,
std::optional<int> height) {
return Response::MethodNotFound(kNotImplemented);
}
protocol::Response BrowserHandlerAndroid::SetDockTile(
std::optional<std::string> label,
std::optional<protocol::Binary> image) {
return Response::MethodNotFound(kNotImplemented);
}
protocol::Response BrowserHandlerAndroid::ExecuteBrowserCommand(
const protocol::Browser::BrowserCommandId& command_id) {
return Response::MethodNotFound(kNotImplemented);
}
protocol::Response BrowserHandlerAndroid::AddPrivacySandboxEnrollmentOverride(
const std::string& in_url) {
return Response::MethodNotFound(kNotImplemented);
}