| // 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 CHROME_BROWSER_UI_USER_EDUCATION_FEATURE_PROMO_CONTROLLER_H_ |
| #define CHROME_BROWSER_UI_USER_EDUCATION_FEATURE_PROMO_CONTROLLER_H_ |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #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 "chrome/browser/ui/user_education/feature_promo_registry.h" |
| #include "chrome/browser/ui/user_education/feature_promo_specification.h" |
| #include "chrome/browser/ui/user_education/help_bubble.h" |
| #include "chrome/browser/ui/user_education/help_bubble_params.h" |
| #include "chrome/browser/ui/user_education/tutorial/tutorial_identifier.h" |
| #include "components/feature_engagement/public/tracker.h" |
| |
| namespace base { |
| struct Feature; |
| } |
| |
| namespace ui { |
| class AcceleratorProvider; |
| class TrackedElement; |
| } // namespace ui |
| |
| 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; |
| |
| // Represents a promo that has been continued after its bubble has been |
| // hidden, as a result of calling CloseBubbleAndContinuePromo(). |
| // |
| // The promo is considered still active until the handle is released or |
| // destroyed and no other promos will be allowed to show. |
| // |
| // PromoHandle is a value-typed, movable smart reference; default constructed |
| // instances are falsy (i.e. operator bool and is_valid() return false), as |
| // are any instances that have been moved or released. |
| class PromoHandle { |
| public: |
| PromoHandle(); |
| PromoHandle(base::WeakPtr<FeaturePromoController> controller, |
| const base::Feature* feature); |
| PromoHandle(PromoHandle&&); |
| ~PromoHandle(); |
| |
| PromoHandle& operator=(PromoHandle&&); |
| |
| explicit operator bool() const { return is_valid(); } |
| bool operator!() const { return !is_valid(); } |
| |
| // Returns whether the handle refers to a valid promo. Returns null for |
| // default-constructed objects and after being moved or released. |
| bool is_valid() const { return feature_; } |
| |
| // Releases the promo and resets the handle. After release, operator bool |
| // will return false regardless of the previous state. |
| void Release(); |
| |
| private: |
| base::WeakPtr<FeaturePromoController> controller_; |
| const base::Feature* feature_ = nullptr; |
| }; |
| |
| 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 PromoHandle represents this ownership. |
| virtual PromoHandle CloseBubbleAndContinuePromo( |
| const base::Feature& iph_feature) = 0; |
| |
| // Returns a weak pointer to this object. |
| virtual base::WeakPtr<FeaturePromoController> GetAsWeakPtr() = 0; |
| |
| protected: |
| // Called when PromoHandle 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<base::AutoReset<bool>>; |
| |
| 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. Immedialy 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<HelpBubble> 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; |
| PromoHandle CloseBubbleAndContinuePromo( |
| const base::Feature& iph_feature) override; |
| base::WeakPtr<FeaturePromoController> 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 class BrowserFeaturePromoControllerTest; |
| friend class 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; |
| |
| // These methods control how snooze buttons appear and function. |
| virtual std::u16string GetSnoozeButtonText() const = 0; |
| virtual std::u16string GetDismissButtonText() const = 0; |
| virtual bool IsOkButtonLeading() 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, |
| const 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<HelpBubble> 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 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 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); |
| |
| // Create appropriate buttons for a snoozable promo on the current platform. |
| std::vector<HelpBubbleButtonParams> CreateSnoozeButtons( |
| const base::Feature& feature); |
| |
| // Create appropriate buttons for a tutorial promo on the current platform. |
| std::vector<HelpBubbleButtonParams> CreateTutorialButtons( |
| const base::Feature& feature, |
| TutorialIdentifier tutorial_id); |
| |
| // The feature promo registry to use. |
| FeaturePromoRegistry* const registry_; |
| |
| // Non-null as long as a promo is showing. Corresponds to an IPH |
| // feature registered with |feature_engagement_tracker_|. |
| raw_ptr<const base::Feature> current_iph_feature_ = nullptr; |
| bool continuing_after_bubble_closed_ = false; |
| |
| // The help bubble, if a feature promo bubble is showing. |
| std::unique_ptr<HelpBubble> 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<HelpBubble> critical_promo_bubble_ = nullptr; |
| |
| // Promo that is being continued during a tutorial launched from the promo |
| // bubble. |
| PromoHandle tutorial_promo_handle_; |
| |
| base::OnceClosure bubble_closed_callback_; |
| base::CallbackListSubscription bubble_closed_subscription_; |
| |
| const raw_ptr<feature_engagement::Tracker> feature_engagement_tracker_; |
| const raw_ptr<HelpBubbleFactoryRegistry> bubble_factory_registry_; |
| const raw_ptr<FeaturePromoSnoozeService> snooze_service_; |
| const raw_ptr<TutorialService> 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. |
| const base::Feature* iph_feature_bypassing_tracker_ = nullptr; |
| |
| base::WeakPtrFactory<FeaturePromoControllerCommon> 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_; |
| }; |
| |
| #endif // CHROME_BROWSER_UI_USER_EDUCATION_FEATURE_PROMO_CONTROLLER_H_ |