chromium/chromeos/ash/components/scalable_iph/scalable_iph.cc

// 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.

#include "chromeos/ash/components/scalable_iph/scalable_iph.h"

#include <memory>
#include <string_view>
#include <vector>

#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/fixed_flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "chromeos/ash/components/scalable_iph/config.h"
#include "chromeos/ash/components/scalable_iph/iph_session.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/feature_constants.h"

namespace scalable_iph {

namespace {

using NotificationParams =
    ::scalable_iph::ScalableIphDelegate::NotificationParams;
using BubbleParams = ::scalable_iph::ScalableIphDelegate::BubbleParams;
using BubbleIcon = ::scalable_iph::ScalableIphDelegate::BubbleIcon;

constexpr char kFunctionCallAfterKeyedServiceShutdown[] =
    "Function call after keyed service shutdown.";

// A set of ScalableIph events which can trigger an IPH.
constexpr auto kIphTriggeringEvents =
    base::MakeFixedFlatSet<ScalableIph::Event>(
        {ScalableIph::Event::kFiveMinTick, ScalableIph::Event::kUnlocked});

bool force_enable_iph_feature_for_testing = false;

std::string GetHelpAppIphEventName(ActionType action_type) {
  switch (action_type) {
    case ActionType::kOpenChrome:
      return kEventNameHelpAppActionTypeOpenChrome;
    case ActionType::kOpenLauncher:
      return kEventNameHelpAppActionTypeOpenLauncher;
    case ActionType::kOpenPersonalizationApp:
      return kEventNameHelpAppActionTypeOpenPersonalizationApp;
    case ActionType::kOpenPlayStore:
      return kEventNameHelpAppActionTypeOpenPlayStore;
    case ActionType::kOpenGoogleDocs:
      return kEventNameHelpAppActionTypeOpenGoogleDocs;
    case ActionType::kOpenGooglePhotos:
      return kEventNameHelpAppActionTypeOpenGooglePhotos;
    case ActionType::kOpenSettingsPrinter:
      return kEventNameHelpAppActionTypeOpenSettingsPrinter;
    case ActionType::kOpenPhoneHub:
      return kEventNameHelpAppActionTypeOpenPhoneHub;
    case ActionType::kOpenYouTube:
      return kEventNameHelpAppActionTypeOpenYouTube;
    case ActionType::kOpenFileManager:
      return kEventNameHelpAppActionTypeOpenFileManager;
    case ActionType::kInvalid:
    default:
      return "";
  }
}

// The list of IPH features `SclableIph` supports. `ScalableIph` checks trigger
// conditions of all events listed in this list when it receives an `Event`.
const std::vector<raw_ptr<const base::Feature, VectorExperimental>>&
GetFeatureListConstant() {
  static const base::NoDestructor<
      std::vector<raw_ptr<const base::Feature, VectorExperimental>>>
      feature_list({
          // This must be sorted from One to Ten. A config expects that IPHs are
          // evaluated in this priority.
          // Timer based.
          &feature_engagement::kIPHScalableIphTimerBasedOneFeature,
          &feature_engagement::kIPHScalableIphTimerBasedTwoFeature,
          &feature_engagement::kIPHScalableIphTimerBasedThreeFeature,
          &feature_engagement::kIPHScalableIphTimerBasedFourFeature,
          &feature_engagement::kIPHScalableIphTimerBasedFiveFeature,
          &feature_engagement::kIPHScalableIphTimerBasedSixFeature,
          &feature_engagement::kIPHScalableIphTimerBasedSevenFeature,
          &feature_engagement::kIPHScalableIphTimerBasedEightFeature,
          &feature_engagement::kIPHScalableIphTimerBasedNineFeature,
          &feature_engagement::kIPHScalableIphTimerBasedTenFeature,
          // Unlocked based.
          &feature_engagement::kIPHScalableIphUnlockedBasedOneFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedTwoFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedThreeFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedFourFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedFiveFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedSixFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedSevenFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedEightFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedNineFeature,
          &feature_engagement::kIPHScalableIphUnlockedBasedTenFeature,
          // Help App based.
          &feature_engagement::kIPHScalableIphHelpAppBasedNudgeFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedOneFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedTwoFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedThreeFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedFourFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedFiveFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedSixFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedSevenFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedEightFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedNineFeature,
          &feature_engagement::kIPHScalableIphHelpAppBasedTenFeature,
          // Gaming.
          &feature_engagement::kIPHScalableIphGamingFeature,
      });
  return *feature_list;
}

const base::flat_map<std::string, ActionType>& GetActionTypesMap() {
  // Key will be set in server side config.
  static const base::NoDestructor<base::flat_map<std::string, ActionType>>
      action_types_map({
          {kActionTypeOpenChrome, ActionType::kOpenChrome},
          {kActionTypeOpenLauncher, ActionType::kOpenLauncher},
          {kActionTypeOpenPersonalizationApp,
           ActionType::kOpenPersonalizationApp},
          {kActionTypeOpenPlayStore, ActionType::kOpenPlayStore},
          {kActionTypeOpenGoogleDocs, ActionType::kOpenGoogleDocs},
          {kActionTypeOpenGooglePhotos, ActionType::kOpenGooglePhotos},
          {kActionTypeOpenSettingsPrinter, ActionType::kOpenSettingsPrinter},
          {kActionTypeOpenPhoneHub, ActionType::kOpenPhoneHub},
          {kActionTypeOpenYouTube, ActionType::kOpenYouTube},
          {kActionTypeOpenFileManager, ActionType::kOpenFileManager},
          {kActionTypeOpenHelpAppPerks, ActionType::kOpenHelpAppPerks},
          {kActionTypeOpenChromebookPerksWeb,
           ActionType::kOpenChromebookPerksWeb},
          {kActionTypeOpenChromebookPerksGfnPriority2022,
           ActionType::kOpenChromebookPerksGfnPriority2022},
          {kActionTypeOpenChromebookPerksMinecraft2023,
           ActionType::kOpenChromebookPerksMinecraft2023},
          {kActionTypeOpenChromebookPerksMinecraftRealms2023,
           ActionType::kOpenChromebookPerksMinecraftRealms2023},
      });
  return *action_types_map;
}

const base::flat_map<std::string, BubbleIcon>& GetBubbleIconsMap() {
  // Key will be set in server side config.
  static const base::NoDestructor<base::flat_map<std::string, BubbleIcon>>
      bubble_icons_map({
          {kBubbleIconChromeIcon, BubbleIcon::kChromeIcon},
          {kBubbleIconPlayStoreIcon, BubbleIcon::kPlayStoreIcon},
          {kBubbleIconGoogleDocsIcon, BubbleIcon::kGoogleDocsIcon},
          {kBubbleIconGooglePhotosIcon, BubbleIcon::kGooglePhotosIcon},
          {kBubbleIconPrintJobsIcon, BubbleIcon::kPrintJobsIcon},
          {kBubbleIconYouTubeIcon, BubbleIcon::kYouTubeIcon},
      });
  return *bubble_icons_map;
}

constexpr auto kAppListItemActivationEventsMap =
    base::MakeFixedFlatMap<std::string_view, ScalableIph::Event>({
        {kWebAppGoogleDocsAppId,
         ScalableIph::Event::kAppListItemActivationGoogleDocs},
        {kWebAppYouTubeAppId,
         ScalableIph::Event::kAppListItemActivationYouTube},
        {kWebAppGooglePhotosAppId,
         ScalableIph::Event::kAppListItemActivationGooglePhotosWeb},
        {kAndroidAppGooglePlayStoreAppId,
         ScalableIph::Event::kAppListItemActivationGooglePlayStore},
        {kAndroidAppGooglePhotosAppId,
         ScalableIph::Event::kAppListItemActivationGooglePhotosAndroid},
    });

constexpr auto kShelfItemActivationEventsMap =
    base::MakeFixedFlatMap<std::string_view, ScalableIph::Event>({
        {kWebAppGoogleDocsAppId,
         ScalableIph::Event::kShelfItemActivationGoogleDocs},
        {kWebAppYouTubeAppId, ScalableIph::Event::kShelfItemActivationYouTube},
        {kWebAppGooglePhotosAppId,
         ScalableIph::Event::kShelfItemActivationGooglePhotosWeb},
        {kAndroidGooglePhotosAppId,
         ScalableIph::Event::kShelfItemActivationGooglePhotosAndroid},
    });

constexpr base::TimeDelta kTimeTickEventInterval = base::Minutes(5);

std::string GetEventName(ScalableIph::Event event) {
  // Use switch statement as you can get a compiler error if you forget to add a
  // conversion.
  switch (event) {
    case ScalableIph::Event::kFiveMinTick:
      return kEventNameFiveMinTick;
    case ScalableIph::Event::kUnlocked:
      return kEventNameUnlocked;
    case ScalableIph::Event::kAppListShown:
      return kEventNameAppListShown;
    case ScalableIph::Event::kAppListItemActivationYouTube:
      return kEventNameAppListItemActivationYouTube;
    case ScalableIph::Event::kAppListItemActivationGoogleDocs:
      return kEventNameAppListItemActivationGoogleDocs;
    case ScalableIph::Event::kAppListItemActivationGooglePhotosWeb:
      return kEventNameAppListItemActivationGooglePhotosWeb;
    case ScalableIph::Event::kOpenPersonalizationApp:
      return kEventNameOpenPersonalizationApp;
    case ScalableIph::Event::kShelfItemActivationYouTube:
      return kEventNameShelfItemActivationYouTube;
    case ScalableIph::Event::kShelfItemActivationGoogleDocs:
      return kEventNameShelfItemActivationGoogleDocs;
    case ScalableIph::Event::kShelfItemActivationGooglePhotosWeb:
      return kEventNameShelfItemActivationGooglePhotosWeb;
    case ScalableIph::Event::kShelfItemActivationGooglePhotosAndroid:
      return kEventNameShelfItemActivationGooglePhotosAndroid;
    case ScalableIph::Event::kShelfItemActivationGooglePlay:
      return kEventNameShelfItemActivationGooglePlay;
    case ScalableIph::Event::kAppListItemActivationGooglePlayStore:
      return kEventNameAppListItemActivationGooglePlayStore;
    case ScalableIph::Event::kAppListItemActivationGooglePhotosAndroid:
      return kEventNameAppListItemActivationGooglePhotosAndroid;
    case ScalableIph::Event::kPrintJobCreated:
      return kEventNamePrintJobCreated;
    case ScalableIph::Event::kGameWindowOpened:
      return kEventNameGameWindowOpened;
  }
}

std::string GetParamValue(const base::Feature& feature,
                          const std::string& param_name) {
  std::unique_ptr<Config> config = GetConfig(feature);
  if (config && config->params.contains(param_name)) {
    return config->params.at(param_name);
  }

  std::string fully_qualified_param_name =
      base::StrCat({feature.name, "_", param_name});
  std::string value = base::GetFieldTrialParamValueByFeature(
      feature, fully_qualified_param_name);

  // Non-fully-qualified name field must always be empty.
  DCHECK(base::GetFieldTrialParamValueByFeature(feature, param_name).empty())
      << param_name
      << " is specified in a non-fully-qualified way. It should be specified "
         "as "
      << fully_qualified_param_name
      << ". It's often the case in Scalable Iph to enable multiple features at "
         "once. To avoid an unexpected fall-back behavior, non-fully-qualified "
         "name is not accepted. Parameter names of custom fields must be "
         "specified in a fully qualified way: [Feature Name]_[Parameter Name]";

  return value;
}

void LogParamValueParseError(Logger* logger,
                             const base::Location& location,
                             const std::string& feature_name,
                             const std::string& param_name) {
  logger->Log(
      location,
      base::StringPrintf(
          "%s does not have a valid %s param value. Stop parsing the config.",
          feature_name.c_str(), param_name.c_str()));
}

UiType ParseUiType(Logger* logger, const base::Feature& feature) {
  std::string ui_type = GetParamValue(feature, kCustomUiTypeParamName);
  if (ui_type != kCustomUiTypeValueNotification &&
      ui_type != kCustomUiTypeValueBubble &&
      ui_type != kCustomUiTypeValueNone) {
    SCALABLE_IPH_LOG(logger) << ui_type << " is not a valid UI type.";
  }

  if (ui_type == kCustomUiTypeValueNotification) {
    return UiType::kNotification;
  }

  if (ui_type == kCustomUiTypeValueBubble) {
    return UiType::kBubble;
  }

  return UiType::kNone;
}

UiType GetUiType(Logger* logger, const base::Feature& feature) {
  std::unique_ptr<Config> config = GetConfig(feature);
  if (config) {
    return config->ui_type;
  }

  return ParseUiType(logger, feature);
}

ActionType ParseActionType(const std::string& action_type_string) {
  auto it = GetActionTypesMap().find(action_type_string);
  if (it == GetActionTypesMap().end()) {
    // If the server side config action type cannot be parsed, will return the
    // kInvalid as the parsed result.
    return ActionType::kInvalid;
  }

  return it->second;
}

std::string ParseActionEventName(const std::string& event_used_param) {
  // The `event_used_param` is in this format:
  // `name:ScalableIphTimerBasedOneEventUsed;comparator:any;window:365;storage:365`.
  auto key_values = base::SplitString(
      event_used_param, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  if (key_values.size() != 4) {
    return "";
  }
  auto name_value = base::SplitString(key_values[0], ":", base::TRIM_WHITESPACE,
                                      base::SPLIT_WANT_NONEMPTY);
  if (name_value.size() != 2) {
    return "";
  }

  if (name_value[0] != "name") {
    return "";
  }
  return name_value[1];
}

ScalableIphDelegate::NotificationIcon GetNotificationIcon(
    const std::string& icon) {
  if (icon == kCustomNotificationIconValueRedeem) {
    return ScalableIphDelegate::NotificationIcon::kRedeem;
  }

  return ScalableIphDelegate::NotificationIcon::kDefault;
}

ScalableIphDelegate::NotificationSummaryText GetNotificationSummaryText(
    const std::string& summary_text) {
  if (summary_text == kCustomNotificationSummaryTextValueNone) {
    return ScalableIphDelegate::NotificationSummaryText::kNone;
  }

  return ScalableIphDelegate::NotificationSummaryText::kWelcomeTips;
}

std::unique_ptr<NotificationParams> ParseNotificationParams(
    Logger* logger,
    const base::Feature& feature) {
  std::unique_ptr<NotificationParams> param =
      std::make_unique<NotificationParams>();
  param->notification_id =
      GetParamValue(feature, kCustomNotificationIdParamName);
  if (param->notification_id.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomNotificationIdParamName);
    return nullptr;
  }
  param->title = GetParamValue(feature, kCustomNotificationTitleParamName);
  if (param->title.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomNotificationTitleParamName);
    return nullptr;
  }

