chromium/chrome/browser/ash/child_accounts/screen_time_controller.cc

// Copyright 2018 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/screen_time_controller.h"

#include <algorithm>
#include <string>
#include <utility>

#include "ash/public/cpp/child_accounts/parent_access_controller.h"
#include "ash/public/cpp/login_screen.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/ash/child_accounts/child_status_reporting_service.h"
#include "chrome/browser/ash/child_accounts/child_status_reporting_service_factory.h"
#include "chrome/browser/ash/child_accounts/time_limit_override.h"
#include "chrome/browser/ash/login/lock/screen_locker.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "content/public/browser/browser_context.h"

namespace ash {

namespace {

constexpr base::TimeDelta kUsageTimeLimitWarningTime = base::Minutes(15);

// Dictionary keys for prefs::kScreenTimeLastState.
constexpr char kScreenStateLocked[] = "locked";
constexpr char kScreenStateCurrentPolicyType[] = "active_policy";
constexpr char kScreenStateTimeUsageLimitEnabled[] = "time_usage_limit_enabled";
constexpr char kScreenStateRemainingUsage[] = "remaining_usage";
constexpr char kScreenStateUsageLimitStarted[] = "usage_limit_started";
constexpr char kScreenStateNextStateChangeTime[] = "next_state_change_time";
constexpr char kScreenStateNextPolicyType[] = "next_active_policy";
constexpr char kScreenStateNextUnlockTime[] = "next_unlock_time";

AuthDisabledReason ConvertLockReason(
    usage_time_limit::PolicyType active_policy) {
  switch (active_policy) {
    case usage_time_limit::PolicyType::kFixedLimit:
      return AuthDisabledReason::kTimeWindowLimit;
    case usage_time_limit::PolicyType::kUsageLimit:
      return AuthDisabledReason::kTimeUsageLimit;
    case usage_time_limit::PolicyType::kOverride:
      return AuthDisabledReason::kTimeLimitOverride;
    case usage_time_limit::PolicyType::kNoPolicy:
      break;
  }
  NOTREACHED_IN_MIGRATION();
  return AuthDisabledReason();
}

}  // namespace

// static
void ScreenTimeController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(prefs::kScreenTimeLastState);
  registry->RegisterDictionaryPref(prefs::kTimeLimitLocalOverride);
  registry->RegisterDictionaryPref(prefs::kUsageTimeLimit);
}

ScreenTimeController::ScreenTimeController(content::BrowserContext* context)
    : context_(context),
      pref_service_(Profile::FromBrowserContext(context)->GetPrefs()),
      clock_(base::DefaultClock::GetInstance()),
      next_state_timer_(std::make_unique<base::OneShotTimer>()),
      usage_time_limit_warning_timer_(std::make_unique<base::OneShotTimer>()),
      last_policy_(pref_service_->GetDict(prefs::kUsageTimeLimit).Clone()),
      time_limit_notifier_(context) {
  session_manager::SessionManager::Get()->AddObserver(this);
  UsageTimeStateNotifier::GetInstance()->AddObserver(this);

  system::TimezoneSettings::GetInstance()->AddObserver(this);
  SystemClockClient::Get()->AddObserver(this);
  pref_change_registrar_.Init(pref_service_);
  pref_change_registrar_.Add(
      prefs::kUsageTimeLimit,
      base::BindRepeating(&ScreenTimeController::OnPolicyChanged,
                          base::Unretained(this)));

  parent_access::ParentAccessService::Get().AddObserver(this);
}

ScreenTimeController::~ScreenTimeController() {
  parent_access::ParentAccessService::Get().RemoveObserver(this);

  session_manager::SessionManager::Get()->RemoveObserver(this);
  UsageTimeStateNotifier::GetInstance()->RemoveObserver(this);

  system::TimezoneSettings::GetInstance()->RemoveObserver(this);
  SystemClockClient::Get()->RemoveObserver(this);
}

void ScreenTimeController::AddObserver(Observer* observer) {
  DCHECK(observer);
  observers_.AddObserver(observer);
}

void ScreenTimeController::RemoveObserver(Observer* observer) {
  DCHECK(observer);
  observers_.RemoveObserver(observer);
}

