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