chromium/ash/quick_pair/ui/fast_pair/fast_pair_notification_controller.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/quick_pair/ui/fast_pair/fast_pair_notification_controller.h"

#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"

#include "components/cross_device/logging/logging.h"

using message_center::MessageCenter;
using message_center::Notification;

namespace {

const message_center::NotifierId kNotifierFastPair =
    message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
                               "ash.fastpair",
                               ash::NotificationCatalogName::kFastPair);
const char kFastPairErrorNotificationId[] =
    "cros_fast_pair_error_notification_id";
const char kFastPairDiscoveryGuestNotificationId[] =
    "cros_fast_pair_discovery_guest_notification_id";
const char kFastPairApplicationAvailableNotificationId[] =
    "cros_fast_pair_application_available_notification_id";
const char kFastPairApplicationInstalledNotificationId[] =
    "cros_fast_pair_application_installed_notification_id";
const char kFastPairDiscoveryUserNotificationId[] =
    "cros_fast_pair_discovery_user_notification_id";
const char kFastPairPairingNotificationId[] =
    "cros_fast_pair_pairing_notification_id";
const char kFastPairAssociateAccountNotificationId[] =
    "cros_fast_pair_associate_account_notification_id";
const char kFastPairDiscoverySubsequentNotificationId[] =
    "cros_fast_pair_discovery_subsequent_notification_id";

// Values outside of the range (e.g. -1) will show an infinite loading
// progress bar.
const int kInfiniteLoadingProgressValue = -1;

// 12 seconds comes from aligning CrOS notification with Android. Android
// determined it takes 12 seconds for a device to be truly lost to the adapter.
// Within 12 seconds, a device can be perceived as lost, yet be found again.
constexpr base::TimeDelta kNotificationTimeout = base::Seconds(12);

// Creates an empty Fast Pair notification with the given id and uses the
// Bluetooth icon and FastPair notifierID.
std::unique_ptr<message_center::Notification> CreateNotification(
    const std::string& id,
    message_center::SystemNotificationWarningLevel warning_level,
    message_center::MessageCenter* message_center) {
  // Remove any existing Fast Pair notifications so only one appears at a time,
  // since there isn't a case where all of them should be showing.
  message_center->RemoveNotificationsForNotifierId(kNotifierFastPair);

  std::unique_ptr<message_center::Notification> notification =
      ash::CreateSystemNotificationPtr(
          /*type=*/message_center::NOTIFICATION_TYPE_SIMPLE,
          /*id=*/id,
          /*title=*/std::u16string(),
          /*message=*/std::u16string(),
          /*display_source=*/std::u16string(), /*origin_url=*/GURL(),
          /*notifier_id=*/kNotifierFastPair,
          /*optional_fields=*/{},
          /*delegate=*/nullptr,
          /*small_image=*/ash::kNotificationBluetoothIcon,
          /*warning_level=*/warning_level);
  notification->set_never_timeout(true);
  notification->set_priority(
      message_center::NotificationPriority::MAX_PRIORITY);

  return notification;
}

}  // namespace

namespace ash {
namespace quick_pair {

// NotificationDelegate implementation for handling click and dismiss events on
// a notification. Used by Error, Pairing, and Discovery notifications.
class NotificationDelegate : public message_center::NotificationDelegate {
 public:
  explicit NotificationDelegate(
      base::RepeatingClosure on_primary_click,
      base::OnceCallback<void(FastPairNotificationDismissReason)> on_close,
      base::RepeatingClosure on_secondary_click = base::DoNothing(),
      base::OneShotTimer* expire_notification_timer = nullptr) {
    on_primary_click_ = on_primary_click;
    on_secondary_click_ = on_secondary_click;
    on_close_ = std::move(on_close);
    expire_notification_timer_ = expire_notification_timer;
  }

  void DismissedByTimeout() { dismissed_by_timeout_ = true; }

 protected:
  ~NotificationDelegate() override = default;

  // message_center::NotificationDelegate override:
  void Click(const std::optional<int>& button_index,
             const std::optional<std::u16string>& reply) override {
    if (!button_index)
      return;

    // If the button displayed on the notification is clicked.
    switch (*button_index) {
      case static_cast<int>(Button::kPrimaryButton):
        on_primary_click_.Run();
        break;
      case static_cast<int>(Button::kSecondaryButton):
        on_secondary_click_.Run();
        break;
    }
  }

