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.

#ifndef CHROME_BROWSER_COMPOSE_CHROME_COMPOSE_CLIENT_H_
#define CHROME_BROWSER_COMPOSE_CHROME_COMPOSE_CLIENT_H_

#include <memory>
#include <optional>
#include <string>

#include "base/containers/flat_map.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/token.h"
#include "chrome/browser/compose/compose_session.h"
#include "chrome/browser/compose/proactive_nudge_tracker.h"
#include "chrome/common/compose/compose.mojom.h"
#include "components/autofill/core/browser/foundations/autofill_manager.h"
#include "components/autofill/core/browser/foundations/scoped_autofill_managers_observation.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/compose/core/browser/compose_client.h"
#include "components/compose/core/browser/compose_dialog_controller.h"
#include "components/compose/core/browser/compose_manager.h"
#include "components/compose/core/browser/compose_manager_impl.h"
#include "components/optimization_guide/core/model_execution/remote_model_executor.h"
#include "components/prefs/pref_member.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"

namespace content {
class Page;
class WebContents;
}  // namespace content

namespace optimization_guide {
class ModelQualityLogsUploaderService;
class OptimizationGuideDecider;
class RemoteModelExecutor;
}  // namespace optimization_guide

class ComposeEnabling;

// An implementation of `ComposeClient` for Desktop and Android.
class ChromeComposeClient
    : public compose::ComposeClient,
      public content::WebContentsObserver,
      public content::WebContentsUserData<ChromeComposeClient>,
      public autofill::AutofillManager::Observer,
      public compose::mojom::ComposeClientUntrustedPageHandler,
      public compose::ProactiveNudgeTracker::Delegate,
      public ComposeSession::Observer,
      public InnerTextProvider {
 public:
  using EntryPoint = autofill::AutofillComposeDelegate::UiEntryPoint;
  class FieldChangeObserver : public autofill::AutofillManager::Observer {
   public:
    explicit FieldChangeObserver(content::WebContents* web_contents);
    ~FieldChangeObserver() override;

    // autofill::AutofillManager::Observer:
    // Used to observe field text content changes so that the proactive nudge
    // can be dismissed after a set number of change events.
    // TODO(b/40286232): Throttling of this event may be added in the future, in
    // which case this implementation would no longer adhere to a strict event
    // count.
    void OnAfterTextFieldValueChanged(
        autofill::AutofillManager& manager,
        autofill::FormGlobalId form,
        autofill::FieldGlobalId field,
        const std::u16string& text_value) override;
    // Used to reset the field content changes count when a new suggestions UI
    // is shown.
    void OnSuggestionsShown(
        autofill::AutofillManager& manager,
        base::span<const autofill::Suggestion> suggestions) override;

    // Asks Autofill to hide any open compose-related popups.
    void HideComposeNudges();

    void SetSkipSuggestionTypeForTest(bool skip_suggestion_type);

    // TODO(b/343204155): SuggestionType check is skipped during testing as the
    // TestAutofillClient API does not currently support use of the Suggestions
    // field.
    bool skip_suggestion_type_for_test_ = false;

    raw_ptr<content::WebContents> web_contents_;
    // Current count of change events fired on the current focused text field,
    // as recorded by `OnAfterTextFieldValueChanged`.
    unsigned int text_field_value_change_event_count_ = 0;

    autofill::ScopedAutofillManagersObservation autofill_managers_observation_{
        this};
  };

  ChromeComposeClient(const ChromeComposeClient&) = delete;
  ChromeComposeClient& operator=(const ChromeComposeClient&) = delete;
  ~ChromeComposeClient() override;

  // compose::ComposeClient:
  compose::ComposeManager& GetManager() override;
  void ShowComposeDialog(
      EntryPoint ui_entry_point,
      const autofill::FormFieldData& trigger_field,
      std::optional<autofill::AutofillClient::PopupScreenLocation>
          popup_screen_location,
      ComposeCallback callback) override;
  bool HasSession(const autofill::FieldGlobalId& trigger_field_id) override;
  bool ShouldTriggerPopup(
      const autofill::FormData& form_data,
      const autofill::FormFieldData& trigger_field,
      autofill::AutofillSuggestionTriggerSource trigger_source) override;
  compose::PageUkmTracker* GetPageUkmTracker() override;
  void DisableProactiveNudge() override;
  void OpenProactiveNudgeSettings() override;
  void AddSiteToNeverPromptList(const url::Origin& origin) override;

  // ComposeSession::Observer:
  void OnSessionComplete(autofill::FieldGlobalId field_global_id,
                         compose::ComposeSessionCloseReason close_reason,
                         const compose::ComposeSessionEvents& events) override;

  // autofill::AutofillManager::Observer:
  // Used to observe field focus changes so that the saved state notification
  // is only shown when an autofill suggestion will not be shown on another
  // field.
  void OnAfterFocusOnFormField(autofill::AutofillManager& manager,
                               autofill::FormGlobalId form,
                               autofill::FieldGlobalId field) override;

  // ComposeClientUntrustedPageHandler
  // Shows the compose dialog.
  void ShowUI() override;
  // Closes the compose dialog. `reason` describes the user action that
  // triggered the close.
  void CloseUI(compose::mojom::CloseReason reason) override;
  // Update corresponding prefs and state when FRE is completed.
  void CompleteFirstRun() override;
  // Opens the Compose-related Chrome settings page in a new tab when the
  // "Go to Settings" link is clicked in the MSBB dialog.
  void OpenComposeSettings() override;

  // InnerTextProvider
  void GetInnerText(content::RenderFrameHost& host,
                    std::optional<int> node_id,
                    content_extraction::InnerTextCallback callback) override;

  bool GetMSBBStateFromPrefs();

  void UpdateAllSessionsWithFirstRunComplete();

  virtual bool ShouldTriggerContextMenu(content::RenderFrameHost* rfh,
                                        content::ContextMenuParams& params);

  void BindComposeDialog(
      mojo::PendingReceiver<compose::mojom::ComposeClientUntrustedPageHandler>
          client_handler,
      mojo::PendingReceiver<compose::mojom::ComposeSessionUntrustedPageHandler>
          handler,
      mojo::PendingRemote<compose::mojom::ComposeUntrustedDialog> dialog);

  // content::WebContentsObserver implementation.
  // Called when the primary page location changes. This includes reloads.
  // TODO: Look into using DocumentUserData or keying sessions on render ID
  // to more accurately save and remove state.
  void PrimaryPageChanged(content::Page& page) override;

  // Notification that the `render_widget_host` for this WebContents has gained
  // focus. We will use this to relaunch a MSBB flow if applicable.
  void OnWebContentsFocused(
      content::RenderWidgetHost* render_widget_host) override;

  // content::WebContentsObserver implementation.
  // Called when there has been direct user interaction with the WebContents.
  // Used to close the dialog when the user scrolls.
  void DidGetUserInteraction(const blink::WebInputEvent& event) override;

  // Called when the focused element changes. This is only used to inform
  // the proactive nudge tracker that focus has changed until the
  // AutofillManager::Observer APIs for focus tracking are fixed.
  void OnFocusChangedInPage(
      const content::FocusedNodeDetails& details) override;

  // compose::ProactiveNudgeTracker::Delegate implementation.
  void ShowProactiveNudge(autofill::FormGlobalId form,
                          autofill::FieldGlobalId field,
                          compose::ComposeEntryPoint entry_point) override;

  // Returns the Compose optimization guide hints for the current URL.
  // compose::ProactiveNudgeTracker::Delegate implementation.
  compose::ComposeHintMetadata GetComposeHintMetadata() override;

  ComposeEnabling& GetComposeEnabling();

  // Returns true when the dialog is showing and false otherwise.
  bool IsDialogShowing();

  // Returns true when the delay timmer to show the popup is running.
  bool IsPopupTimerRunning();

  // Helper methods for setting up testing state.
  int GetSessionCountForTest();
  void SetOptimizationGuideForTest(
      optimization_guide::OptimizationGuideDecider* opt_guide);
  void SetModelExecutorForTest(
      optimization_guide::RemoteModelExecutor* model_executor);
  void SetModelQualityLogsUploaderServiceForTest(
      optimization_guide::ModelQualityLogsUploaderService*
          model_quality_logs_uploader_service);
  void SetSkipShowDialogForTest(bool should_skip);
  void SetSessionIdForTest(base::Token session_id);
  void SetInnerTextProviderForTest(InnerTextProvider* inner_text);

  // If there is an active session calls the OpenFeedbackPage method on it.
  // Used only for testing.
  void OpenFeedbackPageForTest(std::string feedback_id);

 protected:
  explicit ChromeComposeClient(content::WebContents* web_contents);
  optimization_guide::RemoteModelExecutor* GetModelExecutor();
  optimization_guide::ModelQualityLogsUploaderService*
  GetModelQualityLogsUploaderService();
  optimization_guide::OptimizationGuideDecider* GetOptimizationGuide();
  base::Token GetSessionId();
  InnerTextProvider* GetInnerTextProvider();
  std::unique_ptr<ComposeEnabling> compose_enabling_;

 private:
  friend class content::WebContentsUserData<ChromeComposeClient>;
  FRIEND_TEST_ALL_PREFIXES(ChromeComposeClientTest,
                           TestComposeQualityFeedbackPositive);
  FRIEND_TEST_ALL_PREFIXES(ChromeComposeClientTest,
                           TestComposeQualityFeedbackNegative);
  FRIEND_TEST_ALL_PREFIXES(ChromeComposeClientTest,
                           TextFieldChangeThresholdHidesProactiveNudge);

  raw_ptr<Profile> profile_;
  raw_ptr<PrefService> pref_service_;

  // Prepares to open the dialog with an existing session for the active field.
  // Must be called when there is an existing session for the active field (i.e.
  // |GetSessionForACtiveComposeField| returns a valid session.
  // Also records resumption metrics for the existing session.
  void PrepareToResumeExistingSession(ComposeCallback callback,
                                      bool has_selection,
                                      bool popup_clicked);
  // Creates a session for `trigger_field` and initializes it as necessary.
  // `callback` is a callback to the renderer to insert the compose response
  // into the compose field.
  void CreateNewSession(ComposeCallback callback,
                        const autofill::FormFieldData& trigger_field,
                        std::string_view selected_text,
                        bool popup_clicked);

  // Set the exit reason for a session that does not progress past the FRE.
  void SetFirstRunSessionCloseReason(
      compose::ComposeFreOrMsbbSessionCloseReason close_reason);

  // Set the exit reason for a session that does not progress past the
  // MSBB UI.
  void SetMSBBSessionCloseReason(
      compose::ComposeFreOrMsbbSessionCloseReason close_reason);

  // Set the exit reason for a session.
  void SetSessionCloseReason(compose::ComposeSessionCloseReason close_reason);

  // Launch Hats with the active session
  void LaunchHatsSurveyForActiveSession(
      compose::ComposeSessionCloseReason close_reason);

  // Removes `active_compose_field_id_` from `sessions_` and resets
  // `active_compose_field_id_` and `active_compose_form_id_`
  void RemoveActiveSession();

  // Removes all sessions and resets `active_compose_field_id_` and
  // `active_compose_form_id_`.
  void RemoveAllSessions();

  // Shows the saved state notification for `field_id` as long as any newly
  // focused field will not show autofill suggestions.
  void ShowSavedStateNotification(autofill::FieldGlobalId field_id);

  // Returns nullptr if no such session exists.
  ComposeSession* GetSessionForActiveComposeField();

  // Returns true if the active field has an existing session that is not
  // expired.
  bool ActiveFieldHasUnexpiredSession();

  // Checks if the page assessed language is supported by Compose.
  bool IsPageLanguageSupported();

  compose::ComposeManagerImpl manager_{this};

  std::unique_ptr<compose::ComposeDialogController> compose_dialog_controller_;
  // A handle to optimization guide for information about URLs that have
  // recently been navigated to.
  raw_ptr<optimization_guide::OptimizationGuideDecider> opt_guide_;

  std::optional<optimization_guide::RemoteModelExecutor*>
      model_executor_for_test_;

  std::optional<optimization_guide::ModelQualityLogsUploaderService*>
      logs_uploader_service_for_test_;

  std::optional<base::Token> session_id_for_test_;

  // The unique renderer and form IDs of the last field the user selected
  // compose on.
  std::optional<FieldIdentifier> active_compose_ids_;

  // The last trigger source used when calling |ShouldTriggerPopup|. This is
  // reset after the dialog is shown.
  autofill::AutofillSuggestionTriggerSource last_popup_trigger_source_ =
      autofill::AutofillSuggestionTriggerSource::kUnspecified;

  std::optional<InnerTextProvider*> inner_text_provider_for_test_;

  // Saved states for each compose field.
  base::flat_map<autofill::FieldGlobalId, std::unique_ptr<ComposeSession>>
      sessions_;

  // A mojom receiver that is bound to `this` in `BindComposeDialog()`. A pipe
  // may disconnect but this receiver will still be bound, until reset in the
  // next bind call. With mojo, there is no need to immediately reset the
  // binding when the pipe disconnects. Any callbacks in receiver methods can be
  // safely called even when the pipe is disconnected.
  mojo::Receiver<compose::mojom::ComposeClientUntrustedPageHandler>
      client_page_receiver_{this};

  // Time that the last call to show the dialog was started.
  base::TimeTicks show_dialog_start_;

  // Used to test Compose in a tab at |chrome-untrusted://compose|.
  std::unique_ptr<ComposeSession> debug_session_;

  // Collects per-pageload UKM metrics and reports them on destruction (if any
  // were collected).
  std::unique_ptr<compose::PageUkmTracker> page_ukm_tracker_;

  bool skip_show_dialog_for_test_ = false;

  // This boolean gets set to true upon opening the Settings page via the
  // OpenComposeSettings function, and gets set back to false when the current
  // page is refocused using OnWebContentsFocused.
  bool open_settings_requested_ = false;

  // A state machine that decides whether the proactive nudge should be shown at
  // a given moment.
  compose::ProactiveNudgeTracker nudge_tracker_;

  FieldChangeObserver field_change_observer_;

  // Observer for autofill field focus changes. This is used to prevent showing
  // the saved state notification on a previous focused field when an autofill
  // suggestion will be shown in a newly focused field.
  autofill::ScopedAutofillManagersObservation autofill_managers_observation_{
      this};

  BooleanPrefMember proactive_nudge_enabled_;

  // Time since page load, or time since page has changed if it's not loaded
  // yet.
  base::TimeTicks page_change_time_;

  compose::ComposeEntryPoint most_recent_nudge_entry_point_ =
      compose::ComposeEntryPoint::kProactiveNudge;

  base::WeakPtrFactory<ChromeComposeClient> weak_ptr_factory_{this};

  WEB_CONTENTS_USER_DATA_KEY_DECL();
};

#endif  // CHROME_BROWSER_COMPOSE_CHROME_COMPOSE_CLIENT_H_