blob: c83841120bbd4fc8b38594866bcc116e54d96fbd [file] [log] [blame]
// 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/contextual_tasks/contextual_tasks_ui_service.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/uuid.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_context_controller.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_side_panel_coordinator.h"
#include "chrome/browser/contextual_tasks/contextual_tasks_ui.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/webui_url_constants.h"
#include "components/contextual_tasks/public/contextual_task.h"
#include "components/contextual_tasks/public/features.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "net/base/url_util.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
using sessions::SessionTabHelper;
namespace contextual_tasks {
namespace {
constexpr char kAiPageHost[] = "https://google.com";
constexpr char kTaskQueryParam[] = "task";
bool IsContextualTasksHost(const GURL& url) {
return url.scheme() == content::kChromeUIScheme &&
url.host() == chrome::kChromeUIContextualTasksHost;
}
GURL AppendCommonUrlParams(GURL url) {
url = net::AppendQueryParameter(url, "gsc", "2");
// TODO(crbug.com/454388385): Remove this param once authentication flow is
// implemented.
url = net::AppendQueryParameter(url, "gl", "us");
return url;
}
bool IsSignInDomain(const GURL& url) {
if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
return false;
}
std::vector<std::string> sign_in_domains = GetContextualTasksSignInDomains();
for (const auto& sign_in_domain : sign_in_domains) {
if (url.host() == sign_in_domain) {
return true;
}
}
return false;
}
// Gets the contextual task Id from a contextual task host URL.
base::Uuid GetTaskIdFromHostURL(const GURL& url) {
std::string task_id;
net::GetValueForKeyInQuery(url, kTaskQueryParam, &task_id);
return base::Uuid::ParseLowercase(task_id);
}
} // namespace
ContextualTasksUiService::ContextualTasksUiService(
Profile* profile,
ContextualTasksContextController* context_controller)
: profile_(profile), context_controller_(context_controller) {
ai_page_host_ = GURL(kAiPageHost);
}
ContextualTasksUiService::~ContextualTasksUiService() = default;
void ContextualTasksUiService::OnNavigationToAiPageIntercepted(
const GURL& url,
base::WeakPtr<tabs::TabInterface> tab,
bool is_to_new_tab) {
CHECK(context_controller_);
// Create a task for the URL that was just intercepted.
ContextualTask task = context_controller_->CreateTaskFromUrl(url);
// Map the task ID to the a new URL that uses the base AI page URL with the
// query from the one that was intercepted. This is done so the UI knows
// which URL to load initially in the embedded frame.
std::string query;
net::GetValueForKeyInQuery(url, "q", &query);
GURL stripped_query_url = GetDefaultAiPageUrl();
if (!query.empty()) {
stripped_query_url =
net::AppendQueryParameter(stripped_query_url, "q", query);
}
task_id_to_creation_url_[task.GetTaskId()] = stripped_query_url;
GURL ui_url(chrome::kChromeUIContextualTasksURL);
ui_url = net::AppendQueryParameter(ui_url, kTaskQueryParam,
task.GetTaskId().AsLowercaseString());
content::WebContents* contextual_task_web_contents = nullptr;
if (!is_to_new_tab) {
tab->GetContents()->GetController().LoadURLWithParams(
content::NavigationController::LoadURLParams(ui_url));
contextual_task_web_contents = tab->GetContents();
} else {
NavigateParams params(profile_, ui_url, ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
Navigate(&params);
contextual_task_web_contents = params.navigated_or_inserted_contents;
}
// Attach the session Id of the ai page to the task.
if (contextual_task_web_contents) {
AssociateWebContentsToTask(contextual_task_web_contents, task.GetTaskId());
}
}
void ContextualTasksUiService::OnThreadLinkClicked(
const GURL& url,
base::Uuid task_id,
base::WeakPtr<tabs::TabInterface> tab,
base::WeakPtr<BrowserWindowInterface> browser) {
if (!browser) {
return;
}
TabStripModel* tab_strip_model = browser->GetTabStripModel();
std::unique_ptr<content::WebContents> new_contents =
content::WebContents::Create(
content::WebContents::CreateParams(profile_));
content::WebContents* new_contents_ptr = new_contents.get();
new_contents->GetController().LoadURLWithParams(
content::NavigationController::LoadURLParams(url));
// If the source contents is the panel, open the AI page in a new foreground
// tab.
// TODO(crbug.com/458139141): Split this API so we can assume `tab` non-null.
if (!tab) {
// Creates the Tab so session ID is created for the WebContents.
auto tab_to_insert = std::make_unique<tabs::TabModel>(
std::move(new_contents), tab_strip_model);
if (task_id.is_valid()) {
AssociateWebContentsToTask(new_contents_ptr, task_id);
}
// Insert the WebContents after the current active tab.
int active_tab_index = tab_strip_model->active_index();
tab_strip_model->AddTab(std::move(tab_to_insert), active_tab_index + 1,
ui::PAGE_TRANSITION_LINK, AddTabTypes::ADD_ACTIVE);
return;
}
if (tab->GetContents() &&
IsContextualTasksHost(tab->GetContents()->GetLastCommittedURL())) {
content::WebUI* webui = tab->GetContents()->GetWebUI();
if (webui && webui->GetController()) {
webui->GetController()
->GetAs<ContextualTasksUI>()
->OnSidePanelStateChanged();
}
}
// Get the index of the web contents.
const int current_index = tab_strip_model->GetIndexOfTab(tab.get());
// Open the linked page in a tab directly after this one.
tab_strip_model->InsertWebContentsAt(
current_index + 1, std::move(new_contents), AddTabTypes::ADD_ACTIVE);
// Detach the WebContents from tab.
std::unique_ptr<content::WebContents> contextual_task_contents =
tab_strip_model->DetachWebContentsAtForInsertion(
current_index,
TabStripModelChange::RemoveReason::kInsertedIntoSidePanel);
CHECK(new_contents_ptr == tab_strip_model->GetActiveWebContents());
AssociateWebContentsToTask(new_contents_ptr, task_id);
// Transfer the contextual task contents into the side panel cache.
ContextualTasksSidePanelCoordinator::From(browser.get())
->TransferWebContentsFromTab(task_id,
std::move(contextual_task_contents));
// Open the side panel.
// TODO: This currently should be passed the bounds of the
// contents_container_view from BrowserView, though the view is not accessible
// from here. This API could be changed to simply accept the web_contents.
ContextualTasksSidePanelCoordinator::From(browser.get())->Show();
}
bool ContextualTasksUiService::HandleNavigation(
const GURL& navigation_url,
bool initiated_in_page,
content::WebContents* source_contents,
bool is_to_new_tab) {
// Allow any navigation to the contextual tasks host.
if (IsContextualTasksHost(navigation_url)) {
return false;
}
bool is_nav_to_ai = IsAiUrl(navigation_url);
bool is_nav_to_sign_in = IsSignInDomain(navigation_url);
// Try to get the active tab if there is one. This will be null if the link is
// originating from the side panel.
tabs::TabInterface* tab = nullptr;
BrowserWindowInterface* browser = nullptr;
if (source_contents) {
tab = tabs::TabInterface::MaybeGetFromContents(source_contents);
BrowserWindow* window =
BrowserWindow::FindBrowserWindowWithWebContents(source_contents);
if (window) {
browser = window->AsBrowserView()->browser();
}
}
// Intercept any navigation where the wrapping WebContents is the WebUI host
// unless it is the embedded page.
if (IsContextualTasksHost(source_contents->GetLastCommittedURL())) {
if (is_nav_to_ai || !initiated_in_page) {
return false;
}
// Allow users to sign in within the <webview>.
// TODO(crbug.com/454388385): Remove this once the authentication flow is
// implemented.
if (is_nav_to_sign_in) {
return false;
}
base::Uuid task_id;
if (source_contents) {
task_id = GetTaskIdFromHostURL(source_contents->GetLastCommittedURL());
}
// This needs to be posted in case the called method triggers a navigation
// in the same WebContents, invalidating the nav handle used up the chain.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ContextualTasksUiService::OnThreadLinkClicked,
weak_ptr_factory_.GetWeakPtr(), navigation_url, task_id,
tab ? tab->GetWeakPtr() : nullptr,
browser ? browser->GetWeakPtr() : nullptr));
return true;
}
// Navigations to the AI URL in the topmost frame should always be
// intercepted.
if (is_nav_to_ai) {
// This needs to be posted in case the called method triggers a navigation
// in the same WebContents, invalidating the nav handle used up the chain.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&ContextualTasksUiService::OnNavigationToAiPageIntercepted,
weak_ptr_factory_.GetWeakPtr(), navigation_url,
tab ? tab->GetWeakPtr() : nullptr, is_to_new_tab));
return true;
}
// Allow anything else.
return false;
}
GURL ContextualTasksUiService::GetInitialUrlForTask(const base::Uuid& uuid) {
auto it = task_id_to_creation_url_.find(uuid);
if (it != task_id_to_creation_url_.end()) {
return it->second;
}
return GURL();
}
GURL ContextualTasksUiService::GetDefaultAiPageUrl() {
return AppendCommonUrlParams(GURL(GetContextualTasksAiPageUrl()));
}
void ContextualTasksUiService::OnTaskChangedInPanel(
BrowserWindowInterface* browser_window_interface,
const base::Uuid& task_id) {
// If a new thread is started in the panel, affiliated tabs should change
// their thread to the new one.
base::Uuid new_task_id = task_id;
if (!task_id.is_valid()) {
// If the panel is in zero state, create an empty task.
ContextualTask task = context_controller_->CreateTask();
new_task_id = task.GetTaskId();
}
TabStripModel* tab_strip_model = browser_window_interface->GetTabStripModel();
content::WebContents* active_contents =
tab_strip_model->GetActiveWebContents();
SessionID active_id = SessionTabHelper::IdForTab(active_contents);
if (kTaskScopedSidpePanel.Get()) {
// If the current tab is associated with any task, change associations for
// all tabs associated with that task.
std::optional<ContextualTask> current_task =
context_controller_->GetContextualTaskForTab(active_id);
if (current_task) {
std::vector<SessionID> tab_ids =
context_controller_->GetTabsAssociatedWithTask(
current_task->GetTaskId());
for (const auto& id : tab_ids) {
context_controller_->AssociateTabWithTask(new_task_id, id);
}
return;
}
}
context_controller_->AssociateTabWithTask(new_task_id, active_id);
}
void ContextualTasksUiService::MoveTaskUiToToNewTab(
const base::Uuid& task_id,
BrowserWindowInterface* browser) {
auto* coordinator =
contextual_tasks::ContextualTasksSidePanelCoordinator::From(browser);
CHECK(coordinator);
std::unique_ptr<content::WebContents> web_contents =
coordinator->DetachWebContentsForTask(task_id);
if (!web_contents) {
return;
}
NavigateParams params(browser, std::move(web_contents));
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
params.transition = ui::PAGE_TRANSITION_LINK;
Navigate(&params);
coordinator->Close();
}
bool ContextualTasksUiService::IsAiUrl(const GURL& url) {
if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS() ||
!base::EndsWith(url.host(), ai_page_host_.host())) {
return false;
}
if (!base::StartsWith(url.path(), "/search")) {
return false;
}
// AI pages are identified by the "udm" URL param having a value of 50.
std::string udm_value;
if (!net::GetValueForKeyInQuery(url, "udm", &udm_value)) {
return false;
}
return udm_value == "50";
}
void ContextualTasksUiService::AssociateWebContentsToTask(
content::WebContents* web_contents,
const base::Uuid& task_id) {
SessionID session_id = SessionTabHelper::IdForTab(web_contents);
if (session_id.is_valid()) {
context_controller_->AssociateTabWithTask(task_id, session_id);
}
}
} // namespace contextual_tasks