| // 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(); |
| uint8_t deviceid_u8 = static_cast<uint8_t>(deviceid); |
| if (connection->xinput().OpenDevice({deviceid_u8}).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_u8, |
| .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_u8}); |
| return base::FilePath(); |
| } |
| |
| std::string path(reinterpret_cast<char*>(reply->data8->data()), |
| reply->data8->size()); |
| |
| connection->xinput().CloseDevice({deviceid_u8}); |
| |
| 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 |