| // 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/browser/optimization_guide/optimization_guide_keyed_service.h" |
| #include "chrome/common/compose/compose.mojom.h" |
| #include "components/autofill/content/browser/scoped_autofill_managers_observation.h" |
| #include "components/autofill/core/browser/foundations/autofill_manager.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/hints/optimization_guide_decision.h" |
| #include "components/optimization_guide/core/optimization_guide_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" |
| #include "mojo/public/cpp/bindings/remote.h" |
| |
| namespace content { |
| class Page; |
| class WebContents; |
| } // namespace content |
| |
| 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) 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(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::OptimizationGuideModelExecutor* 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::OptimizationGuideModelExecutor* 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::OptimizationGuideModelExecutor*> |
| 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_ |