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

// Copyright 2022 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_presenter_impl.h"

#include <optional>
#include <string>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/quick_pair/common/device.h"
#include "ash/quick_pair/common/fast_pair/fast_pair_metrics.h"
#include "ash/quick_pair/common/quick_pair_browser_delegate.h"
#include "ash/quick_pair/proto/fastpair.pb.h"
#include "ash/quick_pair/repository/fast_pair/fast_pair_image_decoder.h"
#include "ash/quick_pair/repository/fast_pair_repository.h"
#include "ash/quick_pair/ui/actions.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tray_utils.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "components/cross_device/logging/logging.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "ui/message_center/message_center.h"

namespace {

const char kDiscoveryLearnMoreLink[] =
    "https://support.google.com/chromebook?p=fast_pair_m101";
const char kAssociateAccountLearnMoreLink[] =
    "https://support.google.com/chromebook?p=bluetooth_pairing_m101";

bool ShouldShowUserEmail(ash::LoginStatus status) {
  switch (status) {
    case ash::LoginStatus::NOT_LOGGED_IN:
    case ash::LoginStatus::LOCKED:
    case ash::LoginStatus::KIOSK_APP:
    case ash::LoginStatus::GUEST:
    case ash::LoginStatus::PUBLIC:
      return false;
    case ash::LoginStatus::USER:
    case ash::LoginStatus::CHILD:
    default:
      return true;
  }
}

}  // namespace

