chromium/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.cc

// Copyright 2020 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/cert_provisioning/cert_provisioning_scheduler.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <unordered_set>
#include <variant>

#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/ash/cert_provisioning/cert_provisioning_client.h"
#include "chrome/browser/ash/cert_provisioning/cert_provisioning_common.h"
#include "chrome/browser/ash/cert_provisioning/cert_provisioning_metrics.h"
#include "chrome/browser/ash/cert_provisioning/cert_provisioning_worker.h"
#include "chrome/browser/ash/platform_keys/platform_keys_service.h"
#include "chrome/browser/ash/platform_keys/platform_keys_service_factory.h"
#include "chrome/browser/ash/policy/core/user_cloud_policy_manager_ash.h"
#include "chrome/browser/ash/policy/invalidation/affiliated_invalidation_service_provider.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/chromeos/platform_keys/platform_keys.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "components/invalidation/invalidation_listener.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"

namespace ash::cert_provisioning {

namespace {

template <typename Container, typename Value>
void EraseByKey(Container& container, const Value& value) {
  auto iter = container.find(value);
  if (iter == container.end()) {
    return;
  }

  container.erase(iter);
}

const base::TimeDelta kInconsistentDataErrorRetryDelay = base::Seconds(30);

policy::CloudPolicyClient* GetCloudPolicyClientForUser(Profile* profile) {
  policy::UserCloudPolicyManagerAsh* user_cloud_policy_manager =
      profile->GetUserCloudPolicyManagerAsh();
  if (!user_cloud_policy_manager) {
    return nullptr;
  }

  policy::CloudPolicyCore* core = user_cloud_policy_manager->core();
  if (!core) {
    return nullptr;
  }

  return core->client();
}

NetworkStateHandler* GetNetworkStateHandler() {
  // Can happen in tests.
  if (!NetworkHandler::IsInitialized()) {
    return nullptr;
  }
  return NetworkHandler::Get()->network_state_handler();
}

}  // namespace

FailedWorkerInfo::FailedWorkerInfo() = default;
FailedWorkerInfo::~FailedWorkerInfo() = default;
FailedWorkerInfo::FailedWorkerInfo(const FailedWorkerInfo&) = default;
FailedWorkerInfo& FailedWorkerInfo::operator=(const FailedWorkerInfo&) =
    default;

// static
std::unique_ptr<CertProvisioningScheduler>
CertProvisioningSchedulerImpl::CreateUserCertProvisioningScheduler(
    Profile* profile) {
  PrefService* pref_service = profile->GetPrefs();
  policy::CloudPolicyClient* cloud_policy_client =
      GetCloudPolicyClientForUser(profile);
  platform_keys::PlatformKeysService* platform_keys_service =
      GetPlatformKeysService(CertScope::kUser, profile);
  NetworkStateHandler* network_state_handler = GetNetworkStateHandler();

  if (!profile || !pref_service || !cloud_policy_client ||
      !network_state_handler) {
    LOG(ERROR) << "Failed to create user certificate provisioning scheduler";
    return nullptr;
  }

  return std::make_unique<CertProvisioningSchedulerImpl>(
      CertScope::kUser, profile, pref_service,
      std::make_unique<CertProvisioningClientImpl>(*cloud_policy_client),
      platform_keys_service, network_state_handler,
      std::make_unique<CertProvisioningUserInvalidatorFactory>(profile));
}

// static
std::unique_ptr<CertProvisioningScheduler>
CertProvisioningSchedulerImpl::CreateDeviceCertProvisioningScheduler(
    policy::CloudPolicyClient* cloud_policy_client,
    std::variant<policy::AffiliatedInvalidationServiceProvider*,
                 invalidation::InvalidationListener*>
        invalidation_service_provider_or_listener) {
  PrefService* pref_service = g_browser_process->local_state();
  platform_keys::PlatformKeysService* platform_keys_service =
      GetPlatformKeysService(CertScope::kDevice, /*profile=*/nullptr);
  NetworkStateHandler* network_state_handler = GetNetworkStateHandler();

  if (!pref_service || !cloud_policy_client || !network_state_handler ||
      !platform_keys_service) {
    LOG(ERROR) << "Failed to create device certificate provisioning scheduler";
    return nullptr;
  }

  return std::make_unique<CertProvisioningSchedulerImpl>(
      CertScope::kDevice, /*profile=*/nullptr, pref_service,
      std::make_unique<CertProvisioningClientImpl>(*cloud_policy_client),
      platform_keys_service, network_state_handler,
      std::make_unique<CertProvisioningDeviceInvalidatorFactory>(
          invalidation_service_provider_or_listener));
}

CertProvisioningSchedulerImpl::CertProvisioningSchedulerImpl(
    CertScope cert_scope,
    Profile* profile,
    PrefService* pref_service,
    std::unique_ptr<CertProvisioningClient> cert_provisioning_client,
    platform_keys::PlatformKeysService* platform_keys_service,
    NetworkStateHandler* network_state_handler,
    std::unique_ptr<CertProvisioningInvalidatorFactory> invalidator_factory)
    : cert_scope_(cert_scope),
      profile_(profile),
      pref_service_(pref_service),
      cert_provisioning_client_(std::move(cert_provisioning_client)),
      platform_keys_service_(platform_keys_service),
      network_state_handler_(network_state_handler),
      certs_with_ids_getter_(cert_scope, platform_keys_service),
      cert_deleter_(cert_scope, platform_keys_service),
      invalidator_factory_(std::move(invalidator_factory)) {
  CHECK(profile_ || cert_scope_ == CertScope::kDevice);
  CHECK(pref_service_);
  CHECK(cert_provisioning_client_);
  CHECK(platform_keys_service_);
  CHECK(network_state_handler);
  CHECK(invalidator_factory_);

  pref_name_ = GetPrefNameForCertProfiles(cert_scope);
  CHECK(pref_name_);

  scoped_platform_keys_service_observation_.Observe(
      platform_keys_service_.get());

  network_state_handler_observer_.Observe(network_state_handler_.get());

  ScheduleInitialUpdate();
  ScheduleDailyUpdate();
}

CertProvisioningSchedulerImpl::~CertProvisioningSchedulerImpl() = default;

void CertProvisioningSchedulerImpl::ScheduleInitialUpdate() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&CertProvisioningSchedulerImpl::InitialUpdateCerts,
                     weak_factory_.GetWeakPtr()));
}

