chromium/ash/system/input_device_settings/input_device_notifier.cc

// Copyright 2023 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/system/input_device_settings/input_device_notifier.h"

#include <functional>
#include <vector>

#include "ash/bluetooth_devices_observer.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/input_device_settings_controller.h"
#include "ash/public/mojom/input_device_settings.mojom-forward.h"
#include "ash/public/mojom/input_device_settings.mojom.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/input_device_settings/input_device_settings_metadata.h"
#include "ash/system/input_device_settings/input_device_settings_pref_names.h"
#include "ash/system/input_device_settings/input_device_settings_utils.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "base/values.h"
#include "components/prefs/pref_service.h"
#include "device/bluetooth/bluetooth_common.h"
#include "device/bluetooth/bluetooth_device.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/devices/keyboard_device.h"
#include "ui/events/devices/touchpad_device.h"

namespace ash {

namespace {

using DeviceId = InputDeviceSettingsController::DeviceId;

bool AreOnLoginScreen() {
  auto status = Shell::Get()->session_controller()->login_status();
  return status == LoginStatus::NOT_LOGGED_IN;
}

DeviceId ExtractDeviceIdFromInputDevice(const ui::InputDevice& device) {
  return device.id;
}

template <class DeviceMojomPtr>
bool IsDeviceASuspectedImposter(BluetoothDevicesObserver* bluetooth_observer,
                                const ui::InputDevice& device) {
  return false;
}

// Imposter here means a device that has a virtual keyboard device as well as a
// virtual mouse device presented to evdev and the keyboard device is "fake".
bool IsKeyboardAKnownImposterFalsePositive(const ui::InputDevice& device) {
  if (!Shell::Get()->session_controller()->IsActiveUserSessionStarted()) {
    return false;
  }

  PrefService* prefs =
      Shell::Get()->session_controller()->GetActivePrefService();
  if (!prefs) {
    return false;
  }

  const auto& imposters =
      prefs->GetList(prefs::kKeyboardDeviceImpostersListPref);
  const std::string device_key = BuildDeviceKey(device);
  return base::Contains(imposters, device_key);
}

// Imposter here means a device that has a virtual keyboard device as well as a
// virtual mouse device presented to evdev and the mouse device is "fake".
bool IsMouseAKnownImposterFalsePositive(const ui::InputDevice& device) {
  if (!features::IsMouseImposterCheckEnabled()) {
    return false;
  }

  if (!Shell::Get()->session_controller()->IsActiveUserSessionStarted()) {
    return false;
  }

  PrefService* prefs =
      Shell::Get()->session_controller()->GetActivePrefService();
  if (!prefs) {
    return false;
  }

  const auto& imposters = prefs->GetList(prefs::kMouseDeviceImpostersListPref);
  const std::string device_key = BuildDeviceKey(device);
  return base::Contains(imposters, device_key);
}

// Saves `imposter_false_positives_to_add` to the known list of imposters in
// prefs. Clears the list if it successfully adds the devices to prefs.
void SaveKeyboardsToImposterPref(
    base::flat_set<std::string>& imposter_false_positives_to_add) {
  if (!Shell::Get()->session_controller()->IsActiveUserSessionStarted()) {
    return;
  }

  PrefService* prefs =
      Shell::Get()->session_controller()->GetActivePrefService();
  if (!prefs) {
    return;
  }

  auto updated_imposters =
      prefs->GetList(prefs::kKeyboardDeviceImpostersListPref).Clone();
  for (const auto& device_key : imposter_false_positives_to_add) {
    if (base::Contains(updated_imposters, device_key)) {
      continue;
    }

    updated_imposters.Append(device_key);
  }

  prefs->SetList(prefs::kKeyboardDeviceImpostersListPref,
                 std::move(updated_imposters));
  imposter_false_positives_to_add.clear();
}

// Saves `imposter_false_positives_to_add` to the known list of mouse imposters
// in prefs. Clears the list if it successfully adds the devices to prefs.
void SaveMiceToImposterPref(
    base::flat_set<std::string>& imposter_false_positives_to_add) {
  if (!Shell::Get()->session_controller()->IsActiveUserSessionStarted()) {
    return;
  }

  PrefService* prefs =
      Shell::Get()->session_controller()->GetActivePrefService();
  if (!prefs) {
    return;
  }

  auto updated_imposters =
      prefs->GetList(prefs::kMouseDeviceImpostersListPref).Clone();
  for (const auto& device_key : imposter_false_positives_to_add) {
    if (base::Contains(updated_imposters, device_key)) {
      continue;
    }

    updated_imposters.Append(device_key);
  }

  prefs->SetList(prefs::kMouseDeviceImpostersListPref,
                 std::move(updated_imposters));
  imposter_false_positives_to_add.clear();
}

template <>
bool IsDeviceASuspectedImposter<mojom::KeyboardPtr>(
    BluetoothDevicesObserver* bluetooth_observer,
    const ui::InputDevice& device) {
  if (AreOnLoginScreen()) {
    return false;
  }

  // If the device type is keyboard or keyboard mouse combo, it should not be
  // considered an imposter.
  const auto device_type = GetDeviceType(device);
  switch (device_type) {
    case DeviceType::kKeyboard:
    case DeviceType::kKeyboardMouseCombo:
      return false;
    case DeviceType::kMouse:
      return true;
    case DeviceType::kUnknown:
      break;
  }

  if (IsKeyboardAKnownImposterFalsePositive(device)) {
    return false;
  }

  // If the device is bluetooth, check the bluetooth device to see if it is a
  // keyboard or keyboard/mouse combo.
  if (device.type == ui::INPUT_DEVICE_BLUETOOTH) {
    const auto* bluetooth_device =
        bluetooth_observer->GetConnectedBluetoothDevice(device);
    if (!bluetooth_device) {
      return false;
    }

    if (bluetooth_device->GetDeviceType() ==
            device::BluetoothDeviceType::KEYBOARD ||
        bluetooth_device->GetDeviceType() ==
            device::BluetoothDeviceType::KEYBOARD_MOUSE_COMBO) {
      return false;
    }

    return true;
  }

  return device.suspected_keyboard_imposter;
}

template <>
bool IsDeviceASuspectedImposter<mojom::MousePtr>(
    BluetoothDevicesObserver* bluetooth_observer,
    const ui::InputDevice& device) {
  if (AreOnLoginScreen()) {
    return false;
  }

  // If the device type is keyboard, the device should
  // always be considered an imposter.
  const auto device_type = GetDeviceType(device);
  switch (device_type) {
    case DeviceType::kKeyboard:
      return true;
    case DeviceType::kKeyboardMouseCombo:
    case DeviceType::kMouse:
      return false;
    case DeviceType::kUnknown:
      break;
  }

  if (IsMouseAKnownImposterFalsePositive(device)) {
    return false;
  }

  // If the device is bluetooth, check the bluetooth device to see if it is a
  // mouse or mouse/keyboard combo.
  if (device.type == ui::INPUT_DEVICE_BLUETOOTH) {
    const auto* bluetooth_device =
        bluetooth_observer->GetConnectedBluetoothDevice(device);
    if (!bluetooth_device) {
      return false;
    }

    if (bluetooth_device->GetDeviceType() ==
            device::BluetoothDeviceType::MOUSE ||
        bluetooth_device->GetDeviceType() ==
            device::BluetoothDeviceType::KEYBOARD_MOUSE_COMBO) {
      return false;
    }

    return true;
  }

  if (!features::IsMouseImposterCheckEnabled()) {
    return false;
  }

  return device.suspected_mouse_imposter;
}

template <typename T>
DeviceId ExtractDeviceIdFromDeviceMapPair(
    const std::pair<DeviceId, T>& id_t_pair) {
  return id_t_pair.first;
}

// Figures out which devices from `connected_devices` have been added/removed
// and stores them in the passed in vectors. `devices_to_add` and
// `devices_to_remove` will be cleared before being filled with the result.
template <class DeviceMojomPtr, typename InputDeviceType>
void GetAddedAndRemovedDevices(
    BluetoothDevicesObserver* bluetooth_observer,
    std::vector<InputDeviceType> updated_device_list,
    const base::flat_map<DeviceId, DeviceMojomPtr>& connected_devices,
    std::vector<InputDeviceType>* devices_to_add,
    std::vector<DeviceId>* devices_to_remove) {
  // Output parameter vectors must be empty to start.
  devices_to_add->clear();
  devices_to_remove->clear();

  // Sort input device list by id as `base::ranges::set_difference` requires
  // input lists are sorted.
  // Remove any devices marked as imposters as well.
  base::ranges::sort(updated_device_list, base::ranges::less(),
                     ExtractDeviceIdFromInputDevice);
  std::erase_if(updated_device_list, [&](const ui::InputDevice& device) {
    return IsDeviceASuspectedImposter<DeviceMojomPtr>(bluetooth_observer,
                                                      device);
  });

  // Generate a vector with only the device ids from the connected_devices
  // map. Guaranteed to be sorted as flat_map is always in sorted order by
  // key.
  std::vector<DeviceId> connected_devices_ids;
  connected_devices_ids.reserve(connected_devices.size());
  base::ranges::transform(connected_devices,
                          std::back_inserter(connected_devices_ids),
                          ExtractDeviceIdFromDeviceMapPair<DeviceMojomPtr>);
  DCHECK(base::ranges::is_sorted(connected_devices_ids));

  // Compares the `id` field of `updated_device_list` to the ids in
  // `connected_devices_ids`. Devices that are in `updated_device_list` but not
  // in `connected_devices_ids` are inserted into `devices_to_add`.
  // `updated_device_list` and `connected_device_ids` must be sorted.
  base::ranges::set_difference(updated_device_list, connected_devices_ids,
                               std::back_inserter(*devices_to_add),
                               /*Comp=*/base::ranges::less(),
                               /*Proj1=*/ExtractDeviceIdFromInputDevice);

  // Compares the `connected_devices_ids` to the id field of
  // `updated_device_list`. Ids that are in `connected_devices_ids` but not in
  // `updated_device_list` are inserted into `devices_to_remove`.
  // `updated_device_list` and `connected_device_ids` must be sorted.
  base::ranges::set_difference(connected_devices_ids, updated_device_list,
                               std::back_inserter(*devices_to_remove),
                               /*Comp=*/base::ranges::less(),
                               /*Proj1=*/std::identity(),
                               /*Proj2=*/ExtractDeviceIdFromInputDevice);
}

}  // namespace

template <typename MojomDevicePtr, typename InputDeviceType>
InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::InputDeviceNotifier(
    base::flat_map<DeviceId, MojomDevicePtr>* connected_devices,
    InputDeviceListsUpdatedCallback callback)
    : connected_devices_(connected_devices),
      device_lists_updated_callback_(callback) {
  DCHECK(connected_devices_);
  ui::DeviceDataManager::GetInstance()->AddObserver(this);
  Shell::Get()->session_controller()->AddObserver(this);
  bluetooth_devices_observer_ =
      std::make_unique<BluetoothDevicesObserver>(base::BindRepeating(
          &InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::
              OnBluetoothAdapterOrDeviceChanged,
          base::Unretained(this)));
  RefreshDevices();
}

template <typename MojomDevicePtr, typename InputDeviceType>
InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::~InputDeviceNotifier() {
  ui::DeviceDataManager::GetInstance()->RemoveObserver(this);
  Shell::Get()->session_controller()->RemoveObserver(this);
}

template <typename MojomDevicePtr, typename InputDeviceType>
void InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::RefreshDevices() {
  std::vector<InputDeviceType> devices_to_add;
  std::vector<DeviceId> device_ids_to_remove;

  std::vector<InputDeviceType> updated_device_list = GetUpdatedDeviceList();
  HandleImposterPref(updated_device_list);
  GetAddedAndRemovedDevices(bluetooth_devices_observer_.get(),
                            updated_device_list, *connected_devices_,
                            &devices_to_add, &device_ids_to_remove);

  device_lists_updated_callback_.Run(std::move(devices_to_add),
                                     std::move(device_ids_to_remove));
}

template <typename MojomDevicePtr, typename InputDeviceType>
void InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::HandleImposterPref(
    const std::vector<InputDeviceType>& updated_device_list) {
  return;
}

template <>
void InputDeviceNotifier<mojom::MousePtr, ui::InputDevice>::HandleImposterPref(
    const std::vector<ui::InputDevice>& updated_device_list) {
  if (!features::IsMouseImposterCheckEnabled()) {
    return;
  }

  // Use a temporary set to store the device ids of imposter devices so devices
  // get removed upon device disconnect.
  base::flat_set<DeviceId> updated_imposter_devices;
  for (const ui::InputDevice& device : updated_device_list) {
    if (device.suspected_mouse_imposter) {
      updated_imposter_devices.insert(device.id);
      continue;
    }

    // If the device is no longer an imposter and once was (which means it was
    // in `mouse_imposter_devices_`) add it to our list of device keys to add to
    // the known imposter list.
    if (mouse_imposter_devices_.contains(device.id)) {
      mouse_imposter_false_positives_to_add_.insert(BuildDeviceKey(device));
    }
  }
  mouse_imposter_devices_ = std::move(updated_imposter_devices);

  // Always try to add additional devices to the imposter pref list.
  SaveMiceToImposterPref(mouse_imposter_false_positives_to_add_);
}

template <>
void InputDeviceNotifier<mojom::KeyboardPtr, ui::KeyboardDevice>::
    HandleImposterPref(
        const std::vector<ui::KeyboardDevice>& updated_device_list) {
  // Use a temporary set to store the device ids of imposter devices so devices
  // get removed upon device disconnect.
  base::flat_set<DeviceId> updated_imposter_devices;
  for (const ui::KeyboardDevice& device : updated_device_list) {
    if (device.suspected_keyboard_imposter) {
      updated_imposter_devices.insert(device.id);
      continue;
    }

    // If the device is no longer an imposter and once was (which means it was
    // in `keyboard_imposter_devices_`) add it to our list of device keys to add
    // to the known imposter list.
    if (keyboard_imposter_devices_.contains(device.id)) {
      keyboard_imposter_false_positives_to_add_.insert(BuildDeviceKey(device));
    }
  }
  keyboard_imposter_devices_ = std::move(updated_imposter_devices);

  // Always try to add additional devices to the imposter pref list.
  SaveKeyboardsToImposterPref(keyboard_imposter_false_positives_to_add_);
}

template <typename MojomDevicePtr, typename InputDeviceType>
void InputDeviceNotifier<MojomDevicePtr,
                         InputDeviceType>::OnDeviceListsComplete() {
  RefreshDevices();
}

template <typename MojomDevicePtr, typename InputDeviceType>
void InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::
    OnInputDeviceConfigurationChanged(uint8_t input_device_type) {
  RefreshDevices();
}

template <typename MojomDevicePtr, typename InputDeviceType>
void InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::OnLoginStatusChanged(
    LoginStatus login_status) {
  RefreshDevices();
}

template <typename MojomDevicePtr, typename InputDeviceType>
void InputDeviceNotifier<MojomDevicePtr, InputDeviceType>::
    OnBluetoothAdapterOrDeviceChanged(device::BluetoothDevice* device) {
  // Do nothing as OnBluetoothAdapterOrDeviceChanged is very noisy and causes
  // updates to happen many times per second. We expect
  // OnInputDeviceConfigurationChanged to include all devices including
  // bluetooth devices, so refreshing devices here is unnecessary.
}

// Template specialization for retrieving the updated device lists for each
// device type.
template <>
std::vector<ui::KeyboardDevice>
InputDeviceNotifier<mojom::KeyboardPtr,
                    ui::KeyboardDevice>::GetUpdatedDeviceList() {
  return ui::DeviceDataManager::GetInstance()->GetKeyboardDevices();
}

template <>
std::vector<ui::TouchpadDevice>
InputDeviceNotifier<mojom::TouchpadPtr,
                    ui::TouchpadDevice>::GetUpdatedDeviceList() {
  return ui::DeviceDataManager::GetInstance()->GetTouchpadDevices();
}

template <>
std::vector<ui::InputDevice>
InputDeviceNotifier<mojom::MousePtr, ui::InputDevice>::GetUpdatedDeviceList() {
  auto mice = ui::DeviceDataManager::GetInstance()->GetMouseDevices();
  std::erase_if(mice, [](const auto& mouse) {
    // Some I2C touchpads falsely claim to be mice, see b/205272718
    // By filtering out internal mice, i2c touchpads are prevented from being in
    // the "mouse" category in settings.
    return mouse.type == ui::INPUT_DEVICE_INTERNAL;
  });
  return mice;
}

template <>
std::vector<ui::InputDevice>
InputDeviceNotifier<mojom::PointingStickPtr,
                    ui::InputDevice>::GetUpdatedDeviceList() {
  return ui::DeviceDataManager::GetInstance()->GetPointingStickDevices();
}

template <>
std::vector<ui::InputDevice>
InputDeviceNotifier<mojom::GraphicsTabletPtr,
                    ui::InputDevice>::GetUpdatedDeviceList() {
  DCHECK(ash::features::IsPeripheralCustomizationEnabled());
  return ui::DeviceDataManager::GetInstance()->GetGraphicsTabletDevices();
}

// Explicit instantiations for each device type.
template class EXPORT_TEMPLATE_DECLARE(ASH_EXPORT)
    InputDeviceNotifier<mojom::KeyboardPtr, ui::KeyboardDevice>;
template class EXPORT_TEMPLATE_DECLARE(ASH_EXPORT)
    InputDeviceNotifier<mojom::TouchpadPtr, ui::TouchpadDevice>;
template class EXPORT_TEMPLATE_DECLARE(ASH_EXPORT)
    InputDeviceNotifier<mojom::MousePtr, ui::InputDevice>;
template class EXPORT_TEMPLATE_DECLARE(ASH_EXPORT)
    InputDeviceNotifier<mojom::PointingStickPtr, ui::InputDevice>;
template class EXPORT_TEMPLATE_DECLARE(ASH_EXPORT)
    InputDeviceNotifier<mojom::GraphicsTabletPtr, ui::InputDevice>;

}  // namespace ash