  // message_center::NotificationDelegate override:
  void Close(bool by_user) override {
    // If there is an expire notification timer, stop the timer if the user
    // dismisses the notification to prevent the timer firing and removing
    // notifications that might come up later.
    if (expire_notification_timer_) {
      CD_LOG(VERBOSE, Feature::FP)
          << __func__ << ": stopping expiration timer on notification close";
      expire_notification_timer_->Stop();
    }

    if (dismissed_by_timeout_) {
      std::move(on_close_).Run(
          FastPairNotificationDismissReason::kDismissedByTimeout);
      return;
    } else if (by_user) {
      std::move(on_close_).Run(
          FastPairNotificationDismissReason::kDismissedByUser);
      return;
    }

    std::move(on_close_).Run(FastPairNotificationDismissReason::kDismissedByOs);
  }

 private:
  enum class Button { kPrimaryButton, kSecondaryButton };
  bool dismissed_by_timeout_ = false;
  base::RepeatingClosure on_primary_click_;
  base::RepeatingClosure on_secondary_click_;
  base::OnceCallback<void(FastPairNotificationDismissReason)> on_close_;
  raw_ptr<base::OneShotTimer, DanglingUntriaged> expire_notification_timer_;
};

FastPairNotificationController::FastPairNotificationController(
    message_center::MessageCenter* message_center)
    : message_center_(message_center) {
  DCHECK(message_center_);
}

FastPairNotificationController::~FastPairNotificationController() = default;

void FastPairNotificationController::ShowErrorNotification(
    const std::u16string& device_name,
    gfx::Image device_image,
    base::RepeatingClosure launch_bluetooth_pairing,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  // Because the pairing notification is pinned, we need to manually remove it
  // to explicitly say it is done not by the user. This only need to be done
  // for pinned notifications.
  message_center_->RemoveNotification(kFastPairPairingNotificationId,
                                      /*by_user=*/false);

  std::unique_ptr<message_center::Notification> error_notification =
      CreateNotification(
          kFastPairErrorNotificationId,
          message_center::SystemNotificationWarningLevel::CRITICAL_WARNING,
          message_center_);
  error_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_CONNECTION_ERROR_TITLE, device_name));
  error_notification->set_message(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_CONNECTION_ERROR_MESSAGE));

  message_center::ButtonInfo settings_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_SETTINGS_BUTTON));
  error_notification->set_buttons({settings_button});

  error_notification->set_delegate(base::MakeRefCounted<NotificationDelegate>(
      /*on_primary_click=*/launch_bluetooth_pairing,
      /*on_close=*/std::move(on_close)));
  error_notification->SetImage(device_image);

  message_center_->AddNotification(std::move(error_notification));
}

void FastPairNotificationController::ExtendNotification() {
  // If the timer is already running, it implies that there is already a
  // notification being shown for this device. Since the Mediator keeps track
  // of the device for the currently shown notification, we can only get to this
  // point if the notification is for the same device, which means we reset the
  // timeout.
  if (expire_notification_timer_.IsRunning()) {
    CD_LOG(INFO, Feature::FP)
        << __func__ << " extending notification for re-discovered device";
    expire_notification_timer_.Reset();
  }
}

void FastPairNotificationController::ShowUserDiscoveryNotification(
    const std::u16string& device_name,
    const std::u16string& email_address,
    gfx::Image device_image,
    base::RepeatingClosure on_connect_clicked,
    base::RepeatingClosure on_learn_more_clicked,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  std::unique_ptr<message_center::Notification> discovery_notification =
      CreateNotification(kFastPairDiscoveryUserNotificationId,
                         message_center::SystemNotificationWarningLevel::NORMAL,
                         message_center_);
  discovery_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DISCOVERY_NOTIFICATION_TITLE, device_name));
  discovery_notification->set_message(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DISCOVERY_NOTIFICATION_EMAIL_MESSAGE, device_name,
      email_address));

  message_center::ButtonInfo connect_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_CONNECT_BUTTON));
  message_center::ButtonInfo learn_more_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_LEARN_MORE_BUTTON));
  discovery_notification->set_buttons({connect_button, learn_more_button});

  scoped_refptr<NotificationDelegate> notification_delegate =
      base::MakeRefCounted<NotificationDelegate>(
          /*on_primary_click=*/on_connect_clicked,
          /*on_close=*/std::move(on_close),
          /*on_secondary_click=*/on_learn_more_clicked,
          /*expire_notification_timer=*/&expire_notification_timer_);

  discovery_notification->set_delegate(notification_delegate);
  discovery_notification->SetImage(device_image);

  // Start timer for how long to show the notification before removing the
  // notification. After the timeout period, we will remove the notification
  // from the Message Center.
  expire_notification_timer_.Start(
      FROM_HERE, kNotificationTimeout,
      base::BindOnce(
          &FastPairNotificationController::RemoveNotificationsByTimeout,
          weak_ptr_factory_.GetWeakPtr(), notification_delegate));

  message_center_->AddNotification(std::move(discovery_notification));
}

