chromium/chrome/browser/ash/policy/scheduled_task_handler/reboot_notifications_scheduler.cc

// Copyright 2022 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/scheduled_task_handler/reboot_notifications_scheduler.h"

#include <algorithm>
#include <optional>

#include "ash/constants/ash_pref_names.h"
#include "base/check_is_test.h"
#include "base/containers/small_map.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "chrome/browser/ash/app_restore/full_restore_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_prefs/user_prefs.h"

namespace policy {
namespace {
constexpr base::TimeDelta kNotificationDelay = base::Hours(1);
constexpr base::TimeDelta kDialogDelay = base::Minutes(5);

const char* ToString(RebootNotificationsScheduler::Requester requester) {
#define CASE(name)                                    \
  case RebootNotificationsScheduler::Requester::name: \
    return #name;

  switch (requester) {
    CASE(kScheduledRebootPolicy);
    CASE(kRebootCommand);
  }
#undef CASE
}
}  // namespace

// Handles the notification request queue ordered by reboot time with current
// request being the earliest request and all other being pending. Does not
// guarantee requests order with the same reboot time.
class RebootNotificationsScheduler::RequestQueue {
 public:
  struct Request {
    // Scheduled reboot time.
    base::Time reboot_time;
    // Callback to run on "Reboot now" button click.
    RebootButtonCallback reboot_button_callback;
  };

  struct RequsterAndRebootTime {
    Requester requester;
    base::Time reboot_time;

    bool operator==(const RequsterAndRebootTime& other) const {
      return requester == other.requester && reboot_time == other.reboot_time;
    }
  };

  RequestQueue() = default;
  ~RequestQueue() = default;

  RequestQueue(const RequestQueue&) = delete;
  RequestQueue& operator=(const RequestQueue&) = delete;

  // Returns the requester with the earliest reboot time.
  std::optional<RequsterAndRebootTime> current_request() const {
    if (requests_.empty()) {
      return std::nullopt;
    }
    const auto it = std::min_element(requests_.begin(), requests_.end(),
                                     [](const auto& first, const auto& second) {
                                       return first.second.reboot_time <
                                              second.second.reboot_time;
                                     });
    DCHECK(it != requests_.end());

    return RequsterAndRebootTime{.requester = it->first,
                                 .reboot_time = it->second.reboot_time};
  }

  std::vector<Requester> GetRequestersForTesting() const {
    CHECK_IS_TEST();

    std::vector<Requester> requesters;
    for (const auto& [requester, request] : requests_) {
      requesters.push_back(requester);
    }

    return requesters;
  }

  // The very last method to call after which the queue is invalid.
  [[nodiscard]] RebootButtonCallback TakeCallback() {
    DCHECK(current_request());
    const Requester current_requester = current_request()->requester;
    DCHECK(requests_[current_requester].reboot_button_callback);
    return std::move(requests_[current_requester].reboot_button_callback);
  }

  // Returns true if the new request takes place. Returns false if the current
  // request does not change.
  [[nodiscard]] bool Reschedule(Requester requester, Request request) {
    const auto original_top_request = current_request();

    requests_[requester] = std::move(request);
    DCHECK(current_request());

    return original_top_request != current_request();
  }

  // Returns true if the current request is being reset. Returns false if the
  // current request does not change.
  [[nodiscard]] bool Reset(Requester requester) {
    const std::optional<RequsterAndRebootTime> current = current_request();

    requests_.erase(requester);

    const bool was_current_request = current && current->requester == requester;
    return was_current_request;
  }

