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

#include <algorithm>
#include <vector>

#include "base/check.h"
#include "base/command_line.h"
#include "base/debug/alias.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/rand_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/clock.h"
#include "base/time/tick_clock.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/ui/browser_otr_state.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/upgrade_detector/version_history_client.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/network_time/network_time_tracker.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "ui/base/idle/idle.h"

namespace {

// How long to wait between checks for whether the user has been idle.
constexpr int kIdleRepeatingTimerWait = 10;  // Minutes (seconds if testing).

// How much idle time (since last input even was detected) must have passed
// until we notify that a critical update has occurred.
constexpr int kIdleAmount = 2;  // Hours (or seconds, if testing).

// Maximum duration for a relaunch window.
constexpr base::TimeDelta kRelaunchWindowMaxDuration = base::Hours(24);

// The default amount of time between the detector's annoyance level change
// from UPGRADE_ANNOYANCE_GRACE to UPGRADE_ANNOYANCE_HIGH.
constexpr auto kDefaultGracePeriod = base::Hours(1);

bool UseTestingIntervals() {
  // If a command line parameter specifying how long the upgrade check should
  // be, we assume it is for testing and switch to using seconds instead of
  // hours.
  const base::CommandLine& cmd_line = *base::CommandLine::ForCurrentProcess();
  return !cmd_line.GetSwitchValueASCII(switches::kCheckForUpdateIntervalSec)
              .empty();
}

// Returns the start time of the relaunch window on the day of `time`.
base::Time ComputeRelaunchWindowStartForDay(
    const UpgradeDetector::RelaunchWindow& window,
    base::Time time) {
  base::Time::Exploded window_start_exploded;
  time.LocalExplode(&window_start_exploded);
  window_start_exploded.hour = window.hour;
  window_start_exploded.minute = window.minute;
  window_start_exploded.second = 0;
  window_start_exploded.millisecond = 0;
  base::Time window_start;
  if (!base::Time::FromLocalExploded(window_start_exploded, &window_start)) {
    // The start time doesn't exist on that day; likely due to a TZ change at
    // that precise moment (e.g., `window` is 02:00 for zones that observe DST
    // changes at 2am local time). Move forward/backward by one hour and try
    // again. As of this writing, Australia/Lord_Howe is the only zone that
    // doesn't change by one full hour on transitions. Meaning no disrespect to
    // its residents, it is simpler to be fuzzy for that one timezone than to be
    // absolutely accurate.
    if (window_start_exploded.hour < 23) {
      ++window_start_exploded.hour;
    } else {
      --window_start_exploded.hour;
    }

    // The adjusted time could still fail `Time::FromLocalExploded`. This
    // happens on ARM devices in ChromeOS. Once it happens, it could be sticky
    // and creates a crash loop. Return the unadjusted time in this case.
    // See http://crbug.com/40828727
    if (!base::Time::FromLocalExploded(window_start_exploded, &window_start)) {
      LOG(ERROR) << "FromLocalExploded failed with time=" << time
                 << ", now=" << base::Time::Now()
                 << ", year=" << window_start_exploded.year
                 << ", month=" << window_start_exploded.month
                 << ", day=" << window_start_exploded.day_of_month;

      base::debug::Alias(&window_start_exploded);

      // Dump once per chrome run.
      static bool dumped = false;
      if (!dumped) {
        dumped = base::debug::DumpWithoutCrashing();
      }

      return time;
    }
  }
  return window_start;
}

// Returns the value of the RelaunchFastIfOutdated policy, or a zero TimeDelta
// if the policy is unset.
base::TimeDelta GetRelaunchFastIfOutdated() {
  auto* local_state = g_browser_process->local_state();
  if (!local_state) {
    return base::TimeDelta();
  }
  return base::Days(
      local_state->GetInteger(prefs::kRelaunchFastIfOutdated));
}

}  // namespace

// static
void UpgradeDetector::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(prefs::kAttemptedToEnableAutoupdate, false);
}

void UpgradeDetector::Init() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Not all tests provide a PrefService for local_state().
  PrefService* local_state = g_browser_process->local_state();
  if (local_state) {
    pref_change_registrar_.Init(local_state);
    MonitorPrefChanges(prefs::kRelaunchNotificationPeriod);
    MonitorPrefChanges(prefs::kRelaunchWindow);
    MonitorPrefChanges(prefs::kRelaunchFastIfOutdated);
  }
}

