chromium/ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager.h"

#import <vector>

#import "base/functional/bind.h"
#import "base/location.h"
#import "base/metrics/histogram_functions.h"
#import "base/time/time.h"
#import "base/values.h"
#import "components/password_manager/core/browser/leak_detection/leak_detection_request_utils.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/scoped_user_pref_update.h"
#import "components/safe_browsing/core/common/safe_browsing_prefs.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/browser/ntp/shared/metrics/home_metrics.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_utils.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.h"
#import "ios/chrome/browser/upgrade/model/upgrade_recommended_details.h"
#import "ios/chrome/browser/upgrade/model/upgrade_utils.h"
#import "ios/chrome/common/channel_info.h"

IOSChromeSafetyCheckManager::IOSChromeSafetyCheckManager(
    PrefService* pref_service,
    PrefService* local_pref_service,
    scoped_refptr<IOSChromePasswordCheckManager> password_check_manager,
    scoped_refptr<base::SequencedTaskRunner> task_runner)
    : pref_service_(pref_service),
      local_pref_service_(local_pref_service),
      password_check_manager_(password_check_manager),
      task_runner_(task_runner) {
  CHECK(pref_service_);
  CHECK(local_pref_service_);
  CHECK(password_check_manager_);
  CHECK(task_runner_);

  password_check_manager_->AddObserver(this);

  if (IsOmahaServiceRefactorEnabled()) {
    OmahaService::AddObserver(this);
  }

  pref_change_registrar_.Init(pref_service);

  pref_change_registrar_.Add(
      prefs::kSafeBrowsingEnabled,
      base::BindRepeating(
          &IOSChromeSafetyCheckManager::UpdateSafeBrowsingCheckState,
          weak_ptr_factory_.GetWeakPtr()));

  pref_change_registrar_.Add(
      prefs::kSafeBrowsingEnhanced,
      base::BindRepeating(
          &IOSChromeSafetyCheckManager::UpdateSafeBrowsingCheckState,
          weak_ptr_factory_.GetWeakPtr()));

  RestorePreviousSafetyCheckState();

  // Run the Safety Check automatically, if eligible.
  //
  // TODO(crbug.com/354706390): Re-evaluate autorun eligibility during scene
  // state changes for better accuracy and to support future increased autorun
  // frequency.
  //
  // TODO(crbug.com/354707092): Replace
  // `GetLastSafetyCheckRunTimeAcrossAllEntrypoints()` with
  // `GetLastSafetyCheckRunTime()` once the Safety Check (via Settings) is
  // refactored to use `IOSChromeSafetyCheckManager`. For now
  // `GetLastSafetyCheckRunTimeAcrossAllEntrypoints()` returns the last run
  // time, across both entry points.
  if (IsSafetyCheckNotificationsEnabled()) {
    if (CanAutomaticallyRunSafetyCheck(
            GetLatestSafetyCheckRunTimeAcrossAllEntrypoints(
                local_pref_service))) {
      StartSafetyCheck();
    }
  }
}

IOSChromeSafetyCheckManager::~IOSChromeSafetyCheckManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void IOSChromeSafetyCheckManager::Shutdown() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // `OmahaService` instances are not currently destroyed due to the
  // `NoDestructor` implementation. This prevents `OmahaServiceObserver`'s
  // `ServiceWillShutdown()` from being called as intended. As a workaround,
  // manually remove the observation here to ensure proper cleanup.
  if (IsOmahaServiceRefactorEnabled()) {
    OmahaService::RemoveObserver(this);
  }

  for (auto& observer : observers_) {
    observer.ManagerWillShutdown(this);
  }

  DCHECK(observers_.empty());

  pref_change_registrar_.RemoveAll();
  pref_service_ = nullptr;
  local_pref_service_ = nullptr;

  if (password_check_manager_) {
    password_check_manager_->RemoveObserver(this);
    password_check_manager_ = nullptr;
  }

  if (IsOmahaServiceRefactorEnabled()) {
    CHECK(!OmahaServiceObserver::IsInObserverList());
  }
}

