chromium/chrome/browser/ash/eol/eol_notification.cc

// Copyright 2016 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/ash/eol/eol_notification.h"

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/public/cpp/system_notification_builder.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ash/eol/eol_incentive_util.h"
#include "chrome/browser/ash/extended_updates/extended_updates_controller.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/system/system_tray_client_impl.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/dbus/update_engine/update_engine_client.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"

namespace ash {
namespace {

using ::l10n_util::GetStringUTF16;

const char kEolNotificationId[] = "chrome://product_eol";

constexpr int kFirstWarningDaysInAdvance = 180;
constexpr int kSecondWarningDaysInAdvance = 90;

// The first and second incentive notification button indices.
constexpr int kButtonClaim = 0;
constexpr int kButtonAboutUpdates = 1;

// The number of days past the EOL within which the last incentive notification
// is shown.
constexpr int kLastIncentiveEndDaysPastEol = -5;

base::Time FirstWarningDate(base::Time eol_date) {
  return eol_date - base::Days(kFirstWarningDaysInAdvance);
}

base::Time SecondWarningDate(const base::Time& eol_date) {
  return eol_date - base::Days(kSecondWarningDaysInAdvance);
}

}  // namespace

// static
bool EolNotification::ShouldShowEolNotification() {
  // Do not show end of life notification if this device is managed by
  // enterprise user.
  if (g_browser_process->platform_part()
          ->browser_policy_connector_ash()
          ->IsDeviceEnterpriseManaged()) {
    return false;
  }

  return true;
}

EolNotification::EolNotification(Profile* profile)
    : clock_(base::DefaultClock::GetInstance()), profile_(profile) {
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kEolResetDismissedPrefs)) {
    ResetDismissedPrefs();
  }
}

EolNotification::~EolNotification() = default;

void EolNotification::CheckEolInfo() {
  // Request the Eol Info.
  UpdateEngineClient::Get()->GetEolInfo(base::BindOnce(
      &EolNotification::OnEolInfo, weak_ptr_factory_.GetWeakPtr()));
}

void EolNotification::OnEolInfo(UpdateEngineClient::EolInfo eol_info) {
  MaybeShowEolNotification(eol_info.eol_date);

  ExtendedUpdatesController::Get()->OnEolInfo(profile_, eol_info);
}

void EolNotification::MaybeShowEolNotification(base::Time eol_date) {
  // Do not show warning Eol notification if invalid |eol_date|.
  if (eol_date.is_null()) {
    return;
  }

  const base::Time now = clock_->Now();
  const base::Time prev_eol_date =
      profile_->GetPrefs()->GetTime(prefs::kEndOfLifeDate);

  profile_->GetPrefs()->SetTime(prefs::kEndOfLifeDate, eol_date);

  if (!now.is_null() && eol_date != prev_eol_date && now < eol_date) {
    // Reset showed warning prefs if the Eol date changed.
    ResetDismissedPrefs();
  }

  eol_incentive_util::EolIncentiveType incentive_type =
      eol_incentive_util::ShouldShowEolIncentive(profile_, eol_date, now);

  SystemTrayClientImpl* tray_client = SystemTrayClientImpl::Get();
  if (tray_client) {
    tray_client->SetShowEolNotice(
        incentive_type ==
                ash::eol_incentive_util::EolIncentiveType::kEolPassed ||
            incentive_type ==
                ash::eol_incentive_util::EolIncentiveType::kEolPassedRecently,
        incentive_type ==
            ash::eol_incentive_util::EolIncentiveType::kEolPassedRecently);
  }

  if (incentive_type != eol_incentive_util::EolIncentiveType::kNone) {
    MaybeShowEolIncentiveNotification(eol_date, incentive_type);
    return;
  }

  if (eol_date <= now) {
    dismiss_pref_ = prefs::kEolNotificationDismissed;
  } else if (SecondWarningDate(eol_date) <= now) {
    dismiss_pref_ = prefs::kSecondEolWarningDismissed;
  } else if (FirstWarningDate(eol_date) <= now) {
    if (base::FeatureList::IsEnabled(features::kSuppressFirstEolWarning)) {
      dismiss_pref_ = std::nullopt;
      return;
    }
    dismiss_pref_ = prefs::kFirstEolWarningDismissed;
  } else {
    // |now| < FirstWarningDate() so don't show anything.
    dismiss_pref_ = std::nullopt;
    return;
  }

  // Do not show if notification has already been dismissed or is out of range.
  if (!dismiss_pref_ || profile_->GetPrefs()->GetBoolean(*dismiss_pref_))
    return;

  CreateNotification(eol_date, now);
}

