// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_CONTROLLER_H_ #define COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_CONTROLLER_H_ #include #include #include #include "base/auto_reset.h" #include "base/callback.h" #include "base/callback_list.h" #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" #include "components/feature_engagement/public/tracker.h" #include "components/user_education/common/feature_promo_handle.h" #include "components/user_education/common/feature_promo_registry.h" #include "components/user_education/common/feature_promo_specification.h" #include "components/user_education/common/help_bubble.h" #include "components/user_education/common/help_bubble_params.h" #include "components/user_education/common/tutorial_identifier.h" namespace base { struct Feature; } namespace ui { class AcceleratorProvider; class TrackedElement; } // namespace ui // Declaring these in the global namespace for testing purposes. class BrowserFeaturePromoControllerTest; class FeaturePromoSnoozeInteractiveTest; namespace user_education { class FeaturePromoSnoozeService; class HelpBubbleFactoryRegistry; class TutorialService; // Mostly virtual base class for feature promos; used to mock the interface in // tests. class FeaturePromoController { public: using BubbleCloseCallback = base::OnceClosure; FeaturePromoController(); FeaturePromoController(const FeaturePromoController& other) = delete; virtual ~FeaturePromoController(); void operator=(const FeaturePromoController& other) = delete; // Starts the promo if possible. Returns whether it started. // |iph_feature| must be an IPH feature defined in // components/feature_engagement/public/feature_list.cc and registered // with |FeaturePromoRegistry|. Note that this is different than the // feature that the IPH is showing for. // // If the body text is parameterized, pass text replacements in // |body_text_replacements|. // // If a bubble was shown and |close_callback| was provided, it will be // called when the bubble closes. |close_callback| must be valid as // long as the bubble shows. // // For users that can't register their parameters with // FeaturePromoRegistry, see // |FeaturePromoControllerViews::MaybeShowPromoWithParams()|. Prefer // statically registering params with FeaturePromoRegistry and using // this method when possible. virtual bool MaybeShowPromo( const base::Feature& iph_feature, FeaturePromoSpecification::StringReplacements body_text_replacements = {}, BubbleCloseCallback close_callback = BubbleCloseCallback()) = 0; // Returns whether a bubble is showing for the given promo. If // `include_continued_promos` is set, also returns true if a promo bubble has // been hidden with CloseBubbleAndContinuePromo() but the promo is still // active in the background. virtual bool IsPromoActive(const base::Feature& iph_feature, bool include_continued_promos) const = 0; // Starts a promo with the settings for skipping any logging or filtering // provided by the implementation for MaybeShowPromo. virtual bool MaybeShowPromoForDemoPage( const base::Feature* iph_feature, FeaturePromoSpecification::StringReplacements body_text_replacements = {}, BubbleCloseCallback close_callback = BubbleCloseCallback()) = 0; // If a bubble is showing for |iph_feature| close it and end the // promo. Does nothing otherwise. Returns true if a bubble was closed // and false otherwise. // // Calling this has no effect if |CloseBubbleAndContinuePromo()| was // called for |iph_feature|. virtual bool CloseBubble(const base::Feature& iph_feature) = 0; // Like CloseBubble() but does not end the promo yet. The caller takes // ownership of the promo (e.g. to show a highlight in a menu or on a // button). The returned FeaturePromoHandle represents this ownership. virtual FeaturePromoHandle CloseBubbleAndContinuePromo( const base::Feature& iph_feature) = 0; // Returns a weak pointer to this object. virtual base::WeakPtr GetAsWeakPtr() = 0; protected: friend class FeaturePromoHandle; // Called when FeaturePromoHandle is destroyed to finish the promo. virtual void FinishContinuedPromo(const base::Feature& iph_feature) = 0; }; // Manages display of in-product help promos. All IPH displays in Top // Chrome should go through here. class FeaturePromoControllerCommon : public FeaturePromoController { public: using TestLock = std::unique_ptr>; FeaturePromoControllerCommon( feature_engagement::Tracker* feature_engagement_tracker, FeaturePromoRegistry* registry, HelpBubbleFactoryRegistry* help_bubble_registry, FeaturePromoSnoozeService* snooze_service, TutorialService* tutorial_service); ~FeaturePromoControllerCommon() override; // Only for security or privacy critical promos. Immediately shows a // promo with |params|, cancelling any normal promo and blocking any // further promos until it's done. // // Returns an ID that can be passed to CloseBubbleForCriticalPromo() // if successful. This can fail if another critical promo is showing. std::unique_ptr ShowCriticalPromo( const FeaturePromoSpecification& spec, ui::TrackedElement* anchor_element, FeaturePromoSpecification::StringReplacements body_text_replacements = {}); // For systems where there are rendering issues of e.g. displaying the // omnibox and a bubble in the same region on the screen, dismisses a non- // critical promo bubble which overlaps a given screen region. Returns true // if a bubble is closed as a result. bool DismissNonCriticalBubbleInRegion(const gfx::Rect& screen_bounds); // Blocks further promos and closes any existing non-critical ones. [[nodiscard]] TestLock BlockPromosForTesting(); // Returns the associated feature engagement tracker. feature_engagement::Tracker* feature_engagement_tracker() { return feature_engagement_tracker_; } // FeaturePromoController: bool MaybeShowPromo( const base::Feature& iph_feature, FeaturePromoSpecification::StringReplacements body_text_replacements = {}, BubbleCloseCallback close_callback = BubbleCloseCallback()) override; bool IsPromoActive(const base::Feature& iph_feature, bool include_continued_promos = false) const override; bool MaybeShowPromoForDemoPage( const base::Feature* iph_feature, FeaturePromoSpecification::StringReplacements body_text_replacements = {}, BubbleCloseCallback close_callback = BubbleCloseCallback()) override; bool CloseBubble(const base::Feature& iph_feature) override; FeaturePromoHandle CloseBubbleAndContinuePromo( const base::Feature& iph_feature) override; base::WeakPtr GetAsWeakPtr() override; HelpBubbleFactoryRegistry* bubble_factory_registry() { return bubble_factory_registry_; } HelpBubble* promo_bubble_for_testing() { return promo_bubble(); } HelpBubble* critical_promo_bubble_for_testing() { return critical_promo_bubble(); } TutorialService* tutorial_service_for_testing() { return tutorial_service_; } // Blocks a check whether the IPH would be created in an inactive window or // app before showing the IPH. Intended for browser and unit tests. // The actual implementation of the check is in the platform-specific // implementation of CanShowPromo(). [[nodiscard]] static TestLock BlockActiveWindowCheckForTesting(); protected: friend BrowserFeaturePromoControllerTest; friend FeaturePromoSnoozeInteractiveTest; // For IPH not registered with |FeaturePromoRegistry|. Only use this // if it is infeasible to pre-register your IPH. bool MaybeShowPromoFromSpecification( const FeaturePromoSpecification& spec, ui::TrackedElement* anchor_element, FeaturePromoSpecification::StringReplacements body_text_replacements, BubbleCloseCallback close_callback); FeaturePromoSnoozeService* snooze_service() { return snooze_service_; } HelpBubble* promo_bubble() { return promo_bubble_.get(); } const HelpBubble* promo_bubble() const { return promo_bubble_.get(); } HelpBubble* critical_promo_bubble() { return critical_promo_bubble_; } const HelpBubble* critical_promo_bubble() const { return critical_promo_bubble_; } // Gets the context in which to locate the anchor view. virtual ui::ElementContext GetAnchorContext() const = 0; // Determine if the current context and anchor element allow showing a promo. // This lets us rule out e.g. inactive and incognito windows/apps for // non-critical promos. // // Note: Implementations should make sure to check // active_window_check_blocked(). virtual bool CanShowPromo(ui::TrackedElement* anchor_element) const = 0; // Get the accelerator provider to use to look up accelerators. virtual const ui::AcceleratorProvider* GetAcceleratorProvider() const = 0; // Gets the alt text to use for body icons. virtual std::u16string GetBodyIconAltText() const = 0; // Gets the feature associated with prompting the user how to navigate to help // bubbles via the keyboard. It is its own promo, and will stop playing in // most cases when the user has made use of it enough times. // // If null is returned, no attempt will be made to play a prompt. virtual const base::Feature* GetScreenReaderPromptPromoFeature() const = 0; // This is the associated event with the promo feature above. The event is // recorded only if and when the promo is actually played to the user. virtual const char* GetScreenReaderPromptPromoEventName() const = 0; // Returns the special prompt to play with the initial bubble of a tutorial; // instead of the general navigation help prompt returned by // GetFocusHelpBubbleScreenReaderHint(). virtual std::u16string GetTutorialScreenReaderHint() const = 0; // This method returns an appropriate prompt for promoting using a navigation // accelerator to focus the help bubble. virtual std::u16string GetFocusHelpBubbleScreenReaderHint( FeaturePromoSpecification::PromoType promo_type, ui::TrackedElement* anchor_element, bool is_critical_promo) const = 0; FeaturePromoRegistry* registry() { return registry_; } static bool active_window_check_blocked() { return active_window_check_blocked_; } private: // FeaturePromoController: void FinishContinuedPromo(const base::Feature& iph_feature) override; // Returns whether we can play a screen reader prompt for the "focus help // bubble" promo. // TODO(crbug.com/1258216): This must be called *before* we ask if the bubble // will show because a limitation in the current FE backend causes // ShouldTriggerHelpUI() to always return false if another promo is being // displayed. Once we have machinery to allow concurrency in the FE system // all of this logic can be rewritten. bool CheckScreenReaderPromptAvailable() const; // Method that creates the bubble for a feature promo. May return null if the // bubble cannot be shown. std::unique_ptr ShowPromoBubbleImpl( const FeaturePromoSpecification& spec, ui::TrackedElement* anchor_element, FeaturePromoSpecification::StringReplacements body_text_replacements, bool screen_reader_prompt_available, bool is_critical_promo); // Callback that cleans up a help bubble when it is closed. void OnHelpBubbleClosed(HelpBubble* bubble); // Callback for snoozed features. void OnHelpBubbleSnoozed(const base::Feature* feature); // Callback for snoozed tutorial features. . void OnTutorialHelpBubbleSnoozed(const base::Feature* iph_feature, TutorialIdentifier tutorial_id); // Callback when a feature's help bubble is dismissed by any means other than // snoozing (including "OK" or "Got it!" buttons). void OnHelpBubbleDismissed(const base::Feature* feature); // Callback when the dismiss button for IPH for tutorials is clicked. void OnTutorialHelpBubbleDismissed(const base::Feature* iph_feature, TutorialIdentifier tutorial_id); // Callback when a tutorial triggered from a promo is actually started. void OnTutorialStarted(const base::Feature* iph_feature, TutorialIdentifier tutorial_id); // Called when a tutorial launched via StartTutorial() completes. void OnTutorialComplete(const base::Feature* iph_feature); // Called when a tutorial launched via StartTutorial() aborts. void OnTutorialAborted(const base::Feature* iph_feature); // Called when the user opts to take a custom action. void OnCustomAction(const base::Feature* iph_feature, FeaturePromoSpecification::CustomActionCallback callback); // Create appropriate buttons for a snoozable promo on the current platform. std::vector CreateSnoozeButtons( const base::Feature& feature); // Create appropriate buttons for a tutorial promo on the current platform. std::vector CreateTutorialButtons( const base::Feature& feature, TutorialIdentifier tutorial_id); // Create appropriate buttons for a custom action promo. std::vector CreateCustomActionButtons( const base::Feature& feature, const std::u16string& custom_action_caption, FeaturePromoSpecification::CustomActionCallback custom_action_callback, bool custom_action_is_default); // The feature promo registry to use. const raw_ptr registry_; // Non-null as long as a promo is showing. Corresponds to an IPH // feature registered with |feature_engagement_tracker_|. raw_ptr current_iph_feature_ = nullptr; bool continuing_after_bubble_closed_ = false; // The help bubble, if a feature promo bubble is showing. std::unique_ptr promo_bubble_; // Has a value if a critical promo is showing. If this has a value, // |current_iph_feature_| will usually be null. There is one edge case // where this may not be true: when a critical promo is requested // between a normal promo's CloseBubbleAndContinuePromo() call and its // end. raw_ptr critical_promo_bubble_ = nullptr; // Promo that is being continued during a tutorial launched from the promo // bubble. FeaturePromoHandle tutorial_promo_handle_; base::OnceClosure bubble_closed_callback_; base::CallbackListSubscription bubble_closed_subscription_; const raw_ptr feature_engagement_tracker_; const raw_ptr bubble_factory_registry_; const raw_ptr snooze_service_; const raw_ptr tutorial_service_; // When set to true, promos will never be shown. bool promos_blocked_for_testing_ = false; // In the case where the user education demo page wants to bypass the feature // engagement tracker, the current iph feature will be set and then checked // against to verify the right feature is bypassing. this page is located at // internals/user-education. raw_ptr iph_feature_bypassing_tracker_ = nullptr; base::WeakPtrFactory weak_ptr_factory_{this}; // Whether IPH should be allowed to show in an inactive window or app. // Should be checked in implementations of CanShowPromo(). Typically only // modified in tests. static bool active_window_check_blocked_; }; } // namespace user_education #endif // COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_CONTROLLER_H_