base::TimeDelta ScreenTimeController::GetScreenTimeDuration() {
  return ChildStatusReportingServiceFactory::GetForBrowserContext(context_)
      ->GetChildScreenTime();
}

void ScreenTimeController::SetClocksForTesting(
    const base::Clock* clock,
    const base::TickClock* tick_clock,
    scoped_refptr<base::SequencedTaskRunner> task_runner) {
  clock_ = clock;
  next_state_timer_ = std::make_unique<base::OneShotTimer>(tick_clock);
  next_state_timer_->SetTaskRunner(task_runner);
  usage_time_limit_warning_timer_ =
      std::make_unique<base::OneShotTimer>(tick_clock);
  usage_time_limit_warning_timer_->SetTaskRunner(task_runner);
}

void ScreenTimeController::NotifyUsageTimeLimitWarningForTesting() {
  for (Observer& observer : observers_)
    observer.UsageTimeLimitWarning();
}

void ScreenTimeController::CheckTimeLimit(const std::string& source) {
  VLOG(1) << "Checking time limits (source=" << source << ")";

  // Stop all timers. They will be rescheduled below.
  ResetStateTimers();
  ResetInSessionTimers();
  ResetWarningTimers();

  base::Time now = clock_->Now();
  const icu::TimeZone& time_zone =
      system::TimezoneSettings::GetInstance()->GetTimezone();
  std::optional<usage_time_limit::State> last_state = GetLastStateFromPref();
  const base::Value::Dict& time_limit =
      pref_service_->GetDict(prefs::kUsageTimeLimit);
  const base::Value::Dict& local_override =
      pref_service_->GetDict(prefs::kTimeLimitLocalOverride);

  // TODO(agawronska): Usage timestamp should be passed instead of second |now|.
  usage_time_limit::State state = usage_time_limit::GetState(
      time_limit, &local_override, GetScreenTimeDuration(), now, now,
      &time_zone, last_state);
  SaveCurrentStateToPref(state);

  VLOG(1) << "Screen should be locked is set to " << state.is_locked;

  if (state.is_locked) {
    OnScreenLockByPolicy(state.active_policy, state.next_unlock_time);
    DCHECK(!state.next_unlock_time.is_null());
    if (!session_manager::SessionManager::Get()->IsScreenLocked())
      ForceScreenLockByPolicy();
  } else {
    OnScreenLockByPolicyEnd();
    std::optional<TimeLimitNotifier::LimitType> notification_type =
        ConvertPolicyType(state.next_state_active_policy);
    if (notification_type.has_value()) {
      // Schedule notification based on the remaining screen time until lock.
      // TODO(crbug.com/41422189): Dismiss a shown notification when it no
      // longer applies.
      const base::TimeDelta remaining_time = state.next_state_change_time - now;
      time_limit_notifier_.MaybeScheduleLockNotifications(
          notification_type.value(), remaining_time);
    }

    ScheduleUsageTimeLimitWarning(state);
  }

  // Trigger policy update notifications.
  auto updated_policy_types =
      usage_time_limit::UpdatedPolicyTypes(last_policy_, time_limit);
  for (const auto& policy_type : updated_policy_types) {
    std::optional<base::Time> lock_time;
    if (policy_type == usage_time_limit::PolicyType::kOverride)
      lock_time = state.next_state_change_time;

    std::optional<TimeLimitNotifier::LimitType> notification_type =
        ConvertPolicyType(policy_type);
    DCHECK(notification_type);
    time_limit_notifier_.ShowPolicyUpdateNotification(notification_type.value(),
                                                      lock_time);
  }
  last_policy_ = time_limit.Clone();

  // TODO(agawronska): We are creating UsageTimeLimitProcessor second time in
  // this method. Could expected reset time be returned as a part of the state?
  base::Time next_get_state_time =
      std::min(state.next_state_change_time,
               usage_time_limit::GetExpectedResetTime(
                   time_limit, &local_override, now, &time_zone));
  if (!next_get_state_time.is_null()) {
    VLOG(1) << "Scheduling state change timer in " << next_get_state_time - now;
    next_state_timer_->Start(
        FROM_HERE, next_get_state_time - now,
        base::BindOnce(&ScreenTimeController::CheckTimeLimit,
                       base::Unretained(this), "next_state_timer_"));
  }
}

