| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/user_education/recent_session_policy.h" |
| |
| #include <optional> |
| |
| #include "base/dcheck_is_on.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/user_education/browser_user_education_storage_service.h" |
| #include "chrome/browser/user_education/user_education_service.h" |
| |
| namespace { |
| |
| constexpr int kMaxRecords = RecentSessionTracker::kMaxRecentSessionRecords; |
| |
| // Gets midnight at the start of the next local day. |
| // Note: inaccurate if the time is *exactly* midnight, but this will happen so |
| // rarely that it's not worth worrying about. |
| base::Time GetEndOfDay(base::Time time) { |
| return (time + base::Days(1)).LocalMidnight(); |
| } |
| |
| // Counts the number of active days in `recent_sessions` going back `num_days` |
| // from `last_day`. Returns null if session data recording does not go back far |
| // enough to cover the whole span. |
| std::optional<int> CountActiveDays(const RecentSessionData& recent_sessions, |
| base::Time last_day, |
| int num_days) { |
| const base::Time end = GetEndOfDay(last_day); |
| const base::Time start = end - base::Days(num_days); |
| |
| if (recent_sessions.enabled_time > start) { |
| return std::nullopt; |
| } |
| |
| std::vector<bool> active_days(num_days, false); |
| for (const auto& start_time : recent_sessions.recent_session_start_times) { |
| if (start_time >= start && start_time < end) { |
| const size_t index = (start_time - start) / base::Days(1); |
| active_days[index] = true; |
| } |
| } |
| return std::count(active_days.begin(), active_days.end(), true); |
| } |
| |
| std::optional<int> ValueOrNull(int value) { |
| return value ? std::make_optional(value) : std::nullopt; |
| } |
| |
| } // namespace |
| |
| bool RecentSessionPolicyImpl::Constraint::ShouldSkipRecording( |
| const RecentSessionData& recent_sessions) const { |
| return false; |
| } |
| |
| bool RecentSessionPolicyImpl::DailyConstraint::ShouldSkipRecording( |
| const RecentSessionData& recent_sessions) const { |
| // Do not record if there are at least two recent sessions and the most recent |
| // session is on the same calendar day as the second-most-recent session; the |
| // session would have already been recorded on this day. |
| // |
| // It is critical that calendar day is used rather than just a 24-hour period, |
| // since if the test were simply less than 24 hours, there could be a sequence |
| // of, say, 16-hour separations between sessions and only the first one would |
| // be recorded, no matter how long the sequence lasted. |
| return recent_sessions.recent_session_start_times.size() > 1U && |
| GetEndOfDay(recent_sessions.recent_session_start_times[0]) == |
| GetEndOfDay(recent_sessions.recent_session_start_times[1]); |
| } |
| |
| std::optional<int> RecentSessionPolicyImpl::SessionCountConstraint::GetCount( |
| const RecentSessionData& recent_sessions) const { |
| const base::Time start = |
| recent_sessions.recent_session_start_times.front() - base::Days(days_); |
| if (recent_sessions.enabled_time > start) { |
| return std::nullopt; |
| } |
| int count = 0; |
| for (const auto& start_time : recent_sessions.recent_session_start_times) { |
| if (start_time >= start) { |
| ++count; |
| } |
| } |
| return count; |
| } |
| |
| std::optional<int> RecentSessionPolicyImpl::ActiveDaysConstraint::GetCount( |
| const RecentSessionData& recent_sessions) const { |
| return CountActiveDays(recent_sessions, |
| recent_sessions.recent_session_start_times.front(), |
| days_); |
| } |
| |
| std::optional<int> RecentSessionPolicyImpl::ActiveWeeksConstraint::GetCount( |
| const RecentSessionData& recent_sessions) const { |
| int count = 0; |
| base::Time counting_back_from = |
| recent_sessions.recent_session_start_times.front(); |
| for (int week = 0; week < weeks_; ++week) { |
| const auto active_days = |
| CountActiveDays(recent_sessions, counting_back_from, 7); |
| if (!active_days) { |
| return active_days; |
| } else if (*active_days >= active_days_) { |
| ++count; |
| } |
| counting_back_from -= base::Days(7); |
| } |
| return count; |
| } |
| |
| RecentSessionPolicyImpl::ConstraintInfo::ConstraintInfo() = default; |
| RecentSessionPolicyImpl::ConstraintInfo::ConstraintInfo( |
| std::unique_ptr<Constraint> constraint_, |
| std::string histogram_name_, |
| std::optional<int> histogram_max_, |
| std::optional<int> low_usage_max_) |
| : constraint(std::move(constraint_)), |
| histogram_name(std::move(histogram_name_)), |
| histogram_max(histogram_max_), |
| low_usage_max(low_usage_max_) {} |
| RecentSessionPolicyImpl::ConstraintInfo::ConstraintInfo( |
| ConstraintInfo&&) noexcept = default; |
| RecentSessionPolicyImpl::ConstraintInfo& |
| RecentSessionPolicyImpl::ConstraintInfo::operator=(ConstraintInfo&&) noexcept = |
| default; |
| RecentSessionPolicyImpl::ConstraintInfo::~ConstraintInfo() = default; |
| |
| RecentSessionPolicyImpl::RecentSessionPolicyImpl(ConstraintInfos constraints) |
| : constraints_(std::move(constraints)) { |
| CHECK(!constraints_.empty()); |
| for (const auto& constraint : constraints_) { |
| CHECK(constraint.constraint); |
| } |
| } |
| |
| RecentSessionPolicyImpl::~RecentSessionPolicyImpl() = default; |
| |
| void RecentSessionPolicyImpl::RecordRecentUsageMetrics( |
| const RecentSessionData& recent_sessions) { |
| for (const auto& constraint : constraints_) { |
| if (!constraint.histogram_name.empty() && |
| !constraint.constraint->ShouldSkipRecording(recent_sessions)) { |
| if (const auto result = |
| constraint.constraint->GetCount(recent_sessions)) { |
| base::UmaHistogramExactLinear( |
| constraint.histogram_name.c_str(), *result, |
| constraint.histogram_max.value_or(kMaxRecords)); |
| } |
| } |
| } |
| } |
| |
| bool RecentSessionPolicyImpl::ShouldEnableLowUsagePromoMode( |
| const RecentSessionData& recent_sessions) const { |
| for (const auto& constraint : constraints_) { |
| if (const auto limit = constraint.low_usage_max) { |
| const auto result = constraint.constraint->GetCount(recent_sessions); |
| if (!result || *result > *limit) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| // static |
| RecentSessionPolicyImpl::ConstraintInfos |
| RecentSessionPolicyImpl::GetDefaultConstraints() { |
| static constexpr int kShortTermDays = 7; |
| static constexpr int kLongTermWeeks = 4; |
| static constexpr int kLongTermDays = kLongTermWeeks * 7; |
| const int max_active_weeks = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "max_active_weeks", 0); |
| const int max_active_days = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "max_active_days", 0); |
| const int super_active_days = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "super_active_days", 4); |
| const int max_monthly_active_days = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "max_monthly_active_days", 2); |
| const int max_super_active_weeks = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "max_super_active_weeks", 0); |
| const int max_weekly_sessions = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "max_weekly_sessions", 0); |
| const int max_monthly_sessions = base::GetFieldTrialParamByFeatureAsInt( |
| kAllowRecentSessionTracking, "max_monthly_sessions", 0); |
| ConstraintInfos result; |
| result.emplace_back(std::make_unique<ActiveDaysConstraint>(kShortTermDays), |
| "UserEducation.Session.RecentActiveDays", kShortTermDays, |
| ValueOrNull(max_active_days)); |
| result.emplace_back(std::make_unique<ActiveDaysConstraint>(kLongTermDays), |
| "UserEducation.Session.MonthlyActiveDays", kLongTermDays, |
| ValueOrNull(max_monthly_active_days)); |
| result.emplace_back( |
| std::make_unique<ActiveWeeksConstraint>(kLongTermWeeks, 1), |
| "UserEducation.Session.RecentActiveWeeks", kLongTermWeeks, |
| ValueOrNull(max_active_weeks)); |
| result.emplace_back(std::make_unique<ActiveWeeksConstraint>( |
| kLongTermWeeks, super_active_days), |
| "UserEducation.Session.RecentSuperActiveWeeks", |
| kLongTermWeeks, ValueOrNull(max_super_active_weeks)); |
| result.emplace_back(std::make_unique<SessionCountConstraint>(kShortTermDays), |
| "UserEducation.Session.ShortTermCount", |
| kShortTermDays + 1, ValueOrNull(max_weekly_sessions)); |
| result.emplace_back(std::make_unique<SessionCountConstraint>(kLongTermDays), |
| "UserEducation.Session.LongTermCount", kMaxRecords, |
| ValueOrNull(max_monthly_sessions)); |
| return result; |
| } |