// 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/child_accounts/time_limits/app_activity_registry.h"
#include <algorithm>
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/time/default_tick_clock.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_time_limit_utils.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_time_limits_allowlist_policy_wrapper.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_time_notification_delegate.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_time_policy_helpers.h"
#include "chrome/browser/ash/child_accounts/time_limits/persisted_app_info.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/pref_names.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/app_types.h"
namespace ash {
namespace app_time {
namespace {
constexpr base::TimeDelta kFiveMinutes = base::Minutes(5);
constexpr base::TimeDelta kOneMinute = base::Minutes(1);
constexpr base::TimeDelta kZeroMinutes = base::Minutes(0);
enterprise_management::AppActivity::AppState AppStateForReporting(
AppState state) {
switch (state) {
case AppState::kAvailable:
return enterprise_management::AppActivity::DEFAULT;
case AppState::kAlwaysAvailable:
return enterprise_management::AppActivity::ALWAYS_AVAILABLE;
case AppState::kBlocked:
return enterprise_management::AppActivity::BLOCKED;
case AppState::kLimitReached:
return enterprise_management::AppActivity::LIMIT_REACHED;
case AppState::kUninstalled:
return enterprise_management::AppActivity::UNINSTALLED;
default:
return enterprise_management::AppActivity::UNKNOWN;
}
}
} // namespace
AppActivityRegistry::TestApi::TestApi(AppActivityRegistry* registry)
: registry_(registry) {}
AppActivityRegistry::TestApi::~TestApi() = default;
const std::optional<AppLimit>& AppActivityRegistry::TestApi::GetAppLimit(
const AppId& app_id) const {
DCHECK(base::Contains(registry_->activity_registry_, app_id));
return registry_->activity_registry_.at(app_id).limit;
}
std::optional<base::TimeDelta> AppActivityRegistry::TestApi::GetTimeLeft(
const AppId& app_id) const {
return registry_->GetTimeLeftForApp(app_id);
}
void AppActivityRegistry::TestApi::SaveAppActivity() {
registry_->SaveAppActivity();
}
AppActivityRegistry::SystemNotification::SystemNotification(
std::optional<base::TimeDelta> app_time_limit,
AppNotification app_notification)
: time_limit(app_time_limit), notification(app_notification) {}
AppActivityRegistry::SystemNotification::SystemNotification(
const SystemNotification&) = default;
AppActivityRegistry::SystemNotification&
AppActivityRegistry::SystemNotification::operator=(const SystemNotification&) =
default;
AppActivityRegistry::AppDetails::AppDetails() = default;
AppActivityRegistry::AppDetails::AppDetails(const AppActivity& activity)
: activity(activity) {}
AppActivityRegistry::AppDetails::~AppDetails() = default;
void AppActivityRegistry::AppDetails::ResetTimeCheck() {
activity.set_last_notification(AppNotification::kUnknown);
if (app_limit_timer)
app_limit_timer->AbandonAndStop();
}
bool AppActivityRegistry::AppDetails::IsLimitReached() const {
if (!limit.has_value())
return false;
if (limit->restriction() != AppRestriction::kTimeLimit)
return false;
DCHECK(limit->daily_limit());
if (limit->daily_limit() > activity.RunningActiveTime())
return false;
return true;
}
bool AppActivityRegistry::AppDetails::IsLimitEqual(
const std::optional<AppLimit>& another_limit) const {
if (limit.has_value() != another_limit.has_value())
return false;
if (!limit.has_value())
return true;
if (limit->restriction() == another_limit->restriction() &&
limit->daily_limit() == another_limit->daily_limit()) {
return true;
}
return false;
}
// static
void AppActivityRegistry::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kPerAppTimeLimitsAppActivities);
registry->RegisterInt64Pref(prefs::kPerAppTimeLimitsLastSuccessfulReportTime,
0);
registry->RegisterInt64Pref(prefs::kPerAppTimeLimitsLatestLimitUpdateTime, 0);
}
AppActivityRegistry::AppActivityRegistry(
AppServiceWrapper* app_service_wrapper,
AppTimeNotificationDelegate* notification_delegate,
PrefService* pref_service)
: pref_service_(pref_service),
app_service_wrapper_(app_service_wrapper),
notification_delegate_(notification_delegate),
save_data_to_pref_service_(base::DefaultTickClock::GetInstance()) {
DCHECK(app_service_wrapper_);
DCHECK(notification_delegate_);
DCHECK(pref_service_);
if (ShouldCleanUpStoredPref())
CleanRegistry(base::Time::Now() - base::Days(30));
InitializeRegistryFromPref();
save_data_to_pref_service_.Start(FROM_HERE, base::Minutes(5), this,
&AppActivityRegistry::SaveAppActivity);
app_service_wrapper_->AddObserver(this);
}
AppActivityRegistry::~AppActivityRegistry() {
app_service_wrapper_->RemoveObserver(this);
}
void AppActivityRegistry::OnAppInstalled(const AppId& app_id) {
// App might be already present in registry, because we preserve info between
// sessions and app service does not. Make sure not to override cached state.
if (!base::Contains(activity_registry_, app_id)) {
Add(app_id);
} else {
activity_registry_.at(app_id).received_app_installed_ = true;
// First send the system notifications for the application.
SendSystemNotificationsForApp(app_id);
if (GetAppState(app_id) == AppState::kLimitReached) {
NotifyLimitReached(app_id, /* was_active */ false);
} else if (GetAppState(app_id) == AppState::kUninstalled) {
OnAppReinstalled(app_id);
}
}
}
void AppActivityRegistry::OnAppUninstalled(const AppId& app_id) {
// TODO(agawronska): Consider DCHECK instead of it. Not sure if there are
// legit cases when we might go out of sync with AppService.
if (base::Contains(activity_registry_, app_id))
SetAppState(app_id, AppState::kUninstalled);
}
void AppActivityRegistry::OnAppAvailable(const AppId& app_id) {
if (!base::Contains(activity_registry_, app_id))
return;
AppState prev_state = GetAppState(app_id);
if (prev_state == AppState::kLimitReached)
return;
// This may happen in the scenario where the application is uninstalled and
// reinstalled in the same session.
if (prev_state == AppState::kUninstalled) {
OnAppReinstalled(app_id);
}
if (IsWebAppOrExtension(app_id) && app_id != GetChromeAppId() &&
base::Contains(activity_registry_, GetChromeAppId()) &&
GetAppState(app_id) == AppState::kBlocked) {
SetAppState(app_id, GetAppState(GetChromeAppId()));
return;
}
SetAppState(app_id, AppState::kAvailable);
}
void AppActivityRegistry::OnAppBlocked(const AppId& app_id) {
if (!base::Contains(activity_registry_, app_id))
return;
if (GetAppState(app_id) == AppState::kBlocked)
return;
SetAppState(app_id, AppState::kBlocked);
}
void AppActivityRegistry::OnAppActive(const AppId& app_id,
const base::UnguessableToken& instance_id,
base::Time timestamp) {
if (!base::Contains(activity_registry_, app_id))
return;
if (app_id == GetChromeAppId())
return;
AppDetails& app_details = activity_registry_[app_id];
// We are notified that a paused app is active. Notify observers to pause it.
if (GetAppState(app_id) == AppState::kLimitReached) {
// If the instance is in |app_details.paused_instances| then
// AppActivityRegistry has already notified its observers to pause it.
// Return.
if (base::Contains(app_details.paused_instances, instance_id))
return;
app_details.paused_instances.insert(instance_id);
NotifyLimitReached(app_id, /* was_active */ true);
return;
}
if (!IsAppAvailable(app_id))
return;
std::set<base::UnguessableToken>& active_instances =
app_details.active_instances;
if (base::Contains(active_instances, instance_id))
return;
active_instances.insert(instance_id);
// No need to set app as active if there were already active instances for the
// app
if (active_instances.size() > 1)
return;
SetAppActive(app_id, timestamp);
}
void AppActivityRegistry::OnAppInactive(
const AppId& app_id,
const base::UnguessableToken& instance_id,
base::Time timestamp) {
if (!base::Contains(activity_registry_, app_id))
return;
if (app_id == GetChromeAppId())
return;
std::set<base::UnguessableToken>& active_instances =
activity_registry_[app_id].active_instances;
if (!base::Contains(active_instances, instance_id))
return;
active_instances.erase(instance_id);
if (active_instances.size() > 0)
return;
SetAppInactive(app_id, timestamp);
}
void AppActivityRegistry::OnAppDestroyed(
const AppId& app_id,
const base::UnguessableToken& instance_id,
base::Time timestamp) {
if (!base::Contains(activity_registry_, app_id))
return;
if (app_id == GetChromeAppId())
return;
AppDetails& app_details = activity_registry_.at(app_id);
if (base::Contains(app_details.paused_instances, instance_id))
app_details.paused_instances.erase(instance_id);
}
bool AppActivityRegistry::IsAppInstalled(const AppId& app_id) const {
if (base::Contains(activity_registry_, app_id))
return GetAppState(app_id) != AppState::kUninstalled;
return false;
}
bool AppActivityRegistry::IsAppAvailable(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
auto state = GetAppState(app_id);
return state == AppState::kAvailable || state == AppState::kAlwaysAvailable;
}
bool AppActivityRegistry::IsAppBlocked(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
return GetAppState(app_id) == AppState::kBlocked;
}
bool AppActivityRegistry::IsAppTimeLimitReached(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
return GetAppState(app_id) == AppState::kLimitReached;
}
bool AppActivityRegistry::IsAppActive(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
return activity_registry_.at(app_id).activity.is_active();
}
bool AppActivityRegistry::IsAllowlistedApp(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
return GetAppState(app_id) == AppState::kAlwaysAvailable;
}
void AppActivityRegistry::AddAppStateObserver(
AppActivityRegistry::AppStateObserver* observer) {
app_state_observers_.AddObserver(observer);
}
void AppActivityRegistry::RemoveAppStateObserver(
AppActivityRegistry::AppStateObserver* observer) {
app_state_observers_.RemoveObserver(observer);
}
void AppActivityRegistry::SetInstalledApps(
const std::vector<AppId>& installed_apps) {
for (const auto& app : installed_apps)
OnAppInstalled(app);
}
base::TimeDelta AppActivityRegistry::GetActiveTime(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
return activity_registry_.at(app_id).activity.RunningActiveTime();
}
const std::optional<AppLimit>& AppActivityRegistry::GetWebTimeLimit() const {
DCHECK(base::Contains(activity_registry_, GetChromeAppId()));
return activity_registry_.at(GetChromeAppId()).limit;
}
AppState AppActivityRegistry::GetAppState(const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
return activity_registry_.at(app_id).activity.app_state();
}
std::optional<base::TimeDelta> AppActivityRegistry::GetTimeLimit(
const AppId& app_id) const {
if (!base::Contains(activity_registry_, app_id))
return std::nullopt;
const std::optional<AppLimit>& limit = activity_registry_.at(app_id).limit;
if (!limit || limit->restriction() != AppRestriction::kTimeLimit)
return std::nullopt;
DCHECK(limit->daily_limit());
return limit->daily_limit();
}
void AppActivityRegistry::SetReportingEnabled(std::optional<bool> value) {
if (value.has_value())
activity_reporting_enabled_ = value.value();
}
void AppActivityRegistry::GenerateHiddenApps(
enterprise_management::ChildStatusReportRequest* report) {
const std::vector<AppId> hidden_arc_apps =
app_service_wrapper_->GetHiddenArcApps();
for (const auto& app_id : hidden_arc_apps) {
enterprise_management::App* app_info = report->add_hidden_app();
app_info->set_app_id(app_id.app_id());
app_info->set_app_type(AppTypeForReporting(app_id.app_type()));
if (app_id.app_type() == apps::AppType::kArc) {
app_info->add_additional_app_id(
app_service_wrapper_->GetAppServiceId(app_id));
}
}
}
AppActivityReportInterface::ReportParams
AppActivityRegistry::GenerateAppActivityReport(
enterprise_management::ChildStatusReportRequest* report) {
// Calling SaveAppActivity is beneficial even if this method is returning
// early due to reporting not being enabled. This is because it helps move the
// ActiveTimes information from AppActivityRegistry to the stored pref data
// which will then be cleaned in the direct CleanRegistry() call below.
SaveAppActivity();
// If app activity reporting is not enabled, simply return.
if (!activity_reporting_enabled_) {
base::Time timestamp = base::Time::Now();
CleanRegistry(timestamp);
return AppActivityReportInterface::ReportParams{timestamp, false};
}
const base::Value::List& list =
pref_service_->GetList(prefs::kPerAppTimeLimitsAppActivities);
const std::vector<PersistedAppInfo> applications_info =
PersistedAppInfo::PersistedAppInfosFromList(
list,
/* include_app_activity_array */ true);
const base::Time timestamp = base::Time::Now();
bool anything_reported = false;
for (const auto& entry : applications_info) {
const AppId& app_id = entry.app_id();
const std::vector<AppActivity::ActiveTime>& active_times =
entry.active_times();
// Do not report if there is no activity.
if (active_times.empty())
continue;
enterprise_management::AppActivity* app_activity =
report->add_app_activity();
enterprise_management::App* app_info = app_activity->mutable_app_info();
app_info->set_app_id(app_id.app_id());
app_info->set_app_type(AppTypeForReporting(app_id.app_type()));
// AppService is is only different for ARC++ apps.
if (app_id.app_type() == apps::AppType::kArc) {
app_info->add_additional_app_id(
app_service_wrapper_->GetAppServiceId(app_id));
}
app_activity->set_app_state(AppStateForReporting(entry.app_state()));
app_activity->set_populated_at(timestamp.InMillisecondsSinceUnixEpoch());
for (const auto& active_time : active_times) {
enterprise_management::TimePeriod* time_period =
app_activity->add_active_time_periods();
time_period->set_start_timestamp(
active_time.active_from().InMillisecondsSinceUnixEpoch());
time_period->set_end_timestamp(
active_time.active_to().InMillisecondsSinceUnixEpoch());
}
anything_reported = true;
}
return AppActivityReportInterface::ReportParams{timestamp, anything_reported};
}
void AppActivityRegistry::OnSuccessfullyReported(base::Time timestamp) {
CleanRegistry(timestamp);
// Update last successful report time.
pref_service_->SetInt64(
prefs::kPerAppTimeLimitsLastSuccessfulReportTime,
timestamp.ToDeltaSinceWindowsEpoch().InMicroseconds());
}
bool AppActivityRegistry::UpdateAppLimits(
const std::map<AppId, AppLimit>& app_limits) {
base::Time latest_update = latest_app_limit_update_;
bool policy_updated = false;
for (auto& entry : activity_registry_) {
const AppId& app_id = entry.first;
// Web time limits are updated when chrome's time limit is updated.
if (app_id != GetChromeAppId() && IsWebAppOrExtension(app_id))
continue;
std::optional<AppLimit> new_limit = std::nullopt;
if (base::Contains(app_limits, app_id))
new_limit = app_limits.at(app_id);
policy_updated |= SetAppLimit(app_id, new_limit);
if (new_limit && new_limit->last_updated() > latest_update)
latest_update = new_limit->last_updated();
}
latest_app_limit_update_ = latest_update;
// Update the latest app limit update.
pref_service_->SetInt64(
prefs::kPerAppTimeLimitsLatestLimitUpdateTime,
latest_app_limit_update_.ToDeltaSinceWindowsEpoch().InMicroseconds());
return policy_updated;
}
bool AppActivityRegistry::SetAppLimit(
const AppId& app_id,
const std::optional<AppLimit>& app_limit) {
DCHECK(base::Contains(activity_registry_, app_id));
// If an application is not installed but present in the registry return
// early.
if (!IsAppInstalled(app_id))
return false;
// Chrome and web apps should not be blocked.
if (app_limit && app_limit->restriction() == AppRestriction::kBlocked &&
IsWebAppOrExtension(app_id)) {
return false;
}
AppDetails& details = activity_registry_.at(app_id);
// Limit 'data' are considered equal if only the |last_updated_| is different.
// Update the limit to store new |last_updated_| value.
bool did_change = !details.IsLimitEqual(app_limit);
bool updated =
ShowLimitUpdatedNotificationIfNeeded(app_id, details.limit, app_limit);
details.limit = app_limit;
// If |did_change| is false, handle the following corner case before
// returning. The default value for app limit during construction at the
// beginning of the session is std::nullopt. If the application was paused in
// the previous session, and its limit was removed or feature is disabled in
// the current session, the |app_limit| provided will be std::nullopt. Since
// both values(the default app limit and the |app_limit| provided as an
// argument for this method) are the same std::nullopt, |did_change| will be
// false. But we still need to update the state to available as the new app
// limit is std::nullopt.
if (!did_change && (IsAppAvailable(app_id) || app_limit.has_value()))
return updated;
if (IsAllowlistedApp(app_id)) {
if (app_limit.has_value()) {
VLOG(1) << "Tried to set time limit for " << app_id
<< " which is allowlisted.";
}
details.limit = std::nullopt;
return false;
}
if (!IsWebAppOrExtension(app_id)) {
AppLimitUpdated(app_id);
return updated;
}
for (auto& entry : activity_registry_) {
const AppId& app_id_for_entry = entry.first;
AppDetails& details_for_entry = entry.second;
if (ContributesToWebTimeLimit(app_id_for_entry,
GetAppState(app_id_for_entry))) {
details_for_entry.limit = app_limit;
}
}
for (auto& entry : activity_registry_) {
const AppId& app_id_for_entry = entry.first;
if (ContributesToWebTimeLimit(app_id_for_entry,
GetAppState(app_id_for_entry))) {
AppLimitUpdated(app_id_for_entry);
}
}
return updated;
}
void AppActivityRegistry::SetAppAllowlisted(const AppId& app_id) {
if (!base::Contains(activity_registry_, app_id))
return;
SetAppState(app_id, AppState::kAlwaysAvailable);
}
void AppActivityRegistry::OnChromeAppActivityChanged(
ChromeAppActivityState state,
base::Time timestamp) {
AppId chrome_app_id = GetChromeAppId();
if (!base::Contains(activity_registry_, chrome_app_id))
return;
AppDetails& details = activity_registry_[chrome_app_id];
bool was_active = details.activity.is_active();
bool is_active = (state == ChromeAppActivityState::kActive);
// No need to notify observers that limit has reached. They will be notified
// in AppActivityRegistry::OnAppActive.
if (GetAppState(chrome_app_id) == AppState::kLimitReached && is_active)
return;
// No change in state.
if (was_active == is_active)
return;
if (is_active) {
SetAppActive(chrome_app_id, timestamp);
return;
}
SetAppInactive(chrome_app_id, timestamp);
}
void AppActivityRegistry::OnTimeLimitAllowlistChanged(
const AppTimeLimitsAllowlistPolicyWrapper& wrapper) {
std::vector<AppId> allowlisted_apps = wrapper.GetAllowlistAppList();
for (const AppId& app : allowlisted_apps) {
if (!base::Contains(activity_registry_, app))
continue;
if (GetAppState(app) == AppState::kAlwaysAvailable)
continue;
std::optional<AppLimit>& limit = activity_registry_.at(app).limit;
if (limit.has_value())
limit = std::nullopt;
SetAppState(app, AppState::kAlwaysAvailable);
}
}
void AppActivityRegistry::SaveAppActivity() {
{
ScopedListPrefUpdate update(pref_service_,
prefs::kPerAppTimeLimitsAppActivities);
base::Value::List& list = update.Get();
const base::Time now = base::Time::Now();
for (base::Value& entry : list) {
std::optional<AppId> app_id =
policy::AppIdFromAppInfoDict(entry.GetIfDict());
DCHECK(app_id.has_value());
if (!base::Contains(activity_registry_, app_id.value())) {
std::optional<AppState> state =
PersistedAppInfo::GetAppStateFromDict(entry.GetIfDict());
DCHECK(state.has_value() && state.value() == AppState::kUninstalled);
continue;
}
const PersistedAppInfo info =
GetPersistedAppInfoForApp(app_id.value(), now);
info.UpdateAppActivityPreference(entry.GetDict(), /* replace */ false);
}
for (const AppId& app_id : newly_installed_apps_) {
const PersistedAppInfo info = GetPersistedAppInfoForApp(app_id, now);
base::Value::Dict value;
info.UpdateAppActivityPreference(value, /* replace */ false);
list.Append(std::move(value));
}
newly_installed_apps_.clear();
}
// Ensure that the app activity is persisted.
pref_service_->CommitPendingWrite();
}
std::vector<AppId> AppActivityRegistry::GetAppsWithAppRestriction(
AppRestriction restriction) const {
std::vector<AppId> apps_with_limit;
for (const auto& entry : activity_registry_) {
const AppId& app = entry.first;
const AppDetails& details = entry.second;
if (details.limit && details.limit->restriction() == restriction) {
apps_with_limit.push_back(app);
}
}
return apps_with_limit;
}
void AppActivityRegistry::OnResetTimeReached(base::Time timestamp) {
for (std::pair<const AppId, AppDetails>& info : activity_registry_) {
const AppId& app = info.first;
AppDetails& details = info.second;
// Reset running active time.
details.activity.ResetRunningActiveTime(timestamp);
// If timer is running, stop timer. Abandon all tasks set.
details.ResetTimeCheck();
// If the time limit has been reached, mark the app as available.
if (details.activity.app_state() == AppState::kLimitReached)
SetAppState(app, AppState::kAvailable);
// If the application is currently active, schedule a time limit
// check.
if (details.activity.is_active())
ScheduleTimeLimitCheckForApp(app);
}
}
void AppActivityRegistry::CleanRegistry(base::Time timestamp) {
ScopedListPrefUpdate update(pref_service_,
prefs::kPerAppTimeLimitsAppActivities);
base::Value::List& list = update.Get();
for (size_t index = 0; index < list.size();) {
base::Value& entry = list[index];
std::optional<PersistedAppInfo> info =
PersistedAppInfo::PersistedAppInfoFromDict(entry.GetIfDict(), true);
DCHECK(info.has_value());
info->RemoveActiveTimeEarlierThan(timestamp);
info->UpdateAppActivityPreference(entry.GetDict(), /* replace */ true);
if (info->ShouldRemoveApp()) {
// Remove entry in |activity_registry_| if it is present.
activity_registry_.erase(info->app_id());
// To efficiently remove the entry, swap it with the last element and pop
// back.
if (index < list.size() - 1)
std::swap(list[index], list[list.size() - 1]);
list.erase(list.end() - 1);
} else {
++index;
}
}
}
void AppActivityRegistry::OnAppReinstalled(const AppId& app_id) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& details = activity_registry_.at(app_id);
if (details.IsLimitReached()) {
SetAppState(app_id, AppState::kLimitReached);
} else {
SetAppState(app_id, AppState::kAvailable);
}
// Notify observers.
for (auto& observer : app_state_observers_)
observer.OnAppInstalled(app_id);
}
void AppActivityRegistry::Add(const AppId& app_id) {
activity_registry_[app_id].activity = AppActivity(AppState::kAvailable);
activity_registry_[app_id].received_app_installed_ = true;
bool is_app_chrome = app_id == GetChromeAppId();
bool is_web = IsWebAppOrExtension(app_id);
bool is_chrome_installed =
base::Contains(activity_registry_, GetChromeAppId());
if (!is_app_chrome && is_web && is_chrome_installed) {
activity_registry_[app_id].limit = GetWebTimeLimit();
activity_registry_[app_id].activity.SetAppState(
GetAppState(GetChromeAppId()));
}
newly_installed_apps_.push_back(app_id);
for (auto& observer : app_state_observers_)
observer.OnAppInstalled(app_id);
}
void AppActivityRegistry::SetAppState(const AppId& app_id, AppState app_state) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& app_details = activity_registry_.at(app_id);
AppActivity& app_activity = app_details.activity;
AppState previous_state = app_activity.app_state();
// There was no change in state, return.
if (previous_state == app_state)
return;
app_activity.SetAppState(app_state);
if (app_activity.app_state() == AppState::kLimitReached) {
bool was_active = false;
if (app_activity.is_active()) {
was_active = true;
app_details.paused_instances = std::move(app_details.active_instances);
SetAppInactive(app_id, base::Time::Now());
}
NotifyLimitReached(app_id, was_active);
return;
}
if (previous_state == AppState::kLimitReached &&
app_activity.app_state() != AppState::kLimitReached) {
for (auto& observer : app_state_observers_)
observer.OnAppLimitRemoved(app_id);
return;
}
}
void AppActivityRegistry::NotifyLimitReached(const AppId& app_id,
bool was_active) {
DCHECK(base::Contains(activity_registry_, app_id));
DCHECK_EQ(GetAppState(app_id), AppState::kLimitReached);
const std::optional<AppLimit>& limit = activity_registry_.at(app_id).limit;
DCHECK(limit->daily_limit());
for (auto& observer : app_state_observers_) {
observer.OnAppLimitReached(app_id, limit->daily_limit().value(),
was_active);
}
}
void AppActivityRegistry::SetAppActive(const AppId& app_id,
base::Time timestamp) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& app_details = activity_registry_[app_id];
DCHECK(!app_details.activity.is_active());
if (ContributesToWebTimeLimit(app_id, GetAppState(app_id)))
app_details.activity.set_running_active_time(GetWebActiveRunningTime());
app_details.activity.SetAppActive(timestamp);
ScheduleTimeLimitCheckForApp(app_id);
}
void AppActivityRegistry::SetAppInactive(const AppId& app_id,
base::Time timestamp) {
DCHECK(base::Contains(activity_registry_, app_id));
auto& details = activity_registry_.at(app_id);
details.activity.SetAppInactive(timestamp);
details.ResetTimeCheck();
// If the application is a web app, synchronize its running active time with
// those of other inactive web apps.
if (ContributesToWebTimeLimit(app_id, GetAppState(app_id))) {
base::TimeDelta active_time = details.activity.RunningActiveTime();
for (auto& app_info : activity_registry_) {
const AppId& app_id_for_info = app_info.first;
if (!ContributesToWebTimeLimit(app_id_for_info,
GetAppState(app_id_for_info))) {
continue;
}
AppDetails& details_for_info = app_info.second;
if (!details_for_info.activity.is_active())
details_for_info.activity.set_running_active_time(active_time);
}
}
}
void AppActivityRegistry::ScheduleTimeLimitCheckForApp(const AppId& app_id) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& app_details = activity_registry_[app_id];
// If there is no time limit information, don't set the timer.
if (!app_details.limit.has_value())
return;
const AppLimit& limit = app_details.limit.value();
if (limit.restriction() != AppRestriction::kTimeLimit)
return;
if (!app_details.app_limit_timer) {
app_details.app_limit_timer = std::make_unique<base::OneShotTimer>(
base::DefaultTickClock::GetInstance());
}
DCHECK(!app_details.app_limit_timer->IsRunning());
// Check that the timer instance has been created.
std::optional<base::TimeDelta> time_limit = GetTimeLeftForApp(app_id);
DCHECK(time_limit.has_value());
if (time_limit > kFiveMinutes) {
time_limit = time_limit.value() - kFiveMinutes;
} else if (time_limit > kOneMinute) {
time_limit = time_limit.value() - kOneMinute;
} else if (time_limit == kZeroMinutes) {
// Zero minutes case could be handled by using the timer below, but we call
// it explicitly to simplify tests.
CheckTimeLimitForApp(app_id);
return;
}
VLOG(1) << "Schedule app time limit check for " << app_id << " for "
<< time_limit.value();
app_details.app_limit_timer->Start(
FROM_HERE, time_limit.value(),
base::BindOnce(&AppActivityRegistry::CheckTimeLimitForApp,
base::Unretained(this), app_id));
}
std::optional<base::TimeDelta> AppActivityRegistry::GetTimeLeftForApp(
const AppId& app_id) const {
DCHECK(base::Contains(activity_registry_, app_id));
const AppDetails& app_details = activity_registry_.at(app_id);
// If |app_details.limit| doesn't have value, the app has no restriction.
if (!app_details.limit.has_value())
return std::nullopt;
const AppLimit& limit = app_details.limit.value();
if (limit.restriction() != AppRestriction::kTimeLimit)
return std::nullopt;
// If the app has kTimeLimit restriction, DCHECK that daily limit has value.
DCHECK(limit.daily_limit().has_value());
AppState state = app_details.activity.app_state();
if (state == AppState::kAlwaysAvailable || state == AppState::kBlocked)
return std::nullopt;
if (state == AppState::kLimitReached)
return kZeroMinutes;
DCHECK(state == AppState::kAvailable);
base::TimeDelta time_limit = limit.daily_limit().value();
base::TimeDelta active_time;
if (ContributesToWebTimeLimit(app_id, GetAppState(app_id))) {
active_time = GetWebActiveRunningTime();
} else {
active_time = app_details.activity.RunningActiveTime();
}
if (active_time >= time_limit)
return kZeroMinutes;
return time_limit - active_time;
}
void AppActivityRegistry::CheckTimeLimitForApp(const AppId& app_id) {
AppDetails& details = activity_registry_[app_id];
std::optional<base::TimeDelta> time_left = GetTimeLeftForApp(app_id);
AppNotification last_notification = details.activity.last_notification();
if (!time_left.has_value())
return;
DCHECK(details.limit.has_value());
DCHECK(details.limit->daily_limit().has_value());
const base::TimeDelta time_limit = details.limit->daily_limit().value();
if (time_left <= kFiveMinutes && time_left > kOneMinute &&
last_notification != AppNotification::kFiveMinutes) {
MaybeShowSystemNotification(
app_id, SystemNotification(time_limit, AppNotification::kFiveMinutes));
ScheduleTimeLimitCheckForApp(app_id);
return;
}
if (time_left <= kOneMinute && time_left > kZeroMinutes &&
last_notification != AppNotification::kOneMinute) {
MaybeShowSystemNotification(
app_id, SystemNotification(time_limit, AppNotification::kOneMinute));
ScheduleTimeLimitCheckForApp(app_id);
return;
}
if (time_left == kZeroMinutes &&
last_notification != AppNotification::kTimeLimitReached) {
MaybeShowSystemNotification(
app_id,
SystemNotification(time_limit, AppNotification::kTimeLimitReached));
if (ContributesToWebTimeLimit(app_id, GetAppState(app_id))) {
WebTimeLimitReached(base::Time::Now());
} else {
SetAppState(app_id, AppState::kLimitReached);
}
}
}
bool AppActivityRegistry::ShowLimitUpdatedNotificationIfNeeded(
const AppId& app_id,
const std::optional<AppLimit>& old_limit,
const std::optional<AppLimit>& new_limit) {
// Web app limit changes are covered by Chrome notification.
if (app_id != GetChromeAppId() && IsWebAppOrExtension(app_id))
return false;
// Don't show notification if the time limit's update was older than the
// latest update.
if (new_limit && new_limit->last_updated() <= latest_app_limit_update_)
return false;
const bool was_blocked =
old_limit && old_limit->restriction() == AppRestriction::kBlocked;
const bool is_blocked =
new_limit && new_limit->restriction() == AppRestriction::kBlocked;
if (!was_blocked && is_blocked) {
MaybeShowSystemNotification(
app_id, SystemNotification(std::nullopt, AppNotification::kBlocked));
return true;
}
const bool had_time_limit =
old_limit && old_limit->restriction() == AppRestriction::kTimeLimit;
const bool has_time_limit =
new_limit && new_limit->restriction() == AppRestriction::kTimeLimit;
if (was_blocked && !is_blocked && !has_time_limit) {
MaybeShowSystemNotification(
app_id, SystemNotification(std::nullopt, AppNotification::kAvailable));
return true;
}
// Time limit was removed.
if (!has_time_limit && had_time_limit) {
MaybeShowSystemNotification(
app_id,
SystemNotification(std::nullopt, AppNotification::kTimeLimitChanged));
return true;
}
// Time limit was set or value changed.
if (has_time_limit && (!had_time_limit || old_limit->daily_limit() !=
new_limit->daily_limit())) {
MaybeShowSystemNotification(
app_id, SystemNotification(new_limit->daily_limit(),
AppNotification::kTimeLimitChanged));
return true;
}
return false;
}
base::TimeDelta AppActivityRegistry::GetWebActiveRunningTime() const {
base::TimeDelta active_running_time = base::Seconds(0);
for (const auto& app_info : activity_registry_) {
const AppId& app_id = app_info.first;
const AppDetails& details = app_info.second;
if (!ContributesToWebTimeLimit(app_id, GetAppState(app_id))) {
continue;
}
active_running_time = details.activity.RunningActiveTime();
// If the app is active, then it has the most up to date active running
// time.
if (details.activity.is_active())
return active_running_time;
}
return active_running_time;
}
void AppActivityRegistry::WebTimeLimitReached(base::Time timestamp) {
for (auto& app_info : activity_registry_) {
const AppId& app_id = app_info.first;
if (!ContributesToWebTimeLimit(app_id, GetAppState(app_id)))
continue;
SetAppState(app_id, AppState::kLimitReached);
}
}
void AppActivityRegistry::InitializeRegistryFromPref() {
DCHECK(pref_service_);
int64_t last_limits_updates =
pref_service_->GetInt64(prefs::kPerAppTimeLimitsLatestLimitUpdateTime);
latest_app_limit_update_ = base::Time::FromDeltaSinceWindowsEpoch(
base::Microseconds(last_limits_updates));
InitializeAppActivities();
}
void AppActivityRegistry::InitializeAppActivities() {
const base::Value::List& list =
pref_service_->GetList(prefs::kPerAppTimeLimitsAppActivities);
const std::vector<PersistedAppInfo> applications_info =
PersistedAppInfo::PersistedAppInfosFromList(
list,
/* include_app_activity_array */ false);
for (const auto& app_info : applications_info) {
DCHECK(!base::Contains(activity_registry_, app_info.app_id()));
// Don't restore uninstalled application's if its running active time is
// zero.
if (!app_info.ShouldRestoreApp())
continue;
activity_registry_[app_info.app_id()].activity =
AppActivity(app_info.app_state(), app_info.active_running_time());
}
}
PersistedAppInfo AppActivityRegistry::GetPersistedAppInfoForApp(
const AppId& app_id,
base::Time timestamp) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& details = activity_registry_.at(app_id);
base::TimeDelta running_active_time = details.activity.RunningActiveTime();
if (ContributesToWebTimeLimit(app_id, GetAppState(app_id)))
running_active_time = GetWebActiveRunningTime();
// Updates |AppActivity::active_times_| to include the current activity up to
// |timestamp|.
details.activity.CaptureOngoingActivity(timestamp);
std::vector<AppActivity::ActiveTime> activity =
details.activity.TakeActiveTimes();
// If reporting is not enabled, don't save unnecessary data.
if (!activity_reporting_enabled_)
activity.clear();
return PersistedAppInfo(app_id, details.activity.app_state(),
running_active_time, std::move(activity));
}
bool AppActivityRegistry::ShouldCleanUpStoredPref() {
int64_t last_time =
pref_service_->GetInt64(prefs::kPerAppTimeLimitsLastSuccessfulReportTime);
if (last_time == 0)
return false;
base::Time time =
base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(last_time));
return time < base::Time::Now() - base::Days(30);
}
void AppActivityRegistry::SendSystemNotificationsForApp(const AppId& app_id) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& app_details = activity_registry_.at(app_id);
DCHECK(app_details.received_app_installed_);
// TODO(yilkal): Filter out the notifications to show. For example don't show
// 5 min and 1 min left notifications at the same time here. However, time
// limit changed and 1 min left notifications can be shown at the same time.
for (const auto& elem : app_details.pending_notifications_) {
notification_delegate_->ShowAppTimeLimitNotification(
app_id, elem.time_limit, elem.notification);
}
app_details.pending_notifications_.clear();
}
void AppActivityRegistry::MaybeShowSystemNotification(
const AppId& app_id,
const SystemNotification& notification) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& app_details = activity_registry_.at(app_id);
app_details.activity.set_last_notification(notification.notification);
// AppActivityRegistry has not yet received OnAppInstalled call from
// AppService. Add notification to |AppDetails::pending_notifications_|.
if (!app_details.received_app_installed_) {
app_details.pending_notifications_.push_back(notification);
return;
}
// Otherwise, just show the notification.
notification_delegate_->ShowAppTimeLimitNotification(
app_id, notification.time_limit, notification.notification);
}
void AppActivityRegistry::AppLimitUpdated(const AppId& app_id) {
DCHECK(base::Contains(activity_registry_, app_id));
AppDetails& details = activity_registry_.at(app_id);
// Limit for the active app changed - adjust the timers.
// Handling of active app is different, because ongoing activity needs to be
// taken into account.
if (IsAppActive(app_id)) {
details.ResetTimeCheck();
ScheduleTimeLimitCheckForApp(app_id);
return;
}
// Inactive available app reached the limit - update the state.
// If app is in any other state than |kAvailable| the limit does not take
// effect.
if (IsAppAvailable(app_id) && details.IsLimitReached()) {
SetAppInactive(app_id, base::Time::Now());
SetAppState(app_id, AppState::kLimitReached);
return;
}
// Paused inactive app is below the limit again - update the state.
// This can happen if the limit was removed or new limit is greater the the
// previous one. We know that the state should be available, because app can
// only reach the limit if it is available.
if (IsAppTimeLimitReached(app_id) && !details.IsLimitReached())
SetAppState(app_id, AppState::kAvailable);
}
} // namespace app_time
} // namespace ash