chromium/chrome/browser/upgrade_detector/upgrade_detector_chromeos.cc

// Copyright 2012 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/upgrade_detector/upgrade_detector_chromeos.h"

#include <stdint.h>

#include <algorithm>
#include <optional>

#include "ash/constants/ash_features.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/upgrade_detector/build_state.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/dbus/update_engine/update_engine_client.h"
#include "chromeos/ash/components/settings/timezone_settings.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"

namespace {

using ::ash::UpdateEngineClient;

// How long to wait (each cycle) before checking which severity level we should
// be at. Once we reach the highest severity, the timer will stop.
constexpr base::TimeDelta kNotifyCycleDelta = base::Minutes(20);

// The default amount of time it takes for the detector's annoyance level
// (upgrade_notification_stage()) to reach UPGRADE_ANNOYANCE_HIGH once an
// upgrade is detected.
constexpr base::TimeDelta kDefaultHighThreshold = base::Days(7);

// The default amount of time it takes for the detector's annoyance level
// (upgrade_notification_stage()) to reach UPGRADE_ANNOYANCE_ELEVATED once an
// upgrade is detected.
constexpr base::TimeDelta kDefaultElevatedThreshold = base::Days(4);

// The default amount of time between the detector's annoyance level change
// from UPGRADE_ANNOYANCE_ELEVATED to UPGRADE_ANNOYANCE_HIGH.
constexpr base::TimeDelta kDefaultHeadsUpPeriod =
    kDefaultHighThreshold - kDefaultElevatedThreshold;

}  // namespace

UpgradeDetectorChromeos::UpgradeDetectorChromeos(
    const base::Clock* clock,
    const base::TickClock* tick_clock)
    : UpgradeDetector(clock, tick_clock),
      upgrade_notification_timer_(tick_clock),
      initialized_(false),
      toggled_update_flag_(false),
      update_in_progress_(false) {}

UpgradeDetectorChromeos::~UpgradeDetectorChromeos() {}

// static
void UpgradeDetectorChromeos::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(prefs::kRelaunchHeadsUpPeriod,
                                kDefaultHeadsUpPeriod.InMilliseconds());
}

void UpgradeDetectorChromeos::Init() {
  UpgradeDetector::Init();
  MonitorPrefChanges(prefs::kRelaunchHeadsUpPeriod);
  MonitorPrefChanges(prefs::kRelaunchNotification);
  UpdateEngineClient::Get()->AddObserver(this);
  auto* const build_state = g_browser_process->GetBuildState();
  build_state->AddObserver(this);
  installed_version_updater_.emplace(build_state);
  initialized_ = true;
}

void UpgradeDetectorChromeos::Shutdown() {
  // Init() may not be called from tests.
  if (!initialized_)
    return;
  installed_version_updater_.reset();
  g_browser_process->GetBuildState()->RemoveObserver(this);
  UpdateEngineClient::Get()->RemoveObserver(this);
  upgrade_notification_timer_.Stop();
  UpgradeDetector::Shutdown();
  initialized_ = false;
}

base::Time UpgradeDetectorChromeos::GetAnnoyanceLevelDeadline(
    UpgradeNotificationAnnoyanceLevel level) {
  const base::Time detected_time = upgrade_detected_time();
  if (detected_time.is_null())
    return detected_time;
  switch (level) {
    case UpgradeDetector::UPGRADE_ANNOYANCE_NONE:
    case UpgradeDetector::UPGRADE_ANNOYANCE_VERY_LOW:
    case UpgradeDetector::UPGRADE_ANNOYANCE_LOW:
      return detected_time;
    case UpgradeDetector::UPGRADE_ANNOYANCE_ELEVATED:
      return elevated_deadline_;
    case UpgradeDetector::UPGRADE_ANNOYANCE_GRACE:
      return grace_deadline_;
    case UpgradeDetector::UPGRADE_ANNOYANCE_HIGH:
      return high_deadline_;
    case UpgradeDetector::UPGRADE_ANNOYANCE_CRITICAL:
      return upgrade_notification_stage() == UPGRADE_ANNOYANCE_CRITICAL
                 ? detected_time
                 : base::Time();
  }
}

void UpgradeDetectorChromeos::OverrideHighAnnoyanceDeadline(
    base::Time deadline) {
  DCHECK(!upgrade_detected_time().is_null());
  if (deadline > upgrade_detected_time()) {
    high_deadline_override_ = deadline;
    CalculateDeadlines();
    NotifyOnUpgrade();
  }
}

void UpgradeDetectorChromeos::ResetOverriddenDeadline() {
  if (high_deadline_override_.is_null())
    return;

  DCHECK(!upgrade_detected_time().is_null());
  high_deadline_override_ = base::Time();
  CalculateDeadlines();
  NotifyOnUpgrade();
}

