| // 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 "components/omnibox/composebox/composebox_query_controller.h" |
| |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64url.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "components/lens/contextual_input.h" |
| #include "components/lens/lens_features.h" |
| #include "components/lens/lens_payload_construction.h" |
| #include "components/lens/lens_request_construction.h" |
| #include "components/lens/lens_url_utils.h" |
| #include "components/lens/ref_counted_lens_overlay_client_logs.h" |
| #include "components/search_engines/util.h" |
| #include "components/signin/public/base/consent_level.h" |
| #include "components/signin/public/identity_manager/access_token_info.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/variations/variations_client.h" |
| #include "components/version_info/channel.h" |
| #include "google_apis/common/api_error_codes.h" |
| #include "google_apis/gaia/gaia_constants.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/base/url_util.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "third_party/icu/source/common/unicode/locid.h" |
| #include "third_party/icu/source/common/unicode/unistr.h" |
| #include "third_party/icu/source/i18n/unicode/timezone.h" |
| #include "third_party/lens_server_proto/lens_overlay_contextual_inputs.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_payload.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_platform.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_request_id.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_service_deps.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_surface.pb.h" |
| #include "third_party/omnibox_proto/chrome_aim_entry_point.pb.h" |
| |
| #if !BUILDFLAG(IS_IOS) |
| #include "services/data_decoder/public/cpp/decode_image.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #endif // !BUILDFLAG(IS_IOS) |
| |
| using endpoint_fetcher::CredentialsMode; |
| using endpoint_fetcher::EndpointFetcher; |
| using endpoint_fetcher::EndpointFetcherCallback; |
| using endpoint_fetcher::EndpointResponse; |
| using endpoint_fetcher::HttpMethod; |
| |
| constexpr char kContentTypeKey[] = "Content-Type"; |
| constexpr char kContentType[] = "application/x-protobuf"; |
| constexpr char kSessionIdQueryParameterKey[] = "gsessionid"; |
| |
| // TODO(crbug.com/432348301): Move away from hardcoded entrypoint and lns |
| // surface values. |
| constexpr char kLnsSurfaceParameterValue[] = "47"; |
| |
| constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotationTag = |
| net::DefineNetworkTrafficAnnotation("ntp_composebox_query_controller", R"( |
| semantics { |
| sender: "Lens" |
| description: "A request to the service handling the file uploads for " |
| "the Composebox in the NTP in Chrome." |
| trigger: "The user triggered a compose flow in the Chrome NTP " |
| "by clicking on the button in the realbox." |
| data: "Only file data that is explicitly uploaded by the user will " |
| "be sent." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { |
| email: "hujasonx@google.com" |
| } |
| contacts { |
| email: "lens-chrome@google.com" |
| } |
| } |
| user_data { |
| type: USER_CONTENT |
| type: WEB_CONTENT |
| } |
| last_reviewed: "2025-06-20" |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: "This feature is only shown in the NTP by default and does " |
| "nothing without explicit user action, so there is no setting to " |
| "disable the feature." |
| policy_exception_justification: "Not yet implemented." |
| } |
| )"); |
| |
| ComposeboxQueryController::UploadRequest::UploadRequest() = default; |
| ComposeboxQueryController::UploadRequest::~UploadRequest() = default; |
| |
| ComposeboxQueryController::FileInfo::FileInfo() = default; |
| ComposeboxQueryController::FileInfo::~FileInfo() = default; |
| |
| ComposeboxQueryController::CreateSearchUrlRequestInfo:: |
| CreateSearchUrlRequestInfo() = default; |
| ComposeboxQueryController::CreateSearchUrlRequestInfo:: |
| ~CreateSearchUrlRequestInfo() = default; |
| |
| namespace { |
| |
| // Creates a payload for a contextual data upload request, for webpage contents |
| // or for uploaded pdf files. |
| lens::Payload CreateContentextualDataUploadPayload( |
| std::vector<lens::ContextualInput> context_inputs, |
| std::optional<GURL> page_url, |
| std::optional<std::string> page_title) { |
| lens::Payload payload; |
| auto* content = payload.mutable_content(); |
| |
| if (page_url.has_value() && !page_url->is_empty()) { |
| content->set_webpage_url(page_url->spec()); |
| } |
| if (page_title.has_value() && !page_title.value().empty()) { |
| content->set_webpage_title(page_title.value()); |
| } |
| |
| for (const lens::ContextualInput& context_input : context_inputs) { |
| auto* content_data = content->add_content_data(); |
| content_data->set_content_type( |
| MimeTypeToContentType(context_input.content_type_)); |
| |
| // Compress PDF bytes. |
| if (context_input.content_type_ == lens::MimeType::kPdf) { |
| // If compression is successful, set the compression type and return. |
| // Otherwise, fall back to the original bytes. |
| if (lens::ZstdCompressBytes(context_input.bytes_, |
| content_data->mutable_data())) { |
| content_data->set_compression_type(lens::CompressionType::ZSTD); |
| continue; |
| } |
| } |
| |
| // Add non compressed bytes. This happens if compression fails or its not |
| // a PDF. |
| content_data->mutable_data()->assign(context_input.bytes_.begin(), |
| context_input.bytes_.end()); |
| } |
| |
| return payload; |
| } |
| |
| // Creates the server request proto for the pdf / page content upload request. |
| // Called on the main thread after the payload is ready. |
| void CreateFileUploadRequestProtoWithPayloadAndContinue( |
| lens::LensOverlayRequestId request_id, |
| lens::LensOverlayClientContext client_context, |
| RequestBodyProtoCreatedCallback callback, |
| lens::Payload payload) { |
| lens::LensOverlayServerRequest request; |
| auto* objects_request = request.mutable_objects_request(); |
| objects_request->mutable_request_context()->mutable_request_id()->CopyFrom( |
| request_id); |
| objects_request->mutable_request_context() |
| ->mutable_client_context() |
| ->CopyFrom(client_context); |
| objects_request->mutable_payload()->CopyFrom(payload); |
| std::move(callback).Run(request, /*error_type=*/std::nullopt); |
| } |
| |
| // Returns true if the file upload status is valid to include in the multimodal |
| // request. |
| bool IsValidFileUploadStatusForMultimodalRequest( |
| FileUploadStatus upload_status) { |
| return upload_status == FileUploadStatus::kProcessing || |
| upload_status == FileUploadStatus::kUploadStarted || |
| upload_status == FileUploadStatus::kUploadSuccessful; |
| } |
| |
| } // namespace |
| |
| ComposeboxQueryController::ComposeboxQueryController( |
| signin::IdentityManager* identity_manager, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| version_info::Channel channel, |
| std::string locale, |
| TemplateURLService* template_url_service, |
| variations::VariationsClient* variations_client, |
| bool send_lns_surface, |
| bool enable_multi_context_input_flow, |
| bool enable_viewport_images) |
| : identity_manager_(identity_manager), |
| url_loader_factory_(url_loader_factory), |
| channel_(channel), |
| locale_(locale), |
| template_url_service_(template_url_service), |
| variations_client_(variations_client), |
| send_lns_surface_(send_lns_surface), |
| enable_multi_context_input_flow_(enable_multi_context_input_flow), |
| enable_viewport_images_(enable_viewport_images) { |
| create_request_task_runner_ = base::ThreadPool::CreateTaskRunner( |
| {base::TaskPriority::USER_VISIBLE, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); |
| } |
| |
| ComposeboxQueryController::~ComposeboxQueryController() = default; |
| |
| void ComposeboxQueryController::NotifySessionStarted() { |
| FetchClusterInfo(); |
| } |
| |
| void ComposeboxQueryController::NotifySessionAbandoned() { |
| ClearFiles(); |
| ClearClusterInfo(); |
| SetQueryControllerState(QueryControllerState::kOff); |
| session_id_++; |
| } |
| |
| std::unique_ptr<lens::LensOverlayRequestId> |
| ComposeboxQueryController::GetNextRequestId( |
| lens::RequestIdUpdateMode update_mode, |
| lens::MimeType mime_type, |
| lens::LensOverlayRequestId_MediaType media_type) { |
| std::unique_ptr<lens::LensOverlayRequestId> request_id = |
| request_id_generator_.GetNextRequestId(update_mode, media_type); |
| |
| suggest_inputs_.set_encoded_request_id( |
| lens::Base64EncodeRequestId(*request_id)); |
| if (!base::Contains(lens::kUnsupportedVitMimeTypes, mime_type)) { |
| // TODO(crbug.com/445777189): Support multi-context input id flow for |
| // suggest. |
| suggest_inputs_.set_contextual_visual_input_type( |
| lens::VitQueryParamValueForMimeType(mime_type)); |
| } |
| if (cluster_info_.has_value()) { |
| suggest_inputs_.set_search_session_id(cluster_info_->search_session_id()); |
| suggest_inputs_.set_send_gsession_vsrid_for_contextual_suggest(true); |
| } else { |
| suggest_inputs_.clear_search_session_id(); |
| } |
| |
| return request_id; |
| } |
| |
| GURL ComposeboxQueryController::CreateSearchUrl( |
| std::unique_ptr<CreateSearchUrlRequestInfo> search_url_request_info) { |
| num_files_in_request_ = 0; |
| if (!active_files_.empty() && cluster_info_.has_value()) { |
| if (enable_multi_context_input_flow_) { |
| std::unique_ptr<lens::LensOverlayContextualInputs> contextual_inputs = |
| std::make_unique<lens::LensOverlayContextualInputs>(); |
| for (const auto& [file_token, file_info] : active_files_) { |
| if (IsValidFileUploadStatusForMultimodalRequest( |
| file_info->upload_status_)) { |
| num_files_in_request_++; |
| auto* contextual_input = contextual_inputs->add_inputs(); |
| contextual_input->mutable_request_id()->CopyFrom( |
| *file_info->request_id_); |
| } |
| } |
| return GetUrlForMultimodalAim( |
| template_url_service_, |
| omnibox::DESKTOP_CHROME_NTP_REALBOX_ENTRY_POINT, |
| search_url_request_info->query_start_time, |
| cluster_info_->search_session_id(), std::move(contextual_inputs), |
| send_lns_surface_ ? kLnsSurfaceParameterValue : std::string(), |
| base::UTF8ToUTF16(search_url_request_info->query_text), |
| std::move(search_url_request_info->additional_params)); |
| } else { |
| // When multi-context input flow is not enabled, only one file is |
| // supported. |
| // Use the last file uploaded to determine `vit` param. |
| // TODO(crbug.com/446972028): Remove this once multi-context input flow is |
| // fully supported. |
| const std::unique_ptr<FileInfo>& last_file = |
| active_files_.rbegin()->second; |
| if (IsValidFileUploadStatusForMultimodalRequest( |
| last_file->upload_status_)) { |
| num_files_in_request_ = 1; |
| return GetUrlForMultimodalAim( |
| template_url_service_, |
| omnibox::DESKTOP_CHROME_NTP_REALBOX_ENTRY_POINT, |
| search_url_request_info->query_start_time, |
| cluster_info_->search_session_id(), |
| GetNextRequestId(lens::RequestIdUpdateMode::kSearchUrl, |
| last_file->mime_type_, |
| last_file->request_id_->media_type()), |
| last_file->mime_type_, |
| send_lns_surface_ ? kLnsSurfaceParameterValue : std::string(), |
| base::UTF8ToUTF16(search_url_request_info->query_text), |
| std::move(search_url_request_info->additional_params)); |
| } |
| } |
| } |
| // Treat queries in which the cluster info has expired, or the last file is |
| // not valid, as unimodal text queries. |
| // TODO(crbug.com/432125987): Handle file reupload after cluster info |
| // expiration. |
| return GetUrlForAim(template_url_service_, |
| omnibox::DESKTOP_CHROME_NTP_REALBOX_ENTRY_POINT, |
| search_url_request_info->query_start_time, |
| base::UTF8ToUTF16(search_url_request_info->query_text), |
| std::move(search_url_request_info->additional_params)); |
| } |
| |
| void ComposeboxQueryController::AddObserver(FileUploadStatusObserver* obs) { |
| observers_.AddObserver(obs); |
| } |
| |
| void ComposeboxQueryController::RemoveObserver(FileUploadStatusObserver* obs) { |
| observers_.RemoveObserver(obs); |
| } |
| |
| void ComposeboxQueryController::StartFileUploadFlow( |
| const base::UnguessableToken& file_token, |
| std::unique_ptr<lens::ContextualInputData> contextual_input_data, |
| std::optional<lens::ImageEncodingOptions> image_options) { |
| // Create a file info struct to hold the file upload data. |
| auto file_info = std::make_unique<FileInfo>(); |
| file_info->file_token_ = file_token; |
| file_info->mime_type_ = contextual_input_data->primary_content_type.value(); |
| file_info->upload_status_ = FileUploadStatus::kNotUploaded; |
| |
| auto [it, inserted] = active_files_.emplace(file_token, std::move(file_info)); |
| DCHECK(inserted); |
| FileInfo& current_file_info = *it->second; |
| |
| #if BUILDFLAG(IS_IOS) |
| bool has_viewport_screenshot = |
| enable_viewport_images_ && |
| contextual_input_data->viewport_screenshot_bytes.has_value(); |
| #else |
| bool has_viewport_screenshot = |
| enable_viewport_images_ && |
| contextual_input_data->viewport_screenshot.has_value(); |
| #endif // BUILDFLAG(IS_IOS) |
| // Unlike image uploads, PDF / page content uploads need to increment the |
| // long context id instead of the image sequence id. |
| current_file_info.request_id_ = GetNextRequestId( |
| enable_multi_context_input_flow_ |
| ? lens::RequestIdUpdateMode::kMultiContextUploadRequest |
| : (current_file_info.mime_type_ == lens::MimeType::kImage |
| ? lens::RequestIdUpdateMode::kFullImageRequest |
| : (has_viewport_screenshot |
| ? lens::RequestIdUpdateMode:: |
| kPageContentWithViewportRequest |
| : lens::RequestIdUpdateMode::kPageContentRequest)), |
| current_file_info.mime_type_, |
| lens::MimeTypeToMediaType(current_file_info.mime_type_, |
| has_viewport_screenshot)); |
| |
| // Update the file upload status to processing. This will notify the UI |
| // to fetch suggestions at the earliest possible time. The suggest inputs are |
| // set by the previous GetNextRequestId call. If the file upload later fails |
| // due to validation failures, the suggest response will be empty so it is |
| // safe to kick off the suggestions fetch at this point. |
| UpdateFileUploadStatus(file_token, FileUploadStatus::kProcessing, |
| std::nullopt); |
| |
| // If the is_page_context_eligible is set to false, then fail early. |
| if (contextual_input_data->is_page_context_eligible.has_value() && |
| !contextual_input_data->is_page_context_eligible.value()) { |
| // TODO(crbug.com/444276947): Consider adding a new error type for this. |
| UpdateFileUploadStatus( |
| file_token, FileUploadStatus::kValidationFailed, |
| composebox_query::mojom::FileUploadErrorType::kBrowserProcessingError); |
| return; |
| } |
| |
| // Preparing for the upload requests require multiple async flows to |
| // complete before the request is ready to be send to the server. Start the |
| // required flows here, and each flow completes by calling the ready method, |
| // i.e., OnUploadRequestBodyReady(). The ready method will handle waiting |
| // for all the necessary flows to complete before performing the request. |
| // Async Flow 1: Fetching the cluster info, which is shared across all |
| // requests. This flow only occurs once per session and occurs in |
| // NotifySessionStarted(). |
| // Async Flow 2: Retrieve the OAuth headers. |
| current_file_info.file_upload_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| &ComposeboxQueryController::OnUploadRequestHeadersReady, |
| weak_ptr_factory_.GetWeakPtr(), file_token)); |
| |
| // Async Flow 3: Creating the file and viewport upload request. |
| CreateUploadRequestBodiesAndContinue( |
| file_token, std::move(contextual_input_data), image_options); |
| } |
| |
| void ComposeboxQueryController::ClearSuggestInputs() { |
| // Multiple file upload is not supported yet, once it is, the suggest |
| // inputs should instead be updated to reflect this file being deleted. |
| // Suggest inputs must be cleared so when autocomplete is queried again |
| // in the UI, contextual suggestions do not appear. |
| suggest_inputs_.Clear(); |
| } |
| |
| bool ComposeboxQueryController::DeleteFile( |
| const base::UnguessableToken& file_token) { |
| ClearSuggestInputs(); |
| return !!active_files_.erase(file_token); |
| } |
| |
| void ComposeboxQueryController::ClearFiles() { |
| active_files_.clear(); |
| suggest_inputs_.Clear(); |
| } |
| |
| // static |
| void ComposeboxQueryController:: |
| CreateFileUploadRequestProtoWithImageDataAndContinue( |
| lens::LensOverlayRequestId request_id, |
| lens::LensOverlayClientContext client_context, |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> client_logs, |
| RequestBodyProtoCreatedCallback callback, |
| lens::ImageData image_data) { |
| lens::LensOverlayServerRequest request; |
| auto* objects_request = request.mutable_objects_request(); |
| objects_request->mutable_request_context()->mutable_request_id()->CopyFrom( |
| request_id); |
| objects_request->mutable_request_context() |
| ->mutable_client_context() |
| ->CopyFrom(client_context); |
| objects_request->mutable_image_data()->CopyFrom(image_data); |
| request.mutable_client_logs()->CopyFrom(client_logs->client_logs()); |
| std::move(callback).Run(std::move(request), /*error_type=*/std::nullopt); |
| } |
| |
| std::unique_ptr<EndpointFetcher> |
| ComposeboxQueryController::CreateEndpointFetcher( |
| std::string request_string, |
| const GURL& fetch_url, |
| HttpMethod http_method, |
| base::TimeDelta timeout, |
| const std::vector<std::string>& request_headers, |
| const std::vector<std::string>& cors_exempt_headers, |
| UploadProgressCallback upload_progress_callback) { |
| return std::make_unique<EndpointFetcher>( |
| url_loader_factory_, /*identity_manager=*/nullptr, |
| EndpointFetcher::RequestParams::Builder(http_method, |
| kTrafficAnnotationTag) |
| .SetAuthType(endpoint_fetcher::CHROME_API_KEY) |
| .SetChannel(channel_) |
| .SetContentType(kContentType) |
| .SetCorsExemptHeaders(cors_exempt_headers) |
| .SetCredentialsMode(CredentialsMode::kInclude) |
| .SetHeaders(request_headers) |
| .SetPostData(std::move(request_string)) |
| .SetSetSiteForCookies(true) |
| .SetTimeout(timeout) |
| .SetUploadProgressCallback(std::move(upload_progress_callback)) |
| .SetUrl(fetch_url) |
| .Build()); |
| } |
| |
| lens::LensOverlayClientContext ComposeboxQueryController::CreateClientContext() |
| const { |
| lens::LensOverlayClientContext context; |
| context.set_surface(lens::SURFACE_CHROME_NTP); |
| context.set_platform(lens::PLATFORM_LENS_OVERLAY); |
| context.mutable_client_filters()->add_filter()->set_filter_type( |
| lens::AUTO_FILTER); |
| context.mutable_locale_context()->set_language(locale_); |
| context.mutable_locale_context()->set_region( |
| icu::Locale(locale_.c_str()).getCountry()); |
| |
| std::unique_ptr<icu::TimeZone> zone(icu::TimeZone::createDefault()); |
| icu::UnicodeString time_zone_id, time_zone_canonical_id; |
| zone->getID(time_zone_id); |
| UErrorCode status = U_ZERO_ERROR; |
| icu::TimeZone::getCanonicalID(time_zone_id, time_zone_canonical_id, status); |
| if (status == U_ZERO_ERROR) { |
| std::string zone_id_str; |
| time_zone_canonical_id.toUTF8String(zone_id_str); |
| context.mutable_locale_context()->set_time_zone(zone_id_str); |
| } |
| |
| return context; |
| } |
| |
| // TODO(crbug.com/424869589): Clean up code duplication with |
| // LensOverlayQueryController. |
| std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> |
| ComposeboxQueryController::CreateOAuthHeadersAndContinue( |
| OAuthHeadersCreatedCallback callback) { |
| // Use OAuth if the user is logged in. |
| if (identity_manager_ && |
| identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) { |
| signin::AccessTokenFetcher::TokenCallback token_callback = |
| base::BindOnce(&lens::CreateOAuthHeader).Then(std::move(callback)); |
| signin::ScopeSet oauth_scopes; |
| oauth_scopes.insert(GaiaConstants::kLensOAuth2Scope); |
| return std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| signin::OAuthConsumerId::kComposeboxQueryController, identity_manager_, |
| std::move(token_callback), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable, |
| signin::ConsentLevel::kSignin); |
| } |
| |
| // Fall back to fetching the endpoint directly using API key. |
| std::move(callback).Run(std::vector<std::string>()); |
| return nullptr; |
| } |
| |
| void ComposeboxQueryController::ClearClusterInfo() { |
| cluster_info_access_token_fetcher_.reset(); |
| cluster_info_endpoint_fetcher_.reset(); |
| cluster_info_.reset(); |
| request_id_generator_.ResetRequestId(); |
| num_files_in_request_ = 0; |
| suggest_inputs_.Clear(); |
| } |
| |
| void ComposeboxQueryController::ResetRequestClusterInfoState(int session_id) { |
| if (session_id != session_id_) { |
| // The session associated with this timer has been invalidated. |
| return; |
| } |
| ClearClusterInfo(); |
| // Iterate through any existing files and mark them as expired. |
| // TODO(crbug.com/432125987): Handle file reupload after cluster info |
| // expiration. |
| std::vector<base::UnguessableToken> file_tokens_to_expire; |
| for (const auto& [file_token, file_info] : active_files_) { |
| file_tokens_to_expire.push_back(file_token); |
| } |
| |
| for (const auto& file_token : file_tokens_to_expire) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| continue; |
| } |
| // Stop the upload requests if they are in progress. |
| for (const auto& upload_request : file_info->upload_requests_) { |
| if (upload_request->endpoint_fetcher_) { |
| upload_request->endpoint_fetcher_.reset(); |
| } |
| } |
| if (file_info->upload_status_ != FileUploadStatus::kValidationFailed) { |
| UpdateFileUploadStatus(file_token, FileUploadStatus::kUploadExpired, |
| std::nullopt); |
| } |
| } |
| SetQueryControllerState(QueryControllerState::kClusterInfoInvalid); |
| |
| // Fetch new cluster info. |
| FetchClusterInfo(); |
| } |
| |
| void ComposeboxQueryController::FetchClusterInfo() { |
| SetQueryControllerState(QueryControllerState::kAwaitingClusterInfoResponse); |
| |
| // There should not be any in-flight cluster info access token request. |
| CHECK(!cluster_info_access_token_fetcher_); |
| cluster_info_access_token_fetcher_ = CreateOAuthHeadersAndContinue( |
| base::BindOnce(&ComposeboxQueryController::SendClusterInfoNetworkRequest, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ComposeboxQueryController::SendClusterInfoNetworkRequest( |
| std::vector<std::string> request_headers) { |
| cluster_info_access_token_fetcher_.reset(); |
| |
| // Add protobuf content type to the request headers. |
| request_headers.push_back(kContentTypeKey); |
| request_headers.push_back(kContentType); |
| |
| // Get client experiment variations to include in the request. |
| std::vector<std::string> cors_exempt_headers = |
| lens::CreateVariationsHeaders(variations_client_); |
| |
| // Generate the URL to fetch. |
| GURL fetch_url = GURL(lens::features::GetLensOverlayClusterInfoEndpointUrl()); |
| |
| std::string request_string; |
| // Create the client context to include in the request. |
| lens::LensOverlayClientContext client_context = CreateClientContext(); |
| lens::LensOverlayServerClusterInfoRequest request; |
| request.set_surface(client_context.surface()); |
| request.set_platform(client_context.platform()); |
| CHECK(request.SerializeToString(&request_string)); |
| |
| // Create the EndpointFetcher, responsible for making the request using our |
| // given params. Store in class variable to keep endpoint fetcher alive until |
| // the request is made. |
| cluster_info_endpoint_fetcher_ = CreateEndpointFetcher( |
| std::move(request_string), fetch_url, HttpMethod::kPost, |
| base::Milliseconds(lens::features::GetLensOverlayServerRequestTimeout()), |
| request_headers, cors_exempt_headers, base::DoNothing()); |
| |
| // Finally, perform the request. |
| cluster_info_endpoint_fetcher_->PerformRequest( |
| base::BindOnce(&ComposeboxQueryController::HandleClusterInfoResponse, |
| weak_ptr_factory_.GetWeakPtr()), |
| google_apis::GetAPIKey().c_str()); |
| } |
| |
| void ComposeboxQueryController::HandleClusterInfoResponse( |
| std::unique_ptr<endpoint_fetcher::EndpointResponse> response) { |
| cluster_info_endpoint_fetcher_.reset(); |
| if (response->http_status_code != google_apis::ApiErrorCode::HTTP_SUCCESS) { |
| SetQueryControllerState(QueryControllerState::kClusterInfoInvalid); |
| return; |
| } |
| |
| lens::LensOverlayServerClusterInfoResponse server_response; |
| if (!server_response.ParseFromString(response->response)) { |
| SetQueryControllerState(QueryControllerState::kClusterInfoInvalid); |
| return; |
| } |
| |
| // Store the cluster info. |
| cluster_info_ = std::make_optional<lens::LensOverlayClusterInfo>(); |
| cluster_info_->set_server_session_id(server_response.server_session_id()); |
| cluster_info_->set_search_session_id(server_response.search_session_id()); |
| if (server_response.has_routing_info() && |
| !request_id_generator_.HasRoutingInfo()) { |
| std::unique_ptr<lens::LensOverlayRequestId> request_id = |
| request_id_generator_.SetRoutingInfo(server_response.routing_info()); |
| } |
| SetQueryControllerState(QueryControllerState::kClusterInfoReceived); |
| |
| // Iterate through any existing files and send the upload requests if ready. |
| for (const auto& [file_token, file_info] : active_files_) { |
| for (size_t i = 0; i < file_info->upload_requests_.size(); ++i) { |
| if (file_info->upload_requests_[i]->request_body) { |
| SendUploadNetworkRequest(file_info.get(), i); |
| } |
| } |
| } |
| |
| // Clear the cluster info after its lifetime expires. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&ComposeboxQueryController::ResetRequestClusterInfoState, |
| weak_ptr_factory_.GetWeakPtr(), session_id_), |
| base::Seconds( |
| lens::features::GetLensOverlayClusterInfoLifetimeSeconds())); |
| } |
| |
| void ComposeboxQueryController::SetQueryControllerState( |
| QueryControllerState new_state) { |
| if (query_controller_state_ != new_state) { |
| query_controller_state_ = new_state; |
| if (on_query_controller_state_changed_callback_) { |
| on_query_controller_state_changed_callback_.Run(new_state); |
| } |
| } |
| } |
| |
| void ComposeboxQueryController::UpdateFileUploadStatus( |
| const base::UnguessableToken& file_token, |
| FileUploadStatus status, |
| std::optional<FileUploadErrorType> error_type) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| for (auto& observer : observers_) { |
| observer.OnFileUploadStatusChanged(file_token, file_info->mime_type_, |
| status, error_type); |
| } |
| if (!IsValidFileUploadStatusForMultimodalRequest(status)) { |
| active_files_.erase(file_token); |
| } else { |
| file_info->upload_status_ = status; |
| } |
| } |
| |
| #if !BUILDFLAG(IS_IOS) |
| void ComposeboxQueryController::ProcessDecodedImageAndContinue( |
| lens::LensOverlayRequestId request_id, |
| const lens::ImageEncodingOptions& image_options, |
| RequestBodyProtoCreatedCallback callback, |
| const SkBitmap& bitmap) { |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> ref_counted_logs = |
| base::MakeRefCounted<lens::RefCountedLensOverlayClientLogs>(); |
| if (bitmap.isNull() || bitmap.empty()) { |
| std::move(callback).Run(lens::LensOverlayServerRequest(), |
| FileUploadErrorType::kImageProcessingError); |
| return; |
| } |
| |
| // If the bitmap is a viewport bitmap, it will be destroyed after the |
| // owning ContextualInputData is destroyed (i.e. at the end of |
| // CreateUploadRequestBodiesAndContinue). To ensure the bitmap is not |
| // destroyed before it is used, make a copy of the bitmap. |
| SkBitmap bitmap_copy = bitmap; |
| |
| // Downscaling and encoding is done on a background thread to avoid blocking |
| // the main thread. |
| create_request_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&lens::DownscaleAndEncodeBitmap, std::move(bitmap_copy), |
| ref_counted_logs, image_options), |
| base::BindOnce(&ComposeboxQueryController:: |
| CreateFileUploadRequestProtoWithImageDataAndContinue, |
| request_id, CreateClientContext(), ref_counted_logs, |
| std::move(callback))); |
| } |
| #endif // !BUILDFLAG(IS_IOS) |
| |
| void ComposeboxQueryController::CreateImageUploadRequest( |
| const base::UnguessableToken& file_token, |
| const std::vector<uint8_t>& image_data, |
| std::optional<lens::ImageEncodingOptions> image_options, |
| RequestBodyProtoCreatedCallback callback) { |
| #if !BUILDFLAG(IS_IOS) |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| CHECK(image_options.has_value()); |
| data_decoder::DecodeImageIsolated( |
| image_data, data_decoder::mojom::ImageCodec::kDefault, |
| /*shrink_to_fit=*/false, |
| /*max_size_in_bytes=*/std::numeric_limits<int64_t>::max(), |
| /*desired_image_frame_size=*/gfx::Size(), |
| base::BindOnce(&ComposeboxQueryController::ProcessDecodedImageAndContinue, |
| weak_ptr_factory_.GetWeakPtr(), *file_info->request_id_, |
| image_options.value(), std::move(callback))); |
| #endif // !BUILDFLAG(IS_IOS) |
| } |
| |
| void ComposeboxQueryController::CreateUploadRequestBodiesAndContinue( |
| const base::UnguessableToken& file_token, |
| std::unique_ptr<lens::ContextualInputData> contextual_input_data, |
| std::optional<lens::ImageEncodingOptions> image_options) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| // If there is a viewport screenshot, create the viewport upload request body. |
| // TODO(crbug.com/442685171): Pass the pdf page number to the viewport |
| // upload request if available. |
| #if BUILDFLAG(IS_IOS) |
| if (enable_viewport_images_ && |
| contextual_input_data->viewport_screenshot_bytes.has_value()) { |
| CHECK(image_options.has_value()); |
| CreateImageUploadRequest( |
| file_token, |
| // Pass ownership of the viewport screenshot bytes to the callback. |
| std::move(contextual_input_data->viewport_screenshot_bytes.value()), |
| std::move(image_options), |
| base::BindOnce( |
| &ComposeboxQueryController:: |
| AddPageIndexToImageUploadRequestAndContinue, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(contextual_input_data->pdf_current_page), |
| base::BindOnce(&ComposeboxQueryController::OnUploadRequestBodyReady, |
| weak_ptr_factory_.GetWeakPtr(), file_token, |
| file_info->num_outstanding_network_requests_++))); |
| } |
| #else |
| if (enable_viewport_images_ && |
| contextual_input_data->viewport_screenshot.has_value()) { |
| CHECK(image_options.has_value()); |
| ProcessDecodedImageAndContinue( |
| *file_info->request_id_, image_options.value(), |
| base::BindOnce( |
| &ComposeboxQueryController:: |
| AddPageIndexToImageUploadRequestAndContinue, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(contextual_input_data->pdf_current_page), |
| base::BindOnce(&ComposeboxQueryController::OnUploadRequestBodyReady, |
| weak_ptr_factory_.GetWeakPtr(), file_token, |
| file_info->num_outstanding_network_requests_++)), |
| // Pass ownership of the viewport screenshot to the |
| // callback. |
| std::move(*contextual_input_data->viewport_screenshot)); |
| } |
| #endif // !BUILDFLAG(IS_IOS) |
| |
| switch (file_info->mime_type_) { |
| case lens::MimeType::kPdf: |
| [[fallthrough]]; |
| case lens::MimeType::kAnnotatedPageContent: |
| CHECK(contextual_input_data->context_input.has_value() && |
| contextual_input_data->context_input->size() > 0); |
| // Call CreateContentextualDataUploadPayload off the main thread to avoid |
| // blocking the main thread on compression. |
| create_request_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| &CreateContentextualDataUploadPayload, |
| // Pass ownership of the contextual input data to the callback. |
| std::move(contextual_input_data->context_input.value()), |
| contextual_input_data->page_url, |
| contextual_input_data->page_title), |
| base::BindOnce( |
| &CreateFileUploadRequestProtoWithPayloadAndContinue, |
| *file_info->request_id_, CreateClientContext(), |
| |
| base::BindOnce( |
| &ComposeboxQueryController::OnUploadRequestBodyReady, |
| weak_ptr_factory_.GetWeakPtr(), file_token, |
| file_info->num_outstanding_network_requests_++))); |
| break; |
| case lens::MimeType::kImage: |
| CHECK(contextual_input_data->context_input.has_value() && |
| contextual_input_data->context_input->size() == 1); |
| // TODO(crbug.com/441142455): Support image context via SkBitmap. |
| CreateImageUploadRequest( |
| file_token, |
| // Pass ownership of the contextual input data to the callback. |
| std::move(contextual_input_data->context_input->front().bytes_), |
| std::move(image_options), |
| base::BindOnce(&ComposeboxQueryController::OnUploadRequestBodyReady, |
| weak_ptr_factory_.GetWeakPtr(), file_token, |
| file_info->num_outstanding_network_requests_++)); |
| break; |
| default: |
| UpdateFileUploadStatus(file_info->file_token_, |
| FileUploadStatus::kValidationFailed, |
| FileUploadErrorType::kBrowserProcessingError); |
| break; |
| } |
| } |
| |
| void ComposeboxQueryController::AddPageIndexToImageUploadRequestAndContinue( |
| std::optional<size_t> pdf_page_index, |
| RequestBodyProtoCreatedCallback callback, |
| lens::LensOverlayServerRequest request, |
| std::optional<FileUploadErrorType> error_type) { |
| if (!error_type.has_value() && pdf_page_index.has_value()) { |
| request.mutable_objects_request() |
| ->mutable_viewport_request_context() |
| ->set_pdf_page_number(pdf_page_index.value()); |
| } |
| |
| std::move(callback).Run(request, error_type); |
| } |
| |
| void ComposeboxQueryController::OnUploadRequestBodyReady( |
| const base::UnguessableToken& file_token, |
| size_t request_index, |
| lens::LensOverlayServerRequest request, |
| std::optional<FileUploadErrorType> error_type) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| if (error_type.has_value()) { |
| UpdateFileUploadStatus(file_info->file_token_, |
| FileUploadStatus::kValidationFailed, error_type); |
| return; |
| } |
| |
| // Create the upload requests if they haven't been created yet. |
| while (file_info->upload_requests_.size() <= request_index) { |
| file_info->upload_requests_.push_back(std::make_unique<UploadRequest>()); |
| } |
| file_info->upload_requests_[request_index]->request_body = |
| std::make_unique<lens::LensOverlayServerRequest>(request); |
| MaybeSendUploadNetworkRequest(file_token, request_index); |
| } |
| |
| void ComposeboxQueryController::OnUploadRequestHeadersReady( |
| const base::UnguessableToken& file_token, |
| std::vector<std::string> headers) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| file_info->file_upload_access_token_fetcher_.reset(); |
| file_info->request_headers_ = |
| std::make_unique<std::vector<std::string>>(headers); |
| for (size_t i = 0; i < file_info->upload_requests_.size(); ++i) { |
| MaybeSendUploadNetworkRequest(file_token, i); |
| } |
| } |
| |
| void ComposeboxQueryController::MaybeSendUploadNetworkRequest( |
| const base::UnguessableToken& file_token, |
| size_t request_index) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| CHECK_LT(request_index, file_info->upload_requests_.size()); |
| UploadRequest* upload_request = |
| file_info->upload_requests_[request_index].get(); |
| CHECK(upload_request); |
| // Check that the request is ready to be sent and has not yet been sent. |
| if (file_info->request_headers_ && upload_request->request_body && |
| upload_request->response_code == 0 && |
| !upload_request->endpoint_fetcher_ && cluster_info_.has_value()) { |
| SendUploadNetworkRequest(file_info, request_index); |
| } |
| } |
| |
| void ComposeboxQueryController::SendUploadNetworkRequest(FileInfo* file_info, |
| size_t request_index) { |
| CHECK_LT(request_index, file_info->upload_requests_.size()); |
| UploadRequest* upload_request = |
| file_info->upload_requests_[request_index].get(); |
| CHECK(upload_request); |
| CHECK(upload_request->request_body); |
| CHECK(file_info->request_headers_); |
| PerformFetchRequest( |
| upload_request->request_body.get(), file_info->request_headers_.get(), |
| base::Milliseconds( |
| lens::features::GetLensOverlayPageContentRequestTimeoutMs()), |
| base::BindOnce(&ComposeboxQueryController::OnUploadEndpointFetcherCreated, |
| weak_ptr_factory_.GetWeakPtr(), file_info->file_token_, |
| request_index), |
| base::BindOnce(&ComposeboxQueryController::HandleUploadResponse, |
| weak_ptr_factory_.GetWeakPtr(), file_info->file_token_, |
| request_index), |
| /*upload_progress_callback=*/base::DoNothing()); |
| } |
| |
| void ComposeboxQueryController::OnUploadEndpointFetcherCreated( |
| const base::UnguessableToken& file_token, |
| size_t request_index, |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| CHECK_LT(request_index, file_info->upload_requests_.size()); |
| UploadRequest* upload_request = |
| file_info->upload_requests_[request_index].get(); |
| CHECK(upload_request); |
| |
| upload_request->start_time = base::Time::Now(); |
| upload_request->endpoint_fetcher_ = std::move(endpoint_fetcher); |
| if (file_info->upload_status_ == FileUploadStatus::kProcessing) { |
| UpdateFileUploadStatus(file_info->file_token_, |
| FileUploadStatus::kUploadStarted, std::nullopt); |
| } |
| } |
| |
| void ComposeboxQueryController::HandleUploadResponse( |
| const base::UnguessableToken& file_token, |
| size_t request_index, |
| std::unique_ptr<EndpointResponse> response) { |
| FileInfo* file_info = GetFileInfo(file_token); |
| if (!file_info) { |
| return; |
| } |
| |
| file_info->num_outstanding_network_requests_--; |
| |
| CHECK_LT(request_index, file_info->upload_requests_.size()); |
| UploadRequest* upload_request = |
| file_info->upload_requests_[request_index].get(); |
| CHECK(upload_request); |
| |
| upload_request->response_time = base::Time::Now(); |
| upload_request->response_code = response->http_status_code; |
| upload_request->endpoint_fetcher_.reset(); |
| |
| if (response->http_status_code != google_apis::ApiErrorCode::HTTP_SUCCESS) { |
| file_info->upload_error_type_ = FileUploadErrorType::kServerError; |
| UpdateFileUploadStatus(file_token, FileUploadStatus::kUploadFailed, |
| FileUploadErrorType::kServerError); |
| return; |
| } |
| |
| // If the file was still uploading and there are no more outstanding network |
| // requests, update the file upload status to successful. The upload status |
| // would have been set to ServerError if the response code for any prior |
| // request was not successful. |
| if (file_info->upload_status_ == FileUploadStatus::kUploadStarted && |
| file_info->num_outstanding_network_requests_ == 0) { |
| UpdateFileUploadStatus(file_token, FileUploadStatus::kUploadSuccessful, |
| std::nullopt); |
| } |
| } |
| |
| void ComposeboxQueryController::PerformFetchRequest( |
| lens::LensOverlayServerRequest* request, |
| std::vector<std::string>* request_headers, |
| base::TimeDelta timeout, |
| base::OnceCallback<void(std::unique_ptr<endpoint_fetcher::EndpointFetcher>)> |
| fetcher_created_callback, |
| endpoint_fetcher::EndpointFetcherCallback response_received_callback, |
| UploadProgressCallback upload_progress_callback) { |
| CHECK_EQ(query_controller_state_, QueryControllerState::kClusterInfoReceived); |
| CHECK(cluster_info_.has_value()); |
| |
| // Get client experiment variations to include in the request. |
| std::vector<std::string> cors_exempt_headers = |
| lens::CreateVariationsHeaders(variations_client_); |
| |
| // Generate the URL to fetch to and include the server session id if present. |
| GURL fetch_url = GURL(lens::features::GetLensOverlayEndpointURL()); |
| // The endpoint fetches should use the server session id from the cluster |
| // info. |
| fetch_url = |
| net::AppendOrReplaceQueryParameter(fetch_url, kSessionIdQueryParameterKey, |
| cluster_info_->server_session_id()); |
| |
| std::string request_string; |
| CHECK(request->SerializeToString(&request_string)); |
| |
| // Create the EndpointFetcher, responsible for making the request using our |
| // given params. |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher = CreateEndpointFetcher( |
| std::move(request_string), fetch_url, HttpMethod::kPost, |
| base::Milliseconds( |
| lens::features::GetLensOverlayPageContentRequestTimeoutMs()), |
| *request_headers, cors_exempt_headers, |
| std::move(upload_progress_callback)); |
| EndpointFetcher* fetcher = endpoint_fetcher.get(); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(fetcher_created_callback), |
| std::move(endpoint_fetcher))); |
| |
| // Finally, perform the request. |
| fetcher->PerformRequest(std::move(response_received_callback), |
| google_apis::GetAPIKey().c_str()); |
| } |
| |
| ComposeboxQueryController::FileInfo* ComposeboxQueryController::GetFileInfo( |
| const base::UnguessableToken& file_token) { |
| auto it = active_files_.find(file_token); |
| if (it == active_files_.end()) { |
| return nullptr; |
| } |
| return it->second.get(); |
| } |