blob: adb8ed8452afa5153ed179cb71a10435434913ce [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/page_image_service/image_service_impl.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/case_conversion.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "components/omnibox/browser/autocomplete_scheme_classifier.h"
#include "components/omnibox/browser/remote_suggestions_service.h"
#include "components/omnibox/browser/search_suggestion_parser.h"
#include "components/optimization_guide/core/optimization_guide_decider.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/proto/common_types.pb.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/optimization_guide/proto/salient_image_metadata.pb.h"
#include "components/page_image_service/features.h"
#include "components/page_image_service/image_service_consent_helper.h"
#include "components/page_image_service/metrics_util.h"
#include "components/search_engines/search_engine_type.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace page_image_service {
namespace {
// Fulfills all `callbacks` with `result`.
void FulfillAllCallbacks(std::vector<ImageService::ResultCallback> callbacks,
const GURL& result) {
for (auto& callback : callbacks) {
std::move(callback).Run(result);
}
}
} // namespace
// A one-time use object that uses Suggest to get an image URL corresponding
// to `search_query` and `entity_id`. This is a hacky temporary implementation,
// ideally this should be replaced by persisted Suggest-provided entities.
// TODO(tommycli): Move this to its own separate file with unit tests.
class ImageServiceImpl::SuggestEntityImageURLFetcher {
public:
SuggestEntityImageURLFetcher(
const AutocompleteSchemeClassifier& autocomplete_scheme_classifier,
mojom::ClientId client_id,
const std::u16string& search_query,
const std::string& entity_id)
: autocomplete_scheme_classifier_(autocomplete_scheme_classifier),
client_id_(client_id),
search_query_(base::i18n::ToLower(search_query)),
entity_id_(entity_id) {}
SuggestEntityImageURLFetcher(const SuggestEntityImageURLFetcher&) = delete;
// `callback` is called with the result.
void Start(const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
RemoteSuggestionsService* remote_suggestions_service,
base::OnceCallback<void(const GURL&)> callback) {
CHECK(template_url);
CHECK(remote_suggestions_service);
CHECK(!callback_);
callback_ = std::move(callback);
TemplateURLRef::SearchTermsArgs search_terms_args;
search_terms_args.page_classification =
metrics::OmniboxEventProto::JOURNEYS;
search_terms_args.search_terms = search_query_;
// ImageServiceFactory does not create a service instance for OTR profiles.
loader_ = remote_suggestions_service->StartSuggestionsRequest(
RemoteRequestType::kImages, /*is_off_the_record=*/false, template_url,
search_terms_args, search_terms_data,
base::BindOnce(&SuggestEntityImageURLFetcher::OnURLLoadComplete,
weak_factory_.GetWeakPtr()));
}
private:
void OnURLLoadComplete(const network::SimpleURLLoader* source,
const int response_code,
std::unique_ptr<std::string> response_body) {
DCHECK_EQ(loader_.get(), source);
if (response_code != 200) {
UmaHistogramEnumerationForClient(kBackendSuggestResultHistogramName,
PageImageServiceResult::kResponseMissing,
client_id_);
return std::move(callback_).Run(GURL());
}
std::string response_json = SearchSuggestionParser::ExtractJsonData(
source, std::move(response_body));
if (response_json.empty()) {
UmaHistogramEnumerationForClient(
kBackendSuggestResultHistogramName,
PageImageServiceResult::kResponseMalformed, client_id_);
return std::move(callback_).Run(GURL());
}
auto response_data =
SearchSuggestionParser::DeserializeJsonData(response_json);
if (!response_data) {
UmaHistogramEnumerationForClient(
kBackendSuggestResultHistogramName,
PageImageServiceResult::kResponseMalformed, client_id_);
return std::move(callback_).Run(GURL());
}
AutocompleteInput input(search_query_, metrics::OmniboxEventProto::JOURNEYS,
*autocomplete_scheme_classifier_);
SearchSuggestionParser::Results results;
if (!SearchSuggestionParser::ParseSuggestResults(
*response_data, input, *autocomplete_scheme_classifier_,
/*default_result_relevance=*/100,
/*is_keyword_result=*/false, &results)) {
UmaHistogramEnumerationForClient(
kBackendSuggestResultHistogramName,
PageImageServiceResult::kResponseMalformed, client_id_);
return std::move(callback_).Run(GURL());
}
for (const auto& result : results.suggest_results) {
// TODO(tommycli): `entity_id_` is not used yet, because it's always
// empty right now.
GURL url(result.entity_info().image_url());
if (url.is_valid() &&
base::i18n::ToLower(result.match_contents()) == search_query_) {
UmaHistogramEnumerationForClient(kBackendSuggestResultHistogramName,
PageImageServiceResult::kSuccess,
client_id_);
return std::move(callback_).Run(std::move(url));
}
}
// If we didn't find any matching images, still notify the caller.
if (!callback_.is_null()) {
UmaHistogramEnumerationForClient(kBackendSuggestResultHistogramName,
PageImageServiceResult::kNoImage,
client_id_);
std::move(callback_).Run(GURL());
}
}
// Embedder-specific logic on how to classify schemes.
const raw_ref<const AutocompleteSchemeClassifier>
autocomplete_scheme_classifier_;
// The id of the UI requesting the image.
mojom::ClientId client_id_;
// The search query and entity ID we are searching for.
const std::u16string search_query_;
const std::string entity_id_;
// The result callback to be called once we get the answer.
base::OnceCallback<void(const GURL&)> callback_;
// The URL loader used to get the suggestions.
std::unique_ptr<network::SimpleURLLoader> loader_;
base::WeakPtrFactory<SuggestEntityImageURLFetcher> weak_factory_{this};
};
ImageServiceImpl::ImageServiceImpl(
TemplateURLService* template_url_service,
RemoteSuggestionsService* remote_suggestions_service,
optimization_guide::OptimizationGuideDecider* opt_guide,
syncer::SyncService* sync_service,
std::unique_ptr<AutocompleteSchemeClassifier>
autocomplete_scheme_classifier)
: template_url_service_(template_url_service),
remote_suggestions_service_(remote_suggestions_service),
history_consent_helper_(std::make_unique<ImageServiceConsentHelper>(
sync_service,
syncer::DataType::HISTORY_DELETE_DIRECTIVES)),
bookmarks_consent_helper_(std::make_unique<ImageServiceConsentHelper>(
sync_service,
syncer::DataType::BOOKMARKS)),
autocomplete_scheme_classifier_(
std::move(autocomplete_scheme_classifier)) {
if (opt_guide && base::FeatureList::IsEnabled(
kImageServiceOptimizationGuideSalientImages)) {
opt_guide_ = opt_guide;
}
}
ImageServiceImpl::OptGuideRequest::OptGuideRequest() = default;
ImageServiceImpl::OptGuideRequest::~OptGuideRequest() = default;
ImageServiceImpl::OptGuideRequest::OptGuideRequest(OptGuideRequest&& other) =
default;
ImageServiceImpl::~ImageServiceImpl() = default;
base::WeakPtr<ImageService> ImageServiceImpl::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void ImageServiceImpl::FetchImageFor(mojom::ClientId client_id,
const GURL& page_url,
const mojom::Options& options,
ResultCallback callback) {
if (!base::FeatureList::IsEnabled(kImageService)) {
// In general this should never happen, because each UI should have its own
// feature gate, but this is just so we have a whole-service killswitch.
return std::move(callback).Run(GURL());
}
GetConsentToFetchImage(
client_id,
base::BindOnce(&ImageServiceImpl::OnConsentResult, weak_factory_.GetWeakPtr(),
client_id, page_url, options, std::move(callback)));
}
void ImageServiceImpl::GetConsentToFetchImage(
mojom::ClientId client_id,
base::OnceCallback<void(PageImageServiceConsentStatus)> callback) {
switch (client_id) {
case mojom::ClientId::Journeys:
case mojom::ClientId::JourneysSidePanel:
case mojom::ClientId::HistoryEmbeddings:
case mojom::ClientId::NtpQuests:
case mojom::ClientId::NtpTabResumption: {
return history_consent_helper_->EnqueueRequest(std::move(callback),
client_id);
}
case mojom::ClientId::NtpRealbox:
// TODO(b/244507194): Figure out consent story for NTP realbox case.
return std::move(callback).Run(PageImageServiceConsentStatus::kFailure);
case mojom::ClientId::Bookmarks: {
return bookmarks_consent_helper_->EnqueueRequest(std::move(callback),
client_id);
}
}
}
void ImageServiceImpl::OnConsentResult(mojom::ClientId client_id,
const GURL& page_url,
const mojom::Options& options,
ResultCallback callback,
PageImageServiceConsentStatus status) {
base::UmaHistogramEnumeration(kConsentStatusHistogramName, status);
base::UmaHistogramEnumeration(std::string(kConsentStatusHistogramName) + "." +
ClientIdToString(client_id),
status);
if (status != PageImageServiceConsentStatus::kSuccess) {
return std::move(callback).Run(GURL());
}
if (options.suggest_images &&
base::FeatureList::IsEnabled(kImageServiceSuggestPoweredImages) &&
template_url_service_ && remote_suggestions_service_) {
auto search_metadata =
template_url_service_->ExtractSearchMetadata(page_url);
// Fetch entity-keyed images for Google SRP visits only, because only
// Google SRP visits can expect to have a reasonable entity from Google
// Suggest.
if (search_metadata && search_metadata->template_url &&
search_metadata->template_url->GetEngineType(
template_url_service_->search_terms_data()) ==
SEARCH_ENGINE_GOOGLE) {
UmaHistogramEnumerationForClient(
kBackendHistogramName, PageImageServiceBackend::kSuggest, client_id);
return FetchSuggestImage(search_metadata->template_url,
template_url_service_->search_terms_data(),
client_id,
/*search_query=*/search_metadata->search_terms,
/*entity_id=*/"", std::move(callback));
}
}
if (options.optimization_guide_images && opt_guide_ &&
base::FeatureList::IsEnabled(
kImageServiceOptimizationGuideSalientImages)) {
UmaHistogramEnumerationForClient(
kBackendHistogramName, PageImageServiceBackend::kOptimizationGuide,
client_id);
return FetchOptimizationGuideImage(client_id, page_url,
std::move(callback));
}
UmaHistogramEnumerationForClient(kBackendHistogramName,
PageImageServiceBackend::kNoValidBackend,
client_id);
std::move(callback).Run(GURL());
}
void ImageServiceImpl::FetchSuggestImage(const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
mojom::ClientId client_id,
const std::u16string& search_query,
const std::string& entity_id,
ResultCallback callback) {
auto fetcher = std::make_unique<SuggestEntityImageURLFetcher>(
*autocomplete_scheme_classifier_, client_id, search_query, entity_id);
// Use a raw pointer temporary so we can give ownership of the unique_ptr to
// the callback and have a well defined SuggestEntityImageURLFetcher lifetime.
auto* fetcher_raw_ptr = fetcher.get();
fetcher_raw_ptr->Start(
template_url, search_terms_data, remote_suggestions_service_,
base::BindOnce(&ImageServiceImpl::OnSuggestImageFetched,
weak_factory_.GetWeakPtr(), std::move(fetcher),
std::move(callback)));
}
void ImageServiceImpl::OnSuggestImageFetched(
std::unique_ptr<SuggestEntityImageURLFetcher> fetcher,
ResultCallback callback,
const GURL& image_url) {
std::move(callback).Run(image_url);
// `fetcher` is owned by this method and will be deleted now.
}
void ImageServiceImpl::FetchOptimizationGuideImage(mojom::ClientId client_id,
const GURL& page_url,
ResultCallback callback) {
DCHECK(opt_guide_) << "FetchOptimizationGuideImage is never called when "
"opt_guide_ is nullptr.";
OptGuideRequest request;
request.url = page_url;
request.callback = std::move(callback);
auto& request_list = unsent_opt_guide_requests_[client_id];
request_list.push_back(std::move(request));
if (request_list.size() >=
optimization_guide::features::
MaxUrlsForOptimizationGuideServiceHintsFetch()) {
// Erasing the timer also cancels the timer callback.
opt_guide_timers_.erase(client_id);
ProcessAllBatchedOptimizationGuideRequests(client_id);
} else if (request_list.size() == 1U) {
// Otherwise, if we just enqueued our FIRST request, then kick off a timer
// to flush the queue. One millisecond is a long enough time in CPU time.
auto timer = std::make_unique<base::OneShotTimer>();
timer->Start(FROM_HERE, kOptimizationGuideBatchingTimeout,
base::BindOnce(
&ImageServiceImpl::ProcessAllBatchedOptimizationGuideRequests,
weak_factory_.GetWeakPtr(), client_id));
opt_guide_timers_[client_id] = std::move(timer);
}
}
void ImageServiceImpl::ProcessAllBatchedOptimizationGuideRequests(
mojom::ClientId client_id) {
optimization_guide::proto::RequestContext request_context;
switch (client_id) {
case mojom::ClientId::Journeys:
case mojom::ClientId::JourneysSidePanel:
case mojom::ClientId::HistoryEmbeddings: {
request_context = optimization_guide::proto::CONTEXT_JOURNEYS;
break;
}
case mojom::ClientId::NtpQuests:
case mojom::ClientId::NtpRealbox:
case mojom::ClientId::NtpTabResumption: {
request_context = optimization_guide::proto::CONTEXT_NEW_TAB_PAGE;
break;
}
case mojom::ClientId::Bookmarks: {
request_context = optimization_guide::proto::CONTEXT_BOOKMARKS;
break;
}
}
std::vector<OptGuideRequest>& unsent_requests =
unsent_opt_guide_requests_[client_id];
if (unsent_requests.empty()) {
return;
}
// Generate a list of URLs to request in this batch.
std::vector<GURL> urls;
for (auto& request : unsent_requests) {
urls.push_back(request.url);
}
// Move the list of unsent requests to the sent vector.
for (auto& request : unsent_requests) {
sent_opt_guide_requests_[client_id].push_back(std::move(request));
}
unsent_requests.clear();
opt_guide_->CanApplyOptimizationOnDemand(
urls, {optimization_guide::proto::OptimizationType::SALIENT_IMAGE},
request_context,
base::BindRepeating(&ImageServiceImpl::OnOptimizationGuideImageFetched,
weak_factory_.GetWeakPtr(), client_id));
}
void ImageServiceImpl::OnOptimizationGuideImageFetched(
mojom::ClientId client_id,
const GURL& url,
const base::flat_map<
optimization_guide::proto::OptimizationType,
optimization_guide::OptimizationGuideDecisionWithMetadata>& decisions) {
// Extract all waiting callbacks matching `url` to `matching_callbacks`.
std::vector<ResultCallback> matching_callbacks;
{
// Take over the existing whole list via a swap.
std::vector<OptGuideRequest> all_requests;
std::swap(all_requests, sent_opt_guide_requests_[client_id]);
// Steal the matching callbacks, pushing back the other pending requests
// back to the original list.
for (auto& request : all_requests) {
if (request.url == url) {
matching_callbacks.push_back(std::move(request.callback));
} else {
sent_opt_guide_requests_[client_id].push_back(std::move(request));
}
}
}
auto iter = decisions.find(optimization_guide::proto::SALIENT_IMAGE);
if (iter == decisions.end()) {
UmaHistogramEnumerationForClient(
kBackendOptimizationGuideResultHistogramName,
PageImageServiceResult::kResponseMissing, client_id);
return FulfillAllCallbacks(std::move(matching_callbacks), GURL());
}
optimization_guide::OptimizationGuideDecisionWithMetadata decision =
iter->second;
if (decision.decision !=
optimization_guide::OptimizationGuideDecision::kTrue) {
UmaHistogramEnumerationForClient(
kBackendOptimizationGuideResultHistogramName,
PageImageServiceResult::kNoImage, client_id);
return FulfillAllCallbacks(std::move(matching_callbacks), GURL());
}
if (!decision.metadata.any_metadata().has_value()) {
UmaHistogramEnumerationForClient(
kBackendOptimizationGuideResultHistogramName,
PageImageServiceResult::kResponseMalformed, client_id);
return FulfillAllCallbacks(std::move(matching_callbacks), GURL());
}
auto parsed_any = optimization_guide::ParsedAnyMetadata<
optimization_guide::proto::SalientImageMetadata>(
decision.metadata.any_metadata().value());
if (!parsed_any) {
UmaHistogramEnumerationForClient(
kBackendOptimizationGuideResultHistogramName,
PageImageServiceResult::kResponseMalformed, client_id);
return FulfillAllCallbacks(std::move(matching_callbacks), GURL());
}
// Look through the metadata, returning the first valid image URL.
auto salient_image_metadata = *parsed_any;
for (const auto& thumbnail : salient_image_metadata.thumbnails()) {
if (thumbnail.has_image_url()) {
GURL image_url(thumbnail.image_url());
if (image_url.is_valid() && image_url.SchemeIs(url::kHttpsScheme)) {
UmaHistogramEnumerationForClient(
kBackendOptimizationGuideResultHistogramName,
PageImageServiceResult::kSuccess, client_id);
return FulfillAllCallbacks(std::move(matching_callbacks), image_url);
}
}
}
// Fail if we can't find any.
UmaHistogramEnumerationForClient(kBackendOptimizationGuideResultHistogramName,
PageImageServiceResult::kResponseMalformed,
client_id);
return FulfillAllCallbacks(std::move(matching_callbacks), GURL());
}
} // namespace page_image_service