blob: 131458db55718239772035ce04ae5cd7ff7acd0c [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 "components/omnibox/composebox/composebox_query_controller.h"
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#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/lens_features.h"
#include "components/lens/lens_request_construction.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_payload.pb.h"
#include "third_party/lens_server_proto/lens_overlay_platform.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 "components/omnibox/composebox/composebox_image_helper.h"
#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 kOAuthConsumerName[] = "ComposeboxQueryController";
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::FileInfo::FileInfo() = default;
ComposeboxQueryController::FileInfo::~FileInfo() = default;
namespace {
// Creates a pdf file upload request payload.
lens::Payload CreatePDFFileUploadPayload(
scoped_refptr<base::RefCountedBytes> file_data) {
lens::Payload payload;
auto* content = payload.mutable_content();
auto* content_data = content->add_content_data();
content_data->set_content_type(lens::ContentData::CONTENT_TYPE_PDF);
// TODO(crbug.com/427618282): Add compression for PDF bytes.
auto bytes = file_data->as_vector();
content_data->mutable_data()->assign(bytes.begin(), bytes.end());
return payload;
}
// Creates the server request proto for the pdf file 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)
: 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) {
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_++;
}
GURL ComposeboxQueryController::CreateAimUrl(const std::string& query_text,
base::Time query_start_time) {
num_files_in_request_ = 0;
if (!active_files_.empty() && cluster_info_.has_value()) {
// Since multiple file upload isn't supported right now, use the last file
// uploaded to determine `vit` param.
// TODO(crbug.com/428967670): Support multiple file upload.
const std::unique_ptr<FileInfo>& last_file = active_files_.rbegin()->second;
// TODO(crbug.com/428967670): Update `num_files_in_request_` when more than
// 1 file is supported.
num_files_in_request_ = 1;
if (IsValidFileUploadStatusForMultimodalRequest(
last_file->upload_status_)) {
std::unique_ptr<lens::LensOverlayRequestId> request_id =
request_id_generator_.GetNextRequestId(
lens::RequestIdUpdateMode::kSearchUrl);
request_id->set_media_type(last_file->request_id_->media_type());
return GetUrlForMultimodalAim(
template_url_service_,
omnibox::DESKTOP_CHROME_NTP_REALBOX_ENTRY_POINT, query_start_time,
cluster_info_->search_session_id(), std::move(request_id),
last_file->mime_type_,
send_lns_surface_ ? kLnsSurfaceParameterValue : std::string(),
base::UTF8ToUTF16(query_text));
}
}
// 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,
query_start_time, base::UTF8ToUTF16(query_text));
}
void ComposeboxQueryController::AddObserver(FileUploadStatusObserver* obs) {
observers_.AddObserver(obs);
}
void ComposeboxQueryController::RemoveObserver(FileUploadStatusObserver* obs) {
observers_.RemoveObserver(obs);
}
void ComposeboxQueryController::StartFileUploadFlow(
std::unique_ptr<FileInfo> file_info,
scoped_refptr<base::RefCountedBytes> file_data,
std::optional<composebox::ImageEncodingOptions> image_options) {
CHECK_EQ(file_info->upload_status_, FileUploadStatus::kNotUploaded);
const base::UnguessableToken& file_token = file_info->file_token_;
auto [it, inserted] = active_files_.emplace(file_token, std::move(file_info));
DCHECK(inserted);
FileInfo& current_file_info = *it->second;
UpdateFileUploadStatus(file_token, FileUploadStatus::kProcessing,
std::nullopt);
// Unlike image uploads,PDF uploads need to increment the long context id
// instead of the image sequence id.
current_file_info.request_id_ = request_id_generator_.GetNextRequestId(
current_file_info.mime_type_ == lens::MimeType::kPdf
? lens::RequestIdUpdateMode::kPageContentRequest
: lens::RequestIdUpdateMode::kFullImageRequest);
current_file_info.request_id_->set_media_type(
current_file_info.mime_type_ == lens::MimeType::kPdf
? lens::LensOverlayRequestId::MEDIA_TYPE_PDF
: lens::LensOverlayRequestId::MEDIA_TYPE_DEFAULT_IMAGE);
// Preparing for the file upload request requires 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., OnUploadFileRequestBodyReady(). 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.
// This flow only occurs once per session and occurs in
// NotifySessionStarted().
// Async Flow 2: Creating the file upload request.
CreateFileUploadRequestBodyAndContinue(
file_token, std::move(file_data), image_options,
base::BindOnce(&ComposeboxQueryController::OnUploadFileRequestBodyReady,
weak_ptr_factory_.GetWeakPtr(), file_token));
// Async Flow 3: Retrieve the OAuth headers.
current_file_info.file_upload_access_token_fetcher_ =
CreateOAuthHeadersAndContinue(base::BindOnce(
&ComposeboxQueryController::OnUploadFileRequestHeadersReady,
weak_ptr_factory_.GetWeakPtr(), file_token));
}
bool ComposeboxQueryController::DeleteFile(
const base::UnguessableToken& file_token) {
return !!active_files_.erase(file_token);
}
void ComposeboxQueryController::ClearFiles() {
active_files_.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=*/url_loader_factory_,
/*url=*/fetch_url,
/*content_type=*/kContentType,
/*timeout=*/timeout,
/*post_data=*/std::move(request_string),
/*headers=*/request_headers,
/*cors_exempt_headers=*/cors_exempt_headers,
/*channel=*/channel_,
/*request_params=*/
EndpointFetcher::RequestParams::Builder(http_method,
kTrafficAnnotationTag)
.SetCredentialsMode(CredentialsMode::kInclude)
.SetSetSiteForCookies(true)
.SetUploadProgressCallback(std::move(upload_progress_callback))
.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>(
kOAuthConsumerName, identity_manager_, oauth_scopes,
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;
}
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.
for (const auto& [file_token, file_info] : active_files_) {
// Stop the file upload request if it is in progress.
file_info->file_upload_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;
const std::string response_string = response->response;
bool parse_successful = server_response.ParseFromArray(
response_string.data(), response_string.size());
if (!parse_successful) {
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_) {
MaybeSendFileUploadNetworkRequest(file_token);
}
// 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;
}
file_info->upload_status_ = status;
for (auto& observer : observers_) {
observer.OnFileUploadStatusChanged(file_token, file_info->mime_type_,
status, error_type);
}
}
#if !BUILDFLAG(IS_IOS)
void ComposeboxQueryController::ProcessDecodedImageAndContinue(
lens::LensOverlayRequestId request_id,
const composebox::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;
}
// Downscaling and encoding is done on a background thread to avoid blocking
// the main thread.
create_request_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&composebox::DownscaleAndEncodeBitmap, bitmap,
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,
scoped_refptr<base::RefCountedBytes> file_data,
std::optional<composebox::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(
file_data->as_vector(), 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::CreateFileUploadRequestBodyAndContinue(
const base::UnguessableToken& file_token,
scoped_refptr<base::RefCountedBytes> file_data,
std::optional<composebox::ImageEncodingOptions> image_options,
RequestBodyProtoCreatedCallback callback) {
FileInfo* file_info = GetFileInfo(file_token);
if (!file_info) {
return;
}
switch (file_info->mime_type_) {
case lens::MimeType::kPdf:
// Call CreatePDFFileUploadPayload off the main thread to avoid blocking
// the main thread on compression.
create_request_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&CreatePDFFileUploadPayload, std::move(file_data)),
base::BindOnce(&CreateFileUploadRequestProtoWithPayloadAndContinue,
*file_info->request_id_, CreateClientContext(),
std::move(callback)));
break;
case lens::MimeType::kImage: {
CreateImageUploadRequest(file_token, std::move(file_data),
std::move(image_options), std::move(callback));
break;
}
default:
UpdateFileUploadStatus(file_info->file_token_,
FileUploadStatus::kValidationFailed,
FileUploadErrorType::kBrowserProcessingError);
break;
}
}
void ComposeboxQueryController::OnUploadFileRequestBodyReady(
const base::UnguessableToken& file_token,
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;
}
file_info->request_body_ =
std::make_unique<lens::LensOverlayServerRequest>(request);
MaybeSendFileUploadNetworkRequest(file_token);
}
void ComposeboxQueryController::OnUploadFileRequestHeadersReady(
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);
MaybeSendFileUploadNetworkRequest(file_token);
}
void ComposeboxQueryController::MaybeSendFileUploadNetworkRequest(
const base::UnguessableToken& file_token) {
FileInfo* file_info = GetFileInfo(file_token);
if (!file_info) {
return;
}
if (file_info->request_headers_ && file_info->request_body_ &&
cluster_info_.has_value() &&
file_info->upload_status_ == FileUploadStatus::kProcessing &&
query_controller_state_ == QueryControllerState::kClusterInfoReceived) {
SendFileUploadNetworkRequest(file_info);
}
}
void ComposeboxQueryController::SendFileUploadNetworkRequest(
FileInfo* file_info) {
CHECK(file_info->request_body_);
CHECK(file_info->request_headers_);
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(file_info->request_body_->SerializeToString(&request_string));
// Create the EndpointFetcher, responsible for making the request using our
// given params.
file_info->file_upload_endpoint_fetcher_ = CreateEndpointFetcher(
std::move(request_string), fetch_url, HttpMethod::kPost,
base::Milliseconds(
lens::features::GetLensOverlayPageContentRequestTimeoutMs()),
*file_info->request_headers_, cors_exempt_headers,
/*upload_progress_callback=*/base::DoNothing());
file_info->upload_network_request_start_time_ = base::Time::Now();
UpdateFileUploadStatus(file_info->file_token_,
FileUploadStatus::kUploadStarted, std::nullopt);
// Finally, perform the request.
file_info->file_upload_endpoint_fetcher_->PerformRequest(
base::BindOnce(&ComposeboxQueryController::HandleFileUploadResponse,
weak_ptr_factory_.GetWeakPtr(), file_info->file_token_),
google_apis::GetAPIKey().c_str());
}
void ComposeboxQueryController::HandleFileUploadResponse(
const base::UnguessableToken& file_token,
std::unique_ptr<EndpointResponse> response) {
FileInfo* file_info = GetFileInfo(file_token);
if (!file_info) {
return;
}
file_info->server_response_time_ = base::Time::Now();
file_info->response_code_ = response->http_status_code;
file_info->file_upload_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;
}
UpdateFileUploadStatus(file_token, FileUploadStatus::kUploadSuccessful,
std::nullopt);
}
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();
}