// 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.
#import "ios/chrome/browser/enterprise/model/idle/idle_service.h"
#import <UIKit/UIKit.h>
#import "base/check_is_test.h"
#import "components/enterprise/idle/idle_pref_names.h"
#import "components/enterprise/idle/metrics.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
namespace enterprise_idle {
IdleService::IdleService(ChromeBrowserState* browser_state)
: browser_state_(browser_state),
action_runner_(std::make_unique<ActionRunnerImpl>(browser_state_)) {
pref_change_registrar_.Init(browser_state_->GetPrefs());
pref_change_registrar_.Add(
enterprise_idle::prefs::kIdleTimeout,
base::BindRepeating(&IdleService::OnIdleTimeoutPrefChanged,
base::Unretained(this)));
}
IdleService::~IdleService() = default;
void IdleService::Shutdown() {
pref_change_registrar_.RemoveAll();
action_runner_.reset();
}
void IdleService::AddObserver(Observer* observer) {
observer_list_.AddObserver(observer);
}
void IdleService::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
base::TimeDelta IdleService::GetTimeout() const {
return browser_state_->GetPrefs()->GetTimeDelta(
enterprise_idle::prefs::kIdleTimeout);
}
void IdleService::OnApplicationWillEnterForeground() {
base::TimeDelta idle_threshold = GetTimeout();
base::Time last_active_time = GetLastActiveTime();
// Do nothing when the policy is unset.
if (!idle_threshold.is_positive()) {
return;
}
// This case will happen the first time the browser runs with the
// `LastActiveTimestamp` pref or if the policy is not set.
if (last_active_time == base::Time()) {
PostCheckIdleTask(idle_threshold);
} else if (IsIdleAfterPreviouslyBeingActive()) {
// Check `IsIdleAfterPreviouslyBeingActive` for more details about this
// case.
MaybeRunActionsForState(LastState::kIdleOnBackground);
} else {
// The browser's last state was:
// 1. active less than `idle_threshold` minutes ago.
// 2. idle and the actions were already run when it was idle.
// Restart the timer as foregrounding is considered new user activity.
PostCheckIdleTask(idle_threshold);
}
SetLastActiveTime();
}
void IdleService::OnApplicationWillEnterBackground() {
if (!IsIdleTimeoutPolicySet()) {
// Do nothing if the policy is not set.
return;
}
// Relying on `OnApplicationWillEnterForeground` to reset the callback
// is not reliable. The old tasks remain leading to unpredictable scheduling
// behaviour.
for (auto& observer : observer_list_) {
observer.OnApplicationWillEnterBackground();
}
cancelable_actions_callback_.Cancel();
}
void IdleService::OnIdleTimeoutPrefChanged() {
if (GetTimeout().is_positive()) {
CheckIfIdle();
} else {
// Cancel any outstanding callback if idle timeout is no longer valid.
cancelable_actions_callback_.Cancel();
}
}
base::TimeDelta IdleService::GetPossibleTimeToIdle() {
base::TimeDelta time_to_idle =
GetLastActiveTime() - base::Time::Now() + GetTimeout();
return time_to_idle.is_positive() ? time_to_idle : GetTimeout();
}
void IdleService::PostCheckIdleTask(base::TimeDelta time_from_now) {
// No tasks should be scheduled when the policy is not set.
CHECK(GetTimeout().is_positive());
cancelable_actions_callback_.Reset(
base::BindOnce(&IdleService::CheckIfIdle, weak_factory_.GetWeakPtr()));
// Post task to check idle state when it can potentially happen.
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, cancelable_actions_callback_.callback(), time_from_now);
}
void IdleService::CheckIfIdle() {
if (IsIdleAfterPreviouslyBeingActive()) {
MaybeRunActionsForState(LastState::kIdleOnForeground);
return;
}
PostCheckIdleTask(GetPossibleTimeToIdle());
}
bool IdleService::IsIdleAfterPreviouslyBeingActive() {
base::TimeDelta idle_threshold = GetTimeout();
base::Time last_active_time = GetLastActiveTime();
base::Time last_idle_time = browser_state_->GetPrefs()->GetTime(
enterprise_idle::prefs::kLastIdleTimestamp);
// Return false when the policy is not set.
if (!idle_threshold.is_positive()) {
return false;
}
// There are two cases we want to run the actions:
// 1. If the browser has never been idle, the last idle timestamp will be
// empty, so just check the last active time.
// 2. If the browser has been idle at some point before, then became active,
// and the actions have not run since the last time the browser was active.
// The goal of #2 is to avoid running the actions every `idle_threshold`
// minutes if nothing changed in the idle state; i.e. it can be idle now but
// it may have been idle for a long time with no activity. The conditions are
// separated for readability.
bool is_idle_for_first_time =
last_idle_time == base::Time() &&
(base::Time::Now() - last_active_time) >= idle_threshold;
bool is_idle_after_being_active =
last_idle_time != base::Time() &&
last_idle_time < (last_active_time + idle_threshold) &&
(base::Time::Now() - last_active_time) >= idle_threshold;
return is_idle_for_first_time || is_idle_after_being_active;
}
bool IdleService::IsIdleTimeoutPolicySet() {
return GetTimeout().is_positive();
}
void IdleService::RunActionsForStateForTesting(LastState last_state) {
CHECK_IS_TEST();
MaybeRunActionsForState(last_state);
}
void IdleService::MaybeRunActionsForState(LastState last_state) {
last_action_set_ = GetActionSet(
browser_state_->GetPrefs(),
AuthenticationServiceFactory::GetForBrowserState(browser_state_));
if (!IsAnyActionNeededToRun()) {
PostCheckIdleTask(GetTimeout());
return;
}
if (last_state == LastState::kIdleOnBackground) {
metrics::RecordIdleTimeoutCase(metrics::IdleTimeoutCase::kBackground);
for (auto& observer : observer_list_) {
// Show loading UI on re-foreground right away if data will be cleared.
observer.OnIdleTimeoutOnStartup();
}
RunActions();
} else {
metrics::RecordIdleTimeoutCase(metrics::IdleTimeoutCase::kForeground);
idle_timeout_dialog_pending_ = !observer_list_.empty();
idle_trigger_time_ = base::Time::Now();
for (auto& observer : observer_list_) {
// Confirm that the user is not active by showing dialog before running
// actions.
observer.OnIdleTimeoutInForeground();
}
}
PostCheckIdleTask(GetTimeout());
}
void IdleService::RunActions() {
action_runner_->Run(base::BindOnce(&IdleService::OnActionsCompleted,
weak_factory_.GetWeakPtr()));
}
bool IdleService::IsAnyActionNeededToRun() {
// Returns true if any action will run. The return can be false if
// 1. the only idle timeout action that is set is signout, but
// the user is not currently signed in.
// 2. the actions set are not known (empty IdleTimeoutActions pref).
return last_action_set_.clear || last_action_set_.close ||
last_action_set_.signout;
}
void IdleService::SetLastActiveTime() {
GetApplicationContext()->GetLocalState()->SetTime(
enterprise_idle::prefs::kLastActiveTimestamp, base::Time::Now());
}
base::Time IdleService::GetLastActiveTime() {
return GetApplicationContext()->GetLocalState()->GetTime(
enterprise_idle::prefs::kLastActiveTimestamp);
}
void IdleService::OnActionsCompleted() {
idle_timeout_snackbar_pending_ = true;
browser_state_->GetPrefs()->SetTime(
enterprise_idle::prefs::kLastIdleTimestamp, base::Time::Now());
for (auto& observer : observer_list_) {
observer.OnIdleTimeoutActionsCompleted();
}
}
base::Time IdleService::GetIdleTriggerTime() {
return idle_trigger_time_;
}
ActionSet IdleService::GetLastActionSet() {
return last_action_set_;
}
void IdleService::OnIdleTimeoutDialogPresented() {
idle_timeout_dialog_pending_ = false;
}
bool IdleService::ShouldIdleTimeoutDialogBePresented() {
return idle_timeout_dialog_pending_;
}
void IdleService::OnIdleTimeoutSnackbarPresented() {
idle_timeout_snackbar_pending_ = false;
}
bool IdleService::ShouldIdleTimeoutSnackbarBePresented() {
return idle_timeout_snackbar_pending_;
}
void IdleService::SetActionRunnerForTesting(
std::unique_ptr<ActionRunner> action_runner) {
CHECK_IS_TEST();
action_runner_ = std::move(action_runner);
}
ActionRunner* IdleService::GetActionRunnerForTesting() {
CHECK_IS_TEST();
return action_runner_.get();
}
} // namespace enterprise_idle