namespace ash {
namespace quick_pair {

// static
FastPairPresenterImpl::Factory*
    FastPairPresenterImpl::Factory::g_test_factory_ = nullptr;

// static
std::unique_ptr<FastPairPresenter> FastPairPresenterImpl::Factory::Create(
    message_center::MessageCenter* message_center) {
  if (g_test_factory_) {
    return g_test_factory_->CreateInstance(message_center);
  }

  return base::WrapUnique(new FastPairPresenterImpl(message_center));
}

// static
void FastPairPresenterImpl::Factory::SetFactoryForTesting(
    Factory* g_test_factory) {
  g_test_factory_ = g_test_factory;
}

FastPairPresenterImpl::Factory::~Factory() = default;

FastPairPresenterImpl::FastPairPresenterImpl(
    message_center::MessageCenter* message_center)
    : notification_controller_(
          std::make_unique<FastPairNotificationController>(message_center)) {}

FastPairPresenterImpl::~FastPairPresenterImpl() = default;

void FastPairPresenterImpl::ShowDiscovery(scoped_refptr<Device> device,
                                          DiscoveryCallback callback) {
  DCHECK(device);
  const auto metadata_id = device->metadata_id();
  FastPairRepository::Get()->GetDeviceMetadata(
      metadata_id, base::BindRepeating(
                       &FastPairPresenterImpl::OnDiscoveryMetadataRetrieved,
                       weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::OnDiscoveryMetadataRetrieved(
    scoped_refptr<Device> device,
    DiscoveryCallback callback,
    DeviceMetadata* device_metadata,
    bool has_retryable_error) {
  if (!device_metadata) {
    return;
  }

  device->set_version(device_metadata->InferFastPairVersion());

  if (device->protocol() == Protocol::kFastPairSubsequent) {
    ShowSubsequentDiscoveryNotification(device, callback, device_metadata);
    return;
  }

  if (device->version().value() == DeviceFastPairVersion::kV1) {
    RecordFastPairDiscoveredVersion(FastPairVersion::kVersion1);
  } else {
    RecordFastPairDiscoveredVersion(FastPairVersion::kVersion2);
  }

  // If we are in guest-mode, or are missing the IdentifyManager needed to show
  // detailed user notification, show the guest notification. We don't have to
  // verify opt-in status in this case because Guests will be guaranteed to not
  // have opt-in status.
  signin::IdentityManager* identity_manager =
      QuickPairBrowserDelegate::Get()->GetIdentityManager();
  if (!identity_manager ||
      !ShouldShowUserEmail(
          Shell::Get()->session_controller()->login_status())) {
    CD_LOG(VERBOSE, Feature::FP)
        << __func__ << ": in guest mode, showing guest notification";
    ShowGuestDiscoveryNotification(device, callback, device_metadata);
    return;
  }

  // Check if the user is opted in to saving devices to their account. If the
  // user is not opted in, we will show the guest notification which does not
  // mention saving devices to the user account. This is flagged depending if
  // the Fast Pair Saved Devices is enabled and we are using a strict
  // interpretation of the opt-in status.
  if (features::IsFastPairSavedDevicesEnabled() &&
      features::IsFastPairSavedDevicesStrictOptInEnabled()) {
    FastPairRepository::Get()->CheckOptInStatus(base::BindOnce(
        &FastPairPresenterImpl::OnCheckOptInStatus,
        weak_pointer_factory_.GetWeakPtr(), device, callback, device_metadata));
    return;
  }

  // If we don't have SavedDevices flag enabled, then we can ignore the user's
  // opt in status and move forward to showing the User Discovery notification.
  ShowUserDiscoveryNotification(device, callback, device_metadata);
}

void FastPairPresenterImpl::OnCheckOptInStatus(
    scoped_refptr<Device> device,
    DiscoveryCallback callback,
    DeviceMetadata* device_metadata,
    nearby::fastpair::OptInStatus status) {
  CD_LOG(INFO, Feature::FP) << __func__;

  if (status != nearby::fastpair::OptInStatus::STATUS_OPTED_IN) {
    ShowGuestDiscoveryNotification(device, callback, device_metadata);
    return;
  }

  ShowUserDiscoveryNotification(device, callback, device_metadata);
}

void FastPairPresenterImpl::ShowSubsequentDiscoveryNotification(
    scoped_refptr<Device> device,
    DiscoveryCallback callback,
    DeviceMetadata* device_metadata) {
  if (!device_metadata) {
    return;
  }

  // Since Subsequent Pairing scenario can only happen for a signed in user
  // when a device has already been saved to their account, this should never
  // be null. We cannot get to this scenario in Guest Mode.
  signin::IdentityManager* identity_manager =
      QuickPairBrowserDelegate::Get()->GetIdentityManager();
  DCHECK(identity_manager);

  const std::string& email =
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
          .email;
  notification_controller_->ShowSubsequentDiscoveryNotification(
      base::UTF8ToUTF16(device->display_name().value()),
      base::ASCIIToUTF16(email), device_metadata->image(),
      base::BindRepeating(&FastPairPresenterImpl::OnDiscoveryClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindRepeating(&FastPairPresenterImpl::OnDiscoveryLearnMoreClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnDiscoveryDismissed,
                     weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::ShowGuestDiscoveryNotification(
    scoped_refptr<Device> device,
    DiscoveryCallback callback,
    DeviceMetadata* device_metadata) {
  notification_controller_->ShowGuestDiscoveryNotification(
      base::ASCIIToUTF16(device_metadata->GetDetails().name()),
      device_metadata->image(),
      base::BindRepeating(&FastPairPresenterImpl::OnDiscoveryClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindRepeating(&FastPairPresenterImpl::OnDiscoveryLearnMoreClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnDiscoveryDismissed,
                     weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::ShowUserDiscoveryNotification(
    scoped_refptr<Device> device,
    DiscoveryCallback callback,
    DeviceMetadata* device_metadata) {
  // Since we check this in |OnInitialDiscoveryMetadataRetrieved| to determine
  // if we should show the Guest notification, this should never be null.
  signin::IdentityManager* identity_manager =
      QuickPairBrowserDelegate::Get()->GetIdentityManager();
  DCHECK(identity_manager);

  const std::string& email =
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
          .email;
  notification_controller_->ShowUserDiscoveryNotification(
      base::ASCIIToUTF16(device_metadata->GetDetails().name()),
      base::ASCIIToUTF16(email), device_metadata->image(),
      base::BindRepeating(&FastPairPresenterImpl::OnDiscoveryClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindRepeating(&FastPairPresenterImpl::OnDiscoveryLearnMoreClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnDiscoveryDismissed,
                     weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::OnDiscoveryClicked(DiscoveryCallback callback) {
  callback.Run(DiscoveryAction::kPairToDevice);
}

void FastPairPresenterImpl::OnDiscoveryDismissed(
    scoped_refptr<Device> device,
    DiscoveryCallback callback,
    FastPairNotificationDismissReason dismiss_reason) {
  switch (dismiss_reason) {
    case FastPairNotificationDismissReason::kDismissedByUser:
      callback.Run(DiscoveryAction::kDismissedByUser);
      break;
    case FastPairNotificationDismissReason::kDismissedByOs:
      callback.Run(DiscoveryAction::kDismissedByOs);
      break;
    case FastPairNotificationDismissReason::kDismissedByTimeout:
      callback.Run(DiscoveryAction::kDismissedByTimeout);
      break;
    default:
      NOTREACHED();
  }
}

void FastPairPresenterImpl::OnDiscoveryLearnMoreClicked(
    DiscoveryCallback callback) {
  NewWindowDelegate::GetPrimary()->OpenUrl(
      GURL(kDiscoveryLearnMoreLink),
      NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
  callback.Run(DiscoveryAction::kLearnMore);
}

void FastPairPresenterImpl::ShowPairing(scoped_refptr<Device> device) {
  const auto metadata_id = device->metadata_id();
  FastPairRepository::Get()->GetDeviceMetadata(
      metadata_id,
      base::BindOnce(&FastPairPresenterImpl::OnPairingMetadataRetrieved,
                     weak_pointer_factory_.GetWeakPtr(), device));
}

void FastPairPresenterImpl::OnPairingMetadataRetrieved(
    scoped_refptr<Device> device,
    DeviceMetadata* device_metadata,
    bool has_retryable_error) {
  if (!device_metadata) {
    return;
  }

  notification_controller_->ShowPairingNotification(
      base::ASCIIToUTF16(device_metadata->GetDetails().name()),
      device_metadata->image(), base::DoNothing());
}

void FastPairPresenterImpl::ShowPairingFailed(scoped_refptr<Device> device,
                                              PairingFailedCallback callback) {
  const auto metadata_id = device->metadata_id();
  FastPairRepository::Get()->GetDeviceMetadata(
      metadata_id,
      base::BindOnce(&FastPairPresenterImpl::OnPairingFailedMetadataRetrieved,
                     weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::OnPairingFailedMetadataRetrieved(
    scoped_refptr<Device> device,
    PairingFailedCallback callback,
    DeviceMetadata* device_metadata,
    bool has_retryable_error) {
  if (!device_metadata) {
    return;
  }

  notification_controller_->ShowErrorNotification(
      base::ASCIIToUTF16(device_metadata->GetDetails().name()),
      device_metadata->image(),
      base::BindRepeating(&FastPairPresenterImpl::OnNavigateToSettings,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnPairingFailedDismissed,
                     weak_pointer_factory_.GetWeakPtr(), callback));
}

void FastPairPresenterImpl::OnNavigateToSettings(
    PairingFailedCallback callback) {
  if (TrayPopupUtils::CanOpenWebUISettings()) {
    Shell::Get()->system_tray_model()->client()->ShowBluetoothSettings();
    RecordNavigateToSettingsResult(/*success=*/true);
  } else {
    CD_LOG(WARNING, Feature::FP)
        << "Cannot open Bluetooth Settings since it's not possible "
           "to opening WebUI settings";
    RecordNavigateToSettingsResult(/*success=*/false);
  }

  callback.Run(PairingFailedAction::kNavigateToSettings);
}

void FastPairPresenterImpl::OnPairingFailedDismissed(
    PairingFailedCallback callback,
    FastPairNotificationDismissReason dismiss_reason) {
  switch (dismiss_reason) {
    case FastPairNotificationDismissReason::kDismissedByUser:
      callback.Run(PairingFailedAction::kDismissedByUser);
      break;
    case FastPairNotificationDismissReason::kDismissedByOs:
      callback.Run(PairingFailedAction::kDismissed);
      break;
    case FastPairNotificationDismissReason::kDismissedByTimeout:
      // Fast Pair Error Notifications do not have a timeout, so this is never
      // expected to be hit.
      NOTREACHED();
    default:
      NOTREACHED();
  }
}

void FastPairPresenterImpl::ShowAssociateAccount(
    scoped_refptr<Device> device,
    AssociateAccountCallback callback) {
  RecordRetroactiveSuccessFunnelFlow(
      FastPairRetroactiveSuccessFunnelEvent::kNotificationDisplayed);
  const auto metadata_id = device->metadata_id();
  FastPairRepository::Get()->GetDeviceMetadata(
      metadata_id,
      base::BindOnce(
          &FastPairPresenterImpl::OnAssociateAccountMetadataRetrieved,
          weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::OnAssociateAccountMetadataRetrieved(
    scoped_refptr<Device> device,
    AssociateAccountCallback callback,
    DeviceMetadata* device_metadata,
    bool has_retryable_error) {
  CD_LOG(VERBOSE, Feature::FP) << __func__ << ": " << device;
  if (!device_metadata) {
    return;
  }

  device->set_version(device_metadata->InferFastPairVersion());

  signin::IdentityManager* identity_manager =
      QuickPairBrowserDelegate::Get()->GetIdentityManager();
  if (!identity_manager) {
    CD_LOG(ERROR, Feature::FP)
        << __func__
        << ": IdentityManager is not available for Associate Account "
           "notification.";
    return;
  }

  const std::string email =
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
          .email;
  std::u16string device_name;
  // If the name of the device has been set by the user, use that name,
  // otherwise use the OEM default name.
  if (device->display_name().has_value()) {
    device_name = base::UTF8ToUTF16(device->display_name().value());
  } else {
    device_name = base::ASCIIToUTF16(device_metadata->GetDetails().name());
  }

  notification_controller_->ShowAssociateAccount(
      device_name, base::ASCIIToUTF16(email), device_metadata->image(),
      base::BindRepeating(
          &FastPairPresenterImpl::OnAssociateAccountActionClicked,
          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindRepeating(
          &FastPairPresenterImpl::OnAssociateAccountLearnMoreClicked,
          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnAssociateAccountDismissed,
                     weak_pointer_factory_.GetWeakPtr(), callback));
}

void FastPairPresenterImpl::OnAssociateAccountActionClicked(
    AssociateAccountCallback callback) {
  callback.Run(AssociateAccountAction::kAssociateAccount);
}

void FastPairPresenterImpl::OnAssociateAccountLearnMoreClicked(
    AssociateAccountCallback callback) {
  NewWindowDelegate::GetPrimary()->OpenUrl(
      GURL(kAssociateAccountLearnMoreLink),
      NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
  callback.Run(AssociateAccountAction::kLearnMore);
}

void FastPairPresenterImpl::OnAssociateAccountDismissed(
    AssociateAccountCallback callback,
    FastPairNotificationDismissReason dismiss_reason) {
  switch (dismiss_reason) {
    case FastPairNotificationDismissReason::kDismissedByUser:
      callback.Run(AssociateAccountAction::kDismissedByUser);
      break;
    case FastPairNotificationDismissReason::kDismissedByOs:
      callback.Run(AssociateAccountAction::kDismissedByOs);
      break;
    case FastPairNotificationDismissReason::kDismissedByTimeout:
      callback.Run(AssociateAccountAction::kDismissedByTimeout);
      break;
    default:
      NOTREACHED();
  }
}

void FastPairPresenterImpl::ShowInstallCompanionApp(
    scoped_refptr<Device> device,
    CompanionAppCallback callback) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  toast_collision_avoidance_timer_.Start(
      FROM_HERE, ash::ToastData::kDefaultToastDuration,
      base::BindOnce(&FastPairPresenterImpl::ShowInstallCompanionAppDelayed,
                     weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::ShowInstallCompanionAppDelayed(
    scoped_refptr<Device> device,
    CompanionAppCallback callback) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  const auto metadata_id = device->metadata_id();
  FastPairRepository::Get()->GetDeviceMetadata(
      metadata_id,
      base::BindOnce(
          &FastPairPresenterImpl::OnInstallCompanionAppMetadataRetrieved,
          weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::OnInstallCompanionAppMetadataRetrieved(
    scoped_refptr<Device> device,
    CompanionAppCallback callback,
    DeviceMetadata* device_metadata,
    bool has_retryable_error) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  if (!device_metadata) {
    return;
  }

  std::u16string device_name;
  // If the name of the device has been set by the user, use that name,
  // otherwise use the OEM default name.
  if (device->display_name().has_value()) {
    device_name = base::UTF8ToUTF16(device->display_name().value());
  } else {
    device_name = base::ASCIIToUTF16(device_metadata->GetDetails().name());
  }

  notification_controller_->ShowApplicationAvailableNotification(
      device_name, device_metadata->image(),
      base::BindRepeating(&FastPairPresenterImpl::OnCompanionAppInstallClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnCompanionAppDismissed,
                     weak_pointer_factory_.GetWeakPtr(), callback));
}

void FastPairPresenterImpl::ShowLaunchCompanionApp(
    scoped_refptr<Device> device,
    CompanionAppCallback callback) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  toast_collision_avoidance_timer_.Start(
      FROM_HERE, ash::ToastData::kDefaultToastDuration,
      base::BindOnce(&FastPairPresenterImpl::ShowLaunchCompanionAppDelayed,
                     weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::ShowLaunchCompanionAppDelayed(
    scoped_refptr<Device> device,
    CompanionAppCallback callback) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  const auto metadata_id = device->metadata_id();
  FastPairRepository::Get()->GetDeviceMetadata(
      metadata_id,
      base::BindOnce(
          &FastPairPresenterImpl::OnLaunchCompanionAppMetadataRetrieved,
          weak_pointer_factory_.GetWeakPtr(), device, callback));
}

void FastPairPresenterImpl::OnLaunchCompanionAppMetadataRetrieved(
    scoped_refptr<Device> device,
    CompanionAppCallback callback,
    DeviceMetadata* device_metadata,
    bool has_retryable_error) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  if (!device_metadata) {
    return;
  }

  std::u16string device_name;
  // If the name of the device has been set by the user, use that name,
  // otherwise use the OEM default name.
  if (device->display_name().has_value()) {
    device_name = base::UTF8ToUTF16(device->display_name().value());
  } else {
    device_name = base::ASCIIToUTF16(device_metadata->GetDetails().name());
  }

  notification_controller_->ShowApplicationInstalledNotification(
      // temporarily hardcoded text in place of companion app name
      device_name, device_metadata->image(), u"the web companion",
      base::BindRepeating(&FastPairPresenterImpl::OnCompanionAppSetupClicked,
                          weak_pointer_factory_.GetWeakPtr(), callback),
      base::BindOnce(&FastPairPresenterImpl::OnCompanionAppDismissed,
                     weak_pointer_factory_.GetWeakPtr(), callback));
}

void FastPairPresenterImpl::OnCompanionAppInstallClicked(
    CompanionAppCallback callback) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  callback.Run(CompanionAppAction::kDownloadAndLaunchApp);
}

void FastPairPresenterImpl::OnCompanionAppSetupClicked(
    CompanionAppCallback callback) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  callback.Run(CompanionAppAction::kLaunchApp);
}

void FastPairPresenterImpl::OnCompanionAppDismissed(
    CompanionAppCallback callback,
    FastPairNotificationDismissReason dismiss_reason) {
  CHECK(features::IsFastPairPwaCompanionEnabled());

  switch (dismiss_reason) {
    case FastPairNotificationDismissReason::kDismissedByUser:
      callback.Run(CompanionAppAction::kDismissedByUser);
      break;
    case FastPairNotificationDismissReason::kDismissedByOs:
      [[fallthrough]];
    case FastPairNotificationDismissReason::kDismissedByTimeout:
      callback.Run(CompanionAppAction::kDismissed);
      break;
    default:
      NOTREACHED();
  }
}

void FastPairPresenterImpl::RemoveNotifications() {
  notification_controller_->RemoveNotifications();
}

void FastPairPresenterImpl::ExtendNotification() {
  notification_controller_->ExtendNotification();
}

}  // namespace quick_pair
}  // namespace ash