// 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/hid_detection_manager_impl.h"
#include "base/containers/contains.h"
#include "base/no_destructor.h"
#include "chromeos/ash/components/hid_detection/bluetooth_hid_detector_impl.h"
#include "chromeos/ash/components/hid_detection/hid_detection_utils.h"
#include "components/device_event_log/device_event_log.h"
namespace ash::hid_detection {
namespace {
using BluetoothHidType = BluetoothHidDetector::BluetoothHidType;
using InputState = HidDetectionManager::InputState;
// In floss, a virtual device is created when a HID is bonded or paired.
// We do not want to include this virtual device to our list of added devices.
// (b/299955128)
const char* kBlockedDeviceNames[] = {"VIRTUAL_SUSPEND_UHID"};
HidDetectionManagerImpl::InputDeviceManagerBinder&
GetInputDeviceManagerBinderOverride() {
// InputDeviceManagerBinder instance that can be overridden in tests.
static base::NoDestructor<HidDetectionManagerImpl::InputDeviceManagerBinder>
binder;
return *binder;
}
} // namespace
// static
void HidDetectionManagerImpl::SetInputDeviceManagerBinderForTest(
InputDeviceManagerBinder binder) {
GetInputDeviceManagerBinderOverride() = std::move(binder);
}
HidDetectionManagerImpl::HidDetectionManagerImpl(
device::mojom::DeviceService* device_service)
: device_service_{device_service},
bluetooth_hid_detector_{std::make_unique<BluetoothHidDetectorImpl>()} {}
HidDetectionManagerImpl::~HidDetectionManagerImpl() = default;
void HidDetectionManagerImpl::GetIsHidDetectionRequired(
base::OnceCallback<void(bool)> callback) {
BindToInputDeviceManagerIfNeeded();
HID_LOG(EVENT) << "Fetching input devices for GetIsHidDetectionRequired().";
input_device_manager_->GetDevices(
base::BindOnce(&HidDetectionManagerImpl::OnGetDevicesForIsRequired,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void HidDetectionManagerImpl::PerformStartHidDetection() {
BindToInputDeviceManagerIfNeeded();
HID_LOG(EVENT) << "Starting HID detection by fetching input devices.";
input_device_manager_->GetDevicesAndSetClient(
input_device_manager_receiver_.BindNewEndpointAndPassRemote(),
base::BindOnce(&HidDetectionManagerImpl::OnGetDevicesAndSetClient,
weak_ptr_factory_.GetWeakPtr()));
}
void HidDetectionManagerImpl::PerformStopHidDetection() {
HID_LOG(EVENT) << "Stopping HID detection.";
input_device_manager_receiver_.reset();
// Check if any of the connected input devices are connected via Bluetooth.
bool is_using_bluetooth = false;
for (const auto& [device_id, device] : device_id_to_device_map_) {
if (device->type == device::mojom::InputDeviceType::TYPE_BLUETOOTH) {
is_using_bluetooth = true;
break;
}
}
bluetooth_hid_detector_->StopBluetoothHidDetection(is_using_bluetooth);
}
HidDetectionManager::HidDetectionStatus
HidDetectionManagerImpl::ComputeHidDetectionStatus() const {
BluetoothHidDetector::BluetoothHidDetectionStatus bluetooth_status =
bluetooth_hid_detector_->GetBluetoothHidDetectionStatus();
return HidDetectionManager::HidDetectionStatus(
GetInputMetadata(connected_pointer_id_, BluetoothHidType::kPointer,
bluetooth_status.current_pairing_device),
GetInputMetadata(connected_keyboard_id_, BluetoothHidType::kKeyboard,
bluetooth_status.current_pairing_device),
connected_touchscreen_id_.has_value(),
std::move(bluetooth_status.pairing_state));
}
void HidDetectionManagerImpl::InputDeviceAdded(
device::mojom::InputDeviceInfoPtr info) {
// Special case where the added device is a blocked device.
if (std::find(std::begin(kBlockedDeviceNames), std::end(kBlockedDeviceNames),
info->name) != std::end(kBlockedDeviceNames)) {
return;
}
HID_LOG(EVENT) << "Input device added, id: " << info->id
<< ", name: " << info->name;
const std::string& device_id = info->id;
device_id_to_device_map_[device_id] = std::move(info);
hid_detection::RecordHidConnected(*device_id_to_device_map_[device_id]);
if (AttemptSetDeviceAsConnectedHid(*device_id_to_device_map_[device_id])) {
NotifyHidDetectionStatusChanged();
SetInputDevicesStatus();
}
}
void HidDetectionManagerImpl::InputDeviceRemoved(const std::string& id) {
if (!base::Contains(device_id_to_device_map_, id)) {
// Some devices may be removed that were not registered in
// InputDeviceAdded() or OnGetDevicesAndSetClient().
HID_LOG(EVENT)
<< "Input device with id: " << id
<< " was removed that was not in |device_id_to_device_map_|.";
return;
}
HID_LOG(EVENT) << "Input device removed, id: " << id
<< ", name: " << device_id_to_device_map_[id]->name;
hid_detection::RecordHidDisconnected(*device_id_to_device_map_[id]);
device_id_to_device_map_.erase(id);
bool was_connected_hid_disconnected_ = false;
if (id == connected_touchscreen_id_) {
HID_LOG(EVENT) << "Removing connected touchscreen: " << id;
connected_touchscreen_id_.reset();
was_connected_hid_disconnected_ = true;
}
if (id == connected_pointer_id_) {
HID_LOG(EVENT) << "Removing connected pointer: " << id;
connected_pointer_id_.reset();
was_connected_hid_disconnected_ = true;
}
if (id == connected_keyboard_id_) {
HID_LOG(EVENT) << "Removing connected keyboard: " << id;
connected_keyboard_id_.reset();
was_connected_hid_disconnected_ = true;
}
if (was_connected_hid_disconnected_) {
SetConnectedHids();
NotifyHidDetectionStatusChanged();
SetInputDevicesStatus();
}
}
void HidDetectionManagerImpl::OnBluetoothHidStatusChanged() {
NotifyHidDetectionStatusChanged();
}
void HidDetectionManagerImpl::BindToInputDeviceManagerIfNeeded() {
if (input_device_manager_.is_bound())
return;
mojo::PendingReceiver<device::mojom::InputDeviceManager> receiver =
input_device_manager_.BindNewPipeAndPassReceiver();
const auto& binder = GetInputDeviceManagerBinderOverride();
if (binder) {
binder.Run(std::move(receiver));
return;
}
DCHECK(device_service_);
device_service_->BindInputDeviceManager(std::move(receiver));
}
void HidDetectionManagerImpl::OnGetDevicesForIsRequired(
base::OnceCallback<void(bool)> callback,
std::vector<device::mojom::InputDeviceInfoPtr> devices) {
bool has_pointer = false;
bool has_keyboard = false;
for (const auto& device : devices) {
if (hid_detection::IsDevicePointer(*device))
has_pointer = true;
if (device->is_keyboard)
has_keyboard = true;
if (has_pointer && has_keyboard)
break;
}
hid_detection::HidsMissing hids_missing = hid_detection::HidsMissing::kNone;
if (!has_pointer) {
if (!has_keyboard) {
hids_missing = hid_detection::HidsMissing::kPointerAndKeyboard;
} else {
hids_missing = hid_detection::HidsMissing::kPointer;
}
} else if (!has_keyboard) {
hids_missing = hid_detection::HidsMissing::kKeyboard;
}
hid_detection::RecordInitialHidsMissing(hids_missing);
HID_LOG(EVENT)
<< "Fetched " << devices.size()
<< " input devices for GetIsHidDetectionRequired(). Pointer detected: "
<< has_pointer << ", keyboard detected: " << has_keyboard;
// HID detection is not required if both devices are present.
std::move(callback).Run(!(has_pointer && has_keyboard));
}
void HidDetectionManagerImpl::OnGetDevicesAndSetClient(
std::vector<device::mojom::InputDeviceInfoPtr> devices) {
DCHECK(device_id_to_device_map_.empty())
<< " |devices_| should be empty when fetching initial devices.";
for (auto& device : devices) {
device_id_to_device_map_[device->id] = std::move(device);
}
SetConnectedHids();
NotifyHidDetectionStatusChanged();
bluetooth_hid_detector_->StartBluetoothHidDetection(
this, {.pointer_is_missing = !connected_pointer_id_.has_value(),
.keyboard_is_missing = !connected_keyboard_id_.has_value()});
}
bool HidDetectionManagerImpl::SetConnectedHids() {
HID_LOG(EVENT) << "Setting connected HIDs";
bool is_any_device_newly_connected_hid = false;
for (const auto& [device_id, device] : device_id_to_device_map_) {
is_any_device_newly_connected_hid |=
AttemptSetDeviceAsConnectedHid(*device);
}
return is_any_device_newly_connected_hid;
}
bool HidDetectionManagerImpl::AttemptSetDeviceAsConnectedHid(
const device::mojom::InputDeviceInfo& device) {
bool is_device_newly_connected_hid = false;
if (!connected_touchscreen_id_.has_value() &&
hid_detection::IsDeviceTouchscreen(device)) {
HID_LOG(EVENT) << "Touchscreen detected: " << device.id;
connected_touchscreen_id_ = device.id;
is_device_newly_connected_hid = true;
}
if (!connected_pointer_id_.has_value() &&
hid_detection::IsDevicePointer(device)) {
HID_LOG(EVENT) << "Pointer detected: " << device.id;
connected_pointer_id_ = device.id;
is_device_newly_connected_hid = true;
}
if (!connected_keyboard_id_.has_value() && device.is_keyboard) {
HID_LOG(EVENT) << "Keyboard detected: " << device.id;
connected_keyboard_id_ = device.id;
is_device_newly_connected_hid = true;
}
return is_device_newly_connected_hid;
}
HidDetectionManager::InputMetadata HidDetectionManagerImpl::GetInputMetadata(
const std::optional<std::string>& connected_device_id,
BluetoothHidType input_type,
const std::optional<BluetoothHidDetector::BluetoothHidMetadata>&
current_pairing_device) const {
if (connected_device_id.has_value()) {
const device::mojom::InputDeviceInfoPtr& device =
device_id_to_device_map_.find(connected_device_id.value())->second;
DCHECK(device)
<< " |connected_device_id| not found in |device_id_to_device_map_|";
InputState state;
switch (device->type) {
case device::mojom::InputDeviceType::TYPE_BLUETOOTH:
state = InputState::kPairedViaBluetooth;
break;
case device::mojom::InputDeviceType::TYPE_USB:
state = InputState::kConnectedViaUsb;
break;
case device::mojom::InputDeviceType::TYPE_SERIO:
[[fallthrough]];
case device::mojom::InputDeviceType::TYPE_UNKNOWN:
state = InputState::kConnected;
break;
}
return InputMetadata{state, device->name};
}
if (current_pairing_device.has_value() &&
(current_pairing_device.value().type == input_type ||
current_pairing_device.value().type ==
BluetoothHidType::kKeyboardPointerCombo)) {
return InputMetadata{InputState::kPairingViaBluetooth,
current_pairing_device.value().name};
}
return InputMetadata();
}
void HidDetectionManagerImpl::SetInputDevicesStatus() {
bluetooth_hid_detector_->SetInputDevicesStatus(
{.pointer_is_missing = !connected_pointer_id_.has_value(),
.keyboard_is_missing = !connected_keyboard_id_.has_value()});
}
void HidDetectionManagerImpl::SetBluetoothHidDetectorForTest(
std::unique_ptr<BluetoothHidDetector> bluetooth_hid_detector) {
bluetooth_hid_detector_ = std::move(bluetooth_hid_detector);
}
} // namespace ash::hid_detection