blob: cbc4f7ae7baba5fa7d48f1f7eade9f188996c432 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/autofill_assistant/browser/starter.h"
#include "base/base64url.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "components/autofill_assistant/browser/features.h"
#include "components/autofill_assistant/browser/service/api_key_fetcher.h"
#include "components/autofill_assistant/browser/service/server_url_fetcher.h"
#include "components/autofill_assistant/browser/service/service_request_sender.h"
#include "components/autofill_assistant/browser/service/service_request_sender_impl.h"
#include "components/autofill_assistant/browser/service/service_request_sender_local_impl.h"
#include "components/autofill_assistant/browser/service/simple_url_loader_factory.h"
#include "components/autofill_assistant/browser/switches.h"
#include "components/autofill_assistant/browser/trigger_scripts/dynamic_trigger_conditions.h"
#include "components/autofill_assistant/browser/trigger_scripts/static_trigger_conditions.h"
namespace autofill_assistant {
using StartupMode = StartupUtil::StartupMode;
namespace {
// When starting trigger scripts, depending on incoming script parameters, we
// mark users as being in either the control or the experiment group to allow
// for aggregation of UKM metrics.
const char kTriggerScriptExperimentSyntheticFieldTrialName[] =
"AutofillAssistantLiteScriptExperiment";
const char kTriggerScriptExperimentGroup[] = "Experiment";
const char kTriggerScriptControlGroup[] = "Control";
// Creates a service request sender that serves the pre-specified response.
// Creation may fail (return null) if the parameter fails to decode.
std::unique_ptr<ServiceRequestSender> CreateBase64TriggerScriptRequestSender(
const std::string& base64_trigger_script) {
std::string response;
if (!base::Base64UrlDecode(base64_trigger_script,
base::Base64UrlDecodePolicy::IGNORE_PADDING,
&response)) {
return nullptr;
}
return std::make_unique<ServiceRequestSenderLocalImpl>(response);
}
// Creates a service request sender that communicates with a remote endpoint.
std::unique_ptr<ServiceRequestSender> CreateRpcTriggerScriptRequestSender(
content::BrowserContext* browser_context,
StarterPlatformDelegate* delegate) {
return std::make_unique<ServiceRequestSenderImpl>(
browser_context,
/* access_token_fetcher = */ nullptr,
std::make_unique<NativeURLLoaderFactory>(),
ApiKeyFetcher().GetAPIKey(delegate->GetChannel()),
/* auth_enabled = */ false,
/* disable_auth_if_no_access_token = */ true);
}
} // namespace
Starter::Starter(content::WebContents* web_contents,
StarterPlatformDelegate* platform_delegate,
ukm::UkmRecorder* ukm_recorder,
base::WeakPtr<RuntimeManagerImpl> runtime_manager)
: content::WebContentsObserver(web_contents),
platform_delegate_(platform_delegate),
ukm_recorder_(ukm_recorder),
runtime_manager_(runtime_manager) {
CheckSettings();
}
Starter::~Starter() = default;
void Starter::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
// User-initiated navigations during non-trigger-script startups will cancel
// the startup. This is mostly intended for navigations while the onboarding
// is being shown.
if (pending_callback_ && !navigation_handle->WasServerRedirect() &&
!trigger_script_coordinator_ &&
navigation_handle->GetURL() !=
StartupUtil().ChooseStartupUrlForIntent(*pending_trigger_context_)) {
Metrics::RecordDropOut(
waiting_for_onboarding_ ? Metrics::DropOutReason::ONBOARDING_NAVIGATION
: Metrics::DropOutReason::NAVIGATION,
pending_trigger_context_->GetScriptParameters().GetIntent().value_or(
std::string()));
CancelPendingStartup();
}
if (!fetch_trigger_scripts_on_navigation_) {
return;
}
// TODO(arbesser): fetch trigger scripts when appropriate.
}
void Starter::CheckSettings() {
bool prev_is_custom_tab = is_custom_tab_;
is_custom_tab_ = platform_delegate_->GetIsCustomTab();
bool switched_from_cct_to_tab = prev_is_custom_tab && !is_custom_tab_;
bool proactive_help_setting_enabled =
platform_delegate_->GetProactiveHelpSettingEnabled();
bool msbb_setting_enabled =
platform_delegate_->GetMakeSearchesAndBrowsingBetterEnabled();
bool feature_module_installed =
platform_delegate_->GetFeatureModuleInstalled();
fetch_trigger_scripts_on_navigation_ =
base::FeatureList::IsEnabled(
features::kAutofillAssistantInChromeTriggering) &&
proactive_help_setting_enabled && msbb_setting_enabled;
// If there is a pending startup, re-check that the settings are still
// allowing the startup to proceed. If not, cancel the startup.
if (pending_callback_) {
StartupMode startup_mode = StartupUtil().ChooseStartupModeForIntent(
trigger_script_coordinator_ != nullptr
? trigger_script_coordinator_->GetTriggerContext()
: *pending_trigger_context_,
{msbb_setting_enabled, proactive_help_setting_enabled,
feature_module_installed});
switch (startup_mode) {
case StartupMode::START_REGULAR:
return;
case StartupMode::START_BASE64_TRIGGER_SCRIPT:
case StartupMode::START_RPC_TRIGGER_SCRIPT:
if (!switched_from_cct_to_tab) {
return;
}
// Trigger scripts are not allowed to persist when transitioning from
// CCT to regular tab.
CancelPendingStartup();
return;
default:
CancelPendingStartup();
return;
}
}
}
void Starter::Start(std::unique_ptr<TriggerContext> trigger_context,
StarterResultCallback callback) {
DCHECK(trigger_context);
DCHECK(!trigger_context->GetDirectAction());
CancelPendingStartup();
pending_trigger_context_ = std::move(trigger_context);
pending_callback_ = std::move(callback);
if (base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kAutofillAssistantForceOnboarding) == "true") {
platform_delegate_->SetOnboardingAccepted(false);
}
if (base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kAutofillAssistantForceFirstTimeUser) == "true") {
platform_delegate_->SetIsFirstTimeUser(true);
}
StartupMode startup_mode = StartupUtil().ChooseStartupModeForIntent(
*pending_trigger_context_,
{platform_delegate_->GetMakeSearchesAndBrowsingBetterEnabled(),
platform_delegate_->GetProactiveHelpSettingEnabled(),
platform_delegate_->GetFeatureModuleInstalled()});
// Record startup metrics for trigger scripts as soon as possible to establish
// a baseline.
const auto& script_parameters =
pending_trigger_context_->GetScriptParameters();
if (script_parameters.GetRequestsTriggerScript() ||
script_parameters.GetBase64TriggerScriptsResponseProto()) {
Metrics::RecordLiteScriptStarted(
ukm_recorder_, web_contents(), startup_mode,
platform_delegate_->GetFeatureModuleInstalled(),
platform_delegate_->GetIsFirstTimeUser());
}
switch (startup_mode) {
case StartupMode::FEATURE_DISABLED:
case StartupMode::MANDATORY_PARAMETERS_MISSING:
case StartupMode::SETTING_DISABLED:
case StartupMode::NO_INITIAL_URL:
RunCallback(/* start_regular_script = */ false);
return;
case StartupMode::START_BASE64_TRIGGER_SCRIPT:
case StartupMode::START_RPC_TRIGGER_SCRIPT:
case StartupMode::START_REGULAR:
MaybeInstallFeatureModule(startup_mode);
return;
}
}
void Starter::CancelPendingStartup() {
if (!pending_callback_) {
return;
}
platform_delegate_->HideOnboarding();
if (waiting_for_onboarding_) {
Metrics::RecordOnboardingResult(Metrics::OnBoarding::OB_NO_ANSWER);
Metrics::RecordOnboardingResult(Metrics::OnBoarding::OB_SHOWN);
waiting_for_onboarding_ = false;
}
RunCallback(/* start_regular_script = */ false);
trigger_script_coordinator_.reset();
pending_trigger_context_.reset();
}
void Starter::MaybeInstallFeatureModule(StartupMode startup_mode) {
if (platform_delegate_->GetFeatureModuleInstalled()) {
OnFeatureModuleInstalled(
startup_mode,
Metrics::FeatureModuleInstallation::DFM_ALREADY_INSTALLED);
return;
}
platform_delegate_->InstallFeatureModule(
/* show_ui = */ startup_mode == StartupMode::START_REGULAR,
base::BindOnce(&Starter::OnFeatureModuleInstalled,
weak_ptr_factory_.GetWeakPtr(), startup_mode));
}
void Starter::OnFeatureModuleInstalled(
StartupMode startup_mode,
Metrics::FeatureModuleInstallation result) {
Metrics::RecordFeatureModuleInstallation(result);
if (result != Metrics::FeatureModuleInstallation::
DFM_FOREGROUND_INSTALLATION_SUCCEEDED &&
result != Metrics::FeatureModuleInstallation::DFM_ALREADY_INSTALLED) {
Metrics::RecordDropOut(
Metrics::DropOutReason::DFM_INSTALL_FAILED,
pending_trigger_context_->GetScriptParameters().GetIntent().value_or(
std::string()));
RunCallback(/* start_regular_script = */ false);
return;
}
switch (startup_mode) {
case StartupMode::START_REGULAR:
MaybeShowOnboarding();
return;
case StartupMode::START_BASE64_TRIGGER_SCRIPT:
case StartupMode::START_RPC_TRIGGER_SCRIPT:
StartTriggerScript();
return;
default:
DCHECK(false);
RunCallback(/* start_regular_script = */ false);
return;
}
}
void Starter::StartTriggerScript() {
const auto& script_parameters =
pending_trigger_context_->GetScriptParameters();
base::FieldTrialList::CreateFieldTrial(
kTriggerScriptExperimentSyntheticFieldTrialName,
script_parameters.GetTriggerScriptExperiment()
? kTriggerScriptExperimentGroup
: kTriggerScriptControlGroup);
std::unique_ptr<ServiceRequestSender> service_request_sender =
platform_delegate_->GetTriggerScriptRequestSenderToInject();
if (!service_request_sender) {
if (script_parameters.GetBase64TriggerScriptsResponseProto().has_value()) {
service_request_sender = CreateBase64TriggerScriptRequestSender(
script_parameters.GetBase64TriggerScriptsResponseProto().value());
if (!service_request_sender) {
Metrics::RecordLiteScriptFinished(
ukm_recorder_, web_contents(), UNSPECIFIED_TRIGGER_UI_TYPE,
Metrics::LiteScriptFinishedState::
LITE_SCRIPT_BASE64_DECODING_ERROR);
OnTriggerScriptFinished(
Metrics::LiteScriptFinishedState::LITE_SCRIPT_BASE64_DECODING_ERROR,
std::move(pending_trigger_context_), base::nullopt);
return;
}
} else if (script_parameters.GetRequestsTriggerScript().value_or(false)) {
service_request_sender = CreateRpcTriggerScriptRequestSender(
web_contents()->GetBrowserContext(), platform_delegate_);
} else {
// Should never happen.
DCHECK(false);
RunCallback(false);
return;
}
}
DCHECK(service_request_sender);
ServerUrlFetcher url_fetcher{ServerUrlFetcher::GetDefaultServerUrl()};
GURL startup_url = StartupUtil()
.ChooseStartupUrlForIntent(*pending_trigger_context_)
.value();
trigger_script_coordinator_ = std::make_unique<TriggerScriptCoordinator>(
platform_delegate_, web_contents(),
WebController::CreateForWebContents(web_contents()),
std::move(service_request_sender),
url_fetcher.GetTriggerScriptsEndpoint(),
std::make_unique<StaticTriggerConditions>(
platform_delegate_, pending_trigger_context_.get(), startup_url),
std::make_unique<DynamicTriggerConditions>(), ukm_recorder_);
// Note: for the duration of the trigger script, the trigger script
// coordinator will take ownership of the pending trigger context.
trigger_script_coordinator_->Start(
startup_url, std::move(pending_trigger_context_),
base::BindOnce(&Starter::OnTriggerScriptFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void Starter::OnTriggerScriptFinished(
Metrics::LiteScriptFinishedState state,
std::unique_ptr<TriggerContext> trigger_context,
base::Optional<TriggerScriptProto> trigger_script) {
if (state != Metrics::LiteScriptFinishedState::LITE_SCRIPT_PROMPT_SUCCEEDED) {
RunCallback(/* start_regular_script = */ false);
return;
}
// Take back ownership of the trigger context.
pending_trigger_context_ = std::move(trigger_context);
// Note: most trigger scripts show the onboarding on their own and log a
// different metric for the result. We need to be careful to only run the
// regular onboarding if necessary to avoid logging metrics more than once.
if (platform_delegate_->GetOnboardingAccepted()) {
RunCallback(/* start_regular_script = */ true, trigger_script);
return;
} else {
MaybeShowOnboarding(trigger_script);
}
}
void Starter::MaybeShowOnboarding(
base::Optional<TriggerScriptProto> trigger_script) {
if (platform_delegate_->GetOnboardingAccepted()) {
OnOnboardingFinished(trigger_script, /* shown = */ false,
OnboardingResult::ACCEPTED);
return;
}
// Always use bottom sheet onboarding here. Trigger scripts may show a dialog
// onboarding, but if we have reached this part, we're already starting the
// regular script, where we don't offer dialog onboarding.
runtime_manager_->SetUIState(UIState::kShown);
waiting_for_onboarding_ = true;
platform_delegate_->ShowOnboarding(
/* use_dialog_onboarding = */ false, *pending_trigger_context_,
base::BindOnce(&Starter::OnOnboardingFinished,
weak_ptr_factory_.GetWeakPtr(), trigger_script));
}
void Starter::OnOnboardingFinished(
base::Optional<TriggerScriptProto> trigger_script,
bool shown,
OnboardingResult result) {
waiting_for_onboarding_ = false;
auto intent =
pending_trigger_context_->GetScriptParameters().GetIntent().value_or(
std::string());
switch (result) {
case OnboardingResult::DISMISSED:
Metrics::RecordOnboardingResult(Metrics::OnBoarding::OB_NO_ANSWER);
Metrics::RecordDropOut(
Metrics::DropOutReason::ONBOARDING_BACK_BUTTON_CLICKED, intent);
break;
case OnboardingResult::REJECTED:
Metrics::RecordOnboardingResult(Metrics::OnBoarding::OB_CANCELLED);
Metrics::RecordDropOut(Metrics::DropOutReason::DECLINED, intent);
break;
case OnboardingResult::NAVIGATION:
Metrics::RecordOnboardingResult(Metrics::OnBoarding::OB_NO_ANSWER);
Metrics::RecordDropOut(Metrics::DropOutReason::ONBOARDING_NAVIGATION,
intent);
break;
case OnboardingResult::ACCEPTED:
Metrics::RecordOnboardingResult(Metrics::OnBoarding::OB_ACCEPTED);
break;
}
Metrics::RecordOnboardingResult(shown ? Metrics::OnBoarding::OB_SHOWN
: Metrics::OnBoarding::OB_NOT_SHOWN);
if (result != OnboardingResult::ACCEPTED) {
runtime_manager_->SetUIState(UIState::kNotShown);
RunCallback(/* start_regular_script = */ false);
return;
}
// Onboarding is the last step before regular startup.
platform_delegate_->SetOnboardingAccepted(true);
pending_trigger_context_->SetOnboardingShown(shown);
RunCallback(/* start_regular_script = */ true, trigger_script);
}
void Starter::RunCallback(bool start_regular_script,
base::Optional<TriggerScriptProto> trigger_script) {
DCHECK(pending_callback_);
if (!start_regular_script) {
// Catch-all to ensure that after a failed startup attempt we no longer
// register as visible to runtime observers.
runtime_manager_->SetUIState(UIState::kNotShown);
pending_trigger_context_ = nullptr;
std::move(pending_callback_)
.Run(/* start_regular_script = */ false, GURL(), nullptr,
base::nullopt);
return;
}
auto startup_url =
StartupUtil().ChooseStartupUrlForIntent(*pending_trigger_context_);
DCHECK(startup_url.has_value());
std::move(pending_callback_)
.Run(/* start_regular_script = */ true, *startup_url,
std::move(pending_trigger_context_), trigger_script);
}
} // namespace autofill_assistant