chromium/chromeos/ash/components/growth/campaigns_model.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_GROWTH_CAMPAIGNS_MODEL_H_
#define CHROMEOS_ASH_COMPONENTS_GROWTH_CAMPAIGNS_MODEL_H_

#include <memory>
#include <optional>

#include "base/component_export.h"
#include "base/features.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromeos/ash/components/growth/action_performer.h"

namespace base {
class Time;
class Version;
}  // namespace base

namespace gfx {
class Image;
struct VectorIcon;
}  // namespace gfx

namespace ui {
class ImageModel;
}  // namespace ui

namespace growth {

// Entries should not be renumbered and numeric values should never be reused
// as it is used for logging metrics as well. Please keep in sync with
// "CampaignSlot" in tools/metrics/histograms/metadata/ash_growth/enums.xml.
enum class Slot {
  kDemoModeApp = 0,
  kDemoModeFreePlayApps = 1,
  kNudge = 2,
  kNotification = 3,
  kOobePerkDiscovery = 4,
  kMaxValue = kOobePerkDiscovery
};

// These values are deserialized from Growth Campaign, so entries should not
// be renumbered and numeric values should never be reused.
enum class BuiltInVectorIcon { kRedeem = 0, kMaxValue = kRedeem };

// These values are deserialized from Growth Campaign, so entries should not
// be renumbered and numeric values should never be reused.
enum class BuiltInImage {
  kContainerApp = 0,
  kG1 = 1,
  kSparkRebuy = 2,
  kSpark1PApp = 3,
  kSparkV2 = 4,
  kG1Notification = 5,
  kMaxValue = kG1Notification
};

// Supported window anchor element.
// These values are deserialized from Growth Campaign, so entries should not
// be renumbered and numeric values should never be reused.
enum class WindowAnchorType {
  kCaptionButtonContainer = 0,
  kWindowBounds = 1,
  kMaxValue = kWindowBounds
};

// These values are deserialized from Growth Campaign, so entries should not
// be renumbered and numeric values should never be reused.
enum class TriggerType {
  // TODO: b/340950978 - Remove when pass the trigger in GetCampaignsBySlot().
  kUnSpecified = -1,
  kAppOpened = 0,
  kCampaignsLoaded = 1,
  kEvent = 2,
  kDelayedOneShotTimer = 3,
  kMaxValue = kDelayedOneShotTimer
};

class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH) Trigger {
 public:
  explicit Trigger(TriggerType type);

  TriggerType type;

  // `event` is only used for `kEvent` trigger, which needs to be matched with
  // one of the event name in the `triggerEvents` in the `TriggerTargeting`.
  std::string event;
};

// Dictionary of supported targetings. For example:
// {
//    "demoMode" : {...},
//    "session": {...}
// }
using Targeting = base::Value::Dict;

// List of `Targeting`.
using Targetings = base::Value::List;

// Dictionary of supported payloads. For example:
// {
//   "demoMode": {
//     "attractionLoop": {
//       "videoSrcLang1": "/asset/lang1.mp4",
//       "videoSrcLang2": "/asset/lang2.mp4"
//     }
//   }
// }
using Payload = base::Value::Dict;

// Dictionary of Campaign. For example:
// {
//    "id": 1,
//    "studyId":1,
//    "targetings": {...}
//    "payload": {...}
// }
using Campaign = base::Value::Dict;

// List of campaigns.
using Campaigns = base::Value::List;

COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH)
const Payload* GetPayloadBySlot(const Campaign* campaign, Slot slot);

COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH)
std::optional<int> GetCampaignId(const Campaign* campaign);

COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH)
std::optional<int> GetCampaignGroupId(const Campaign* campaign);

COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH)
std::optional<int> GetStudyId(const Campaign* campaign);

COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH)
std::optional<bool> ShouldRegisterTrialWithTriggerEventName(
    const Campaign* campaign);

// Lists of campaigns keyed by the targeted slot. The key is the slot ID in
// string. For example:
// {
//   "0": [...]
//   "1": [...]
// }
using CampaignsPerSlot = base::Value::Dict;

Campaigns* GetMutableCampaignsBySlot(CampaignsPerSlot* campaigns_per_slot,
                                     Slot slot);

