| // 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/chrome_compose_client.h" | 
 |  | 
 | #include <memory> | 
 | #include <optional> | 
 | #include <string> | 
 | #include <utility> | 
 |  | 
 | #include "base/functional/bind.h" | 
 | #include "base/json/values_util.h" | 
 | #include "base/metrics/user_metrics.h" | 
 | #include "base/strings/utf_string_conversion_utils.h" | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "base/task/bind_post_task.h" | 
 | #include "base/task/thread_pool.h" | 
 | #include "base/third_party/icu/icu_utf.h" | 
 | #include "chrome/browser/compose/compose_enabling.h" | 
 | #include "chrome/browser/compose/compose_text_usage_logger.h" | 
 | #include "chrome/browser/compose/proactive_nudge_tracker.h" | 
 | #include "chrome/browser/compose/proto/compose_optimization_guide.pb.h" | 
 | #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h" | 
 | #include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h" | 
 | #include "chrome/browser/profiles/profile.h" | 
 | #include "chrome/browser/profiles/profile_manager.h" | 
 | #include "chrome/browser/segmentation_platform/segmentation_platform_service_factory.h" | 
 | #include "chrome/browser/signin/identity_manager_factory.h" | 
 | #include "chrome/browser/translate/chrome_translate_client.h" | 
 | #include "chrome/browser/ui/browser.h" | 
 | #include "chrome/browser/ui/browser_dialogs.h" | 
 | #include "chrome/browser/ui/browser_element_identifiers.h" | 
 | #include "chrome/browser/ui/browser_finder.h" | 
 | #include "chrome/browser/ui/chrome_pages.h" | 
 | #include "chrome/browser/ui/hats/hats_service_factory.h" | 
 | #include "chrome/browser/ui/hats/survey_config.h" | 
 | #include "chrome/browser/ui/user_education/show_promo_in_page.h" | 
 | #include "chrome/common/compose/type_conversions.h" | 
 | #include "chrome/common/pref_names.h" | 
 | #include "chrome/common/webui_url_constants.h" | 
 | #include "components/autofill/content/browser/content_autofill_client.h" | 
 | #include "components/autofill/content/browser/content_autofill_driver.h" | 
 | #include "components/autofill/core/browser/filling/filling_product.h" | 
 | #include "components/autofill/core/browser/foundations/autofill_client.h" | 
 | #include "components/autofill/core/browser/suggestions/suggestion.h" | 
 | #include "components/autofill/core/common/aliases.h" | 
 | #include "components/autofill/core/common/form_field_data.h" | 
 | #include "components/compose/core/browser/compose_features.h" | 
 | #include "components/compose/core/browser/compose_manager_impl.h" | 
 | #include "components/compose/core/browser/compose_metrics.h" | 
 | #include "components/compose/core/browser/config.h" | 
 | #include "components/optimization_guide/core/optimization_guide_features.h" | 
 | #include "components/optimization_guide/proto/features/compose.pb.h" | 
 | #include "components/strings/grit/components_strings.h" | 
 | #include "components/unified_consent/pref_names.h" | 
 | #include "components/unified_consent/url_keyed_data_collection_consent_helper.h" | 
 | #include "content/public/browser/browser_thread.h" | 
 | #include "content/public/browser/context_menu_params.h" | 
 | #include "content/public/browser/page.h" | 
 | #include "content/public/browser/render_frame_host.h" | 
 | #include "content/public/browser/web_contents.h" | 
 | #include "content/public/browser/web_contents_observer.h" | 
 | #include "content/public/browser/web_contents_user_data.h" | 
 | #include "mojo/public/cpp/bindings/callback_helpers.h" | 
 | #include "services/metrics/public/cpp/ukm_recorder.h" | 
 | #include "services/metrics/public/cpp/ukm_source_id.h" | 
 | #include "ui/base/l10n/l10n_util.h" | 
 | #include "ui/gfx/geometry/rect_conversions.h" | 
 | #include "ui/gfx/geometry/rect_f.h" | 
 |  | 
 | namespace { | 
 |  | 
 | std::u16string RemoveLastCharIfInvalid(std::u16string str) { | 
 |   // TODO(b/323902463): Have Autofill send a valid string, i.e. truncated to a | 
 |   // valid grapheme, in FormFieldData.selected_text to ensure greatest | 
 |   // preservation of the original selected text. | 
 |   if (!str.empty() && CBU16_IS_LEAD(str.back())) { | 
 |     str.pop_back(); | 
 |   } | 
 |   return str; | 
 | } | 
 |  | 
 | bool ComposeNudgeShowStatusDisabledByConfig(compose::ComposeShowStatus status) { | 
 |   switch (status) { | 
 |     case compose::ComposeShowStatus:: | 
 |         kProactiveNudgeDisabledGloballyByUserPreference: | 
 |     case compose::ComposeShowStatus:: | 
 |         kProactiveNudgeDisabledForSiteByUserPreference: | 
 |     case compose::ComposeShowStatus::kProactiveNudgeFeatureDisabled: | 
 |     case compose::ComposeShowStatus::kProactiveNudgeDisabledByMSBB: | 
 |     case compose::ComposeShowStatus:: | 
 |         kProactiveNudgeBlockedBySegmentationPlatform: | 
 |       return true; | 
 |     default: | 
 |       return false; | 
 |   } | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | // ChromeComposeClient::FieldChangeObserver | 
 | ChromeComposeClient::FieldChangeObserver::FieldChangeObserver( | 
 |     content::WebContents* web_contents) | 
 |     : web_contents_(web_contents) { | 
 |   autofill_managers_observation_.Observe( | 
 |       autofill::ContentAutofillClient::FromWebContents(web_contents), | 
 |       autofill::ScopedAutofillManagersObservation::InitializationPolicy:: | 
 |           kObservePreexistingManagers); | 
 | } | 
 |  | 
 | ChromeComposeClient::FieldChangeObserver::~FieldChangeObserver() = default; | 
 |  | 
 | void ChromeComposeClient::FieldChangeObserver::OnSuggestionsShown( | 
 |     autofill::AutofillManager& manager, | 
 |     base::span<const autofill::Suggestion> suggestions) { | 
 |   text_field_value_change_event_count_ = 0; | 
 | } | 
 |  | 
 | void ChromeComposeClient::FieldChangeObserver::OnAfterTextFieldValueChanged( | 
 |     autofill::AutofillManager& manager, | 
 |     autofill::FormGlobalId form, | 
 |     autofill::FieldGlobalId field, | 
 |     const std::u16string& text_value) { | 
 |   ++text_field_value_change_event_count_; | 
 |   if (text_field_value_change_event_count_ >= | 
 |       compose::GetComposeConfig().nudge_field_change_event_max) { | 
 |     HideComposeNudges(); | 
 |     text_field_value_change_event_count_ = 0; | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::FieldChangeObserver::HideComposeNudges() { | 
 |   if (autofill::AutofillClient* autofill_client = | 
 |           autofill::ContentAutofillClient::FromWebContents(web_contents_)) { | 
 |     // Only hide open suggestions if they are of compose type. | 
 |     base::span<const autofill::Suggestion> suggestions = | 
 |         autofill_client->GetAutofillSuggestions(); | 
 |     if ((suggestions.size() == 1 && | 
 |          autofill::GetFillingProductFromSuggestionType(suggestions[0].type) == | 
 |              autofill::FillingProduct::kCompose) || | 
 |         skip_suggestion_type_for_test_) { | 
 |       autofill_client->HideAutofillSuggestions( | 
 |           autofill::SuggestionHidingReason::kFieldValueChanged); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::FieldChangeObserver::SetSkipSuggestionTypeForTest( | 
 |     bool skip_suggestion_type) { | 
 |   skip_suggestion_type_for_test_ = skip_suggestion_type; | 
 | } | 
 |  | 
 | ChromeComposeClient::ChromeComposeClient(content::WebContents* web_contents) | 
 |     : content::WebContentsObserver(web_contents), | 
 |       content::WebContentsUserData<ChromeComposeClient>(*web_contents), | 
 |       profile_( | 
 |           Profile::FromBrowserContext(GetWebContents().GetBrowserContext())), | 
 |       nudge_tracker_(segmentation_platform::SegmentationPlatformServiceFactory:: | 
 |                          GetForProfile(profile_), | 
 |                      this), | 
 |       field_change_observer_(web_contents) { | 
 |   auto ukm_source_id = | 
 |       GetWebContents().GetPrimaryMainFrame()->GetPageUkmSourceId(); | 
 |   page_ukm_tracker_ = std::make_unique<compose::PageUkmTracker>(ukm_source_id); | 
 |   opt_guide_ = OptimizationGuideKeyedServiceFactory::GetForProfile(profile_); | 
 |   pref_service_ = profile_->GetPrefs(); | 
 |   proactive_nudge_enabled_.Init(prefs::kEnableProactiveNudge, pref_service_); | 
 |  | 
 |   compose_enabling_ = std::make_unique<ComposeEnabling>( | 
 |       profile_, IdentityManagerFactory::GetForProfileIfExists(profile_), | 
 |       OptimizationGuideKeyedServiceFactory::GetForProfile(profile_)); | 
 |  | 
 |   if (GetOptimizationGuide()) { | 
 |     std::vector<optimization_guide::proto::OptimizationType> types; | 
 |     if (compose_enabling_->IsEnabled().has_value()) { | 
 |       types.push_back(optimization_guide::proto::OptimizationType::COMPOSE); | 
 |     } | 
 |  | 
 |     if (!types.empty()) { | 
 |       GetOptimizationGuide()->RegisterOptimizationTypes(types); | 
 |     } | 
 |   } | 
 |  | 
 |   autofill_managers_observation_.Observe( | 
 |       autofill::ContentAutofillDriverFactory::FromWebContents(web_contents), | 
 |       autofill::ScopedAutofillManagersObservation::InitializationPolicy:: | 
 |           kObservePreexistingManagers); | 
 |   nudge_tracker_.StartObserving(web_contents); | 
 | } | 
 |  | 
 | ChromeComposeClient::~ChromeComposeClient() { | 
 |   // Sessions may call back during destruction through ComposeSession::Observer. | 
 |   // Let's ensure that happens before destroying anything else. | 
 |   sessions_.clear(); | 
 |   debug_session_.reset(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::BindComposeDialog( | 
 |     mojo::PendingReceiver<compose::mojom::ComposeClientUntrustedPageHandler> | 
 |         client_handler, | 
 |     mojo::PendingReceiver<compose::mojom::ComposeSessionUntrustedPageHandler> | 
 |         handler, | 
 |     mojo::PendingRemote<compose::mojom::ComposeUntrustedDialog> dialog) { | 
 |   client_page_receiver_.reset(); | 
 |   client_page_receiver_.Bind(std::move(client_handler)); | 
 |  | 
 |   url::Origin origin = | 
 |       GetWebContents().GetPrimaryMainFrame()->GetLastCommittedOrigin(); | 
 |   if (origin == | 
 |       url::Origin::Create(GURL(chrome::kChromeUIUntrustedComposeUrl))) { | 
 |     debug_session_ = std::make_unique<ComposeSession>( | 
 |         &GetWebContents(), GetModelExecutor(), | 
 |         GetModelQualityLogsUploaderService(), GetSessionId(), | 
 |         GetInnerTextProvider(), | 
 |         autofill::FieldGlobalId{{}, autofill::FieldRendererId(-1)}, | 
 |         IsPageLanguageSupported(), this); | 
 |     debug_session_->set_collect_inner_text(false); | 
 |     debug_session_->set_fre_complete( | 
 |         pref_service_->GetBoolean(prefs::kPrefHasCompletedComposeFRE)); | 
 |     debug_session_->set_current_msbb_state(GetMSBBStateFromPrefs()); | 
 |     debug_session_->Bind(std::move(handler), std::move(dialog)); | 
 |     return; | 
 |   } | 
 |  | 
 |   std::optional<FieldIdentifier> target_field; | 
 |   if (skip_show_dialog_for_test_) { | 
 |     target_field = active_compose_ids_; | 
 |   } else if (compose_dialog_controller_) { | 
 |     target_field = compose_dialog_controller_->GetFieldIds(); | 
 |   } | 
 |   if (!target_field.has_value()) { | 
 |     DLOG(WARNING) | 
 |         << "Unable to bind dialog because no controller is available."; | 
 |     compose_dialog_controller_.reset(); | 
 |     return; | 
 |   } | 
 |   if (!HasSession(target_field->first)) { | 
 |     DLOG(WARNING) << "Unable to bind dialog because there is no session for " | 
 |                      "the underlying field."; | 
 |     compose_dialog_controller_.reset(); | 
 |     return; | 
 |   } | 
 |   active_compose_ids_ = target_field; | 
 |   sessions_.at(active_compose_ids_.value().first) | 
 |       ->Bind(std::move(handler), std::move(dialog)); | 
 | } | 
 |  | 
 | void ChromeComposeClient::ShowComposeDialog( | 
 |     EntryPoint ui_entry_point, | 
 |     const autofill::FormFieldData& trigger_field, | 
 |     std::optional<autofill::AutofillClient::PopupScreenLocation> | 
 |         popup_screen_location, | 
 |     ComposeCallback callback) { | 
 |   active_compose_ids_ = std::make_optional<FieldIdentifier>( | 
 |       trigger_field.global_id(), trigger_field.renderer_form_id()); | 
 |  | 
 |   // The selected text received from Autofill is a UTF-16 string truncated using | 
 |   // substr, which will result in a rendered invalid character in the Compose | 
 |   // dialog if it splits a surrogate pair character. Ensure that any invalid | 
 |   // characters are removed. | 
 |   std::string selected_text = | 
 |       base::UTF16ToUTF8(RemoveLastCharIfInvalid(trigger_field.selected_text())); | 
 |  | 
 |   // We only want to resume if there is an existing, unexpired session and the | 
 |   // popup was clicked or the selection is empty. If the context menu is clicked | 
 |   // with a selection we start a new session using the selection. | 
 |   bool popup_clicked = ui_entry_point == EntryPoint::kAutofillPopup; | 
 |   bool resume_current_session = ActiveFieldHasUnexpiredSession() && | 
 |                                 (popup_clicked || selected_text.empty()); | 
 |  | 
 |   if (resume_current_session) { | 
 |     PrepareToResumeExistingSession(std::move(callback), | 
 |                                    /*has_selection=*/!selected_text.empty(), | 
 |                                    popup_clicked); | 
 |   } else { | 
 |     CreateNewSession(std::move(callback), trigger_field, selected_text, | 
 |                      popup_clicked); | 
 |   } | 
 |   last_popup_trigger_source_ = | 
 |       autofill::AutofillSuggestionTriggerSource::kUnspecified; | 
 |  | 
 |   if (!skip_show_dialog_for_test_) { | 
 |     // The bounds given by autofill are relative to the top level frame. Here we | 
 |     // offset by the WebContents container to make up for that. | 
 |     gfx::RectF bounds_in_screen = trigger_field.bounds(); | 
 |     bounds_in_screen.Offset( | 
 |         GetWebContents().GetContainerBounds().OffsetFromOrigin()); | 
 |  | 
 |     show_dialog_start_ = base::TimeTicks::Now(); | 
 |     DCHECK(active_compose_ids_.has_value()); | 
 |     compose_dialog_controller_ = chrome::ShowComposeDialog( | 
 |         GetWebContents(), bounds_in_screen, active_compose_ids_.value()); | 
 |   } | 
 | } | 
 |  | 
 | bool ChromeComposeClient::HasSession( | 
 |     const autofill::FieldGlobalId& trigger_field_id) { | 
 |   auto it = sessions_.find(trigger_field_id); | 
 |   return it != sessions_.end(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::ShowUI() { | 
 |   if (compose_dialog_controller_) { | 
 |     compose_dialog_controller_->ShowUI( | 
 |         base::BindOnce(&ChromeComposeClient::ShowSavedStateNotification, | 
 |                        weak_ptr_factory_.GetWeakPtr(), | 
 |                        /*field_id=*/active_compose_ids_->first)); | 
 |     compose::LogComposeDialogOpenLatency(base::TimeTicks::Now() - | 
 |                                          show_dialog_start_); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::CloseUI(compose::mojom::CloseReason reason) { | 
 |   switch (reason) { | 
 |     case compose::mojom::CloseReason::kFirstRunCloseButton: | 
 |       SetFirstRunSessionCloseReason( | 
 |           compose::ComposeFreOrMsbbSessionCloseReason::kCloseButtonPressed); | 
 |       break; | 
 |     case compose::mojom::CloseReason::kMSBBCloseButton: | 
 |       SetMSBBSessionCloseReason( | 
 |           compose::ComposeFreOrMsbbSessionCloseReason::kCloseButtonPressed); | 
 |       break; | 
 |     case compose::mojom::CloseReason::kCloseButton: | 
 |       base::RecordAction( | 
 |           base::UserMetricsAction("Compose.EndedSession.CloseButtonClicked")); | 
 |       SetSessionCloseReason( | 
 |           compose::ComposeSessionCloseReason::kCloseButtonPressed); | 
 |       LaunchHatsSurveyForActiveSession( | 
 |           compose::ComposeSessionCloseReason::kCloseButtonPressed); | 
 |       break; | 
 |     case compose::mojom::CloseReason::kInsertButton: | 
 |       base::RecordAction( | 
 |           base::UserMetricsAction("Compose.EndedSession.InsertButtonClicked")); | 
 |       SetSessionCloseReason( | 
 |           compose::ComposeSessionCloseReason::kInsertedResponse); | 
 |       SetMSBBSessionCloseReason(compose::ComposeFreOrMsbbSessionCloseReason:: | 
 |                                     kAckedOrAcceptedWithInsert); | 
 |       LaunchHatsSurveyForActiveSession( | 
 |           compose::ComposeSessionCloseReason::kInsertedResponse); | 
 |       SetFirstRunSessionCloseReason( | 
 |           compose::ComposeFreOrMsbbSessionCloseReason:: | 
 |               kAckedOrAcceptedWithInsert); | 
 |       page_ukm_tracker_->ComposeTextInserted(); | 
 |       break; | 
 |   } | 
 |  | 
 |   RemoveActiveSession(); | 
 |  | 
 |   if (compose_dialog_controller_) { | 
 |     compose_dialog_controller_->Close(); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::CompleteFirstRun() { | 
 |   pref_service_->SetBoolean(prefs::kPrefHasCompletedComposeFRE, true); | 
 |  | 
 |   // This marks the end of the FRE "session" as the dialog moves to the main UI | 
 |   // state. Mark all existing sessions as having completed the FRE and log | 
 |   // relevant metrics. | 
 |   UpdateAllSessionsWithFirstRunComplete(); | 
 |   open_settings_requested_ = false; | 
 |   SetFirstRunSessionCloseReason(compose::ComposeFreOrMsbbSessionCloseReason:: | 
 |                                     kAckedOrAcceptedWithoutInsert); | 
 | } | 
 |  | 
 | void ChromeComposeClient::OpenComposeSettings() { | 
 |   Browser* browser = chrome::FindBrowserWithTab(&GetWebContents()); | 
 |   // `browser` should never be null here. This can only be triggered when there | 
 |   // is an active ComposeSession, which  is indirectly owned by the same | 
 |   // WebContents that holds the field that the Compose dialog is triggered from. | 
 |   // The session is created when that dialog is opened and it is destroyed if | 
 |   // its WebContents is destroyed. | 
 |   CHECK(browser); | 
 |  | 
 |   ShowPromoInPage::Params params; | 
 |   params.target_url = chrome::GetSettingsUrl(chrome::kSyncSetupSubPage); | 
 |   params.bubble_anchor_id = kAnonymizedUrlCollectionPersonalizationSettingId; | 
 |   params.bubble_arrow = user_education::HelpBubbleArrow::kBottomRight; | 
 |   params.bubble_text = | 
 |       l10n_util::GetStringUTF16(IDS_COMPOSE_MSBB_IPH_BUBBLE_TEXT); | 
 |   params.close_button_alt_text_id = | 
 |       IDS_COMPOSE_MSBB_IPH_BUBBLE_CLOSE_BUTTON_LABEL_TEXT; | 
 |  | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |   if (active_session) { | 
 |     active_session->set_msbb_settings_opened(); | 
 |   } | 
 |  | 
 |   base::RecordAction( | 
 |       base::UserMetricsAction("Compose.SessionPaused.MSBBSettingsShown")); | 
 |   ShowPromoInPage::Start(browser, std::move(params)); | 
 |  | 
 |   open_settings_requested_ = true; | 
 | } | 
 |  | 
 | void ChromeComposeClient::GetInnerText( | 
 |     content::RenderFrameHost& host, | 
 |     std::optional<int> node_id, | 
 |     content_extraction::InnerTextCallback callback) { | 
 |   content_extraction::GetInnerText(host, node_id, std::move(callback)); | 
 | } | 
 |  | 
 | void ChromeComposeClient::UpdateAllSessionsWithFirstRunComplete() { | 
 |   if (debug_session_) { | 
 |     debug_session_->SetFirstRunCompleted(); | 
 |   } | 
 |   for (const auto& session : sessions_) { | 
 |     session.second->SetFirstRunCompleted(); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::PrepareToResumeExistingSession( | 
 |     ComposeCallback callback, | 
 |     bool has_selection, | 
 |     bool popup_clicked) { | 
 |   ComposeSession* current_session = GetSessionForActiveComposeField(); | 
 |   CHECK(current_session); | 
 |   current_session->set_compose_callback(std::move(callback)); | 
 |   // Update the msbb state which can change while the session is hidden. | 
 |   current_session->set_current_msbb_state(GetMSBBStateFromPrefs()); | 
 |   current_session->MaybeRefreshPageContext(has_selection); | 
 |  | 
 |   if (popup_clicked) { | 
 |     if (last_popup_trigger_source_ == | 
 |         autofill::AutofillSuggestionTriggerSource::kComposeDialogLostFocus) { | 
 |       compose::LogResumeSessionEntryPoint( | 
 |           compose::ComposeEntryPoint::kSavedStateNotification); | 
 |     } else { | 
 |       compose::LogResumeSessionEntryPoint( | 
 |           compose::ComposeEntryPoint::kSavedStateNudge); | 
 |     } | 
 |   } else { | 
 |     compose::LogResumeSessionEntryPoint( | 
 |         compose::ComposeEntryPoint::kContextMenu); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::CreateNewSession( | 
 |     ComposeCallback callback, | 
 |     const autofill::FormFieldData& trigger_field, | 
 |     std::string_view selected_text, | 
 |     bool popup_clicked) { | 
 |   ComposeSession* current_session; | 
 |   autofill::FieldGlobalId trigger_field_id = active_compose_ids_.value().first; | 
 |   if (HasSession(trigger_field_id)) { | 
 |     current_session = sessions_.at(trigger_field_id).get(); | 
 |  | 
 |     // Set the final state for the existing session which will be closed to | 
 |     // start a new one. | 
 |     compose::ComposeFreOrMsbbSessionCloseReason fre_or_msbb_close_reason; | 
 |     if (current_session->HasExpired()) { | 
 |       base::RecordAction( | 
 |           base::UserMetricsAction("Compose.EndedSession.EndedImplicitly")); | 
 |       SetSessionCloseReason( | 
 |           compose::ComposeSessionCloseReason::kExceededMaxDuration); | 
 |       fre_or_msbb_close_reason = | 
 |           compose::ComposeFreOrMsbbSessionCloseReason::kExceededMaxDuration; | 
 |     } else { | 
 |       base::RecordAction(base::UserMetricsAction( | 
 |           "Compose.EndedSession.NewSessionWithSelectedText")); | 
 |       SetSessionCloseReason( | 
 |           compose::ComposeSessionCloseReason::kReplacedWithNewSession); | 
 |       fre_or_msbb_close_reason = | 
 |           compose::ComposeFreOrMsbbSessionCloseReason::kReplacedWithNewSession; | 
 |     } | 
 |     // If the existing session has not accepted consent then set the equivalent | 
 |     // close reason here. If consent was accepted in this session the close | 
 |     // reason will remain as |kAckedOrAcceptedWithoutInsert|. | 
 |     if (!current_session->get_fre_complete()) { | 
 |       SetFirstRunSessionCloseReason(fre_or_msbb_close_reason); | 
 |     } | 
 |     if (!current_session->get_current_msbb_state()) { | 
 |       SetMSBBSessionCloseReason(fre_or_msbb_close_reason); | 
 |     } | 
 |   } | 
 |  | 
 |   auto new_session = std::make_unique<ComposeSession>( | 
 |       &GetWebContents(), GetModelExecutor(), | 
 |       GetModelQualityLogsUploaderService(), GetSessionId(), | 
 |       GetInnerTextProvider(), trigger_field.global_id(), | 
 |       IsPageLanguageSupported(), this, std::move(callback)); | 
 |   current_session = new_session.get(); | 
 |   sessions_.insert_or_assign(active_compose_ids_.value().first, | 
 |                              std::move(new_session)); | 
 |  | 
 |   // Set the FRE state of the new session. | 
 |   auto fre_state = | 
 |       pref_service_->GetBoolean(prefs::kPrefHasCompletedComposeFRE); | 
 |   current_session->set_fre_complete(fre_state); | 
 |  | 
 |   // Set the MSBB state of the new session. | 
 |   current_session->set_current_msbb_state(GetMSBBStateFromPrefs()); | 
 |  | 
 |   current_session->InitializeWithText(selected_text); | 
 |  | 
 |   // Record the UI state that new sessions are created in. | 
 |   if (!fre_state) { | 
 |     base::RecordAction( | 
 |         base::UserMetricsAction("Compose.DialogSeen.FirstRunDisclaimer")); | 
 |   } else if (!GetMSBBStateFromPrefs()) { | 
 |     base::RecordAction( | 
 |         base::UserMetricsAction("Compose.DialogSeen.FirstRunMSBB")); | 
 |   } else { | 
 |     base::RecordAction( | 
 |         base::UserMetricsAction("Compose.DialogSeen.MainDialog")); | 
 |   } | 
 |  | 
 |   // Only record the selection length for new sessions. | 
 |   auto utf8_chars = base::CountUnicodeCharacters(selected_text); | 
 |   compose::LogComposeDialogSelectionLength( | 
 |       utf8_chars.has_value() ? utf8_chars.value() : 0); | 
 |  | 
 |   if (popup_clicked) { | 
 |     switch (most_recent_nudge_entry_point_) { | 
 |       case compose::ComposeEntryPoint::kProactiveNudge: | 
 |         current_session->set_started_with_proactive_nudge(); | 
 |         page_ukm_tracker_->ProactiveNudgeOpened(); | 
 |         compose::LogComposeProactiveNudgeCtr( | 
 |             compose::ComposeNudgeCtrEvent::kDialogOpened); | 
 |         compose::LogStartSessionEntryPoint( | 
 |             compose::ComposeEntryPoint::kProactiveNudge); | 
 |         break; | 
 |       case compose::ComposeEntryPoint::kSelectionNudge: | 
 |         compose::LogComposeSelectionNudgeCtr( | 
 |             compose::ComposeNudgeCtrEvent::kDialogOpened); | 
 |         compose::LogStartSessionEntryPoint( | 
 |             compose::ComposeEntryPoint::kSelectionNudge); | 
 |         break; | 
 |       case compose::ComposeEntryPoint::kContextMenu: | 
 |       case compose::ComposeEntryPoint::kSavedStateNudge: | 
 |       case compose::ComposeEntryPoint::kSavedStateNotification: | 
 |         break; | 
 |     } | 
 |   } else { | 
 |     compose::LogStartSessionEntryPoint( | 
 |         compose::ComposeEntryPoint::kContextMenu); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::RemoveActiveSession() { | 
 |   if (debug_session_) { | 
 |     debug_session_.reset(); | 
 |     return; | 
 |   } | 
 |   if (!active_compose_ids_.has_value()) { | 
 |     return; | 
 |   } | 
 |   auto it = sessions_.find(active_compose_ids_.value().first); | 
 |   CHECK(it != sessions_.end()) | 
 |       << "Attempted to remove compose session that doesn't exist."; | 
 |   sessions_.erase(active_compose_ids_.value().first); | 
 |   active_compose_ids_.reset(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetMSBBSessionCloseReason( | 
 |     compose::ComposeFreOrMsbbSessionCloseReason close_reason) { | 
 |   if (debug_session_) { | 
 |     return; | 
 |   } | 
 |  | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |  | 
 |   if (active_session) { | 
 |     active_session->SetMSBBCloseReason(close_reason); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetFirstRunSessionCloseReason( | 
 |     compose::ComposeFreOrMsbbSessionCloseReason close_reason) { | 
 |   if (debug_session_) { | 
 |     return; | 
 |   } | 
 |  | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |  | 
 |   if (active_session) { | 
 |     active_session->SetFirstRunCloseReason(close_reason); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetSessionCloseReason( | 
 |     compose::ComposeSessionCloseReason close_reason) { | 
 |   if (debug_session_) { | 
 |     return; | 
 |   } | 
 |  | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |  | 
 |   if (active_session) { | 
 |     active_session->SetCloseReason(close_reason); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::LaunchHatsSurveyForActiveSession( | 
 |     compose::ComposeSessionCloseReason close_reason) { | 
 |   if (debug_session_) { | 
 |     return; | 
 |   } | 
 |  | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |  | 
 |   if (active_session) { | 
 |     active_session->LaunchHatsSurvey(close_reason); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::RemoveAllSessions() { | 
 |   if (debug_session_) { | 
 |     debug_session_.reset(); | 
 |   } | 
 |  | 
 |   sessions_.erase(sessions_.begin(), sessions_.end()); | 
 |   active_compose_ids_.reset(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::ShowSavedStateNotification( | 
 |     autofill::FieldGlobalId field_id) { | 
 |   if (!active_compose_ids_.has_value()) { | 
 |     // Do not show the saved state notification on a previous field if another | 
 |     // autofill suggestion is showing in the newly focused field. | 
 |     return; | 
 |   } | 
 |   if (active_compose_ids_->first != field_id && | 
 |       HasSession(active_compose_ids_->first)) { | 
 |     // Do not show the saved state notification on a previous field if focusing | 
 |     // on a new field that will show a compose nudge. Do not show nudge and | 
 |     // saved state notification on two different fields at the same time. | 
 |     return; | 
 |   } | 
 |  | 
 |   if (autofill::AutofillDriver* driver = | 
 |           autofill::ContentAutofillDriver::GetForRenderFrameHost( | 
 |               GetWebContents().GetPrimaryMainFrame())) { | 
 |     driver->RendererShouldTriggerSuggestions( | 
 |         field_id, | 
 |         autofill::AutofillSuggestionTriggerSource::kComposeDialogLostFocus); | 
 |   } | 
 | } | 
 |  | 
 | ComposeSession* ChromeComposeClient::GetSessionForActiveComposeField() { | 
 |   if (active_compose_ids_.has_value()) { | 
 |     auto it = sessions_.find(active_compose_ids_.value().first); | 
 |     if (it != sessions_.end()) { | 
 |       return it->second.get(); | 
 |     } | 
 |   } | 
 |   return nullptr; | 
 | } | 
 |  | 
 | bool ChromeComposeClient::IsPageLanguageSupported() { | 
 |   translate::TranslateManager* translate_manager = | 
 |       ChromeTranslateClient::GetManagerFromWebContents(&GetWebContents()); | 
 |   return compose_enabling_->IsPageLanguageSupported(translate_manager); | 
 | } | 
 |  | 
 | bool ChromeComposeClient::GetMSBBStateFromPrefs() { | 
 |   std::unique_ptr<unified_consent::UrlKeyedDataCollectionConsentHelper> helper = | 
 |       unified_consent::UrlKeyedDataCollectionConsentHelper:: | 
 |           NewAnonymizedDataCollectionConsentHelper(profile_->GetPrefs()); | 
 |   return !(helper != nullptr && !helper->IsEnabled()); | 
 | } | 
 |  | 
 | compose::ComposeManager& ChromeComposeClient::GetManager() { | 
 |   return manager_; | 
 | } | 
 |  | 
 | ComposeEnabling& ChromeComposeClient::GetComposeEnabling() { | 
 |   return *compose_enabling_; | 
 | } | 
 |  | 
 | compose::PageUkmTracker* ChromeComposeClient::GetPageUkmTracker() { | 
 |   return page_ukm_tracker_.get(); | 
 | } | 
 |  | 
 | bool ChromeComposeClient::ActiveFieldHasUnexpiredSession() { | 
 |   if (ComposeSession* current_session = GetSessionForActiveComposeField()) { | 
 |     return !current_session->HasExpired(); | 
 |   } | 
 |   return false; | 
 | } | 
 |  | 
 | bool ChromeComposeClient::ShouldTriggerPopup( | 
 |     const autofill::FormData& form_data, | 
 |     const autofill::FormFieldData& form_field_data, | 
 |     autofill::AutofillSuggestionTriggerSource trigger_source) { | 
 |   // Saved state notification needs the active field set earlier here at nudge | 
 |   // triggering, rather than later when the compose dialog is shown so that we | 
 |   // can know if the user focused on a different field. | 
 |   active_compose_ids_ = std::make_optional<FieldIdentifier>( | 
 |       form_field_data.global_id(), form_field_data.renderer_form_id()); | 
 |  | 
 |   if (ActiveFieldHasUnexpiredSession()) { | 
 |     if (compose_enabling_->ShouldTriggerSavedStatePopup(trigger_source)) { | 
 |       last_popup_trigger_source_ = trigger_source; | 
 |       return true; | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   auto proactive_nudge_status = compose_enabling_->ShouldTriggerNoStatePopup( | 
 |       form_field_data.autocomplete_attribute(), | 
 |       form_field_data.allows_writing_suggestions(), profile_, pref_service_, | 
 |       ChromeTranslateClient::GetManagerFromWebContents(&GetWebContents()), | 
 |       GetWebContents().GetPrimaryMainFrame()->GetLastCommittedOrigin(), | 
 |       form_field_data.origin(), | 
 |       GetWebContents().GetPrimaryMainFrame()->GetLastCommittedURL(), | 
 |       GetMSBBStateFromPrefs()); | 
 |  | 
 |   compose::ProactiveNudgeTracker::Signals nudge_signals; | 
 |   nudge_signals.ukm_source_id = | 
 |       GetWebContents().GetPrimaryMainFrame()->GetPageUkmSourceId(); | 
 |   nudge_signals.page_origin = | 
 |       web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(); | 
 |   nudge_signals.page_url = web_contents()->GetURL(); | 
 |   nudge_signals.form = form_data; | 
 |   nudge_signals.field = form_field_data; | 
 |   nudge_signals.page_change_time = page_change_time_; | 
 |  | 
 |   if (!proactive_nudge_status.has_value()) { | 
 |     compose::LogComposeProactiveNudgeShowStatus(proactive_nudge_status.error()); | 
 |     // Record that the nudge could have shown if it was disabled by | 
 |     // configuration or flags. | 
 |     if (ComposeNudgeShowStatusDisabledByConfig( | 
 |             proactive_nudge_status.error())) { | 
 |       page_ukm_tracker_->ComposeProactiveNudgeShouldShow(); | 
 |     } | 
 |     if (proactive_nudge_status.error() == | 
 |             compose::ComposeShowStatus::kProactiveNudgeFeatureDisabled && | 
 |         compose::GetComposeConfig().selection_nudge_enabled) { | 
 |       // If the proactive nudge is disabled but the selection nudge is is | 
 |       // enabled we need to initialize the nudge tracker for this form field to | 
 |       // accept the selection nudge. | 
 |       return nudge_tracker_.ProactiveNudgeRequestedForFormField( | 
 |           std::move(nudge_signals)); | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   // ProactiveNudgeRequestedForFormField logs metrics for showing the nudge. | 
 |   if (nudge_tracker_.ProactiveNudgeRequestedForFormField( | 
 |           std::move(nudge_signals))) { | 
 |     last_popup_trigger_source_ = trigger_source; | 
 |     return true; | 
 |   } | 
 |   return false; | 
 | } | 
 |  | 
 | bool ChromeComposeClient::IsPopupTimerRunning() { | 
 |   return nudge_tracker_.IsTimerRunning(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::DisableProactiveNudge() { | 
 |   nudge_tracker_.OnUserDisabledNudge(/*single_site_only=*/false); | 
 |   proactive_nudge_enabled_.SetValue(false); | 
 |  | 
 |   switch (most_recent_nudge_entry_point_) { | 
 |     case compose::ComposeEntryPoint::kProactiveNudge: | 
 |       compose::LogComposeProactiveNudgeCtr( | 
 |           compose::ComposeNudgeCtrEvent::kUserDisabledProactiveNudge); | 
 |       GetPageUkmTracker()->ProactiveNudgeDisabledGlobally(); | 
 |       break; | 
 |     case compose::ComposeEntryPoint::kSelectionNudge: | 
 |       compose::LogComposeSelectionNudgeCtr( | 
 |           compose::ComposeNudgeCtrEvent::kUserDisabledProactiveNudge); | 
 |       break; | 
 |     case compose::ComposeEntryPoint::kContextMenu: | 
 |     case compose::ComposeEntryPoint::kSavedStateNudge: | 
 |     case compose::ComposeEntryPoint::kSavedStateNotification: | 
 |       break; | 
 |   } | 
 |  | 
 |   if (base::FeatureList::IsEnabled( | 
 |           compose::features::kHappinessTrackingSurveysForComposeAcceptance)) { | 
 |     HatsService* hats_service = HatsServiceFactory::GetForProfile( | 
 |         profile_, /*create_if_necessary*/ true); | 
 |     if (hats_service) { | 
 |       hats_service->LaunchSurveyForWebContents( | 
 |           kHatsSurveyTriggerComposeNudgeClose, web_contents(), {}, {}); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::OpenProactiveNudgeSettings() { | 
 |   Browser* browser = chrome::FindBrowserWithTab(&GetWebContents()); | 
 |   // `browser` should never be null here. This can only be triggered when there | 
 |   // is an active ComposeSession, which  is indirectly owned by the same | 
 |   // WebContents that holds the field that the Compose dialog is triggered from. | 
 |   // The session is created when that dialog is opened and it is destroyed if | 
 |   // its WebContents is destroyed. | 
 |   CHECK(browser); | 
 |  | 
 |   switch (most_recent_nudge_entry_point_) { | 
 |     case compose::ComposeEntryPoint::kProactiveNudge: | 
 |       compose::LogComposeProactiveNudgeCtr( | 
 |           compose::ComposeNudgeCtrEvent::kOpenSettings); | 
 |       break; | 
 |     case compose::ComposeEntryPoint::kSelectionNudge: | 
 |       compose::LogComposeSelectionNudgeCtr( | 
 |           compose::ComposeNudgeCtrEvent::kOpenSettings); | 
 |       break; | 
 |     case compose::ComposeEntryPoint::kContextMenu: | 
 |     case compose::ComposeEntryPoint::kSavedStateNudge: | 
 |     case compose::ComposeEntryPoint::kSavedStateNotification: | 
 |       break; | 
 |   } | 
 |  | 
 |   chrome::ShowSettingsSubPage(browser, chrome::kAiHelpMeWriteSubpage); | 
 | } | 
 |  | 
 | void ChromeComposeClient::AddSiteToNeverPromptList(const url::Origin& origin) { | 
 |   nudge_tracker_.OnUserDisabledNudge(/*single_site_only=*/true); | 
 |   ScopedDictPrefUpdate update(pref_service_, | 
 |                               prefs::kProactiveNudgeDisabledSitesWithTime); | 
 |   update->Set(origin.Serialize(), base::TimeToValue(base::Time::Now())); | 
 |  | 
 |   switch (most_recent_nudge_entry_point_) { | 
 |     case compose::ComposeEntryPoint::kProactiveNudge: | 
 |       compose::LogComposeProactiveNudgeCtr( | 
 |           compose::ComposeNudgeCtrEvent::kUserDisabledSite); | 
 |       GetPageUkmTracker()->ProactiveNudgeDisabledForSite(); | 
 |       break; | 
 |     case compose::ComposeEntryPoint::kSelectionNudge: | 
 |       compose::LogComposeSelectionNudgeCtr( | 
 |           compose::ComposeNudgeCtrEvent::kUserDisabledSite); | 
 |       break; | 
 |     case compose::ComposeEntryPoint::kContextMenu: | 
 |     case compose::ComposeEntryPoint::kSavedStateNudge: | 
 |     case compose::ComposeEntryPoint::kSavedStateNotification: | 
 |       break; | 
 |   } | 
 | } | 
 |  | 
 | bool ChromeComposeClient::ShouldTriggerContextMenu( | 
 |     content::RenderFrameHost* rfh, | 
 |     content::ContextMenuParams& params) { | 
 |   translate::TranslateManager* translate_manager = | 
 |       ChromeTranslateClient::GetManagerFromWebContents(&GetWebContents()); | 
 |   bool allow_context_menu = compose_enabling_->ShouldTriggerContextMenu( | 
 |       profile_, translate_manager, rfh, params); | 
 |   if (allow_context_menu) { | 
 |     page_ukm_tracker_->MenuItemShown(); | 
 |   } | 
 |   return allow_context_menu; | 
 | } | 
 |  | 
 | void ChromeComposeClient::OnSessionComplete( | 
 |     autofill::FieldGlobalId field_global_id, | 
 |     compose::ComposeSessionCloseReason close_reason, | 
 |     const compose::ComposeSessionEvents& events) { | 
 |   nudge_tracker_.ComposeSessionCompleted(field_global_id, close_reason, events); | 
 | } | 
 |  | 
 | void ChromeComposeClient::OnAfterFocusOnFormField( | 
 |     autofill::AutofillManager& manager, | 
 |     autofill::FormGlobalId form, | 
 |     autofill::FieldGlobalId field) { | 
 |   // Reset the `active_compose_ids_` on every focus change. This will be set to | 
 |   // a valid value when triggering a compose nudge or showing the compose | 
 |   // dialog. | 
 |   active_compose_ids_.reset(); | 
 | } | 
 |  | 
 | optimization_guide::OptimizationGuideModelExecutor* | 
 | ChromeComposeClient::GetModelExecutor() { | 
 |   return model_executor_for_test_.value_or( | 
 |       OptimizationGuideKeyedServiceFactory::GetForProfile( | 
 |           Profile::FromBrowserContext(GetWebContents().GetBrowserContext()))); | 
 | } | 
 |  | 
 | optimization_guide::ModelQualityLogsUploaderService* | 
 | ChromeComposeClient::GetModelQualityLogsUploaderService() { | 
 |   return logs_uploader_service_for_test_.value_or( | 
 |       OptimizationGuideKeyedServiceFactory::GetForProfile( | 
 |           Profile::FromBrowserContext(GetWebContents().GetBrowserContext())) | 
 |           ->GetModelQualityLogsUploaderService()); | 
 | } | 
 |  | 
 | base::Token ChromeComposeClient::GetSessionId() { | 
 |   return session_id_for_test_.value_or(base::Token::CreateRandom()); | 
 | } | 
 |  | 
 | optimization_guide::OptimizationGuideDecider* | 
 | ChromeComposeClient::GetOptimizationGuide() { | 
 |   return opt_guide_; | 
 | } | 
 |  | 
 | InnerTextProvider* ChromeComposeClient::GetInnerTextProvider() { | 
 |   return inner_text_provider_for_test_.value_or(this); | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetModelExecutorForTest( | 
 |     optimization_guide::OptimizationGuideModelExecutor* model_executor) { | 
 |   model_executor_for_test_ = model_executor; | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetModelQualityLogsUploaderServiceForTest( | 
 |     optimization_guide::ModelQualityLogsUploaderService* | 
 |         logs_uploader_service) { | 
 |   logs_uploader_service_for_test_ = logs_uploader_service; | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetSkipShowDialogForTest(bool should_skip) { | 
 |   skip_show_dialog_for_test_ = should_skip; | 
 | } | 
 |  | 
 | void ChromeComposeClient::SetSessionIdForTest(base::Token session_id) { | 
 |   session_id_for_test_ = session_id; | 
 | } | 
 | void ChromeComposeClient::SetInnerTextProviderForTest( | 
 |     InnerTextProvider* inner_text) { | 
 |   inner_text_provider_for_test_ = inner_text; | 
 | } | 
 |  | 
 | bool ChromeComposeClient::IsDialogShowing() { | 
 |   return compose_dialog_controller_ && | 
 |          compose_dialog_controller_->IsDialogShowing(); | 
 | } | 
 |  | 
 | int ChromeComposeClient::GetSessionCountForTest() { | 
 |   return sessions_.size(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::OpenFeedbackPageForTest(std::string feedback_id) { | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |  | 
 |   if (active_session) { | 
 |     active_session->OpenFeedbackPage(feedback_id); | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::PrimaryPageChanged(content::Page& page) { | 
 |   RemoveAllSessions(); | 
 |  | 
 |   page_ukm_tracker_ = std::make_unique<compose::PageUkmTracker>( | 
 |       page.GetMainDocument().GetPageUkmSourceId()); | 
 |  | 
 |   nudge_tracker_.Clear(); | 
 |  | 
 |   compose::ComposeTextUsageLogger::GetOrCreateForCurrentDocument( | 
 |       &page.GetMainDocument()); | 
 |  | 
 |   page_change_time_ = base::TimeTicks::Now(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::OnWebContentsFocused( | 
 |     content::RenderWidgetHost* render_widget_host) { | 
 |   if (!compose_enabling_->IsEnabledForProfile(profile_)) { | 
 |     return; | 
 |   } | 
 |   ComposeSession* active_session = GetSessionForActiveComposeField(); | 
 |   if (open_settings_requested_) { | 
 |     open_settings_requested_ = false; | 
 |  | 
 |     if (active_session && !active_session->get_current_msbb_state() && | 
 |         active_compose_ids_.has_value()) { | 
 |       content::RenderFrameHost* top_level_frame = | 
 |           GetWebContents().GetPrimaryMainFrame(); | 
 |       if (auto* driver = autofill::ContentAutofillDriver::GetForRenderFrameHost( | 
 |               top_level_frame)) { | 
 |         GetManager().OpenCompose( | 
 |             *driver, active_compose_ids_.value().second, | 
 |             active_compose_ids_.value().first, | 
 |             compose::ComposeManagerImpl::UiEntryPoint::kContextMenu); | 
 |       } | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | void ChromeComposeClient::DidGetUserInteraction( | 
 |     const blink::WebInputEvent& event) { | 
 |   if (IsDialogShowing() && | 
 |       event.GetType() == blink::WebInputEvent::Type::kGestureScrollBegin) { | 
 |     // TODO(b/318571287): Log when the dialog is closed due to scrolling. | 
 |     compose_dialog_controller_->Close(); | 
 |   } | 
 | } | 
 | void ChromeComposeClient::OnFocusChangedInPage( | 
 |     const content::FocusedNodeDetails& details) { | 
 |   // TODO(crbug/337690061): Use Autofill events to track focus change. | 
 |   return nudge_tracker_.FocusChangedInPage(); | 
 | } | 
 |  | 
 | void ChromeComposeClient::ShowProactiveNudge( | 
 |     autofill::FormGlobalId form, | 
 |     autofill::FieldGlobalId field, | 
 |     compose::ComposeEntryPoint entry_point) { | 
 |   if (autofill::AutofillDriver* driver = | 
 |           autofill::ContentAutofillDriver::GetForRenderFrameHost( | 
 |               GetWebContents().GetPrimaryMainFrame())) { | 
 |     driver->RendererShouldTriggerSuggestions( | 
 |         field, autofill::AutofillSuggestionTriggerSource:: | 
 |                    kComposeDelayedProactiveNudge); | 
 |   } | 
 |   most_recent_nudge_entry_point_ = entry_point; | 
 | } | 
 |  | 
 | compose::ComposeHintMetadata ChromeComposeClient::GetComposeHintMetadata() { | 
 |   if (!opt_guide_) { | 
 |     return compose::ComposeHintMetadata::default_instance(); | 
 |   } | 
 |  | 
 |   optimization_guide::OptimizationMetadata opt_guide_metadata; | 
 |   auto opt_guide_has_hint = opt_guide_->CanApplyOptimization( | 
 |       GetWebContents().GetPrimaryMainFrame()->GetLastCommittedURL(), | 
 |       optimization_guide::proto::OptimizationType::COMPOSE, | 
 |       &opt_guide_metadata); | 
 |   if (opt_guide_has_hint != | 
 |       optimization_guide::OptimizationGuideDecision::kTrue) { | 
 |     return compose::ComposeHintMetadata::default_instance(); | 
 |   } | 
 |  | 
 |   if (opt_guide_metadata.any_metadata().has_value()) { | 
 |     std::optional<compose::ComposeHintMetadata> compose_metadata = | 
 |         optimization_guide::ParsedAnyMetadata<compose::ComposeHintMetadata>( | 
 |             opt_guide_metadata.any_metadata().value()); | 
 |     if (compose_metadata.has_value()) { | 
 |       return compose_metadata.value(); | 
 |     } | 
 |   } | 
 |  | 
 |   return compose::ComposeHintMetadata::default_instance(); | 
 | } | 
 |  | 
 | WEB_CONTENTS_USER_DATA_KEY_IMPL(ChromeComposeClient); |