blob: 8421782a5efebeb90956290288222def1f01be12 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/aim_eligibility_service.h"
#include <memory>
#include <string>
#include "base/base64.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "components/omnibox/browser/aim_eligibility_service_observer.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/search/search.h"
#include "components/search_engines/template_url_service.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "third_party/omnibox_proto/aim_eligibility_response.pb.h"
#include "url/gurl.h"
namespace {
// If disabled, AIM is completely turned off (kill switch).
BASE_FEATURE(kAimEnabled, "AimEnabled", base::FEATURE_ENABLED_BY_DEFAULT);
// If enabled, uses the server response for AIM eligibility for all locales.
BASE_FEATURE(kAimServerEligibilityEnabled,
"AimServerEligibilityEnabled",
base::FEATURE_DISABLED_BY_DEFAULT);
// If enabled, uses the server response for AIM eligibility for English locales.
// Has no effect if kAimServerEligibilityEnabled is enabled.
BASE_FEATURE(kAimServerEligibilityEnabledEn,
"AimServerEligibilityEnabledEn",
base::FEATURE_DISABLED_BY_DEFAULT);
// For recording UMA metrics. These aren't strictly omnibox-only, but omnibox is
// a major consumer of `AimEligibilityService`, and the few metrics here don't
// warrant creating a new metric namespace.
// The status of the server request. See `ServerRequestStatus`.
static constexpr char kUmaServerRequestStatusHistogramName[] =
"Omnibox.AimEligibility.ServerRequestStatus";
// Which AIM features were eligible according to the server request.
static constexpr char kUmaServerEligibilityHistogramPrefix[] =
"Omnibox.AimEligibility.ServerEligibility.";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange(ServerAimEligibilityRequestStatus)
enum class ServerRequestStatus {
kSent = 0,
kErrorResponse = 1,
kFailedToParse = 2,
kSuccess = 3,
kMaxValue = kSuccess,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/omnibox/enums.xml:ServerAimEligibilityRequestStatus)
static constexpr char kRequestEndpoint[] =
"http://www.google.com/async/folae?async=_fmt:pb";
const net::NetworkTrafficAnnotationTag kRequestTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("aim_eligibility_fetch", R"(
semantics {
sender: "Chrome AI Mode Eligibility Service"
description:
"Retrieves the set of AI Mode features the client is eligible for "
"from the server."
trigger:
"Requests are made on startup, when user's profile state changes, "
"and periodically while Chrome is running."
user_data {
type: NONE
}
data:
"No request body is sent; this is a GET request with no query params."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts { email: "chrome-desktop-search@google.com" }
}
last_reviewed: "2025-08-06"
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting: "Coupled to Google default search."
policy_exception_justification:
"Not gated by policy. Setting AIModeSetting to '1' prevents the "
"response from being used. But Google Chrome still makes the "
"requests and saves the response to disk so that it's available when "
"the policy is unset."
})");
} // namespace
// static
void AimEligibilityService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterStringPref(kResponsePrefName, "");
}
AimEligibilityService::AimEligibilityService(
PrefService& pref_service,
TemplateURLService& template_url_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: pref_service_(pref_service),
template_url_service_(template_url_service),
url_loader_factory_(url_loader_factory) {
// TODO(crbug.com/436898763): Call `StartServerEligibilityRequest()` to
// refresh the server response when service is constructed and when user state
// changes. E.g. user signs in/out, starts/stops syncing, switches profiles.
// Some of those actions may create a new service; if so, we don't need to
// listen to those events and start StartServerEligibilityRequest manually,
// because it'll be called in the constructor anyways. Switching profiles
// probably creates a new service.
ReadFromPref();
}
AimEligibilityService::~AimEligibilityService() = default;
bool AimEligibilityService::IsCountry(const std::string& country) const {
// Country codes are in lowercase ISO 3166-1 alpha-2 format; e.g., us, br, in.
// See components/variations/service/variations_service.h
return GetCountryCode() == country;
}
bool AimEligibilityService::IsLanguage(const std::string& language) const {
// Locale follows BCP 47 format; e.g., en-US, fr-FR, ja-JP.
// See ui/base/l10n/l10n_util.h
return base::StartsWith(GetLocale(), language, base::CompareCase::SENSITIVE);
}
void AimEligibilityService::AddObserver(
AimEligibilityServiceObserver* observer) {
observers_.AddObserver(observer);
}
void AimEligibilityService::RemoveObserver(
AimEligibilityServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
bool AimEligibilityService::IsServerEligibilityEnabled() const {
return base::FeatureList::IsEnabled(kAimServerEligibilityEnabled) ||
(base::FeatureList::IsEnabled(kAimServerEligibilityEnabledEn) &&
IsLanguage("en"));
}
bool AimEligibilityService::IsAimLocallyEligible() const {
// Kill switch: If AIM is completely disabled, return false.
if (!base::FeatureList::IsEnabled(kAimEnabled)) {
return false;
}
// Always check Google DSE and Policy requirements.
if (!search::DefaultSearchProviderIsGoogle(&template_url_service_.get()) ||
!omnibox::IsAimAllowedByPolicy(&pref_service_.get())) {
return false;
}
return true;
}
bool AimEligibilityService::IsAimEligible() const {
// Check local eligibility first.
if (!IsAimLocallyEligible()) {
return false;
}
// Conditionally check server response eligibility requirement.
if (IsServerEligibilityEnabled()) {
return most_recent_response_.is_eligible();
}
return true;
}
void AimEligibilityService::NotifyObservers() const {
for (auto& observer : observers_) {
observer.OnAimEligibilityChanged();
}
}
bool AimEligibilityService::ParseResponseString(
const std::string& response_string) {
// Parse into a temporary variable 1st so that if parsing fails,
// `most_recent_response_` isn't cleared.
omnibox::AimEligibilityResponse response_proto;
if (!response_proto.ParseFromString(response_string)) {
return false;
}
most_recent_response_ = response_proto;
return true;
}
void AimEligibilityService::WriteToPref(
const std::string& response_string) const {
pref_service_->SetString(kResponsePrefName,
base::Base64Encode(response_string));
}
void AimEligibilityService::ReadFromPref() {
const std::string& read_string = pref_service_->GetString(kResponsePrefName);
std::string decoded;
if (base::Base64Decode(read_string, &decoded)) {
ParseResponseString(decoded);
}
}
void AimEligibilityService::StartServerEligibilityRequest() {
// Don't make server requests if AIM or server requests are disabled.
if (!base::FeatureList::IsEnabled(kAimEnabled) ||
!IsServerEligibilityEnabled()) {
return;
}
std::unique_ptr<network::ResourceRequest> request =
std::make_unique<network::ResourceRequest>();
request->url = GURL{kRequestEndpoint};
request->credentials_mode = network::mojom::CredentialsMode::kInclude;
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(request),
kRequestTrafficAnnotation);
base::UmaHistogramEnumeration(kUmaServerRequestStatusHistogramName,
ServerRequestStatus::kSent);
loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&AimEligibilityService::OnServerEligibilityResponse,
weak_factory_.GetWeakPtr(), std::move(loader)));
}
void AimEligibilityService::OnServerEligibilityResponse(
std::unique_ptr<network::SimpleURLLoader> loader,
std::unique_ptr<std::string> response_string) {
// TODO(crbug.com/436900259): Add UMA metrics for whether the response
// returned 200, was parsed successfully, and which features were eligible.
// This will let us know how watered down UMA and finch are compared due to
// mismatched server eligibility criteria and estimate the actual population
// size.
if (!response_string) {
base::UmaHistogramEnumeration(kUmaServerRequestStatusHistogramName,
ServerRequestStatus::kErrorResponse);
return;
}
if (!ParseResponseString(*response_string)) {
base::UmaHistogramEnumeration(kUmaServerRequestStatusHistogramName,
ServerRequestStatus::kFailedToParse);
return;
}
base::UmaHistogramEnumeration(kUmaServerRequestStatusHistogramName,
ServerRequestStatus::kSuccess);
base::UmaHistogramBoolean(
base::StrCat({kUmaServerEligibilityHistogramPrefix, "is_eligible"}),
most_recent_response_.is_eligible());
WriteToPref(*response_string);
NotifyObservers();
}