void EolNotification::CreateNotification(base::Time eol_date, base::Time now) {
  CHECK(!eol_date.is_null());
  CHECK(!now.is_null());

  message_center::RichNotificationData data;
  ash::SystemNotificationBuilder notification_builder;

  DCHECK_EQ(BUTTON_MORE_INFO, data.buttons.size());
  data.buttons.emplace_back(GetStringUTF16(IDS_LEARN_MORE));

  if (now < eol_date) {
    // Notifies user that updates will stop occurring at a month and year.
    notification_builder
        .SetTitleWithArgs(IDS_PENDING_EOL_NOTIFICATION_TITLE,
                          {TimeFormatMonthAndYearForTimeZone(
                              eol_date, icu::TimeZone::getGMT())})
        .SetMessageWithArgs(IDS_PENDING_EOL_NOTIFICATION_MESSAGE,
                            {ui::GetChromeOSDeviceName()})
        .SetCatalogName(NotificationCatalogName::kPendingEOL)
        .SetSmallImage(vector_icons::kBusinessIcon);
  } else {
    DCHECK_EQ(BUTTON_DISMISS, data.buttons.size());
    data.buttons.emplace_back(GetStringUTF16(IDS_EOL_DISMISS_BUTTON));

    // Notifies user that updates will no longer occur after this final update.
    notification_builder.SetTitleId(IDS_EOL_NOTIFICATION_TITLE)
        .SetMessageWithArgs(IDS_EOL_NOTIFICATION_EOL,
                            {ui::GetChromeOSDeviceName()})
        .SetCatalogName(NotificationCatalogName::kEOL)
        .SetSmallImage(kNotificationEndOfSupportIcon);
  }

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Display(
      NotificationHandler::Type::TRANSIENT,
      notification_builder.SetId(kEolNotificationId)
          .SetOriginUrl(GURL(kEolNotificationId))
          .SetOptionalFields(data)
          .SetDelegate(
              base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
                  weak_ptr_factory_.GetWeakPtr()))
          .Build(false),
      /*metadata=*/nullptr);

  eol_incentive_util::RecordShowSourceHistogram(
      eol_incentive_util::EolIncentiveShowSource::kNotification_Original);
}

void EolNotification::Close(bool by_user) {
  // Only the final Eol notification has an explicit dismiss button, and
  // is only dismissible by that button.  The first and second warning
  // buttons do not have an explicit dismiss button.
  if (!by_user || !dismiss_pref_ ||
      dismiss_pref_ == prefs::kEolNotificationDismissed) {
    return;
  }

  profile_->GetPrefs()->SetBoolean(*dismiss_pref_, true);
}

void EolNotification::Click(const std::optional<int>& button_index,
                            const std::optional<std::u16string>& reply) {
  if (!button_index) {
    return;
  }

  if (dismiss_pref_ == prefs::kEolApproachingIncentiveNotificationDismissed ||
      dismiss_pref_ == prefs::kEolPassedFinalIncentiveDismissed) {
    bool use_offer_url = features::kEolIncentiveParam.Get() !=
                         features::EolIncentiveParam::kNoOffer;
    switch (*button_index) {
      case kButtonClaim:
        // Open link for eol incentive notification.
        NewWindowDelegate::GetPrimary()->OpenUrl(
            GURL(use_offer_url ? chrome::kEolIncentiveNotificationOfferURL
                               : chrome::kEolIncentiveNotificationNoOfferURL),
            NewWindowDelegate::OpenUrlFrom::kUserInteraction,
            NewWindowDelegate::Disposition::kNewForegroundTab);

        if (dismiss_pref_ ==
            prefs::kEolApproachingIncentiveNotificationDismissed) {
          // Record button pressed for eol approaching.
          eol_incentive_util::RecordButtonClicked(
              use_offer_url ? eol_incentive_util::EolIncentiveButtonType::
                                  kNotification_Offer_Approaching
                            : eol_incentive_util::EolIncentiveButtonType::
                                  kNotification_NoOffer_Approaching);
        } else {
          // Record button pressed for eol recently passed.
          eol_incentive_util::RecordButtonClicked(
              use_offer_url ? eol_incentive_util::EolIncentiveButtonType::
                                  kNotification_Offer_RecentlyPassed
                            : eol_incentive_util::EolIncentiveButtonType::
                                  kNotification_NoOffer_RecentlyPassed);
        }
        break;
      case kButtonAboutUpdates:
        // Open link to learn more about updates.
        NewWindowDelegate::GetPrimary()->OpenUrl(
            GURL(chrome::kEolNotificationURL),
            NewWindowDelegate::OpenUrlFrom::kUserInteraction,
            NewWindowDelegate::Disposition::kNewForegroundTab);

        eol_incentive_util::RecordButtonClicked(
            dismiss_pref_ ==
                    prefs::kEolApproachingIncentiveNotificationDismissed
                ? eol_incentive_util::EolIncentiveButtonType::
                      kNotification_AboutUpdates_Approaching
                : eol_incentive_util::EolIncentiveButtonType::
                      kNotification_AboutUpdates_RecentlyPassed);
        break;
    }
    profile_->GetPrefs()->SetBoolean(prefs::kEolNotificationDismissed, true);
  } else {
    switch (*button_index) {
      case BUTTON_MORE_INFO: {
        const GURL url(dismiss_pref_ == prefs::kEolNotificationDismissed
                           ? chrome::kEolNotificationURL
                           : chrome::kAutoUpdatePolicyURL);
        // Show eol link.
        NewWindowDelegate::GetPrimary()->OpenUrl(
            url, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
            NewWindowDelegate::Disposition::kNewForegroundTab);

        eol_incentive_util::RecordButtonClicked(
            eol_incentive_util::EolIncentiveButtonType::
                kNotification_Original_LearnMore);
        break;
      }
      case BUTTON_DISMISS:
        CHECK(dismiss_pref_);
        eol_incentive_util::RecordButtonClicked(
            eol_incentive_util::EolIncentiveButtonType::
                kNotification_Original_Dismiss);
        // Set dismiss pref.
        profile_->GetPrefs()->SetBoolean(*dismiss_pref_, true);
        break;
    }
  }

  if (dismiss_pref_ && (*dismiss_pref_ != prefs::kEolNotificationDismissed)) {
    profile_->GetPrefs()->SetBoolean(*dismiss_pref_, true);
  }

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Close(
      NotificationHandler::Type::TRANSIENT, kEolNotificationId);
}