void CertProvisioningSchedulerImpl::ScheduleDailyUpdate() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&CertProvisioningSchedulerImpl::DailyUpdateWorkers,
                     weak_factory_.GetWeakPtr()),
      base::Days(1));
}

void CertProvisioningSchedulerImpl::ScheduleRetry(
    const CertProfileId& profile_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // TODO: b/299054905 - Instead of using a hardcoded delay time, trigger a
  // policy refresh and restart workers when policies have been applied.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&CertProvisioningSchedulerImpl::UpdateOneWorkerImpl,
                     weak_factory_.GetWeakPtr(), profile_id),
      kInconsistentDataErrorRetryDelay);
}

void CertProvisioningSchedulerImpl::ScheduleRenewal(
    const CertProfileId& profile_id,
    base::TimeDelta delay) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (base::Contains(scheduled_renewals_, profile_id)) {
    return;
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&CertProvisioningSchedulerImpl::InitiateRenewal,
                     weak_factory_.GetWeakPtr(), profile_id),
      delay);
}

void CertProvisioningSchedulerImpl::InitialUpdateCerts() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  DeleteCertsWithoutPolicy();
}

void CertProvisioningSchedulerImpl::DeleteCertsWithoutPolicy() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // No-op if the PlatformKeysService has already been shut down.
  if (!platform_keys_service_) {
    return;
  }

  cert_deleter_.DeleteCerts(
      base::MakeFlatSet<CertProfileId>(GetCertProfiles(), {},
                                       &CertProfile::profile_id),
      base::BindOnce(
          &CertProvisioningSchedulerImpl::OnDeleteCertsWithoutPolicyDone,
          weak_factory_.GetWeakPtr()));
}

void CertProvisioningSchedulerImpl::OnDeleteCertsWithoutPolicyDone(
    chromeos::platform_keys::Status status) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (status != chromeos::platform_keys::Status::kSuccess) {
    LOG(ERROR) << "Failed to delete certificates without policies: "
               << chromeos::platform_keys::StatusToString(status);
  }

  DeserializeWorkers();
  CleanVaKeysIfIdle();
}

void CertProvisioningSchedulerImpl::CleanVaKeysIfIdle() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!workers_.empty()) {
    OnCleanVaKeysIfIdleDone(true);
    return;
  }

  DeleteVaKeysByPrefix(
      cert_scope_, profile_, kKeyNamePrefix,
      base::BindOnce(&CertProvisioningSchedulerImpl::OnCleanVaKeysIfIdleDone,
                     weak_factory_.GetWeakPtr()));
}

