chromium/chrome/browser/ash/policy/off_hours/device_off_hours_controller.cc

// Copyright 2017 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/off_hours/device_off_hours_controller.h"

#include <optional>
#include <string>
#include <tuple>
#include <utility>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/time/default_clock.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/ash/login/users/chrome_user_manager_util.h"
#include "chrome/browser/ash/policy/off_hours/off_hours_proto_parser.h"
#include "chrome/browser/ash/settings/device_settings_service.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/policy/weekly_time/time_utils.h"
#include "components/prefs/pref_value_map.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"

namespace policy::off_hours {

namespace em = ::enterprise_management;

DeviceOffHoursController::DeviceOffHoursController()
    : timer_(std::make_unique<base::WallClockTimer>()),
      clock_(base::DefaultClock::GetInstance()) {
  auto* system_clock_client = ash::SystemClockClient::Get();
  if (system_clock_client) {
    system_clock_client->AddObserver(this);
    system_clock_client->WaitForServiceToBeAvailable(
        base::BindOnce(&DeviceOffHoursController::SystemClockInitiallyAvailable,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

DeviceOffHoursController::~DeviceOffHoursController() {
  if (ash::SystemClockClient::Get())
    ash::SystemClockClient::Get()->RemoveObserver(this);
}

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

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

void DeviceOffHoursController::SetClockForTesting(
    base::Clock* clock,
    const base::TickClock* timer_clock) {
  clock_ = clock;
  timer_ = std::make_unique<base::WallClockTimer>(clock, timer_clock);
}

bool DeviceOffHoursController::IsCurrentSessionAllowedOnlyForOffHours() const {
  if (!is_off_hours_mode())
    return false;

  const user_manager::UserManager* user_manager =
      user_manager::UserManager::Get();
  const user_manager::UserList& logged_in_users =
      user_manager->GetLoggedInUsers();
  user_manager::UserList users_to_check;
  for (user_manager::User* user : logged_in_users) {
    if (user->GetType() == user_manager::UserType::kRegular ||
        user->GetType() == user_manager::UserType::kGuest ||
        user->GetType() == user_manager::UserType::kChild) {
      users_to_check.push_back(user);
    }
  }

  if (users_to_check.empty())
    return false;

  // If at least one logged in user won't be allowed after OffHours,
  // the session will be terminated.
  return !ash::chrome_user_manager_util::AreAllUsersAllowed(
      users_to_check, device_settings_proto_);
}

void DeviceOffHoursController::UpdateOffHoursPolicy(
    const em::ChromeDeviceSettingsProto& device_settings_proto) {
  device_settings_proto_ = device_settings_proto;
  std::vector<WeeklyTimeInterval> off_hours_intervals;
  if (device_settings_proto.has_device_off_hours()) {
    const em::DeviceOffHoursProto& container(
        device_settings_proto.device_off_hours());
    std::optional<std::string> timezone = ExtractTimezoneFromProto(container);
    if (timezone) {
      off_hours_intervals = weekly_time_utils::ConvertIntervalsToGmt(
          ExtractWeeklyTimeIntervalsFromProto(container, *timezone, clock_));
    }
  }
  off_hours_intervals_.swap(off_hours_intervals);
  UpdateOffHoursMode();
}

void DeviceOffHoursController::NotifyOffHoursEndTimeChanged() const {
  VLOG(1) << "OffHours end time is changed to " << off_hours_end_time_;
  for (auto& observer : observers_)
    observer.OnOffHoursEndTimeChanged();
}

void DeviceOffHoursController::OffHoursModeIsChanged() const {
  VLOG(1) << "OffHours mode is changed to " << off_hours_mode_;
  ash::DeviceSettingsService::Get()->Load();
}

void DeviceOffHoursController::UpdateOffHoursMode() {
  if (off_hours_intervals_.empty() || !is_clock_network_synchronized_) {
    if (!is_clock_network_synchronized_) {
      VLOG(1) << "The system clock isn't network synchronized. OffHours mode "
                 "is unavailable.";
    }
    SetOffHoursEndTime(base::Time{});
    StopOffHoursTimer();
    SetOffHoursMode(false);
    return;
  }

  namespace wtu = weekly_time_utils;
  const base::Time now = clock_->Now();
  const bool in_interval = wtu::Contains(now, off_hours_intervals_);
  const std::optional<base::Time> update_time =
      wtu::GetNextEventTime(now, off_hours_intervals_);

  // weekly off_hours_intervals_ is not empty -> update_time has a value
  DCHECK(update_time);

  SetOffHoursEndTime(in_interval ? update_time.value() : base::Time{});
  StartOffHoursTimer(update_time.value());
  SetOffHoursMode(in_interval);
}

void DeviceOffHoursController::SetOffHoursEndTime(
    base::Time off_hours_end_time) {
  if (off_hours_end_time == off_hours_end_time_)
    return;
  off_hours_end_time_ = off_hours_end_time;
  NotifyOffHoursEndTimeChanged();
}

void DeviceOffHoursController::SetOffHoursMode(bool off_hours_enabled) {
  if (off_hours_mode_ == off_hours_enabled)
    return;
  off_hours_mode_ = off_hours_enabled;
  DVLOG(1) << "OffHours mode: " << off_hours_mode_;
  if (!off_hours_mode_)
    SetOffHoursEndTime(base::Time());
  OffHoursModeIsChanged();
}

void DeviceOffHoursController::StartOffHoursTimer(base::Time update_time) {
  DVLOG(1) << "OffHours mode timer starts with run time " << update_time;
  timer_->Start(FROM_HERE, update_time,
                base::BindOnce(&DeviceOffHoursController::UpdateOffHoursMode,
                               weak_ptr_factory_.GetWeakPtr()));
}

void DeviceOffHoursController::StopOffHoursTimer() {
  timer_->Stop();
}

void DeviceOffHoursController::SystemClockUpdated() {
  // Triggered when the device time is changed. When it happens the "OffHours"
  // mode could be changed too, because "OffHours" mode directly depends on the
  // current device time. Ask SystemClockClient to update information about the
  // system time synchronization with the network time asynchronously.
  // Information will be received by NetworkSynchronizationUpdated method.
  ash::SystemClockClient::Get()->GetLastSyncInfo(
      base::BindOnce(&DeviceOffHoursController::NetworkSynchronizationUpdated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceOffHoursController::SystemClockInitiallyAvailable(
    bool service_is_available) {
  if (!service_is_available)
    return;
  ash::SystemClockClient::Get()->GetLastSyncInfo(
      base::BindOnce(&DeviceOffHoursController::NetworkSynchronizationUpdated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceOffHoursController::NetworkSynchronizationUpdated(
    bool network_synchronized) {
  // Triggered when information about the system time synchronization with
  // network is received.
  is_clock_network_synchronized_ = network_synchronized;
  UpdateOffHoursMode();
}

}  // namespace policy::off_hours