| // 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/target_handler_android.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "chrome/browser/android/devtools_manager_delegate_android.h" |
| #include "chrome/browser/android/tab_android.h" |
| #include "chrome/browser/devtools/devtools_browser_context_manager.h" |
| #include "chrome/browser/devtools/protocol/browser_handler_android.h" |
| #include "chrome/browser/profiles/profile.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/create_browser_window.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "content/public/browser/devtools_agent_host.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/common/url_utils.h" |
| #include "ui/base/page_transition_types.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| |
| using content::WebContents; |
| |
| TargetHandlerAndroid::TargetHandlerAndroid( |
| protocol::UberDispatcher* dispatcher, |
| bool is_trusted, |
| bool may_read_local_files, |
| BrowserHandlerAndroid* browser_handler) |
| : is_trusted_(is_trusted), |
| may_read_local_files_(may_read_local_files), |
| browser_handler_(browser_handler) { |
| protocol::Target::Dispatcher::wire(dispatcher, this); |
| } |
| |
| TargetHandlerAndroid::~TargetHandlerAndroid() = default; |
| |
| protocol::Response TargetHandlerAndroid::SetRemoteLocations( |
| std::unique_ptr<protocol::Array<protocol::Target::RemoteLocation>> |
| locations) { |
| remote_locations_.clear(); |
| if (!locations) { |
| return protocol::Response::Success(); |
| } |
| |
| for (const auto& location : *locations) { |
| remote_locations_.insert( |
| net::HostPortPair(location->GetHost(), location->GetPort())); |
| } |
| |
| return protocol::Response::Success(); |
| } |
| |
| protocol::Response TargetHandlerAndroid::CreateTarget( |
| const std::string& url, |
| std::optional<int> left, |
| std::optional<int> top, |
| std::optional<int> width, |
| std::optional<int> height, |
| std::optional<std::string> window_state, |
| std::optional<std::string> browser_context_id, |
| std::optional<bool> enable_begin_frame_control, |
| std::optional<bool> new_window, |
| std::optional<bool> background, |
| std::optional<bool> for_tab, |
| std::optional<bool> hidden, |
| std::optional<bool> focus, |
| std::string* out_target_id) { |
| if (hidden.value_or(false)) { |
| return protocol::Response::FallThrough(); |
| } |
| |
| GURL gurl(url); |
| if (gurl.is_empty()) { |
| gurl = GURL(url::kAboutBlankURL); |
| } |
| |
| const TabModelList::TabModelVector& models = TabModelList::models(); |
| if (models.empty()) { |
| return protocol::Response::ServerError("Could not find TabModelList"); |
| } |
| |
| TabModel* tab_model = models[0]; |
| CHECK(tab_model); |
| |
| GURL inner_url = gurl; |
| if (gurl.SchemeIs(content::kViewSourceScheme)) { |
| inner_url = GURL(gurl.GetContent()); |
| } |
| |
| if (!is_trusted_ && (inner_url.SchemeIs(content::kChromeUIUntrustedScheme) || |
| inner_url.SchemeIs(content::kChromeDevToolsScheme))) { |
| return protocol::Response::ServerError( |
| "Navigating to a URL with a privileged scheme is not allowed"); |
| } |
| |
| if (!may_read_local_files_ && inner_url.SchemeIsFile()) { |
| return protocol::Response::ServerError( |
| "Creating a target with a local URL is not allowed"); |
| } |
| |
| Profile* const default_profile = tab_model->GetProfile(); |
| CHECK(default_profile); |
| |
| Profile* profile = default_profile; |
| if (browser_context_id.has_value()) { |
| profile = DevToolsBrowserContextManager::GetInstance().GetProfileById( |
| *browser_context_id); |
| if (!profile) { |
| return protocol::Response::ServerError( |
| "Failed to find browser context with id " + *browser_context_id); |
| } |
| } |
| |
| std::unique_ptr<WebContents> owned_web_contents = |
| WebContents::Create(WebContents::CreateParams(profile)); |
| if (!owned_web_contents) { |
| return protocol::Response::ServerError("Could not create a Tab"); |
| } |
| WebContents* web_contents = owned_web_contents.get(); |
| DevToolsManagerDelegateAndroid::MarkCreatedByDevTools(*web_contents); |
| |
| if (new_window.value_or(false) && profile == default_profile) { |
| BrowserWindowCreateParams create_params(*profile, |
| /*from_user_gesture=*/false); |
| create_params.web_contents = std::move(owned_web_contents); |
| |
| // Browser-level auto-attach can expose the detached tab before its Activity |
| // is registered. The synchronous overload provides the stable session id |
| // and predicted window state documented for pending windows; navigation |
| // continues through the supplied WebContents rather than BWI::OpenURL(). |
| BrowserWindowInterface* browser_window = |
| CreateBrowserWindow(std::move(create_params)); |
| if (browser_window) { |
| if (browser_handler_) { |
| // Pending Android windows are intentionally absent from the global |
| // browser-window iterator. Track the BWI returned to this DevTools |
| // session so Browser commands can address it while the Activity starts. |
| browser_handler_->TrackBrowserWindow(browser_window); |
| } |
| |
| TabAndroid* tab = TabAndroid::FromWebContents(web_contents); |
| CHECK(tab); |
| tab->SetWindowSessionID(browser_window->GetSessionID()); |
| |
| content::NavigationController::LoadURLParams load_params(gurl); |
| load_params.transition_type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL; |
| web_contents->GetController().LoadURLWithParams(load_params); |
| } else { |
| // Preserve the established target-creation behavior when Android cannot |
| // create a separate browser window. The failed call destroyed its |
| // supplied WebContents, so the fallback must create a fresh tab. |
| web_contents = |
| tab_model->CreateNewTabForDevTools(gurl, /*new_window=*/true); |
| if (!web_contents) { |
| return protocol::Response::ServerError("Could not create a Tab"); |
| } |
| DevToolsManagerDelegateAndroid::MarkCreatedByDevTools(*web_contents); |
| } |
| } else { |
| // Insert the pre-created WebContents so default-profile and explicit- |
| // context targets use the same tab-model path. Keeping disposable profiles |
| // in the current model also avoids racing context disposal with a pending |
| // Android Activity. |
| tabs::TabInterface* new_tab = tab_model->CreateTab( |
| /*parent=*/nullptr, std::move(owned_web_contents), |
| TabModel::kInvalidIndex, TabModel::TabLaunchType::FROM_CHROME_UI, |
| /*should_pin=*/false); |
| if (!new_tab || !new_tab->GetContents()) { |
| return protocol::Response::ServerError("Could not create a Tab"); |
| } |
| web_contents = new_tab->GetContents(); |
| content::NavigationController::LoadURLParams load_params(gurl); |
| web_contents->GetController().LoadURLWithParams(load_params); |
| } |
| |
| if (for_tab.value_or(false)) { |
| *out_target_id = |
| content::DevToolsAgentHost::GetOrCreateForTab(web_contents)->GetId(); |
| } else { |
| *out_target_id = |
| content::DevToolsAgentHost::GetOrCreateFor(web_contents)->GetId(); |
| } |
| |
| return protocol::Response::Success(); |
| } |