| // 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/active_task_context_provider_impl.h" |
| |
| #include <map> |
| |
| #include "base/feature_list.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/contextual_search/contextual_search_web_contents_helper.h" |
| #include "chrome/browser/contextual_tasks/active_task_context_provider.h" |
| #include "chrome/browser/contextual_tasks/contextual_tasks_panel_controller.h" |
| #include "chrome/browser/tab_list/tab_list_interface.h" |
| #include "chrome/browser/tab_list/tab_list_interface_observer.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "components/contextual_search/contextual_search_session_handle.h" |
| #include "components/contextual_tasks/public/context_decoration_params.h" |
| #include "components/contextual_tasks/public/contextual_task.h" |
| #include "components/contextual_tasks/public/contextual_task_context.h" |
| #include "components/contextual_tasks/public/features.h" |
| #include "components/omnibox/common/composebox_features.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "content/public/browser/page.h" |
| #include "content/public/common/url_constants.h" |
| |
| namespace contextual_tasks { |
| |
| namespace { |
| |
| std::set<tabs::TabHandle> GetTabsFromContext( |
| const ContextualTaskContext& context, |
| BrowserWindowInterface* browser_window, |
| contextual_search::ContextualSearchSessionHandle* session_handle) { |
| std::set<tabs::TabHandle> tabs; |
| |
| // Map SessionID to its GURL and title. |
| std::map<SessionID, std::pair<GURL, std::u16string>> |
| context_session_ids_url_map; |
| // Add the tabs from context if they exist in the current browser window. |
| for (const auto& attachment : context.GetUrlAttachments()) { |
| SessionID id = attachment.GetTabSessionId(); |
| if (id.is_valid()) { |
| context_session_ids_url_map[id] = |
| std::make_pair(attachment.GetURL(), attachment.GetTitle()); |
| } |
| } |
| |
| if (context_session_ids_url_map.empty()) { |
| return tabs; |
| } |
| |
| TabListInterface* tab_list = TabListInterface::From(browser_window); |
| if (!tab_list) { |
| return tabs; |
| } |
| |
| for (int i = 0; i < tab_list->GetTabCount(); ++i) { |
| tabs::TabInterface* tab = tab_list->GetTab(i); |
| content::WebContents* web_contents = tab ? tab->GetContents() : nullptr; |
| if (!web_contents) { |
| continue; |
| } |
| SessionID tab_id = sessions::SessionTabHelper::IdForTab(web_contents); |
| auto matching_attachment = context_session_ids_url_map.find(tab_id); |
| if (matching_attachment != context_session_ids_url_map.end()) { |
| // Verify if the tab's current URL is equivalent to the context URL. |
| // If not, the tab has navigated away and is removed from context. |
| if (omnibox::IsTabDeselectionInComposeboxEnabled()) { |
| bool is_equivalent = |
| session_handle |
| ? session_handle->AreUrlsEquivalent( |
| matching_attachment->second.first, |
| base::UTF16ToUTF8(matching_attachment->second.second), |
| web_contents->GetLastCommittedURL(), |
| base::UTF16ToUTF8(tab->GetTitle())) |
| : (matching_attachment->second.first == |
| web_contents->GetLastCommittedURL()); |
| if (!is_equivalent) { |
| continue; |
| } |
| } |
| |
| if (session_handle && session_handle->IsTabDeselected( |
| tab_id, web_contents->GetLastCommittedURL(), |
| base::UTF16ToUTF8(tab->GetTitle()))) { |
| continue; |
| } |
| tabs.insert(tab->GetHandle()); |
| } |
| } |
| |
| return tabs; |
| } |
| |
| } // namespace |
| |
| // static |
| ActiveTaskContextProvider* ActiveTaskContextProvider::From( |
| BrowserWindowInterface* window) { |
| return Get(window->GetUnownedUserDataHost()); |
| } |
| |
| DEFINE_USER_DATA(ActiveTaskContextProvider); |
| |
| ActiveTaskContextProviderImpl::ActiveTaskContextProviderImpl( |
| BrowserWindowInterface* browser_window, |
| ContextualTasksService* contextual_tasks_service) |
| : browser_window_(browser_window), |
| contextual_tasks_service_(contextual_tasks_service), |
| scoped_unowned_user_data_(browser_window->GetUnownedUserDataHost(), |
| *this) { |
| CHECK(contextual_tasks_service_); |
| } |
| |
| void ActiveTaskContextProviderImpl::SetContextualTasksPanelController( |
| ContextualTasksPanelController* contextual_tasks_panel_controller) { |
| contextual_tasks_panel_controller_ = contextual_tasks_panel_controller; |
| |
| if (contextual_tasks_panel_controller_) { |
| // Start observing the tab strip and ContextualTasksService. |
| contextual_tasks_service_observation_.Observe(contextual_tasks_service_); |
| auto* tab_list_interface = TabListInterface::From(browser_window_); |
| if (tab_list_interface) { |
| tab_list_interface->AddTabListInterfaceObserver(this); |
| // Observe the active tab's WebContents on startup. |
| OnActiveTabChanged(*tab_list_interface, |
| tab_list_interface->GetActiveTab()); |
| } |
| } |
| } |
| |
| // Local tab underlines are manual tab strip underlines that are explicitly |
| // requested by clients (e.g., NTP realbox or omnibox popup searchbox handlers) |
| // before a backend task has been created or is active. |
| void ActiveTaskContextProviderImpl::AddLocalTabUnderline( |
| tabs::TabHandle tab_handle) { |
| auto* tab_list = TabListInterface::From(browser_window_); |
| auto* active_tab = tab_list ? tab_list->GetActiveTab() : nullptr; |
| tabs::TabHandle owner_handle = |
| active_tab ? active_tab->GetHandle() : tabs::TabHandle::Null(); |
| local_tab_underlines_[owner_handle].insert(tab_handle); |
| NotifyObservers(); |
| } |
| |
| void ActiveTaskContextProviderImpl::RemoveLocalTabUnderline( |
| tabs::TabHandle tab_handle) { |
| for (auto& [owner_handle, underlines] : local_tab_underlines_) { |
| underlines.erase(tab_handle); |
| } |
| NotifyObservers(); |
| } |
| |
| void ActiveTaskContextProviderImpl::ClearAllLocalTabUnderlines() { |
| local_tab_underlines_.clear(); |
| NotifyObservers(); |
| } |
| |
| ActiveTaskContextProviderImpl::~ActiveTaskContextProviderImpl() { |
| for (auto& observer : observers_) { |
| observer.OnActiveTaskContextProviderDestroyed(); |
| } |
| if (auto* tab_list_interface = TabListInterface::From(browser_window_)) { |
| tab_list_interface->RemoveTabListInterfaceObserver(this); |
| } |
| } |
| |
| void ActiveTaskContextProviderImpl::AddObserver( |
| ActiveTaskContextProvider::Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ActiveTaskContextProviderImpl::RemoveObserver( |
| ActiveTaskContextProvider::Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnActiveTabChanged( |
| TabListInterface& tab_list, |
| tabs::TabInterface* active_tab) { |
| // Start observing the new active tab's WebContents. |
| Observe(active_tab ? active_tab->GetContents() : nullptr); |
| |
| // Update the context based on the new active tab. |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::PrimaryPageChanged(content::Page& page) { |
| GURL url = page.GetMainDocument().GetLastCommittedURL(); |
| bool is_aim_page = (url.scheme() == content::kChromeUIScheme && |
| url.host() == chrome::kChromeUIContextualTasksHost) || |
| (url.scheme() == content::kChromeUIScheme && |
| url.host() == chrome::kChromeUINewTabPageHost); |
| |
| if (!is_aim_page) { |
| if (base::FeatureList::IsEnabled(omnibox::kContextManagementInComposebox)) { |
| tabs::TabInterface* navigating_tab = |
| tabs::TabInterface::MaybeGetFromContents(web_contents()); |
| tabs::TabHandle navigating_tab_handle = navigating_tab |
| ? navigating_tab->GetHandle() |
| : tabs::TabHandle::Null(); |
| |
| // Only erase local underlines owned by the navigating tab, not all |
| // of the tabs, because now, there are multiple owners of tab underlines, |
| // not just one. This is because this tab's AIM/entrypoint searchbox has |
| // likely changed upon navigation, and no other tabs. If it navigates |
| // back to an AIM page, it will add the underlined tabs again in the |
| // contextual searchbox handler. |
| local_tab_underlines_.erase(navigating_tab_handle); |
| } |
| |
| auto* helper = |
| ContextualSearchWebContentsHelper::FromWebContents(web_contents()); |
| if (helper && helper->task_id()) { |
| SessionID tab_id = sessions::SessionTabHelper::IdForTab(web_contents()); |
| contextual_tasks_service_->DisassociateTabFromTask(*helper->task_id(), |
| tab_id); |
| helper->SetTaskSession(/*task_id=*/std::nullopt, /*handle=*/nullptr, |
| /*input_state_model=*/nullptr); |
| } |
| } |
| |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnTaskAdded( |
| const ContextualTask& task, |
| ContextualTasksService::TriggerSource source) { |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnTaskUpdated( |
| const ContextualTask& task, |
| ContextualTasksService::TriggerSource source) { |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnTaskRemoved( |
| const base::Uuid& task_id, |
| ContextualTasksService::TriggerSource source) { |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnTaskAssociatedToTab( |
| const base::Uuid& task_id, |
| SessionID tab_id) { |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnTaskDisassociatedFromTab( |
| const base::Uuid& task_id, |
| SessionID tab_id) { |
| RefreshContext(); |
| } |
| |
| void ActiveTaskContextProviderImpl::RefreshContext() { |
| if (!contextual_tasks::IsContextualTasksUIEnabled()) { |
| ResetStateAndNotifyObservers(); |
| return; |
| } |
| |
| // Increment the callback ID to invalidate any outstanding callbacks. |
| callback_id_++; |
| |
| contextual_search::ContextualSearchSessionHandle* session_handle = nullptr; |
| if (contextual_tasks_panel_controller_) { |
| auto [task_id, handle] = contextual_tasks_panel_controller_ |
| ->GetSessionHandleForActiveTabOrPanel(); |
| session_handle = handle; |
| active_task_id_ = task_id; |
| } else { |
| ResetStateAndNotifyObservers(); |
| } |
| |
| session_handle_ = session_handle ? session_handle->AsWeakPtr() : nullptr; |
| |
| if (!active_task_id_.has_value()) { |
| ResetStateAndNotifyObservers(); |
| return; |
| } |
| |
| if (!session_handle) { |
| ResetStateAndNotifyObservers(); |
| return; |
| } |
| |
| auto context_decoration_params = std::make_unique<ContextDecorationParams>(); |
| context_decoration_params->contextual_search_session_handle = |
| session_handle->AsWeakPtr(); |
| contextual_tasks_service_->GetContextForTask( |
| active_task_id_.value(), {}, std::move(context_decoration_params), |
| base::BindOnce(&ActiveTaskContextProviderImpl::OnGetContextForTask, |
| weak_ptr_factory_.GetWeakPtr(), callback_id_)); |
| } |
| |
| void ActiveTaskContextProviderImpl::OnGetContextForTask( |
| int callback_id, |
| std::unique_ptr<ContextualTaskContext> context) { |
| // Ignore stale results if a newer request has been sent. |
| if (callback_id != callback_id_) { |
| return; |
| } |
| |
| if (context) { |
| backend_context_tabs_ = |
| GetTabsFromContext(*context, browser_window_, session_handle_.get()); |
| } else { |
| backend_context_tabs_.clear(); |
| } |
| |
| NotifyObservers(); |
| } |
| |
| void ActiveTaskContextProviderImpl::ResetStateAndNotifyObservers() { |
| active_task_id_ = std::nullopt; |
| backend_context_tabs_.clear(); |
| session_handle_ = nullptr; |
| NotifyObservers(); |
| } |
| |
| void ActiveTaskContextProviderImpl::NotifyObservers() { |
| // Combine both the backend task context tab underlines and the local manual |
| // underlines for the active tab. |
| std::set<tabs::TabHandle> tabs_to_underline = backend_context_tabs_; |
| |
| auto* tab_list = TabListInterface::From(browser_window_); |
| auto* active_tab = tab_list ? tab_list->GetActiveTab() : nullptr; |
| tabs::TabHandle owner_handle = |
| active_tab ? active_tab->GetHandle() : tabs::TabHandle::Null(); |
| |
| auto it = local_tab_underlines_.find(owner_handle); |
| if (it != local_tab_underlines_.end()) { |
| for (auto handle : it->second) { |
| tabs_to_underline.insert(handle); |
| } |
| } |
| |
| // Add auto-suggested tab if chip is showing. |
| if (contextual_tasks_panel_controller_ && |
| contextual_tasks_panel_controller_->IsPanelOpenForContextualTask()) { |
| auto maybe_handle = |
| contextual_tasks_panel_controller_->GetAutoSuggestedTabHandle(); |
| if (maybe_handle) { |
| tabs_to_underline.insert(*maybe_handle); |
| } |
| } |
| |
| for (auto& obs : observers_) { |
| obs.OnContextTabsChanged(tabs_to_underline); |
| } |
| } |
| |
| } // namespace contextual_tasks |