blob: 35e2b16b88a58e529efd68cf199144c4a9bad7a3 [file] [log] [blame]
// Copyright 2018 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/ui/views/relaunch_notification/relaunch_notification_controller.h"
#include <algorithm>
#include <utility>
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/safe_browsing/application_advanced_protection_status_detector.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/core/common/features.h"
namespace {
// A type represending the possible RelaunchNotification policy setting values.
enum class RelaunchNotificationSetting {
// Indications are via the Chrome menu only -- no work for the controller.
kChromeMenuOnly,
// Present the relaunch recommended bubble in the last active browser window
// on Chrome desktop, or the relaunch recommended notification in the unified
// system tray on Chrome OS.
kRecommendedBubble,
// Present the relaunch required dialog in the last active browser window on
// Chrome desktop, or the relaunch required notification in the unified system
// tray on Chrome OS.
kRequiredDialog,
};
// Returns the policy setting, mapping out-of-range values to kChromeMenuOnly.
RelaunchNotificationSetting ReadPreference() {
PrefService* local_state = g_browser_process->local_state();
if (local_state) {
switch (local_state->GetInteger(prefs::kRelaunchNotification)) {
case 1:
return RelaunchNotificationSetting::kRecommendedBubble;
case 2:
return RelaunchNotificationSetting::kRequiredDialog;
}
}
return RelaunchNotificationSetting::kChromeMenuOnly;
}
} // namespace
RelaunchNotificationController::RelaunchNotificationController(
UpgradeDetector* upgrade_detector)
: RelaunchNotificationController(upgrade_detector,
base::DefaultClock::GetInstance(),
base::DefaultTickClock::GetInstance()) {}
RelaunchNotificationController::~RelaunchNotificationController() {
StopObservingUpgrades();
}
// static
constexpr base::TimeDelta RelaunchNotificationController::kRelaunchGracePeriod;
RelaunchNotificationController::RelaunchNotificationController(
UpgradeDetector* upgrade_detector,
const base::Clock* clock,
const base::TickClock* tick_clock)
: upgrade_detector_(upgrade_detector),
clock_(clock),
last_notification_style_(NotificationStyle::kNone),
last_level_(UpgradeDetector::UPGRADE_ANNOYANCE_NONE),
timer_(clock_, tick_clock) {
PrefService* local_state = g_browser_process->local_state();
if (local_state) {
pref_change_registrar_.Init(local_state);
// base::Unretained is safe here because |this| outlives the registrar.
pref_change_registrar_.Add(
prefs::kRelaunchNotification,
base::BindRepeating(&RelaunchNotificationController::HandleCurrentStyle,
base::Unretained(this)));
}
// Need to register with the UpgradeDetector right at the start to observe any
// calls to override the preference value controlling the notification style.
StartObservingUpgrades();
#if !BUILDFLAG(IS_CHROMEOS)
StartObservingAPStatus();
#endif
// Synchronize the instance with the current state of the preference and
// Advanced Protection status.
HandleCurrentStyle();
}
void RelaunchNotificationController::OnUpgradeRecommended() {
if (last_notification_style_ == NotificationStyle::kNone) {
return;
}
UpgradeDetector::UpgradeNotificationAnnoyanceLevel current_level =
upgrade_detector_->upgrade_notification_stage();
const base::Time current_high_deadline =
upgrade_detector_->GetAnnoyanceLevelDeadline(
UpgradeDetector::UPGRADE_ANNOYANCE_HIGH);
// Nothing to do if there has been no change in the level and deadline. If
// appropriate, a notification for this level has already been shown.
if (current_level == last_level_ &&
current_high_deadline == last_high_deadline_) {
return;
}
switch (current_level) {
case UpgradeDetector::UPGRADE_ANNOYANCE_NONE:
case UpgradeDetector::UPGRADE_ANNOYANCE_VERY_LOW:
// While it's unexpected that the level could move back down, it's not a
// challenge to do the right thing.
CloseRelaunchNotification();
break;
case UpgradeDetector::UPGRADE_ANNOYANCE_LOW:
case UpgradeDetector::UPGRADE_ANNOYANCE_ELEVATED:
case UpgradeDetector::UPGRADE_ANNOYANCE_GRACE:
case UpgradeDetector::UPGRADE_ANNOYANCE_HIGH:
ShowRelaunchNotification(current_level, current_high_deadline);
break;
case UpgradeDetector::UPGRADE_ANNOYANCE_CRITICAL:
// Critical notifications are handled by ToolbarView.
// TODO(grt): Reconsider this when implementing the relaunch required
// dialog. Obeying the administrator's wish to force a relaunch when
// the annoyance level reaches HIGH is more important than showing the
// critical update dialog. Perhaps handling of "critical" events should
// be decoupled from the "relaunch to update" events.
CloseRelaunchNotification();
break;
}
last_level_ = current_level;
last_high_deadline_ = current_high_deadline;
}
void RelaunchNotificationController::OnRelaunchOverriddenToRequired(
bool overridden) {
if (notification_type_required_overridden_ == overridden) {
return;
}
notification_type_required_overridden_ = overridden;
HandleCurrentStyle();
}
void RelaunchNotificationController::
OnApplicationAdvancedProtectionStatusChanged(bool enabled) {
HandleCurrentStyle();
}
void RelaunchNotificationController::HandleCurrentStyle() {
NotificationStyle notification_style = NotificationStyle::kNone;
if (notification_type_required_overridden_) {
notification_style = NotificationStyle::kRequired;
} else {
switch (ReadPreference()) {
case RelaunchNotificationSetting::kChromeMenuOnly:
DCHECK_EQ(notification_style, NotificationStyle::kNone);
break;
case RelaunchNotificationSetting::kRecommendedBubble:
notification_style = NotificationStyle::kRecommended;
break;
case RelaunchNotificationSetting::kRequiredDialog:
notification_style = NotificationStyle::kRequired;
break;
}
}
// Force the style to `kRequired` if Advanced Protection is enabled and the
// relaunch required policy is not already in effect.
if (notification_style != NotificationStyle::kRequired &&
advanced_protection_observation_.IsObserving() &&
advanced_protection_observation_.GetSource()
->IsUnderAdvancedProtection()) {
notification_style_overridden_for_advanced_protection_ = true;
notification_style = NotificationStyle::kRequired;
} else {
notification_style_overridden_for_advanced_protection_ = false;
}
// Nothing to do if there has been no change in the notification style.
if (notification_style == last_notification_style_) {
return;
}
// Close the bubble or dialog if either is open.
if (last_notification_style_ != NotificationStyle::kNone) {
CloseRelaunchNotification();
}
// Reset state so that a notifications is shown anew in a new style if needed.
last_level_ = UpgradeDetector::UPGRADE_ANNOYANCE_NONE;
if (notification_style == NotificationStyle::kNone) {
// AppMenuIconController takes care of updating the Chrome menu as needed.
last_notification_style_ = notification_style;
return;
}
last_notification_style_ = notification_style;
// Synchronize the instance with the current state of detection.
OnUpgradeRecommended();
}
void RelaunchNotificationController::StartObservingUpgrades() {
upgrade_detector_->AddObserver(this);
}
void RelaunchNotificationController::StopObservingUpgrades() {
upgrade_detector_->RemoveObserver(this);
}
void RelaunchNotificationController::StartObservingAPStatus() {
// advanced_protection_detector is available when
// `safe_browsing::kRelaunchNotificationForAdvancedProtection` is enabled.
if (auto* advanced_protection_detector =
g_browser_process->GetFeatures()
->application_advanced_protection_status_detector()) {
advanced_protection_observation_.Observe(advanced_protection_detector);
}
}
void RelaunchNotificationController::ShowRelaunchNotification(
UpgradeDetector::UpgradeNotificationAnnoyanceLevel level,
base::Time high_deadline) {
DCHECK_NE(last_notification_style_, NotificationStyle::kNone);
if (last_notification_style_ == NotificationStyle::kRecommended) {
// Show the dialog if there has been a level change.
if (level != last_level_) {
NotifyRelaunchRecommended(level ==
UpgradeDetector::UPGRADE_ANNOYANCE_HIGH);
}
// If this is the final showing (the one at the "high" level), start the
// timer to reshow the bubble at each "elevated to high" interval.
if (level == UpgradeDetector::UPGRADE_ANNOYANCE_HIGH) {
StartReshowTimer();
} else {
// Make sure the timer isn't running following a drop down from HIGH to a
// lower level.
timer_.Stop();
}
} else {
HandleRelaunchRequiredState(level, high_deadline);
}
}
void RelaunchNotificationController::CloseRelaunchNotification() {
DCHECK_NE(last_notification_style_, NotificationStyle::kNone);
// Nothing needs to be closed if the annoyance level is none, very low, or
// critical.
if (last_level_ == UpgradeDetector::UPGRADE_ANNOYANCE_NONE ||
last_level_ == UpgradeDetector::UPGRADE_ANNOYANCE_VERY_LOW ||
last_level_ == UpgradeDetector::UPGRADE_ANNOYANCE_CRITICAL) {
return;
}
// Explicit closure cancels either repeatedly reshowing the bubble or the
// forced relaunch.
timer_.Stop();
Close();
}
void RelaunchNotificationController::HandleRelaunchRequiredState(
UpgradeDetector::UpgradeNotificationAnnoyanceLevel level,
base::Time high_deadline) {
DCHECK_EQ(last_notification_style_, NotificationStyle::kRequired);
const base::Time now = clock_->Now();
// Make no changes if the level has not changed, the new deadline is not in
// the future, and the browser is within the grace period of the previous
// deadline. The user has already seen the one-hour countdown so just let it
// go.
// The right thing would be make UpgradeDetector responsible for recomputing
// the deadline and not reduce the HIGH annoyance level backwards if it would
// make it earlier than a previous GRACE.
if (level == last_level_ && timer_.IsRunning()) {
const base::Time& desired_run_time = timer_.desired_run_time();
DCHECK(!desired_run_time.is_null());
if (high_deadline <= now &&
desired_run_time - now <= kRelaunchGracePeriod) {
return;
}
}
base::Time deadline = high_deadline;
// (re)Start the timer if it is not running or there has been a deadline
// change.
if (!timer_.IsRunning() || high_deadline != last_high_deadline_) {
// Give the user at least one hour to relaunch if the new deadline is in the
// past. This could occur in the following cases :-
// a) The device goes to sleep before the first notification and wakes up
// after the deadline.
// b) A change in policy value moves the deadline in the past.
if (high_deadline <= now) {
deadline = now + kRelaunchGracePeriod;
}
// (re)Start the timer to perform the relaunch when the deadline is reached.
timer_.Start(FROM_HERE, deadline, this,
&RelaunchNotificationController::OnRelaunchDeadlineExpired);
}
platform_impl_.SetDeadline(deadline);
// Show the dialog if there has been a level change or if the deadline is in
// the past.
if (!platform_impl_.IsRequiredNotificationShown() &&
(level != last_level_ || high_deadline <= now)) {
NotifyRelaunchRequired();
}
}
base::Time RelaunchNotificationController::IncreaseRelaunchDeadlineOnShow() {
DCHECK(timer_.IsRunning());
DCHECK(!timer_.desired_run_time().is_null());
base::Time relaunch_deadline = timer_.desired_run_time();
// Push the dealdine back if needed so that the user has at least the grace
// period to decide what to do.
relaunch_deadline =
std::max(clock_->Now() + kRelaunchGracePeriod, relaunch_deadline);
timer_.Start(FROM_HERE, relaunch_deadline, this,
&RelaunchNotificationController::OnRelaunchDeadlineExpired);
return relaunch_deadline;
}
void RelaunchNotificationController::StartReshowTimer() {
DCHECK_EQ(last_notification_style_, NotificationStyle::kRecommended);
DCHECK(!last_relaunch_notification_time_.is_null());
// Use the delta between the elevated and high annoyance levels as the
// reshow period.
const auto reshow_period = upgrade_detector_->GetAnnoyanceLevelDeadline(
UpgradeDetector::UPGRADE_ANNOYANCE_HIGH) -
upgrade_detector_->GetAnnoyanceLevelDeadline(
UpgradeDetector::UPGRADE_ANNOYANCE_ELEVATED);
// Compute the next time to show the notification.
const auto desired_run_time =
last_relaunch_notification_time_ + reshow_period;
timer_.Start(FROM_HERE, desired_run_time, this,
&RelaunchNotificationController::OnReshowRelaunchRecommended);
}
void RelaunchNotificationController::OnReshowRelaunchRecommended() {
DCHECK_EQ(last_notification_style_, NotificationStyle::kRecommended);
NotifyRelaunchRecommended(true);
StartReshowTimer();
}
void RelaunchNotificationController::NotifyRelaunchRecommended(
bool past_deadline) {
last_relaunch_notification_time_ = clock_->Now();
DoNotifyRelaunchRecommended(past_deadline);
}
void RelaunchNotificationController::DoNotifyRelaunchRecommended(
bool past_deadline) {
platform_impl_.NotifyRelaunchRecommended(
upgrade_detector_->upgrade_detected_time(), past_deadline);
}
void RelaunchNotificationController::NotifyRelaunchRequired() {
DCHECK(timer_.IsRunning());
DCHECK(!timer_.desired_run_time().is_null());
DoNotifyRelaunchRequired(
notification_style_overridden_for_advanced_protection_,
timer_.desired_run_time(),
base::BindOnce(
&RelaunchNotificationController::IncreaseRelaunchDeadlineOnShow,
base::Unretained(this)));
}
void RelaunchNotificationController::DoNotifyRelaunchRequired(
bool is_notification_style_ap_required,
base::Time relaunch_deadline,
base::OnceCallback<base::Time()> on_visible) {
platform_impl_.NotifyRelaunchRequired(relaunch_deadline,
#if BUILDFLAG(IS_CHROMEOS)
notification_type_required_overridden_,
#else
is_notification_style_ap_required,
#endif
std::move(on_visible));
}
void RelaunchNotificationController::Close() {
platform_impl_.CloseRelaunchNotification();
}
void RelaunchNotificationController::OnRelaunchDeadlineExpired() {
chrome::RelaunchIgnoreUnloadHandlers();
}