void FastPairNotificationController::ShowGuestDiscoveryNotification(
    const std::u16string& device_name,
    const gfx::Image device_image,
    base::RepeatingClosure on_connect_clicked,
    base::RepeatingClosure on_learn_more_clicked,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  std::unique_ptr<message_center::Notification> discovery_notification =
      CreateNotification(kFastPairDiscoveryGuestNotificationId,
                         message_center::SystemNotificationWarningLevel::NORMAL,
                         message_center_);
  discovery_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DISCOVERY_NOTIFICATION_TITLE, device_name));
  discovery_notification->set_message(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DISCOVERY_NOTIFICATION_MESSAGE, device_name));

  message_center::ButtonInfo connect_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_CONNECT_BUTTON));
  message_center::ButtonInfo learn_more_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_LEARN_MORE_BUTTON));
  discovery_notification->set_buttons({connect_button, learn_more_button});

  scoped_refptr<NotificationDelegate> notification_delegate =
      base::MakeRefCounted<NotificationDelegate>(
          /*on_primary_click=*/on_connect_clicked,
          /*on_close=*/std::move(on_close),
          /*on_secondary_click=*/on_learn_more_clicked,
          /*expire_notification_timer=*/&expire_notification_timer_);

  discovery_notification->set_delegate(notification_delegate);
  discovery_notification->SetImage(device_image);

  // Start timer for how long to show the notification before removing the
  // notification. After the timeout period, we will remove the notification
  // from the Message Center.
  expire_notification_timer_.Start(
      FROM_HERE, kNotificationTimeout,
      base::BindOnce(
          &FastPairNotificationController::RemoveNotificationsByTimeout,
          weak_ptr_factory_.GetWeakPtr(), notification_delegate));

  message_center_->AddNotification(std::move(discovery_notification));
}

void FastPairNotificationController::ShowSubsequentDiscoveryNotification(
    const std::u16string& device_name,
    const std::u16string& email_address,
    gfx::Image device_image,
    base::RepeatingClosure on_connect_clicked,
    base::RepeatingClosure on_learn_more_clicked,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  std::unique_ptr<message_center::Notification> discovery_notification =
      CreateNotification(kFastPairDiscoverySubsequentNotificationId,
                         message_center::SystemNotificationWarningLevel::NORMAL,
                         message_center_);
  discovery_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DISCOVERY_NOTIFICATION_TITLE, device_name));
  discovery_notification->set_message(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DISCOVERY_NOTIFICATION_SUBSEQUENT, device_name,
      email_address));

  message_center::ButtonInfo connect_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_CONNECT_BUTTON));
  message_center::ButtonInfo learn_more_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_LEARN_MORE_BUTTON));
  discovery_notification->set_buttons({connect_button, learn_more_button});

  scoped_refptr<NotificationDelegate> notification_delegate =
      base::MakeRefCounted<NotificationDelegate>(
          /*on_primary_click=*/on_connect_clicked,
          /*on_close=*/std::move(on_close),
          /*on_secondary_click=*/on_learn_more_clicked,
          /*expire_notification_timer=*/&expire_notification_timer_);

  discovery_notification->set_delegate(notification_delegate);
  discovery_notification->SetImage(device_image);

  // Start timer for how long to show the notification before removing the
  // notification. After the timeout period, we will remove the notification
  // from the Message Center.
  expire_notification_timer_.Start(
      FROM_HERE, kNotificationTimeout,
      base::BindOnce(
          &FastPairNotificationController::RemoveNotificationsByTimeout,
          weak_ptr_factory_.GetWeakPtr(), notification_delegate));

  message_center_->AddNotification(std::move(discovery_notification));
}

void FastPairNotificationController::ShowApplicationAvailableNotification(
    const std::u16string& device_name,
    gfx::Image device_image,
    base::RepeatingClosure download_app_callback,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  std::unique_ptr<message_center::Notification>
      application_available_notification = CreateNotification(
          kFastPairApplicationAvailableNotificationId,
          message_center::SystemNotificationWarningLevel::NORMAL,
          message_center_);
  application_available_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_DOWNLOAD_NOTIFICATION_APP_TITLE, device_name));
  application_available_notification->set_message(l10n_util::GetStringUTF16(
      IDS_FAST_PAIR_DOWNLOAD_NOTIFICATION_APP_MESSAGE));

  message_center::ButtonInfo download_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_DOWNLOAD_APP_BUTTON));
  application_available_notification->set_buttons({download_button});

  application_available_notification->set_delegate(
      base::MakeRefCounted<NotificationDelegate>(
          /*on_primary_click=*/download_app_callback,
          /*on_close=*/std::move(on_close)));
  application_available_notification->SetImage(device_image);

  message_center_->AddNotification(
      std::move(application_available_notification));
}

