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

// 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/policy/scheduled_task_handler/device_scheduled_update_checker.h"

#include <time.h>

#include <algorithm>
#include <memory>
#include <utility>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/scheduled_task_executor.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/scheduled_task_executor_impl.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/scheduled_task_util.h"
#include "chrome/browser/ash/policy/scheduled_task_handler/task_executor_with_retries.h"
#include "chrome/common/chrome_features.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "chromeos/ash/components/settings/timezone_settings.h"

namespace policy {

namespace {

// Reason associated to acquire |ScopedWakeLock|.
constexpr char kWakeLockReason[] = "DeviceScheduledUpdateChecker";

// Task name used for parsing ScheduledTaskData.
constexpr char kTaskTimeFieldName[] = "update_check_time";

}  // namespace

// |cros_settings_subscription_| will be destroyed as part of this object
// guaranteeing to not run |OnScheduledUpdateCheckDataChanged| after its
// destruction. Therefore, it's safe to use "this" while adding this observer.
// Similarly, |start_update_check_timer_task_executor_| and
// |os_and_policies_update_checker_| will be destroyed as part of this object,
// so it's safe to use "this" with any callbacks.
DeviceScheduledUpdateChecker::DeviceScheduledUpdateChecker(
    ash::CrosSettings* cros_settings,
    ash::NetworkStateHandler* network_state_handler,
    std::unique_ptr<ScheduledTaskExecutor> update_check_executor)
    : cros_settings_(cros_settings),
      cros_settings_subscription_(cros_settings_->AddSettingsObserver(
          ash::kDeviceScheduledUpdateCheck,
          base::BindRepeating(
              &DeviceScheduledUpdateChecker::OnScheduledUpdateCheckDataChanged,
              base::Unretained(this)))),
      start_update_check_timer_task_executor_(
          update_checker_internal::kMaxStartUpdateCheckTimerRetryIterations,
          update_checker_internal::kStartUpdateCheckTimerRetryTime),
      os_and_policies_update_checker_(network_state_handler),
      update_check_executor_(std::move(update_check_executor)) {
  ash::system::TimezoneSettings::GetInstance()->AddObserver(this);
  // Check if policy already exists.
  OnScheduledUpdateCheckDataChanged();
}

DeviceScheduledUpdateChecker::~DeviceScheduledUpdateChecker() {
  ash::system::TimezoneSettings::GetInstance()->RemoveObserver(this);
}

void DeviceScheduledUpdateChecker::OnUpdateCheckTimerExpired() {
  // Following things needs to be done on every update check event. These will
  // be done serially to make state management easier -
  // - Download updates and refresh policies.
  // - Calculate and start the next update check timer.
  // For both these operations a wake lock should be held to ensure that the
  // update check is completed successfully and the device doesn't suspend while
  // calculating time ticks and setting the timer for the next update check. The
  // wake lock is held even when the above two operations are retried on
  // failure. This decision was made to simplify the design.
  //
  // The wake lock will be released in |OnUpdateCheckTimerStartResult| either
  // due to successful completion of step 2 above or failure and letting the
  // device suspend and reacquire the wake lock again in
  // |StartUpdateCheckTimer|.

  // If no policy exists, state should have been reset and this callback
  // shouldn't have fired.
  DCHECK(scheduled_update_check_data_);

  // |os_and_policies_update_checker_| will be destroyed as part of this object,
  // so it's safe to use "this" with any callbacks. This overrides any previous
  // update check calls. Since, the minimum update frequency is daily there is
  // very little chance of stepping on an existing update check that hasn't
  // finished for a day. Timeouts will ensure that an update check completes,
  // successfully or unsuccessfully, way before a day.
  os_and_policies_update_checker_.Start(
      base::BindOnce(
          &DeviceScheduledUpdateChecker::OnUpdateCheckCompletion,
          base::Unretained(this),
          ScopedWakeLock(device::mojom::WakeLockType::kPreventAppSuspension,
                         kWakeLockReason)),
      update_checker_internal::kOsAndPoliciesUpdateCheckHardTimeout);
}

void DeviceScheduledUpdateChecker::TimezoneChanged(
    const icu::TimeZone& time_zone) {
  // Anytime the time zone changes, the update check timer delay should be
  // recalculated and the timer should be started with updated values according
  // to the new time zone.
  // |scheduled_update_check_data_->next_scheduled_task_time_ticks| also needs
  // to be reset, as it would be incorrect in the context of a new time zone.
  // For this purpose, treat it as a new policy and call
  // |OnScheduledUpdateCheckDataChanged| instead of |MaybeStartUpdateCheckTimer|
  // directly.
  OnScheduledUpdateCheckDataChanged();
}

void DeviceScheduledUpdateChecker::OnScheduledUpdateCheckDataChanged() {
  // If the policy is removed or is not supported on the device, then reset all
  // state including any existing update checks.
  // The policy is not supported if device can not reliably schedule RTC wake
  // in a required range. The specific feature used is one that describes a
  // a known bug on some platforms, where setting rtc wake further than 24 hours
  // away crashes the device. Alternative ways to fix it are too risky, since
  // they may break a bigger proportion of the devices when pushed.
  const base::Value* value =
      cros_settings_->GetPref(ash::kDeviceScheduledUpdateCheck);
  if (!base::FeatureList::IsEnabled(::features::kSupportsRtcWakeOver24Hours) ||
      !value) {
    ResetState();
    return;
  }

  // Keep any old policy timers running if a new policy is ill-formed and can't
  // be used to set a new timer.
  std::optional<ScheduledTaskExecutor::ScheduledTaskData>
      scheduled_update_check_data =
          scheduled_task_util::ParseScheduledTask(*value, kTaskTimeFieldName);
  if (!scheduled_update_check_data) {
    LOG(ERROR) << "Failed to parse policy";
    return;
  }

  // Policy has been updated, calculate and set |update_check_timer_| again.
  scheduled_update_check_data_ = std::move(scheduled_update_check_data);
  MaybeStartUpdateCheckTimer(
      ScopedWakeLock(device::mojom::WakeLockType::kPreventAppSuspension,
                     kWakeLockReason),
      false /* is_retry */);
}

void DeviceScheduledUpdateChecker::StartUpdateCheckTimer(
    ScopedWakeLock scoped_wake_lock) {
  // The device shouldn't suspend while calculating time ticks and setting the
  // timer for the next update check. Otherwise the next update check timer will
  // be inaccurately scheduled. Hence, a wake lock must always be held for this
  // entire task. The wake lock could already be held at this
  // point due to |OnUpdateCheckTimerExpired|.

  // |update_check_executor_| will be destroyed as part of this object and is
  // guaranteed to not run callbacks after its destruction. Therefore, it's
  // safe to use base::Unretained(this) when starting the executor.
  update_check_executor_->Start(
      &scheduled_update_check_data_.value(),
      base::BindOnce(
          &DeviceScheduledUpdateChecker::OnUpdateCheckTimerStartResult,
          base::Unretained(this), std::move(scoped_wake_lock)),
      base::BindOnce(&DeviceScheduledUpdateChecker::OnUpdateCheckTimerExpired,
                     base::Unretained(this)));
}

void DeviceScheduledUpdateChecker::OnUpdateCheckTimerStartResult(
    ScopedWakeLock scoped_wake_lock,
    bool result) {
  // Schedule a retry if |update_check_timer_| failed to start.
  if (!result) {
    LOG(ERROR) << "Failed to start update check timer";
    MaybeStartUpdateCheckTimer(std::move(scoped_wake_lock),
                               true /* is_retry */);
  }
}

void DeviceScheduledUpdateChecker::OnStartUpdateCheckTimerRetryFailure() {
  // Retrying has a limit. In the unlikely scenario this is met, the next update
  // check can only happen when a new policy comes in or Chrome is restarted.
  // The wake lock acquired in |MaybeStartUpdateCheckTimer| will be released
  // because |task| was destroyed in |start_update_check_timer_executor_|.
  LOG(ERROR) << "Failed to start update check timer after all retries";
  ResetState();
}

void DeviceScheduledUpdateChecker::MaybeStartUpdateCheckTimer(
    ScopedWakeLock scoped_wake_lock,
    bool is_retry) {
  // No need to start the next update check timer if the policy has been
  // removed. This can happen if an update check was ongoing, a new policy
  // came in but failed to start the timer which reset all state in
  // |OnStartUpdateCheckTimerRetryFailure|.
  if (!scheduled_update_check_data_)
    return;

  // Only start the timer if an existing update check is not running because if
  // it is, it will start the timer in |OnUpdateCheckCompletion|.
  if (os_and_policies_update_checker_.IsRunning())
    return;

  // Safe to use |this| as |start_update_check_timer_task_executor_| is a
  // member of |this|. Wake lock needs to be held to calculate next update check
  // timer accurately without the device suspending mid-calculation.
  if (is_retry) {
    start_update_check_timer_task_executor_.ScheduleRetry(
        base::BindOnce(&DeviceScheduledUpdateChecker::StartUpdateCheckTimer,
                       base::Unretained(this), std::move(scoped_wake_lock)));
  } else {
    start_update_check_timer_task_executor_.Start(
        base::BindOnce(&DeviceScheduledUpdateChecker::StartUpdateCheckTimer,
                       base::Unretained(this), std::move(scoped_wake_lock)),
        base::BindOnce(
            &DeviceScheduledUpdateChecker::OnStartUpdateCheckTimerRetryFailure,
            base::Unretained(this)));
  }
}

void DeviceScheduledUpdateChecker::OnUpdateCheckCompletion(
    ScopedWakeLock scoped_wake_lock,
    bool result) {
  // Start the next update check timer irrespective of the current update
  // check succeeding or not.
  LOG_IF(ERROR, !result) << "Update check failed";
  MaybeStartUpdateCheckTimer(std::move(scoped_wake_lock), false /* is_retry */);
}

void DeviceScheduledUpdateChecker::ResetState() {
  update_check_executor_->Reset();
  scheduled_update_check_data_ = std::nullopt;
  os_and_policies_update_checker_.Stop();
  start_update_check_timer_task_executor_.Stop();
}

}  // namespace policy