| // 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 |