void UpgradeDetectorChromeos::OnUpdate(const BuildState* build_state) {
  if (build_state->update_type() == BuildState::UpdateType::kNone) {
    // If the update state changed to `kNone`, reset the state as there is no
    // longer a valid update.
    upgrade_notification_timer_.Stop();
    set_upgrade_available(UPGRADE_AVAILABLE_NONE);
    set_upgrade_detected_time(base::Time());
  } else if (upgrade_detected_time().is_null()) {
    // Only start the timer if the build state is valid.
    set_upgrade_detected_time(clock()->Now());
    CalculateDeadlines();
  }

  update_in_progress_ = false;
  set_is_rollback(build_state->update_type() ==
                  BuildState::UpdateType::kEnterpriseRollback);
  set_is_factory_reset_required(build_state->update_type() ==
                                BuildState::UpdateType::kChannelSwitchRollback);
  NotifyOnUpgrade();
}

// static
base::TimeDelta UpgradeDetectorChromeos::GetRelaunchHeadsUpPeriod() {
  // Not all tests provide a PrefService for local_state().
  auto* local_state = g_browser_process->local_state();
  if (!local_state)
    return base::TimeDelta();
  const auto* preference =
      local_state->FindPreference(prefs::kRelaunchHeadsUpPeriod);
  const int value = preference->GetValue()->GetInt();
  // Enforce the preference's documented minimum value.
  static constexpr base::TimeDelta kMinValue = base::Hours(1);
  if (preference->IsDefaultValue() || value < kMinValue.InMilliseconds())
    return base::TimeDelta();
  return base::Milliseconds(value);
}

void UpgradeDetectorChromeos::CalculateDeadlines() {
  base::TimeDelta notification_period = GetRelaunchNotificationPeriod();
  if (notification_period.is_zero())
    notification_period = kDefaultHighThreshold;

  const RelaunchWindow relaunch_window =
      GetRelaunchWindowPolicyValue().value_or(GetDefaultRelaunchWindow());
  high_deadline_ = AdjustDeadline(upgrade_detected_time() + notification_period,
                                  relaunch_window);

  base::TimeDelta heads_up_period = GetRelaunchHeadsUpPeriod();
  if (heads_up_period.is_zero())
    heads_up_period = kDefaultHeadsUpPeriod;
  elevated_deadline_ =
      std::max(high_deadline_ - heads_up_period, upgrade_detected_time());

  base::TimeDelta grace_period =
      GetGracePeriod(high_deadline_ - elevated_deadline_);
  grace_deadline_ = high_deadline_ - grace_period;

  if (!high_deadline_override_.is_null() &&
      high_deadline_ > high_deadline_override_) {
    elevated_deadline_ = upgrade_detected_time();
    high_deadline_ = std::max(elevated_deadline_, high_deadline_override_);
    grace_period = GetGracePeriod(high_deadline_ - elevated_deadline_);
    grace_deadline_ = high_deadline_ - grace_period;
  }
  DCHECK(grace_deadline_ >= elevated_deadline_);
}

void UpgradeDetectorChromeos::UpdateStatusChanged(
    const update_engine::StatusResult& status) {
  if (status.current_operation() ==
      update_engine::Operation::NEED_PERMISSION_TO_UPDATE) {
    // Update engine broadcasts this state only when update is available but
    // downloading over cellular connection requires user's agreement.
    NotifyUpdateOverCellularAvailable();
  } else if (status.current_operation() ==
             update_engine::Operation::UPDATED_BUT_DEFERRED) {
    // Update engine broadcasts this state when update is downloaded but
    // deferred.
    NotifyUpdateDeferred(/*use_notification=*/false);
    // Start timer for notification.
    upgrade_notification_timer_.Start(
        FROM_HERE, kDefaultHighThreshold, this,
        &UpgradeDetectorChromeos::NotifyOnDeferredUpgrade);
  } else if (!update_in_progress_ &&
             status.current_operation() ==
                 update_engine::Operation::DOWNLOADING) {
    update_in_progress_ = true;
    if (!upgrade_detected_time().is_null())
      NotifyOnUpgrade();
  }
  // TODO(b/219067273): Cleanup toggling from ash into platform code.
  if (!toggled_update_flag_) {
    // Only send feature flag status one time.
    toggled_update_flag_ = true;
    UpdateEngineClient::Get()->ToggleFeature(
        update_engine::kFeatureRepeatedUpdates,
        base::FeatureList::IsEnabled(ash::features::kAllowRepeatedUpdates));
  }
}

void UpgradeDetectorChromeos::OnUpdateOverCellularOneTimePermissionGranted() {
  NotifyUpdateOverCellularOneTimePermissionGranted();
}

void UpgradeDetectorChromeos::OnMonitoredPrefsChanged() {
  // Check the current stage and potentially notify observers now if a change to
  // the observed policies results in changes to the thresholds.
  if (upgrade_detected_time().is_null())
    return;
  const base::Time old_elevated_deadline = elevated_deadline_;
  const base::Time old_high_deadline = high_deadline_;
  CalculateDeadlines();
  if (elevated_deadline_ != old_elevated_deadline ||
      high_deadline_ != old_high_deadline) {
    NotifyOnUpgrade();
  }
}

