blob: 5a61786b8a0b0bcf7a61775fe62cd747bb4492c2 [file] [log] [blame]
// 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_list.h"
#include "chrome/browser/ui/browser_otr_state.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/1307913
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::Value::List* 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() || BrowserList::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();
}
}