blob: 250bf948297eee9543cd6d1cb4bf8ab7ab48ba75 [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/ui/lens/lens_search_controller.h"
#include "base/check.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/lens/core/mojom/geometry.mojom.h"
#include "chrome/browser/profiles/profile.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/lens/lens_overlay_controller.h"
#include "chrome/browser/ui/lens/lens_overlay_event_handler.h"
#include "chrome/browser/ui/lens/lens_overlay_image_helper.h"
#include "chrome/browser/ui/lens/lens_overlay_query_controller.h"
#include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h"
#include "chrome/browser/ui/lens/lens_overlay_theme_utils.h"
#include "chrome/browser/ui/lens/lens_permission_bubble_controller.h"
#include "chrome/browser/ui/lens/lens_search_contextualization_controller.h"
#include "chrome/browser/ui/lens/lens_searchbox_controller.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/webui/webui_embedding_context.h"
#include "components/lens/lens_features.h"
#include "components/lens/lens_overlay_permission_utils.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/optimization_guide/content/browser/page_context_eligibility.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/rect.h"
namespace {
void CheckInitialized(bool initialized) {
CHECK(initialized)
<< "The LensSearchController has not been initialized. Initialize() must "
"be called before using the LensSearchController.";
}
LensSearchController* GetLensSearchControllerFromTabInterface(
tabs::TabInterface* tab_interface) {
return tab_interface
? tab_interface->GetTabFeatures()->lens_search_controller()
: nullptr;
}
} // namespace
LensSearchController::LensSearchController(tabs::TabInterface* tab)
: tab_(tab) {
tab_subscriptions_.push_back(tab_->RegisterDidActivate(base::BindRepeating(
&LensSearchController::TabForegrounded, weak_ptr_factory_.GetWeakPtr())));
tab_subscriptions_.push_back(tab_->RegisterWillDeactivate(
base::BindRepeating(&LensSearchController::TabWillEnterBackground,
weak_ptr_factory_.GetWeakPtr())));
tab_subscriptions_.push_back(tab_->RegisterWillDiscardContents(
base::BindRepeating(&LensSearchController::WillDiscardContents,
weak_ptr_factory_.GetWeakPtr())));
tab_subscriptions_.push_back(tab_->RegisterWillDetach(base::BindRepeating(
&LensSearchController::WillDetach, weak_ptr_factory_.GetWeakPtr())));
}
LensSearchController::~LensSearchController() = default;
// TODO(crbug.com/404941800): Reconsider which of these controllers should be
// created in Initialize() vs created on demand when invoked.
void LensSearchController::Initialize(
variations::VariationsClient* variations_client,
signin::IdentityManager* identity_manager,
PrefService* pref_service,
syncer::SyncService* sync_service,
ThemeService* theme_service) {
CHECK(!initialized_);
initialized_ = true;
variations_client_ = variations_client;
identity_manager_ = identity_manager;
pref_service_ = pref_service;
theme_service_ = theme_service;
// Create Gen204 controller first as query controller depends on it.
gen204_controller_ = std::make_unique<lens::LensOverlayGen204Controller>();
lens_overlay_controller_ = CreateLensOverlayController(
tab_, this, variations_client, identity_manager, pref_service,
sync_service, theme_service);
lens_overlay_side_panel_coordinator_ =
CreateLensOverlaySidePanelCoordinator();
lens_searchbox_controller_ = CreateLensSearchboxController();
lens_contextualization_controller_ =
CreateLensSearchContextualizationController();
lens_overlay_event_handler_ =
std::make_unique<lens::LensOverlayEventHandler>(this);
CreatePageContextEligibilityAPI();
}
// static.
LensSearchController* LensSearchController::FromWebUIWebContents(
content::WebContents* webui_web_contents) {
return GetLensSearchControllerFromTabInterface(
webui::GetTabInterface(webui_web_contents));
}
// static.
LensSearchController* LensSearchController::FromTabWebContents(
content::WebContents* tab_web_contents) {
return GetLensSearchControllerFromTabInterface(
tabs::TabInterface::GetFromContents(tab_web_contents));
}
void LensSearchController::OpenLensOverlay(
lens::LensOverlayInvocationSource invocation_source) {
CheckInitialized(initialized_);
// If the eligibility checks fail, do not procced with opening any UI.
if (!RunLensEligibilityChecks(
invocation_source,
/*permission_granted_callback=*/base::BindRepeating(
&LensSearchController::OpenLensOverlay,
weak_ptr_factory_.GetWeakPtr(), invocation_source))) {
return;
}
state_ = State::kInitializing;
// Create the query controller to be used for the current invocation.
CHECK(!lens_overlay_query_controller_);
lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
lens_overlay_controller_->ShowUI(invocation_source,
lens_overlay_query_controller_.get());
}
void LensSearchController::OpenLensOverlayWithPendingRegionFromBounds(
lens::LensOverlayInvocationSource invocation_source,
const gfx::Rect& tab_bounds,
const gfx::Rect& view_bounds,
const gfx::Rect& region_bounds,
const SkBitmap& region_bitmap) {
OpenLensOverlayWithPendingRegion(
invocation_source,
lens::GetCenterRotatedBoxFromTabViewAndImageBounds(
tab_bounds, view_bounds, region_bounds),
region_bitmap);
}
void LensSearchController::OpenLensOverlayWithPendingRegion(
lens::LensOverlayInvocationSource invocation_source,
lens::mojom::CenterRotatedBoxPtr region,
const SkBitmap& region_bitmap) {
// If the eligibility checks fail, do not procced with opening any UI.
if (!RunLensEligibilityChecks(
invocation_source,
/*permission_granted_callback=*/base::BindRepeating(
&LensSearchController::OpenLensOverlayWithPendingRegion,
weak_ptr_factory_.GetWeakPtr(), invocation_source,
base::Passed(region.Clone()), region_bitmap))) {
return;
}
state_ = State::kInitializing;
// Create the query controller to be used for the current invocation.
CHECK(!lens_overlay_query_controller_);
lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
lens_overlay_controller_->ShowUIWithPendingRegion(
lens_overlay_query_controller_.get(), invocation_source,
std::move(region), region_bitmap);
}
void LensSearchController::StartContextualization(
lens::LensOverlayInvocationSource invocation_source) {
// If the eligibility checks fail, do not procced with opening any UI.
if (!RunLensEligibilityChecks(
invocation_source,
/*permission_granted_callback=*/base::BindRepeating(
&LensSearchController::StartContextualization,
weak_ptr_factory_.GetWeakPtr(), invocation_source))) {
return;
}
state_ = State::kInitializing;
// Create the query controller to be used for the current invocation.
CHECK(!lens_overlay_query_controller_);
lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
// TODO(crbug.com/404941800): This flow should not start the overlay once
// contextualization is separated from the overlay.
lens_overlay_controller_->StartContextualizationWithoutOverlay(
invocation_source, lens_overlay_query_controller_.get());
}
void LensSearchController::IssueContextualSearchRequest(
const GURL& destination_url,
AutocompleteMatchType::Type match_type,
bool is_zero_prefix_suggestion) {
// TODO(crbug.com/402497756): For prototyping, reusing the existing
// omnibox entry point. However, for production, create a new invocation
// source for this new entry point.
lens::LensOverlayInvocationSource invocation_source =
lens::LensOverlayInvocationSource::kOmnibox;
// If the eligibility checks fail, do not procced with opening any UI.
if (!RunLensEligibilityChecks(
invocation_source,
/*permission_granted_callback=*/base::BindRepeating(
&LensSearchController::IssueContextualSearchRequest,
weak_ptr_factory_.GetWeakPtr(), destination_url, match_type,
is_zero_prefix_suggestion))) {
return;
}
state_ = State::kInitializing;
CHECK(!lens_overlay_query_controller_);
lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
// TODO(crbug.com/404941800): This flow should not start the overlay once
// contextualization is separated from the overlay.
lens_overlay_controller_->IssueContextualSearchRequest(
destination_url, lens_overlay_query_controller_.get(), match_type,
is_zero_prefix_suggestion, invocation_source);
}
void LensSearchController::CloseLensAsync(
lens::LensOverlayDismissalSource dismissal_source) {
if (state() == State::kOff) {
return;
}
// Close the side panel if it is showing. This provides a smooth closing
// animation.
auto* side_panel_coordinator =
tab_->GetBrowserWindowInterface()->GetFeatures().side_panel_coordinator();
CHECK(side_panel_coordinator);
if (state_ == State::kActive && side_panel_coordinator->GetCurrentEntryId() ==
SidePanelEntry::Id::kLensOverlayResults) {
// If a close was triggered while the Lens side panel is showing, instead of
// just immediately closing all UI, the side panel should close to show a
// smooth closing animation. Once the side panel deregisters, it will
// recall the close method in OnSidePanelHidden() which will finish the
// closing process.
state_ = State::kClosingSidePanel;
last_dismissal_source_ = dismissal_source;
side_panel_coordinator->Close();
return;
}
state_ = State::kClosing;
// If the overlay is showing, and the side panel is not, the overlay needs to
// fade out. Play the fade out animation and then clean up the rest of the UI
// afterwards.
if (lens_overlay_controller_->state() != LensOverlayController::State::kOff) {
lens_overlay_controller_->TriggerOverlayCloseAnimation(
base::BindOnce(&LensSearchController::CloseLensPart2,
weak_ptr_factory_.GetWeakPtr(), dismissal_source));
} else {
CloseLensPart2(dismissal_source);
}
}
void LensSearchController::CloseLensSync(
lens::LensOverlayDismissalSource dismissal_source) {
if (state() == State::kOff) {
return;
}
state_ = State::kClosing;
CloseLensPart2(dismissal_source);
}
bool LensSearchController::IsActive() {
return state_ == State::kActive;
}
bool LensSearchController::IsClosing() {
return state_ == State::kClosing || state_ == State::kClosingSidePanel;
}
tabs::TabInterface* LensSearchController::GetTabInterface() {
return tab_;
}
const GURL& LensSearchController::GetPageURL() const {
if (lens::CanSharePageURLWithLensOverlay(pref_service_)) {
return tab_->GetContents()->GetVisibleURL();
}
return GURL::EmptyGURL();
}
base::WeakPtr<LensSearchController> LensSearchController::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
LensOverlayController* LensSearchController::lens_overlay_controller() {
CheckInitialized(initialized_);
return lens_overlay_controller_.get();
}
const LensOverlayController* LensSearchController::lens_overlay_controller()
const {
CheckInitialized(initialized_);
return lens_overlay_controller_.get();
}
lens::LensOverlaySidePanelCoordinator*
LensSearchController::lens_overlay_side_panel_coordinator() {
CheckInitialized(initialized_);
return lens_overlay_side_panel_coordinator_.get();
}
lens::LensSearchboxController*
LensSearchController::lens_searchbox_controller() {
CheckInitialized(initialized_);
return lens_searchbox_controller_.get();
}
lens::LensOverlayEventHandler*
LensSearchController::lens_overlay_event_handler() {
CheckInitialized(initialized_);
return lens_overlay_event_handler_.get();
}
optimization_guide::PageContextEligibility*
LensSearchController::page_context_eligibility() {
CheckInitialized(initialized_);
if (page_context_eligibility_) {
return page_context_eligibility_;
}
return nullptr;
}
lens::LensSearchContextualizationController*
LensSearchController::lens_search_contextualization_controller() {
CheckInitialized(initialized_);
return lens_contextualization_controller_.get();
}
std::unique_ptr<LensOverlayController>
LensSearchController::CreateLensOverlayController(
tabs::TabInterface* tab,
LensSearchController* lens_search_controller,
variations::VariationsClient* variations_client,
signin::IdentityManager* identity_manager,
PrefService* pref_service,
syncer::SyncService* sync_service,
ThemeService* theme_service) {
return std::make_unique<LensOverlayController>(
tab, lens_search_controller, variations_client, identity_manager,
pref_service, sync_service, theme_service);
}
std::unique_ptr<lens::LensOverlayQueryController>
LensSearchController::CreateLensQueryController(
lens::LensOverlayFullImageResponseCallback full_image_callback,
lens::LensOverlayUrlResponseCallback url_callback,
lens::LensOverlayInteractionResponseCallback interaction_callback,
lens::LensOverlaySuggestInputsCallback suggest_inputs_callback,
lens::LensOverlayThumbnailCreatedCallback thumbnail_created_callback,
lens::UploadProgressCallback upload_progress_callback,
variations::VariationsClient* variations_client,
signin::IdentityManager* identity_manager,
Profile* profile,
lens::LensOverlayInvocationSource invocation_source,
bool use_dark_mode,
lens::LensOverlayGen204Controller* gen204_controller) {
return std::make_unique<lens::LensOverlayQueryController>(
std::move(full_image_callback), std::move(url_callback),
std::move(interaction_callback), std::move(suggest_inputs_callback),
std::move(thumbnail_created_callback),
std::move(upload_progress_callback), variations_client, identity_manager,
profile, invocation_source, use_dark_mode, gen204_controller);
}
std::unique_ptr<lens::LensOverlaySidePanelCoordinator>
LensSearchController::CreateLensOverlaySidePanelCoordinator() {
return std::make_unique<lens::LensOverlaySidePanelCoordinator>(this);
}
std::unique_ptr<lens::LensSearchboxController>
LensSearchController::CreateLensSearchboxController() {
return std::make_unique<lens::LensSearchboxController>(this);
}
std::unique_ptr<lens::LensSearchContextualizationController>
LensSearchController::CreateLensSearchContextualizationController() {
return std::make_unique<lens::LensSearchContextualizationController>(this);
}
void LensSearchController::CreatePageContextEligibilityAPI() {
// Post to a background thread to avoid blocking the set up of the overlay.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(&optimization_guide::PageContextEligibility::Get),
base::BindOnce(&LensSearchController::OnPageContextEligibilityAPILoaded,
weak_ptr_factory_.GetWeakPtr()));
}
void LensSearchController::OnPageContextEligibilityAPILoaded(
optimization_guide::PageContextEligibility* page_context_eligibility) {
page_context_eligibility_ = page_context_eligibility;
}
std::unique_ptr<lens::LensOverlayQueryController>
LensSearchController::CreateLensQueryController(
lens::LensOverlayInvocationSource invocation_source) {
Profile* profile =
Profile::FromBrowserContext(tab_->GetContents()->GetBrowserContext());
return CreateLensQueryController(
base::BindRepeating(&LensSearchController::HandleStartQueryResponse,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&LensSearchController::HandleInteractionURLResponse,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&LensSearchController::HandleInteractionResponse,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&LensSearchController::HandleSuggestInputsResponse,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&LensSearchController::HandleThumbnailCreated,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(
&LensSearchController::HandlePageContentUploadProgress,
weak_ptr_factory_.GetWeakPtr()),
variations_client_, identity_manager_, profile,
/*invocation_source=*/invocation_source,
/*use_dark_mode=*/lens::LensOverlayShouldUseDarkMode(theme_service_),
gen204_controller_.get());
}
bool LensSearchController::RunLensEligibilityChecks(
lens::LensOverlayInvocationSource invocation_source,
base::RepeatingClosure permission_granted_callback) {
// The UI should only show if the tab is in the foreground or if the tab web
// contents is not in a crash state.
if (!tab_->IsActivated() || tab_->GetContents()->IsCrashed()) {
return false;
}
// Exit early if the Lens feature is already active.
if (state() != State::kOff) {
return false;
}
// If the user hasn't granted permission, request user permission before
// showing the UI.
if (!lens::CanSharePageScreenshotWithLensOverlay(pref_service_) ||
(lens::features::IsLensOverlayContextualSearchboxEnabled() &&
!lens::CanSharePageContentWithLensOverlay(pref_service_))) {
if (!lens_permission_bubble_controller_) {
lens_permission_bubble_controller_ =
std::make_unique<lens::LensPermissionBubbleController>(
*tab_, pref_service_, invocation_source);
}
lens_permission_bubble_controller_->RequestPermission(
tab_->GetContents(), std::move(permission_granted_callback));
return false;
}
return true;
}
void LensSearchController::NotifyOverlayOpened() {
CHECK(state() == State::kInitializing);
state_ = State::kActive;
}
void LensSearchController::CloseLensPart2(
lens::LensOverlayDismissalSource dismissal_source) {
// Let the controllers know to cleanup.
// TODO(crbug.com/404941800): Move logging to a shared location to not be
// dependent on the overlay controller.
lens_overlay_controller_->CloseUI(dismissal_source);
lens_searchbox_controller_->CloseUI();
lens_permission_bubble_controller_.reset();
// Cleanup the query controller after the overlay controller to prevent
// dangling ptrs.
lens_overlay_query_controller_.reset();
state_ = State::kOff;
}
void LensSearchController::OnSidePanelWillHide(
SidePanelEntryHideReason reason) {
// If the tab is not in the foreground, this is not relevant.
if (!tab_->IsActivated()) {
return;
}
if (!IsClosing()) {
if (reason == SidePanelEntryHideReason::kReplaced) {
// If the Lens side panel is being replaced, don't close the side panel.
// Instead, set the state and dismissal source and wait for
// OnSidePanelHidden to be called.
state_ = State::kClosingSidePanel;
last_dismissal_source_ =
lens::LensOverlayDismissalSource::kSidePanelEntryReplaced;
} else {
// Trigger the close animation and notify the overlay that the side
// panel is closing so that it can fade out the UI.
CloseLensAsync(lens::LensOverlayDismissalSource::kSidePanelCloseButton);
}
}
}
void LensSearchController::OnSidePanelHidden() {
if (state_ != State::kClosingSidePanel) {
return;
}
CHECK(last_dismissal_source_.has_value());
CloseLensPart2(*last_dismissal_source_);
last_dismissal_source_.reset();
}
void LensSearchController::HandleStartQueryResponse(
std::vector<lens::mojom::OverlayObjectPtr> objects,
lens::mojom::TextPtr text,
bool is_error) {
lens_overlay_controller_->HandleStartQueryResponse(std::move(objects),
std::move(text), is_error);
}
void LensSearchController::HandleInteractionURLResponse(
lens::proto::LensOverlayUrlResponse response) {
lens_overlay_controller_->HandleInteractionURLResponse(response);
}
void LensSearchController::HandleInteractionResponse(
lens::mojom::TextPtr text) {
lens_overlay_controller_->HandleInteractionResponse(std::move(text));
}
void LensSearchController::HandleSuggestInputsResponse(
lens::proto::LensOverlaySuggestInputs suggest_inputs) {
lens_overlay_controller_->HandleSuggestInputsResponse(suggest_inputs);
}
void LensSearchController::HandlePageContentUploadProgress(uint64_t position,
uint64_t total) {
lens_overlay_controller_->HandlePageContentUploadProgress(position, total);
}
void LensSearchController::HandleThumbnailCreated(
const std::string& thumbnail_bytes) {
lens_searchbox_controller_->HandleThumbnailCreated(thumbnail_bytes);
}
void LensSearchController::TabForegrounded(tabs::TabInterface* tab) {
// Ignore the event if the overlay is not backgrounded.
if (state_ != State::kBackground) {
return;
}
// Notify the overlay controller of the tab foregrounded event so it can
// restore to the previous state.
lens_overlay_controller_->TabForegrounded(tab);
state_ = State::kActive;
}
void LensSearchController::TabWillEnterBackground(tabs::TabInterface* tab) {
if (state_ == State::kOff) {
return;
}
// Ignore the event if the overlay is already backgrounded.
if (state_ == State::kBackground) {
return;
}
// Notify the overlay controller of the tab will enter background event so
// it can hide the overlay.
lens_overlay_controller_->TabWillEnterBackground(tab);
state_ = State::kBackground;
}
void LensSearchController::WillDiscardContents(
tabs::TabInterface* tab,
content::WebContents* old_contents,
content::WebContents* new_contents) {
// Background tab contents discarded.
CloseLensSync(lens::LensOverlayDismissalSource::kTabContentsDiscarded);
}
void LensSearchController::WillDetach(tabs::TabInterface* tab,
tabs::TabInterface::DetachReason reason) {
// When dragging a tab into a new window, all window-specific state must be
// reset. As this flow is not fully functional, close the overlay regardless
// of `reason`. https://crbug.com/342921671.
switch (reason) {
case tabs::TabInterface::DetachReason::kDelete:
CloseLensSync(lens::LensOverlayDismissalSource::kTabClosed);
return;
case tabs::TabInterface::DetachReason::kInsertIntoOtherWindow:
CloseLensSync(lens::LensOverlayDismissalSource::kTabDragNewWindow);
return;
}
}