 private:
  // Holds requests per `Requester`.
  base::small_map<std::map<Requester, Request>,
                  static_cast<size_t>(Requester::kMaxValue) + 1>
      requests_;
};

RebootNotificationsScheduler* RebootNotificationsScheduler::instance = nullptr;

RebootNotificationsScheduler::RebootNotificationsScheduler()
    : RebootNotificationsScheduler(base::DefaultClock::GetInstance(),
                                   base::DefaultTickClock::GetInstance()) {}

RebootNotificationsScheduler::RebootNotificationsScheduler(
    const base::Clock* clock,
    const base::TickClock* tick_clock)
    : requester_queue_(std::make_unique<RequestQueue>()),
      notification_timer_(clock, tick_clock),
      dialog_timer_(clock, tick_clock),
      clock_(clock) {
  DCHECK(!RebootNotificationsScheduler::Get());
  RebootNotificationsScheduler::SetInstance(this);
  if (session_manager::SessionManager::Get())
    observation_.Observe(session_manager::SessionManager::Get());
}

RebootNotificationsScheduler::~RebootNotificationsScheduler() {
  DCHECK_EQ(instance, this);
  observation_.Reset();
  RebootNotificationsScheduler::SetInstance(nullptr);
}

// static
RebootNotificationsScheduler* RebootNotificationsScheduler::Get() {
  return RebootNotificationsScheduler::instance;
}

// static
void RebootNotificationsScheduler::RegisterProfilePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(ash::prefs::kShowPostRebootNotification, false);
}

// static
bool RebootNotificationsScheduler::ShouldShowPostRebootNotification(
    Profile* profile) {
  DCHECK(profile);
  PrefService* prefs = user_prefs::UserPrefs::Get(profile);
  return IsPostRebootPrefSet(prefs);
}

void RebootNotificationsScheduler::SchedulePendingRebootNotifications(
    RebootButtonCallback reboot_button_callback,
    const base::Time& reboot_time,
    Requester requester) {
  if (!requester_queue_->Reschedule(
          requester, RequestQueue::Request{.reboot_time = reboot_time,
                                           .reboot_button_callback = std::move(
                                               reboot_button_callback)})) {
    LOG(WARNING) << "Notification request is pending: " << ToString(requester)
                 << " at " << reboot_time;
    return;
  }

  LOG(WARNING) << "Notification is rescheduled: "
               << ToString(requester_queue_->current_request()->requester)
               << " at " << requester_queue_->current_request()->reboot_time;

  SchedulePendingRebootNotificationsForCurrentRequester();
}

void RebootNotificationsScheduler::
    SchedulePendingRebootNotificationsForCurrentRequester() {
  DCHECK(requester_queue_->current_request()) << "A request must be scheduled.";

  ResetNotificationState();

  base::TimeDelta delay =
      GetRebootDelay(requester_queue_->current_request()->reboot_time);

  if (delay > kNotificationDelay) {
    base::Time timer_run_time =
        requester_queue_->current_request()->reboot_time - kNotificationDelay;
    notification_timer_.Start(
        FROM_HERE, timer_run_time,
        base::BindOnce(
            &RebootNotificationsScheduler::MaybeShowPendingRebootNotification,
            weak_ptr_factory_.GetWeakPtr()));
  } else {
    MaybeShowPendingRebootNotification();
  }

  if (delay > kDialogDelay) {
    base::Time timer_run_time =
        requester_queue_->current_request()->reboot_time - kDialogDelay;
    dialog_timer_.Start(
        FROM_HERE, timer_run_time,
        base::BindOnce(
            &RebootNotificationsScheduler::MaybeShowPendingRebootDialog,
            weak_ptr_factory_.GetWeakPtr()));
    return;
  }
  MaybeShowPendingRebootDialog();
}

void RebootNotificationsScheduler::SchedulePostRebootNotification() {
  PrefService* prefs = GetPrefsForActiveProfile();
  if (prefs) {
    prefs->SetBoolean(ash::prefs::kShowPostRebootNotification, true);
  }
}

void RebootNotificationsScheduler::OnUserSessionStarted(bool is_primary_user) {
  // Return if we need to wait for the initialization of full restore service.
  if (ShouldWaitFullRestoreInit())
    return;

  MaybeShowPostRebootNotification(true /*show_simple_notification*/);
}