  // Notification body text is an optional field. This can take an empty string.
  param->text = GetParamValue(feature, kCustomNotificationBodyTextParamName);

  param->button.text =
      GetParamValue(feature, kCustomNotificationButtonTextParamName);
  if (param->button.text.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomNotificationButtonTextParamName);
    return nullptr;
  }
  std::string action_type =
      GetParamValue(feature, kCustomButtonActionTypeParamName);
  if (action_type.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomButtonActionTypeParamName);
    return nullptr;
  }
  param->button.action.action_type = ParseActionType(action_type);
  if (param->button.action.action_type == ActionType::kInvalid) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomButtonActionTypeParamName);
    return nullptr;
  }
  std::string event_used =
      GetParamValue(feature, kCustomButtonActionEventParamName);
  if (event_used.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomButtonActionEventParamName);
    return nullptr;
  }
  param->button.action.iph_event_name = ParseActionEventName(event_used);
  if (param->button.action.iph_event_name.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomButtonActionEventParamName);
    return nullptr;
  }

  std::string image_type =
      GetParamValue(feature, kCustomNotificationImageTypeParamName);
  param->image_type = ScalableIphDelegate::NotificationImageType::kNoImage;
  if (image_type == kCustomNotificationImageTypeValueWallpaper) {
    param->image_type = ScalableIphDelegate::NotificationImageType::kWallpaper;
  } else if (image_type == kCustomNotificationImageTypeValueMinecraft) {
    param->image_type = ScalableIphDelegate::NotificationImageType::kMinecraft;
  }

  std::string icon = GetParamValue(feature, kCustomNotificationIconParamName);
  if (!icon.empty()) {
    param->icon = GetNotificationIcon(icon);
  }
  SCALABLE_IPH_LOG(logger) << kCustomNotificationIconParamName
                           << " is specified as " << icon << ". " << param->icon
                           << " is set.";

  std::string summary_text =
      GetParamValue(feature, kCustomNotificationSummaryTextParamName);
  if (!summary_text.empty()) {
    param->summary_text = GetNotificationSummaryText(summary_text);
  }
  SCALABLE_IPH_LOG(logger) << kCustomNotificationSummaryTextParamName
                           << " is specified as " << summary_text << ". "
                           << param->summary_text << " is set.";

  std::string source =
      GetParamValue(feature, kCustomNotificationSourceTextParamName);
  if (!source.empty()) {
    param->source = source;
  } else {
    param->source = kCustomNotificationSourceTextValueDefault;
  }
  SCALABLE_IPH_LOG(logger) << kCustomNotificationSourceTextParamName
                           << " is specified as " << source << ". "
                           << param->source << " is set.";

  return param;
}