void IOSChromeSafetyCheckManager::StartSafetyCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Do nothing if the Safety Check is already running.
  if (running_safety_check_state_ == RunningSafetyCheckState::kRunning) {
    return;
  }

  LogCurrentSafetyCheckRunTime();

  // Asynchronous checks
  StartPasswordCheck();
  StartUpdateChromeCheck();

  // Synchronous checks
  UpdateSafeBrowsingCheckState();
}

void IOSChromeSafetyCheckManager::StopSafetyCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Do nothing if the Safety Check is not running.
  if (running_safety_check_state_ != RunningSafetyCheckState::kRunning) {
    return;
  }

  StopPasswordCheck();
  StopUpdateChromeCheck();
}

void IOSChromeSafetyCheckManager::RestorePreviousSafetyCheckState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  last_safety_check_run_time_ =
      local_pref_service_->GetTime(prefs::kIosSafetyCheckManagerLastRunTime);

  std::optional<SafeBrowsingSafetyCheckState> safe_browsing_check_state =
      SafeBrowsingSafetyCheckStateForName(local_pref_service_->GetString(
          prefs::kIosSafetyCheckManagerSafeBrowsingCheckResult));

  if (safe_browsing_check_state.has_value() &&
      safe_browsing_check_state.value() !=
          SafeBrowsingSafetyCheckState::kRunning) {
    SetSafeBrowsingCheckState(safe_browsing_check_state.value());
  }

  password_manager::InsecurePasswordCounts insecure_password_counts =
      DictToInsecurePasswordCounts(pref_service_->GetDict(
          prefs::kIosSafetyCheckManagerInsecurePasswordCounts));
  insecure_password_counts_ = insecure_password_counts;
  previous_insecure_password_counts_ = insecure_password_counts;

  std::optional<PasswordSafetyCheckState> password_check_state =
      PasswordSafetyCheckStateForName(pref_service_->GetString(
          prefs::kIosSafetyCheckManagerPasswordCheckResult));

  if (password_check_state.has_value() &&
      password_check_state.value() != PasswordSafetyCheckState::kRunning) {
    SetPasswordCheckState(password_check_state.value());
  }

  std::optional<UpdateChromeSafetyCheckState> update_chrome_check_state =
      UpdateChromeSafetyCheckStateForName(local_pref_service_->GetString(
          prefs::kIosSafetyCheckManagerUpdateCheckResult));

  if (update_chrome_check_state.has_value() &&
      update_chrome_check_state.value() !=
          UpdateChromeSafetyCheckState::kRunning) {
    SetUpdateChromeCheckState(update_chrome_check_state.value());
  }
}

void IOSChromeSafetyCheckManager::StartPasswordCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Do nothing if the Password check is already running.
  if (password_check_state_ == PasswordSafetyCheckState::kRunning) {
    return;
  }

  ignore_password_check_changes_ = false;

  previous_password_check_state_ = password_check_state_;

  previous_insecure_password_counts_ = insecure_password_counts_;

  password_check_manager_->StartPasswordCheck(
      password_manager::LeakDetectionInitiator::kIosProactivePasswordCheckup);

  // NOTE: There's no need to explicitly set `password_check_state_` to
  // `kRunning` here because this class conforms to
  // `IOSChromePasswordCheckManager::Observer`. Whenever the observer method
  // `PasswordCheckStatusChanged()` is called with a running state,
  // `password_check_state_` will then be set to `kRunning`.
}

void IOSChromeSafetyCheckManager::StopPasswordCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Do nothing if the Password Check is not running.
  if (password_check_state_ != PasswordSafetyCheckState::kRunning) {
    return;
  }

  SetInsecurePasswordCounts(previous_insecure_password_counts_);

  SetPasswordCheckState(previous_password_check_state_);

  ignore_password_check_changes_ = true;

  password_check_manager_->StopPasswordCheck();
}