void UpgradeDetectorChromeos::NotifyOnUpgrade() {
  const base::Time current_time = clock()->Now();
  // The delay from now until the next highest notification stage is reached, or
  // zero if the highest notification stage has been reached.
  base::TimeDelta next_notify_call;

  const auto last_stage = upgrade_notification_stage();
  // These if statements must be sorted (highest interval first).
  // Update notifications are pinned. These levels are used to either show the
  // notification for the first time, or upgrade it if it already exists.
  // The stages of notification for regular updates go as follows:
  // Upgrade time -> quiet period -> notifications should start appearing
  // (elevated deadline) -> the highest is almost reached (grace deadline) ->
  // highest stage (high deadline). No more new notifications from this point.
  // Rollback and other powerwashing updates should ignore the "quiet period"
  // and start notifying since the moment the update is available.
  // If RelaunchNotification policy is not set, the user should also be notified
  // immediately without waiting to reach "elevated deadline".
  if (update_in_progress_) {
    // Cancel any notification of a previous update (if there was one) while a
    // new update is being downloaded.
    set_upgrade_notification_stage(UPGRADE_ANNOYANCE_NONE);
  } else if (upgrade_detected_time().is_null()) {
    // There is no update.
    set_upgrade_notification_stage(UPGRADE_ANNOYANCE_NONE);
  } else if (current_time >= high_deadline_) {
    // The highest notification stage is reached.
    set_upgrade_notification_stage(UPGRADE_ANNOYANCE_HIGH);
  } else if (current_time >= grace_deadline_) {
    // The notification stage is increased and almost reached the highest stage.
    set_upgrade_notification_stage(UPGRADE_ANNOYANCE_GRACE);
    next_notify_call = high_deadline_ - current_time;
  } else if (current_time >= elevated_deadline_) {
    // The notification stage is increased from quiet time. Notifications will
    // start appearing for regular updates.
    set_upgrade_notification_stage(UPGRADE_ANNOYANCE_ELEVATED);
    next_notify_call = grace_deadline_ - current_time;
  } else {
    // We are in "quiet period".
    // The user should not be notified if the policy is set unless the update is
    // a rollback or a powerwash update.
    // Rollback and powerwash updates should always be notified immediately.
    if (!IsRelaunchNotificationPolicyEnabled() || is_rollback() ||
        is_factory_reset_required()) {
      // UPGRADE_ANNOYANCE_LOW allows to show a notification immediately without
      // interfering with the rest of the logic related to the policy.
      set_upgrade_notification_stage(UPGRADE_ANNOYANCE_LOW);
    } else {
      // Notifications are delayed.
      set_upgrade_notification_stage(UPGRADE_ANNOYANCE_NONE);
    }
    // The stage will change when "elevated deadline" is reached.
    next_notify_call = elevated_deadline_ - current_time;
  }
  const auto new_stage = upgrade_notification_stage();

  if (!next_notify_call.is_zero()) {
    // Schedule the next wakeup in 20 minutes or when the next change to the
    // notification stage should take place.
    upgrade_notification_timer_.Start(
        FROM_HERE, std::min(next_notify_call, kNotifyCycleDelta), this,
        &UpgradeDetectorChromeos::NotifyOnUpgrade);
  } else if (upgrade_notification_timer_.IsRunning()) {
    // Explicitly stop the timer in case this call is due to a stage change that
    // brought the instance up to the "high" annoyance level.
    upgrade_notification_timer_.Stop();
  }

  // Issue a notification if the stage is above "none" or if it's dropped down
  // to "none" from something higher.
  if (new_stage != UPGRADE_ANNOYANCE_NONE ||
      last_stage != UPGRADE_ANNOYANCE_NONE) {
    NotifyUpgrade();
  }
}

void UpgradeDetectorChromeos::NotifyOnDeferredUpgrade() {
  upgrade_notification_timer_.Stop();
  NotifyUpdateDeferred(/*use_notification=*/true);
}

// static
UpgradeDetectorChromeos* UpgradeDetectorChromeos::GetInstance() {
  static base::NoDestructor<UpgradeDetectorChromeos> instance(
      base::DefaultClock::GetInstance(), base::DefaultTickClock::GetInstance());
  return instance.get();
}

// static
UpgradeDetector* UpgradeDetector::GetInstance() {
  return UpgradeDetectorChromeos::GetInstance();
}

// static
base::TimeDelta UpgradeDetector::GetDefaultHighAnnoyanceThreshold() {
  return kDefaultHighThreshold;
}

// static
base::TimeDelta UpgradeDetector::GetDefaultElevatedAnnoyanceThreshold() {
  return kDefaultElevatedThreshold;
}

// static
UpgradeDetector::RelaunchWindow UpgradeDetector::GetDefaultRelaunchWindow() {
  // Two hours starting at 2am.
  return RelaunchWindow(/*start_hour=*/2, /*start_minute=*/0, base::Hours(2));
}