std::unique_ptr<NotificationParams> GetNotificationParams(
    Logger* logger,
    const base::Feature& feature) {
  std::unique_ptr<Config> config = GetConfig(feature);
  if (config) {
    return std::move(config->notification_params);
  }

  return ParseNotificationParams(logger, feature);
}

BubbleIcon ParseBubbleIcon(const std::string& icon_string) {
  auto it = GetBubbleIconsMap().find(icon_string);
  if (it == GetBubbleIconsMap().end()) {
    // If the server side config bubble icon cannot be parsed, will return the
    // kNoIcon as the parsed result.
    return BubbleIcon::kNoIcon;
  }
  return it->second;
}

std::unique_ptr<BubbleParams> ParseBubbleParams(Logger* logger,
                                                const base::Feature& feature) {
  std::unique_ptr<BubbleParams> param = std::make_unique<BubbleParams>();
  param->bubble_id = GetParamValue(feature, kCustomBubbleIdParamName);
  if (param->bubble_id.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomBubbleIdParamName);
    return nullptr;
  }
  // Title of bubble could be empty.
  param->title = GetParamValue(feature, kCustomBubbleTitleParamName);
  param->text = GetParamValue(feature, kCustomBubbleTextParamName);
  if (param->text.empty()) {
    LogParamValueParseError(logger, FROM_HERE, feature.name,
                            kCustomBubbleTextParamName);
    return nullptr;
  }

  // Button and action:
  // Some nudge may not have a button and action.
  param->button.text = GetParamValue(feature, kCustomBubbleButtonTextParamName);
  if (!param->button.text.empty()) {
    std::string action_type =
        GetParamValue(feature, kCustomButtonActionTypeParamName);
    if (action_type.empty()) {
      LogParamValueParseError(logger, FROM_HERE, feature.name,
                              kCustomButtonActionTypeParamName);
      return nullptr;
    }

    param->button.action.action_type = ParseActionType(action_type);
    if (param->button.action.action_type == ActionType::kInvalid) {
      LogParamValueParseError(logger, FROM_HERE, feature.name,
                              kCustomButtonActionTypeParamName);
      return nullptr;
    }

    std::string event_used =
        GetParamValue(feature, kCustomButtonActionEventParamName);
    if (event_used.empty()) {
      LogParamValueParseError(logger, FROM_HERE, feature.name,
                              kCustomButtonActionEventParamName);
      return nullptr;
    }
    param->button.action.iph_event_name = ParseActionEventName(event_used);
    if (param->button.action.iph_event_name.empty()) {
      LogParamValueParseError(logger, FROM_HERE, feature.name,
                              kCustomButtonActionEventParamName);
      return nullptr;
    }
  }

  auto icon_string = GetParamValue(feature, kCustomBubbleIconParamName);
  param->icon = ParseBubbleIcon(icon_string);
  param->anchor_view_app_id =
      GetParamValue(feature, kCustomBubbleAnchorViewAppIdParamName);

  return param;
}