void IOSChromeSafetyCheckManager::StartUpdateChromeCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Do nothing if the Update Chrome check is already running.
  if (update_chrome_check_state_ == UpdateChromeSafetyCheckState::kRunning) {
    return;
  }

  ignore_omaha_changes_ = false;

  previous_update_chrome_check_state_ = update_chrome_check_state_;

  StartOmahaCheck();
}

void IOSChromeSafetyCheckManager::StopUpdateChromeCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (update_chrome_check_state_ != UpdateChromeSafetyCheckState::kRunning) {
    return;
  }

  SetUpdateChromeCheckState(previous_update_chrome_check_state_);

  ignore_omaha_changes_ = true;
}

void IOSChromeSafetyCheckManager::PasswordCheckStatusChanged(
    PasswordCheckState state) {
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &IOSChromeSafetyCheckManager::ConvertAndSetPasswordCheckState,
          weak_ptr_factory_.GetWeakPtr(), std::move(state)));
}

void IOSChromeSafetyCheckManager::InsecureCredentialsChanged() {
  // The insecure credentials list may have changed while the Password check is
  // running, so schedule a refresh of the Password Check Status.
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &IOSChromeSafetyCheckManager::RefreshOutdatedPasswordCheckState,
          weak_ptr_factory_.GetWeakPtr()));
}

void IOSChromeSafetyCheckManager::ManagerWillShutdown(
    IOSChromePasswordCheckManager* password_check_manager) {
  CHECK_EQ(password_check_manager, password_check_manager_);
  password_check_manager_->RemoveObserver(this);
  password_check_manager_ = nullptr;
}

void IOSChromeSafetyCheckManager::UpgradeRecommendedDetailsChanged(
    UpgradeRecommendedDetails details) {
  CHECK(IsOmahaServiceRefactorEnabled());

  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&IOSChromeSafetyCheckManager::HandleOmahaResponse,
                     weak_ptr_factory_.GetWeakPtr(), details));
}

void IOSChromeSafetyCheckManager::ServiceWillShutdown(
    OmahaService* omaha_service) {
  CHECK(IsOmahaServiceRefactorEnabled());

  omaha_service->RemoveObserver(this);
}

SafeBrowsingSafetyCheckState
IOSChromeSafetyCheckManager::GetSafeBrowsingCheckState() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return safe_browsing_check_state_;
}

PasswordSafetyCheckState IOSChromeSafetyCheckManager::GetPasswordCheckState()
    const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return password_check_state_;
}

password_manager::InsecurePasswordCounts
IOSChromeSafetyCheckManager::GetInsecurePasswordCounts() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return insecure_password_counts_;
}

UpdateChromeSafetyCheckState
IOSChromeSafetyCheckManager::GetUpdateChromeCheckState() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return update_chrome_check_state_;
}

// Returns the Chrome app upgrade URL for the App Store.
const GURL& IOSChromeSafetyCheckManager::GetChromeAppUpgradeUrl() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return upgrade_url_;
}

std::vector<password_manager::CredentialUIEntry>
IOSChromeSafetyCheckManager::GetInsecureCredentials() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return password_check_manager_->GetInsecureCredentials();
}

base::Time IOSChromeSafetyCheckManager::GetLastSafetyCheckRunTime() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return last_safety_check_run_time_;
}

// Returns the Chrome app next version.
std::string IOSChromeSafetyCheckManager::GetChromeAppNextVersion() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return next_version_;
}

// TODO(crbug.com/40922030): Add UMA logs related to the Safe Browsing check.
void IOSChromeSafetyCheckManager::SetSafeBrowsingCheckState(
    SafeBrowsingSafetyCheckState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (safe_browsing_check_state_ == state) {
    return;
  }

  safe_browsing_check_state_ = state;

  local_pref_service_->SetString(
      prefs::kIosSafetyCheckManagerSafeBrowsingCheckResult,
      NameForSafetyCheckState(state));

  for (auto& observer : observers_) {
    observer.SafeBrowsingCheckStateChanged(safe_browsing_check_state_);
  }
}

