910e62b5创建于 1月15日历史提交
// 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/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/browser/suggestions/suggestion_hiding_reason.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/hints/optimization_guide_decision.h"
#include "components/optimization_guide/core/model_execution/remote_model_executor.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::RemoteModelExecutor*
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::RemoteModelExecutor* 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().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);