void CertProvisioningSchedulerImpl::OnCleanVaKeysIfIdleDone(
    bool delete_result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!delete_result) {
    LOG(ERROR) << "Failed to delete keys while idle";
  }

  RegisterForPrefsChanges();
  UpdateAllWorkers();
}

void CertProvisioningSchedulerImpl::RegisterForPrefsChanges() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  pref_change_registrar_.Init(pref_service_);
  pref_change_registrar_.Add(
      pref_name_,
      base::BindRepeating(&CertProvisioningSchedulerImpl::OnPrefsChange,
                          weak_factory_.GetWeakPtr()));
}

void CertProvisioningSchedulerImpl::DailyUpdateWorkers() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  failed_cert_profiles_.clear();
  UpdateAllWorkers();
  ScheduleDailyUpdate();
}

void CertProvisioningSchedulerImpl::DeserializeWorkers() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  const base::Value::Dict& saved_workers =
      pref_service_->GetDict(GetPrefNameForSerialization(cert_scope_));

  for (const auto kv : saved_workers) {
    const base::Value::Dict& saved_worker = kv.second.GetDict();

    std::unique_ptr<CertProvisioningWorker> worker =
        CertProvisioningWorkerFactory::Get()->Deserialize(
            cert_scope_, profile_, pref_service_, saved_worker,
            cert_provisioning_client_.get(), invalidator_factory_->Create(),
            base::BindRepeating(
                &CertProvisioningSchedulerImpl::OnVisibleStateChanged,
                weak_factory_.GetWeakPtr()),
            base::BindOnce(&CertProvisioningSchedulerImpl::OnProfileFinished,
                           weak_factory_.GetWeakPtr()));
    if (!worker) {
      // Deserialization error message was already logged.
      continue;
    }

    AddWorkerToMap(std::move(worker));
  }
}

void CertProvisioningSchedulerImpl::OnPrefsChange() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  UpdateAllWorkers();
}

void CertProvisioningSchedulerImpl::InitiateRenewal(
    const CertProfileId& cert_profile_id) {
  scheduled_renewals_.erase(cert_profile_id);
  UpdateOneWorkerImpl(cert_profile_id);
}

bool CertProvisioningSchedulerImpl::UpdateOneWorker(
    const CertProfileId& cert_profile_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto worker_iter = workers_.find(cert_profile_id);
  if (worker_iter == workers_.end()) {
    return false;
  }

  RecordEvent(worker_iter->second->GetCertProfile().protocol_version,
              cert_scope_, CertProvisioningEvent::kWorkerRetryManual);
  UpdateOneWorkerImpl(cert_profile_id);
  return true;
}

void CertProvisioningSchedulerImpl::UpdateOneWorkerImpl(
    const CertProfileId& cert_profile_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  EraseByKey(failed_cert_profiles_, cert_profile_id);

  std::optional<CertProfile> cert_profile = GetOneCertProfile(cert_profile_id);
  if (!cert_profile) {
    return;
  }

  UpdateWorkerList({std::move(cert_profile).value()});
}

void CertProvisioningSchedulerImpl::UpdateAllWorkers() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::vector<CertProfile> profiles = GetCertProfiles();
  CancelWorkersWithoutPolicy(profiles);

  if (profiles.empty()) {
    return;
  }

  UpdateWorkerList(std::move(profiles));
}

void CertProvisioningSchedulerImpl::UpdateWorkerList(
    std::vector<CertProfile> profiles) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // No-op if the PlatformKeysService has already been shut down.
  if (!platform_keys_service_) {
    return;
  }

  if (!MaybeWaitForInternetConnection()) {
    return;
  }

  if (certs_with_ids_getter_.IsRunning()) {
    queued_profiles_to_update_.insert(std::make_move_iterator(profiles.begin()),
                                      std::make_move_iterator(profiles.end()));
    return;
  }

  certs_with_ids_getter_.GetCertsWithIds(base::BindOnce(
      &CertProvisioningSchedulerImpl::UpdateWorkerListWithExistingCerts,
      weak_factory_.GetWeakPtr(), std::move(profiles)));
}