std::unique_ptr<BubbleParams> GetBubbleParams(Logger* logger,
                                              const base::Feature& feature) {
  std::unique_ptr<Config> config = GetConfig(feature);
  if (config) {
    return std::move(config->bubble_params);
  }

  return ParseBubbleParams(logger, feature);
}

bool ValidateVersionNumber(const base::Feature& feature) {
  std::unique_ptr<Config> config = GetConfig(feature);
  if (config) {
    return config->version_number == kCurrentVersionNumber;
  }

  std::string version_number_value =
      GetParamValue(feature, kCustomParamsVersionNumberParamName);
  if (version_number_value.empty()) {
    return false;
  }

  int version_number = 0;
  if (!base::StringToInt(version_number_value, &version_number)) {
    return false;
  }

  return version_number == kCurrentVersionNumber;
}

}  // namespace

// static
bool ScalableIph::IsAnyIphFeatureEnabled() {
  if (force_enable_iph_feature_for_testing) {
    return true;
  }

  const std::vector<raw_ptr<const base::Feature, VectorExperimental>>&
      feature_list = GetFeatureListConstant();
  for (auto feature : feature_list) {
    if (base::FeatureList::IsEnabled(*feature)) {
      return true;
    }
  }
  return false;
}

// static
void ScalableIph::ForceEnableIphFeatureForTesting() {
  CHECK_IS_TEST();
  CHECK(!force_enable_iph_feature_for_testing)
      << "Iph feature is already force enabled";

  force_enable_iph_feature_for_testing = true;
}