const Campaigns* GetCampaignsBySlot(const CampaignsPerSlot* campaigns_per_slot,
                                    Slot slot);

const Targetings* GetTargetings(const Campaign* campaign);

const Payload* GetPayloadBySlot(const Campaign* campaign, Slot slot);

class TargetingBase {
 public:
  TargetingBase(const Targeting* targeting_dict, const char* targeting_path);
  TargetingBase(const TargetingBase&) = delete;
  TargetingBase& operator=(const TargetingBase) = delete;
  ~TargetingBase();

  // True if the specific targeting (e.g: demoMode) was found in the targeting
  // dictionary. The campaign will be selected if the targeted criteria is not
  // found and defer to the next criteria matching.
  bool IsValid() const;

 protected:
  const base::Value::List* GetListCriteria(const char* path_suffix) const;
  const std::optional<bool> GetBoolCriteria(const char* path_suffix) const;
  const std::optional<int> GetIntCriteria(const char* path_suffix) const;
  const std::string* GetStringCriteria(const char* path_suffix) const;
  const base::Value::Dict* GetDictCriteria(const char* path_suffix) const;

 private:
  const std::string GetCriteriaPath(const char* path_suffix) const;

  // The dictionary that contains targeting definition. Owned by
  // `CampaignsManager`.
  raw_ptr<const Targeting> targeting_;
  // The targeting path.
  const char* targeting_path_;
};

// Demo mode targeting. For example:
// {
//   "retailers": ["bb", "bsb"];
//   "storeIds": ["2", "4", "6"],
//   "country": ["US"],
//   "capability": {
//     "isFeatureAwareDevice": false,
//     "isCloudGamingDevice": true,
//   }
// }
class DemoModeTargeting : public TargetingBase {
 public:
  explicit DemoModeTargeting(const Targeting* targeting_dict);
  DemoModeTargeting(const DemoModeTargeting&) = delete;
  DemoModeTargeting& operator=(const DemoModeTargeting) = delete;
  ~DemoModeTargeting();

  const base::Value::List* GetStoreIds() const;
  const base::Value::List* GetRetailers() const;
  const base::Value::List* GetCountries() const;
  const std::optional<base::Version> GetAppMinVersion() const;
  const std::optional<base::Version> GetAppMaxVersion() const;
  const std::optional<bool> TargetCloudGamingDevice() const;
  const std::optional<bool> TargetFeatureAwareDevice() const;
};

// Wrapper around time window targeting dictionary.
//
// The structure looks like:
// {
//   "start": 1697046365,
//   "end": 1697046598
// }
//
// Start and end are the number of seconds since epoch in UTC.
class TimeWindowTargeting {
 public:
  explicit TimeWindowTargeting(const base::Value::Dict* time_window_dict);
  TimeWindowTargeting(const TimeWindowTargeting&) = delete;
  TimeWindowTargeting& operator=(const TimeWindowTargeting) = delete;
  ~TimeWindowTargeting();

  const base::Time GetStartTime() const;
  const base::Time GetEndTime() const;

 private:
  raw_ptr<const base::Value::Dict> time_window_dict_;
};

// Wrapper around number range targeting dictionary.
//
// The structure looks like:
// {
//   "start": 3,
//   "end": 5
// }
class NumberRangeTargeting {
 public:
  explicit NumberRangeTargeting(const base::Value::Dict* number_range_dict);
  NumberRangeTargeting(const NumberRangeTargeting&) = delete;
  NumberRangeTargeting& operator=(const NumberRangeTargeting) = delete;
  ~NumberRangeTargeting();

  const std::optional<int> GetStart() const;
  const std::optional<int> GetEnd() const;

 private:
  raw_ptr<const base::Value::Dict> number_range_dict_;
};

// Wrapper around Device targeting dictionary. The structure looks like:
// {
//   "locales": ["en-US", "zh-CN"];
//   "milestone": {
//      "min": 117,
//      "max": 120
//   }
// }
class DeviceTargeting : public TargetingBase {
 public:
  explicit DeviceTargeting(const Targeting* targeting_dict);
  DeviceTargeting(const DeviceTargeting&) = delete;
  DeviceTargeting& operator=(const DeviceTargeting) = delete;
  ~DeviceTargeting();