// TODO(crbug.com/40922030): Add UMA logs related to the Password check.
void IOSChromeSafetyCheckManager::ConvertAndSetPasswordCheckState(
    PasswordCheckState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // If the Password check reports the device is offline, propogate this
  // information to the Update Chrome check.
  if (state == PasswordCheckState::kOffline) {
    SetUpdateChromeCheckState(UpdateChromeSafetyCheckState::kNetError);
  }

  const std::vector<password_manager::CredentialUIEntry> insecure_credentials =
      password_check_manager_->GetInsecureCredentials();

  password_manager::InsecurePasswordCounts counts =
      password_manager::CountInsecurePasswordsPerInsecureType(
          insecure_credentials);

  SetInsecurePasswordCounts(counts);

  PasswordSafetyCheckState check_state = CalculatePasswordSafetyCheckState(
      state, insecure_credentials, password_check_state_);

  SetPasswordCheckState(check_state);
}

// TODO(crbug.com/40922030): Add UMA logs related to the Password check.
void IOSChromeSafetyCheckManager::RefreshOutdatedPasswordCheckState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  PasswordCheckState state = password_check_manager_->GetPasswordCheckState();

  // If the Password check reports the device is offline, propogate this
  // information to the Update Chrome check.
  if (state == PasswordCheckState::kOffline) {
    SetUpdateChromeCheckState(UpdateChromeSafetyCheckState::kNetError);
  }

  const std::vector<password_manager::CredentialUIEntry> insecure_credentials =
      password_check_manager_->GetInsecureCredentials();

  password_manager::InsecurePasswordCounts counts =
      password_manager::CountInsecurePasswordsPerInsecureType(
          insecure_credentials);

  SetInsecurePasswordCounts(counts);

  PasswordSafetyCheckState check_state = CalculatePasswordSafetyCheckState(
      state, insecure_credentials, password_check_state_);

  SetPasswordCheckState(check_state);
}

// TODO(crbug.com/40922030): Add UMA logs related to the Password check.
void IOSChromeSafetyCheckManager::SetPasswordCheckState(
    PasswordSafetyCheckState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (password_check_state_ == state || ignore_password_check_changes_) {
    return;
  }

  // Only log that there was a freshness signal if the new state has a different
  // end result (a password issue, safe).
  bool should_log_freshness =
      state != PasswordSafetyCheckState::kDefault &&
      state != previous_password_check_state_ &&
      (state == PasswordSafetyCheckState::kUnmutedCompromisedPasswords ||
       state == PasswordSafetyCheckState::kReusedPasswords ||
       state == PasswordSafetyCheckState::kWeakPasswords ||
       state == PasswordSafetyCheckState::kSafe);

  if (should_log_freshness) {
    RecordModuleFreshnessSignal(ContentSuggestionsModuleType::kSafetyCheck);
    base::UmaHistogramEnumeration(
        "IOS.SafetyCheck.FreshnessTrigger",
        IOSSafetyCheckFreshnessTrigger::kPasswordCheckStateChanged);
  }

  password_check_state_ = state;

  pref_service_->SetString(prefs::kIosSafetyCheckManagerPasswordCheckResult,
                           NameForSafetyCheckState(state));

  for (auto& observer : observers_) {
    observer.PasswordCheckStateChanged(password_check_state_,
                                       insecure_password_counts_);
  }

  RefreshSafetyCheckRunningState();
}

void IOSChromeSafetyCheckManager::SetInsecurePasswordCounts(
    password_manager::InsecurePasswordCounts counts) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (ignore_password_check_changes_) {
    return;
  }

  insecure_password_counts_ = counts;

  ScopedDictPrefUpdate insecure_password_counts_update(
      pref_service_, prefs::kIosSafetyCheckManagerInsecurePasswordCounts);

  insecure_password_counts_update->Set(kSafetyCheckCompromisedPasswordsCountKey,
                                       counts.compromised_count);
  insecure_password_counts_update->Set(kSafetyCheckDismissedPasswordsCountKey,
                                       counts.dismissed_count);
  insecure_password_counts_update->Set(kSafetyCheckReusedPasswordsCountKey,
                                       counts.reused_count);
  insecure_password_counts_update->Set(kSafetyCheckWeakPasswordsCountKey,
                                       counts.weak_count);
}