ScalableIph::ScalableIph(feature_engagement::Tracker* tracker,
                         std::unique_ptr<ScalableIphDelegate> delegate,
                         std::unique_ptr<Logger> logger)
    : tracker_(tracker),
      delegate_(std::move(delegate)),
      logger_(std::move(logger)) {
  CHECK(tracker_);
  CHECK(delegate_);
  CHECK(logger_);

  delegate_observation_.Observe(delegate_.get());

  EnsureTimerStarted();

  online_ = delegate_->IsOnline();

  SCALABLE_IPH_LOG(GetLogger()) << "Initialize: Online: " << online_;

  tracker_->AddOnInitializedCallback(
      base::BindOnce(&ScalableIph::CheckTriggerConditionsOnInitSuccess,
                     weak_ptr_factory_.GetWeakPtr()));
}

ScalableIph::~ScalableIph() = default;

void ScalableIph::Shutdown() {
  timer_.Stop();

  tracker_ = nullptr;

  delegate_observation_.Reset();
  delegate_.reset();
}

void ScalableIph::OnConnectionChanged(bool online) {
  if (online_ == online) {
    return;
  }

  online_ = online;

  SCALABLE_IPH_LOG(GetLogger())
      << "Connection status changed. Online: " << online;

  tracker_->AddOnInitializedCallback(
      base::BindOnce(&ScalableIph::CheckTriggerConditionsOnInitSuccess,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ScalableIph::OnSessionStateChanged(
    ScalableIphDelegate::SessionState session_state) {
  if (session_state_ == session_state) {
    // Note that `OnSessionStateChanged` can be called more than once with the
    // same `session_state` as `session_manager::SessionState` does not map to
    // `ScalableIphDelegate::SessionState` with a 1:1 mapping, e.g.
    // `ScalableIphDelegate::SessionState::kOther` is mapped to several states
    // of `session_manager::SessionState`.
    return;
  }

  const bool unlocked =
      session_state_ == ScalableIphDelegate::SessionState::kLocked &&
      session_state != ScalableIphDelegate::SessionState::kLocked;

  session_state_ = session_state;

  SCALABLE_IPH_LOG(GetLogger())
      << "Session state changed to " << session_state
      << ". Whether this is considered to be an unlocked event or not: "
      << unlocked;

  if (unlocked) {
    RecordEvent(Event::kUnlocked);
  }

  if (session_state_ == ScalableIphDelegate::SessionState::kActive) {
    // Run conditions check as an IPH might be shown after a login.
    tracker_->AddOnInitializedCallback(
        base::BindOnce(&ScalableIph::CheckTriggerConditionsOnInitSuccess,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void ScalableIph::OnSuspendDoneWithoutLockScreen() {
  if (session_state_ == ScalableIphDelegate::SessionState::kLocked) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Unexpected ScalableIph::OnSuspendDoneWithoutLockScreen call";
    DCHECK(false) << "OnSuspendDoneWithoutLockScreen should never be called "
                     "with a lock screen";
  }

  SCALABLE_IPH_LOG(GetLogger())
      << "Recording kUnlocked because of OnSuspendDoneWithoutLockScreen";
  RecordEvent(Event::kUnlocked);
}

void ScalableIph::OnAppListVisibilityChanged(bool shown) {
  SCALABLE_IPH_LOG(GetLogger())
      << "App list visibility changed. Shown: " << shown;

  if (shown) {
    RecordEvent(Event::kAppListShown);
  }
}

void ScalableIph::OnHasSavedPrintersChanged(bool has_saved_printers) {
  DCHECK_NE(has_saved_printers_, has_saved_printers);

  has_saved_printers_ = has_saved_printers;

  SCALABLE_IPH_LOG(GetLogger())
      << "Has saved printers status changed. Has saved printers: "
      << has_saved_printers;

  if (!has_saved_printers_closure_for_testing_.is_null()) {
    has_saved_printers_closure_for_testing_.Run();
    has_saved_printers_closure_for_testing_.Reset();
  }
}

void ScalableIph::OnPhoneHubOnboardingEligibleChanged(
    bool phonehub_onboarding_eligible) {
  DCHECK_NE(phonehub_onboarding_eligible_, phonehub_onboarding_eligible);

  SCALABLE_IPH_LOG(GetLogger())
      << "Phonehub onboarding eligible state has "
         "changed: Phone hub onboarding eligible: from: "
      << phonehub_onboarding_eligible_
      << " to: " << phonehub_onboarding_eligible;

  phonehub_onboarding_eligible_ = phonehub_onboarding_eligible;
}

void ScalableIph::PerformActionForIphSession(ActionType action_type) {
  SCALABLE_IPH_LOG(GetLogger())
      << "Performing an action for an iph session. Action type:" << action_type;
  PerformAction(action_type);
}

void ScalableIph::MaybeRecordAppListItemActivation(const std::string& id) {
  auto it = kAppListItemActivationEventsMap.find(id);
  if (it == kAppListItemActivationEventsMap.end()) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Observed an app list item activation. But not recording an app "
           "list item activation as it's not listed in the map.";
    return;
  }

  SCALABLE_IPH_LOG(GetLogger())
      << "Recording an app list item activation as event: " << it->second;
  // Record an event via `RecordEvent` instead of directly notifying an event to
  // `tracker_` as `RecordEvent` can do common tasks, e.g. Making sure that a
  // `tracker_` is initialized, etc.
  RecordEvent(it->second);
}

void ScalableIph::MaybeRecordShelfItemActivationById(const std::string& id) {
  auto it = kShelfItemActivationEventsMap.find(id);
  if (it == kShelfItemActivationEventsMap.end()) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Observed a shelf item activation. But not recording a shelf item "
           "activation as it's not listed in the map.";
    return;
  }

  SCALABLE_IPH_LOG(GetLogger())
      << "Recording a shelf item activation as event: " << it->second;
  RecordEvent(it->second);
}

void ScalableIph::OverrideFeatureListForTesting(
    const std::vector<raw_ptr<const base::Feature, VectorExperimental>>
        feature_list) {
  CHECK(feature_list_for_testing_.size() == 0)
      << "It's NOT allowed to override feature list twice for testing";
  CHECK(feature_list.size() > 0) << "An empty list is NOT allowed to set.";

  feature_list_for_testing_ = feature_list;
}

void ScalableIph::OverrideTaskRunnerForTesting(
    scoped_refptr<base::SequencedTaskRunner> task_runner) {
  CHECK(timer_.IsRunning())
      << "Timer is expected to be always running until Shutdown";
  timer_.Stop();
  timer_.SetTaskRunner(task_runner);
  EnsureTimerStarted();
}

const std::vector<raw_ptr<const base::Feature, VectorExperimental>>&
ScalableIph::GetFeatureListConstantForTesting() {
  CHECK_IS_TEST();
  return GetFeatureListConstant();
}

bool ScalableIph::ShouldPinHelpAppToShelf() {
  return ash::features::AreHelpAppWelcomeTipsEnabled();
}

void ScalableIph::PerformActionForHelpApp(ActionType action_type) {
  SCALABLE_IPH_LOG(GetLogger())
      << "Perform action for help app. Action type: " << action_type;

  std::string iph_event_name = GetHelpAppIphEventName(action_type);

  // ActionType enum is defined on the client side. We can use CHECK as this is
  // a client side constraint.
  CHECK(!iph_event_name.empty()) << "Unable to resolve the IPH event name to "
                                    "an action type for the help app";

  tracker_->NotifyEvent(iph_event_name);

  PerformAction(action_type);
}

void ScalableIph::PerformAction(ActionType action_type) {
  delegate_->PerformActionForScalableIph(action_type);
}

void ScalableIph::SetHasSavedPrintersChangedClosureForTesting(
    base::RepeatingClosure has_saved_printers_closure) {
  CHECK(has_saved_printers_closure_for_testing_.is_null());
  has_saved_printers_closure_for_testing_ =
      std::move(has_saved_printers_closure);
}

void ScalableIph::RecordEvent(ScalableIph::Event event) {
  SCALABLE_IPH_LOG(GetLogger()) << "Record event. Event: " << event;

  if (!tracker_) {
    DCHECK(false) << kFunctionCallAfterKeyedServiceShutdown;
    return;
  }

  // `AddOnInitializedCallback` immediately calls the callback if it's already
  // initialized.
  tracker_->AddOnInitializedCallback(
      base::BindOnce(&ScalableIph::RecordEventInternal,
                     weak_ptr_factory_.GetWeakPtr(), event));
}

Logger* ScalableIph::GetLogger() {
  return logger_.get();
}

void ScalableIph::EnsureTimerStarted() {
  timer_.Start(FROM_HERE, kTimeTickEventInterval,
               base::BindRepeating(&ScalableIph::RecordTimeTickEvent,
                                   weak_ptr_factory_.GetWeakPtr()));
}

void ScalableIph::RecordTimeTickEvent() {
  // Do not record timer event outside of an active session, e.g. OOBE, lock
  // screen.
  if (session_state_ != ScalableIphDelegate::SessionState::kActive) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Observed time tick event. But not recording it as session state is "
           "not Active. Current session state is: "
        << session_state_;
    return;
  }

  SCALABLE_IPH_LOG(GetLogger()) << "Record time tick event.";
  RecordEvent(Event::kFiveMinTick);
}

void ScalableIph::RecordEventInternal(ScalableIph::Event event,
                                      bool init_success) {
  if (!tracker_) {
    DCHECK(false) << kFunctionCallAfterKeyedServiceShutdown;
    return;
  }

  if (!init_success) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Failed to initialize feature_engagement::Tracker";
    DCHECK(false) << "Failed to initialize feature_engagement::Tracker.";
    return;
  }

  if (session_state_ != ScalableIphDelegate::SessionState::kActive) {
    SCALABLE_IPH_LOG(GetLogger())
        << "No event is expected to be recorded outside of an active session.";
    return;
  }

  const std::string event_name = GetEventName(event);
  SCALABLE_IPH_LOG(GetLogger()) << "Recording event as " << event_name;
  tracker_->NotifyEvent(event_name);

  if (kIphTriggeringEvents.contains(event)) {
    SCALABLE_IPH_LOG(GetLogger()) << event
                                  << " is a condition check triggering event. "
                                     "Running trigger conditions check.";
    CheckTriggerConditions(event);
  }
}

void ScalableIph::CheckTriggerConditionsOnInitSuccess(bool init_success) {
  if (!init_success) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Failed to initialize feature_engagement::Tracker.";
    return;
  }

  CheckTriggerConditions(std::nullopt);
}

void ScalableIph::CheckTriggerConditions(
    const std::optional<ScalableIph::Event>& trigger_event) {
  // Make sure that `tracker_` is initialized. `tracker_` should not cause crash
  // even if we call `ShouldTriggerHelpUI` before initialization. But it returns
  // false. It can become a difficult to notice/debug bug if we accidentally
  // introduce a code path where we call it before initialization.
  DCHECK(tracker_->IsInitialized());

  if (session_state_ != ScalableIphDelegate::SessionState::kActive) {
    SCALABLE_IPH_LOG(GetLogger()) << "Session state is not Active. No trigger "
                                     "condition check. Session state is "
                                  << session_state_;
    return;
  }

  SCALABLE_IPH_LOG(GetLogger()) << "Running trigger conditions check.";
  for (const base::Feature* feature : GetFeatureList()) {
    SCALABLE_IPH_LOG(GetLogger()) << "Checking: " << feature->name;

    if (!base::FeatureList::IsEnabled(*feature)) {
      SCALABLE_IPH_LOG(GetLogger())
          << feature->name << " is not enabled. Skipping condition check.";
      continue;
    }

    if (!ValidateVersionNumber(*feature)) {
      SCALABLE_IPH_LOG(GetLogger())
          << "Version number does not match with the current version "
             "number. Skipping a config: "
          << feature->name;
      continue;
    }

    if (!CheckCustomConditions(*feature, trigger_event)) {
      SCALABLE_IPH_LOG(GetLogger())
          << "Custom conditions are not satisfied for " << feature->name;
      continue;
    }
    SCALABLE_IPH_LOG(GetLogger())
        << "Custom conditions are satisfied for " << feature->name;

    if (!tracker_->ShouldTriggerHelpUI(*feature)) {
      SCALABLE_IPH_LOG(GetLogger())
          << "Trigger conditions in feature_engagement::Tracker are not "
             "satisfied for "
          << feature->name;
      continue;
    }
    SCALABLE_IPH_LOG(GetLogger())
        << "Trigger conditions in feature_engagement::Tracker are satisfied "
           "for "
        << feature->name;

    UiType ui_type = GetUiType(GetLogger(), *feature);
    switch (ui_type) {
      case UiType::kNotification: {
        std::unique_ptr<NotificationParams> notification_params =
            GetNotificationParams(GetLogger(), *feature);
        if (!notification_params) {
          SCALABLE_IPH_LOG(GetLogger())
              << "Failed to parse notification params for " << feature->name
              << ". Skipping the config.";
          continue;
        }
        SCALABLE_IPH_LOG(GetLogger()) << "Triggering a notification.";

        if (delegate_->ShowNotification(
                *notification_params.get(),
                std::make_unique<IphSession>(*feature, tracker_, this))) {
          SCALABLE_IPH_LOG(GetLogger())
              << "Requested the UI framework to show a notification. Request "
                 "status: success. -> Do not check other trigger conditions to "
                 "avoid triggering multiple IPHs at the same time.";
          return;
        }

        SCALABLE_IPH_LOG(GetLogger())
            << "Requested the UI framework to show a notification. Request "
               "status: failure. -> Keep checking other trigger conditions as "
               "this IPH should not be shown.";
        continue;
      }
      case UiType::kBubble: {
        std::unique_ptr<BubbleParams> bubble_params =
            GetBubbleParams(GetLogger(), *feature);
        if (!bubble_params) {
          SCALABLE_IPH_LOG(GetLogger())
              << "Failed to parse bubble params for " << feature->name
              << ". Skipping the config.";
          continue;
        }
        SCALABLE_IPH_LOG(GetLogger()) << "Triggering a bubble.";
        if (delegate_->ShowBubble(
                *bubble_params.get(),
                std::make_unique<IphSession>(*feature, tracker_, this))) {
          SCALABLE_IPH_LOG(GetLogger())
              << "Requested the UI framework to show a bubble. Request status: "
                 "success. -> Do not check other trigger conditions to avoid "
                 "triggering multiple IPHs at the same time.";
          return;
        }

        SCALABLE_IPH_LOG(GetLogger())
            << "Requested the UI framework to show a bubble. Request status: "
               "failure. -> Keep checking other trigger conditions as this IPH "
               "should not be shown.";
        continue;
      }
      case UiType::kNone:
        SCALABLE_IPH_LOG(GetLogger())
            << "Condition gets satisfied. But specified ui type is None.";
        break;
    }
  }
}

bool ScalableIph::CheckCustomConditions(
    const base::Feature& feature,
    const std::optional<ScalableIph::Event>& trigger_event) {
  SCALABLE_IPH_LOG(GetLogger())
      << "Checking custom conditions for " << feature.name;
  return CheckTriggerEvent(feature, trigger_event) &&
         CheckNetworkConnection(feature) && CheckClientAge(feature) &&
         CheckHasSavedPrinters(feature) &&
         CheckPhoneHubOnboardingEligible(feature);
}

bool ScalableIph::CheckTriggerEvent(
    const base::Feature& feature,
    const std::optional<ScalableIph::Event>& trigger_event) {
  if (!trigger_event.has_value()) {
    SCALABLE_IPH_LOG(GetLogger())
        << "This condition check is NOT triggered by an event. Skipping this "
           "trigger event condition check.";
    return true;
  }

  SCALABLE_IPH_LOG(GetLogger())
      << "Checking trigger event condition for " << feature.name;

  std::string trigger_event_condition =
      GetParamValue(feature, kCustomConditionTriggerEventParamName);
  if (trigger_event_condition.empty()) {
    SCALABLE_IPH_LOG(GetLogger()) << "No trigger event condition specified.";
    return true;
  }

  std::string trigger_event_name = GetEventName(trigger_event.value());

  const bool result = trigger_event_condition == trigger_event_name;
  SCALABLE_IPH_LOG(GetLogger())
      << "Specified trigger event name is " << trigger_event_condition
      << ". This condition check is triggered by " << trigger_event.value()
      << ". Compared trigger event name is " << trigger_event_name
      << ". Result: " << result;
  return result;
}

bool ScalableIph::CheckNetworkConnection(const base::Feature& feature) {
  SCALABLE_IPH_LOG(GetLogger())
      << "Checking network condition for " << feature.name;
  std::string connection_condition =
      GetParamValue(feature, kCustomConditionNetworkConnectionParamName);
  if (connection_condition.empty()) {
    SCALABLE_IPH_LOG(GetLogger()) << "No network condition specified.";
    return true;
  }

  // If an invalid value is provided, does not satisfy a condition for a
  // fail-safe behavior.
  if (connection_condition != kCustomConditionNetworkConnectionOnline) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Only " << kCustomConditionNetworkConnectionOnline
        << " is the valid value for network connection condition";
    return false;
  }

  SCALABLE_IPH_LOG(GetLogger())
      << "Expecting online. Current status is: Online: " << online_;
  return online_;
}