  const base::Value::List* GetLocales() const;
  const base::Value::List* GetUserLocales() const;
  const base::Value::List* GetIncludedCountries() const;
  const base::Value::List* GetExcludedCountries() const;
  const std::optional<int> GetMinMilestone() const;
  const std::optional<int> GetMaxMilestone() const;
  const std::optional<base::Version> GetMinVersion() const;
  const std::optional<base::Version> GetMaxVersion() const;
  const std::optional<bool> GetFeatureAwareDevice() const;
  std::unique_ptr<TimeWindowTargeting> GetRegisteredTime() const;
  const std::unique_ptr<NumberRangeTargeting> GetDeviceAge() const;
};

// Wrapper around session targeting dictionary.
//
// The structure looks like:
// {
//   "session": {
//      "experimentTag": [...]
//   }
// }
class SessionTargeting : public TargetingBase {
 public:
  explicit SessionTargeting(const Targeting* targeting_dict);
  SessionTargeting(const SessionTargeting&) = delete;
  SessionTargeting& operator=(const SessionTargeting) = delete;
  ~SessionTargeting();

  std::optional<const base::Feature*> GetFeature() const;
  const base::Value::List* GetExperimentTags() const;

  std::optional<bool> GetMinorUser() const;
  std::optional<bool> GetIsOwner() const;
};

// Wrapper around app targeting dictionary.
//
// The structure looks like:
// {
//   "appId": "app_id",
// }
class AppTargeting {
 public:
  explicit AppTargeting(const base::Value::Dict* app);
  AppTargeting(const AppTargeting&) = delete;
  AppTargeting& operator=(const AppTargeting) = delete;
  ~AppTargeting();

  const std::string* GetAppId() const;

 private:
  raw_ptr<const base::Value::Dict> app_dict_;
};

// Wrapper around events targeting dictionary.
//
// The structure looks like:
// {
//   "events": {
//     // A list of list of conditions to meet.
//     // The inside list condition is logic OR.
//     // The outer list condition is logic AND.
//     // conditions_met = (A || B || C) && D;
//     "conditions": [// Thsese two conditions are logic AND.
//       [ // These three conditions are logic OR.
//         "name:A;comparator:==2;window:365;storage:365",
//         "name:B;comparator:==5;window:365;storage:365",
//         "name:C;comparator:==8;window:365;storage:365"
//       ],
//       [
//         "name:D;comparator:<3;window:365;storage:365"
//       ]
//     ]
//   }
// }
class EventsTargeting {
 public:
  explicit EventsTargeting(const base::Value::Dict* config);
  EventsTargeting(const EventsTargeting&) = delete;
  EventsTargeting& operator=(const EventsTargeting) = delete;
  ~EventsTargeting();

  int GetImpressionCap() const;
  int GetDismissalCap() const;
  std::optional<int> GetGroupImpressionCap() const;
  std::optional<int> GetGroupDismissalCap() const;
  const base::Value::List* GetEventsConditions() const;

 private:
  raw_ptr<const base::Value::Dict> config_dict_;
};

// Wrapper around trigger targeting dictionary.
//
// The structure looks like:
// {
//   "triggerType": 0,
//   "triggerEvents": ["a", "b"]
// }
class TriggerTargeting {
 public:
  explicit TriggerTargeting(const base::Value::Dict* app);
  TriggerTargeting(const TriggerTargeting&) = delete;
  TriggerTargeting& operator=(const TriggerTargeting) = delete;
  ~TriggerTargeting();

  std::optional<int> GetTriggerType() const;
  const base::Value::List* GetTriggerEvents() const;

 private:
  raw_ptr<const base::Value::Dict> trigger_dict_;
};

// Wrapper around runtime targeting dictionary.
//
// The structure looks like:
// {
//   "runtime": {
//      "schedulings": [...]
//      "appsOpend": [...]
//      "triggers": [...]
//   }
// }
class RuntimeTargeting : public TargetingBase {
 public:
  explicit RuntimeTargeting(const Targeting* targeting_dict);
  RuntimeTargeting(const RuntimeTargeting&) = delete;
  RuntimeTargeting& operator=(const RuntimeTargeting) = delete;
  ~RuntimeTargeting();

