| // 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 <map> |
| |
| #include "base/base64url.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/json/json_reader.h" |
| #include "base/logging.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/no_destructor.h" |
| #include "components/autofill_assistant/browser/features.h" |
| #include "components/autofill_assistant/browser/intent_strings.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" |
| #include "components/autofill_assistant/browser/url_utils.h" |
| #include "components/ukm/content/source_url_recorder.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.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[] = |
| "AutofillAssistantTriggerScriptExperiment"; |
| const char kTriggerScriptExperimentGroup[] = "Experiment"; |
| const char kTriggerScriptControlGroup[] = "Control"; |
| |
| // The maximum number of items to be kept in the cache. If this number is |
| // exceeded, the entry that hasn't been accessed the longest is automatically |
| // removed. |
| constexpr size_t kMaxFailedTriggerScriptsCacheSize = 100; |
| constexpr size_t kMaxUserDenylistedCacheSize = 100; |
| |
| // The duration for which cache entries are considered fresh. Stale entries in |
| // the cache are ignored. |
| constexpr base::TimeDelta kMaxFailedTriggerScriptsCacheDuration = |
| base::TimeDelta::FromHours(1); |
| constexpr base::TimeDelta kMaxUserDenylistedCacheDuration = |
| base::TimeDelta::FromHours(1); |
| |
| // 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); |
| } |
| |
| // Returns whether |trigger_context| contains either the REQUEST_TRIGGER_SCRIPT |
| // or the TRIGGER_SCRIPTS_BASE64 script parameter. |
| bool IsTriggerScriptContext(const TriggerContext& trigger_context) { |
| const auto& script_parameters = trigger_context.GetScriptParameters(); |
| return script_parameters.GetRequestsTriggerScript() || |
| script_parameters.GetBase64TriggerScriptsResponseProto(); |
| } |
| |
| // The heuristic is shared across all instances and initialized on first use. As |
| // such, we do not support updating the heuristic while Chrome is running. |
| const scoped_refptr<StarterHeuristic> GetOrCreateStarterHeuristic() { |
| static const base::NoDestructor<scoped_refptr<StarterHeuristic>> |
| starter_heuristic( |
| [] { return base::MakeRefCounted<StarterHeuristic>(); }()); |
| return *starter_heuristic; |
| } |
| |
| // The cache of failed trigger script fetches is shared across all instances and |
| // initialized on first use. |
| base::HashingMRUCache<std::string, base::TimeTicks>* |
| GetOrCreateFailedTriggerScriptFetchesCache() { |
| static base::NoDestructor<base::HashingMRUCache<std::string, base::TimeTicks>> |
| cached_failed_trigger_script_fetches(kMaxFailedTriggerScriptsCacheSize); |
| return cached_failed_trigger_script_fetches.get(); |
| } |
| |
| // Goes through the |cache| and removes entries that have gone stale, i.e., |
| // entries that were added before |cutoff_ticks|. |
| void ClearStaleCacheEntries( |
| base::HashingMRUCache<std::string, base::TimeTicks>* cache, |
| base::TimeTicks cutoff_ticks) { |
| // Go in reverse order until the oldest entry is younger than |cutoff_ticks|. |
| for (auto it = cache->rbegin(); it != cache->rend();) { |
| if (it->second > cutoff_ticks) { |
| return; |
| } |
| it = cache->Erase(it); |
| } |
| } |
| |
| // Returns true if |cache| has an entry for |url| that is younger than |
| // |cutoff_ticks|, false otherwise. Does not change the order of the cache. |
| bool HasFreshCacheEntry( |
| const base::HashingMRUCache<std::string, base::TimeTicks>& cache, |
| const GURL& url, |
| base::TimeTicks cutoff_ticks) { |
| std::string domain = url_utils::GetOrganizationIdentifyingDomain(url); |
| auto it = cache.Peek(domain); |
| return (it != cache.end() && (it->second > cutoff_ticks)); |
| } |
| |
| // Returns the debug parameters for implicit triggering specified in the command |
| // line, or the default proto if the command line switch was not specified or |
| // invalid. |
| ImplicitTriggeringDebugParametersProto |
| GetImplicitTriggeringDebugParametersFromCommandLine() { |
| std::string parameters = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kAutofillAssistantImplicitTriggeringDebugParameters); |
| if (parameters.empty()) { |
| return {}; |
| } |
| |
| if (!base::Base64UrlDecode(parameters, |
| base::Base64UrlDecodePolicy::IGNORE_PADDING, |
| ¶meters)) { |
| VLOG(1) << "Failed to base64-decode debug trigger parameters: " |
| << parameters; |
| return {}; |
| } |
| |
| ImplicitTriggeringDebugParametersProto proto; |
| if (!proto.ParseFromString(parameters)) { |
| VLOG(1) << "Failed to parse debug trigger parameters: " << parameters; |
| return {}; |
| } |
| return proto; |
| } |
| |
| } // namespace |
| |
| Starter::Starter(content::WebContents* web_contents, |
| StarterPlatformDelegate* platform_delegate, |
| ukm::UkmRecorder* ukm_recorder, |
| base::WeakPtr<RuntimeManagerImpl> runtime_manager, |
| const base::TickClock* tick_clock) |
| : content::WebContentsObserver(web_contents), |
| next_ukm_source_id_(ukm::GetSourceIdForWebContentsDocument(web_contents)), |
| cached_failed_trigger_script_fetches_( |
| GetOrCreateFailedTriggerScriptFetchesCache()), |
| user_denylisted_domains_(kMaxUserDenylistedCacheSize), |
| implicit_triggering_debug_parameters_( |
| GetImplicitTriggeringDebugParametersFromCommandLine()), |
| platform_delegate_(platform_delegate), |
| ukm_recorder_(ukm_recorder), |
| runtime_manager_(runtime_manager), |
| starter_heuristic_(GetOrCreateStarterHeuristic()), |
| tick_clock_(tick_clock) { |
| CheckSettings(); |
| } |
| |
| Starter::~Starter() = default; |
| |
| void Starter::DidStartNavigation(content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsInMainFrame()) { |
| return; |
| } |
| next_ukm_source_id_ = navigation_handle->GetNextPageUkmSourceId(); |
| } |
| |
| void Starter::DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsInMainFrame()) { |
| return; |
| } |
| |
| // Navigating away from the deeplink domain during startup OR ending up on an |
| // error page will break the flow, unless a trigger script is currently |
| // running (in which case, the trigger script will handle this event). |
| if (IsStartupPending() && navigation_handle->HasCommitted() && |
| !trigger_script_coordinator_) { |
| bool navigated_to_target_domain = url_utils::IsSamePublicSuffixDomain( |
| navigation_handle->GetURL(), |
| StartupUtil() |
| .ChooseStartupUrlForIntent(*GetPendingTriggerContext()) |
| .value_or(GURL())); |
| |
| if (navigated_to_target_domain) { |
| if (waiting_for_deeplink_navigation_) { |
| Start(std::move(pending_trigger_context_)); |
| } |
| // Ignore; navigations to the target domain during startup are allowed. |
| return; |
| } |
| |
| if (waiting_for_deeplink_navigation_) { |
| if (navigated_to_target_domain) { |
| Start(std::move(pending_trigger_context_)); |
| return; |
| } |
| // Note: this will record for the current domain, not the target domain. |
| // There seems to be no way to avoid this. |
| Metrics::RecordTriggerScriptStarted( |
| ukm_recorder_, next_ukm_source_id_, |
| navigation_handle->IsErrorPage() |
| ? Metrics::TriggerScriptStarted::NAVIGATION_ERROR |
| : Metrics::TriggerScriptStarted::NAVIGATED_AWAY); |
| CancelPendingStartup(absl::nullopt); |
| } else { |
| // Regular startup was interrupted (most likely during the onboarding). |
| Metrics::RecordDropOut(waiting_for_onboarding_ |
| ? Metrics::DropOutReason::ONBOARDING_NAVIGATION |
| : Metrics::DropOutReason::NAVIGATION, |
| GetPendingTriggerContext() |
| ->GetScriptParameters() |
| .GetIntent() |
| .value_or(std::string())); |
| CancelPendingStartup(absl::nullopt); |
| } |
| // Note: do not early-return here. While the previous startup has failed, we |
| // may have navigated to a new supported domain and may need to start |
| // implicitly. |
| } |
| |
| if (navigation_handle->HasCommitted() && !navigation_handle->IsErrorPage()) { |
| MaybeStartImplicitlyForUrl(navigation_handle->GetURL()); |
| } |
| } |
| |
| void Starter::MaybeStartImplicitlyForUrl(const GURL& url) { |
| if (!fetch_trigger_scripts_on_navigation_ || IsStartupPending() || |
| platform_delegate_->IsRegularScriptRunning() || !url.is_valid()) { |
| return; |
| } |
| |
| // If we have failed to fetch a trigger script for this domain before, or if |
| // the user has denylisted the domain, don't try again. |
| base::TimeTicks now_ticks = tick_clock_->NowTicks(); |
| if (HasFreshCacheEntry(*cached_failed_trigger_script_fetches_, url, |
| now_ticks - kMaxFailedTriggerScriptsCacheDuration) || |
| HasFreshCacheEntry(user_denylisted_domains_, url, |
| now_ticks - kMaxUserDenylistedCacheDuration)) { |
| return; |
| } |
| |
| // Run the heuristic in a separate task. |
| starter_heuristic_->RunHeuristicAsync( |
| url, base::BindOnce(&Starter::OnHeuristicMatch, |
| weak_ptr_factory_.GetWeakPtr(), url)); |
| } |
| |
| void Starter::OnHeuristicMatch(const GURL& url, |
| absl::optional<std::string> intent) { |
| if (!intent || IsStartupPending() || !fetch_trigger_scripts_on_navigation_) { |
| return; |
| } |
| |
| std::map<std::string, std::string> script_parameters = { |
| {"ENABLED", "true"}, |
| {"START_IMMEDIATELY", "false"}, |
| {"REQUEST_TRIGGER_SCRIPT", "true"}, |
| {"ORIGINAL_DEEPLINK", url.spec()}, |
| {"INTENT", *intent}}; |
| // Add/overwrite with debug parameters if specified. |
| for (const auto& debug_param : |
| implicit_triggering_debug_parameters_.additional_script_parameters()) { |
| script_parameters[debug_param.name()] = debug_param.value(); |
| } |
| |
| Start(std::make_unique<TriggerContext>( |
| std::make_unique<ScriptParameters>(script_parameters), |
| TriggerContext::Options{/* experiment_ids = */ std::string(), |
| /* is_cct = */ is_custom_tab_, |
| /* onboarding_shown = */ false, |
| /* is_direct_action = */ false, |
| /* initial_url = */ std::string()})); |
| } |
| |
| bool Starter::IsStartupPending() const { |
| return GetPendingTriggerContext() != nullptr; |
| } |
| |
| TriggerContext* Starter::GetPendingTriggerContext() const { |
| if (trigger_script_coordinator_) { |
| return &trigger_script_coordinator_->GetTriggerContext(); |
| } |
| return pending_trigger_context_.get(); |
| } |
| |
| void Starter::OnTabInteractabilityChanged(bool is_interactable) { |
| CheckSettings(); |
| if (trigger_script_coordinator_) { |
| trigger_script_coordinator_->OnTabInteractabilityChanged(is_interactable); |
| } |
| } |
| |
| 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(); |
| bool prev_fetch_trigger_scripts_on_navigation = |
| fetch_trigger_scripts_on_navigation_; |
| fetch_trigger_scripts_on_navigation_ = |
| ((base::FeatureList::IsEnabled( |
| features::kAutofillAssistantInCCTTriggering) && |
| is_custom_tab_) || |
| (base::FeatureList::IsEnabled( |
| features::kAutofillAssistantInTabTriggering) && |
| !is_custom_tab_)) && |
| 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 (IsStartupPending()) { |
| StartupMode startup_mode = StartupUtil().ChooseStartupModeForIntent( |
| *GetPendingTriggerContext(), |
| {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( |
| Metrics::TriggerScriptFinishedState::CCT_TO_TAB_NOT_SUPPORTED); |
| return; |
| default: |
| CancelPendingStartup(Metrics::TriggerScriptFinishedState:: |
| DISABLED_PROACTIVE_HELP_SETTING); |
| return; |
| } |
| } else if (!prev_fetch_trigger_scripts_on_navigation && |
| fetch_trigger_scripts_on_navigation_) { |
| MaybeStartImplicitlyForUrl(web_contents()->GetLastCommittedURL()); |
| } |
| } |
| |
| void Starter::Start(std::unique_ptr<TriggerContext> trigger_context) { |
| DCHECK(trigger_context); |
| DCHECK(!trigger_context->GetDirectAction()); |
| CancelPendingStartup(Metrics::TriggerScriptFinishedState::CANCELED); |
| pending_trigger_context_ = std::move(trigger_context); |
| |
| 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()}); |
| |
| // Trigger scripts may need to wait for navigation to the deeplink domain to |
| // ensure that UKMs are recorded for the right source-id. |
| auto startup_url = |
| StartupUtil().ChooseStartupUrlForIntent(*pending_trigger_context_); |
| if (IsTriggerScriptContext(*pending_trigger_context_) && |
| !startup_url.has_value()) { |
| // Fail immediately if there is no deeplink domain to wait for. |
| // Note: this will record the impression for the current domain. |
| Metrics::RecordTriggerScriptStarted( |
| ukm_recorder_, next_ukm_source_id_, |
| Metrics::TriggerScriptStarted::NO_INITIAL_URL); |
| OnStartDone(/* start_regular_script = */ false); |
| return; |
| } |
| if (IsTriggerScriptContext(*pending_trigger_context_) && |
| !url_utils::IsSamePublicSuffixDomain( |
| web_contents()->GetLastCommittedURL(), |
| startup_url.value_or(GURL()))) { |
| waiting_for_deeplink_navigation_ = true; |
| return; |
| } |
| |
| // Record startup metrics for trigger scripts as soon as possible to establish |
| // a baseline. |
| if (IsTriggerScriptContext(*pending_trigger_context_)) { |
| Metrics::RecordTriggerScriptStarted( |
| ukm_recorder_, next_ukm_source_id_, 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: |
| OnStartDone(/* 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( |
| absl::optional<Metrics::TriggerScriptFinishedState> state) { |
| if (!IsStartupPending()) { |
| 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; |
| } |
| OnStartDone(/* start_regular_script = */ false); |
| if (trigger_script_coordinator_ && state) { |
| trigger_script_coordinator_->Stop(*state); |
| } |
| 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())); |
| OnStartDone(/* 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); |
| OnStartDone(/* 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::RecordTriggerScriptFinished( |
| ukm_recorder_, next_ukm_source_id_, UNSPECIFIED_TRIGGER_UI_TYPE, |
| Metrics::TriggerScriptFinishedState::BASE64_DECODING_ERROR); |
| OnTriggerScriptFinished( |
| Metrics::TriggerScriptFinishedState::BASE64_DECODING_ERROR, |
| std::move(pending_trigger_context_), absl::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); |
| OnStartDone(/* start_regular_script = */ 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_, |
| next_ukm_source_id_); |
| |
| // 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::TriggerScriptFinishedState state, |
| std::unique_ptr<TriggerContext> trigger_context, |
| absl::optional<TriggerScriptProto> trigger_script) { |
| // Update caches on error or user-cancel. |
| if (trigger_script_coordinator_) { |
| std::string domain = url_utils::GetOrganizationIdentifyingDomain( |
| trigger_script_coordinator_->GetDeeplink()); |
| switch (state) { |
| case Metrics::TriggerScriptFinishedState::NO_TRIGGER_SCRIPT_AVAILABLE: |
| case Metrics::TriggerScriptFinishedState::GET_ACTIONS_FAILED: |
| cached_failed_trigger_script_fetches_->Put(domain, |
| tick_clock_->NowTicks()); |
| ClearStaleCacheEntries( |
| cached_failed_trigger_script_fetches_, |
| tick_clock_->NowTicks() - kMaxFailedTriggerScriptsCacheDuration); |
| break; |
| case Metrics::TriggerScriptFinishedState::PROMPT_FAILED_CANCEL_SESSION: |
| user_denylisted_domains_.Put(domain, tick_clock_->NowTicks()); |
| ClearStaleCacheEntries( |
| &user_denylisted_domains_, |
| tick_clock_->NowTicks() - kMaxUserDenylistedCacheDuration); |
| break; |
| default: { |
| auto cache_it = cached_failed_trigger_script_fetches_->Peek(domain); |
| if (cache_it != cached_failed_trigger_script_fetches_->end()) { |
| cached_failed_trigger_script_fetches_->Erase(cache_it); |
| } |
| break; |
| } |
| } |
| } |
| |
| // Delete the coordinator asynchronously, to give this notification time to |
| // end gracefully. |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&Starter::DeleteTriggerScriptCoordinator, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| if (state != Metrics::TriggerScriptFinishedState::PROMPT_SUCCEEDED) { |
| OnStartDone(/* 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()) { |
| OnStartDone(/* start_regular_script = */ true, trigger_script); |
| return; |
| } else { |
| MaybeShowOnboarding(trigger_script); |
| } |
| } |
| |
| void Starter::MaybeShowOnboarding( |
| absl::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, *GetPendingTriggerContext(), |
| base::BindOnce(&Starter::OnOnboardingFinished, |
| weak_ptr_factory_.GetWeakPtr(), trigger_script)); |
| } |
| |
| void Starter::OnOnboardingFinished( |
| absl::optional<TriggerScriptProto> trigger_script, |
| bool shown, |
| OnboardingResult result) { |
| waiting_for_onboarding_ = false; |
| auto intent = |
| GetPendingTriggerContext()->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); |
| OnStartDone(/* start_regular_script = */ false); |
| return; |
| } |
| |
| // Onboarding is the last step before regular startup. |
| platform_delegate_->SetOnboardingAccepted(true); |
| pending_trigger_context_->SetOnboardingShown(shown); |
| OnStartDone(/* start_regular_script = */ true, trigger_script); |
| } |
| |
| void Starter::OnStartDone(bool start_regular_script, |
| absl::optional<TriggerScriptProto> trigger_script) { |
| if (!start_regular_script) { |
| // Catch-all to ensure that after a failed startup attempt we reset the |
| // UI state. |
| runtime_manager_->SetUIState(platform_delegate_->IsRegularScriptVisible() |
| ? UIState::kShown |
| : UIState::kNotShown); |
| pending_trigger_context_.reset(); |
| return; |
| } |
| |
| auto startup_url = |
| StartupUtil().ChooseStartupUrlForIntent(*pending_trigger_context_); |
| DCHECK(startup_url.has_value()); |
| platform_delegate_->StartRegularScript( |
| *startup_url, std::move(pending_trigger_context_), trigger_script); |
| } |
| |
| void Starter::DeleteTriggerScriptCoordinator() { |
| trigger_script_coordinator_.reset(); |
| } |
| |
| } // namespace autofill_assistant |