void UpgradeDetector::Shutdown() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  weak_factory_.InvalidateWeakPtrs();
  pref_change_task_pending_ = false;
  idle_check_timer_.Stop();
  pref_change_registrar_.Reset();
}

void UpgradeDetector::OverrideRelaunchNotificationToRequired(bool overridden) {
  NotifyRelaunchOverriddenToRequired(overridden);
}

void UpgradeDetector::AddObserver(UpgradeObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observer_list_.AddObserver(observer);
}

void UpgradeDetector::RemoveObserver(UpgradeObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observer_list_.RemoveObserver(observer);
}

void UpgradeDetector::NotifyOutdatedInstall() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnOutdatedInstall();
}

void UpgradeDetector::NotifyOutdatedInstallNoAutoUpdate() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnOutdatedInstallNoAutoUpdate();
}

void UpgradeDetector::NotifyUpgradeForTesting() {
  NotifyUpgrade();
}

UpgradeDetector::UpgradeDetector(const base::Clock* clock,
                                 const base::TickClock* tick_clock)
    : clock_(clock),
      tick_clock_(tick_clock),
      upgrade_available_(UPGRADE_AVAILABLE_NONE),
      best_effort_experiment_updates_available_(false),
      critical_experiment_updates_available_(false),
      critical_update_acknowledged_(false),
      idle_check_timer_(tick_clock_),
      upgrade_notification_stage_(UPGRADE_ANNOYANCE_NONE),
      notify_upgrade_(false) {}

UpgradeDetector::~UpgradeDetector() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Ensure that Shutdown() was called.
  DCHECK(pref_change_registrar_.IsEmpty());
}

void UpgradeDetector::MonitorPrefChanges(const std::string& pref) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Not all tests provide a PrefService to be monitored.
  if (pref_change_registrar_.prefs()) {
    // base::Unretained is safe here because |this| outlives the registrar.
    pref_change_registrar_.Add(
        pref, base::BindRepeating(&UpgradeDetector::OnRelaunchPrefChanged,
                                  base::Unretained(this)));
  }
}

// static
base::TimeDelta UpgradeDetector::GetRelaunchNotificationPeriod() {
  // 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::kRelaunchNotificationPeriod);
  const int value = preference->GetValue()->GetInt();
  // Enforce the preference's documented minimum value.
  constexpr base::TimeDelta kMinValue = base::Hours(1);
  if (preference->IsDefaultValue() || value < kMinValue.InMilliseconds())
    return base::TimeDelta();
  return base::Milliseconds(value);
}

// static
bool UpgradeDetector::IsRelaunchNotificationPolicyEnabled() {
  // Not all tests provide a PrefService for local_state().
  auto* local_state = g_browser_process->local_state();
  if (!local_state)
    return false;

  // "Chrome menu only" means that the policy is disabled.
  constexpr int kChromeMenuOnly = 0;
  return local_state->GetInteger(prefs::kRelaunchNotification) !=
         kChromeMenuOnly;
}

