blob: be84479d35574bc167731ed8bf72faf84251ae97 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// 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_FEATURE_PROMO_CONTROLLER_H_
#define COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_FEATURE_PROMO_CONTROLLER_H_
#include <initializer_list>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include "base/auto_reset.h"
#include "base/callback_list.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/user_education/common/feature_promo/feature_promo_handle.h"
#include "components/user_education/common/feature_promo/feature_promo_lifecycle.h"
#include "components/user_education/common/feature_promo/feature_promo_registry.h"
#include "components/user_education/common/feature_promo/feature_promo_result.h"
#include "components/user_education/common/feature_promo/feature_promo_session_policy.h"
#include "components/user_education/common/feature_promo/feature_promo_specification.h"
#include "components/user_education/common/help_bubble/help_bubble.h"
#include "components/user_education/common/help_bubble/help_bubble_params.h"
#include "components/user_education/common/tutorial/tutorial_identifier.h"
#include "components/user_education/common/user_education_context.h"
#include "components/user_education/common/user_education_data.h"
#include "ui/base/interaction/element_identifier.h"
namespace ui {
class TrackedElement;
} // namespace ui
// Declaring these in the global namespace for testing purposes.
class BrowserFeaturePromoController2xTestBase;
class BrowserFeaturePromoControllerTestHelper;
class FeaturePromoLifecycleUiTest;
namespace user_education {
class HelpBubbleFactoryRegistry;
class UserEducationStorageService;
class TutorialService;
// Describes the status of a feature promo.
enum class FeaturePromoStatus {
kNotRunning, // The promo is not running or queued.
kQueued, // The promo is queued but not yet shown.
kBubbleShowing, // The promo bubble is showing.
kContinued // The bubble was closed but the promo is still active.
};
// Enum for client code to specify why a promo should be programmatically ended.
enum class EndFeaturePromoReason {
// Used to indicate that the user left the flow of the FeaturePromo.
// For example, this may mean the user ignored a page-specific FeaturePromo
// by navigating to another page.
kAbortPromo,
// Used to indicate that the user interacted with the promoted feature
// in some meaningful way. For example, if an IPH is anchored to
// a page action then clicking the page action might indicate that the
// user engaged with the feature.
kFeatureEngaged,
};
struct FeaturePromoParams;
// Mostly virtual base class for feature promos; used to mock the interface in
// tests.
class FeaturePromoController {
public:
using BubbleCloseCallback = base::OnceClosure;
using ShowPromoResultCallback =
base::OnceCallback<void(FeaturePromoResult promo_result)>;
FeaturePromoController();
FeaturePromoController(const FeaturePromoController& other) = delete;
virtual ~FeaturePromoController();
void operator=(const FeaturePromoController& other) = delete;
// Queries whether the given promo could be shown at the current moment.
//
// In general it is unnecessary to call this method if the intention is to
// show the promo; just call `MaybeShowPromo()` directly. However, in cases
// where determining whether to try to show a promo would be prohibitively
// expensive, this is a slightly less expensive out (but please note that it
// is not zero cost; a number of prefs and application states do need to be
// queried).
//
// Note that some fields of `params` may be ignored if they are not needed to
// perform the checks involved.
virtual FeaturePromoResult CanShowPromo(
const FeaturePromoParams& params,
const UserEducationContextPtr& context) const = 0;
// Starts the promo if possible. If a result callback is specified, it will be
// called with the result of trying to show the promo. In cases where a promo
// could be queued, the callback may happen significantly later.
virtual void MaybeShowPromo(FeaturePromoParams params,
UserEducationContextPtr context) = 0;
// Tries to start the promo at a time when the Feature Engagement backend may
// not yet be initialized. Once it is initialized (which could be
// immediately), attempts to show the promo and calls
// `params.show_promo_result_callback` with the result. If EndPromo() is
// called before the promo is shown, the promo is canceled immediately.
//
// A promo may be queued and then not show due to its Feature Engagement
// conditions not being satisfied. For example, if multiple promos with a
// session limit of 1 are queued, both may queue successfully, but only one
// will actually show. If you care about whether the promo is actually shown,
// set an appropriate `show_promo_result_callback`.
//
// Note: Since `show_promo_result_callback` is asynchronous and can
// theoretically still be pending after the caller's scope disappears, care
// must be taken to avoid a UAF on callback; the caller should prefer to
// either not bind transient objects (e.g. only use the callback for things
// like UMA logging) or use a weak pointer to avoid this situation.
//
// Otherwise, this is identical to MaybeShowPromo().
virtual void MaybeShowStartupPromo(FeaturePromoParams params,
UserEducationContextPtr context) = 0;
// Gets the current status of the promo associated with `iph_feature`.
virtual FeaturePromoStatus GetPromoStatus(
const base::Feature& iph_feature) const = 0;
// Gets the feature for the current promo.
virtual const base::Feature* GetCurrentPromoFeature() const = 0;
// Gets the specification for a feature promo, if a promo is currently
// showing anchored to the given element identifier.
//
// This is used by menus to continue the promo and highlight menu items
// when the user opens the menu.
virtual const FeaturePromoSpecification*
GetCurrentPromoSpecificationForAnchor(
ui::ElementIdentifier menu_element_id) const = 0;
// Returns whether a particular promo has previously been dismissed.
// Useful in cases where determining if a promo should show could be
// expensive. If `last_close_reason` is set, and the promo has been
// dismissed, it wil be populated with the most recent close reason.
// (The value is undefined if this method returns false.)
//
// Note that while `params` is a full parameters block, only `feature` and
// `key` are actually used.
virtual bool HasPromoBeenDismissed(
const FeaturePromoParams& params,
FeaturePromoClosedReason* last_close_reason = nullptr) const = 0;
// Returns whether the promo for `iph_feature` matches kBubbleShowing or any
// of `additional_status`.
template <typename... Args>
bool IsPromoActive(const base::Feature& iph_feature,
Args... additional_status) const {
const FeaturePromoStatus actual = GetPromoStatus(iph_feature);
const std::initializer_list<FeaturePromoStatus> list{additional_status...};
DCHECK(!base::Contains(list, FeaturePromoStatus::kNotRunning));
return actual == FeaturePromoStatus::kBubbleShowing ||
base::Contains(list, actual);
}
// Starts a promo with the settings for skipping any logging or filtering
// provided by the implementation for MaybeShowPromo.
virtual void MaybeShowPromoForDemoPage(FeaturePromoParams params,
UserEducationContextPtr context) = 0;
// Ends or cancels the current promo if it is queued. Returns true if a promo
// was successfully canceled or a bubble closed.
//
// Has no effect for promos closed with CloseBubbleAndContinuePromo(); discard
// or release the FeaturePromoHandle to end those promos.
virtual bool EndPromo(const base::Feature& iph_feature,
EndFeaturePromoReason end_promo_reason) = 0;
// Closes the promo for `iph_feature` - which must be showing - but continues
// the promo via the return value. Dispose or release the resulting handle to
// actually end the promo.
//
// Useful when a promo chains into some other user action and you don't want
// other promos to be able to show until after the operation is finished.
virtual FeaturePromoHandle CloseBubbleAndContinuePromo(
const base::Feature& iph_feature) = 0;
// Returns a weak pointer to this object.
virtual base::WeakPtr<FeaturePromoController> GetAsWeakPtr() = 0;
#if !BUILDFLAG(IS_ANDROID)
// If `feature` has a registered promo, notifies the tracker that the feature
// has been used.
virtual void NotifyFeatureUsedIfValid(const base::Feature& feature) = 0;
#endif
// Posts `result` to `callback` on a fresh call stack. Requires a functioning
// message pump.
static void PostShowPromoResult(ShowPromoResultCallback callback,
FeaturePromoResult result);
protected:
friend class FeaturePromoHandle;
// Called when FeaturePromoHandle is destroyed to finish the promo.
virtual void FinishContinuedPromo(const base::Feature& iph_feature) = 0;
// Records when and why an IPH was not shown.
virtual void RecordPromoNotShown(
const char* feature_name,
FeaturePromoResult::Failure failure) const = 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,
UserEducationStorageService* storage_service,
FeaturePromoSessionPolicy* session_policy,
TutorialService* tutorial_service);
~FeaturePromoControllerCommon() override;
// 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);
// FeaturePromoController:
FeaturePromoStatus GetPromoStatus(
const base::Feature& iph_feature) const override;
const FeaturePromoSpecification* GetCurrentPromoSpecificationForAnchor(
ui::ElementIdentifier menu_element_id) const override;
bool HasPromoBeenDismissed(
const FeaturePromoParams& params,
FeaturePromoClosedReason* close_reason = nullptr) const override;
bool EndPromo(const base::Feature& iph_feature,
EndFeaturePromoReason end_promo_reason) override;
FeaturePromoHandle CloseBubbleAndContinuePromo(
const base::Feature& iph_feature) final;
#if !BUILDFLAG(IS_ANDROID)
void NotifyFeatureUsedIfValid(const base::Feature& feature) override;
#endif
const HelpBubbleFactoryRegistry* bubble_factory_registry() const {
return bubble_factory_registry_;
}
HelpBubbleFactoryRegistry* bubble_factory_registry() {
return bubble_factory_registry_;
}
HelpBubble* promo_bubble_for_testing() { return 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 unit tests. For browser and interactive tests, prefer to use
// `InteractiveFeaturePromoTest`.
[[nodiscard]] static TestLock BlockActiveWindowCheckForTesting();
// Returns true if `BlockActiveWindowCheckForTesting()` is active.
static bool active_window_check_blocked() {
return active_window_check_blocked_;
}
protected:
friend BrowserFeaturePromoController2xTestBase;
friend FeaturePromoLifecycleUiTest;
enum class ShowSource { kNormal, kQueue, kDemo };
// Records when and why an IPH was not shown.
void RecordPromoNotShown(const char* feature_name,
FeaturePromoResult::Failure failure) const final;
const base::Feature* GetCurrentPromoFeature() const final;
// Method that creates the bubble for a feature promo. May return null if the
// bubble cannot be shown.
std::unique_ptr<HelpBubble> ShowPromoBubbleImpl(
FeaturePromoSpecification::BuildHelpBubbleParams build_params,
UserEducationContextPtr context);
// Does the work of ending a promo with the specified `close_reason`.
bool EndPromo(const base::Feature& iph_feature,
FeaturePromoClosedReason close_reason);
// Closes any existing help bubble in `context`; usually called after
// canceling any existing promo to clear up tutorial bubbles, etc.
void CloseHelpBubbleIfPresent(ui::ElementContext context);
// Returns whether we can play a screen reader prompt for the "focus help
// bubble" promo.
// TODO(crbug.com/40200981): 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 CheckExtendedPropertiesPromptAvailable(bool for_demo) const;
// Creates a lifecycle for the given promo.
std::unique_ptr<FeaturePromoLifecycle> CreateLifecycleFor(
const FeaturePromoSpecification& spec,
const FeaturePromoParams& params) const;
// Derived classes need non-const access to these members in const methods.
// Be careful when calling them.
UserEducationStorageService* storage_service() const {
return storage_service_;
}
feature_engagement::Tracker* feature_engagement_tracker() const {
return feature_engagement_tracker_;
}
FeaturePromoSessionPolicy* session_policy() { return session_policy_; }
const FeaturePromoSessionPolicy* session_policy() const {
return session_policy_;
}
FeaturePromoLifecycle* current_promo() { return current_promo_.get(); }
const FeaturePromoLifecycle* current_promo() const {
return current_promo_.get();
}
void set_current_promo(std::unique_ptr<FeaturePromoLifecycle> current_promo) {
current_promo_ = std::move(current_promo);
}
const FeaturePromoPriorityProvider::PromoPriorityInfo& last_promo_info()
const {
return last_promo_info_;
}
void set_last_promo_info(
const FeaturePromoPriorityProvider::PromoPriorityInfo& last_promo_info) {
last_promo_info_ = last_promo_info;
}
HelpBubble* promo_bubble() {
return current_promo_ ? current_promo_->help_bubble() : nullptr;
}
const HelpBubble* promo_bubble() const {
return current_promo_ ? current_promo_->help_bubble() : nullptr;
}
// Saves the close callback for the current bubble.
void set_bubble_closed_callback(BubbleCloseCallback callback) {
bubble_closed_callback_ = std::move(callback);
}
const FeaturePromoRegistry* registry() const { return registry_; }
FeaturePromoRegistry* registry() { return registry_; }
// Removes a promo from the queue and returns whether the promo was found and
// canceled.
virtual bool MaybeUnqueuePromo(const base::Feature& iph_feature) = 0;
// Returns whether `iph_feature` is queued to be shown.
virtual bool IsPromoQueued(const base::Feature& iph_feature) const = 0;
// Possibly fires a queued promo based on certain conditions.
virtual void MaybeShowQueuedPromo() = 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 ui::AcceleratorProvider* accelerator_provider) const = 0;
// Gets a typed weak pointer to this object.
virtual base::WeakPtr<FeaturePromoControllerCommon> GetCommonWeakPtr() = 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,
const ui::AcceleratorProvider* accelerator_provider) const = 0;
// Returns the anchor context for a help bubble, in case the help bubble isn't
// in the same context as the caller. May return null.
virtual UserEducationContextPtr GetContextForHelpBubble(
const ui::TrackedElement* anchor_element) const = 0;
private:
friend BrowserFeaturePromoControllerTestHelper;
void RecordPromoEnded(FeaturePromoClosedReason close_reason,
bool continue_after_close);
FeaturePromoHandle CloseBubbleAndContinuePromoWithReason(
const base::Feature& iph_action,
FeaturePromoClosedReason close_reason);
// FeaturePromoController:
void FinishContinuedPromo(const base::Feature& iph_feature) override;
// Callback that cleans up a help bubble when it is closed.
void OnHelpBubbleClosed(HelpBubble* bubble, HelpBubble::CloseReason reason);
// Callback when the help bubble times out.
void OnHelpBubbleTimedOut(const base::Feature* feature);
// 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 times out.
void OnHelpBubbleTimeout(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,
bool via_action_button);
// 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,
const UserEducationContextPtr& context,
const UserEducationContextPtr& bubble_context,
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,
const UserEducationContextPtr& context,
const UserEducationContextPtr& bubble_context,
FeaturePromoSpecification::CustomActionCallback callback);
// Create appropriate buttons for a toast promo that's part of a rotating
// promo.
std::vector<HelpBubbleButtonParams> CreateRotatingToastButtons(
const base::Feature& feature);
// Create appropriate buttons for a snoozeable promo on the current platform.
std::vector<HelpBubbleButtonParams> CreateSnoozeButtons(
const base::Feature& feature,
bool can_snooze);
// Create appropriate buttons for a tutorial promo on the current platform.
std::vector<HelpBubbleButtonParams> CreateTutorialButtons(
const base::Feature& feature,
const UserEducationContextPtr& context,
const UserEducationContextPtr& bubble_context,
bool can_snooze,
TutorialIdentifier tutorial_id);
// Create appropriate buttons for a custom action promo.
std::vector<HelpBubbleButtonParams> CreateCustomActionButtons(
const base::Feature& feature,
const UserEducationContextPtr& context,
const UserEducationContextPtr& bubble_context,
const std::u16string& custom_action_caption,
FeaturePromoSpecification::CustomActionCallback custom_action_callback,
bool custom_action_is_default,
int custom_action_dismiss_string_id);
// The feature promo registry to use.
const raw_ptr<FeaturePromoRegistry> registry_;
// Non-null as long as a promo is showing.
std::unique_ptr<FeaturePromoLifecycle> current_promo_;
// Policy info about the most recent promo that was shown.
// Updated when a new promo is shown.
FeaturePromoPriorityProvider::PromoPriorityInfo last_promo_info_;
// Promo that is being continued during a tutorial launched from the promo
// bubble.
FeaturePromoHandle tutorial_promo_handle_;
BubbleCloseCallback bubble_closed_callback_;
base::CallbackListSubscription bubble_closed_subscription_;
base::CallbackListSubscription custom_ui_result_subscription_;
const raw_ptr<feature_engagement::Tracker> feature_engagement_tracker_;
const raw_ptr<HelpBubbleFactoryRegistry> bubble_factory_registry_;
const raw_ptr<UserEducationStorageService> storage_service_;
const raw_ptr<FeaturePromoSessionPolicy> session_policy_;
const raw_ptr<TutorialService> tutorial_service_;
// 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_;
};
// Params for showing a promo; you can pass a single feature or add additional
// params as necessary. Replaces the old parameter list as it was (a) long and
// unwieldy, and (b) violated the prohibition on optional parameters in virtual
// methods.
struct FeaturePromoParams {
// NOLINTNEXTLINE(google-explicit-constructor)
FeaturePromoParams(const base::Feature& iph_feature,
const std::string& key = std::string());
FeaturePromoParams(FeaturePromoParams&& other) noexcept;
FeaturePromoParams& operator=(FeaturePromoParams&& other) noexcept;
~FeaturePromoParams();
// The feature for the IPH to show. 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.
raw_ref<const base::Feature> feature;
// The key required for keyed promos. Should be left empty for all other
// (i.e. non-keyed) promos.
std::string key;
// Will be called when the promo actually shows or fails to show. For queued
// promos, will be called when the promo is shown. For non-queued promos, will
// be posted immediately with the result of the request (arrives on a fresh
// message loop call stack).
FeaturePromoController::ShowPromoResultCallback show_promo_result_callback;
// If a bubble was shown and `close_callback` is provided, it will be called
// when the bubble closes. The callback must remain valid as long as the
// bubble shows.
FeaturePromoController::BubbleCloseCallback close_callback;
// If the body text is parameterized, pass parameters here.
FeaturePromoSpecification::FormatParameters body_params =
FeaturePromoSpecification::NoSubstitution();
// If the accessible text is parameterized, pass parameters here.
FeaturePromoSpecification::FormatParameters screen_reader_params =
FeaturePromoSpecification::NoSubstitution();
// If the title text is parameterized, pass parameters here.
FeaturePromoSpecification::FormatParameters title_params =
FeaturePromoSpecification::NoSubstitution();
};
std::ostream& operator<<(std::ostream& os, FeaturePromoStatus status);
} // namespace user_education
#endif // COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_FEATURE_PROMO_CONTROLLER_H_