1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
|
// 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 <map>
#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_data.h"
#include "ui/base/interaction/element_identifier.h"
namespace ui {
class AcceleratorProvider;
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 = 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) = 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) = 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) = 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;
// 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);
#if !BUILDFLAG(IS_ANDROID)
// If `feature` has a registered promo, notifies the tracker that the feature
// has been used.
void NotifyFeatureUsedIfValid(const base::Feature& feature);
#endif
// 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;
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);
// 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 context in which to locate the anchor view.
virtual ui::ElementContext GetAnchorContext() 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;
// 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 = 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,
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 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,
bool can_snooze,
TutorialIdentifier tutorial_id);
// Create appropriate buttons for a custom action promo.
std::vector<HelpBubbleButtonParams> CreateCustomActionButtons(
const base::Feature& feature,
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_
|