blob: 19006c5a6a8129258939eed8c43069b8c23c3af5 [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 <type_traits>
#include "base/check.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "chrome/browser/about_flags.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 "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/flags_ui/feature_entry.h"
#include "components/flags_ui/flags_storage.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_frame_host.h"
namespace {
bool AutocompleteAllowed(std::string_view autocomplete_attribute) {
// Check autocomplete is not turned off.
return autocomplete_attribute != std::string("off");
}
} // namespace
int ComposeEnabling::enabled_for_testing_{0};
int ComposeEnabling::skip_user_check_for_testing_{0};
ComposeEnabling::ComposeEnabling(
TranslateLanguageProvider* translate_language_provider,
Profile* profile,
signin::IdentityManager* identity_manager,
OptimizationGuideKeyedService* opt_guide)
: profile_(profile),
opt_guide_(opt_guide),
identity_manager_(identity_manager) {
DCHECK(profile_);
translate_language_provider_ = translate_language_provider;
}
ComposeEnabling::~ComposeEnabling() {
opt_guide_ = nullptr;
identity_manager_ = nullptr;
translate_language_provider_ = 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_)));
}
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();
}
// 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::kFeatureFlagDisabled);
}
// 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::proto::ModelExecutionFeature::
MODEL_EXECUTION_FEATURE_COMPOSE)) {
DVLOG(2) << "Feature not available for this user";
return base::unexpected(
compose::ComposeShowStatus::kUserNotAllowedByOptimizationGuide);
}
DVLOG(2) << "enabled";
return base::ok();
}
// TODO(b/303502029): make return state an enum instead of a bool so we
// can return a different value when we have saved state for this field.
bool ComposeEnabling::ShouldTriggerPopup(
std::string_view autocomplete_attribute,
Profile* profile,
translate::TranslateManager* translate_manager,
bool ongoing_session,
const url::Origin& top_level_frame_origin,
const url::Origin& element_frame_origin,
GURL url) {
if (!base::FeatureList::IsEnabled(compose::features::kEnableComposeNudge)) {
return false;
}
// Check URL with Optimization guide.
compose::ComposeHintDecision decision =
GetOptimizationGuidanceForUrl(url, profile);
if (decision == compose::ComposeHintDecision::
COMPOSE_HINT_DECISION_COMPOSE_DISABLED ||
decision ==
compose::ComposeHintDecision::COMPOSE_HINT_DECISION_DISABLE_NUDGE) {
return false;
}
// TODO(b/319661274): Support fenced frame checks from the Autofill popup
// entry point.
bool is_in_fenced_frame = false;
if (!PageLevelChecks(translate_manager, url, top_level_frame_origin,
element_frame_origin, is_in_fenced_frame)
.has_value()) {
return false;
}
auto& config = compose::GetComposeConfig();
if (ongoing_session) {
if (!config.popup_with_saved_state) {
return false;
}
} else {
if (!config.popup_with_no_saved_state) {
return false;
}
// 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 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(
compose::ComposeShowStatus::kShouldShow);
return true;
}
compose::LogComposeContextMenuShowStatus(show_status.error());
DVLOG(2) << "page level checks failed";
return false;
}
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);
}
if (!base::FeatureList::IsEnabled(
compose::features::kEnableComposeLanguageBypass) &&
!translate_language_provider_->IsLanguageSupported(translate_manager)) {
DVLOG(2) << "language not supported";
return base::unexpected(compose::ComposeShowStatus::kUnsupportedLanguage);
}
return base::ok();
}