chromium/ash/user_education/welcome_tour/welcome_tour_prefs.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 "ash/user_education/welcome_tour/welcome_tour_prefs.h"

#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "ash/user_education/user_education_types.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/welcome_tour/welcome_tour_metrics.h"
#include "base/json/values_util.h"
#include "base/strings/strcat.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"

namespace ash::welcome_tour_prefs {
namespace {

// Constants -------------------------------------------------------------------

// TODO: crbug.com/353568276 - Clean up the deprecated prefs.
// static constexpr char kTimeOfFirstInteractionPrefPrefix[] =
//     "ash.welcome_tour.interaction_time.";
// static constexpr char kTimeOfFirstTourCompletion[] =
//     "ash.welcome_tour.completed.first_time";
// static constexpr char kTimeOfFirstTourPrevention[] =
//     "ash.welcome_tour.prevented.first_time";
// static constexpr char kReasonForFirstTourPrevention[] =
//     "ash.welcome_tour.prevented.first_reason";

static constexpr char kFirstExperimentalArm[] =
    "ash.welcome_tour.v2.experimental_arm.first";
static constexpr char kTimeOfFirstInteractionPrefPrefix[] =
    "ash.welcome_tour.v2.interaction_time.";
static constexpr char kTimeOfFirstTourAborted[] =
    "ash.welcome_tour.v2.aborted.first_time";
static constexpr char kTimeOfFirstTourCompletion[] =
    "ash.welcome_tour.v2.completed.first_time";
static constexpr char kTimeOfFirstTourPrevention[] =
    "ash.welcome_tour.v2.prevented.first_time";
static constexpr char kReasonForFirstTourPrevention[] =
    "ash.welcome_tour.v2.prevented.first_reason";

// Helpers ---------------------------------------------------------------------

std::string GetTimeOfFirstInteractionPrefName(
    welcome_tour_metrics::Interaction interaction) {
  return base::StrCat({kTimeOfFirstInteractionPrefPrefix,
                       welcome_tour_metrics::ToString(interaction),
                       ".first_time"});
}

std::optional<base::Time> GetTimePrefIfSet(PrefService* prefs,
                                           const char* pref_name) {
  auto* pref = prefs->FindPreference(pref_name);
  return pref->IsDefaultValue() ? std::nullopt
                                : base::ValueToTime(pref->GetValue());
}

bool TourWasPreventedAsHoldback(PrefService* prefs) {
  return GetReasonForFirstTourPrevention(prefs) ==
         welcome_tour_metrics::PreventedReason::kHoldbackExperimentArm;
}

}  // namespace

// Utilities -------------------------------------------------------------------

std::optional<welcome_tour_metrics::ExperimentalArm> GetFirstExperimentalArm(
    PrefService* prefs) {
  CHECK(features::IsWelcomeTourEnabled());

  auto* pref = prefs->FindPreference(kFirstExperimentalArm);
  if (!pref || pref->IsDefaultValue() || !pref->GetValue()->is_int()) {
    return std::nullopt;
  }

  auto arm = static_cast<welcome_tour_metrics::ExperimentalArm>(
      pref->GetValue()->GetInt());
  if (welcome_tour_metrics::kAllExperimentalArmsSet.Has(arm)) {
    return arm;
  }

  return std::nullopt;
}

std::optional<base::Time> GetTimeOfFirstInteraction(
    PrefService* prefs,
    welcome_tour_metrics::Interaction interaction) {
  auto pref_name = GetTimeOfFirstInteractionPrefName(interaction);
  auto* pref = prefs->FindPreference(pref_name);
  return pref->IsDefaultValue() ? std::nullopt
                                : base::ValueToTime(pref->GetValue());
}

std::optional<base::Time> GetTimeOfFirstTourAborted(PrefService* prefs) {
  CHECK(features::IsWelcomeTourEnabled());
  return GetTimePrefIfSet(prefs, kTimeOfFirstTourAborted);
}

std::optional<base::Time> GetTimeOfFirstTourAttempt(PrefService* prefs) {
  std::optional<base::Time> first_aborted = GetTimeOfFirstTourAborted(prefs);
  std::optional<base::Time> first_completion =
      GetTimeOfFirstTourCompletion(prefs);
  std::optional<base::Time> first_prevention =
      TourWasPreventedAsHoldback(prefs) ? GetTimeOfFirstTourPrevention(prefs)
                                        : std::nullopt;

  if (!first_aborted && !first_completion && !first_prevention) {
    return std::nullopt;
  }

  return std::min(first_aborted.value_or(base::Time::Max()),
                  std::min(first_completion.value_or(base::Time::Max()),
                           first_prevention.value_or(base::Time::Max())));
}

std::optional<base::Time> GetTimeOfFirstTourCompletion(PrefService* prefs) {
  CHECK(features::IsWelcomeTourEnabled());
  return GetTimePrefIfSet(prefs, kTimeOfFirstTourCompletion);
}

std::optional<base::Time> GetTimeOfFirstTourPrevention(PrefService* prefs) {
  CHECK(features::IsWelcomeTourEnabled());
  return GetTimePrefIfSet(prefs, kTimeOfFirstTourPrevention);
}

std::optional<welcome_tour_metrics::PreventedReason>
GetReasonForFirstTourPrevention(PrefService* prefs) {
  using welcome_tour_metrics::PreventedReason;

  CHECK(features::IsWelcomeTourEnabled());

  auto* pref = prefs->FindPreference(kReasonForFirstTourPrevention);
  if (!pref || pref->IsDefaultValue() || !pref->GetValue()->is_int()) {
    return std::nullopt;
  }

  auto reason = static_cast<PreventedReason>(pref->GetValue()->GetInt());
  if (welcome_tour_metrics::kAllPreventedReasonsSet.Has(reason)) {
    return reason;
  }

  return PreventedReason::kUnknown;
}

bool MarkFirstExperimentalArm(PrefService* prefs,
                              welcome_tour_metrics::ExperimentalArm arm) {
  CHECK(features::IsWelcomeTourEnabled());

  if (prefs->FindPreference(kFirstExperimentalArm)->IsDefaultValue()) {
    prefs->SetInteger(kFirstExperimentalArm, static_cast<int>(arm));
    return true;
  }

  return false;
}

bool MarkFirstTourPrevention(PrefService* prefs,
                             welcome_tour_metrics::PreventedReason reason) {
  CHECK(features::IsWelcomeTourEnabled());

  if (prefs->FindPreference(kTimeOfFirstTourPrevention)->IsDefaultValue()) {
    prefs->SetTime(kTimeOfFirstTourPrevention, base::Time::Now());
    prefs->SetInteger(kReasonForFirstTourPrevention, static_cast<int>(reason));
    return true;
  }

  return false;
}

bool MarkTimeOfFirstInteraction(PrefService* prefs,
                                welcome_tour_metrics::Interaction interaction) {
  CHECK(features::IsWelcomeTourEnabled());

  const auto now = base::Time::Now();
  const auto time_to_measure_from = GetTimeOfFirstTourAttempt(prefs);

  // This function should only be called if the tour has been attempted, so that
  // we always have a time to measure the delta from.
  CHECK(time_to_measure_from.has_value());

  // Set the continuous time pref.
  const auto time_pref_name = GetTimeOfFirstInteractionPrefName(interaction);
  if (prefs->FindPreference(time_pref_name)->IsDefaultValue()) {
    prefs->SetTime(time_pref_name, now);
    return true;
  }

  return false;
}

bool MarkTimeOfFirstTourAborted(PrefService* prefs) {
  CHECK(features::IsWelcomeTourEnabled());
  if (prefs->FindPreference(kTimeOfFirstTourAborted)->IsDefaultValue()) {
    prefs->SetTime(kTimeOfFirstTourAborted, base::Time::Now());
    return true;
  }
  return false;
}

bool MarkTimeOfFirstTourCompletion(PrefService* prefs) {
  CHECK(features::IsWelcomeTourEnabled());
  if (prefs->FindPreference(kTimeOfFirstTourCompletion)->IsDefaultValue()) {
    prefs->SetTime(kTimeOfFirstTourCompletion, base::Time::Now());
    return true;
  }
  return false;
}

void RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterTimePref(kTimeOfFirstTourAborted, base::Time());
  registry->RegisterTimePref(kTimeOfFirstTourCompletion, base::Time());
  registry->RegisterTimePref(kTimeOfFirstTourPrevention, base::Time());
  registry->RegisterIntegerPref(kFirstExperimentalArm, -1);
  registry->RegisterIntegerPref(kReasonForFirstTourPrevention, -1);

  for (const auto interaction : welcome_tour_metrics::kAllInteractionsSet) {
    registry->RegisterTimePref(GetTimeOfFirstInteractionPrefName(interaction),
                               base::Time());
  }
}

}  // namespace ash::welcome_tour_prefs