void RebootNotificationsScheduler::MaybeShowPostRebootNotification(
    bool show_simple_notification) {
  PrefService* prefs = GetPrefsForActiveProfile();
  // Return if the pref is not set for the profile.
  if (!IsPostRebootPrefSet(prefs))
    return;

  if (show_simple_notification) {
    notification_controller_.MaybeShowPostRebootNotification();
  }
  prefs->SetBoolean(ash::prefs::kShowPostRebootNotification, false);
  // No need to observe any more, since we showed the post reboot notification,
  // either as a simple one or integrated with full restore.
  observation_.Reset();
}

std::optional<RebootNotificationsScheduler::Requester>
RebootNotificationsScheduler::GetCurrentRequesterForTesting() const {
  CHECK_IS_TEST();
  return requester_queue_->current_request()
             ? requester_queue_->current_request()->requester
             : std::optional<Requester>(std::nullopt);
}

std::vector<RebootNotificationsScheduler::Requester>
RebootNotificationsScheduler::GetRequestersForTesting() const {
  CHECK_IS_TEST();
  return requester_queue_->GetRequestersForTesting();  // IN-TEST
}

void RebootNotificationsScheduler::CancelRebootNotifications(
    Requester requester) {
  if (!requester_queue_->Reset(requester)) {
    // The current notification request did not change. Nothing to reset or
    // reschedule.
    return;
  }

  // The current notification request changed: either reschedule for a new one
  // taken from pending or hide notifications if there's no more pending.

  if (requester_queue_->current_request()) {
    SchedulePendingRebootNotificationsForCurrentRequester();
  } else {
    ResetNotificationState();
  }
}

void RebootNotificationsScheduler::ResetNotificationState() {
  if (notification_timer_.IsRunning())
    notification_timer_.Stop();
  if (dialog_timer_.IsRunning())
    dialog_timer_.Stop();
  CloseNotifications();
}

void RebootNotificationsScheduler::MaybeShowPendingRebootNotification() {
  DCHECK(requester_queue_->current_request());
  notification_controller_.MaybeShowPendingRebootNotification(
      requester_queue_->current_request()->reboot_time,
      base::BindRepeating(&RebootNotificationsScheduler::OnRebootButtonClicked,
                          base::Unretained(this)));
}

void RebootNotificationsScheduler::MaybeShowPendingRebootDialog() {
  DCHECK(requester_queue_->current_request());
  notification_controller_.MaybeShowPendingRebootDialog(
      requester_queue_->current_request()->reboot_time,
      base::BindOnce(&RebootNotificationsScheduler::OnRebootButtonClicked,
                     base::Unretained(this)));
}

PrefService* RebootNotificationsScheduler::GetPrefsForActiveProfile() const {
  Profile* profile = ProfileManager::GetActiveUserProfile();
  if (!profile)
    return nullptr;
  return user_prefs::UserPrefs::Get(profile);
}

void RebootNotificationsScheduler::OnRebootButtonClicked() {
  std::move(requester_queue_->TakeCallback()).Run();
}

void RebootNotificationsScheduler::SetInstance(
    RebootNotificationsScheduler* reboot_notifications_scheduler) {
  RebootNotificationsScheduler::instance = reboot_notifications_scheduler;
}

base::TimeDelta RebootNotificationsScheduler::GetRebootDelay(
    const base::Time& reboot_time) const {
  return reboot_time - clock_->Now();
}

void RebootNotificationsScheduler::CloseNotifications() {
  notification_controller_.CloseRebootNotification();
  notification_controller_.CloseRebootDialog();
}

bool RebootNotificationsScheduler::ShouldWaitFullRestoreInit() const {
  Profile* profile = ProfileManager::GetActiveUserProfile();
  return ash::full_restore::FullRestoreServiceFactory::
      IsFullRestoreAvailableForProfile(profile);
}

bool RebootNotificationsScheduler::IsPostRebootPrefSet(PrefService* prefs) {
  if (!prefs)
    return false;
  return prefs->GetBoolean(ash::prefs::kShowPostRebootNotification);
}

}  // namespace policy