void EolNotification::OverrideClockForTesting(base::Clock* clock) {
  if (!clock) {
    clock_ = base::DefaultClock::GetInstance();
  } else {
    clock_ = clock;
  }
}

void EolNotification::MaybeShowEolIncentiveNotification(
    base::Time eol_date,
    eol_incentive_util::EolIncentiveType incentive_type) {
  const base::Time now = clock_->Now();
  const base::TimeDelta time_to_eol = eol_date - now;
  const int days_to_eol = time_to_eol.InDays();

  switch (incentive_type) {
    case eol_incentive_util::EolIncentiveType::kNone:
    case eol_incentive_util::EolIncentiveType::kEolPassed:
      if (days_to_eol < kLastIncentiveEndDaysPastEol &&
          !profile_->GetPrefs()->GetBoolean(
              prefs::kEolPassedFinalIncentiveDismissed) &&
          !profile_->GetPrefs()->GetBoolean(prefs::kEolNotificationDismissed)) {
        // Once the timeframe for showing the final incentive notification has
        // passed, if the final incentive notification was not dismissed, and
        // the final EOL notification has not been dismissed, then show the
        // final EOL notification.
        dismiss_pref_ = prefs::kEolNotificationDismissed;
        CreateNotification(eol_date, now);
      }
      return;
    case eol_incentive_util::EolIncentiveType::kEolApproaching:
      dismiss_pref_ = prefs::kEolApproachingIncentiveNotificationDismissed;
      break;
    case eol_incentive_util::EolIncentiveType::kEolPassedRecently:
      dismiss_pref_ = prefs::kEolPassedFinalIncentiveDismissed;
      break;
  }

  if (!dismiss_pref_ || profile_->GetPrefs()->GetBoolean(*dismiss_pref_)) {
    return;
  }

  ShowIncentiveNotification(eol_date, incentive_type);
}

