| // 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(); |
| } |