chromium/chromeos/ash/components/scalable_iph/scalable_iph.h

// 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/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 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,
  };

  // 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();

 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_