bool ScalableIph::CheckClientAge(const base::Feature& feature) {
  SCALABLE_IPH_LOG(GetLogger()) << "Checking client age for " << feature.name;
  std::string client_age_condition =
      GetParamValue(feature, kCustomConditionClientAgeInDaysParamName);
  if (client_age_condition.empty()) {
    SCALABLE_IPH_LOG(GetLogger()) << "No client age condition specified.";
    return true;
  }

  // Use `SCALABLE_IPH_LOG`s for logging instead of `DCHECK(false)` as we want
  // to test those fail-safe behaviors in browser_tests.
  int max_client_age = 0;
  if (!base::StringToInt(client_age_condition, &max_client_age)) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Failed to parse client age condition. It must be an integer.";
    return false;
  }

  if (max_client_age < 0) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Client age condition must be a positive integer value.";
    return false;
  }

  int client_age = delegate_->ClientAgeInDays();
  if (client_age < 0) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Client age is a negative number. This can happen if a "
           "user changes time zone, etc. Condition is not satisfied "
           "for a fail safe behavior.";
    return false;
  }

  const bool result = client_age <= max_client_age;
  SCALABLE_IPH_LOG(GetLogger())
      << "Current client age is " << client_age
      << ". Specified max client age is " << max_client_age
      << " (inclusive). Condition satisfied is: " << result;
  return result;
}