// TODO(crbug.com/40922030): Add UMA logs related to the Update Chrome check.
void IOSChromeSafetyCheckManager::SetUpdateChromeCheckState(
    UpdateChromeSafetyCheckState state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (update_chrome_check_state_ == state || ignore_omaha_changes_) {
    return;
  }

  // Only log that there was a freshness signal if the new state has a different
  // end result (out of date, up to date).
  bool should_log_freshness =
      state != UpdateChromeSafetyCheckState::kDefault &&
      state != previous_update_chrome_check_state_ &&
      (state == UpdateChromeSafetyCheckState::kOutOfDate ||
       state == UpdateChromeSafetyCheckState::kUpToDate);

  if (should_log_freshness) {
    RecordModuleFreshnessSignal(ContentSuggestionsModuleType::kSafetyCheck);
    base::UmaHistogramEnumeration(
        "IOS.SafetyCheck.FreshnessTrigger",
        IOSSafetyCheckFreshnessTrigger::kUpdateChromeCheckStateChanged);
  }

  update_chrome_check_state_ = state;

  local_pref_service_->SetString(prefs::kIosSafetyCheckManagerUpdateCheckResult,
                                 NameForSafetyCheckState(state));

  for (auto& observer : observers_) {
    observer.UpdateChromeCheckStateChanged(update_chrome_check_state_);
  }

  RefreshSafetyCheckRunningState();
}

// TODO(crbug.com/40922030): Add UMA logs related to the Update Chrome check.
void IOSChromeSafetyCheckManager::SetUpdateChromeDetails(
    GURL upgrade_url,
    std::string next_version) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (ignore_omaha_changes_) {
    return;
  }

  upgrade_url_ = upgrade_url;
  next_version_ = next_version;
}

// TODO(crbug.com/40922030): Add UMA logs related to the Safe Browsing check.
void IOSChromeSafetyCheckManager::UpdateSafeBrowsingCheckState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (pref_service_->IsManagedPreference(prefs::kSafeBrowsingEnabled)) {
    SetSafeBrowsingCheckState(SafeBrowsingSafetyCheckState::kManaged);
  } else if (pref_service_->GetBoolean(prefs::kSafeBrowsingEnabled)) {
    SetSafeBrowsingCheckState(SafeBrowsingSafetyCheckState::kSafe);
  } else {
    SetSafeBrowsingCheckState(SafeBrowsingSafetyCheckState::kUnsafe);
  }
}

// TODO(crbug.com/40922030): Add UMA logs related to the Update Chrome check.
void IOSChromeSafetyCheckManager::StartOmahaCheck() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  SetUpdateChromeCheckState(UpdateChromeSafetyCheckState::kRunning);

  // Only make Omaha requests on the proper channels.
  switch (::GetChannel()) {
    case version_info::Channel::STABLE:
    case version_info::Channel::BETA:
    case version_info::Channel::DEV:
    case version_info::Channel::CANARY: {
      OmahaService::CheckNow(
          base::BindOnce(&IOSChromeSafetyCheckManager::HandleOmahaResponse,
                         weak_ptr_factory_.GetWeakPtr()));
      break;
    }
    default:
      break;
  }

  // If the Omaha response isn't received after `kOmahaNetworkWaitTime`,
  // consider this an Omaha failure.
  task_runner_->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&IOSChromeSafetyCheckManager::HandleOmahaError,
                     weak_ptr_factory_.GetWeakPtr()),
      kOmahaNetworkWaitTime);
}