void FastPairNotificationController::ShowApplicationInstalledNotification(
    const std::u16string& device_name,
    gfx::Image device_image,
    const std::u16string& app_name,
    base::RepeatingClosure launch_app_callback,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  std::unique_ptr<message_center::Notification>
      application_installed_notification = CreateNotification(
          kFastPairApplicationInstalledNotificationId,
          message_center::SystemNotificationWarningLevel::NORMAL,
          message_center_);
  application_installed_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_SETUP_APP_NOTIFICATION_TITLE, device_name));
  application_installed_notification->set_message(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_SETUP_APP_NOTIFICATION_MESSAGE));

  message_center::ButtonInfo setup_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_SETUP_APP_BUTTON));
  application_installed_notification->set_buttons({setup_button});

  application_installed_notification->set_delegate(
      base::MakeRefCounted<NotificationDelegate>(
          /*on_primary_click=*/launch_app_callback,
          /*on_close=*/std::move(on_close)));
  application_installed_notification->SetImage(device_image);

  message_center_->AddNotification(
      std::move(application_installed_notification));
}

void FastPairNotificationController::ShowPairingNotification(
    const std::u16string& device_name,
    gfx::Image device_image,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  // If we get to this point in the pairing flow where we are showing the
  // Pairing notification, then the user has elected to begin pairing and we
  // can stop the timer that was waiting for user interaction on the
  // Discovery notification. We do not need the timer for the Pairing
  // notification since it will be removed when pairing succeeds or fails by
  // the system.
  expire_notification_timer_.Stop();

  std::unique_ptr<message_center::Notification> pairing_notification =
      CreateNotification(kFastPairPairingNotificationId,
                         message_center::SystemNotificationWarningLevel::NORMAL,
                         message_center_);
  pairing_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_PAIRING_NOTIFICATION_TITLE, device_name));

  pairing_notification->set_delegate(base::MakeRefCounted<NotificationDelegate>(
      /*on_primary_click=*/base::DoNothing(),
      /*on_close=*/std::move(on_close)));
  pairing_notification->set_type(message_center::NOTIFICATION_TYPE_PROGRESS);
  pairing_notification->set_progress(kInfiniteLoadingProgressValue);
  pairing_notification->SetImage(device_image);
  pairing_notification->set_pinned(true);

  message_center_->AddNotification(std::move(pairing_notification));
}

void FastPairNotificationController::ShowAssociateAccount(
    const std::u16string& device_name,
    const std::u16string& email_address,
    gfx::Image device_image,
    base::RepeatingClosure on_save_clicked,
    base::RepeatingClosure on_learn_more_clicked,
    base::OnceCallback<void(FastPairNotificationDismissReason)> on_close) {
  std::unique_ptr<message_center::Notification> associate_account_notification =
      CreateNotification(kFastPairAssociateAccountNotificationId,
                         message_center::SystemNotificationWarningLevel::NORMAL,
                         message_center_);
  associate_account_notification->set_title(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_ASSOCIATE_ACCOUNT_NOTIFICATION_TITLE, device_name));
  associate_account_notification->set_message(l10n_util::GetStringFUTF16(
      IDS_FAST_PAIR_ASSOCIATE_ACCOUNT_NOTIFICATION_MESSAGE, device_name,
      email_address));

  message_center::ButtonInfo save_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_SAVE_BUTTON));
  message_center::ButtonInfo learn_more_button(
      l10n_util::GetStringUTF16(IDS_FAST_PAIR_LEARN_MORE_BUTTON));
  associate_account_notification->set_buttons({save_button, learn_more_button});

  scoped_refptr<NotificationDelegate> notification_delegate =
      base::MakeRefCounted<NotificationDelegate>(
          /*on_primary_click=*/on_save_clicked,
          /*on_close=*/std::move(on_close),
          /*on_secondary_click=*/on_learn_more_clicked,
          /*expire_notification_timer=*/&expire_notification_timer_);

  associate_account_notification->set_delegate(notification_delegate);
  associate_account_notification->SetImage(device_image);

  // Start timer for how long to show the notification before removing the
  // notification. After the timeout period, we will remove the notification
  // from the Message Center.
  expire_notification_timer_.Start(
      FROM_HERE, kNotificationTimeout,
      base::BindOnce(
          &FastPairNotificationController::RemoveNotificationsByTimeout,
          weak_ptr_factory_.GetWeakPtr(), notification_delegate));

  message_center_->AddNotification(std::move(associate_account_notification));
}

void FastPairNotificationController::RemoveNotifications() {
  message_center_->RemoveNotificationsForNotifierId(kNotifierFastPair);
}

void FastPairNotificationController::RemoveNotificationsByTimeout(
    scoped_refptr<NotificationDelegate> notification_delegate) {
  notification_delegate->DismissedByTimeout();
  RemoveNotifications();
}

}  // namespace quick_pair
}  // namespace ash