void ScreenTimeController::ForceScreenLockByPolicy() {
  DCHECK(!session_manager::SessionManager::Get()->IsScreenLocked());

  // Avoid abrupt session restart that looks like a crash and happens when lock
  // screen is requested before sign in completion. It is safe, because time
  // limits will be reevaluated when session state changes to active.
  // TODO(agawronska): Remove the flag when it is confirmed that this does not
  // cause a bug (https://crbug.com/924844).
  if (base::FeatureList::IsEnabled(features::kDMServerOAuthForChildUser) &&
      session_manager::SessionManager::Get()->session_state() !=
          session_manager::SessionState::ACTIVE) {
    return;
  }

  SessionManagerClient::Get()->RequestLockScreen();
}

void ScreenTimeController::OnAccessCodeValidation(
    ParentCodeValidationResult result,
    std::optional<AccountId> account_id) {
  AccountId current_user_id =
      ProfileHelper::Get()
          ->GetUserByProfile(Profile::FromBrowserContext(context_))
          ->GetAccountId();
  if (result != ParentCodeValidationResult::kValid || !account_id ||
      account_id.value() != current_user_id)
    return;

  if (!session_manager::SessionManager::Get()->IsScreenLocked())
    return;

  usage_time_limit::TimeLimitOverride local_override(
      usage_time_limit::TimeLimitOverride::Action::kUnlock, clock_->Now(),
      std::nullopt);
  // Replace previous local override stored in pref, because PAC can only be
  // entered if previous override is not active anymore.
  pref_service_->SetDict(prefs::kTimeLimitLocalOverride,
                         local_override.ToDictionary());
  pref_service_->CommitPendingWrite();

  CheckTimeLimit("OnAccessCodeValidation");
}

void ScreenTimeController::OnScreenLockByPolicy(
    usage_time_limit::PolicyType active_policy,
    base::Time next_unlock_time) {
  if (!session_manager::SessionManager::Get()->IsScreenLocked())
    return;

  // Show lock message.
  AccountId account_id =
      ProfileHelper::Get()
          ->GetUserByProfile(Profile::FromBrowserContext(context_))
          ->GetAccountId();
  ScreenLocker::default_screen_locker()->TemporarilyDisableAuthForUser(
      account_id, AuthDisabledData(ConvertLockReason(active_policy),
                                   next_unlock_time, GetScreenTimeDuration(),
                                   true /*disable_lock_screen_media*/));

  // Add parent access code button.
  // TODO(agawronska): Move showing shelf button to ash.
  LoginScreen::Get()->ShowParentAccessButton(true);
}

void ScreenTimeController::OnScreenLockByPolicyEnd() {
  if (!session_manager::SessionManager::Get()->IsScreenLocked())
    return;

  AccountId account_id =
      ProfileHelper::Get()
          ->GetUserByProfile(Profile::FromBrowserContext(context_))
          ->GetAccountId();
  ScreenLocker::default_screen_locker()->ReenableAuthForUser(account_id);

  // TODO(agawronska): Move showing shelf button to ash.
  LoginScreen::Get()->ShowParentAccessButton(false);
}

void ScreenTimeController::OnPolicyChanged() {
  CheckTimeLimit("OnPolicyChanged");
}

void ScreenTimeController::ResetStateTimers() {
  VLOG(1) << "Stopping state timers";
  next_state_timer_->Stop();
}

void ScreenTimeController::ResetWarningTimers() {
  VLOG(1) << "Stopping warning timers";
  usage_time_limit_warning_timer_->Stop();
}

void ScreenTimeController::ResetInSessionTimers() {
  VLOG(1) << "Stopping in-session timers";
  time_limit_notifier_.UnscheduleNotifications();
}

void ScreenTimeController::ScheduleUsageTimeLimitWarning(
    const usage_time_limit::State& state) {
  if (state.next_state_active_policy ==
          usage_time_limit::PolicyType::kUsageLimit &&
      UsageTimeStateNotifier::GetInstance()->GetState() ==
          UsageTimeStateNotifier::UsageTimeState::ACTIVE) {
    base::Time now = clock_->Now();
    base::TimeDelta time_until_next_state = state.next_state_change_time - now;
    base::TimeDelta delay =
        time_until_next_state < kUsageTimeLimitWarningTime
            ? base::TimeDelta()
            : time_until_next_state - kUsageTimeLimitWarningTime;

    VLOG(1) << "Scheduling usage time limit warning in " << delay.InMinutes()
            << " minutes.";
    usage_time_limit_warning_timer_->Start(
        FROM_HERE, delay,
        base::BindOnce(&ScreenTimeController::UsageTimeLimitWarning,
                       base::Unretained(this)));
  }
}