void CertProvisioningSchedulerImpl::UpdateWorkerListWithExistingCerts(
    std::vector<CertProfile> profiles,
    base::flat_map<CertProfileId, scoped_refptr<net::X509Certificate>>
        existing_certs_with_ids,
    chromeos::platform_keys::Status status) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (status != chromeos::platform_keys::Status::kSuccess) {
    LOG(ERROR) << "Failed to get existing cert ids: "
               << chromeos::platform_keys::StatusToString(status);
    return;
  }

  for (const auto& profile : profiles) {
    if (base::Contains(failed_cert_profiles_, profile.profile_id)) {
      continue;
    }

    auto cert_iter = existing_certs_with_ids.find(profile.profile_id);
    if (cert_iter == existing_certs_with_ids.end()) {
      // The certificate does not exists and should be provisioned.
      ProcessProfile(profile);
      continue;
    }

    const auto& cert = cert_iter->second;
    base::Time now = base::Time::Now();
    if ((now + profile.renewal_period) >= cert->valid_expiry()) {
      // The certificate should be renewed immediately.
      ProcessProfile(profile);
      continue;
    }

    if ((now + base::Days(1) + profile.renewal_period) >=
        cert->valid_expiry()) {
      // The certificate should be renewed within 1 day.
      base::Time target_time = cert->valid_expiry() - profile.renewal_period;
      ScheduleRenewal(profile.profile_id, /*delay=*/target_time - now);
      continue;
    }
  }

  if (!queued_profiles_to_update_.empty()) {
    // base::flat_set::extract() guaranties that the set is `empty()`
    // afterwards.
    UpdateWorkerList(std::move(queued_profiles_to_update_).extract());
  }
}

void CertProvisioningSchedulerImpl::ProcessProfile(
    const CertProfile& cert_profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  CertProvisioningWorker* worker = FindWorker(cert_profile.profile_id);
  if (!worker) {
    CreateCertProvisioningWorker(cert_profile);
    return;
  }

  if ((worker->GetCertProfile().policy_version !=
       cert_profile.policy_version)) {
    // The worker has outdated policy version. Make it stop, clean up current
    // state and report back through its callback. That will trigger retry for
    // its certificate profile.
    worker->Stop(CertProvisioningWorkerState::kInconsistentDataError);
    return;
  }

  if (worker->IsWaiting()) {
    worker->DoStep();
    return;
  }

  // There already is an active worker for this profile. No action required.
  return;
}

void CertProvisioningSchedulerImpl::CreateCertProvisioningWorker(
    CertProfile cert_profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::unique_ptr<CertProvisioningWorker> worker =
      CertProvisioningWorkerFactory::Get()->Create(
          GenerateCertProvisioningId(), cert_scope_, profile_, pref_service_,
          cert_profile, cert_provisioning_client_.get(),
          invalidator_factory_->Create(),
          base::BindRepeating(
              &CertProvisioningSchedulerImpl::OnVisibleStateChanged,
              weak_factory_.GetWeakPtr()),
          base::BindOnce(&CertProvisioningSchedulerImpl::OnProfileFinished,
                         weak_factory_.GetWeakPtr()));
  CertProvisioningWorker* worker_unowned = AddWorkerToMap(std::move(worker));
  worker_unowned->DoStep();
}

void CertProvisioningSchedulerImpl::OnProfileFinished(
    CertProfile profile,
    std::string process_id,
    CertProvisioningWorkerState state) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto worker_iter = workers_.find(profile.profile_id);
  if (worker_iter == workers_.end()) {
    NOTREACHED_IN_MIGRATION();
    LOG(WARNING) << "Finished worker is not found"
                 << base::StringPrintf(" [cppId: %s]", process_id.c_str());
    return;
  }
  bool recreate = false;
  switch (state) {
    case CertProvisioningWorkerState::kSucceeded:
      VLOG(0) << "Successfully provisioned certificate"
              << base::StringPrintf(" [cppId: %s, profileId: %s]",
                                    process_id.c_str(),
                                    profile.profile_id.c_str());
      break;
    case CertProvisioningWorkerState::kInconsistentDataError:
      LOG(WARNING) << "Inconsistent data error"
                   << base::StringPrintf(" [cppId: %s, profileId: %s]",
                                         process_id.c_str(),
                                         profile.profile_id.c_str());
      ScheduleRetry(profile.profile_id);
      break;
    case CertProvisioningWorkerState::kCanceled:
      if (worker_iter->second->IsWorkerMarkedForReset()) {
        recreate = true;
      }
      break;
    default:
      LOG(ERROR) << "Failed to process certificate"
                 << base::StringPrintf(" [cppId: %s, profileId: %s]",
                                       process_id.c_str(),
                                       profile.profile_id.c_str());
      UpdateFailedCertProfiles(*(worker_iter->second));
      break;
  }

  if (recreate) {
    // Avoid updating the ui after removal of the worker as it it will close the
    // dialogue while reseting. The ui will be updated when the new worker is
    // created.
    RemoveWorkerFromMap(worker_iter,
                        /*send_visible_state_changed_update=*/false);
    CreateCertProvisioningWorker(std::move(profile));
  } else {
    RemoveWorkerFromMap(worker_iter,
                        /*send_visible_state_changed_update=*/true);
  }
}

