| // Copyright 2024 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_overlay_query_controller.h" |
| |
| #include <optional> |
| |
| #include "base/base64url.h" |
| #include "base/containers/span.h" |
| #include "base/containers/span_reader.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/notreached.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.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 "chrome/browser/browser_process.h" |
| #include "chrome/browser/lens/core/mojom/geometry.mojom.h" |
| #include "chrome/browser/lens/core/mojom/overlay_object.mojom-forward.h" |
| #include "chrome/browser/lens/core/mojom/text.mojom-forward.h" |
| #include "chrome/browser/lens/core/mojom/text.mojom.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/lens/lens_overlay_gen204_controller.h" |
| #include "chrome/browser/ui/lens/lens_overlay_image_helper.h" |
| #include "chrome/browser/ui/lens/lens_overlay_proto_converter.h" |
| #include "chrome/browser/ui/lens/lens_overlay_url_builder.h" |
| #include "chrome/browser/ui/lens/lens_search_feature_flag_utils.h" |
| #include "chrome/common/channel_info.h" |
| #include "components/base32/base32.h" |
| #include "components/endpoint_fetcher/endpoint_fetcher.h" |
| #include "components/lens/lens_features.h" |
| #include "components/lens/lens_overlay_mime_type.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/proto/server/lens_overlay_response.pb.h" |
| #include "components/lens/ref_counted_lens_overlay_client_logs.h" |
| #include "components/metrics_services_manager/metrics_services_manager.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/version_info/channel.h" |
| #include "google_apis/common/api_error_codes.h" |
| #include "google_apis/gaia/gaia_constants.h" |
| #include "google_apis/gaia/gaia_urls.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 "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.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_client_platform.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_document.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_filters.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_platform.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_polygon.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_request_id.pb.h" |
| #include "third_party/lens_server_proto/lens_overlay_request_type.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/lens_server_proto/lens_overlay_visual_search_interaction_data.pb.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| using endpoint_fetcher::CredentialsMode; |
| using endpoint_fetcher::EndpointFetcher; |
| using endpoint_fetcher::EndpointFetcherCallback; |
| using endpoint_fetcher::EndpointResponse; |
| using endpoint_fetcher::HttpMethod; |
| |
| namespace lens { |
| |
| using LatencyType = LensOverlayGen204Controller::LatencyType; |
| |
| namespace { |
| |
| // The name string for the header for variations information. |
| constexpr char kContentTypeKey[] = "Content-Type"; |
| constexpr char kContentType[] = "application/x-protobuf"; |
| constexpr char kSessionIdQueryParameterKey[] = "gsessionid"; |
| constexpr char kGen204IdentifierQueryParameter[] = "plla"; |
| constexpr char kVisualSearchInteractionDataQueryParameterKey[] = "vsint"; |
| constexpr char kVisualInputTypeQueryParameterKey[] = "vit"; |
| |
| constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotationTag = |
| net::DefineNetworkTrafficAnnotation("lens_overlay", R"( |
| semantics { |
| sender: "Lens" |
| description: "A request to the service handling the Lens " |
| "Overlay feature in Chrome." |
| trigger: "The user triggered a Lens Overlay Flow by entering " |
| "the experience via the right click menu option for " |
| "searching images on the page." |
| data: "If the contextual searchbox is enabled, the viewport " |
| "screenshot, page content, page URL and user interaction data are " |
| "sent to the server. Page content refers to the page the user is " |
| "on, extracted, and sent to the server as bytes. If the contextual " |
| "searchbox is disabled, only the screenshot of the current webpage " |
| "viewport (image bytes) and user interaction data (coordinates of " |
| "a box within the screenshot or tapped object-id) are 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: "2024-11-06" |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: "This feature is only shown in menus by default and does " |
| "nothing without explicit user action, so there is no setting to " |
| "disable the feature." |
| chrome_policy { |
| LensOverlaySettings { |
| LensOverlaySettings: 1 |
| } |
| } |
| } |
| )"); |
| |
| // Creates a request id with an invalid (negative) sequence id. This is used to |
| // indicate that the request id is invalid, e.g. to indicate that there is no |
| // valid interaction data associated with a text-only query in |
| // IsCurrentInteractionSequence. |
| std::unique_ptr<lens::LensOverlayRequestId> CreateInvalidRequestId() { |
| auto request_id = std::make_unique<lens::LensOverlayRequestId>(); |
| request_id->set_sequence_id(-1); |
| return request_id; |
| } |
| |
| lens::CoordinateType ConvertToServerCoordinateType( |
| lens::mojom::CenterRotatedBox_CoordinateType type) { |
| switch (type) { |
| case lens::mojom::CenterRotatedBox_CoordinateType::kNormalized: |
| return lens::CoordinateType::NORMALIZED; |
| case lens::mojom::CenterRotatedBox_CoordinateType::kImage: |
| return lens::CoordinateType::IMAGE; |
| case lens::mojom::CenterRotatedBox_CoordinateType::kUnspecified: |
| return lens::CoordinateType::COORDINATE_TYPE_UNSPECIFIED; |
| } |
| } |
| |
| lens::CenterRotatedBox ConvertToServerCenterRotatedBox( |
| lens::mojom::CenterRotatedBoxPtr box) { |
| lens::CenterRotatedBox out_box; |
| out_box.set_center_x(box->box.x()); |
| out_box.set_center_y(box->box.y()); |
| out_box.set_width(box->box.width()); |
| out_box.set_height(box->box.height()); |
| out_box.set_coordinate_type( |
| ConvertToServerCoordinateType(box->coordinate_type)); |
| return out_box; |
| } |
| |
| std::map<std::string, std::string> AddVisualInputTypeQueryParam( |
| std::map<std::string, std::string> additional_search_query_params, |
| lens::MimeType content_type) { |
| std::string vitValue = lens::VitQueryParamValueForMimeType(content_type); |
| additional_search_query_params.insert( |
| {kVisualInputTypeQueryParameterKey, vitValue}); |
| return additional_search_query_params; |
| } |
| |
| lens::LensOverlayInteractionRequestMetadata::Type ContentTypeToInteractionType( |
| lens::MimeType content_type) { |
| switch (content_type) { |
| case lens::MimeType::kPdf: |
| return lens::LensOverlayInteractionRequestMetadata::PDF_QUERY; |
| case lens::MimeType::kHtml: |
| case lens::MimeType::kPlainText: |
| case lens::MimeType::kAnnotatedPageContent: |
| return lens::LensOverlayInteractionRequestMetadata::WEBPAGE_QUERY; |
| case lens::MimeType::kUnknown: |
| break; |
| case lens::MimeType::kImage: |
| case lens::MimeType::kVideo: |
| case lens::MimeType::kAudio: |
| case lens::MimeType::kJson: |
| // These content types are not supported for the page content upload flow. |
| NOTREACHED() << "Unsupported option in page content upload"; |
| } |
| return lens::LensOverlayInteractionRequestMetadata::CONTEXTUAL_SEARCH_QUERY; |
| } |
| |
| lens::LensOverlayClientLogs::LensOverlayEntryPoint |
| LenOverlayEntryPointFromInvocationSource( |
| lens::LensOverlayInvocationSource invocation_source) { |
| switch (invocation_source) { |
| case lens::LensOverlayInvocationSource::kAppMenu: |
| return lens::LensOverlayClientLogs::APP_MENU; |
| case lens::LensOverlayInvocationSource::kContentAreaContextMenuPage: |
| return lens::LensOverlayClientLogs::PAGE_CONTEXT_MENU; |
| case lens::LensOverlayInvocationSource::kContentAreaContextMenuImage: |
| return lens::LensOverlayClientLogs::IMAGE_CONTEXT_MENU; |
| case lens::LensOverlayInvocationSource::kContentAreaContextMenuText: |
| return lens::LensOverlayClientLogs::TEXT_CONTEXT_MENU; |
| case lens::LensOverlayInvocationSource::kContentAreaContextMenuVideo: |
| return lens::LensOverlayClientLogs::VIDEO_CONTEXT_MENU; |
| case lens::LensOverlayInvocationSource::kOmnibox: |
| return lens::LensOverlayClientLogs::OMNIBOX_BUTTON; |
| case lens::LensOverlayInvocationSource::kOmniboxContextualSuggestion: |
| return lens::LensOverlayClientLogs::OMNIBOX_CONTEXTUAL_SUGGESTION; |
| case lens::LensOverlayInvocationSource::kOmniboxPageAction: |
| return lens::LensOverlayClientLogs::OMNIBOX_PAGE_ACTION; |
| case lens::LensOverlayInvocationSource::kHomeworkActionChip: |
| return lens::LensOverlayClientLogs::HOMEWORK_ACTION_CHIP; |
| case lens::LensOverlayInvocationSource::kToolbar: |
| return lens::LensOverlayClientLogs::TOOLBAR_BUTTON; |
| case lens::LensOverlayInvocationSource::kFindInPage: |
| return lens::LensOverlayClientLogs::FIND_IN_PAGE; |
| case lens::LensOverlayInvocationSource::kLVFShutterButton: |
| case lens::LensOverlayInvocationSource::kLVFGallery: |
| case lens::LensOverlayInvocationSource::kContextMenu: |
| case lens::LensOverlayInvocationSource::kAIHub: |
| case lens::LensOverlayInvocationSource::kFREPromo: |
| NOTREACHED() << "Invocation source not supported."; |
| } |
| return lens::LensOverlayClientLogs::UNKNOWN_ENTRY_POINT; |
| } |
| |
| // Divides the content_bytes into small chunks, which are then compressed. |
| std::vector<std::string> MakeChunks(base::span<const uint8_t> content_bytes) { |
| base::SpanReader reader(content_bytes); |
| size_t max_chunk_size = lens::features::GetLensOverlayChunkSizeBytes(); |
| std::vector<std::string> chunks; |
| |
| while (reader.remaining() > 0) { |
| size_t chunk_size = std::min(reader.remaining(), max_chunk_size); |
| auto current_chunk = reader.Read(chunk_size); |
| CHECK(current_chunk.has_value()); |
| |
| std::string chunk; |
| const bool success = lens::ZstdCompressBytes(current_chunk.value(), &chunk); |
| if (!success) { |
| // If any of the chunks fail to compress, then the request should fail. |
| return std::vector<std::string>(); |
| } |
| |
| chunks.push_back(chunk); |
| } |
| return chunks; |
| } |
| |
| // Creates the lens::LensOverlayUploadChunkRequest for the given chunk. |
| lens::LensOverlayUploadChunkRequest CreateUploadChunkRequest( |
| int64_t chunk_id, |
| int64_t total_chunks, |
| std::string chunk, |
| lens::LensOverlayRequestContext request_context) { |
| lens::LensOverlayUploadChunkRequest request; |
| request.mutable_request_context()->CopyFrom(request_context); |
| request.mutable_debug_options()->set_total_chunks(total_chunks); |
| request.set_chunk_id(chunk_id); |
| request.mutable_chunk_bytes()->assign(chunk.begin(), chunk.end()); |
| return request; |
| } |
| |
| // Returns the lens::Payload to be sent after uploading chunked data using the |
| // repeated Content field instead of the deprecated payload fields. |
| lens::Payload CreatePageContentPayloadForChunks( |
| base::span<const lens::PageContent> page_content, |
| lens::MimeType primary_content_type, |
| GURL page_url, |
| std::optional<std::string> page_title, |
| int64_t total_stored_chunks) { |
| lens::Payload payload; |
| auto* content = payload.mutable_content(); |
| |
| if (!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()); |
| } |
| |
| auto* content_data = content->add_content_data(); |
| content_data->set_content_type( |
| lens::MimeTypeToContentType(primary_content_type)); |
| content_data->mutable_stored_chunk_options()->set_read_stored_chunks(true); |
| content_data->mutable_stored_chunk_options()->set_total_stored_chunks( |
| total_stored_chunks); |
| content_data->set_compression_type(lens::CompressionType::ZSTD); |
| return payload; |
| } |
| |
| // Returns the lens::Payload using the repeated Content field instead of the |
| // deprecated payload fields. |
| lens::Payload CreatePageContentPayload( |
| base::span<const lens::PageContent> page_contents, |
| GURL page_url, |
| std::optional<std::string> page_title) { |
| lens::Payload payload; |
| auto* content = payload.mutable_content(); |
| |
| if (!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::PageContent& page_content : page_contents) { |
| auto* content_data = content->add_content_data(); |
| content_data->set_content_type( |
| MimeTypeToContentType(page_content.content_type_)); |
| |
| // Compress PDF bytes. |
| if (page_content.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(page_content.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(page_content.bytes_.begin(), |
| page_content.bytes_.end()); |
| } |
| |
| return payload; |
| } |
| |
| } // namespace |
| |
| PageContent::PageContent() : content_type_(lens::MimeType::kUnknown) {} |
| PageContent::PageContent(std::vector<uint8_t> bytes, |
| lens::MimeType content_type) |
| : bytes_(bytes), content_type_(content_type) {} |
| PageContent::PageContent(const PageContent& other) = default; |
| PageContent::~PageContent() = default; |
| |
| LensOverlayQueryController::LensOverlayQueryController( |
| LensOverlayFullImageResponseCallback full_image_callback, |
| LensOverlayUrlResponseCallback url_callback, |
| LensOverlayInteractionResponseCallback interaction_response_callback, |
| LensOverlaySuggestInputsCallback suggest_inputs_callback, |
| LensOverlayThumbnailCreatedCallback thumbnail_created_callback, |
| UploadProgressCallback page_content_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) |
| : full_image_callback_(std::move(full_image_callback)), |
| interaction_response_callback_(std::move(interaction_response_callback)), |
| suggest_inputs_callback_(std::move(suggest_inputs_callback)), |
| thumbnail_created_callback_(std::move(thumbnail_created_callback)), |
| page_content_upload_progress_callback_( |
| std::move(page_content_upload_progress_callback)), |
| request_id_generator_( |
| std::make_unique<lens::LensOverlayRequestIdGenerator>()), |
| url_callback_(std::move(url_callback)), |
| variations_client_(variations_client), |
| identity_manager_(identity_manager), |
| profile_(profile), |
| invocation_source_(invocation_source), |
| use_dark_mode_(use_dark_mode), |
| gen204_controller_(gen204_controller) { |
| encoding_task_runner_ = base::ThreadPool::CreateTaskRunner( |
| {base::TaskPriority::USER_VISIBLE, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); |
| compression_task_runner_ = base::ThreadPool::CreateTaskRunner( |
| {base::TaskPriority::USER_VISIBLE, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); |
| encoding_task_tracker_ = std::make_unique<base::CancelableTaskTracker>(); |
| compression_task_tracker_ = std::make_unique<base::CancelableTaskTracker>(); |
| } |
| |
| LensOverlayQueryController::~LensOverlayQueryController() { |
| EndQuery(); |
| } |
| |
| void LensOverlayQueryController::StartQueryFlow( |
| const SkBitmap& screenshot, |
| GURL page_url, |
| std::optional<std::string> page_title, |
| std::vector<lens::mojom::CenterRotatedBoxPtr> significant_region_boxes, |
| base::span<const lens::PageContent> underlying_page_contents, |
| lens::MimeType primary_content_type, |
| std::optional<uint32_t> pdf_current_page, |
| float ui_scale_factor, |
| base::TimeTicks invocation_time) { |
| original_screenshot_ = screenshot; |
| page_url_ = page_url; |
| page_title_ = page_title; |
| significant_region_boxes_ = std::move(significant_region_boxes); |
| underlying_page_contents_ = underlying_page_contents; |
| primary_content_type_ = primary_content_type; |
| pdf_current_page_ = pdf_current_page; |
| ui_scale_factor_ = ui_scale_factor; |
| invocation_time_ = invocation_time; |
| gen204_id_ = base::RandUint64(); |
| gen204_controller_->OnQueryFlowStart(invocation_source_, profile_, |
| gen204_id_); |
| |
| if (primary_content_type_ != lens::MimeType::kUnknown) { |
| suggest_inputs_.set_contextual_visual_input_type( |
| lens::VitQueryParamValueForMimeType(primary_content_type_)); |
| RunSuggestInputsCallback(); |
| } |
| |
| // Reset translation languages in case they were set in a previous request. |
| translate_options_.reset(); |
| |
| PrepareAndFetchFullImageRequest(); |
| } |
| |
| void LensOverlayQueryController::EndQuery() { |
| ResetPageContentData(); |
| gen204_controller_->OnQueryFlowEnd(); |
| full_image_endpoint_fetcher_.reset(); |
| interaction_endpoint_fetcher_.reset(); |
| pending_interaction_callback_.Reset(); |
| cluster_info_access_token_fetcher_.reset(); |
| full_image_access_token_fetcher_.reset(); |
| interaction_access_token_fetcher_.reset(); |
| page_url_ = GURL(); |
| page_title_.reset(); |
| translate_options_.reset(); |
| cluster_info_.reset(); |
| encoding_task_tracker_->TryCancelAll(); |
| compression_task_tracker_->TryCancelAll(); |
| query_controller_state_ = QueryControllerState::kOff; |
| } |
| |
| void LensOverlayQueryController::MaybeRestartQueryFlow() { |
| if (query_controller_state_ == QueryControllerState::kClusterInfoExpired) { |
| PrepareAndFetchFullImageRequest(); |
| } |
| } |
| |
| void LensOverlayQueryController::SendFullPageTranslateQuery( |
| const std::string& source_language, |
| const std::string& target_language) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendFullPageTranslateQuery called when query controller is off"; |
| translate_options_ = TranslateOptions(source_language, target_language); |
| |
| // Send a normal full image request. The parameters to make it a translate |
| // request will be set when the actual request is sent based on the instance |
| // variables. |
| PrepareAndFetchFullImageRequest(); |
| } |
| |
| void LensOverlayQueryController::SendEndTranslateModeQuery() { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendEndTranslateModeQuery called when query controller is off"; |
| |
| translate_options_.reset(); |
| PrepareAndFetchFullImageRequest(); |
| } |
| |
| void LensOverlayQueryController::ResetPageContentData() { |
| underlying_page_contents_ = base::span<const lens::PageContent>(); |
| primary_content_type_ = lens::MimeType::kUnknown; |
| pdf_current_page_ = std::nullopt; |
| page_url_ = GURL(); |
| page_title_ = std::nullopt; |
| partial_content_ = base::span<const std::u16string>(); |
| } |
| |
| void LensOverlayQueryController::SendUpdatedPageContent( |
| std::optional<base::span<const lens::PageContent>> underlying_page_content, |
| std::optional<lens::MimeType> primary_content_type, |
| std::optional<GURL> new_page_url, |
| std::optional<std::string> new_page_title, |
| std::optional<uint32_t> pdf_current_page, |
| const SkBitmap& screenshot) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendUpdatedPageContent called when query controller is off"; |
| |
| if (underlying_page_content.has_value()) { |
| underlying_page_contents_ = underlying_page_content.value(); |
| primary_content_type_ = primary_content_type.value(); |
| page_url_ = new_page_url.value(); |
| page_title_ = new_page_title; |
| } |
| pdf_current_page_ = pdf_current_page; |
| if (!screenshot.drawsNothing()) { |
| original_screenshot_ = screenshot; |
| } |
| |
| suggest_inputs_.set_contextual_visual_input_type( |
| VitQueryParamValueForMimeType(primary_content_type_)); |
| RunSuggestInputsCallback(); |
| |
| if (query_controller_state_ == |
| QueryControllerState::kAwaitingClusterInfoResponse) { |
| // If we are waiting for the cluster info response, we should not send the |
| // page content update request immediately. Instead, the cluster info |
| // response handler will call PrepareAndFetchFullImageRequest and |
| // PrepareAndFetchPageContentRequest. |
| return; |
| } |
| if (!screenshot.drawsNothing()) { |
| PrepareAndFetchFullImageRequest(); |
| } |
| if (underlying_page_content.has_value()) { |
| PrepareAndFetchPageContentRequest(); |
| } |
| } |
| |
| void LensOverlayQueryController::SendPartialPageContentRequest( |
| base::span<const std::u16string> partial_content) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendPartialPageContentRequest called when query controller is off"; |
| partial_content_ = partial_content; |
| |
| PrepareAndFetchPartialPageContentRequest(); |
| } |
| |
| void LensOverlayQueryController::SendRegionSearch( |
| base::Time query_start_time, |
| lens::mojom::CenterRotatedBoxPtr region, |
| lens::LensOverlaySelectionType lens_selection_type, |
| std::map<std::string, std::string> additional_search_query_params, |
| std::optional<SkBitmap> region_bytes) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendRegionSearch called when query controller is off"; |
| |
| SendInteraction(query_start_time, /*region=*/std::move(region), |
| /*query_text=*/std::nullopt, |
| /*object_id=*/std::nullopt, lens_selection_type, |
| additional_search_query_params, region_bytes); |
| } |
| |
| void LensOverlayQueryController::SendContextualTextQuery( |
| base::Time query_start_time, |
| const std::string& query_text, |
| lens::LensOverlaySelectionType lens_selection_type, |
| std::map<std::string, std::string> additional_search_query_params) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendContextualTextQuery called when query controller is off"; |
| if (underlying_page_contents_.empty()) { |
| SendTextOnlyQuery(query_start_time, query_text, lens_selection_type, |
| additional_search_query_params); |
| return; |
| } |
| |
| // If the contextual search query shouldn't be sent now, hold it until the |
| // full page content upload is finished and/or the full image query for an |
| // updated screenshot is finished. |
| if (!ShouldSendContextualSearchQuery()) { |
| pending_contextual_query_callback_ = base::BindOnce( |
| &LensOverlayQueryController::SendContextualTextQuery, |
| weak_ptr_factory_.GetWeakPtr(), query_start_time, query_text, |
| lens_selection_type, additional_search_query_params); |
| return; |
| } |
| |
| // Include the vit to get contextualized results. |
| additional_search_query_params = AddVisualInputTypeQueryParam( |
| additional_search_query_params, primary_content_type_); |
| |
| SendInteraction(query_start_time, /*region=*/nullptr, query_text, |
| /*object_id=*/std::nullopt, lens_selection_type, |
| additional_search_query_params, std::nullopt); |
| } |
| |
| void LensOverlayQueryController::SendTextOnlyQuery( |
| base::Time query_start_time, |
| const std::string& query_text, |
| lens::LensOverlaySelectionType lens_selection_type, |
| std::map<std::string, std::string> additional_search_query_params) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendTextOnlyQuery called when query controller is off"; |
| |
| // Although the text only flow might not send an interaction request, we |
| // should replace any in-flight interaction requests to cancel previously |
| // issued fetches. |
| latest_interaction_request_data_ = std::make_unique<LensServerFetchRequest>( |
| CreateInvalidRequestId(), |
| /*query_start_time_ms=*/base::TimeTicks::Now()); |
| |
| // The visual search interaction log data should be added as late as possible, |
| // so that is_parent_query can be accurately set if the user issues multiple |
| // interactions in quick succession. |
| if (lens::features::SendVisualSearchInteractionParamForLensTextQueries() && |
| IsLensTextSelectionType(lens_selection_type)) { |
| std::string encoded_vsint = |
| GetEncodedVisualSearchInteractionLogData(lens_selection_type); |
| suggest_inputs_.set_encoded_visual_search_interaction_log_data( |
| encoded_vsint); |
| additional_search_query_params.insert( |
| {kVisualSearchInteractionDataQueryParameterKey, encoded_vsint}); |
| } else { |
| suggest_inputs_.clear_encoded_visual_search_interaction_log_data(); |
| } |
| suggest_inputs_.clear_encoded_image_signals(); |
| RunSuggestInputsCallback(); |
| |
| lens::proto::LensOverlayUrlResponse lens_overlay_url_response; |
| lens_overlay_url_response.set_url( |
| lens::BuildTextOnlySearchURL(query_start_time, query_text, page_url_, |
| page_title_, additional_search_query_params, |
| invocation_source_, lens_selection_type, |
| use_dark_mode_) |
| .spec()); |
| lens_overlay_url_response.set_page_url(page_url_.spec()); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(url_callback_, lens_overlay_url_response)); |
| } |
| |
| void LensOverlayQueryController::SendMultimodalRequest( |
| base::Time query_start_time, |
| lens::mojom::CenterRotatedBoxPtr region, |
| const std::string& query_text, |
| lens::LensOverlaySelectionType multimodal_selection_type, |
| std::map<std::string, std::string> additional_search_query_params, |
| std::optional<SkBitmap> region_bytes) { |
| CHECK(query_controller_state_ != QueryControllerState::kOff) |
| << "SendMultimodalRequest called when query controller is off"; |
| |
| if (base::TrimWhitespaceASCII(query_text, base::TRIM_ALL).empty()) { |
| return; |
| } |
| SendInteraction(query_start_time, /*region=*/std::move(region), query_text, |
| /*object_id=*/std::nullopt, multimodal_selection_type, |
| additional_search_query_params, region_bytes); |
| } |
| |
| void LensOverlayQueryController::SendTaskCompletionGen204IfEnabled( |
| lens::mojom::UserAction user_action) { |
| SendTaskCompletionGen204IfEnabled(latest_encoded_analytics_id_, user_action, |
| latest_request_id_); |
| } |
| |
| void LensOverlayQueryController::SendSemanticEventGen204IfEnabled( |
| lens::mojom::SemanticEvent event) { |
| std::optional<lens::LensOverlayRequestId> request_id = std::nullopt; |
| if (event == lens::mojom::SemanticEvent::kTextGleamsViewStart) { |
| request_id = std::make_optional(latest_request_id_); |
| } |
| SendSemanticEventGen204IfEnabled(event, request_id); |
| } |
| |
| std::unique_ptr<lens::LensOverlayRequestId> |
| LensOverlayQueryController::GetNextRequestId(RequestIdUpdateMode update_mode) { |
| std::unique_ptr<lens::LensOverlayRequestId> request_id = |
| request_id_generator_->GetNextRequestId(update_mode); |
| latest_request_id_ = *request_id.get(); |
| latest_encoded_analytics_id_ = |
| request_id_generator_->GetBase32EncodedAnalyticsId(); |
| std::string encoded_request_id = Base64EncodeRequestId(*request_id); |
| suggest_inputs_.set_encoded_request_id(encoded_request_id); |
| RunSuggestInputsCallback(); |
| return request_id; |
| } |
| |
| void LensOverlayQueryController::RunSuggestInputsCallback() { |
| suggest_inputs_.set_send_gsession_vsrid_for_contextual_suggest(true); |
| suggest_inputs_.set_send_gsession_vsrid_vit_for_lens_suggest( |
| lens::features::GetLensOverlaySendLensInputsForLensSuggest()); |
| suggest_inputs_.set_send_vsint_for_lens_suggest( |
| lens::features:: |
| GetLensOverlaySendLensVisualInteractionDataForLensSuggest()); |
| if (cluster_info_.has_value()) { |
| suggest_inputs_.set_search_session_id(cluster_info_->search_session_id()); |
| } else { |
| suggest_inputs_.clear_search_session_id(); |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(suggest_inputs_callback_, suggest_inputs_)); |
| } |
| |
| void LensOverlayQueryController::ResetRequestClusterInfoStateForTesting() { |
| ResetRequestClusterInfoState(); |
| } |
| |
| std::unique_ptr<EndpointFetcher> |
| LensOverlayQueryController::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=*/profile_ |
| ? profile_->GetURLLoaderFactory().get() |
| : g_browser_process->shared_url_loader_factory(), |
| /*identity_manager=*/nullptr, |
| EndpointFetcher::RequestParams::Builder(http_method, |
| kTrafficAnnotationTag) |
| .SetAuthType(endpoint_fetcher::CHROME_API_KEY) |
| .SetChannel(chrome::GetChannel()) |
| .SetContentType(kContentType) |
| .SetCorsExemptHeaders(cors_exempt_headers) |
| .SetCredentialsMode(CredentialsMode::kInclude) |
| .SetHeaders(request_headers) |
| .SetPostData(std::move(request_string)) |
| .SetSetSiteForCookies(true) |
| .SetTimeout(timeout) |
| .SetUrl(fetch_url) |
| .SetUploadProgressCallback(std::move(upload_progress_callback)) |
| .Build()); |
| } |
| |
| void LensOverlayQueryController::SendLatencyGen204IfEnabled( |
| lens::LensOverlayGen204Controller::LatencyType latency_type, |
| base::TimeTicks start_time_ticks, |
| std::string vit_query_param_value, |
| std::optional<base::TimeDelta> cluster_info_latency, |
| std::optional<std::string> encoded_analytics_id, |
| std::optional<lens::LensOverlayRequestId> request_id) { |
| base::TimeDelta latency_duration = base::TimeTicks::Now() - start_time_ticks; |
| gen204_controller_->SendLatencyGen204IfEnabled( |
| latency_type, latency_duration, vit_query_param_value, |
| cluster_info_latency, encoded_analytics_id, request_id); |
| } |
| |
| void LensOverlayQueryController::SendTaskCompletionGen204IfEnabled( |
| std::string encoded_analytics_id, |
| lens::mojom::UserAction user_action, |
| lens::LensOverlayRequestId request_id) { |
| gen204_controller_->SendTaskCompletionGen204IfEnabled( |
| encoded_analytics_id, user_action, request_id); |
| } |
| |
| void LensOverlayQueryController::SendSemanticEventGen204IfEnabled( |
| lens::mojom::SemanticEvent event, |
| std::optional<lens::LensOverlayRequestId> request_id) { |
| gen204_controller_->SendSemanticEventGen204IfEnabled(event, request_id); |
| } |
| |
| LensOverlayQueryController::LensServerFetchRequest::LensServerFetchRequest( |
| std::unique_ptr<lens::LensOverlayRequestId> request_id, |
| base::TimeTicks query_start_time) |
| : request_id_(std::move(request_id)), query_start_time_(query_start_time) {} |
| LensOverlayQueryController::LensServerFetchRequest::~LensServerFetchRequest() = |
| default; |
| |
| std::string LensOverlayQueryController::GetVsridForNewTab() { |
| std::unique_ptr<lens::LensOverlayRequestId> request_id = |
| request_id_generator_->GetNextRequestId( |
| RequestIdUpdateMode::kOpenInNewTab); |
| return Base64EncodeRequestId(*request_id); |
| } |
| |
| void LensOverlayQueryController::FetchClusterInfoRequest() { |
| query_controller_state_ = QueryControllerState::kAwaitingClusterInfoResponse; |
| |
| // There should not be any in-flight cluster info request. |
| CHECK(!cluster_info_access_token_fetcher_); |
| cluster_info_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| &LensOverlayQueryController::PerformClusterInfoFetchRequest, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*query_start_time=*/base::TimeTicks::Now())); |
| } |
| |
| void LensOverlayQueryController::PerformClusterInfoFetchRequest( |
| base::TimeTicks query_start_time, |
| 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 = |
| CreateVariationsHeaders(variations_client_); |
| |
| // Generate the URL to fetch. |
| GURL fetch_url = GURL(lens::features::GetLensOverlayClusterInfoEndpointUrl()); |
| |
| HttpMethod request_method; |
| std::string request_string; |
| request_method = HttpMethod::kPost; |
| |
| // Create the client context to include in the request. |
| lens::LensOverlayClientContext client_context = CreateClientContext(); |
| lens::LensOverlayServerClusterInfoRequest request; |
| request.set_enable_search_session_id(true); |
| request.set_surface(client_context.surface()); |
| request.set_platform(client_context.platform()); |
| request.mutable_rendering_context()->CopyFrom( |
| client_context.rendering_context()); |
| 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, request_method, |
| base::Milliseconds(lens::features::GetLensOverlayServerRequestTimeout()), |
| request_headers, cors_exempt_headers, base::DoNothing()); |
| |
| // Finally, perform the request. |
| cluster_info_endpoint_fetcher_->PerformRequest( |
| base::BindOnce( |
| &LensOverlayQueryController::ClusterInfoFetchResponseHandler, |
| weak_ptr_factory_.GetWeakPtr(), query_start_time), |
| google_apis::GetAPIKey().c_str()); |
| |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialClusterInfoRequestSent, |
| VitQueryParamValueForMimeType(primary_content_type_), |
| /*request_id=*/std::nullopt); |
| } |
| |
| void LensOverlayQueryController::ClusterInfoFetchResponseHandler( |
| base::TimeTicks query_start_time, |
| std::unique_ptr<EndpointResponse> response) { |
| cluster_info_endpoint_fetcher_.reset(); |
| query_controller_state_ = QueryControllerState::kReceivedClusterInfoResponse; |
| |
| if (response->http_status_code != google_apis::ApiErrorCode::HTTP_SUCCESS) { |
| // If there was an error with the cluster info request, we should still try |
| // and send the full image request as a fallback. |
| PrepareAndFetchFullImageRequest(); |
| return; |
| } |
| |
| lens::LensOverlayServerClusterInfoResponse server_response; |
| if (!server_response.ParseFromString(response->response)) { |
| // If there was an error with the cluster info request, we should still try |
| // and send the full image request as a fallback. |
| PrepareAndFetchFullImageRequest(); |
| 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 routing info is enabled, store the routing info to be included in |
| // followup requests. |
| if (lens::features::IsLensOverlayRoutingInfoEnabled() && |
| server_response.has_routing_info() && |
| !request_id_generator_->HasRoutingInfo()) { |
| std::unique_ptr<lens::LensOverlayRequestId> request_id = |
| request_id_generator_->SetRoutingInfo(server_response.routing_info()); |
| suggest_inputs_.set_encoded_request_id(Base64EncodeRequestId(*request_id)); |
| } |
| |
| // Update the suggest inputs with the cluster info's search session id. |
| RunSuggestInputsCallback(); |
| |
| // Clear the cluster info after its lifetime expires. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&LensOverlayQueryController::ResetRequestClusterInfoState, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::Seconds( |
| lens::features::GetLensOverlayClusterInfoLifetimeSeconds())); |
| |
| // Store the fetch response time. |
| cluster_info_fetch_response_time_ = base::TimeTicks::Now() - query_start_time; |
| |
| // Continue with the full image request which will use the session id from the |
| // cluster info we just received. |
| PrepareAndFetchFullImageRequest(); |
| PrepareAndFetchPageContentRequest(); |
| PrepareAndFetchPartialPageContentRequest(); |
| } |
| |
| void LensOverlayQueryController::PrepareAndFetchFullImageRequest() { |
| if (query_controller_state_ == |
| QueryControllerState::kAwaitingClusterInfoResponse) { |
| // If we are still waiting for the cluster info response, we can't send the |
| // full image request yet. Once the cluster info response is received, |
| // PrepareAndFetchFullImageRequest will be called again. |
| return; |
| } |
| |
| // If the cluster info optimization is enabled, request the cluster info prior |
| // to making the full image request. Also do this for the contextual search |
| // flow since the request flow for contextual searchbox will fail without the |
| // cluster info handshake. |
| if (!cluster_info_ && |
| (lens::features::IsLensOverlayClusterInfoOptimizationEnabled() || |
| lens::IsLensOverlayContextualSearchboxEnabled())) { |
| FetchClusterInfoRequest(); |
| return; |
| } |
| |
| // If the screenshot draws nothing, return. |
| if (original_screenshot_.drawsNothing()) { |
| return; |
| } |
| |
| // There can be multiple full image requests that are called. For example, |
| // when translate mode is enabled after opening the overlay or when turning |
| // translate mode back off after enabling. Reset if there is one pending. |
| full_image_endpoint_fetcher_.reset(); |
| query_controller_state_ = QueryControllerState::kAwaitingFullImageResponse; |
| |
| // Create the client logs needed throughout the async process to attach to |
| // the full image request. |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> ref_counted_logs = |
| base::MakeRefCounted<lens::RefCountedLensOverlayClientLogs>(); |
| ref_counted_logs->client_logs().set_metrics_collection_disabled( |
| !g_browser_process->GetMetricsServicesManager() || |
| !g_browser_process->GetMetricsServicesManager()->IsMetricsConsentGiven()); |
| ref_counted_logs->client_logs().set_lens_overlay_entry_point( |
| LenOverlayEntryPointFromInvocationSource(invocation_source_)); |
| |
| // Initialize latest_full_image_request_data_ with a next request id to |
| // ensure once the async processes finish, no new full image request has |
| // started. |
| latest_full_image_request_data_ = std::make_unique<LensServerFetchRequest>( |
| GetNextRequestId(initial_request_id_ |
| ? RequestIdUpdateMode::kFullImageRequest |
| : RequestIdUpdateMode::kInitialRequest), |
| /*query_start_time=*/base::TimeTicks::Now()); |
| int current_sequence_id = latest_full_image_request_data_->sequence_id(); |
| |
| // If this is the first full image request, store the request id for all the |
| // other first batch of requests to use. |
| if (initial_request_id_ == nullptr) { |
| initial_request_id_ = std::make_unique<lens::LensOverlayRequestId>(); |
| initial_request_id_->CopyFrom( |
| *latest_full_image_request_data_->request_id_); |
| } |
| |
| // If there is a pending interaction, we can create and issue it now that the |
| // cluster info and full-image request id are available. |
| if (cluster_info_.has_value() && pending_interaction_callback_) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(pending_interaction_callback_)); |
| } |
| |
| // Preparing for the full image request requires multiple async flows to |
| // complete before the request is ready to be send to the server. We start |
| // these flows here, and each flow completes by calling |
| // FullImageRequestDataReady() with its data. FullImageRequestDataReady() will |
| // handle waiting for all necessary flows to complete before performing the |
| // request. |
| // |
| // Async Flow 1: Creating the full image request. |
| // Do the image encoding asynchronously to prevent the main thread from |
| // blocking on the encoding. |
| encoding_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&lens::DownscaleAndEncodeBitmap, original_screenshot_, |
| ui_scale_factor_, ref_counted_logs), |
| base::BindOnce(&LensOverlayQueryController:: |
| CreateFullImageRequestAndTryPerformFullImageRequest, |
| weak_ptr_factory_.GetWeakPtr(), current_sequence_id, |
| ref_counted_logs)); |
| |
| // Async Flow 2: Retrieve the OAuth headers. |
| CreateOAuthHeadersAndTryPerformFullPageRequest(current_sequence_id); |
| } |
| |
| void LensOverlayQueryController::PrepareImageDataForFullImageRequest( |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> ref_counted_logs, |
| lens::ImageData image_data) { |
| ref_counted_logs->client_logs().set_paella_id(gen204_id_); |
| |
| resized_bitmap_size_ = gfx::Size(image_data.image_metadata().width(), |
| image_data.image_metadata().height()); |
| AddSignificantRegions(image_data, std::move(significant_region_boxes_)); |
| } |
| |
| void LensOverlayQueryController:: |
| CreateFullImageRequestAndTryPerformFullImageRequest( |
| int sequence_id, |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> ref_counted_logs, |
| lens::ImageData image_data) { |
| DCHECK_EQ(query_controller_state_, |
| QueryControllerState::kAwaitingFullImageResponse); |
| PrepareImageDataForFullImageRequest(ref_counted_logs, image_data); |
| |
| // Create the request. |
| lens::LensOverlayServerRequest request; |
| request.mutable_client_logs()->CopyFrom(ref_counted_logs->client_logs()); |
| lens::LensOverlayRequestContext request_context; |
| |
| // The request ID is guaranteed to exist since it is set in the constructor |
| // of latest_full_image_request_data_. |
| DCHECK(latest_full_image_request_data_->request_id_); |
| request_context.mutable_request_id()->CopyFrom( |
| *latest_full_image_request_data_->request_id_); |
| |
| request_context.mutable_client_context()->CopyFrom(CreateClientContext()); |
| request.mutable_objects_request()->mutable_request_context()->CopyFrom( |
| request_context); |
| request.mutable_objects_request()->mutable_image_data()->CopyFrom(image_data); |
| |
| if (pdf_current_page_.has_value()) { |
| request.mutable_objects_request() |
| ->mutable_viewport_request_context() |
| ->set_pdf_page_number(pdf_current_page_.value()); |
| } |
| |
| FullImageRequestDataReady(sequence_id, request); |
| } |
| |
| void LensOverlayQueryController::CreateOAuthHeadersAndTryPerformFullPageRequest( |
| int sequence_id) { |
| DCHECK_EQ(query_controller_state_, |
| QueryControllerState::kAwaitingFullImageResponse); |
| |
| // If there is already a pending access token fetcher, we purposefully |
| // override it since we no longer care about the previous request. |
| full_image_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| static_cast<void (LensOverlayQueryController::*)( |
| int, std::vector<std::string>)>( |
| &LensOverlayQueryController::FullImageRequestDataReady), |
| weak_ptr_factory_.GetWeakPtr(), sequence_id)); |
| } |
| |
| void LensOverlayQueryController::FullImageRequestDataReady( |
| int sequence_id, |
| lens::LensOverlayServerRequest request) { |
| if (!IsCurrentFullImageSequence(sequence_id)) { |
| // Ignore superseded request. |
| return; |
| } |
| |
| latest_full_image_request_data_->request_ = |
| std::make_unique<lens::LensOverlayServerRequest>(request); |
| FullImageRequestDataHelper(sequence_id); |
| } |
| |
| void LensOverlayQueryController::FullImageRequestDataReady( |
| int sequence_id, |
| std::vector<std::string> headers) { |
| if (!IsCurrentFullImageSequence(sequence_id)) { |
| // Ignore superseded request. |
| return; |
| } |
| |
| full_image_access_token_fetcher_.reset(); |
| latest_full_image_request_data_->request_headers_ = |
| std::make_unique<std::vector<std::string>>(headers); |
| FullImageRequestDataHelper(sequence_id); |
| } |
| |
| void LensOverlayQueryController::FullImageRequestDataHelper(int sequence_id) { |
| CHECK(latest_full_image_request_data_->sequence_id() == sequence_id); |
| if (latest_full_image_request_data_->request_ && |
| latest_full_image_request_data_->request_headers_) { |
| PerformFullImageRequest(); |
| } |
| } |
| |
| bool LensOverlayQueryController::IsCurrentFullImageSequence(int sequence_id) { |
| CHECK(latest_full_image_request_data_); |
| return latest_full_image_request_data_->sequence_id() == sequence_id; |
| } |
| |
| void LensOverlayQueryController::PerformFullImageRequest() { |
| PerformFetchRequest( |
| latest_full_image_request_data_->request_.get(), |
| latest_full_image_request_data_->request_headers_.get(), |
| base::Milliseconds(lens::features::GetLensOverlayServerRequestTimeout()), |
| base::BindOnce( |
| &LensOverlayQueryController::OnFullImageEndpointFetcherCreated, |
| weak_ptr_factory_.GetWeakPtr(), |
| *latest_full_image_request_data_->request_id_.get()), |
| base::BindOnce(&LensOverlayQueryController::FullImageFetchResponseHandler, |
| weak_ptr_factory_.GetWeakPtr(), |
| latest_full_image_request_data_->sequence_id())); |
| } |
| |
| void LensOverlayQueryController::FullImageFetchResponseHandler( |
| int request_sequence_id, |
| std::unique_ptr<EndpointResponse> response) { |
| // If this request sequence ID does not match the latest sent then we should |
| // ignore the response. |
| if (latest_full_image_request_data_->sequence_id() != request_sequence_id) { |
| return; |
| } |
| |
| DCHECK_EQ(query_controller_state_, |
| QueryControllerState::kAwaitingFullImageResponse); |
| |
| CHECK(full_image_endpoint_fetcher_); |
| full_image_endpoint_fetcher_.reset(); |
| query_controller_state_ = QueryControllerState::kReceivedFullImageResponse; |
| |
| if (response->http_status_code != google_apis::ApiErrorCode::HTTP_SUCCESS) { |
| RunFullImageCallbackForError(); |
| return; |
| } |
| |
| lens::LensOverlayServerResponse server_response; |
| if (!server_response.ParseFromString(response->response)) { |
| RunFullImageCallbackForError(); |
| return; |
| } |
| |
| if (!server_response.has_objects_response()) { |
| RunFullImageCallbackForError(); |
| return; |
| } |
| |
| if (!cluster_info_.has_value()) { |
| if (!server_response.objects_response().has_cluster_info()) { |
| RunFullImageCallbackForError(); |
| return; |
| } |
| |
| cluster_info_ = std::make_optional<lens::LensOverlayClusterInfo>(); |
| cluster_info_->CopyFrom(server_response.objects_response().cluster_info()); |
| |
| // Clear the cluster info after its lifetime expires. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| &LensOverlayQueryController::ResetRequestClusterInfoState, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::Seconds( |
| lens::features::GetLensOverlayClusterInfoLifetimeSeconds())); |
| |
| // If routing info is enabled, store the routing info to be included in |
| // followup requests. |
| if (lens::features::IsLensOverlayRoutingInfoEnabled() && |
| cluster_info_->has_routing_info() && |
| !request_id_generator_->HasRoutingInfo()) { |
| std::unique_ptr<lens::LensOverlayRequestId> new_request_id = |
| request_id_generator_->SetRoutingInfo(cluster_info_->routing_info()); |
| suggest_inputs_.set_encoded_request_id( |
| Base64EncodeRequestId(*new_request_id)); |
| } |
| } |
| |
| SendFullImageLatencyGen204IfEnabled( |
| latest_full_image_request_data_->query_start_time_, |
| translate_options_.has_value(), kImageVisualInputTypeQueryParameterValue); |
| |
| // Image signals and vsint are only valid after an interaction request. |
| suggest_inputs_.clear_encoded_image_signals(); |
| suggest_inputs_.clear_encoded_visual_search_interaction_log_data(); |
| RunSuggestInputsCallback(); |
| |
| if (pending_interaction_callback_) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(pending_interaction_callback_)); |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(full_image_callback_, |
| lens::CreateObjectsMojomArrayFromServerResponse( |
| server_response), |
| lens::CreateTextMojomFromServerResponse( |
| server_response, resized_bitmap_size_), |
| /*is_error=*/false)); |
| } |
| |
| void LensOverlayQueryController::RunFullImageCallbackForError() { |
| ResetRequestClusterInfoState(); |
| // Needs to be set to received response so this query can be retried on the |
| // next interaction request. |
| query_controller_state_ = |
| QueryControllerState::kReceivedFullImageErrorResponse; |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(full_image_callback_, |
| std::vector<lens::mojom::OverlayObjectPtr>(), |
| /*text=*/nullptr, /*is_error=*/true)); |
| } |
| |
| void LensOverlayQueryController::PrepareAndFetchPageContentRequest() { |
| if (query_controller_state_ == QueryControllerState::kClusterInfoExpired) { |
| // If the cluster info has expired, we need to refetch the cluster info. The |
| // full image request will recall this method once the cluster info is |
| // fetched. |
| MaybeRestartQueryFlow(); |
| return; |
| } |
| |
| if (underlying_page_contents_.empty() || |
| underlying_page_contents_.front().bytes_.empty()) { |
| // No need to send the request without underlying content bytes. |
| return; |
| } |
| |
| compression_task_tracker_->TryCancelAll(); |
| page_contents_request_start_time_ = base::TimeTicks::Now(); |
| page_content_request_in_progress_ = true; |
| chunk_upload_in_progress_ = false; |
| remaining_upload_chunk_responses_ = 0; |
| remaining_chunk_retries = lens::features::GetLensOverlayUploadChunkRetries(); |
| |
| // The initial request id should be set by the time we get here. If not, call |
| // below will crash. |
| CHECK(initial_request_id_); |
| |
| // Send a chunk request if the upload is a PDF larger than the chunk size. |
| // If not, send a normal page content request. |
| if (lens::features::IsLensOverlayUploadChunkingEnabled() && |
| primary_content_type_ == lens::MimeType::kPdf && |
| underlying_page_contents_.front().bytes_.size() > |
| lens::features::GetLensOverlayChunkSizeBytes()) { |
| chunk_upload_in_progress_ = true; |
| // Post MakeChunks to a task off the main thread so compression does not |
| // throttle the main thread. |
| compression_task_tracker_->PostTaskAndReplyWithResult( |
| compression_task_runner_.get(), FROM_HERE, |
| base::BindOnce(&MakeChunks, underlying_page_contents_.front().bytes_), |
| base::BindOnce( |
| &LensOverlayQueryController::PrepareAndFetchUploadChunkRequests, |
| weak_ptr_factory_.GetWeakPtr(), |
| is_first_page_contents_request_ |
| ? *initial_request_id_ |
| : *GetNextRequestId( |
| lens::RequestIdUpdateMode::kPageContentRequest))); |
| } else { |
| // Post CreatePageContentPayload to a task off the main thread so |
| // compression does not throttle the main thread. |
| compression_task_tracker_->PostTaskAndReplyWithResult( |
| compression_task_runner_.get(), FROM_HERE, |
| base::BindOnce(&CreatePageContentPayload, underlying_page_contents_, |
| page_url_, page_title_), |
| base::BindOnce( |
| &LensOverlayQueryController::PrepareAndFetchPageContentRequestPart2, |
| weak_ptr_factory_.GetWeakPtr(), |
| is_first_page_contents_request_ |
| ? *initial_request_id_ |
| : *GetNextRequestId( |
| lens::RequestIdUpdateMode::kPageContentRequest))); |
| } |
| |
| // If this is the second or later page content request, the partial page |
| // content should no longer be considered first. |
| if (!is_first_page_contents_request_) { |
| is_first_partial_page_contents_request_ = false; |
| } |
| // Any subsequent page content requests will be considered non-first. |
| is_first_page_contents_request_ = false; |
| } |
| |
| void LensOverlayQueryController::PrepareAndFetchUploadChunkRequests( |
| lens::LensOverlayRequestId request_id, |
| std::vector<std::string> chunks) { |
| if (!chunks.size()) { |
| return; |
| } |
| chunk_progress = std::vector<size_t>(chunks.size()); |
| total_chunk_progress_ = 0; |
| total_chunk_upload_size_ = 0; |
| |
| lens::LensOverlayRequestContext request_context; |
| request_context.mutable_request_id()->CopyFrom(request_id); |
| request_context.mutable_client_context()->CopyFrom(CreateClientContext()); |
| |
| std::vector<lens::LensOverlayUploadChunkRequest> requests; |
| for (size_t i = 0; i < chunks.size(); i++) { |
| total_chunk_upload_size_ += chunks[i].size(); |
| requests.push_back( |
| CreateUploadChunkRequest(i, chunks.size(), chunks[i], request_context)); |
| } |
| pending_upload_chunk_requests_ = requests; |
| upload_chunk_sequence_id = request_id.sequence_id(); |
| |
| chunk_upload_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| &LensOverlayQueryController::PrepareAndFetchUploadChunkRequestsPart2, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void LensOverlayQueryController::PrepareAndFetchUploadChunkRequestsPart2( |
| std::vector<std::string> headers) { |
| chunk_upload_access_token_fetcher_.reset(); |
| pending_upload_chunk_headers_ = headers; |
| remaining_upload_chunk_responses_ = pending_upload_chunk_requests_.size(); |
| for (size_t i = 0; i < pending_upload_chunk_requests_.size(); i++) { |
| FetchUploadChunkRequest(i); |
| } |
| } |
| |
| void LensOverlayQueryController::FetchUploadChunkRequest( |
| size_t chunk_request_index) { |
| auto& request = pending_upload_chunk_requests_[chunk_request_index]; |
| std::string request_string; |
| CHECK(request.SerializeToString(&request_string)); |
| |
| PerformFetchRequest( |
| std::move(request_string), &pending_upload_chunk_headers_, |
| base::Milliseconds( |
| lens::features::GetLensOverlayUploadChunkRequestTimeoutMs()), |
| base::BindOnce( |
| &LensOverlayQueryController::OnChunkUploadEndpointFetcherCreated, |
| weak_ptr_factory_.GetWeakPtr(), |
| request.request_context().request_id()), |
| base::BindOnce(&LensOverlayQueryController::UploadChunkResponseHandler, |
| weak_ptr_factory_.GetWeakPtr(), |
| request.request_context().request_id(), |
| pending_upload_chunk_requests_.size()), |
| base::BindRepeating( |
| &LensOverlayQueryController::UploadChunkProgressHandler, |
| weak_ptr_factory_.GetWeakPtr(), chunk_request_index), |
| GURL(lens::features::GetLensOverlayUploadChunkEndpointURL())); |
| } |
| |
| void LensOverlayQueryController::UploadChunkResponseHandler( |
| lens::LensOverlayRequestId request_id, |
| size_t total_chunks, |
| std::unique_ptr<EndpointResponse> response) { |
| // If there is a newer sequence id, a new request has been initiated before |
| // this one has completed. Do nothing and return. |
| if (request_id.sequence_id() != upload_chunk_sequence_id) { |
| return; |
| } |
| |
| remaining_upload_chunk_responses_--; |
| // If this is the last chunk to receive a response, perform the page content |
| // request. |
| if (remaining_upload_chunk_responses_ == 0) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&CreatePageContentPayloadForChunks, |
| underlying_page_contents_, primary_content_type_, |
| page_url_, page_title_, total_chunks), |
| base::BindOnce( |
| &LensOverlayQueryController::PrepareAndFetchPageContentRequestPart2, |
| weak_ptr_factory_.GetWeakPtr(), request_id)); |
| } |
| } |
| |
| void LensOverlayQueryController::PrepareAndFetchPageContentRequestPart2( |
| lens::LensOverlayRequestId request_id, |
| lens::Payload payload) { |
| // Create the request. |
| lens::LensOverlayServerRequest request; |
| lens::LensOverlayRequestContext request_context; |
| |
| request_context.mutable_request_id()->CopyFrom(request_id); |
| request_context.mutable_client_context()->CopyFrom(CreateClientContext()); |
| request.mutable_objects_request()->mutable_request_context()->CopyFrom( |
| request_context); |
| |
| request.mutable_objects_request()->mutable_payload()->CopyFrom(payload); |
| |
| // Save the request in case it needs to be resent. |
| pending_page_content_request_.CopyFrom(request); |
| |
| page_content_access_token_fetcher_ = CreateOAuthHeadersAndContinue( |
| base::BindOnce(&LensOverlayQueryController::PerformPageContentRequest, |
| weak_ptr_factory_.GetWeakPtr(), std::move(request))); |
| } |
| |
| void LensOverlayQueryController::PerformPageContentRequest( |
| lens::LensOverlayServerRequest request, |
| std::vector<std::string> headers) { |
| page_content_access_token_fetcher_.reset(); |
| |
| PerformFetchRequest( |
| &request, &headers, |
| base::Milliseconds( |
| lens::features::GetLensOverlayPageContentRequestTimeoutMs()), |
| base::BindOnce( |
| &LensOverlayQueryController::OnPageContentEndpointFetcherCreated, |
| weak_ptr_factory_.GetWeakPtr(), |
| request.objects_request().request_context().request_id()), |
| base::BindOnce(&LensOverlayQueryController::PageContentResponseHandler, |
| weak_ptr_factory_.GetWeakPtr(), |
| request.objects_request().request_context().request_id()), |
| // If this is a chunked upload, upload progress will have already been |
| // reported by the chunk uploads, so skip passing in the upload progress |
| // handler here. |
| chunk_upload_in_progress_ |
| ? base::NullCallback() |
| : base::BindRepeating( |
| &LensOverlayQueryController::PageContentUploadProgressHandler, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void LensOverlayQueryController::PageContentResponseHandler( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointResponse> response) { |
| page_content_endpoint_fetcher_.reset(); |
| |
| // Ensure the page content upload doesn't need to be retried. |
| // If it does, exit early. |
| if (MaybeRetryPageContentUpload(std::move(response))) { |
| return; |
| } |
| |
| // The upload progress handler is not guaranteed to execute, so if a response |
| // is received, mark the request as no longer in progress to allow the |
| // interaction request to be sent. |
| PageContentUploadFinished(); |
| |
| // If the chunk uploads have already completed, or if upload chunking was not |
| // done, this will send the gen204 ping and clear the endpoint fetchers. |
| MaybeSendPageContentUploadLatencyGen204(request_id); |
| } |
| |
| bool LensOverlayQueryController::MaybeRetryPageContentUpload( |
| std::unique_ptr<EndpointResponse> response) { |
| // Check if the server response contains missing chunk errors to handle. |
| // Proceed without handling if out of retries. |
| if (remaining_chunk_retries > 0) { |
| remaining_chunk_retries--; |
| lens::LensOverlayServerResponse server_response; |
| bool parse_successful = server_response.ParseFromString(response->response); |
| if (parse_successful && |
| server_response.error().error_type() == |
| LensOverlayServerError_ErrorType:: |
| LensOverlayServerError_ErrorType_MISSING_CHUNKS) { |
| auto missing_chunks_metadata = |
| server_response.error().missing_chunks_metadata(); |
| if (!missing_chunks_metadata.has_chunk_metadata()) { |
| // Interaction request likely misrouted. Resend it. |
| page_content_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| &LensOverlayQueryController::PerformPageContentRequest, |
| weak_ptr_factory_.GetWeakPtr(), pending_page_content_request_)); |
| return true; |
| } |
| if (missing_chunks_metadata.missing_chunk_ids_size() > 0) { |
| // Missing chunks. Resend the missing chunks. |
| chunk_upload_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| &LensOverlayQueryController::RetryUploadChunkRequests, |
| weak_ptr_factory_.GetWeakPtr(), |
| server_response.error() |
| .missing_chunks_metadata() |
| .missing_chunk_ids())); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| void LensOverlayQueryController::RetryUploadChunkRequests( |
| const google::protobuf::RepeatedField<int64_t>& chunk_ids, |
| std::vector<std::string> headers) { |
| chunk_upload_access_token_fetcher_.reset(); |
| pending_upload_chunk_headers_ = headers; |
| remaining_upload_chunk_responses_ = chunk_ids.size(); |
| for (int64_t chunk_id : chunk_ids) { |
| FetchUploadChunkRequest(chunk_id); |
| } |
| } |
| |
| void LensOverlayQueryController::MaybeSendPageContentUploadLatencyGen204( |
| lens::LensOverlayRequestId request_id) { |
| if (!page_content_request_in_progress_ && |
| remaining_upload_chunk_responses_ == 0) { |
| chunk_upload_endpoint_fetchers_.clear(); |
| SendLatencyGen204IfEnabled( |
| LatencyType::kPageContentUploadLatency, |
| page_contents_request_start_time_, |
| VitQueryParamValueForMimeType(primary_content_type_), |
| /*cluster_info_latency=*/std::nullopt, |
| /*encoded_analytics_id=*/std::nullopt, |
| std::make_optional<lens::LensOverlayRequestId>(request_id)); |
| } |
| } |
| |
| void LensOverlayQueryController::PageContentUploadProgressHandler( |
| uint64_t position, |
| uint64_t total) { |
| if (page_content_upload_progress_callback_) { |
| page_content_upload_progress_callback_.Run(position, total); |
| } |
| } |
| |
| void LensOverlayQueryController::UploadChunkProgressHandler( |
| size_t chunk_request_index, |
| uint64_t position, |
| uint64_t total) { |
| // Caller of this callback should be sequenced. |
| |
| // Save the reported position of each chunk to the chunk_progress vector. |
| // Instead of repeatedly summing over the entire vector, increment the total |
| // chunk progress by the difference between the currently reported position |
| // and the last reported position of the chunk. |
| total_chunk_progress_ += position - chunk_progress[chunk_request_index]; |
| chunk_progress[chunk_request_index] = position; |
| |
| // Overhead causes the total progress to be very slightly above the total |
| // upload size (by about 0.01%). Cap to avoid reporting progress > 100%. |
| if (total_chunk_progress_ > total_chunk_upload_size_) { |
| total_chunk_progress_ = total_chunk_upload_size_; |
| } |
| |
| if (page_content_upload_progress_callback_) { |
| page_content_upload_progress_callback_.Run(total_chunk_progress_, |
| total_chunk_upload_size_); |
| } |
| } |
| |
| void LensOverlayQueryController::PageContentUploadFinished() { |
| pending_page_content_request_.Clear(); |
| page_content_request_in_progress_ = false; |
| chunk_upload_in_progress_ = false; |
| chunk_progress.clear(); |
| if (pending_contextual_query_callback_) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(pending_contextual_query_callback_)); |
| } |
| } |
| |
| void LensOverlayQueryController::PrepareAndFetchPartialPageContentRequest() { |
| if (!cluster_info_ || !IsPartialPageContentSubstantial()) { |
| // Cannot send this request without cluster info. Do not send the request |
| // if the partial page content is not substantial enough to yield deatialed |
| // results. |
| return; |
| } |
| |
| partial_page_contents_request_start_time_ = base::TimeTicks::Now(); |
| |
| // Create the request. |
| lens::LensOverlayServerRequest request; |
| lens::LensOverlayRequestContext request_context; |
| |
| // If this is the first partial page content request, use the initial request |
| // id. Otherwise, use the request id generator. |
| if (is_first_partial_page_contents_request_) { |
| CHECK(initial_request_id_); |
| request_context.mutable_request_id()->CopyFrom(*initial_request_id_); |
| } else { |
| request_context.mutable_request_id()->CopyFrom(*GetNextRequestId( |
| lens::RequestIdUpdateMode::kPartialPageContentRequest)); |
| } |
| request_context.mutable_client_context()->CopyFrom(CreateClientContext()); |
| request.mutable_objects_request()->mutable_request_context()->CopyFrom( |
| request_context); |
| |
| // Create the partial page content payload. |
| lens::Payload payload; |
| payload.set_request_type(lens::RequestType::REQUEST_TYPE_EARLY_PARTIAL_PDF); |
| |
| // Add the partial page content to the payload. |
| lens::LensOverlayDocument partial_pdf_document; |
| for (size_t i = 0; i < partial_content_.size(); ++i) { |
| const auto& page_text = partial_content_[i]; |
| auto* page = partial_pdf_document.add_pages(); |
| page->set_page_number(i + 1); |
| page->add_text_segments(base::UTF16ToUTF8(page_text)); |
| } |
| |
| auto* content = payload.mutable_content(); |
| auto* content_data = content->add_content_data(); |
| content_data->set_content_type( |
| lens::ContentData::CONTENT_TYPE_EARLY_PARTIAL_PDF); |
| partial_pdf_document.SerializeToString(content_data->mutable_data()); |
| |
| // Add the page url to the payload if it is available. |
| if (!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()); |
| } |
| |
| request.mutable_objects_request()->mutable_payload()->CopyFrom(payload); |
| |
| partial_page_content_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| &LensOverlayQueryController::PerformPartialPageContentRequest, |
| weak_ptr_factory_.GetWeakPtr(), std::move(request))); |
| |
| is_first_partial_page_contents_request_ = false; |
| } |
| |
| void LensOverlayQueryController::PerformPartialPageContentRequest( |
| lens::LensOverlayServerRequest request, |
| std::vector<std::string> headers) { |
| partial_page_content_access_token_fetcher_.reset(); |
| |
| PerformFetchRequest( |
| &request, &headers, |
| base::Milliseconds( |
| lens::features::GetLensOverlayPageContentRequestTimeoutMs()), |
| base::BindOnce(&LensOverlayQueryController:: |
| OnPartialPageContentEndpointFetcherCreated, |
| weak_ptr_factory_.GetWeakPtr(), |
| request.objects_request().request_context().request_id()), |
| base::BindOnce( |
| &LensOverlayQueryController::PartialPageContentResponseHandler, |
| weak_ptr_factory_.GetWeakPtr(), |
| request.objects_request().request_context().request_id())); |
| } |
| |
| void LensOverlayQueryController::PartialPageContentResponseHandler( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointResponse> response) { |
| partial_page_content_endpoint_fetcher_.reset(); |
| |
| SendLatencyGen204IfEnabled( |
| LatencyType::kPartialPageContentUploadLatency, |
| partial_page_contents_request_start_time_, |
| VitQueryParamValueForMimeType(primary_content_type_), |
| /*cluster_info_latency=*/std::nullopt, |
| /*encoded_analytics_id=*/std::nullopt, |
| std::make_optional<lens::LensOverlayRequestId>(request_id)); |
| } |
| |
| void LensOverlayQueryController::SendInteraction( |
| base::Time query_start_time, |
| lens::mojom::CenterRotatedBoxPtr region, |
| std::optional<std::string> query_text, |
| std::optional<std::string> object_id, |
| lens::LensOverlaySelectionType selection_type, |
| std::map<std::string, std::string> additional_search_query_params, |
| std::optional<SkBitmap> region_bytes) { |
| // Cancel any pending encoding from previous SendInteraction requests. |
| encoding_task_tracker_->TryCancelAll(); |
| // Reset any pending interaction requests that will get fired via the full |
| // image request / response handlers. |
| pending_interaction_callback_.Reset(); |
| |
| // If the cluster info is missing add the interaction to the pending callback |
| // to be sent once the cluster info is available. |
| if (!cluster_info_.has_value()) { |
| pending_interaction_callback_ = |
| base::BindOnce(&LensOverlayQueryController::SendInteraction, |
| weak_ptr_factory_.GetWeakPtr(), query_start_time, |
| std::move(region), query_text, object_id, selection_type, |
| additional_search_query_params, region_bytes); |
| |
| // If the cluster info is expired, restart a new query flow so the pending |
| // interaction request will be sent once the cluster info is available. |
| MaybeRestartQueryFlow(); |
| return; |
| } |
| |
| if (!latest_full_image_request_data_) { |
| // The request id sequence for the interaction request must follow a full |
| // image request. If we have not yet created a full image request id, the |
| // request id generator will not be ready to create the interaction request |
| // id. In that case, save the interaction data to create the request after |
| // the full image request id sequence has been incremented. |
| pending_interaction_callback_ = |
| base::BindOnce(&LensOverlayQueryController::SendInteraction, |
| weak_ptr_factory_.GetWeakPtr(), query_start_time, |
| std::move(region), query_text, object_id, selection_type, |
| additional_search_query_params, region_bytes); |
| return; |
| } |
| |
| // Create the logs used across the async. |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> ref_counted_logs = |
| base::MakeRefCounted<lens::RefCountedLensOverlayClientLogs>(); |
| ref_counted_logs->client_logs().set_lens_overlay_entry_point( |
| LenOverlayEntryPointFromInvocationSource(invocation_source_)); |
| ref_counted_logs->client_logs().set_paella_id(gen204_id_); |
| ref_counted_logs->client_logs().set_metrics_collection_disabled( |
| !g_browser_process->GetMetricsServicesManager() || |
| !g_browser_process->GetMetricsServicesManager()->IsMetricsConsentGiven()); |
| |
| // Initialize latest_interaction_request_data_ with a new request ID to |
| // ensure once the async processes finish, no new interaction request has |
| // started. |
| latest_interaction_request_data_ = std::make_unique<LensServerFetchRequest>( |
| GetNextRequestId(RequestIdUpdateMode::kInteractionRequest), |
| /*query_start_time_ms=*/base::TimeTicks::Now()); |
| int current_sequence_id = latest_interaction_request_data_->sequence_id(); |
| |
| // Add the create URL callback to be run after the request is sent. |
| latest_interaction_request_data_->request_sent_callback_ = base::BindOnce( |
| &LensOverlayQueryController::CreateSearchUrlAndSendToCallback, |
| weak_ptr_factory_.GetWeakPtr(), query_start_time, query_text, |
| additional_search_query_params, selection_type, |
| GetNextRequestId(RequestIdUpdateMode::kSearchUrl)); |
| |
| // The interaction request requires multiple async flows to complete before |
| // the request is ready to be send to the server. We start these flows here, |
| // and each flow completes by calling InteractionRequestDataReady() with its |
| // data. InteractionRequestDataReady() will handle waiting for all necessary |
| // flows to complete before performing the request. |
| // |
| // Async Flow 1: Downscale the image region for the interaction request. |
| // Do the image encoding asynchronously to prevent the main thread from |
| // blocking on the encoding. |
| encoding_task_tracker_->PostTaskAndReplyWithResult( |
| encoding_task_runner_.get(), FROM_HERE, |
| base::BindOnce(&lens::DownscaleAndEncodeBitmapRegionIfNeeded, |
| original_screenshot_, region.Clone(), region_bytes, |
| ref_counted_logs), |
| base::BindOnce( |
| &LensOverlayQueryController:: |
| CreateInteractionRequestAndTryPerformInteractionRequest, |
| weak_ptr_factory_.GetWeakPtr(), current_sequence_id, region.Clone(), |
| query_text, object_id, ref_counted_logs)); |
| |
| // Async Flow 2: Retrieve the OAuth headers. |
| CreateOAuthHeadersAndTryPerformInteractionRequest(current_sequence_id); |
| } |
| |
| void LensOverlayQueryController:: |
| CreateInteractionRequestAndTryPerformInteractionRequest( |
| int sequence_id, |
| lens::mojom::CenterRotatedBoxPtr region, |
| std::optional<std::string> query_text, |
| std::optional<std::string> object_id, |
| scoped_refptr<lens::RefCountedLensOverlayClientLogs> ref_counted_logs, |
| std::optional<lens::ImageCropAndBitmap> image_crop_and_bitmap) { |
| // The request index should match our counter after encoding finishes. |
| CHECK(sequence_id == latest_interaction_request_data_->sequence_id()); |
| |
| // Pass the image crop and region bitmap for this request to the thumbnail |
| // created callback. |
| if (image_crop_and_bitmap.has_value()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| thumbnail_created_callback_, |
| image_crop_and_bitmap->image_crop.image().image_content(), |
| image_crop_and_bitmap->region_bitmap)); |
| } |
| |
| // Create the interaction request. |
| lens::LensOverlayServerRequest server_request = CreateInteractionRequest( |
| std::move(region), query_text, object_id, |
| image_crop_and_bitmap |
| ? std::make_optional(image_crop_and_bitmap->image_crop) |
| : std::nullopt, |
| ref_counted_logs->client_logs()); |
| |
| // Continue the async process. |
| InteractionRequestDataReady(sequence_id, std::move(server_request)); |
| } |
| |
| void LensOverlayQueryController:: |
| CreateOAuthHeadersAndTryPerformInteractionRequest(int sequence_id) { |
| // If there is already a pending access token fetcher, we purposefully |
| // override it to cancel the old request. |
| interaction_access_token_fetcher_ = |
| CreateOAuthHeadersAndContinue(base::BindOnce( |
| static_cast<void (LensOverlayQueryController::*)( |
| int, std::vector<std::string>)>( |
| &LensOverlayQueryController::InteractionRequestDataReady), |
| weak_ptr_factory_.GetWeakPtr(), sequence_id)); |
| } |
| |
| void LensOverlayQueryController::InteractionRequestDataReady( |
| int sequence_id, |
| lens::LensOverlayServerRequest request) { |
| if (!IsCurrentInteractionSequence(sequence_id)) { |
| // Ignore superseded request. |
| return; |
| } |
| |
| latest_interaction_request_data_->request_ = |
| std::make_unique<lens::LensOverlayServerRequest>(request); |
| TryPerformInteractionRequest(sequence_id); |
| } |
| |
| void LensOverlayQueryController::InteractionRequestDataReady( |
| int sequence_id, |
| std::vector<std::string> headers) { |
| if (!IsCurrentInteractionSequence(sequence_id)) { |
| // Ignore superseded request. |
| return; |
| } |
| |
| interaction_access_token_fetcher_.reset(); |
| latest_interaction_request_data_->request_headers_ = |
| std::make_unique<std::vector<std::string>>(headers); |
| TryPerformInteractionRequest(sequence_id); |
| } |
| |
| void LensOverlayQueryController::TryPerformInteractionRequest(int sequence_id) { |
| if (!IsCurrentInteractionSequence(sequence_id)) { |
| // Ignore superseded request. |
| return; |
| } |
| |
| if (!latest_interaction_request_data_->request_ || |
| !latest_interaction_request_data_->request_headers_) { |
| // Exit early since not all request data is ready. |
| return; |
| } |
| |
| // Allow the query controller to perform the interaction request before the |
| // full image response is received if the early interaction optimization is |
| // enabled. |
| if (lens::features::IsLensOverlayEarlyInteractionOptimizationEnabled() && |
| query_controller_state_ == |
| QueryControllerState::kAwaitingFullImageResponse && |
| cluster_info_.has_value()) { |
| PerformInteractionRequest(); |
| return; |
| } |
| |
| // If a full image request is in flight, wait for the full image response |
| // before sending the request. |
| if (query_controller_state_ == |
| QueryControllerState::kAwaitingClusterInfoResponse || |
| query_controller_state_ == |
| QueryControllerState::kAwaitingFullImageResponse) { |
| pending_interaction_callback_ = base::BindOnce( |
| &LensOverlayQueryController::TryPerformInteractionRequest, |
| weak_ptr_factory_.GetWeakPtr(), sequence_id); |
| return; |
| } |
| |
| // All elements needed are ready so perform the request. |
| PerformInteractionRequest(); |
| } |
| |
| bool LensOverlayQueryController::IsCurrentInteractionSequence(int sequence_id) { |
| CHECK(latest_interaction_request_data_); |
| return latest_interaction_request_data_->sequence_id() == sequence_id; |
| } |
| |
| void LensOverlayQueryController::PerformInteractionRequest() { |
| // The interaction request is composed of two steps, sending the request to |
| // the server, and creating the URL to load in the side panel. |
| PerformFetchRequest( |
| latest_interaction_request_data_->request_.get(), |
| latest_interaction_request_data_->request_headers_.get(), |
| base::Milliseconds(lens::features::GetLensOverlayServerRequestTimeout()), |
| base::BindOnce( |
| &LensOverlayQueryController::OnInteractionEndpointFetcherCreated, |
| weak_ptr_factory_.GetWeakPtr(), |
| *latest_interaction_request_data_->request_id_.get()), |
| base::BindOnce( |
| &LensOverlayQueryController::InteractionFetchResponseHandler, |
| weak_ptr_factory_.GetWeakPtr(), |
| latest_interaction_request_data_->sequence_id())); |
| |
| // Run the callback to create the search URL and pass it to the side panel. |
| CHECK(latest_interaction_request_data_->request_sent_callback_.has_value()); |
| std::move(latest_interaction_request_data_->request_sent_callback_.value()) |
| .Run(); |
| } |
| |
| void LensOverlayQueryController::CreateSearchUrlAndSendToCallback( |
| base::Time query_start_time, |
| std::optional<std::string> query_text, |
| std::map<std::string, std::string> additional_search_query_params, |
| lens::LensOverlaySelectionType selection_type, |
| std::unique_ptr<lens::LensOverlayRequestId> request_id) { |
| // Cluster info must be set already. |
| CHECK(cluster_info_.has_value()); |
| |
| additional_search_query_params.insert( |
| {kGen204IdentifierQueryParameter, |
| base::NumberToString(gen204_id_).c_str()}); |
| |
| // The visual search interaction log data should be added as late as possible, |
| // so that is_parent_query can be accurately set if the user issues multiple |
| // interactions in quick succession. |
| std::string encoded_vsint = |
| GetEncodedVisualSearchInteractionLogData(selection_type); |
| additional_search_query_params.insert( |
| {kVisualSearchInteractionDataQueryParameterKey, encoded_vsint}); |
| suggest_inputs_.set_encoded_visual_search_interaction_log_data(encoded_vsint); |
| RunSuggestInputsCallback(); |
| |
| // Generate and send the Lens search url. |
| lens::proto::LensOverlayUrlResponse lens_overlay_url_response; |
| lens_overlay_url_response.set_url( |
| lens::BuildLensSearchURL( |
| query_start_time, query_text, page_url_, page_title_, |
| std::move(request_id), cluster_info_.value(), |
| additional_search_query_params, invocation_source_, use_dark_mode_) |
| .spec()); |
| lens_overlay_url_response.set_page_url(page_url_.spec()); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(url_callback_, lens_overlay_url_response)); |
| } |
| |
| void LensOverlayQueryController::InteractionFetchResponseHandler( |
| int sequence_id, |
| std::unique_ptr<EndpointResponse> response) { |
| // If this request sequence ID does not match the latest sent then we should |
| // ignore the response. |
| if (latest_interaction_request_data_->sequence_id() != sequence_id) { |
| return; |
| } |
| |
| if (response->http_status_code != google_apis::ApiErrorCode::HTTP_SUCCESS) { |
| RunInteractionCallbackForError(); |
| return; |
| } |
| |
| lens::LensOverlayServerResponse server_response; |
| if (!server_response.ParseFromString(response->response)) { |
| RunInteractionCallbackForError(); |
| return; |
| } |
| |
| if (!server_response.has_interaction_response()) { |
| RunInteractionCallbackForError(); |
| return; |
| } |
| |
| // Attach the analytics id associated with the interaction request to the |
| // latency gen204 ping. This may differ from latest_encoded_analytics_id_ if |
| // the user makes an objects request while the interaction request is in |
| // flight. |
| std::string encoded_analytics_id = base32::Base32Encode( |
| base::as_byte_span( |
| latest_interaction_request_data_->request_id_.get()->analytics_id()), |
| base32::Base32EncodePolicy::OMIT_PADDING); |
| SendLatencyGen204IfEnabled( |
| LatencyType::kInteractionRequestFetchLatency, |
| latest_interaction_request_data_->query_start_time_, |
| VitQueryParamValueForMimeType(primary_content_type_), |
| /*cluster_info_latency=*/std::nullopt, |
| std::make_optional(encoded_analytics_id), |
| *latest_interaction_request_data_->request_id_.get()); |
| |
| if (!(lens::IsLensOverlayContextualSearchboxEnabled() && |
| !lens::features::GetLensOverlaySendImageSignalsForLensSuggest())) { |
| // Always include the image signals unless the contextual searchbox is |
| // enabled and the image signals feature flag is disabled. |
| suggest_inputs_.set_encoded_image_signals( |
| server_response.interaction_response().encoded_response()); |
| RunSuggestInputsCallback(); |
| } |
| |
| if (server_response.interaction_response().has_text()) { |
| interaction_response_callback_.Run(CreateTextMojomFromInteractionResponse( |
| server_response.interaction_response(), |
| latest_interaction_request_data_.get() |
| ->request_->interaction_request() |
| .image_crop() |
| .zoomed_crop(), |
| resized_bitmap_size_)); |
| } |
| |
| RunSuggestInputsCallback(); |
| } |
| |
| void LensOverlayQueryController::RunInteractionCallbackForError() { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(suggest_inputs_callback_, |
| lens::proto::LensOverlaySuggestInputs())); |
| } |
| |
| void LensOverlayQueryController::SendFullImageLatencyGen204IfEnabled( |
| base::TimeTicks start_time_ticks, |
| bool is_translate_query, |
| std::string vit_query_param_value) { |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialFullPageObjectsResponseReceived, |
| vit_query_param_value, |
| *latest_full_image_request_data_->request_id_.get()); |
| |
| SendLatencyGen204IfEnabled( |
| is_translate_query ? lens::LensOverlayGen204Controller::LatencyType:: |
| kFullPageTranslateRequestFetchLatency |
| : lens::LensOverlayGen204Controller::LatencyType:: |
| kFullPageObjectsRequestFetchLatency, |
| start_time_ticks, vit_query_param_value, |
| cluster_info_fetch_response_time_, |
| /*encoded_analytics_id=*/std::nullopt, |
| *latest_full_image_request_data_->request_id_.get()); |
| cluster_info_fetch_response_time_.reset(); |
| } |
| |
| void LensOverlayQueryController::SendInitialLatencyGen204IfNotAlreadySent( |
| lens::LensOverlayGen204Controller::LatencyType latency_type, |
| std::string vit_query_param_value, |
| std::optional<lens::LensOverlayRequestId> request_id) { |
| if (sent_initial_latency_request_events_.contains(latency_type)) { |
| return; |
| } |
| |
| SendLatencyGen204IfEnabled(latency_type, invocation_time_, |
| vit_query_param_value, |
| /*cluster_info_latency=*/std::nullopt, |
| /*encoded_analytics_id=*/std::nullopt, request_id); |
| sent_initial_latency_request_events_.insert(latency_type); |
| } |
| |
| void LensOverlayQueryController::PerformFetchRequest( |
| lens::LensOverlayServerRequest* request, |
| std::vector<std::string>* request_headers, |
| base::TimeDelta timeout, |
| base::OnceCallback<void(std::unique_ptr<EndpointFetcher>)> |
| fetcher_created_callback, |
| EndpointFetcherCallback response_received_callback, |
| UploadProgressCallback upload_progress_callback) { |
| CHECK(request); |
| std::string request_string; |
| CHECK(request->SerializeToString(&request_string)); |
| GURL fetch_url = GURL(lens::features::GetLensOverlayEndpointURL()); |
| PerformFetchRequest(std::move(request_string), request_headers, timeout, |
| std::move(fetcher_created_callback), |
| std::move(response_received_callback), |
| std::move(upload_progress_callback), fetch_url); |
| } |
| |
| void LensOverlayQueryController::PerformFetchRequest( |
| std::string request_string, |
| std::vector<std::string>* request_headers, |
| base::TimeDelta timeout, |
| base::OnceCallback<void(std::unique_ptr<EndpointFetcher>)> |
| fetcher_created_callback, |
| EndpointFetcherCallback response_received_callback, |
| UploadProgressCallback upload_progress_callback, |
| GURL fetch_url) { |
| CHECK(request_headers); |
| |
| // Get client experiment variations to include in the request. |
| std::vector<std::string> cors_exempt_headers = |
| CreateVariationsHeaders(variations_client_); |
| |
| // Generate the URL to fetch to and include the server session id if present. |
| if (cluster_info_.has_value()) { |
| // 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()); |
| } |
| |
| // 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, timeout, |
| *request_headers, cors_exempt_headers, |
| std::move(upload_progress_callback)); |
| EndpointFetcher* fetcher = endpoint_fetcher.get(); |
| |
| // Run callback that the fetcher was created. This is used to keep the |
| // endpoint_fetcher alive while the request is made. |
| 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()); |
| } |
| |
| lens::LensOverlayClientContext |
| LensOverlayQueryController::CreateClientContext() { |
| lens::LensOverlayClientContext context; |
| if (lens::features::IsUpdatedClientContextEnabled()) { |
| context.set_surface(lens::SURFACE_LENS_OVERLAY); |
| context.set_platform(lens::PLATFORM_LENS_OVERLAY); |
| } else { |
| context.set_surface(lens::SURFACE_CHROMIUM); |
| context.set_platform(lens::PLATFORM_WEB); |
| context.mutable_rendering_context()->set_rendering_environment( |
| lens::RENDERING_ENV_LENS_OVERLAY); |
| } |
| context.mutable_client_filters()->add_filter()->set_filter_type( |
| lens::AUTO_FILTER); |
| context.mutable_locale_context()->set_language( |
| g_browser_process->GetApplicationLocale()); |
| context.mutable_locale_context()->set_region( |
| icu::Locale(g_browser_process->GetApplicationLocale().c_str()) |
| .getCountry()); |
| |
| // Add the appropriate context filters. If source and target languages have |
| // been set, this should add translate. |
| if (translate_options_.has_value()) { |
| context.mutable_client_filters()->clear_filter(); |
| lens::AppliedFilter* translate_filter = |
| context.mutable_client_filters()->add_filter(); |
| translate_filter->set_filter_type(lens::TRANSLATE); |
| translate_filter->mutable_translate()->set_source_language( |
| translate_options_->source_language); |
| translate_filter->mutable_translate()->set_target_language( |
| translate_options_->target_language); |
| } |
| |
| 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; |
| } |
| |
| std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> |
| LensOverlayQueryController::CreateOAuthHeadersAndContinue( |
| OAuthHeadersCreatedCallback callback) { |
| // Use OAuth if the flag is enabled and the user is logged in. |
| if (lens::features::UseOauthForLensOverlayRequests() && identity_manager_ && |
| identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) { |
| signin::AccessTokenFetcher::TokenCallback token_callback = |
| base::BindOnce(&lens::CreateOAuthHeader).Then(std::move(callback)); |
| |
| // If an access token fetcher is already in flight, it is intentionally |
| // replaced by this newer one. |
| return std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| signin::OAuthConsumerId::kLensOverlayQueryController, 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; |
| } |
| |
| std::string |
| LensOverlayQueryController::GetEncodedVisualSearchInteractionLogData( |
| lens::LensOverlaySelectionType selection_type) { |
| lens::LensOverlayVisualSearchInteractionData interaction_data; |
| interaction_data.mutable_log_data()->mutable_filter_data()->set_filter_type( |
| lens::AUTO_FILTER); |
| interaction_data.mutable_log_data() |
| ->mutable_user_selection_data() |
| ->set_selection_type(selection_type); |
| interaction_data.mutable_log_data()->set_is_parent_query(!parent_query_sent_); |
| interaction_data.mutable_log_data()->set_client_platform( |
| lens::CLIENT_PLATFORM_LENS_OVERLAY); |
| |
| // If there was an interaction request made, then the selection type, region |
| // and object id should be set if they exist. The interaction request may not |
| // exist if the user made a text-selection query. |
| if (latest_interaction_request_data_->request_ && |
| latest_interaction_request_data_->request_->has_interaction_request()) { |
| auto sent_interaction_request = |
| latest_interaction_request_data_->request_->interaction_request(); |
| interaction_data.set_interaction_type( |
| sent_interaction_request.interaction_request_metadata().type()); |
| if (sent_interaction_request.has_interaction_request_metadata() && |
| sent_interaction_request.interaction_request_metadata() |
| .has_selection_metadata() && |
| sent_interaction_request.interaction_request_metadata() |
| .selection_metadata() |
| .has_object()) { |
| interaction_data.set_object_id( |
| sent_interaction_request.interaction_request_metadata() |
| .selection_metadata() |
| .object() |
| .object_id()); |
| } else if (sent_interaction_request.has_image_crop()) { |
| // The zoomed crop field should only be set if the object id is not set. |
| interaction_data.mutable_zoomed_crop()->CopyFrom( |
| sent_interaction_request.image_crop().zoomed_crop()); |
| } |
| } else { |
| // If there was no interaction request, then the selection type should be |
| // set to text selection. |
| interaction_data.set_interaction_type( |
| lens::LensOverlayInteractionRequestMetadata::TEXT_SELECTION); |
| } |
| |
| parent_query_sent_ = true; |
| |
| std::string serialized_proto; |
| CHECK(interaction_data.SerializeToString(&serialized_proto)); |
| std::string encoded_proto; |
| base::Base64UrlEncode(serialized_proto, |
| base::Base64UrlEncodePolicy::OMIT_PADDING, |
| &encoded_proto); |
| return encoded_proto; |
| } |
| |
| lens::LensOverlayServerRequest |
| LensOverlayQueryController::CreateInteractionRequest( |
| lens::mojom::CenterRotatedBoxPtr region, |
| std::optional<std::string> query_text, |
| std::optional<std::string> object_id, |
| std::optional<lens::ImageCrop> image_crop, |
| lens::LensOverlayClientLogs client_logs) { |
| lens::LensOverlayServerRequest server_request; |
| server_request.mutable_client_logs()->CopyFrom(client_logs); |
| // The request ID is guaranteed to exist since it is set in the constructor |
| // of latest_interaction_request_data_. |
| DCHECK(latest_interaction_request_data_->request_id_); |
| |
| lens::LensOverlayRequestContext request_context; |
| request_context.mutable_request_id()->CopyFrom( |
| *latest_interaction_request_data_->request_id_); |
| request_context.mutable_client_context()->CopyFrom(CreateClientContext()); |
| server_request.mutable_interaction_request() |
| ->mutable_request_context() |
| ->CopyFrom(request_context); |
| |
| lens::LensOverlayInteractionRequestMetadata interaction_request_metadata; |
| if (!region.is_null() && image_crop.has_value()) { |
| // Add the region for region search and multimodal requests. |
| server_request.mutable_interaction_request() |
| ->mutable_image_crop() |
| ->CopyFrom(*image_crop); |
| interaction_request_metadata.set_type( |
| lens::LensOverlayInteractionRequestMetadata::REGION_SEARCH); |
| interaction_request_metadata.mutable_selection_metadata() |
| ->mutable_region() |
| ->mutable_region() |
| ->CopyFrom(ConvertToServerCenterRotatedBox(region.Clone())); |
| |
| // Add the text, for multimodal requests. |
| if (query_text.has_value()) { |
| interaction_request_metadata.mutable_query_metadata() |
| ->mutable_text_query() |
| ->set_query(*query_text); |
| } |
| } else if (object_id.has_value()) { |
| // Add object request details. |
| interaction_request_metadata.set_type( |
| lens::LensOverlayInteractionRequestMetadata::TAP); |
| interaction_request_metadata.mutable_selection_metadata() |
| ->mutable_object() |
| ->set_object_id(*object_id); |
| } else if (query_text.has_value()) { |
| // If there is only `query_text`, this is a contextual flow. |
| interaction_request_metadata.set_type( |
| ContentTypeToInteractionType(primary_content_type_)); |
| interaction_request_metadata.mutable_query_metadata() |
| ->mutable_text_query() |
| ->set_query(*query_text); |
| } else { |
| // There should be a region or an object id in the request. |
| NOTREACHED(); |
| } |
| |
| server_request.mutable_interaction_request() |
| ->mutable_interaction_request_metadata() |
| ->CopyFrom(interaction_request_metadata); |
| return server_request; |
| } |
| |
| void LensOverlayQueryController::ResetRequestClusterInfoState() { |
| pending_interaction_callback_.Reset(); |
| interaction_endpoint_fetcher_.reset(); |
| cluster_info_.reset(); |
| query_controller_state_ = QueryControllerState::kClusterInfoExpired; |
| request_id_generator_->ResetRequestId(); |
| suggest_inputs_.Clear(); |
| RunSuggestInputsCallback(); |
| parent_query_sent_ = false; |
| is_first_page_contents_request_ = true; |
| is_first_partial_page_contents_request_ = true; |
| } |
| |
| void LensOverlayQueryController::OnFullImageEndpointFetcherCreated( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher) { |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialFullPageObjectsRequestSent, |
| VitQueryParamValueForMimeType(primary_content_type_), request_id); |
| full_image_endpoint_fetcher_ = std::move(endpoint_fetcher); |
| } |
| |
| void LensOverlayQueryController::OnPageContentEndpointFetcherCreated( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher) { |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialPageContentRequestSent, |
| VitQueryParamValueForMimeType(primary_content_type_), request_id); |
| page_content_endpoint_fetcher_ = std::move(endpoint_fetcher); |
| } |
| |
| void LensOverlayQueryController::OnPartialPageContentEndpointFetcherCreated( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher) { |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialPartialPageContentRequestSent, |
| VitQueryParamValueForMimeType(primary_content_type_), request_id); |
| partial_page_content_endpoint_fetcher_ = std::move(endpoint_fetcher); |
| } |
| |
| void LensOverlayQueryController::OnInteractionEndpointFetcherCreated( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher) { |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialInteractionRequestSent, |
| VitQueryParamValueForMimeType(primary_content_type_), request_id); |
| interaction_endpoint_fetcher_ = std::move(endpoint_fetcher); |
| } |
| |
| void LensOverlayQueryController::OnChunkUploadEndpointFetcherCreated( |
| lens::LensOverlayRequestId request_id, |
| std::unique_ptr<EndpointFetcher> endpoint_fetcher) { |
| SendInitialLatencyGen204IfNotAlreadySent( |
| LatencyType::kInvocationToInitialPageContentRequestSent, |
| VitQueryParamValueForMimeType(primary_content_type_), request_id); |
| chunk_upload_endpoint_fetchers_.push_back(std::move(endpoint_fetcher)); |
| } |
| |
| bool LensOverlayQueryController::ShouldSendContextualSearchQuery() { |
| // Can send the query if the page content request has finished. |
| return !page_content_request_in_progress_ && cluster_info_.has_value(); |
| } |
| |
| bool LensOverlayQueryController::IsPartialPageContentSubstantial() { |
| // If the partial page content is empty, exit early. |
| if (partial_content_.empty()) { |
| return false; |
| } |
| |
| // Get the average number of characters per page. |
| int total_characters = 0; |
| for (const std::u16string& page_text : partial_content_) { |
| total_characters += page_text.size(); |
| } |
| const int characters_per_page = total_characters / partial_content_.size(); |
| |
| // If the average is over the scanned pdf character per page heuristic, the |
| // query is considered substantial. |
| return characters_per_page > |
| lens::features::GetScannedPdfCharacterPerPageHeuristic(); |
| } |
| } // namespace lens |