void ScreenTimeController::SaveCurrentStateToPref(
    const usage_time_limit::State& state) {
  base::Value::Dict state_dict;

  state_dict.Set(kScreenStateLocked, base::Value(state.is_locked));
  state_dict.Set(kScreenStateCurrentPolicyType,
                 static_cast<int>(state.active_policy));
  state_dict.Set(kScreenStateTimeUsageLimitEnabled,
                 state.is_time_usage_limit_enabled);
  state_dict.Set(
      kScreenStateRemainingUsage,
      base::checked_cast<int>(state.remaining_usage.InMilliseconds()));
  state_dict.Set(kScreenStateUsageLimitStarted,
                 state.time_usage_limit_started.InSecondsFSinceUnixEpoch());
  state_dict.Set(kScreenStateNextStateChangeTime,
                 state.next_state_change_time.InSecondsFSinceUnixEpoch());
  state_dict.Set(kScreenStateNextPolicyType,
                 static_cast<int>(state.next_state_active_policy));
  state_dict.Set(kScreenStateNextUnlockTime,
                 state.next_unlock_time.InSecondsFSinceUnixEpoch());

  pref_service_->SetDict(prefs::kScreenTimeLastState, std::move(state_dict));
  pref_service_->CommitPendingWrite();
}

std::optional<usage_time_limit::State>
ScreenTimeController::GetLastStateFromPref() {
  const base::Value::Dict& last_state =
      pref_service_->GetDict(prefs::kScreenTimeLastState);
  usage_time_limit::State result;
  if (last_state.empty())
    return std::nullopt;

  // Verify is_locked from the pref is a boolean value.
  const base::Value* is_locked = last_state.Find(kScreenStateLocked);
  if (!is_locked || !is_locked->is_bool())
    return std::nullopt;
  result.is_locked = is_locked->GetBool();

  // Verify active policy type is a value of usage_time_limit::PolicyType.
  const base::Value* active_policy =
      last_state.Find(kScreenStateCurrentPolicyType);
  // TODO(crbug.com/40567736): Add kCount in usage_time_limit::PolicyType
  // instead of checking kUsageLimit here.
  if (!active_policy || !active_policy->is_int() ||
      active_policy->GetInt() < 0 ||
      active_policy->GetInt() >
          static_cast<int>(usage_time_limit::PolicyType::kUsageLimit)) {
    return std::nullopt;
  }
  result.active_policy =
      static_cast<usage_time_limit::PolicyType>(active_policy->GetInt());

  // Verify time_usage_limit_enabled from the pref is a boolean value.
  const base::Value* time_usage_limit_enabled =
      last_state.Find(kScreenStateTimeUsageLimitEnabled);
  if (!time_usage_limit_enabled || !time_usage_limit_enabled->is_bool())
    return std::nullopt;
  result.is_time_usage_limit_enabled = time_usage_limit_enabled->GetBool();

  // Verify remaining_usage from the pref is a int value.
  const base::Value* remaining_usage =
      last_state.Find(kScreenStateRemainingUsage);
  if (!remaining_usage || !remaining_usage->is_int())
    return std::nullopt;
  result.remaining_usage = base::Milliseconds(remaining_usage->GetInt());

  // Verify time_usage_limit_started from the pref is a double value.
  const base::Value* time_usage_limit_started =
      last_state.Find(kScreenStateUsageLimitStarted);
  if (!time_usage_limit_started || !time_usage_limit_started->is_double())
    return std::nullopt;
  result.time_usage_limit_started = base::Time::FromSecondsSinceUnixEpoch(
      time_usage_limit_started->GetDouble());

  // Verify next_state_change_time from the pref is a double value.
  const base::Value* next_state_change_time =
      last_state.Find(kScreenStateNextStateChangeTime);
  if (!next_state_change_time || !next_state_change_time->is_double())
    return std::nullopt;
  result.next_state_change_time = base::Time::FromSecondsSinceUnixEpoch(
      next_state_change_time->GetDouble());

  // Verify next policy type is a value of usage_time_limit::PolicyType.
  const base::Value* next_active_policy =
      last_state.Find(kScreenStateNextPolicyType);
  if (!next_active_policy || !next_active_policy->is_int() ||
      next_active_policy->GetInt() < 0 ||
      next_active_policy->GetInt() >
          static_cast<int>(usage_time_limit::PolicyType::kUsageLimit)) {
    return std::nullopt;
  }
  result.next_state_active_policy =
      static_cast<usage_time_limit::PolicyType>(next_active_policy->GetInt());

  // Verify next_unlock_time from the pref is a double value.
  const base::Value* next_unlock_time =
      last_state.Find(kScreenStateNextUnlockTime);
  if (!next_unlock_time || !next_unlock_time->is_double())
    return std::nullopt;
  result.next_unlock_time =
      base::Time::FromSecondsSinceUnixEpoch(next_unlock_time->GetDouble());
  return result;
}