bool CertProvisioningSchedulerImpl::ResetOneWorker(
    const CertProfileId& cert_profile_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::optional<CertProfile> cert_profile = GetOneCertProfile(cert_profile_id);
  if (!cert_profile) {
    return false;
  }
  CertProvisioningWorker* worker = FindWorker(cert_profile_id);
  if (!worker) {
    UpdateOneWorkerImpl(cert_profile_id);
    return true;
  }
  if (worker->IsWorkerMarkedForReset()) {
    return true;
  }
  worker->MarkWorkerForReset();
  worker->Stop(CertProvisioningWorkerState::kCanceled);
  return true;
}

CertProvisioningWorker* CertProvisioningSchedulerImpl::FindWorker(
    CertProfileId profile_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto iter = workers_.find(profile_id);
  if (iter == workers_.end()) {
    return nullptr;
  }

  return iter->second.get();
}

CertProvisioningWorker* CertProvisioningSchedulerImpl::AddWorkerToMap(
    std::unique_ptr<CertProvisioningWorker> worker) {
  CertProvisioningWorker* worker_unowned = worker.get();
  workers_[worker_unowned->GetCertProfile().profile_id] = std::move(worker);
  OnVisibleStateChanged();
  return worker_unowned;
}

void CertProvisioningSchedulerImpl::RemoveWorkerFromMap(
    WorkerMap::iterator worker_iter,
    bool send_visible_state_changed_update) {
  workers_.erase(worker_iter);
  // In a case like removing an existing worker for the intent of recreation,
  // the ui should not be sent an update here as this will cause the worker
  // dialogue to be closed. Instead, the ui update will be triggered by the new
  // worker.
  if (send_visible_state_changed_update) {
    OnVisibleStateChanged();
  }
}

std::optional<CertProfile> CertProvisioningSchedulerImpl::GetOneCertProfile(
    const CertProfileId& cert_profile_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  const base::Value& profile_list = pref_service_->GetValue(pref_name_);

  for (const base::Value& cur_profile : profile_list.GetList()) {
    const base::Value::Dict& cur_profile_dict = cur_profile.GetDict();
    const CertProfileId* id = cur_profile_dict.FindString(kCertProfileIdKey);
    if (!id || (*id != cert_profile_id)) {
      continue;
    }

    return CertProfile::MakeFromValue(cur_profile_dict);
  }

  return std::nullopt;
}

std::vector<CertProfile> CertProvisioningSchedulerImpl::GetCertProfiles() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  const base::Value& profile_list = pref_service_->GetValue(pref_name_);

  std::vector<CertProfile> result_profiles;
  for (const base::Value& cur_profile : profile_list.GetList()) {
    std::optional<CertProfile> p =
        CertProfile::MakeFromValue(cur_profile.GetDict());
    if (!p) {
      LOG(WARNING) << "Failed to parse certificate profile";
      continue;
    }

    result_profiles.emplace_back(std::move(p.value()));
  }

  return result_profiles;
}

const WorkerMap& CertProvisioningSchedulerImpl::GetWorkers() const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  return workers_;
}

const base::flat_map<CertProfileId, FailedWorkerInfo>&
CertProvisioningSchedulerImpl::GetFailedCertProfileIds() const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  return failed_cert_profiles_;
}

base::CallbackListSubscription CertProvisioningSchedulerImpl::AddObserver(
    base::RepeatingClosure callback) {
  return observers_.Add(std::move(callback));
}

bool CertProvisioningSchedulerImpl::MaybeWaitForInternetConnection() {
  const NetworkState* network = network_state_handler_->DefaultNetwork();
  bool is_online = network && network->IsOnline();

  if (is_online) {
    is_waiting_for_online_ = false;
    return true;
  }

  WaitForInternetConnection();
  return false;
}

void CertProvisioningSchedulerImpl::WaitForInternetConnection() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (is_waiting_for_online_) {
    return;
  }

  if (!workers_.empty()) {
    VLOG(0) << "Waiting for internet connection";
  }

  is_waiting_for_online_ = true;
  for (auto& kv : workers_) {
    auto& worker_ptr = kv.second;
    worker_ptr->Pause();
  }
}