bool ScalableIph::CheckHasSavedPrinters(const base::Feature& feature) {
  SCALABLE_IPH_LOG(GetLogger())
      << "Checking has saved printers condition for " << feature.name;
  std::string has_saved_printers_condition =
      GetParamValue(feature, kCustomConditionHasSavedPrintersParamName);
  if (has_saved_printers_condition.empty()) {
    SCALABLE_IPH_LOG(GetLogger())
        << "No has saved printers condition specified.";
    return true;
  }

  if (has_saved_printers_condition !=
          kCustomConditionHasSavedPrintersValueTrue &&
      has_saved_printers_condition !=
          kCustomConditionHasSavedPrintersValueFalse) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Invalid value provided for "
        << kCustomConditionHasSavedPrintersParamName
        << ". This condition is not satisfied for a fail-safe behavior.";
    return false;
  }

  const bool expected_value =
      has_saved_printers_condition == kCustomConditionHasSavedPrintersValueTrue;
  const bool result = has_saved_printers_ == expected_value;
  SCALABLE_IPH_LOG(GetLogger())
      << "Expected value is " << expected_value
      << ". Current has saved printers value is " << has_saved_printers_
      << ". Result is " << result;
  return result;
}

bool ScalableIph::CheckPhoneHubOnboardingEligible(
    const base::Feature& feature) {
  SCALABLE_IPH_LOG(GetLogger())
      << "Checking phone hub onboarding eligible for " << feature.name;

  std::string phonehub_onboarding_eligible_value = GetParamValue(
      feature, kCustomConditionPhoneHubOnboardingEligibleParamName);
  if (phonehub_onboarding_eligible_value.empty()) {
    SCALABLE_IPH_LOG(GetLogger())
        << "No phone hub onboarding eligible condition specified.";
    return true;
  }

  if (phonehub_onboarding_eligible_value !=
      kCustomConditionPhoneHubOnboardingEligibleValueTrue) {
    SCALABLE_IPH_LOG(GetLogger())
        << "Only " << kCustomConditionPhoneHubOnboardingEligibleValueTrue
        << " is a valid value for "
        << kCustomConditionPhoneHubOnboardingEligibleParamName
        << ". Provided value: " << phonehub_onboarding_eligible_value
        << ". Condition not satisfied for a fail-safe behavior.";
    return false;
  }

  SCALABLE_IPH_LOG(GetLogger())
      << "Expected value is "
      << kCustomConditionPhoneHubOnboardingEligibleValueTrue
      << ". Current phone hub onboarding eligible value is "
      << phonehub_onboarding_eligible_ << ". Result is "
      << phonehub_onboarding_eligible_;
  return phonehub_onboarding_eligible_;
}

const std::vector<raw_ptr<const base::Feature, VectorExperimental>>&
ScalableIph::GetFeatureList() const {
  if (!feature_list_for_testing_.empty()) {
    return feature_list_for_testing_;
  }

  return GetFeatureListConstant();
}

std::ostream& operator<<(std::ostream& out, ScalableIph::Event event) {
  return out << GetEventName(event);
}

}  // namespace scalable_iph