// static
base::Time UpgradeDetector::AdjustDeadline(base::Time deadline,
                                           const RelaunchWindow& window) {
  DCHECK(window.IsValid());
  const base::TimeDelta duration = window.duration;

  // Window duration greater than equal to 24 hours means window covers the
  // whole day, so no need to adjust.
  if (duration >= kRelaunchWindowMaxDuration)
    return deadline;

  // Compute the window on the day of the deadline.
  const base::Time window_start =
      ComputeRelaunchWindowStartForDay(window, deadline);

  if (deadline >= window_start + duration) {
    // Push the deadline forward into a random interval in the next day's
    // window. The next day may be 25, 24 or 23 hours in the future. Take a stab
    // at 24 hours (the norm) and retry once if needed.
    base::Time next_window_start =
        ComputeRelaunchWindowStartForDay(window, deadline + base::Hours(24));
    if (next_window_start == window_start) {
      // The clocks must be set back, yielding a longer day. For example, 24
      // hours after a deadline of 00:30 could be at 23:30 on the same day due
      // to a DST change in the interim that sets clocks backward by one hour.
      // Try again. Use 26 rather than 25 in case some jurisdiction decides to
      // implement a shift of greater than 1 hour.
      next_window_start =
          ComputeRelaunchWindowStartForDay(window, deadline + base::Hours(26));
    } else if (next_window_start - window_start >= base::Hours(26)) {
      // The clocks must be set forward, yielding a shorter day, and we jumped
      // two days rather than one. For example, 24 hours after a deadline of
      // 23:30 could be at 00:30 two days later due to a DST change in the
      // interim that sets clocks forward by one hour". Try again.
      next_window_start =
          ComputeRelaunchWindowStartForDay(window, deadline + base::Hours(23));
    }
    return next_window_start + base::RandTimeDeltaUpTo(duration);
  }

  // Is the deadline within this day's window?
  if (deadline >= window_start)
    return deadline;

  // Compute the relaunch window starting on the day prior to the deadline for
  // cases where the relaunch window straddles midnight.
  base::Time prev_window_start =
      ComputeRelaunchWindowStartForDay(window, deadline - base::Hours(24));
  // The above cases do not apply here:
  // a) Previous day window jumped two days rather than one - This could arise
  // if, for example, 24 hours before the deadline of 00:30 is 23:30 two days
  // ago due to a DST change in the interim that set clocks forward by one hour.
  // But then 00:30 would actually mean 01:30 this day which would mean 00:30 on
  // the previous day. b) Previous day window on the same day - This could arise
  // if, for example, 24 hours before the deadline of 23:30 is 00:30 on the same
  // day due to clocks set back at by one hour. This is already covered in the
  // above condition `deadline >= window_start`.
  if (deadline < prev_window_start + duration)
    return deadline;

  // The deadline is after previous day's window. Push the deadline forward into
  // a random interval in the day's window.
  return window_start + base::RandTimeDeltaUpTo(duration);
}

// static
std::optional<UpgradeDetector::RelaunchWindow>
UpgradeDetector::GetRelaunchWindowPolicyValue() {
  // Not all tests provide a PrefService for local_state().
  auto* local_state = g_browser_process->local_state();
  if (!local_state)
    return std::nullopt;

  const auto* preference = local_state->FindPreference(prefs::kRelaunchWindow);
  DCHECK(preference);
  if (preference->IsDefaultValue())
    return std::nullopt;

  const base::Value* policy_value = preference->GetValue();
  DCHECK(policy_value->is_dict());

  const base::ListValue* entries = policy_value->GetDict().FindList("entries");
  if (!entries || entries->empty()) {
    return std::nullopt;
  }

  // Currently only single daily window is supported.
  const auto& window = entries->front().GetDict();
  const std::optional<int> hour = window.FindIntByDottedPath("start.hour");
  const std::optional<int> minute = window.FindIntByDottedPath("start.minute");
  const std::optional<int> duration_mins = window.FindInt("duration_mins");

  if (!hour || !minute || !duration_mins)
    return std::nullopt;

  return RelaunchWindow(hour.value(), minute.value(),
                        base::Minutes(duration_mins.value()));
}

// static
base::TimeDelta UpgradeDetector::GetGracePeriod(
    base::TimeDelta elevated_to_high_delta) {
  return std::min(kDefaultGracePeriod, elevated_to_high_delta / 2);
}

bool UpgradeDetector::GetNetworkTimeWithFallback(base::Time& current_time) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (g_browser_process->network_time_tracker()->GetNetworkTime(
          &current_time, /*uncertainty=*/nullptr) ==
      network_time::NetworkTimeTracker::NETWORK_TIME_AVAILABLE) {
    return true;
  }
  // When network time has not been initialized yet, simply rely on the
  // machine's current time.
  current_time = base::Time::Now();
  return false;
}

// static
bool UpgradeDetector::ShouldRelaunchFast() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!last_served_date_.has_value()) {
    return false;
  }
  base::TimeDelta max_age = GetRelaunchFastIfOutdated();
  if (max_age.is_zero()) {
    return false;
  }
  base::Time current_time;
  GetNetworkTimeWithFallback(current_time);
  return current_time - last_served_date_.value() > max_age;
}

bool UpgradeDetector::ShouldFetchLastServedDate() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return !fetched_last_served_date_ &&
         !GetRelaunchFastIfOutdated().is_zero();
}

void UpgradeDetector::FetchLastServedDate() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (fetched_last_served_date_) {
    // Only do it once.
    return;
  }
  fetched_last_served_date_ = true;
  GetLastServedDate(version_info::GetVersion(),
                    base::BindOnce(&UpgradeDetector::OnGotLastServedDate,
                                   weak_factory_.GetWeakPtr()));
}

