// Copyright 2019 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/ash/policy/status_collector/activity_storage.h"
#include <algorithm>
#include <limits>
#include <memory>
#include "base/base64.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
namespace policy {
// Activity periods are keyed with day and activity key in format:
// '<day_timestamp>:<BASE64 encoded activity key>'
constexpr char kActivityKeySeparator = ':';
ActivityStorage::ActivityStorage(PrefService* pref_service,
const std::string& pref_name,
base::TimeDelta day_start_offset)
: pref_service_(pref_service),
pref_name_(pref_name),
day_start_offset_(day_start_offset) {
DCHECK(pref_service_);
const PrefService::PrefInitializationStatus pref_service_status =
pref_service_->GetInitializationStatus();
DCHECK(pref_service_status != PrefService::INITIALIZATION_STATUS_WAITING &&
pref_service_status != PrefService::INITIALIZATION_STATUS_ERROR);
}
ActivityStorage::~ActivityStorage() = default;
base::Time ActivityStorage::GetBeginningOfDay(base::Time timestamp) const {
DCHECK(!timestamp.is_max());
return timestamp.LocalMidnight() + day_start_offset_;
}
void ActivityStorage::PruneActivityPeriods(
base::Time base_time,
base::TimeDelta max_past_activity_interval,
base::TimeDelta max_future_activity_interval) {
base::Time min_time = base_time - max_past_activity_interval;
base::Time max_time = max_past_activity_interval.is_max()
? base::Time::Max()
: base_time + max_future_activity_interval;
TrimActivityPeriods(LocalTimeToUtcDayStart(min_time),
LocalTimeToUtcDayStart(max_time));
}
void ActivityStorage::TrimActivityPeriods(int64_t min_day_key,
int64_t max_day_key) {
base::Value::Dict copy;
ForEachActivityPeriodFromPref(base::BindRepeating(
[](base::Value::Dict& copy, int64_t min_day_key, int64_t max_day_key,
int64_t start, int64_t end, const std::string& activity_id) {
int64_t day_key = start;
// Remove data that is too old, or too far in the future.
if (start >= max_day_key || end <= min_day_key)
return;
// Trim data that crosses beginning threshold.
start = std::max(start, min_day_key);
// Trim data that crosses ending threshold.
end = std::min(end, max_day_key);
// Skip intervals where there was no activity.
int64_t duration = end - start;
if (duration <= 0)
return;
const std::string key = MakeActivityPeriodPrefKey(day_key, activity_id);
copy.SetByDottedPath(key, base::saturated_cast<int>(duration));
},
std::ref(copy), min_day_key, max_day_key));
// Flush the activities into pref_service_
pref_service_->SetDict(pref_name_, std::move(copy));
}
void ActivityStorage::RemoveOverlappingActivityPeriods() {
std::map<int64_t, base::TimeDelta> day_capacities;
std::map<std::string, ActivityStorage::Activities> periods_by_activity_id;
ForEachActivityPeriodFromPref(base::BindRepeating(
[](std::map<std::string, ActivityStorage::Activities>*
periods_by_activity_id,
std::map<int64_t, base::TimeDelta>* day_capacities,
const int64_t start, const int64_t end,
const std::string& activity_id) {
if (day_capacities->count(start) == 0)
day_capacities->emplace(start, base::Days(1));
if (day_capacities->at(start).is_zero())
return;
base::TimeDelta duration = std::min(base::Milliseconds(end - start),
day_capacities->at(start));
day_capacities->at(start) -= duration;
enterprise_management::TimePeriod period;
period.set_start_timestamp(start);
period.set_end_timestamp(start + duration.InMilliseconds());
if (periods_by_activity_id->count(activity_id) == 0) {
Activities activities;
periods_by_activity_id->emplace(activity_id, activities);
}
Activities& activities = periods_by_activity_id->at(activity_id);
activities.push_back(period);
},
&periods_by_activity_id, &day_capacities));
SetActivityPeriods(periods_by_activity_id);
}
const ActivityStorage::Activities ActivityStorage::GetActivityPeriodsWithNoId(
base::Time end_time) const {
const auto& activity_periods = GetActivityPeriods(end_time);
std::string no_id;
if (activity_periods.count(no_id)) {
return activity_periods.at(no_id);
} else {
return {};
}
}
const std::map<std::string, ActivityStorage::Activities>
ActivityStorage::GetActivityPeriods(base::Time end_time) const {
int64_t day_key = LocalTimeToUtcDayStart(end_time);
std::map<std::string, ActivityStorage::Activities> periods_by_activity_id;
ForEachActivityPeriodFromPref(base::BindRepeating(
[](std::map<std::string, ActivityStorage::Activities>*
periods_by_activity_id,
int64_t day_key, const int64_t start, const int64_t end,
const std::string& activity_id) {
if (end > day_key) {
return;
}
enterprise_management::TimePeriod period;
period.set_start_timestamp(start);
period.set_end_timestamp(end);
if (periods_by_activity_id->count(activity_id) == 0) {
Activities activities;
periods_by_activity_id->emplace(activity_id, activities);
}
Activities& activities = periods_by_activity_id->at(activity_id);
activities.push_back(period);
},
&periods_by_activity_id, day_key));
return periods_by_activity_id;
}
void ActivityStorage::AddActivityPeriod(base::Time start,
base::Time end,
const std::string& activity_id) {
DCHECK(start <= end);
DCHECK(!start.is_max());
DCHECK(!end.is_max());
ScopedDictPrefUpdate update(pref_service_, pref_name_);
base::Value::Dict& activity_times = update.Get();
// Assign the period to day buckets in local time.
base::Time midnight = GetBeginningOfDay(start);
while (midnight < end) {
midnight += base::Days(1);
int64_t activity = (std::min(end, midnight) - start).InMilliseconds();
const int64_t day_key = LocalTimeToUtcDayStart(start);
const std::string key = MakeActivityPeriodPrefKey(day_key, activity_id);
VLOG(1) << "Add Activity: "
<< base::Time::FromMillisecondsSinceUnixEpoch(day_key) << " to "
<< base::Time::FromMillisecondsSinceUnixEpoch(day_key + activity);
const auto previous_activity = activity_times.FindIntByDottedPath(key);
if (previous_activity.has_value()) {
activity += previous_activity.value();
}
activity_times.Set(key, static_cast<int>(activity));
start = midnight;
}
}
void ActivityStorage::SetActivityPeriods(
const std::map<std::string, Activities>& new_activity_periods) {
base::Value::Dict copy;
for (const auto& activity_pair : new_activity_periods) {
const std::string& activity_id = activity_pair.first;
const Activities& activities = activity_pair.second;
for (const auto& activity : activities) {
const std::string& key =
MakeActivityPeriodPrefKey(activity.start_timestamp(), activity_id);
copy.Set(key, base::saturated_cast<int>(activity.end_timestamp() -
activity.start_timestamp()));
}
}
pref_service_->SetDict(pref_name_, std::move(copy));
}
int64_t ActivityStorage::LocalTimeToUtcDayStart(base::Time timestamp) const {
if (timestamp.is_max()) {
// If timestamp is base::Time::Max(), trying to calculate day start
// is not needed, just keep it as is. timestamp like this cannot be part
// of an actual activity interval, it only happens as a threshold for
// activities report.
return timestamp.InMillisecondsSinceUnixEpoch();
}
base::Time::Exploded exploded;
base::Time day_start = GetBeginningOfDay(timestamp);
// TODO(crbug.com/40569404): directly test this time change. Currently it is
// tested through ScreenTimeControllerBrowsertest.
if (timestamp < day_start)
day_start -= base::Days(1);
day_start.LocalExplode(&exploded);
base::Time out_time;
bool conversion_success = base::Time::FromUTCExploded(exploded, &out_time);
DCHECK(conversion_success);
return out_time.InMillisecondsSinceUnixEpoch();
}
// static
std::string ActivityStorage::MakeActivityPeriodPrefKey(
int64_t start,
const std::string& activity_id) {
const std::string day_key = base::NumberToString(start);
if (activity_id.empty())
return day_key;
return day_key + kActivityKeySeparator + base::Base64Encode(activity_id);
}
// static
bool ActivityStorage::ParseActivityPeriodPrefKey(const std::string& key,
int64_t* start_timestamp,
std::string* activity_id) {
auto separator_pos = key.find(kActivityKeySeparator);
if (separator_pos == std::string::npos) {
activity_id->clear();
return base::StringToInt64(key, start_timestamp);
}
return base::StringToInt64(key.substr(0, separator_pos), start_timestamp) &&
base::Base64Decode(key.substr(separator_pos + 1), activity_id);
}
void ActivityStorage::ForEachActivityPeriodFromPref(
const base::RepeatingCallback<
void(const int64_t, const int64_t, const std::string&)>& f) const {
const base::Value::Dict& stored_activity_periods =
pref_service_->GetDict(pref_name_);
for (const auto item : stored_activity_periods) {
int64_t timestamp;
std::string activity_id;
if (!ParseActivityPeriodPrefKey(item.first, ×tamp, &activity_id)) {
LOG(WARNING) << "Cannot parse recorded activity key: '" << item.first
<< "'";
continue;
}
if (!item.second.is_int()) {
LOG(WARNING) << "Cannot parse recorded activity duration: '"
<< item.second << "'";
continue;
}
if (item.second.GetInt() > 0) {
f.Run(timestamp, timestamp + item.second.GetInt(), activity_id);
}
}
}
} // namespace policy