// 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 "chromeos/ash/components/hid_detection/bluetooth_hid_detector_impl.h"
#include "ash/public/cpp/bluetooth_config_service.h"
#include "base/strings/utf_string_conversions.h"
#include "components/device_event_log/device_event_log.h"
#include "hid_detection_utils.h"
namespace ash::hid_detection {
namespace {
using bluetooth_config::mojom::BluetoothDevicePropertiesPtr;
using bluetooth_config::mojom::BluetoothSystemState;
using bluetooth_config::mojom::DeviceType;
using bluetooth_config::mojom::KeyEnteredHandler;
// Returns the BluetoothHidType corresponding with |device|'s device type, or
// std::nullopt if |device| is not a HID.
std::optional<BluetoothHidDetector::BluetoothHidType> GetBluetoothHidType(
const BluetoothDevicePropertiesPtr& device) {
switch (device->device_type) {
case DeviceType::kMouse:
[[fallthrough]];
case DeviceType::kTablet:
return BluetoothHidDetector::BluetoothHidType::kPointer;
case DeviceType::kKeyboard:
return BluetoothHidDetector::BluetoothHidType::kKeyboard;
case DeviceType::kKeyboardMouseCombo:
return BluetoothHidDetector::BluetoothHidType::kKeyboardPointerCombo;
default:
return std::nullopt;
}
}
} // namespace
BluetoothHidDetectorImpl::BluetoothHidDetectorImpl() = default;
BluetoothHidDetectorImpl::~BluetoothHidDetectorImpl() {
DCHECK_EQ(kNotStarted, state_) << " HID detection must be stopped before "
<< "BluetoothHidDetectorImpl is destroyed.";
}
void BluetoothHidDetectorImpl::SetInputDevicesStatus(
InputDevicesStatus input_devices_status) {
HID_LOG(EVENT) << "Input devices status set, pointer missing: "
<< input_devices_status.pointer_is_missing
<< ", keyboard missing: "
<< input_devices_status.keyboard_is_missing;
input_devices_status_ = input_devices_status;
if (!current_pairing_device_)
return;
std::optional<BluetoothHidDetector::BluetoothHidType>
current_pairing_device_hid_type =
GetBluetoothHidType(current_pairing_device_.value());
DCHECK(current_pairing_device_hid_type)
<< current_pairing_device_.value()->id
<< " does not have a valid HID type, device type: "
<< current_pairing_device_.value()->device_type;
if (IsHidTypeMissing(current_pairing_device_hid_type.value()))
return;
// If the HID type of |current_pairing_device_| is no longer missing, this can
// mean 2 things:
// 1. |current_pairing_device_| has successfully finished pairing, and the
// client of this class has now invoked this method to inform this class
// that the HID type is no longer missing. Clear the current pairing state
// and process the next device in the queue.
// 2. |current_pairing_device_| is currently pairing, and a device of the
// same type has been detected as connected. Clear the current pairing
// state, which will cause the current pairing to cancel, and process the
// next device in the queue.
HID_LOG(EVENT) << "Device type of "
<< current_pairing_device_.value()->device_type << " for "
<< current_pairing_device_.value()->id
<< " is no longer missing";
ClearCurrentPairingState();
}
const BluetoothHidDetector::BluetoothHidDetectionStatus
BluetoothHidDetectorImpl::GetBluetoothHidDetectionStatus() {
if (!current_pairing_device_.has_value()) {
return BluetoothHidDetectionStatus(
/*current_pairing_device*/ std::nullopt,
/*pairing_state=*/std::nullopt);
}
std::optional<BluetoothHidPairingState> pairing_state;
if (current_pairing_state_.has_value()) {
pairing_state = BluetoothHidPairingState{
current_pairing_state_.value().code,
current_pairing_state_.value().num_keys_entered};
}
std::optional<BluetoothHidType> hid_type =
GetBluetoothHidType(current_pairing_device_.value());
DCHECK(hid_type) << " |current_pairing_device_| has an invalid HID type";
return BluetoothHidDetectionStatus{
BluetoothHidMetadata{
base::UTF16ToUTF8(current_pairing_device_.value()->public_name),
hid_type.value()},
std::move(pairing_state)};
}
void BluetoothHidDetectorImpl::PerformStartBluetoothHidDetection(
InputDevicesStatus input_devices_status) {
DCHECK_EQ(kNotStarted, state_);
HID_LOG(EVENT) << "Starting Bluetooth HID detection, pointer missing: "
<< input_devices_status.pointer_is_missing
<< ", keyboard missing: "
<< input_devices_status.keyboard_is_missing;
input_devices_status_ = input_devices_status;
state_ = kStarting;
num_pairing_attempts_ = 0;
GetBluetoothConfigService(
cros_bluetooth_config_remote_.BindNewPipeAndPassReceiver());
cros_bluetooth_config_remote_->ObserveSystemProperties(
system_properties_observer_receiver_.BindNewPipeAndPassRemote());
}
void BluetoothHidDetectorImpl::PerformStopBluetoothHidDetection(
bool is_using_bluetooth) {
DCHECK_NE(kNotStarted, state_)
<< " Call to StopBluetoothHidDetection() while "
<< "HID detection is inactive.";
HID_LOG(EVENT) << "Stopping Bluetooth HID detection, |is_using_bluetooth|: "
<< is_using_bluetooth;
hid_detection::RecordBluetoothPairingAttempts(num_pairing_attempts_);
state_ = kNotStarted;
cros_bluetooth_config_remote_->SetBluetoothHidDetectionInactive(
is_using_bluetooth);
cros_bluetooth_config_remote_.reset();
system_properties_observer_receiver_.reset();
ResetDiscoveryState();
}
void BluetoothHidDetectorImpl::OnPropertiesUpdated(
bluetooth_config::mojom::BluetoothSystemPropertiesPtr properties) {
switch (state_) {
case kNotStarted:
NOTREACHED_IN_MIGRATION()
<< "SystemPropertiesObserver should not be bound while in "
"state |kNotStarted|";
return;
case kStarting:
if (properties->system_state == BluetoothSystemState::kEnabled) {
HID_LOG(EVENT)
<< "Bluetooth adapter is already enabled, starting discovery";
state_ = kDetecting;
cros_bluetooth_config_remote_->StartDiscovery(
bluetooth_discovery_delegate_receiver_.BindNewPipeAndPassRemote());
} else if (properties->system_state == BluetoothSystemState::kDisabled ||
properties->system_state == BluetoothSystemState::kDisabling) {
HID_LOG(EVENT) << "Bluetooth adapter is disabled or disabling, "
<< "enabling adapter";
state_ = kEnablingAdapter;
cros_bluetooth_config_remote_->SetBluetoothEnabledWithoutPersistence();
} else {
HID_LOG(EVENT)
<< "Bluetooth adapter is unavailable or enabling, waiting "
<< "for next state change";
}
return;
case kEnablingAdapter:
if (properties->system_state == BluetoothSystemState::kEnabled) {
HID_LOG(EVENT)
<< "Bluetooth adapter has become enabled, starting discovery";
state_ = kDetecting;
cros_bluetooth_config_remote_->StartDiscovery(
bluetooth_discovery_delegate_receiver_.BindNewPipeAndPassRemote());
}
return;
case kDetecting:
if (properties->system_state != BluetoothSystemState::kEnabled) {
HID_LOG(EVENT) << "Bluetooth adapter has stopped being enabled while "
<< "Bluetooth HID detection is in progress";
state_ = kStoppedExternally;
}
return;
case kStoppedExternally:
if (properties->system_state == BluetoothSystemState::kEnabled) {
HID_LOG(EVENT) << "Bluetooth adapter has become enabled after being "
<< "unenabled externally, starting discovery";
state_ = kDetecting;
cros_bluetooth_config_remote_->StartDiscovery(
bluetooth_discovery_delegate_receiver_.BindNewPipeAndPassRemote());
}
return;
}
}
void BluetoothHidDetectorImpl::OnBluetoothDiscoveryStarted(
mojo::PendingRemote<bluetooth_config::mojom::DevicePairingHandler>
handler) {
HID_LOG(EVENT) << "Bluetooth discovery started.";
DCHECK(!device_pairing_handler_remote_);
device_pairing_handler_remote_.Bind(std::move(handler));
}
void BluetoothHidDetectorImpl::OnBluetoothDiscoveryStopped() {
HID_LOG(EVENT) << "Bluetooth discovery stopped.";
ResetDiscoveryState();
}
void BluetoothHidDetectorImpl::OnDiscoveredDevicesListChanged(
std::vector<BluetoothDevicePropertiesPtr> discovered_devices) {
for (const auto& discovered_device : discovered_devices) {
if (!ShouldAttemptToPairWithDevice(discovered_device))
continue;
if (queued_device_ids_.contains(discovered_device->id))
continue;
queued_device_ids_.insert(discovered_device->id);
queue_->emplace(discovered_device.Clone());
HID_LOG(EVENT) << "Queuing device: " << discovered_device->id << ". ["
<< queue_->size() << "] devices now in queue.";
}
ProcessQueue();
}
void BluetoothHidDetectorImpl::RequestPinCode(RequestPinCodeCallback callback) {
DCHECK(current_pairing_device_)
<< "RequestPinCode() called with no |current_pairing_device_|";
// RequestPinCode auth is not attributed to HIDs, cancel the pairing.
HID_LOG(EVENT) << "RequestPinCode auth required for "
<< current_pairing_device_.value()->id
<< ", cancelling pairing";
ClearCurrentPairingState();
}
void BluetoothHidDetectorImpl::RequestPasskey(RequestPasskeyCallback callback) {
DCHECK(current_pairing_device_)
<< "RequestPasskey() called with no |current_pairing_device_|";
// RequestPasskey auth is not attributed to HIDs, cancel the pairing.
HID_LOG(EVENT) << "RequestPasskey auth required for "
<< current_pairing_device_.value()->id
<< ", cancelling pairing";
ClearCurrentPairingState();
}
void BluetoothHidDetectorImpl::DisplayPinCode(
const std::string& pin_code,
mojo::PendingReceiver<KeyEnteredHandler> handler) {
DCHECK(current_pairing_device_)
<< "DisplayPinCode() called with no |current_pairing_device_|";
HID_LOG(EVENT) << "DisplayPinCode auth required for "
<< current_pairing_device_.value()->id
<< ", pin code: " << pin_code;
RequirePairingCode(pin_code, std::move(handler));
}
void BluetoothHidDetectorImpl::DisplayPasskey(
const std::string& passkey,
mojo::PendingReceiver<KeyEnteredHandler> handler) {
DCHECK(current_pairing_device_)
<< "DisplayPasskey() called with no |current_pairing_device_|";
HID_LOG(EVENT) << "DisplayPasskey auth required for "
<< current_pairing_device_.value()->id
<< ", passkey: " << passkey;
RequirePairingCode(passkey, std::move(handler));
}
void BluetoothHidDetectorImpl::ConfirmPasskey(const std::string& passkey,
ConfirmPasskeyCallback callback) {
DCHECK(current_pairing_device_)
<< "ConfirmPasskey() called with no |current_pairing_device_|";
// ConfirmPasskey auth is not attributed to HIDs, cancel the pairing.
HID_LOG(EVENT) << "ConfirmPasskey auth required for "
<< current_pairing_device_.value()->id
<< ", cancelling pairing";
ClearCurrentPairingState();
}
void BluetoothHidDetectorImpl::AuthorizePairing(
AuthorizePairingCallback callback) {
DCHECK(current_pairing_device_)
<< "AuthorizePairing() called with no |current_pairing_device_|";
HID_LOG(EVENT) << "AuthorizePairing auth required for "
<< current_pairing_device_.value()->id
<< ", automatically authorizing";
std::move(callback).Run(/*confirmed=*/true);
}
void BluetoothHidDetectorImpl::HandleKeyEntered(uint8_t num_keys_entered) {
DCHECK(current_pairing_device_)
<< "HandleKeyEntered() called with no |current_pairing_device_|";
DCHECK(current_pairing_state_)
<< "HandleKeyEntered() called with no |current_pairing_state_|";
HID_LOG(EVENT) << "HandleKeyEntered called with "
<< static_cast<unsigned>(num_keys_entered) << " keys entered";
current_pairing_state_->num_keys_entered = num_keys_entered;
NotifyBluetoothHidDetectionStatusChanged();
}
bool BluetoothHidDetectorImpl::IsHidTypeMissing(
BluetoothHidDetector::BluetoothHidType hid_type) {
switch (hid_type) {
case BluetoothHidDetector::BluetoothHidType::kPointer:
return input_devices_status_.pointer_is_missing;
case BluetoothHidDetector::BluetoothHidType::kKeyboard:
return input_devices_status_.keyboard_is_missing;
case BluetoothHidDetector::BluetoothHidType::kKeyboardPointerCombo:
return input_devices_status_.pointer_is_missing ||
input_devices_status_.keyboard_is_missing;
}
}
bool BluetoothHidDetectorImpl::ShouldAttemptToPairWithDevice(
const BluetoothDevicePropertiesPtr& device) {
std::optional<BluetoothHidDetector::BluetoothHidType> hid_type =
GetBluetoothHidType(device);
if (!hid_type)
return false;
return IsHidTypeMissing(hid_type.value());
}
void BluetoothHidDetectorImpl::ProcessQueue() {
if (current_pairing_device_)
return;
if (queue_->empty()) {
HID_LOG(DEBUG) << "No devices queued";
return;
}
current_pairing_device_ = std::move(queue_->front());
queue_->pop();
HID_LOG(EVENT) << "Popped device with id: "
<< current_pairing_device_.value()->id
<< " from front of queue. [" << queue_->size()
<< "] devices now in queue.";
if (!ShouldAttemptToPairWithDevice(current_pairing_device_.value())) {
HID_LOG(EVENT) << "Device with id " << current_pairing_device_.value()->id
<< " no longer should be attempted to be paired with, "
<< "processing next device in queue. Device type: "
<< current_pairing_device_.value()->device_type;
current_pairing_device_.reset();
ProcessQueue();
return;
}
HID_LOG(EVENT) << "Pairing with device with id: "
<< current_pairing_device_.value()->id;
++num_pairing_attempts_;
// Start a timer to make sure that the queue never gets stuck, such as in
// b/242358619.
current_pairing_timer_.Start(
FROM_HERE, kMaxPairingSessionDuration,
base::BindOnce(&BluetoothHidDetectorImpl::OnPairingTimeout,
weak_ptr_factory_.GetWeakPtr()));
device_pairing_handler_remote_->PairDevice(
current_pairing_device_.value()->id,
device_pairing_delegate_receiver_.BindNewPipeAndPassRemote(),
base::BindOnce(&BluetoothHidDetectorImpl::OnPairDevice,
weak_ptr_factory_.GetWeakPtr(),
std::make_unique<base::ElapsedTimer>()));
NotifyBluetoothHidDetectionStatusChanged();
}
void BluetoothHidDetectorImpl::OnPairDevice(
std::unique_ptr<base::ElapsedTimer> metrics_timer,
bluetooth_config::mojom::PairingResult pairing_result) {
DCHECK(current_pairing_device_)
<< "OnPairDevice() called with no |current_pairing_device_|";
HID_LOG(EVENT) << "Finished pairing with "
<< current_pairing_device_.value()->id
<< ", result: " << pairing_result << ", [" << queue_->size()
<< "] devices still in queue.";
const bool success =
pairing_result == bluetooth_config::mojom::PairingResult::kSuccess;
hid_detection::RecordBluetoothPairingResult(success,
metrics_timer->Elapsed());
// If pairing has succeeded, wait for SetInputDevicesStatus() to be called
// with the corresponding HID type no longer missing.
if (success) {
HID_LOG(EVENT)
<< "Pairing succeeded, waiting for input devices status to update.";
return;
}
HID_LOG(ERROR) << "Pairing failed, clearing current pairing state and "
<< "processing the next device in queue.";
ClearCurrentPairingState();
}
void BluetoothHidDetectorImpl::OnPairingTimeout() {
HID_LOG(ERROR) << "Pairing session has timed out, clearing current pairing "
<< "state.";
hid_detection::RecordPairingTimeoutExceeded();
ClearCurrentPairingState();
}
void BluetoothHidDetectorImpl::ClearCurrentPairingState() {
// If there is an ongoing pairing, it will be cancelled. Invalidate the
// pairing finished callback. This will also invalidate the
// |current_pairing_timer_| callback.
weak_ptr_factory_.InvalidateWeakPtrs();
queued_device_ids_.erase(current_pairing_device_.value()->id);
current_pairing_device_.reset();
current_pairing_state_.reset();
device_pairing_delegate_receiver_.reset();
key_entered_handler_receiver_.reset();
NotifyBluetoothHidDetectionStatusChanged();
ProcessQueue();
}
void BluetoothHidDetectorImpl::ResetDiscoveryState() {
// Reset Mojo-related properties.
bluetooth_discovery_delegate_receiver_.reset();
device_pairing_handler_remote_.reset();
device_pairing_delegate_receiver_.reset();
key_entered_handler_receiver_.reset();
// Reset queue-related properties.
current_pairing_device_.reset();
current_pairing_state_.reset();
current_pairing_timer_.Stop();
queue_ = std::make_unique<base::queue<BluetoothDevicePropertiesPtr>>();
queued_device_ids_.clear();
// Inform the client that no device is currently pairing.
NotifyBluetoothHidDetectionStatusChanged();
}
void BluetoothHidDetectorImpl::RequirePairingCode(
const std::string& code,
mojo::PendingReceiver<KeyEnteredHandler> handler) {
DCHECK(!current_pairing_state_) << "RequirePairingCode() called "
<< "with |current_pairing_state_| already "
<< "initialized";
DCHECK(!key_entered_handler_receiver_.is_bound());
key_entered_handler_receiver_.Bind(std::move(handler));
current_pairing_state_ =
BluetoothHidPairingState{code, /*num_keys_entered=*/0u};
NotifyBluetoothHidDetectionStatusChanged();
}
} // namespace ash::hid_detection