void UpgradeDetector::NotifyUpgrade() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // An implementation will request that a notification be sent after dropping
  // back to the "none" annoyance level if the RelaunchNotificationPeriod
  // setting changes to a large enough value such that none of the revised
  // thresholds have been hit. In this case, consumers should not perceive that
  // an upgrade is available when checking notify_upgrade(). In practice, this
  // is only the case on desktop Chrome and not Chrome OS, where the lowest
  // threshold is hit the moment the upgrade is detected.
  notify_upgrade_ = upgrade_notification_stage_ != UPGRADE_ANNOYANCE_NONE;

  NotifyUpgradeRecommended();
  if (upgrade_available_ == UPGRADE_NEEDED_OUTDATED_INSTALL) {
    NotifyOutdatedInstall();
  } else if (upgrade_available_ == UPGRADE_NEEDED_OUTDATED_INSTALL_NO_AU) {
    NotifyOutdatedInstallNoAutoUpdate();
  } else if (upgrade_available_ == UPGRADE_AVAILABLE_CRITICAL ||
             critical_experiment_updates_available_) {
    TriggerCriticalUpdate();
  }
}

void UpgradeDetector::NotifyUpgradeRecommended() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnUpgradeRecommended();
}

void UpgradeDetector::NotifyCriticalUpgradeInstalled() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnCriticalUpgradeInstalled();
}

void UpgradeDetector::NotifyUpdateDeferred(bool use_notification) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnUpdateDeferred(use_notification);
}

void UpgradeDetector::NotifyUpdateOverCellularAvailable() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnUpdateOverCellularAvailable();
}

void UpgradeDetector::NotifyUpdateOverCellularOneTimePermissionGranted() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnUpdateOverCellularOneTimePermissionGranted();
}

void UpgradeDetector::NotifyRelaunchOverriddenToRequired(bool overridden) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (observer_list_.empty())
    return;

  for (auto& observer : observer_list_)
    observer.OnRelaunchOverriddenToRequired(overridden);
}

void UpgradeDetector::TriggerCriticalUpdate() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const base::TimeDelta idle_timer =
      UseTestingIntervals() ? base::Seconds(kIdleRepeatingTimerWait)
                            : base::Minutes(kIdleRepeatingTimerWait);
  idle_check_timer_.Start(FROM_HERE, idle_timer, this,
                          &UpgradeDetector::CheckIdle);
}

void UpgradeDetector::CheckIdle() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Don't proceed while an off-the-record or Guest window is open. The timer
  // will still keep firing, so this function will get a chance to re-evaluate
  // this.
  if (IsOffTheRecordSessionActive() ||
      GlobalBrowserCollection::GetInstance()->GetGuestBrowserCount()) {
    return;
  }

  // CalculateIdleState expects an interval in seconds.
  int idle_time_allowed =
      UseTestingIntervals() ? kIdleAmount : kIdleAmount * 60 * 60;

  ui::IdleState state = ui::CalculateIdleState(idle_time_allowed);

  switch (state) {
    case ui::IDLE_STATE_LOCKED:
      // Computer is locked, auto-restart.
      idle_check_timer_.Stop();
      chrome::AttemptRestart();
      break;
    case ui::IDLE_STATE_IDLE:
      // Computer has been idle for long enough, show warning.
      idle_check_timer_.Stop();
      NotifyCriticalUpgradeInstalled();
      break;
    case ui::IDLE_STATE_ACTIVE:
    case ui::IDLE_STATE_UNKNOWN:
      break;
  }
}

void UpgradeDetector::OnRelaunchPrefChanged() {
  // Coalesce simultaneous changes to multiple prefs into a single call to the
  // implementation's RecomputeSchedule method by making the call in a
  // task that will run after processing returns to the main event loop.
  if (pref_change_task_pending_)
    return;

  pref_change_task_pending_ = true;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(
                     [](base::WeakPtr<UpgradeDetector> weak_this) {
                       if (weak_this) {
                         weak_this->pref_change_task_pending_ = false;
                         weak_this->RecomputeSchedule();
                       }
                     },
                     weak_factory_.GetWeakPtr()));
}

void UpgradeDetector::OnGotLastServedDate(
    std::optional<base::Time> last_served_date) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (last_served_date.has_value()) {
    last_served_date_ = last_served_date;
    RecomputeSchedule();
  }
}