// NOTE: In a fast-follow CL, this method may expand to store Omaha results
// in Prefs or NSUserDefaults.
//
// For now, all Omaha data will be maintained in-memory and tied to the
// lifecycle of the this class.
void IOSChromeSafetyCheckManager::HandleOmahaResponse(
    UpgradeRecommendedDetails details) {
  UpdateChromeSafetyCheckState state = UpdateChromeSafetyCheckState::kDefault;

  if (details.is_up_to_date) {
    state = UpdateChromeSafetyCheckState::kUpToDate;
  } else if (!details.upgrade_url.is_valid() || details.next_version.empty() ||
             !base::Version(details.next_version).IsValid()) {
    state = UpdateChromeSafetyCheckState::kOmahaError;
  } else {
    state = UpdateChromeSafetyCheckState::kOutOfDate;
  }

  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&IOSChromeSafetyCheckManager::SetUpdateChromeCheckState,
                     weak_ptr_factory_.GetWeakPtr(), std::move(state)));

  if (details.upgrade_url.is_valid() && !details.next_version.empty()) {
    GURL upgrade_url = details.upgrade_url;
    std::string next_version = details.next_version;

    task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&IOSChromeSafetyCheckManager::SetUpdateChromeDetails,
                       weak_ptr_factory_.GetWeakPtr(), std::move(upgrade_url),
                       std::move(next_version)));
  }
}

// TODO(crbug.com/40922030): Add UMA logs related to the Update Chrome check.
void IOSChromeSafetyCheckManager::HandleOmahaError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (update_chrome_check_state_ == UpdateChromeSafetyCheckState::kRunning) {
    SetUpdateChromeCheckState(UpdateChromeSafetyCheckState::kOmahaError);
  }
}

void IOSChromeSafetyCheckManager::RefreshSafetyCheckRunningState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  RunningSafetyCheckState current_state = running_safety_check_state_;

  const bool running_checks =
      safe_browsing_check_state_ == SafeBrowsingSafetyCheckState::kRunning ||
      password_check_state_ == PasswordSafetyCheckState::kRunning ||
      update_chrome_check_state_ == UpdateChromeSafetyCheckState::kRunning;

  RunningSafetyCheckState new_state = running_checks
                                          ? RunningSafetyCheckState::kRunning
                                          : RunningSafetyCheckState::kDefault;

  // Do nothing if the current and new states match, i.e. there's no need to
  // notify observers that nothing has changed.
  if (current_state == new_state) {
    return;
  }

  running_safety_check_state_ = new_state;

  for (auto& observer : observers_) {
    observer.RunningStateChanged(running_safety_check_state_);
  }
}

void IOSChromeSafetyCheckManager::LogCurrentSafetyCheckRunTime() {
  base::Time now = base::Time::Now();

  last_safety_check_run_time_ = now;

  local_pref_service_->SetTime(prefs::kIosSafetyCheckManagerLastRunTime, now);
}

void IOSChromeSafetyCheckManager::AddObserver(
    IOSChromeSafetyCheckManagerObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  observers_.AddObserver(observer);
}

void IOSChromeSafetyCheckManager::RemoveObserver(
    IOSChromeSafetyCheckManagerObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  observers_.RemoveObserver(observer);
}

void IOSChromeSafetyCheckManager::StartOmahaCheckForTesting() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  StartOmahaCheck();
}

RunningSafetyCheckState
IOSChromeSafetyCheckManager::GetRunningCheckStateForTesting() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return running_safety_check_state_;
}

void IOSChromeSafetyCheckManager::SetPasswordCheckStateForTesting(
    PasswordSafetyCheckState state) {
  SetPasswordCheckState(state);
}

void IOSChromeSafetyCheckManager::SetInsecurePasswordCountsForTesting(
    password_manager::InsecurePasswordCounts counts) {
  SetInsecurePasswordCounts(counts);
}

void IOSChromeSafetyCheckManager::InsecureCredentialsChangedForTesting() {
  InsecureCredentialsChanged();
}

void IOSChromeSafetyCheckManager::PasswordCheckStatusChangedForTesting(
    PasswordCheckState state) {
  PasswordCheckStatusChanged(state);
}

void IOSChromeSafetyCheckManager::RestorePreviousSafetyCheckStateForTesting() {
  RestorePreviousSafetyCheckState();
}