text: // Copyright 2014 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "ui/events/platform/x11/x11_hotplug_event_handler.h"
  
  #include <stdint.h>
  
  #include <algorithm>
  #include <cmath>
  #include <limits>
  #include <set>
  #include <string>
  #include <vector>
  
  #include "base/bind.h"
  #include "base/check.h"
  #include "base/command_line.h"
  #include "base/files/file_path.h"
  #include "base/location.h"
  #include "base/process/launch.h"
  #include "base/single_thread_task_runner.h"
  #include "base/strings/string_util.h"
  #include "base/system/sys_info.h"
  #include "base/task/post_task.h"
  #include "base/task/thread_pool.h"
  #include "base/threading/thread_task_runner_handle.h"
  #include "ui/events/devices/device_data_manager.h"
  #include "ui/events/devices/device_hotplug_event_observer.h"
  #include "ui/events/devices/device_util_linux.h"
  #include "ui/events/devices/input_device.h"
  #include "ui/events/devices/touchscreen_device.h"
  #include "ui/gfx/x/connection.h"
  #include "ui/gfx/x/extension_manager.h"
  #include "ui/gfx/x/future.h"
  #include "ui/gfx/x/x11_atom_cache.h"
  
  namespace ui {
  
  namespace {
  
  // Names of all known internal devices that should not be considered as
  // keyboards.
  // TODO(rsadam@): Identify these devices using udev rules. (Crbug.com/420728.)
  const char* kKnownInvalidKeyboardDeviceNames[] = {
      "Power Button", "Sleep Button", "Video Bus",
      "gpio-keys.5",  "gpio-keys.12", "ROCKCHIP-I2S Headset Jack"};
  
  enum DeviceType {
    DEVICE_TYPE_KEYBOARD,
    DEVICE_TYPE_MOUSE,
    DEVICE_TYPE_TOUCHPAD,
    DEVICE_TYPE_TOUCHSCREEN,
    DEVICE_TYPE_OTHER
  };
  
  using KeyboardDeviceCallback =
      base::OnceCallback<void(const std::vector<InputDevice>&)>;
  
  using TouchscreenDeviceCallback =
      base::OnceCallback<void(const std::vector<TouchscreenDevice>&)>;
  
  using InputDeviceCallback =
      base::OnceCallback<void(const std::vector<InputDevice>&)>;
  
  // Used for updating the state on the UI thread once device information is
  // parsed on helper threads.
  struct UiCallbacks {
    KeyboardDeviceCallback keyboard_callback;
    TouchscreenDeviceCallback touchscreen_callback;
    InputDeviceCallback mouse_callback;
    InputDeviceCallback touchpad_callback;
    base::OnceClosure hotplug_finished_callback;
  };
  
  // Identical to FP3232_TO_DOUBLE from libxi's XExtInt.c
  double Fp3232ToDouble(const x11::Input::Fp3232& x) {
    return static_cast<double>(x.integral) +
           static_cast<double>(x.frac) / (1ULL << 32);
  }
  
  // Stores a copy of the x11::Input::ValuatorClass values so X11 device
  // processing can happen on a worker thread. This is needed since X11 structs
  // are not copyable.
  struct ValuatorClassInfo {
    explicit ValuatorClassInfo(const x11::Input::ValuatorClass& info)
        : label(static_cast<x11::Atom>(info.label)),
          max(Fp3232ToDouble(info.max)),
          min(Fp3232ToDouble(info.min)),
          mode(info.mode),
          number(info.number) {}
  
    x11::Atom label;
    double max;
    double min;
    x11::Input::ValuatorMode mode;
    uint16_t number;
  };
  
  // Stores a copy of the XITouchClassInfo values so X11 device processing can
  // happen on a worker thread. This is needed since X11 structs are not copyable.
  struct TouchClassInfo {
    TouchClassInfo() = default;
  
    explicit TouchClassInfo(const x11::Input::DeviceClass::Touch& info)
        : mode(info.mode), num_touches(info.num_touches) {}
  
    x11::Input::TouchMode mode{};
    int num_touches = 0;
  };
  
  struct DeviceInfo {
    DeviceInfo(const x11::Input::XIDeviceInfo& device,
               DeviceType type,
               const base::FilePath& path)
        : id(device.deviceid),
          name(device.name),
          use(device.type),
          type(type),
          path(path) {
      for (const auto& device_class : device.classes) {
        if (device_class.valuator.has_value()) {
          valuator_class_infos.emplace_back(*device_class.valuator);
        } else if (device_class.touch.has_value()) {
          // A device can have at most one XITouchClassInfo. Ref:
          // http://manpages.ubuntu.com/manpages/saucy/man3/XIQueryDevice.3.html
          DCHECK(!touch_class_info.num_touches);
          touch_class_info = TouchClassInfo(*device_class.touch);
        }
      }
    }
  
    // Unique device identifier.
    x11::Input::DeviceId id;
  
    // Internal device name.
    std::string name;
  
    // Device type (ie: XIMasterPointer)
    x11::Input::DeviceType use;
  
    // Specifies the type of the device.
    DeviceType type;
  
    // Path to the actual device (ie: /dev/input/eventXX)
    base::FilePath path;
  
    std::vector<x11::Input::DeviceClass::Valuator> valuator_class_infos;
  
    TouchClassInfo touch_class_info;
  };
  
  // X11 display cache used on worker threads. This is filled on the UI thread and
  // passed in to the worker threads.
  struct DisplayState {
    x11::Atom mt_position_x;
    x11::Atom mt_position_y;
  };
  
  // Returns true if |name| is the name of a known invalid keyboard device. Note,
  // this may return false negatives.
  bool IsKnownInvalidKeyboardDevice(const std::string& name) {
    std::string trimmed(name);
    base::TrimWhitespaceASCII(name, base::TRIM_TRAILING, &trimmed);
    for (const char* device_name : kKnownInvalidKeyboardDeviceNames) {
      if (trimmed == device_name)
        return true;
    }
    return false;
  }
  
  // Returns true if |name| is the name of a known XTEST device. Note, this may
  // return false negatives.
  bool IsTestDevice(const std::string& name) {
    return name.find("XTEST") != std::string::npos;
  }
  
  base::FilePath GetDevicePath(x11::Connection* connection,
                               const x11::Input::XIDeviceInfo& device) {
    // Skip the main pointer and keyboard since XOpenDevice() generates a
    // BadDevice error when passed these devices.
    if (device.type == x11::Input::DeviceType::MasterPointer ||
        device.type == x11::Input::DeviceType::MasterKeyboard)
      return base::FilePath();
  
    // Input device has a property "Device Node" pointing to its dev input node,
    // e.g.   Device Node (250): "/dev/input/event8"
    x11::Atom device_node = x11::GetAtom("Device Node");
    if (device_node == x11::Atom::None)
      return base::FilePath();
  
    auto deviceid = static_cast<uint16_t>(device.deviceid);
    if (deviceid > std::numeric_limits<uint8_t>::max())
      return base::FilePath();
    if (connection->xinput().OpenDevice({deviceid}).Sync().error)
      return base::FilePath();
  
    x11::Input::GetDevicePropertyRequest req{
        .property = device_node,
        .type = x11::Atom::Any,
        .offset = 0,
        .len = std::numeric_limits<uint32_t>::max(),
        .device_id = deviceid,
        .c_delete = false,
    };
    auto reply = connection->xinput().GetDeviceProperty(req).Sync();
    if (!reply || reply->type != x11::Atom::STRING || !reply->data8.has_value()) {
      connection->xinput().CloseDevice({deviceid});
      return base::FilePath();
    }
  
    std::string path(reinterpret_cast<char*>(reply->data8->data()),
                     reply->data8->size());
  
    connection->xinput().CloseDevice({deviceid});
  
    return base::FilePath(path);
  }
  
  // Helper used to parse keyboard information. When it is done it uses
  // |reply_runner| and |callback| to update the state on the UI thread.
  void HandleKeyboardDevicesInWorker(const std::vector<DeviceInfo>& device_infos,
                                     scoped_refptr<base::TaskRunner> reply_runner,
                                     KeyboardDeviceCallback callback) {
    std::vector<InputDevice> devices;
  
    for (const DeviceInfo& device_info : device_infos) {
      if (device_info.type != DEVICE_TYPE_KEYBOARD)
        continue;
      if (device_info.use != x11::Input::DeviceType::SlaveKeyboard)
        continue;  // Assume all keyboards are keyboard slaves
      if (IsKnownInvalidKeyboardDevice(device_info.name))
        continue;  // Skip invalid devices.
      InputDeviceType type = GetInputDeviceTypeFromPath(device_info.path);
      devices.emplace_back(static_cast<uint16_t>(device_info.id), type,
                           device_info.name);
    }
  
    reply_runner->PostTask(FROM_HERE,
                           base::BindOnce(std::move(callback), devices));
  }
  
  // Helper used to parse mouse information. When it is done it uses
  // |reply_runner| and |callback| to update the state on the UI thread.
  void HandleMouseDevicesInWorker(const std::vector<DeviceInfo>& device_infos,
                                  scoped_refptr<base::TaskRunner> reply_runner,
                                  InputDeviceCallback callback) {
    std::vector<InputDevice> devices;
    for (const DeviceInfo& device_info : device_infos) {
      if (device_info.type != DEVICE_TYPE_MOUSE ||
          device_info.use != x11::Input::DeviceType::SlavePointer) {
        continue;
      }
  
      InputDeviceType type = GetInputDeviceTypeFromPath(device_info.path);
      devices.emplace_back(static_cast<uint16_t>(device_info.id), type,
                           device_info.name);
    }
  
    reply_runner->PostTask(FROM_HERE,
                           base::BindOnce(std::move(callback), devices));
  }
  
  // Helper used to parse touchpad information. When it is done it uses
  // |reply_runner| and |callback| to update the state on the UI thread.
  void HandleTouchpadDevicesInWorker(const std::vector<DeviceInfo>& device_infos,
                                     scoped_refptr<base::TaskRunner> reply_runner,
                                     InputDeviceCallback callback) {
    std::vector<InputDevice> devices;
    for (const DeviceInfo& device_info : device_infos) {
      if (device_info.type != DEVICE_TYPE_TOUCHPAD ||
          device_info.use != x11::Input::DeviceType::SlavePointer) {
        continue;
      }
  
      InputDeviceType type = GetInputDeviceTypeFromPath(device_info.path);
      devices.emplace_back(static_cast<uint16_t>(device_info.id), type,
                           device_info.name);
    }
  
    reply_runner->PostTask(FROM_HERE,
                           base::BindOnce(std::move(callback), devices));
  }
  
  // Helper used to parse touchscreen information. When it is done it uses
  // |reply_runner| and |callback| to update the state on the UI thread.
  void HandleTouchscreenDevicesInWorker(
      const std::vector<DeviceInfo>& device_infos,
      const DisplayState& display_state,
      scoped_refptr<base::TaskRunner> reply_runner,
      TouchscreenDeviceCallback callback) {
    std::vector<TouchscreenDevice> devices;
    if (display_state.mt_position_x == x11::Atom::None ||
        display_state.mt_position_y == x11::Atom::None)
      return;
  
    for (const DeviceInfo& device_info : device_infos) {
      if (device_info.type != DEVICE_TYPE_TOUCHSCREEN ||
          (device_info.use != x11::Input::DeviceType::FloatingSlave &&
           device_info.use != x11::Input::DeviceType::SlavePointer)) {
        continue;
      }
  
      // Touchscreens should be direct touch devices.
      if (device_info.touch_class_info.mode != x11::Input::TouchMode::Direct)
        continue;
  
      double max_x = -1.0;
      double max_y = -1.0;
  
      for (const auto& valuator : device_info.valuator_class_infos) {
        if (display_state.mt_position_x == valuator.label) {
          // Ignore X axis valuator with unexpected properties
          if (valuator.number == 0 &&
              valuator.mode == x11::Input::ValuatorMode::Absolute &&
              Fp3232ToDouble(valuator.min) == 0.0) {
            max_x = Fp3232ToDouble(valuator.max);
          }
        } else if (display_state.mt_position_y == valuator.label) {
          // Ignore Y axis valuator with unexpected properties
          if (valuator.number == 1 &&
              valuator.mode == x11::Input::ValuatorMode::Absolute &&
              Fp3232ToDouble(valuator.min) == 0.0) {
            max_y = Fp3232ToDouble(valuator.max);
          }
        }
      }
  
      // Touchscreens should have absolute X and Y axes.
      if (max_x > 0.0 && max_y > 0.0) {
        InputDeviceType type = GetInputDeviceTypeFromPath(device_info.path);
        // TODO(jamescook): Detect pen/stylus.
        const bool has_stylus = false;
        // |max_x| and |max_y| are inclusive values, so we need to add 1 to get
        // the size.
        devices.emplace_back(static_cast<uint16_t>(device_info.id), type,
                             device_info.name, gfx::Size(max_x + 1, max_y + 1),
                             device_info.touch_class_info.num_touches,
                             has_stylus);
      }
    }
  
    reply_runner->PostTask(FROM_HERE,
                           base::BindOnce(std::move(callback), devices));
  }
  
  // Called on a worker thread to parse the device information.
  void HandleHotplugEventInWorker(const std::vector<DeviceInfo>& devices,
                                  const DisplayState& display_state,
                                  scoped_refptr<base::TaskRunner> reply_runner,
                                  UiCallbacks callbacks) {
    HandleTouchscreenDevicesInWorker(devices, display_state, reply_runner,
                                     std::move(callbacks.touchscreen_callback));
    HandleKeyboardDevicesInWorker(devices, reply_runner,
                                  std::move(callbacks.keyboard_callback));
    HandleMouseDevicesInWorker(devices, reply_runner,
                               std::move(callbacks.mouse_callback));
    HandleTouchpadDevicesInWorker(devices, reply_runner,
                                  std::move(callbacks.touchpad_callback));
    reply_runner->PostTask(FROM_HERE,
                           std::move(callbacks.hotplug_finished_callback));
  }
  
  DeviceHotplugEventObserver* GetHotplugEventObserver() {
    return DeviceDataManager::GetInstance();
  }
  
  void OnKeyboardDevices(const std::vector<InputDevice>& devices) {
    GetHotplugEventObserver()->OnKeyboardDevicesUpdated(devices);
  }
  
  void OnTouchscreenDevices(const std::vector<TouchscreenDevice>& devices) {
    GetHotplugEventObserver()->OnTouchscreenDevicesUpdated(devices);
  }
  
  void OnMouseDevices(const std::vector<InputDevice>& devices) {
    GetHotplugEventObserver()->OnMouseDevicesUpdated(devices);
  }
  
  void OnTouchpadDevices(const std::vector<InputDevice>& devices) {
    GetHotplugEventObserver()->OnTouchpadDevicesUpdated(devices);
  }
  
  void OnHotplugFinished() {
    GetHotplugEventObserver()->OnDeviceListsComplete();
  }
  
  }  // namespace
  
  X11HotplugEventHandler::X11HotplugEventHandler() = default;
  
  X11HotplugEventHandler::~X11HotplugEventHandler() = default;
  
  void X11HotplugEventHandler::OnHotplugEvent() {
    auto* connection = x11::Connection::Get();
    const XDeviceList& device_list_xi =
        DeviceListCacheX11::GetInstance()->GetXDeviceList(connection);
    const XIDeviceList& device_list_xi2 =
        DeviceListCacheX11::GetInstance()->GetXI2DeviceList(connection);
  
    const int kMaxDeviceNum = 128;
    DeviceType device_types[kMaxDeviceNum];
    for (auto& device_type : device_types)
      device_type = DEVICE_TYPE_OTHER;
  
    for (const auto& device : device_list_xi) {
      uint8_t id = device.device_id;
      if (id >= kMaxDeviceNum)
        continue;
  
      // In XWayland, physical devices are not exposed to X Server, but
      // rather X11 and Wayland uses wayland protocol to communicate
      // devices.
  
      // So, xinput that Chromium uses to enumerate devices prepends
      // "xwayland-" to each device name. Though, Wayland doesn't expose TOUCHPAD
      // directly. Instead, it's part of xwayland-pointer.
      x11::Atom type = device.device_type;
      if (type == x11::GetAtom("KEYBOARD") ||
          type == x11::GetAtom("xwayland-keyboard")) {
        device_types[id] = DEVICE_TYPE_KEYBOARD;
      } else if (type == x11::GetAtom("MOUSE") ||
                 type == x11::GetAtom("xwayland-pointer")) {
        device_types[id] = DEVICE_TYPE_MOUSE;
      } else if (type == x11::GetAtom("TOUCHPAD")) {
        device_types[id] = DEVICE_TYPE_TOUCHPAD;
      } else if (type == x11::GetAtom("TOUCHSCREEN") ||
                 type == x11::GetAtom("xwayland-touch")) {
        device_types[id] = DEVICE_TYPE_TOUCHSCREEN;
      }
    }
  
    std::vector<DeviceInfo> device_infos;
    for (const auto& device : device_list_xi2) {
      if (!device.enabled || IsTestDevice(device.name))
        continue;
  
      auto deviceid = static_cast<uint16_t>(device.deviceid);
      DeviceType device_type =
          deviceid < kMaxDeviceNum ? device_types[deviceid] : DEVICE_TYPE_OTHER;
      device_infos.emplace_back(device, device_type,
                                GetDevicePath(connection, device));
    }
  
    // X11 is not thread safe, so first get all the required state.
    DisplayState display_state;
    display_state.mt_position_x = x11::GetAtom("Abs MT Position X");
    display_state.mt_position_y = x11::GetAtom("Abs MT Position Y");
  
    UiCallbacks callbacks;
    callbacks.keyboard_callback = base::BindOnce(&OnKeyboardDevices);
    callbacks.touchscreen_callback = base::BindOnce(&OnTouchscreenDevices);
    callbacks.mouse_callback = base::BindOnce(&OnMouseDevices);
    callbacks.touchpad_callback = base::BindOnce(&OnTouchpadDevices);
    callbacks.hotplug_finished_callback = base::BindOnce(&OnHotplugFinished);
  
    // Parse the device information asynchronously since this operation may block.
    // Once the device information is extracted the parsed devices will be
    // returned via the callbacks.
    base::ThreadPool::PostTask(
        FROM_HERE,
        {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
        base::BindOnce(&HandleHotplugEventInWorker, device_infos, display_state,
                       base::ThreadTaskRunnerHandle::Get(),
                       std::move(callbacks)));
  }
  
  }  // namespace ui
  