blob: 40a5d08dddc00d047121c479da1ae21bd7c613f8 [file] [log] [blame]
// Copyright 2023 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/compose/compose_enabling.h"
#include <functional>
#include <memory>
#include <tuple>
#include <type_traits>
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "chrome/browser/about_flags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/compose/proto/compose_optimization_guide.pb.h"
#include "chrome/browser/flag_descriptions.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/common/pref_names.h"
#include "components/compose/buildflags.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/compose/core/browser/compose_metrics.h"
#include "components/compose/core/browser/config.h"
#include "components/optimization_guide/core/model_execution/feature_keys.h"
#include "components/prefs/pref_service.h"
#include "components/variations/service/variations_service.h"
#include "components/variations/service/variations_service_utils.h"
#include "components/webui/flags/feature_entry.h"
#include "components/webui/flags/flags_storage.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_frame_host.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/constants/chromeos_features.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace {
bool AutocompleteAllowed(std::string_view autocomplete_attribute) {
// Check autocomplete is not turned off.
return autocomplete_attribute != std::string("off");
}
std::unique_ptr<std::string>& GetCountryCodeOverride() {
static base::NoDestructor<std::unique_ptr<std::string>> country_code_override(
nullptr);
return *country_code_override;
}
std::string GetCountryCode() {
if (GetCountryCodeOverride()) {
return *GetCountryCodeOverride();
}
std::string country_code =
base::ToLowerASCII(variations::GetCurrentCountryCode(
g_browser_process->variations_service()));
DLOG_IF(WARNING, country_code.empty()) << "Couldn't get country info.";
return country_code;
}
// Given a set of countries checks if the current variations country is in the
// list. A list with a single item that is "*" will accept all countries.
// Return tuple: (current_country_code, enabled_for_country).
std::tuple<std::string, bool> IsEnabledForCountry(
const base::flat_set<std::string>& enabled_countries) {
std::string country_code = GetCountryCode();
if (enabled_countries.size() == 1 && enabled_countries.contains("*")) {
return {country_code, true};
}
return {country_code, enabled_countries.contains(country_code)};
}
} // namespace
// Static members' initializers.
int ComposeEnabling::enabled_for_testing_{0};
int ComposeEnabling::skip_user_check_for_testing_{0};
ComposeEnabling::ComposeEnabling(
Profile* profile,
signin::IdentityManager* identity_manager,
OptimizationGuideKeyedService* opt_guide)
: profile_(profile),
opt_guide_(opt_guide),
identity_manager_(identity_manager) {
DCHECK(profile_);
}
ComposeEnabling::~ComposeEnabling() {
opt_guide_ = nullptr;
identity_manager_ = nullptr;
profile_ = nullptr;
}
// Static.
ComposeEnabling::ScopedOverride
ComposeEnabling::ScopedEnableComposeForTesting() {
enabled_for_testing_++;
return std::make_unique<base::ScopedClosureRunner>(base::BindOnce(
[](int& enabled_for_testing) {
enabled_for_testing--;
DCHECK(enabled_for_testing >= 0);
},
std::ref(enabled_for_testing_)));
}
// Static.
ComposeEnabling::ScopedOverride
ComposeEnabling::ScopedSkipUserCheckForTesting() {
skip_user_check_for_testing_++;
return std::make_unique<base::ScopedClosureRunner>(base::BindOnce(
[](int& skip_user_check_for_testing) {
skip_user_check_for_testing--;
DCHECK(skip_user_check_for_testing >= 0);
},
std::ref(skip_user_check_for_testing_)));
}
// Static.
ComposeEnabling::ScopedOverride ComposeEnabling::OverrideCountryForTesting(
std::string country_code) {
CHECK(!GetCountryCodeOverride());
GetCountryCodeOverride() = std::make_unique<std::string>(country_code);
return std::make_unique<base::ScopedClosureRunner>(
base::BindOnce([]() { GetCountryCodeOverride().reset(); }));
}
compose::ComposeHintDecision ComposeEnabling::GetOptimizationGuidanceForUrl(
const GURL& url,
Profile* profile) {
if (!opt_guide_) {
DVLOG(2) << "Optimization guide not found, returns unspecified";
return compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED;
}
optimization_guide::OptimizationMetadata metadata;
auto opt_guide_has_hint = opt_guide_->CanApplyOptimization(
url, optimization_guide::proto::OptimizationType::COMPOSE, &metadata);
if (opt_guide_has_hint !=
optimization_guide::OptimizationGuideDecision::kTrue) {
DVLOG(2) << "Optimization guide has no hint, returns unspecified";
return compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED;
}
std::optional<compose::ComposeHintMetadata> compose_metadata;
if (metadata.any_metadata().has_value()) {
compose_metadata =
optimization_guide::ParsedAnyMetadata<compose::ComposeHintMetadata>(
metadata.any_metadata().value());
}
if (!compose_metadata.has_value()) {
DVLOG(2) << "Optimization guide has no metadata, returns unspecified";
return compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED;
}
DVLOG(2) << "Optimization guide returns enum "
<< static_cast<int>(compose_metadata->decision());
return compose_metadata->decision();
}
// Member function public entry point.
base::expected<void, compose::ComposeShowStatus> ComposeEnabling::IsEnabled() {
return CheckEnabling(opt_guide_, identity_manager_);
}
// Static public entry point.
bool ComposeEnabling::IsEnabledForProfile(Profile* profile) {
OptimizationGuideKeyedService* opt_guide =
OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfileIfExists(profile);
return CheckEnabling(opt_guide, identity_manager).has_value();
}
bool ComposeEnabling::IsSettingVisible(Profile* profile) {
OptimizationGuideKeyedService* opt_guide =
OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfileIfExists(profile);
auto enabled = CheckEnabling(opt_guide, identity_manager);
if (!enabled.has_value() &&
enabled.error() ==
compose::ComposeShowStatus::kUserNotAllowedByOptimizationGuide) {
return opt_guide->IsSettingVisible(
optimization_guide::UserVisibleFeatureKey::kCompose);
}
return enabled.has_value();
}
// Private static.
base::expected<void, compose::ComposeShowStatus> ComposeEnabling::CheckEnabling(
OptimizationGuideKeyedService* opt_guide,
signin::IdentityManager* identity_manager) {
if (enabled_for_testing_) {
DVLOG(2) << "enabled for testing";
return base::ok();
}
if (identity_manager == nullptr || opt_guide == nullptr) {
DVLOG(2) << "feature not reachable, a required pointer is nullptr";
return base::unexpected(compose::ComposeShowStatus::kGenericBlocked);
}
// Check if the compose feature is still eligible.
if (!base::FeatureList::IsEnabled(compose::features::kComposeEligible)) {
DVLOG(2) << "feature not eligible";
return base::unexpected(compose::ComposeShowStatus::kNotComposeEligible);
}
// Check that the feature flag is enabled.
if (!base::FeatureList::IsEnabled(compose::features::kEnableCompose)) {
DVLOG(2) << "feature not enabled ";
return base::unexpected(
compose::ComposeShowStatus::kComposeFeatureFlagDisabled);
}
// Check if we're running in an enabled country. Note that an empty country
// code will cause Compose to be disabled.
std::string country_code;
bool is_enabled_for_country;
std::tie(country_code, is_enabled_for_country) =
IsEnabledForCountry(compose::GetComposeConfig().enabled_countries);
if (!is_enabled_for_country) {
DVLOG(2) << "not running in an enabled country: \"" << country_code << "\"";
return base::unexpected(
country_code.empty()
? compose::ComposeShowStatus::kUndefinedCountry
: compose::ComposeShowStatus::kComposeNotEnabledInCountry);
}
// Check signin status.
CoreAccountInfo core_account_info =
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
if (core_account_info.IsEmpty() ||
identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
core_account_info.account_id)) {
DVLOG(2) << "user not signed in";
return base::unexpected(compose::ComposeShowStatus::kSignedOut);
}
// TODO(b/314199871): Remove test bypass once this check becomes mock-able.
if (!skip_user_check_for_testing_ &&
(!opt_guide->ShouldFeatureBeCurrentlyEnabledForUser(
optimization_guide::UserVisibleFeatureKey::kCompose))) {
DVLOG(2) << "Feature not available for this user";
return base::unexpected(
compose::ComposeShowStatus::kUserNotAllowedByOptimizationGuide);
}
// For ChromeOS only, check whether this device is supported.
#if BUILDFLAG(IS_CHROMEOS)
if (chromeos::features::ShouldDisableChromeComposeOnChromeOS()) {
DVLOG(2) << "feature disabled on ChromeOS";
return base::unexpected(compose::ComposeShowStatus::kDisabledOnChromeOS);
}
#endif // BUILDFLAG(IS_CHROMEOS)
DVLOG(2) << "enabled";
return base::ok();
}
base::expected<void, compose::ComposeShowStatus>
ComposeEnabling::ShouldTriggerNoStatePopup(
std::string_view autocomplete_attribute,
bool allows_writing_suggestions,
Profile* profile,
PrefService* prefs,
translate::TranslateManager* translate_manager,
const url::Origin& top_level_frame_origin,
const url::Origin& element_frame_origin,
GURL url,
bool is_msbb_enabled) {
// Check if we're running in a country where the no state popup is enabled.
// Note that an empty country code will block the no state popup.
std::string country_code;
bool is_enabled_for_country;
std::tie(country_code, is_enabled_for_country) = IsEnabledForCountry(
compose::GetComposeConfig().proactive_nudge_countries);
if (!is_enabled_for_country) {
DVLOG(2) << "not running in an enabled country: \"" << country_code << "\"";
return base::unexpected(
country_code.empty()
? compose::ComposeShowStatus::kUndefinedCountry
: compose::ComposeShowStatus::kComposeNotEnabledInCountry);
}
// TODO(b/319661274): Support fenced frame checks from the Autofill popup
// entry point.
bool is_in_fenced_frame = false;
if (auto page_checks =
PageLevelChecks(translate_manager, url, top_level_frame_origin,
element_frame_origin, is_in_fenced_frame);
!page_checks.has_value()) {
return base::unexpected(page_checks.error());
}
// The no state popup should not show for unsupported languages even if the
// language bypass feature is enabled.
if (!IsPageLanguageSupported(translate_manager)) {
DVLOG(2) << "language not supported";
return base::unexpected(compose::ComposeShowStatus::kUnsupportedLanguage);
}
if (!is_msbb_enabled) {
return base::unexpected(
compose::ComposeShowStatus::kProactiveNudgeDisabledByMSBB);
}
// Check URL with Optimization guide.
switch (GetOptimizationGuidanceForUrl(url, profile)) {
case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_COMPOSE_DISABLED:
return base::unexpected(compose::ComposeShowStatus::kPerUrlChecksFailed);
case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_DISABLE_NUDGE:
if (!compose::GetComposeConfig()
.proactive_nudge_bypass_optimization_guide) {
return base::unexpected(
compose::ComposeShowStatus::kProactiveNudgeDisabledByServerConfig);
}
break;
case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED:
if (!base::FeatureList::IsEnabled(
compose::features::kEnableNudgeForUnspecifiedHint)) {
return base::unexpected(
compose::ComposeShowStatus::kProactiveNudgeUnknownServerConfig);
}
break;
case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_ENABLED:
break;
}
// Check autocomplete attribute if the proactive nudge would be presented.
// TODO(b/303288183): Decide if we should keep this check or not.
if (!AutocompleteAllowed(autocomplete_attribute)) {
DVLOG(2) << "autocomplete=off";
return base::unexpected(compose::ComposeShowStatus::kAutocompleteOff);
}
if (!allows_writing_suggestions) {
DVLOG(2) << "writingsuggestions=false";
return base::unexpected(
compose::ComposeShowStatus::kWritingSuggestionsFalse);
}
if (!prefs->GetBoolean(prefs::kEnableProactiveNudge)) {
return base::unexpected(
compose::ComposeShowStatus::
kProactiveNudgeDisabledGloballyByUserPreference);
}
if (prefs->GetDict(prefs::kProactiveNudgeDisabledSitesWithTime)
.Find(element_frame_origin.Serialize())) {
return base::unexpected(compose::ComposeShowStatus::
kProactiveNudgeDisabledForSiteByUserPreference);
}
if (!compose::GetComposeConfig().proactive_nudge_enabled) {
return base::unexpected(
compose::ComposeShowStatus::kProactiveNudgeFeatureDisabled);
}
return base::ok();
}
bool ComposeEnabling::ShouldTriggerSavedStatePopup(
autofill::AutofillSuggestionTriggerSource trigger_source) {
// No need to preform field and page level checks since there is already saved
// state. Only check config and features.
if (!compose::GetComposeConfig().saved_state_nudge_enabled) {
return false;
}
if (trigger_source ==
autofill::AutofillSuggestionTriggerSource::kComposeDialogLostFocus &&
!base::FeatureList::IsEnabled(
compose::features::kEnableComposeSavedStateNotification)) {
return false;
}
return true;
}
bool ComposeEnabling::ShouldTriggerContextMenu(
Profile* profile,
translate::TranslateManager* translate_manager,
content::RenderFrameHost* rfh,
content::ContextMenuParams& params) {
// Make sure the underlying field is one the feature works for.
if (!(params.is_content_editable_for_autofill ||
(params.form_control_type &&
*params.form_control_type ==
blink::mojom::FormControlType::kTextArea))) {
compose::LogComposeContextMenuShowStatus(
compose::ComposeShowStatus::kIncompatibleFieldType);
DVLOG(2) << "not a supported text field";
return false;
}
// Get the page URL of the outermost frame.
GURL url = rfh->GetMainFrame()->GetLastCommittedURL();
// Check URL with the optimization guide.
compose::ComposeHintDecision decision =
GetOptimizationGuidanceForUrl(url, profile);
if (decision ==
compose::ComposeHintDecision::COMPOSE_HINT_DECISION_COMPOSE_DISABLED) {
compose::LogComposeContextMenuShowStatus(
compose::ComposeShowStatus::kPerUrlChecksFailed);
DVLOG(2) << "disabled for the main frame URL";
return false;
}
auto show_status = PageLevelChecks(
translate_manager, url, rfh->GetMainFrame()->GetLastCommittedOrigin(),
params.frame_origin, rfh->IsNestedWithinFencedFrame());
if (!show_status.has_value()) {
compose::LogComposeContextMenuShowStatus(show_status.error());
DVLOG(2) << "page level checks failed";
return false;
}
if (!base::FeatureList::IsEnabled(
compose::features::kEnableComposeLanguageBypassForContextMenu) &&
!IsPageLanguageSupported(translate_manager)) {
DVLOG(2) << "language not supported";
compose::LogComposeContextMenuShowStatus(
compose::ComposeShowStatus::kUnsupportedLanguage);
return false;
}
compose::LogComposeContextMenuShowStatus(
compose::ComposeShowStatus::kShouldShow);
return true;
}
base::expected<void, compose::ComposeShowStatus>
ComposeEnabling::PageLevelChecks(translate::TranslateManager* translate_manager,
GURL url,
const url::Origin& top_level_frame_origin,
const url::Origin& element_frame_origin,
bool is_nested_within_fenced_frame) {
if (auto profile_show_status = IsEnabled();
!profile_show_status.has_value()) {
DVLOG(2) << "not enabled";
return profile_show_status;
}
if (!url.SchemeIsHTTPOrHTTPS()) {
DVLOG(2) << "incorrect scheme";
return base::unexpected(compose::ComposeShowStatus::kIncorrectScheme);
}
if (is_nested_within_fenced_frame) {
DVLOG(2) << "field nested within fenced frame not supported";
return base::unexpected(
compose::ComposeShowStatus::kFormFieldNestedInFencedFrame);
}
// Note: This does not check frames between the current and the top level
// frame. Because all our metadata for compose is either based on the origin
// of the top level frame or actually part of the top level frame, this is
// sufficient for now. TODO(b/309162238) follow up on whether this is
// sufficient long-term.
if (top_level_frame_origin != element_frame_origin) {
DVLOG(2) << "cross frame origin not supported";
return base::unexpected(
compose::ComposeShowStatus::kFormFieldInCrossOriginFrame);
}
return base::ok();
}
bool ComposeEnabling::IsPageLanguageSupported(
translate::TranslateManager* translate_manager) {
std::string page_language =
translate_manager
? translate_manager->GetLanguageState()->source_language()
: "";
// TODO(b/307814938): Make this finch configurable.
// Only English is supported for MVP, we will add more languages over time.
// We accept the empty string which might be returned if the translate system
// has not yet deterimed the language, and "und" which means translate
// couldn't find an answer.
return (page_language == "en" || page_language == "und" ||
page_language.empty());
}