void EolNotification::ShowIncentiveNotification(
    base::Time eol_date,
    eol_incentive_util::EolIncentiveType incentive_type) {
  message_center::RichNotificationData data;
  ash::SystemNotificationBuilder notification_builder;

  gfx::ImageSkia incentive_image =
      *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
          IDR_EOL_INCENTIVE_NOTIFICATION);
  SkBitmap background_bitmap;
  background_bitmap.allocN32Pixels(incentive_image.width(),
                                   incentive_image.height());
  background_bitmap.eraseColor(
      DarkLightModeController::Get()->IsDarkModeEnabled() ? gfx::kGoogleGrey800
                                                          : SK_ColorWHITE);
  gfx::ImageSkia background =
      gfx::ImageSkia::CreateFrom1xBitmap(background_bitmap);
  data.image = gfx::Image(gfx::ImageSkiaOperations::CreateSuperimposedImage(
      background, incentive_image));

  features::EolIncentiveParam incentive_param =
      ash::features::kEolIncentiveParam.Get();

  switch (incentive_param) {
    case features::EolIncentiveParam::kNoOffer:
      data.buttons.emplace_back(GetStringUTF16(IDS_LEARN_MORE));

      if (incentive_type ==
          eol_incentive_util::EolIncentiveType::kEolApproaching) {
        notification_builder
            .SetTitle(GetStringUTF16(
                IDS_EOL_INCENTIVE_NOTIFICATION_TITLE_NO_OFFER_EXPIRING_SOON))
            .SetMessageWithArgs(
                IDS_EOL_INCENTIVE_NOTIFICATION_MESSAGE_NO_OFFER_EXPIRING_SOON,
                {TimeFormatMonthAndYearForTimeZone(eol_date,
                                                   icu::TimeZone::getGMT())});
      } else {
        notification_builder
            .SetTitle(GetStringUTF16(
                IDS_EOL_INCENTIVE_NOTIFICATION_TITLE_NO_OFFER_EXPIRED))
            .SetMessage(GetStringUTF16(
                IDS_EOL_INCENTIVE_NOTIFICATION_MESSAGE_NO_OFFER_EXPIRED));
      }
      break;
    case features::EolIncentiveParam::kOffer:
      data.buttons.emplace_back(
          GetStringUTF16(IDS_EOL_INCENTIVE_NOTIFICATION_OFFER_SHOP_BUTTON));
      data.buttons.emplace_back(
          GetStringUTF16(IDS_EOL_INCENTIVE_NOTIFICATION_OFFER_ABOUT_BUTTON));
      notification_builder.SetTitle(
          GetStringUTF16(IDS_EOL_INCENTIVE_NOTIFICATION_TITLE_OFFER));

      if (incentive_type ==
          eol_incentive_util::EolIncentiveType::kEolApproaching) {
        notification_builder.SetMessageWithArgs(
            IDS_EOL_INCENTIVE_NOTIFICATION_MESSAGE_OFFER_EXPIRING_SOON,
            {TimeFormatMonthAndYearForTimeZone(eol_date,
                                               icu::TimeZone::getGMT())});
      } else {
        notification_builder.SetMessage(GetStringUTF16(
            IDS_EOL_INCENTIVE_NOTIFICATION_MESSAGE_OFFER_EXPIRED));
      }
      break;
    case features::EolIncentiveParam::kOfferWithWarning:
      data.buttons.emplace_back(
          GetStringUTF16(IDS_EOL_INCENTIVE_NOTIFICATION_OFFER_SHOP_BUTTON));
      data.buttons.emplace_back(
          GetStringUTF16(IDS_EOL_INCENTIVE_NOTIFICATION_OFFER_ABOUT_BUTTON));

      if (incentive_type ==
          eol_incentive_util::EolIncentiveType::kEolApproaching) {
        notification_builder
            .SetTitle(GetStringUTF16(
                IDS_EOL_INCENTIVE_NOTIFICATION_TITLE_OFFER_WITH_WARNING_EXPIRING_SOON))
            .SetMessageWithArgs(
                IDS_EOL_INCENTIVE_NOTIFICATION_MESSAGE_OFFER_EXPIRING_SOON,
                {TimeFormatMonthAndYearForTimeZone(eol_date,
                                                   icu::TimeZone::getGMT())});
      } else {
        notification_builder
            .SetTitle(GetStringUTF16(
                IDS_EOL_INCENTIVE_NOTIFICATION_TITLE_OFFER_WITH_WARNING_EXPIRED))
            .SetMessage(GetStringUTF16(
                IDS_EOL_INCENTIVE_NOTIFICATION_MESSAGE_OFFER_EXPIRED));
      }
      break;
  }

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Display(
      NotificationHandler::Type::TRANSIENT,
      notification_builder.SetId(kEolNotificationId)
          .SetCatalogName(NotificationCatalogName::kEOLIncentive)
          .SetOriginUrl(GURL(kEolNotificationId))
          .SetOptionalFields(data)
          .SetDelegate(
              base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
                  weak_ptr_factory_.GetWeakPtr()))
          .Build(false),
      /*metadata=*/nullptr);

  if (incentive_type == eol_incentive_util::EolIncentiveType::kEolApproaching) {
    // Record approaching eol notification shown.
    eol_incentive_util::RecordShowSourceHistogram(
        eol_incentive_util::EolIncentiveShowSource::kNotification_Approaching);
  } else {
    // Record recently passed eol notification shown.
    eol_incentive_util::RecordShowSourceHistogram(
        eol_incentive_util::EolIncentiveShowSource::
            kNotification_RecentlyPassed);
  }
}

void EolNotification::ResetDismissedPrefs() {
  profile_->GetPrefs()->SetBoolean(prefs::kFirstEolWarningDismissed, false);
  profile_->GetPrefs()->SetBoolean(prefs::kSecondEolWarningDismissed, false);
  profile_->GetPrefs()->SetBoolean(prefs::kEolNotificationDismissed, false);
  profile_->GetPrefs()->SetBoolean(
      prefs::kEolApproachingIncentiveNotificationDismissed, false);
  profile_->GetPrefs()->SetBoolean(prefs::kEolPassedFinalIncentiveDismissed,
                                   false);
}

}  // namespace ash