void ScreenTimeController::UsageTimeLimitWarning() {
  base::Time now = clock_->Now();
  const icu::TimeZone& time_zone =
      system::TimezoneSettings::GetInstance()->GetTimezone();
  const base::Value::Dict& time_limit =
      pref_service_->GetDict(prefs::kUsageTimeLimit);
  const base::Value::Dict& local_override =
      pref_service_->GetDict(prefs::kTimeLimitLocalOverride);

  std::optional<base::TimeDelta> remaining_usage =
      usage_time_limit::GetRemainingTimeUsage(time_limit, &local_override, now,
                                              GetScreenTimeDuration(),
                                              &time_zone);

  // Remaining time usage can be bigger than |kUsageTimeLimitWarningTime|
  // because it is counted in another class so the timers might be called with
  // some delay. We must ensure that the observers will be called when the
  // usage time is less than |kUsageTimeLimitWarningTime|.
  if (remaining_usage && remaining_usage < kUsageTimeLimitWarningTime) {
    for (Observer& observer : observers_)
      observer.UsageTimeLimitWarning();
  } else {
    CheckTimeLimit("UsageTimeLimitWarning");
  }
}

std::optional<TimeLimitNotifier::LimitType>
ScreenTimeController::ConvertPolicyType(
    usage_time_limit::PolicyType policy_type) {
  switch (policy_type) {
    case usage_time_limit::PolicyType::kFixedLimit:
      return std::make_optional(TimeLimitNotifier::LimitType::kBedTime);
    case usage_time_limit::PolicyType::kUsageLimit:
      return std::make_optional(TimeLimitNotifier::LimitType::kScreenTime);
    case usage_time_limit::PolicyType::kOverride:
      return std::make_optional(TimeLimitNotifier::LimitType::kOverride);
    case usage_time_limit::PolicyType::kNoPolicy:
      return std::nullopt;
  }
}

void ScreenTimeController::OnSessionStateChanged() {
  TRACE_EVENT0("ui", "ScreenTimeController::OnSessionStateChanged");
  session_manager::SessionState session_state =
      session_manager::SessionManager::Get()->session_state();
  std::optional<usage_time_limit::State> last_state = GetLastStateFromPref();
  if (session_state == session_manager::SessionState::LOCKED && last_state &&
      last_state->is_locked) {
    OnScreenLockByPolicy(last_state->active_policy,
                         last_state->next_unlock_time);
  }
}

void ScreenTimeController::OnUsageTimeStateChange(
    const UsageTimeStateNotifier::UsageTimeState state) {
  if (state == UsageTimeStateNotifier::UsageTimeState::INACTIVE) {
    ResetWarningTimers();
    ResetInSessionTimers();
  } else {
    CheckTimeLimit("OnUsageTimeStateChange");
  }
}

void ScreenTimeController::TimezoneChanged(const icu::TimeZone& timezone) {
  CheckTimeLimit("TimezoneChanged");
}

void ScreenTimeController::SystemClockUpdated() {
  CheckTimeLimit("SystemClockUpdated");
}

}  // namespace ash