| // 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/ui/lens/lens_searchbox_controller.h" |
| |
| #include "chrome/browser/lens/core/mojom/lens_ghost_loader.mojom.h" |
| #include "chrome/browser/ui/lens/lens_overlay_controller.h" |
| #include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h" |
| #include "chrome/browser/ui/lens/lens_overlay_url_builder.h" |
| #include "chrome/browser/ui/lens/lens_search_contextualization_controller.h" |
| #include "chrome/browser/ui/lens/lens_search_controller.h" |
| #include "chrome/browser/ui/lens/lens_session_metrics_logger.h" |
| #include "components/lens/lens_features.h" |
| #include "components/lens/lens_url_utils.h" |
| #include "components/lens/proto/server/lens_overlay_response.pb.h" |
| #include "components/omnibox/browser/lens_suggest_inputs_utils.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "components/sessions/core/session_id.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "url/gurl.h" |
| |
| namespace lens { |
| |
| LensSearchboxController::LensSearchboxInitializationData:: |
| LensSearchboxInitializationData() = default; |
| |
| LensSearchboxController::LensSearchboxController( |
| LensSearchController* lens_search_controller) |
| : lens_search_controller_(lens_search_controller) {} |
| LensSearchboxController::~LensSearchboxController() = default; |
| |
| void LensSearchboxController::BindOverlayGhostLoader( |
| mojo::PendingRemote<lens::mojom::LensGhostLoaderPage> page) { |
| overlay_ghost_loader_page_.reset(); |
| overlay_ghost_loader_page_.Bind(std::move(page)); |
| |
| // If the page is not context eligible, show the error state once the ghost |
| // loader is bound. |
| if (!lens_search_controller_->lens_search_contextualization_controller() |
| ->GetCurrentPageContextEligibility()) { |
| ShowGhostLoaderErrorState(); |
| } |
| } |
| |
| void LensSearchboxController::BindSidePanelGhostLoader( |
| mojo::PendingRemote<lens::mojom::LensGhostLoaderPage> page) { |
| side_panel_ghost_loader_page_.reset(); |
| side_panel_ghost_loader_page_.Bind(std::move(page)); |
| } |
| |
| void LensSearchboxController::OnSessionStart(bool suppress_contextualization) { |
| // Initialize any data needed for the searchbox. |
| init_data_ = std::make_unique<LensSearchboxInitializationData>(); |
| init_data_->suppress_contextualization = suppress_contextualization; |
| } |
| |
| void LensSearchboxController::SetSidePanelSearchboxHandler( |
| std::unique_ptr<LensSearchboxHandler> handler) { |
| side_panel_searchbox_handler_ = std::move(handler); |
| } |
| |
| void LensSearchboxController::SetContextualSearchboxHandler( |
| std::unique_ptr<LensSearchboxHandler> handler) { |
| overlay_searchbox_handler_ = std::move(handler); |
| } |
| |
| void LensSearchboxController::ResetOverlaySearchboxHandler() { |
| overlay_searchbox_handler_.reset(); |
| } |
| |
| void LensSearchboxController::ResetSidePanelSearchboxHandler() { |
| side_panel_searchbox_handler_.reset(); |
| } |
| |
| void LensSearchboxController::SetSearchboxInputText(const std::string& text) { |
| if (side_panel_searchbox_handler_ && |
| side_panel_searchbox_handler_->IsRemoteBound()) { |
| init_data_->text_query = text; |
| side_panel_searchbox_handler_->SetInputText(text); |
| } else { |
| // If the side panel was not bound at the time of request, we store the |
| // query as pending to send it to the searchbox on bind. |
| pending_text_query_ = text; |
| } |
| } |
| |
| void LensSearchboxController::SetSearchboxThumbnail( |
| const std::string& thumbnail_uri) { |
| // Init data can be empty if overlay is opened in a normal tab by navigating |
| // to the WebUI url in the omnibox. |
| if (!init_data_) { |
| return; |
| } |
| |
| // Store the thumbnail. |
| init_data_->thumbnail_uri = thumbnail_uri; |
| |
| if (side_panel_searchbox_handler_ && |
| side_panel_searchbox_handler_->IsRemoteBound()) { |
| side_panel_searchbox_handler_->SetThumbnail( |
| init_data_->show_side_panel_thumbnail ? thumbnail_uri : "", |
| /*is_deletable=*/!IsContextualSearchbox()); |
| } |
| |
| if (overlay_searchbox_handler_ && |
| overlay_searchbox_handler_->IsRemoteBound()) { |
| overlay_searchbox_handler_->SetThumbnail( |
| thumbnail_uri, /*is_deletable=*/!IsContextualSearchbox()); |
| } |
| } |
| |
| void LensSearchboxController::SetShowSidePanelSearchboxThumbnail(bool shown) { |
| if (!init_data_) { |
| return; |
| } |
| |
| init_data_->show_side_panel_thumbnail = shown; |
| |
| if (side_panel_searchbox_handler_ && |
| side_panel_searchbox_handler_->IsRemoteBound()) { |
| side_panel_searchbox_handler_->SetThumbnail( |
| shown ? init_data_->thumbnail_uri : "", |
| /*is_deletable=*/!IsContextualSearchbox()); |
| } |
| } |
| |
| void LensSearchboxController::HandleSuggestInputsResponse( |
| lens::proto::LensOverlaySuggestInputs suggest_inputs) { |
| if (!init_data_) { |
| DCHECK(init_data_) |
| << "The initialization data should be set on searchbox startup, which " |
| "should have happened before any suggest inputs were received."; |
| return; |
| } |
| |
| // If the handshake was already complete, without the new suggest inputs, |
| // exit early so that LensOverlayController::OnHandshakeComplete() isn't |
| // called multiple times. |
| if (lens_search_controller_->IsHandshakeComplete()) { |
| init_data_->suggest_inputs_ = suggest_inputs; |
| return; |
| } |
| |
| // Check if the handshake with the server has been completed with the new |
| // inputs. If so, this is the first time the suggest inputs satisfy the |
| // handshake criteria, so notify the overlay that the handshake is complete. |
| init_data_->suggest_inputs_ = suggest_inputs; |
| if (lens_search_controller_->IsHandshakeComplete()) { |
| // Notify the overlay that it is now safe to query autocomplete. |
| lens_search_controller_->lens_overlay_controller()->OnHandshakeComplete(); |
| |
| // Send the suggest inputs to any pending callbacks. |
| pending_suggest_inputs_callbacks_.Notify(GetLensSuggestInputs()); |
| } |
| } |
| |
| void LensSearchboxController::CloseUI() { |
| overlay_searchbox_handler_.reset(); |
| side_panel_searchbox_handler_.reset(); |
| overlay_ghost_loader_page_.reset(); |
| side_panel_ghost_loader_page_.reset(); |
| init_data_ = std::make_unique<LensSearchboxInitializationData>(); |
| pending_text_query_ = std::nullopt; |
| pending_suggest_inputs_callbacks_.Notify(std::nullopt); |
| } |
| |
| bool LensSearchboxController::IsContextualSearchbox() const { |
| // TODO(crbug.com/405441183): This logic will break the side panel searchbox |
| // if there is no overlay, so it should be moved to a shared location. |
| return GetPageClassification() == |
| metrics::OmniboxEventProto::CONTEXTUAL_SEARCHBOX; |
| } |
| |
| bool LensSearchboxController::IsSidePanelSearchbox() const { |
| return side_panel_searchbox_handler_ != nullptr; |
| } |
| |
| void LensSearchboxController::GetIsContextualSearchbox( |
| GetIsContextualSearchboxCallback callback) { |
| std::move(callback).Run(IsContextualSearchbox()); |
| } |
| |
| base::CallbackListSubscription |
| LensSearchboxController::GetLensSuggestInputsWhenReady( |
| ::LensOverlaySuggestInputsCallback callback) { |
| // Exit early if the overlay is either off or going to soon be off. |
| if (lens_search_controller_->IsClosing() || |
| lens_search_controller_->IsOff()) { |
| std::move(callback).Run(std::nullopt); |
| return {}; |
| } |
| |
| // If the handshake is complete, return the Lens suggest inputs immediately. |
| if (lens_search_controller_->IsHandshakeComplete()) { |
| std::move(callback).Run(init_data_->suggest_inputs_); |
| return {}; |
| } |
| return pending_suggest_inputs_callbacks_.Add(std::move(callback)); |
| } |
| |
| const GURL& LensSearchboxController::GetPageURL() const { |
| return lens_search_controller_->GetPageURL(); |
| } |
| |
| SessionID LensSearchboxController::GetTabId() const { |
| return sessions::SessionTabHelper::IdForTab(GetTabWebContents()); |
| } |
| |
| metrics::OmniboxEventProto::PageClassification |
| LensSearchboxController::GetPageClassification() const { |
| // There are two cases where we are assuming to be in a contextual flow: |
| // 1) We are in the zero state with the overlay CSB showing. |
| // 2) A user has made a contextual query and the live page is now showing. |
| // TODO(crbug.com/404941800): Remove dependency on LensOverlayController. |
| // Instead, it should check if contextualization is currently active. Which |
| // also requires disabling contextualization when the user goes down the |
| // visual search path. |
| const LensOverlayController::State state = |
| lens_search_controller_->lens_overlay_controller()->state(); |
| bool state_supports_contextualization = |
| state == LensOverlayController::State::kHidden || |
| state == LensOverlayController::State::kOverlay || |
| (state == LensOverlayController::State::kOff && |
| lens_search_controller_->lens_search_contextualization_controller() |
| ->IsActive()); |
| if (state_supports_contextualization && |
| !init_data_->suppress_contextualization) { |
| return metrics::OmniboxEventProto::CONTEXTUAL_SEARCHBOX; |
| } |
| return init_data_->thumbnail_uri.empty() |
| ? metrics::OmniboxEventProto::SEARCH_SIDE_PANEL_SEARCHBOX |
| : metrics::OmniboxEventProto::LENS_SIDE_PANEL_SEARCHBOX; |
| } |
| |
| std::string& LensSearchboxController::GetThumbnail() { |
| return init_data_->thumbnail_uri; |
| } |
| |
| const lens::proto::LensOverlaySuggestInputs& |
| LensSearchboxController::GetLensSuggestInputs() const { |
| return init_data_ |
| ? init_data_->suggest_inputs_ |
| : lens::proto::LensOverlaySuggestInputs().default_instance(); |
| } |
| |
| void LensSearchboxController::OnTextModified() { |
| lens_search_controller_->lens_overlay_controller()->ClearTextSelection(); |
| } |
| |
| void LensSearchboxController::OnThumbnailRemoved() { |
| lens_search_controller_->lens_overlay_controller()->ClearRegionSelection(); |
| } |
| |
| void LensSearchboxController::OnSuggestionAccepted( |
| const GURL& destination_url, |
| AutocompleteMatchType::Type match_type, |
| bool is_zero_prefix_suggestion) { |
| base::Time query_start_time = base::Time::Now(); |
| std::string query_text = ExtractTextQueryParameterValue(destination_url); |
| std::map<std::string, std::string> additional_query_parameters = |
| GetParametersMapWithoutQuery(destination_url); |
| |
| // TODO(crbug.com/413138792): Move the logic to issue a searchbox query to |
| // this class. |
| lens_search_controller_->lens_overlay_controller()->IssueSearchBoxRequest( |
| query_start_time, query_text, match_type, is_zero_prefix_suggestion, |
| additional_query_parameters); |
| } |
| |
| void LensSearchboxController::OnFocusChanged(bool focused) { |
| // TOOD(crbug.com/404941800): Implement OnSearchboxFocusChanged logic in this |
| // class. |
| lens_search_controller_->lens_overlay_controller()->OnSearchboxFocusChanged( |
| focused); |
| } |
| |
| void LensSearchboxController::OnPageBound() { |
| // Send any pending inputs for the searchbox. |
| if (pending_text_query_.has_value() && side_panel_searchbox_handler_ && |
| side_panel_searchbox_handler_->IsRemoteBound()) { |
| side_panel_searchbox_handler_->SetInputText(*pending_text_query_); |
| pending_text_query_.reset(); |
| } |
| // If there is a thumbnail, make sure the searchbox receives it. |
| if (init_data_ && !init_data_->thumbnail_uri.empty()) { |
| SetSearchboxThumbnail(init_data_->thumbnail_uri); |
| } |
| } |
| |
| void LensSearchboxController::ShowGhostLoaderErrorState() { |
| if (!IsContextualSearchbox()) { |
| return; |
| } |
| if (overlay_ghost_loader_page_) { |
| overlay_ghost_loader_page_->ShowErrorState(); |
| } |
| if (side_panel_ghost_loader_page_) { |
| side_panel_ghost_loader_page_->ShowErrorState(); |
| } |
| } |
| |
| void LensSearchboxController::OnZeroSuggestShown() { |
| if (!IsContextualSearchbox()) { |
| return; |
| } |
| |
| // If this is in the side panel, it is not the initial query. |
| lens_search_controller_->lens_session_metrics_logger()->OnZeroSuggestShown( |
| /*is_initial_query=*/!IsSidePanelSearchbox()); |
| } |
| |
| void LensSearchboxController::AddSearchboxStateToSearchQuery( |
| lens::SearchQuery& search_query) { |
| search_query.selected_region_thumbnail_uri_ = init_data_->thumbnail_uri; |
| } |
| |
| content::WebContents* LensSearchboxController::GetTabWebContents() const { |
| return lens_search_controller_->GetTabInterface()->GetContents(); |
| } |
| |
| } // namespace lens |