// 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/pairing/retroactive_pairing_detector_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/quick_pair/common/constants.h"
#include "ash/quick_pair/common/device.h"
#include "ash/quick_pair/common/protocol.h"
#include "ash/quick_pair/fast_pair_handshake/fast_pair_gatt_service_client_lookup_impl.h"
#include "ash/quick_pair/message_stream/message_stream.h"
#include "ash/quick_pair/repository/fast_pair_repository.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.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/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/cross_device/logging/logging.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/floss/floss_features.h"
namespace {
bool ShouldBeEnabledForLoginStatus(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;
}
}
// Enforce a 60 second timeout to discover a device for the retroactive pairing
// scenario to align with Android implementation and adhere to the Fast Pair
// spec where providers can only allow account keys to be written within the
// 60 seconds following classic Bluetooth pairing:
// https://developers.google.com/nearby/fast-pair/specifications/extensions/retroactiveacctkey#RetroactivelyWritingAccountKey
// The device is expected to enforce this requirement, however as mentioned
// above, to align with Android, ChromeOS will include consideration for the
// 60 seconds expected to retroactively write the account key.
constexpr base::TimeDelta kRetroactiveDevicePairingTimeout = base::Seconds(60);
} // namespace
namespace ash {
namespace quick_pair {
RetroactivePairingDetectorImpl::RetroactivePairingDetectorImpl(
PairerBroker* pairer_broker,
MessageStreamLookup* message_stream_lookup)
: pairer_broker_(pairer_broker),
message_stream_lookup_(message_stream_lookup) {
// If there is no signed in user, don't enabled the retroactive pairing
// scenario, so don't initiate any objects or observations, but store the
// pointers in the case that we get logged in later on.
if (!ShouldBeEnabledForLoginStatus(
Shell::Get()->session_controller()->login_status())) {
CD_LOG(INFO, Feature::FP)
<< __func__
<< ": No logged in user to enable retroactive pairing scenario";
// Observe log in events in the case the login was delayed.
shell_observation_.Observe(Shell::Get()->session_controller());
return;
}
// If we get to this point in the constructor, it means that the user is
// logged in to enable this scenario, so we can being our observations. If we
// get any log in events, we know to ignore them, since we already
// instantiated our retroactive pairing detector.
retroactive_pairing_detector_instatiated_ = true;
device::BluetoothAdapterFactory::Get()->GetAdapter(
base::BindOnce(&RetroactivePairingDetectorImpl::OnGetAdapter,
weak_ptr_factory_.GetWeakPtr()));
message_stream_lookup_observation_.Observe(message_stream_lookup_.get());
pairer_broker_observation_.Observe(pairer_broker_.get());
}
void RetroactivePairingDetectorImpl::OnLoginStatusChanged(
LoginStatus login_status) {
if (!ShouldBeEnabledForLoginStatus(login_status) || !pairer_broker_ ||
!message_stream_lookup_ || retroactive_pairing_detector_instatiated_) {
return;
}
CD_LOG(VERBOSE, Feature::FP)
<< __func__
<< ": Logged in user, instantiate retroactive pairing scenario.";
retroactive_pairing_detector_instatiated_ = true;
device::BluetoothAdapterFactory::Get()->GetAdapter(
base::BindOnce(&RetroactivePairingDetectorImpl::OnGetAdapter,
weak_ptr_factory_.GetWeakPtr()));
message_stream_lookup_observation_.Observe(message_stream_lookup_.get());
pairer_broker_observation_.Observe(pairer_broker_.get());
}
void RetroactivePairingDetectorImpl::OnGetAdapter(
scoped_refptr<device::BluetoothAdapter> adapter) {
adapter_ = adapter;
adapter_observation_.Reset();
adapter_observation_.Observe(adapter_.get());
}
RetroactivePairingDetectorImpl::~RetroactivePairingDetectorImpl() {
// Remove any observation of remaining MessageStreams.
for (auto it = message_streams_.begin(); it != message_streams_.end(); it++) {
it->second->RemoveObserver(this);
}
}
void RetroactivePairingDetectorImpl::AddObserver(
RetroactivePairingDetector::Observer* observer) {
observers_.AddObserver(observer);
}
void RetroactivePairingDetectorImpl::RemoveObserver(
RetroactivePairingDetector::Observer* observer) {
observers_.RemoveObserver(observer);
}
void RetroactivePairingDetectorImpl::OnDevicePaired(
scoped_refptr<Device> device) {
// The classic address is assigned to the Device during the
// initial Fast Pair pairing protocol and if it doesn't exist,
// then it wasn't properly paired during initial Fast Pair
// pairing.
if (!device->classic_address()) {
return;
}
// The Bluetooth Adapter system event `DevicePairedChanged` fires before
// Fast Pair's `OnDevicePaired`, and a Fast Pair pairing is expected to have
// both events. If a device is Fast Paired, it is already inserted in the
// |potential_retroactive_addresses_| in `DevicePairedChanged`; we need to
// remove it to prevent a false positive.
if (base::Contains(potential_retroactive_addresses_,
device->classic_address().value())) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__
<< ": encountered a false positive for a potential retroactive pairing "
"device. Removing device at address = "
<< device->classic_address().value();
RemoveDeviceInformation(device->classic_address().value());
return;
}
}
void RetroactivePairingDetectorImpl::DevicePairedChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
bool new_paired_status) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__ << ": " << device->GetNameForDisplay()
<< " new_paired_status=" << (new_paired_status ? "paired" : "not paired");
// This event fires whenever a device pairing has changed with the adapter.
// If the |new_paired_status| is false, it means a device was unpaired with
// the adapter, so we early return since it would not be a device to
// retroactively pair to.
if (!new_paired_status) {
return;
}
// Both classic paired and Fast paired devices call this function, so we
// have to add the device to |potential_retroactive_addresses_|. We expect
// devices paired via Fast Pair to always call `OnDevicePaired` after calling
// this function, which will remove the device from
// |potential_retroactive_addresses_|.
const std::string& classic_address = device->GetAddress();
potential_retroactive_addresses_.insert(classic_address);
AddDevicePairingInformation(classic_address);
// In order to confirm that this device is a retroactive pairing, we need to
// first check if it has already been saved to the user's account. If it has
// already been saved, we don't want to prompt the user to save a device
// again.
FastPairRepository::Get()->IsDeviceSavedToAccount(
classic_address,
base::BindOnce(&RetroactivePairingDetectorImpl::AttemptRetroactivePairing,
weak_ptr_factory_.GetWeakPtr(), classic_address));
}
void RetroactivePairingDetectorImpl::AttemptRetroactivePairing(
const std::string& classic_address,
bool is_device_saved_to_account) {
// This check handles the case where the request for checked if the device is
// saved takes longer than expected. We register `AttemptRetroactivePairing`
// as a callback for when this request completes, but it gets called after
// we get the call to `OnDevicePaired`, which removes the device information.
// If the device is removed via `OnDevicePaired`, this indicated a Fast Pair
// pairing event, in which case we will never show a retroactive pairing
// notification, so we can stop the flow here for this device.
if (!base::Contains(potential_retroactive_addresses_, classic_address)) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__ << ": device at " << classic_address
<< ": was removed before call to Footprints completed";
return;
}
if (is_device_saved_to_account) {
CD_LOG(INFO, Feature::FP)
<< __func__ << ": device already saved to user's account";
RemoveDeviceInformation(classic_address);
return;
}
device::BluetoothDevice* device = adapter_->GetDevice(classic_address);
if (!device) {
CD_LOG(WARNING, Feature::FP)
<< __func__ << ": Lost device to potentially retroactively pair to.";
RemoveDeviceInformation(classic_address);
return;
}
CD_LOG(VERBOSE, Feature::FP) << __func__ << ": device = " << classic_address;
// For BLE devices, check it supports Fast Pair. Then, since the message
// stream is optional for BLE HIDs, and the BLE address is already known, the
// only remaining parameter needed is the model ID, which we retrieve via GATT
// characteristic.
if (ash::features::IsFastPairHIDEnabled() &&
// Fast Pair HID only works on Floss.
floss::features::IsFlossEnabled() &&
device->GetType() == device::BLUETOOTH_TRANSPORT_LE &&
base::Contains(device->GetUUIDs(), kFastPairBluetoothUuid)) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__
<< ": BLE fast pair device detected, creating GATT connection";
CreateGattConnection(device);
return;
}
// Attempt to retrieve a MessageStream instance immediately, if it was
// already connected.
MessageStream* message_stream =
message_stream_lookup_->GetMessageStream(classic_address);
if (!message_stream) {
return;
}
message_streams_[classic_address] = message_stream;
GetModelIdAndAddressFromMessageStream(classic_address, message_stream);
}
void RetroactivePairingDetectorImpl::CreateGattConnection(
device::BluetoothDevice* device) {
auto* fast_pair_gatt_service_client =
FastPairGattServiceClientLookup::GetInstance()->Get(device);
if (fast_pair_gatt_service_client) {
if (fast_pair_gatt_service_client->IsConnected()) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__
<< ": Reusing existing GATT service client to retrieve model ID";
fast_pair_gatt_service_client->ReadModelIdAsync(
base::BindOnce(&RetroactivePairingDetectorImpl::OnReadModelId,
weak_ptr_factory_.GetWeakPtr(), device->GetAddress()));
return;
} else {
// If the previous gatt service client did not connect successfully
// or is no longer connected, erase it before attempting to create a new
// gatt connection for the device.
FastPairGattServiceClientLookup::GetInstance()->Erase(device);
}
}
CD_LOG(VERBOSE, Feature::FP)
<< __func__ << ": Creating new GATT service client to retrieve model ID";
FastPairGattServiceClientLookup::GetInstance()->Create(
adapter_, device,
base::BindOnce(
&RetroactivePairingDetectorImpl::OnGattClientInitializedCallback,
weak_ptr_factory_.GetWeakPtr(), device->GetAddress()));
}
void RetroactivePairingDetectorImpl::OnGattClientInitializedCallback(
const std::string& address,
std::optional<PairFailure> failure) {
if (failure) {
CD_LOG(WARNING, Feature::FP)
<< __func__
<< ": Failed to initialize GATT service client with failure = "
<< failure.value();
return;
}
device::BluetoothDevice* device = adapter_->GetDevice(address);
if (!device) {
CD_LOG(WARNING, Feature::FP)
<< __func__ << ": Lost device to potentially retroactively pair to.";
return;
}
auto* fast_pair_gatt_service_client =
FastPairGattServiceClientLookup::GetInstance()->Get(device);
if (!fast_pair_gatt_service_client ||
!fast_pair_gatt_service_client->IsConnected()) {
CD_LOG(WARNING, Feature::FP) << __func__
<< ": Fast Pair Gatt Service Client failed to "
"be created or is no longer connected.";
FastPairGattServiceClientLookup::GetInstance()->Erase(device);
return;
}
CD_LOG(VERBOSE, Feature::FP) << __func__
<< ": Fast Pair GATT service client initialized "
"successfully. Reading Model ID.";
fast_pair_gatt_service_client->ReadModelIdAsync(
base::BindOnce(&RetroactivePairingDetectorImpl::OnReadModelId,
weak_ptr_factory_.GetWeakPtr(), device->GetAddress()));
}
void RetroactivePairingDetectorImpl::OnReadModelId(
const std::string& address,
std::optional<device::BluetoothGattService::GattErrorCode> error_code,
const std::vector<uint8_t>& value) {
if (error_code) {
CD_LOG(WARNING, Feature::FP)
<< __func__ << ": Failed to read model ID with failure = "
<< static_cast<uint32_t>(error_code.value());
return;
}
if (value.size() != 3) {
CD_LOG(WARNING, Feature::FP) << __func__ << ": model ID malformed.";
return;
}
std::string model_id;
for (auto byte : value) {
model_id.append(base::StringPrintf("%02X", byte));
}
CD_LOG(INFO, Feature::FP) << __func__ << ": Model ID " << model_id
<< " found for device " << address;
NotifyDeviceFound(model_id, address, address);
}
void RetroactivePairingDetectorImpl::OnMessageStreamConnected(
const std::string& device_address,
MessageStream* message_stream) {
CD_LOG(VERBOSE, Feature::FP) << __func__ << ":" << device_address;
if (!message_stream) {
return;
}
if (!base::Contains(potential_retroactive_addresses_, device_address)) {
return;
}
message_streams_[device_address] = message_stream;
GetModelIdAndAddressFromMessageStream(device_address, message_stream);
}
void RetroactivePairingDetectorImpl::AddDevicePairingInformation(
const std::string& device_address) {
CD_LOG(VERBOSE, Feature::FP) << __func__;
// There is potential for the device at |device_address| to already be in
// the map (in the case of repairing for example). If it is already in the
// map, update the timeout with the new timestamp. If it isn't already in
// the map, create a value with default empty values, and add the expiry
// timeout.
device_pairing_information_[device_address].expiry_timestamp =
base::Time::Now() + kRetroactiveDevicePairingTimeout;
// Anytime |device_pairing_information_| is updated, parse list to remove
// expired devices.
RemoveExpiredDevicesFromStoredDeviceData();
}
void RetroactivePairingDetectorImpl::GetModelIdAndAddressFromMessageStream(
const std::string& device_address,
MessageStream* message_stream) {
DCHECK(message_stream);
// The device at |device_address| is expected to be added in
// `AddDevicePairingInformation` once discovered.
DCHECK(base::Contains(device_pairing_information_, device_address));
// If the MessageStream is immediately available and |DevicePairedChanged|
// fires before FastPair's |OnDevicePaired|, it might be possible for us to
// find a false positive for a retroactive pairing scenario which we mitigate
// here.
if (!base::Contains(potential_retroactive_addresses_, device_address)) {
return;
}
// Iterate over messages for ble address and model id, which is what we
// need for retroactive pairing.
for (auto& message : message_stream->messages()) {
if (message->is_model_id()) {
device_pairing_information_[device_address].model_id =
message->get_model_id();
} else if (message->is_ble_address_update()) {
device_pairing_information_[device_address].ble_address =
message->get_ble_address_update();
}
}
// If we don't have model id and ble address for device, then we will add
// ourselves as an observer and wait for these messages to come in. There is
// a possibility that they will not come in if the device does not
// support retroactive pairing.
if (device_pairing_information_[device_address].model_id.empty() ||
device_pairing_information_[device_address].ble_address.empty()) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__ << ": BLE address = "
<< (device_pairing_information_[device_address].ble_address.empty()
? "empty"
: device_pairing_information_[device_address].ble_address)
<< " model ID = "
<< (device_pairing_information_[device_address].model_id.empty()
? "empty"
: device_pairing_information_[device_address].model_id)
<< " observing Message Stream for future messages for device = "
<< device_address;
message_stream->AddObserver(this);
return;
}
// At this point, we have both the model id and BLE address for the device,
// but we check if it has reached its expiry timeout. If so, we do not
// notify of the scenario being detected. `CheckAndRemoveIfDeviceExpired`
// will remove corresponding device information if it has expired.
if (CheckAndRemoveIfDeviceExpired(device_address)) {
return;
}
NotifyDeviceFound(device_pairing_information_[device_address].model_id,
device_pairing_information_[device_address].ble_address,
device_address);
}
bool RetroactivePairingDetectorImpl::CheckAndRemoveIfDeviceExpired(
const std::string& device_address) {
if (base::Time::Now() >=
device_pairing_information_[device_address].expiry_timestamp) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__ << ": device at " << device_address
<< " has exceeded the time allotted for detecting "
"retroactive scenario. Removing device information.";
RemoveDeviceInformation(device_address);
return true;
}
return false;
}
void RetroactivePairingDetectorImpl::OnModelIdMessage(
const std::string& device_address,
const std::string& model_id) {
CD_LOG(VERBOSE, Feature::FP) << __func__ << ": model id = " << model_id
<< "for device = " << device_address;
device_pairing_information_[device_address].model_id = model_id;
CheckPairingInformation(device_address);
}
void RetroactivePairingDetectorImpl::OnBleAddressUpdateMessage(
const std::string& device_address,
const std::string& ble_address) {
CD_LOG(VERBOSE, Feature::FP) << __func__ << ": ble address " << ble_address
<< " for device = " << device_address;
device_pairing_information_[device_address].ble_address = ble_address;
CheckPairingInformation(device_address);
}
void RetroactivePairingDetectorImpl::CheckPairingInformation(
const std::string& device_address) {
// The device at |device_address| is expected to be added in
// `AddDevicePairingInformation` once discovered.
DCHECK(base::Contains(device_pairing_information_, device_address));
// If the MessageStream is immediately available and |DevicePairedChanged|
// fires before FastPair's |OnDevicePaired|, it might be possible for us to
// find a false positive for a retroactive pairing scenario which we mitigate
// here. Also check if the device has expired for detecting scenario, if so
// do not continue. `CheckAndRemoveIfDeviceExpired` will remove device
// information if it has expired.
if (!base::Contains(potential_retroactive_addresses_, device_address) ||
CheckAndRemoveIfDeviceExpired(device_address)) {
return;
}
if (device_pairing_information_[device_address].model_id.empty() ||
device_pairing_information_[device_address].ble_address.empty()) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__
<< ": don't have both model id and ble address for device = "
<< device_address;
return;
}
NotifyDeviceFound(device_pairing_information_[device_address].model_id,
device_pairing_information_[device_address].ble_address,
device_address);
}
void RetroactivePairingDetectorImpl::OnDisconnected(
const std::string& device_address) {
CD_LOG(VERBOSE, Feature::FP) << __func__;
message_streams_[device_address]->RemoveObserver(this);
message_streams_.erase(device_address);
}
void RetroactivePairingDetectorImpl::OnMessageStreamDestroyed(
const std::string& device_address) {
CD_LOG(VERBOSE, Feature::FP) << __func__;
message_streams_[device_address]->RemoveObserver(this);
message_streams_.erase(device_address);
}
void RetroactivePairingDetectorImpl::NotifyDeviceFound(
const std::string& model_id,
const std::string& ble_address,
const std::string& classic_address) {
CD_LOG(INFO, Feature::FP) << __func__;
// Before we notify that the device is found for retroactive pairing, we
// should check if the user is opted in to saving devices to their account.
// The reason why we check this every time we want to notify a device is found
// rather than having the user's opt-in status determine whether or not the
// retroactive pairing scenario is instantiated is because the user might be
// opted out when the user initially logs in to the Chromebook (when this
// class is created), but then opted-in later one, and then unable to save
// devices to their account, or vice versa. By checking every time we want
// to notify a device is found, we can accurately reflect a user's status
// in the moment. This is flagged on whether the user has the Fast Pair
// Saved Devices flag enabled.
if (features::IsFastPairSavedDevicesEnabled() &&
features::IsFastPairSavedDevicesStrictOptInEnabled()) {
FastPairRepository::Get()->CheckOptInStatus(
base::BindOnce(&RetroactivePairingDetectorImpl::OnCheckOptInStatus,
weak_ptr_factory_.GetWeakPtr(), model_id, ble_address,
classic_address));
return;
}
// If the SavedDevices flag is not enabled, we don't have to check opt in
// status and can move forward with verifying the device found.
VerifyDeviceFound(model_id, ble_address, classic_address);
}
void RetroactivePairingDetectorImpl::OnCheckOptInStatus(
const std::string& model_id,
const std::string& ble_address,
const std::string& classic_address,
nearby::fastpair::OptInStatus status) {
CD_LOG(INFO, Feature::FP) << __func__;
if (status != nearby::fastpair::OptInStatus::STATUS_OPTED_IN) {
CD_LOG(INFO, Feature::FP)
<< __func__
<< ": User is not opted in to save devices to their account";
RemoveDeviceInformation(classic_address);
return;
}
VerifyDeviceFound(model_id, ble_address, classic_address);
}
void RetroactivePairingDetectorImpl::VerifyDeviceFound(
const std::string& model_id,
const std::string& ble_address,
const std::string& classic_address) {
CD_LOG(INFO, Feature::FP) << __func__;
device::BluetoothDevice* bluetooth_device =
adapter_->GetDevice(classic_address);
if (!bluetooth_device) {
CD_LOG(WARNING, Feature::FP)
<< __func__ << ": Lost device to potentially retroactively pair to.";
RemoveDeviceInformation(classic_address);
return;
}
auto device = base::MakeRefCounted<Device>(model_id, ble_address,
Protocol::kFastPairRetroactive);
device->set_classic_address(classic_address);
device->set_display_name(bluetooth_device->GetName());
CD_LOG(INFO, Feature::FP)
<< __func__ << ": Found device for Retroactive Pairing " << device;
for (auto& observer : observers_) {
observer.OnRetroactivePairFound(device);
}
DCHECK(device->classic_address());
RemoveDeviceInformation(device->classic_address().value());
}
void RetroactivePairingDetectorImpl::RemoveDeviceInformation(
const std::string& device_address) {
CD_LOG(VERBOSE, Feature::FP) << __func__ << ": device = " << device_address;
RemoveDeviceInformationHelper(device_address);
// Anytime |device_pairing_information_| is updated, parse list to remove
// expired devices.
RemoveExpiredDevicesFromStoredDeviceData();
}
void RetroactivePairingDetectorImpl::RemoveDeviceInformationHelper(
const std::string& device_address) {
CD_LOG(INFO, Feature::FP) << __func__;
potential_retroactive_addresses_.erase(device_address);
device_pairing_information_.erase(device_address);
// We can potentially get to a state where we need to RemoveDeviceInformation
// before the MessageStreams are observed, connected, and/or added to our
// list here if we get a false positive instance of a potential retroactive
// pairing device.
if (base::Contains(message_streams_, device_address)) {
message_streams_[device_address]->RemoveObserver(this);
message_streams_.erase(device_address);
}
}
void RetroactivePairingDetectorImpl::
RemoveExpiredDevicesFromStoredDeviceData() {
// If the RetroactivePairingDetector never receives the model id or
// BLE address from the MessageStream, it will not be removed in
// `CheckPairingInformation` if it has exceeded the allotted time for
// detecting the scenario (kDetectRetroactiveScenarioTimeout). We clean up
// these devices here.
std::vector<std::string> devices_to_remove;
for (auto it = device_pairing_information_.begin();
it != device_pairing_information_.end(); ++it) {
if (base::Time::Now() >= it->second.expiry_timestamp) {
devices_to_remove.push_back(it->first);
}
}
for (const std::string& device_address : devices_to_remove) {
CD_LOG(VERBOSE, Feature::FP)
<< __func__ << ": Removing device at " << device_address
<< "that has exceeded the time allotted for detecting "
"retroactive scenario.";
RemoveDeviceInformationHelper(device_address);
}
}
void RetroactivePairingDetectorImpl::OnPairFailure(scoped_refptr<Device> device,
PairFailure failure) {}
void RetroactivePairingDetectorImpl::OnAccountKeyWrite(
scoped_refptr<Device> device,
std::optional<AccountKeyFailure> error) {}
} // namespace quick_pair
} // namespace ash