  const std::vector<std::unique_ptr<TimeWindowTargeting>> GetSchedulings()
      const;

  // Returns a list of apps to be matched against the current opened app.
  const std::vector<std::unique_ptr<AppTargeting>> GetAppsOpened() const;

  const std::vector<std::string> GetActiveUrlRegexes() const;

  std::unique_ptr<EventsTargeting> GetEventsConfig() const;

  // Returns a list of triggers against the current trigger, e.g. `kAppOpened`.
  const std::vector<std::unique_ptr<TriggerTargeting>> GetTriggers() const;

  const base::Value::List* GetUserPrefTargetings() const;
};

// Wrapper around the action dictionary for performing an action, including
// action type and action params.
// For example:
// {
//   "action": {
//     "type": 3,
//     "params": {
//       "url": "https://www.google.com",
//       "disposition": 0
//     }
//   }
// }
class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH) Action {
 public:
  explicit Action(const base::Value::Dict* action_dict);
  Action(const Action&) = delete;
  Action& operator=(const Action) = delete;
  ~Action();

  std::optional<growth::ActionType> GetActionType() const;
  const base::Value::Dict* GetParams() const;

  raw_ptr<const base::Value::Dict> action_dict_;
};

// Wrapper around anchor.
//
// The structure looks like:
// {
//   "activeAppWindowAnchorType": 0  // CAPTION_BUTTON_CONTAINER
// }
// TODO(b/329698643): Consider moving to nudge controller if Anchor is not used
// by other surfaces.
class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH) Anchor {
 public:
  explicit Anchor(const base::Value::Dict* anchor_dict);
  Anchor(const Anchor&) = delete;
  Anchor& operator=(const Anchor) = delete;
  ~Anchor();

  const std::optional<WindowAnchorType> GetActiveAppWindowAnchorType() const;
  const std::string* GetShelfAppButtonId() const;

 private:
  raw_ptr<const base::Value::Dict> anchor_dict_;
};

// Wrapper around image dictionary.
//
// The structure looks like:
// {
//  "builtInImage": 0
// }
class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH) Image {
 public:
  explicit Image(const base::Value::Dict* image_dict);
  Image(const Image&) = delete;
  Image& operator=(const Image) = delete;
  ~Image();

  const gfx::Image* GetImage() const;

 private:
  // Get built in icon based on the given image data.
  const gfx::Image* GetBuiltInImage() const;

  raw_ptr<const base::Value::Dict> image_dict_;
};

// Wrapper around vector icon dictionary.
//
// The structure looks like:
// {
//  "builtVectorIcon": 0
// }
class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH) VectorIcon {
 public:
  explicit VectorIcon(const base::Value::Dict* vector_icon_dict);
  VectorIcon(const VectorIcon&) = delete;
  VectorIcon& operator=(const VectorIcon) = delete;
  ~VectorIcon();

  const gfx::VectorIcon* GetVectorIcon() const;

 private:
  // Get built in icon based on the given image data.
  const gfx::VectorIcon* GetBuiltInVectorIcon() const;

  raw_ptr<const base::Value::Dict> vector_icon_dict_;
};

// Wrapper around image model dictionary.
//
// The structure looks like:
// {
//   "image": {
//    "builtInImage": 0
//   }
// }
class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_GROWTH) ImageModel {
 public:
  explicit ImageModel(const base::Value::Dict* image_model_dict);
  ImageModel(const Image&) = delete;
  ImageModel& operator=(const ImageModel) = delete;
  ~ImageModel();

  const std::optional<ui::ImageModel> GetImageModel() const;

 private:
  // Get built in icon based on the given image data.
  // If given data is referring to an image, the image will be resized to 60 *
  // 60 so it can be used in the nudge.
  // TODO: b/340945779 - consider moving the resize logic to
  // `ShowNudgeActionPerformer`.
  const std::optional<ui::ImageModel> GetBuiltInImageModel() const;

  raw_ptr<const base::Value::Dict> image_model_dict_;
};

}  // namespace growth

#endif  // CHROMEOS_ASH_COMPONENTS_GROWTH_CAMPAIGNS_MODEL_H_