void CertProvisioningSchedulerImpl::OnNetworkChange(
    const NetworkState* network) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // If waiting for connection and some network becomes online, try to continue.
  if (is_waiting_for_online_ && network && network->IsOnline()) {
    is_waiting_for_online_ = false;
    UpdateAllWorkers();
    return;
  }

  // If not waiting, check that after this network change some connection still
  // exists.
  if (!is_waiting_for_online_ && !workers_.empty()) {
    MaybeWaitForInternetConnection();
    return;
  }
}

void CertProvisioningSchedulerImpl::DefaultNetworkChanged(
    const NetworkState* network) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  OnNetworkChange(network);
}

void CertProvisioningSchedulerImpl::NetworkConnectionStateChanged(
    const NetworkState* network) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  OnNetworkChange(network);
}

void CertProvisioningSchedulerImpl::UpdateFailedCertProfiles(
    const CertProvisioningWorker& worker) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  FailedWorkerInfo info;
  info.state_before_failure = worker.GetPreviousState();
  info.cert_profile_name = worker.GetCertProfile().name;
  info.public_key = worker.GetPublicKey();
  info.last_update_time = worker.GetLastUpdateTime();
  info.failure_message = worker.GetFailureMessage();

  failed_cert_profiles_[worker.GetCertProfile().profile_id] = std::move(info);
}

void CertProvisioningSchedulerImpl::OnPlatformKeysServiceShutDown() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // The |platform_keys_service_| will only return errors going forward, so
  // stop using it. Shutdown all workers, as if this CertProvisioningScheduler
  // was destroyed, and stop pending tasks that may depend on
  // |platform_keys_service_|.
  workers_.clear();
  certs_with_ids_getter_.Cancel();
  cert_deleter_.Cancel();
  pref_change_registrar_.RemoveAll();
  weak_factory_.InvalidateWeakPtrs();

  scoped_platform_keys_service_observation_.Reset();
  platform_keys_service_ = nullptr;
}

void CertProvisioningSchedulerImpl::CancelWorkersWithoutPolicy(
    const std::vector<CertProfile>& profiles) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (workers_.empty()) {
    return;
  }

  std::unordered_set<CertProfileId> cert_profile_ids;
  for (const CertProfile& profile : profiles) {
    cert_profile_ids.insert(profile.profile_id);
  }

  for (auto& kv : workers_) {
    auto& worker_ptr = kv.second;
    if (cert_profile_ids.find(worker_ptr->GetCertProfile().profile_id) ==
        cert_profile_ids.end()) {
      // This will trigger clean up (if any) in the worker and make it call its
      // callback.
      worker_ptr->Stop(CertProvisioningWorkerState::kCanceled);
    }
  }
}

void CertProvisioningSchedulerImpl::OnVisibleStateChanged() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // |notify_observers_pending_| prevents the scheduler from sending multiple
  // notifications from a single synchronous code execution sequence. Extra
  // notifications are discarded.
  if (observers_.empty() || notify_observers_pending_) {
    return;
  }

  // |hold_back_updates_timer_| prevents the scheduler from sending multiple
  // notifications within a specified time period from asynchronous tasks. Extra
  // notifications are combined into one and delayed.
  if (hold_back_updates_timer_.IsRunning()) {
    update_after_hold_back_ = true;
    return;
  }
  constexpr base::TimeDelta kTimeToHoldBackUpdates = base::Milliseconds(300);
  hold_back_updates_timer_.Start(
      FROM_HERE, kTimeToHoldBackUpdates,
      base::BindOnce(
          &CertProvisioningSchedulerImpl::OnHoldBackUpdatesTimerExpired,
          weak_factory_.GetWeakPtr()));

  notify_observers_pending_ = true;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &CertProvisioningSchedulerImpl::NotifyObserversVisibleStateChanged,
          weak_factory_.GetWeakPtr()));
}

void CertProvisioningSchedulerImpl::OnHoldBackUpdatesTimerExpired() {
  if (update_after_hold_back_) {
    update_after_hold_back_ = false;
    NotifyObserversVisibleStateChanged();
  }
}

void CertProvisioningSchedulerImpl::NotifyObserversVisibleStateChanged() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  notify_observers_pending_ = false;
  observers_.Notify();
}

}  // namespace ash::cert_provisioning