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
|
// 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 CHROMEOS_ASH_COMPONENTS_SCALABLE_IPH_SCALABLE_IPH_H_
#define CHROMEOS_ASH_COMPONENTS_SCALABLE_IPH_SCALABLE_IPH_H_
#include <optional>
#include <ostream>
#include <vector>
#include "base/component_export.h"
#include "base/containers/enum_set.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "base/timer/timer.h"
#include "chromeos/ash/components/scalable_iph/logger.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph_constants.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph_delegate.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/keyed_service/core/keyed_service.h"
namespace scalable_iph {
// `ScalableIph` provides a scalable way to deliver IPHs.
//
// - Scalable: we provide a scalable way by building this framework on top of
// the feature engagement framework. A developer can set up an IPH without
// modifying a binary. See feature engagement doc for details about its
// flexibility: //components/feature_engagement/README.md.
//
// - IPH: in-product-help.
//
// Class diagram:
// =============================================================================
//
// //chromeos/ash/components | //chrome/browser/ash
// -----------------------------------------------------------------------------
//
// |-------------|
// | |
// | | |---------------------| |-----|
// | | -[TriggerIph]-> | | -[ShowUI]--> | |
// | | ---[Action]---> | ScalableIphDelegate | -[OpenUrl]-> | Ash |
// | | <--[Observer]-- | | <-[Events]-- | |
// | | |---------------------| |-----|
// | | |
// | | |---------| |
// | ScalableIph | <---[Action]--- | HelpApp | |
// | | |---------| |
// | | \|/
// | | |------------|
// | | <-------------------[Action]----------------- | IphSession |
// | | |------------|
// | | |
// | | \|/
// | | |---------------------------------|
// | | -------[Interact]------> | //components/feature_engagement |
// |-------------| |---------------------------------|
//
// ScalableIph: The main component of Scalable Iph framework. This class checks
// trigger conditions and parse Scalable Iph custom fields, e.g.
// Custom conditions.
// ScalableIphDelegate: A delegate class for `ScalableIph` to delegate its tasks
// to Ash or Chrome. An implementation of
// `ScalableIphDelegate` will be in //chrome/browser/ash.
// Delegated tasks will be:
// - Show an IPH UI, e.g. Notification.
// - Observe events, e.g. Network connection.
// - Perform actions, e.g. Open a URL.
// IphSession: An object for managing a single IPH session. If an UI is opened
// by `ScalableIph` (e.g. Notification, Bubble), `IphSession` is
// passed to those code for `ScalableIph` to manage an IPH session
// and for those UIs to perform actions. `IphSession` can interact
// with a `feature_engagement::Tracker` directly as it holds a
// reference to it. But it has to delegate actions to `ScalableIph`
// as it is in //chromeos/ash/components. `ScalableIph` delegates
// them again to `ScalableIphDelegate`.
//
class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_SCALABLE_IPH) ScalableIph
: public KeyedService,
public ScalableIphDelegate::Observer,
public IphSession::Delegate {
public:
// List of events ScalableIph supports.
enum class Event {
kFiveMinTick = 0,
kUnlocked,
kAppListShown,
kAppListItemActivationYouTube,
kAppListItemActivationGoogleDocs,
kAppListItemActivationGooglePhotosWeb,
kOpenPersonalizationApp,
kShelfItemActivationYouTube,
kShelfItemActivationGoogleDocs,
kShelfItemActivationGooglePhotosWeb,
kShelfItemActivationGooglePhotosAndroid,
kShelfItemActivationGooglePlay,
kAppListItemActivationGooglePlayStore,
kAppListItemActivationGooglePhotosAndroid,
kPrintJobCreated,
kGameWindowOpened,
};
enum SessionStateTransition {
// The state machine expects that it advances its state to the new state. In
// practice, this means that we update `session_state_` in `ScalableIph`
// instance.
kAdvanceState,
// Observe this session state transition as unlocked event. Note that
// unlocked event includes a session start in `ScalableIph`.
kUnlock
};
using TransitionSet = base::EnumSet<SessionStateTransition,
SessionStateTransition::kAdvanceState,
SessionStateTransition::kUnlock>;
// Returns true if any iph feature flag is enabled. Otherwise false.
static bool IsAnyIphFeatureEnabled();
// Force enable `IsAnyIphFeatureEnabled` check for testing. Note that no
// actual iph feature flag gets enabled by this.
static void ForceEnableIphFeatureForTesting();
ScalableIph(feature_engagement::Tracker* tracker,
std::unique_ptr<ScalableIphDelegate> delegate,
std::unique_ptr<Logger> logger);
void RecordEvent(Event event);
Logger* GetLogger();
ScalableIphDelegate* delegate_for_testing() { return delegate_.get(); }
// KeyedService:
~ScalableIph() override;
void Shutdown() override;
// ScalableIphDelegate::Observer:
void OnConnectionChanged(bool online) override;
void OnSessionStateChanged(ScalableIphDelegate::SessionState state) override;
void OnSuspendDoneWithoutLockScreen() override;
void OnAppListVisibilityChanged(bool shown) override;
void OnHasSavedPrintersChanged(bool has_saved_printers) override;
void OnPhoneHubOnboardingEligibleChanged(
bool phonehub_onboarding_eligible) override;
// IphSession::Delegate:
void PerformActionForIphSession(ActionType action_type) override;
void OverrideFeatureListForTesting(
const std::vector<raw_ptr<const base::Feature, VectorExperimental>>
features);
void OverrideTaskRunnerForTesting(
scoped_refptr<base::SequencedTaskRunner> task_runner);
// Called for a user action in the help app. All the logging related to
// help app action events will be done here before calling `PerformAction`.
void PerformActionForHelpApp(ActionType action_type);
// Perform `action_type` as a result of a user action, e.g. A link click in a
// help app, etc. This notifies a corresponding IPH event to the feature
// engagement framework.
//
// UIs which were initiated with `IphSession` (e.g. Notification, Bubble)
// should use `IphSession::PerformAction` instead of this method.
void PerformAction(ActionType action_type);
// `SyncedPrintersManager` stores its observers in `ObserverListThreadSafe`,
// which invokes observers via `TaskRunner`. Test code can set a closure to
// this method to wait an observer of `ScalableIph` being called.
//
// Note:
// We cannot wait this by registering another observer in a test and wait it.
// Observers are stored in an unordered map. There is no guarantee on the
// order of calls.
void SetHasSavedPrintersChangedClosureForTesting(
base::RepeatingClosure has_saved_printers_closure);
// Maybe record an app list item or a shelf item activation of `id`.
void MaybeRecordAppListItemActivation(const std::string& id);
void MaybeRecordShelfItemActivationById(const std::string& id);
// Returns true if the help app should be pinned to the bottom shelf.
bool ShouldPinHelpAppToShelf();
static const std::vector<raw_ptr<const base::Feature, VectorExperimental>>&
GetFeatureListConstantForTesting();
static TransitionSet GetTransitionForTesting(
ScalableIphDelegate::SessionState from,
ScalableIphDelegate::SessionState to);
bool CheckTriggerEventForTesting(
const base::Feature& feature,
const std::optional<ScalableIph::Event>& trigger_event);
private:
void EnsureTimerStarted();
void RecordTimeTickEvent();
void RecordUnlockedEvent();
void RecordEventInternal(Event event, bool init_success);
void CheckTriggerConditionsOnInitSuccess(bool init_success);
void CheckTriggerConditions(
const std::optional<ScalableIph::Event>& trigger_event);
// Check all custom conditions assigned to `feature`. Returns true if all
// conditions are valid and satisfied. Otherwise false including an invalid
// config case.
bool CheckCustomConditions(const base::Feature& feature,
const std::optional<Event>& trigger_event);
bool CheckTriggerEvent(const base::Feature& feature,
const std::optional<Event>& trigger_event);
bool CheckNetworkConnection(const base::Feature& feature);
bool CheckClientAge(const base::Feature& feature);
bool CheckHasSavedPrinters(const base::Feature& feature);
bool CheckPhoneHubOnboardingEligible(const base::Feature& feature);
const std::vector<raw_ptr<const base::Feature, VectorExperimental>>&
GetFeatureList() const;
raw_ptr<feature_engagement::Tracker> tracker_;
std::unique_ptr<ScalableIphDelegate> delegate_;
base::RepeatingTimer timer_;
bool online_ = false;
ScalableIphDelegate::SessionState session_state_ =
ScalableIphDelegate::SessionState::kUnknownInitialValue;
bool has_saved_printers_ = false;
bool phonehub_onboarding_eligible_ = false;
std::unique_ptr<Logger> logger_;
base::RepeatingClosure has_saved_printers_closure_for_testing_;
std::vector<raw_ptr<const base::Feature, VectorExperimental>>
feature_list_for_testing_;
base::ScopedObservation<ScalableIphDelegate, ScalableIph>
delegate_observation_{this};
base::WeakPtrFactory<ScalableIph> weak_ptr_factory_{this};
};
std::ostream& operator<<(std::ostream& out, ScalableIph::Event event);
} // namespace scalable_iph
#endif // CHROMEOS_ASH_COMPONENTS_SCALABLE_IPH_SCALABLE_IPH_H_
|