/** * SPDX-FileCopyrightText: 2015 Martin Gräßlin * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "waylandremoteinput.h" #include #include #include #include #include #include #include #include #include #include #include namespace { // Translation table to keep in sync within all the implementations int SpecialKeysMap[] = { 0, // Invalid KEY_BACKSPACE, // 1 KEY_TAB, // 2 KEY_LINEFEED, // 3 KEY_LEFT, // 4 KEY_UP, // 5 KEY_RIGHT, // 6 KEY_DOWN, // 7 KEY_PAGEUP, // 8 KEY_PAGEDOWN, // 9 KEY_HOME, // 10 KEY_END, // 11 KEY_ENTER, // 12 KEY_DELETE, // 13 KEY_ESC, // 14 KEY_SYSRQ, // 15 KEY_SCROLLLOCK, // 16 0, // 17 0, // 18 0, // 19 0, // 20 KEY_F1, // 21 KEY_F2, // 22 KEY_F3, // 23 KEY_F4, // 24 KEY_F5, // 25 KEY_F6, // 26 KEY_F7, // 27 KEY_F8, // 28 KEY_F9, // 29 KEY_F10, // 30 KEY_F11, // 31 KEY_F12, // 32 }; } Q_GLOBAL_STATIC(RemoteDesktopSession, s_session); class Xkb { public: Xkb() { m_xkbcontext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS)); m_xkbkeymap.reset(xkb_keymap_new_from_names(m_xkbcontext.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS)); m_xkbstate.reset(xkb_state_new(m_xkbkeymap.get())); } Xkb(int keymapFd, int size) { m_xkbcontext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS)); char *map = static_cast(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, keymapFd, 0)); if (map != MAP_FAILED) { m_xkbkeymap.reset(xkb_keymap_new_from_string(m_xkbcontext.get(), map, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS)); munmap(map, size); } close(keymapFd); if (!m_xkbkeymap) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Failed to create keymap"; m_xkbkeymap.reset(xkb_keymap_new_from_names(m_xkbcontext.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS)); } m_xkbstate.reset(xkb_state_new(m_xkbkeymap.get())); } std::optional keycodeFromKeysym(xkb_keysym_t keysym) { auto layout = xkb_state_serialize_layout(m_xkbstate.get(), XKB_STATE_LAYOUT_EFFECTIVE); const xkb_keycode_t max = xkb_keymap_max_keycode(m_xkbkeymap.get()); for (xkb_keycode_t keycode = xkb_keymap_min_keycode(m_xkbkeymap.get()); keycode < max; keycode++) { uint levelCount = xkb_keymap_num_levels_for_key(m_xkbkeymap.get(), keycode, layout); for (uint currentLevel = 0; currentLevel < levelCount; currentLevel++) { const xkb_keysym_t *syms; uint num_syms = xkb_keymap_key_get_syms_by_level(m_xkbkeymap.get(), keycode, layout, currentLevel, &syms); for (uint sym = 0; sym < num_syms; sym++) { if (syms[sym] == keysym) { return {keycode - 8}; } } } } return {}; } private: template using deleter = std::integral_constant; std::unique_ptr> m_xkbcontext; std::unique_ptr> m_xkbkeymap; std::unique_ptr> m_xkbstate; }; RemoteDesktopSession::RemoteDesktopSession() : iface(new OrgFreedesktopPortalRemoteDesktopInterface(QLatin1String("org.freedesktop.portal.Desktop"), QLatin1String("/org/freedesktop/portal/desktop"), QDBusConnection::sessionBus(), this)) , m_eiNotifier(QSocketNotifier::Read) , m_xkb(new Xkb) { connect(&m_eiNotifier, &QSocketNotifier::activated, this, &RemoteDesktopSession::handleEiEvents); } void RemoteDesktopSession::createSession() { if (isValid()) { qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "pass, already created"; return; } m_connecting = true; // create session const auto handleToken = QStringLiteral("kdeconnect%1").arg(QRandomGenerator::global()->generate()); const auto sessionParameters = QVariantMap{{QLatin1String("session_handle_token"), handleToken}, {QLatin1String("handle_token"), handleToken}}; auto sessionReply = iface->CreateSession(sessionParameters); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(sessionReply); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, sessionReply](QDBusPendingCallWatcher *self) { self->deleteLater(); if (sessionReply.isError()) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Could not create the remote control session" << sessionReply.error(); m_connecting = false; return; } bool b = QDBusConnection::sessionBus().connect(QString(), sessionReply.value().path(), QLatin1String("org.freedesktop.portal.Request"), QLatin1String("Response"), this, SLOT(handleXdpSessionCreated(uint, QVariantMap))); Q_ASSERT(b); qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "authenticating" << sessionReply.value().path(); }); } void RemoteDesktopSession::handleXdpSessionCreated(uint code, const QVariantMap &results) { if (code != 0) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Failed to create session with code" << code << results; return; } m_connecting = false; m_xdpPath = QDBusObjectPath(results.value(QLatin1String("session_handle")).toString()); QVariantMap startParameters = { {QLatin1String("handle_token"), QStringLiteral("kdeconnect%1").arg(QRandomGenerator::global()->generate())}, {QStringLiteral("types"), QVariant::fromValue(7)}, // request all (KeyBoard, Pointer, TouchScreen) {QLatin1String("persist_mode"), QVariant::fromValue(2)}, // Persist permission until explicitly revoked by user }; KConfigGroup stateConfig = KSharedConfig::openStateConfig()->group(QStringLiteral("mousepad")); QString restoreToken = stateConfig.readEntry(QStringLiteral("RestoreToken"), QString()); if (restoreToken.length() > 0) { startParameters[QLatin1String("restore_token")] = restoreToken; } QDBusConnection::sessionBus().connect(QString(), m_xdpPath.path(), QLatin1String("org.freedesktop.portal.Session"), QLatin1String("Closed"), this, SLOT(handleXdpSessionFinished(uint, QVariantMap))); auto reply = iface->SelectDevices(m_xdpPath, startParameters); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, reply](QDBusPendingCallWatcher *self) { self->deleteLater(); if (reply.isError()) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Could not start the remote control session" << reply.error(); m_connecting = false; return; } bool b = QDBusConnection::sessionBus().connect(QString(), reply.value().path(), QLatin1String("org.freedesktop.portal.Request"), QLatin1String("Response"), this, SLOT(handleXdpSessionConfigured(uint, QVariantMap))); Q_ASSERT(b); qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "configuring" << reply.value().path(); }); } void RemoteDesktopSession::handleXdpSessionConfigured(uint code, const QVariantMap &results) { if (code != 0) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Failed to configure session with code" << code << results; m_connecting = false; return; } const QVariantMap startParameters = { {QLatin1String("handle_token"), QStringLiteral("kdeconnect%1").arg(QRandomGenerator::global()->generate())}, }; auto reply = iface->Start(m_xdpPath, {}, startParameters); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, reply](QDBusPendingCallWatcher *self) { self->deleteLater(); if (reply.isError()) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Could not start the remote control session" << reply.error(); m_connecting = false; return; } bool b = QDBusConnection::sessionBus().connect(QString(), reply.value().path(), QLatin1String("org.freedesktop.portal.Request"), QLatin1String("Response"), this, SLOT(handleXdpSessionStarted(uint, QVariantMap))); Q_ASSERT(b); qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "starting" << reply.value().path(); }); } void RemoteDesktopSession::handleXdpSessionStarted(uint code, const QVariantMap &results) { Q_UNUSED(code); KConfigGroup stateConfig = KSharedConfig::openStateConfig()->group(QStringLiteral("mousepad")); stateConfig.writeEntry(QStringLiteral("RestoreToken"), results[QStringLiteral("restore_token")].toString()); auto call = iface->ConnectToEIS(m_xdpPath, {}); connect(new QDBusPendingCallWatcher(call), &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); QDBusReply reply = *watcher; if (!reply.isValid()) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Error connecting to eis" << reply.error(); return; } connectToEi(reply.value().takeFileDescriptor()); }); } void RemoteDesktopSession::connectToEi(int fd) { m_eiNotifier.setSocket(fd); m_eiNotifier.setEnabled(true); m_ei = ei_new_sender(this); ei_log_set_handler(m_ei, nullptr); ei_setup_backend_fd(m_ei, fd); } void RemoteDesktopSession::handleEiEvents() { ei_dispatch(m_ei); while (auto event = ei_get_event(m_ei)) { const auto type = ei_event_get_type(event); switch (type) { case EI_EVENT_CONNECT: qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "Connected to ei"; break; case EI_EVENT_DISCONNECT: qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Disconnected from ei"; break; case EI_EVENT_SEAT_ADDED: { auto seat = ei_event_get_seat(event); ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE, EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, nullptr); break; } case EI_EVENT_SEAT_REMOVED: break; case EI_EVENT_DEVICE_ADDED: { auto device = ei_event_get_device(event); if (ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD) && !m_keyboard) { auto keymap = ei_device_keyboard_get_keymap(device); if (ei_keymap_get_type(keymap) != EI_KEYMAP_TYPE_XKB) { break; } m_xkb.reset(new Xkb(ei_keymap_get_fd(keymap), ei_keymap_get_size(keymap))); m_keyboard = device; } if (ei_device_has_capability(device, EI_DEVICE_CAP_POINTER) && !m_pointer) { m_pointer = device; } if (ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE) && !m_absolutePointer) { m_absolutePointer = device; } } break; case EI_EVENT_DEVICE_REMOVED: { auto device = ei_event_get_device(event); if (device == m_keyboard) { m_keyboard = nullptr; } if (device == m_pointer) { m_pointer = nullptr; } if (device == m_absolutePointer) { m_absolutePointer = nullptr; } break; } case EI_EVENT_DEVICE_PAUSED: break; case EI_EVENT_DEVICE_RESUMED: ei_device_start_emulating(ei_event_get_device(event), 0); break; case EI_EVENT_FRAME: case EI_EVENT_POINTER_MOTION: case EI_EVENT_POINTER_MOTION_ABSOLUTE: case EI_EVENT_BUTTON_BUTTON: case EI_EVENT_SCROLL_DELTA: case EI_EVENT_SCROLL_DISCRETE: case EI_EVENT_SCROLL_STOP: case EI_EVENT_SCROLL_CANCEL: case EI_EVENT_KEYBOARD_MODIFIERS: case EI_EVENT_KEYBOARD_KEY: case EI_EVENT_DEVICE_START_EMULATING: case EI_EVENT_DEVICE_STOP_EMULATING: case EI_EVENT_TOUCH_DOWN: case EI_EVENT_TOUCH_MOTION: case EI_EVENT_TOUCH_UP: qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "Unexpected event of type" << ei_event_get_type(event); break; } ei_event_unref(event); } } void RemoteDesktopSession::handleXdpSessionFinished(uint /*code*/, const QVariantMap & /*results*/) { m_xdpPath = {}; m_eiNotifier.setEnabled(false); ei_unref(m_ei); m_pointer = nullptr; m_keyboard = nullptr; m_absolutePointer = nullptr; } void RemoteDesktopSession::pointerButton(int button, bool down) { if (m_ei && m_pointer) { ei_device_button_button(m_pointer, button, down); ei_device_frame(m_pointer, ei_now(m_ei)); } else { iface->NotifyPointerButton(m_xdpPath, {}, BTN_LEFT, down); } } void RemoteDesktopSession::pointerAxis(double dx, double dy) { if (m_ei && m_pointer) { // Qt/Kdeconnect use inverted vertical scroll direction compared to libei ei_device_scroll_delta(m_pointer, dx, dy * -1); ei_device_frame(m_pointer, ei_now(m_ei)); } else { iface->NotifyPointerAxis(m_xdpPath, {}, dx, dy); } } void RemoteDesktopSession::pointerMotion(double dx, double dy) { if (m_ei && m_pointer) { ei_device_pointer_motion(m_pointer, dx, dy); ei_device_frame(m_pointer, ei_now(m_ei)); } else { iface->NotifyPointerMotion(m_xdpPath, {}, dx, dy); } } void RemoteDesktopSession::pointerMotionAbsolute(double x, double y) { if (m_ei && m_absolutePointer) { qDebug() << x << y; ei_device_pointer_motion_absolute(m_absolutePointer, x, y); ei_device_frame(m_absolutePointer, ei_now(m_ei)); } else { iface->NotifyPointerMotionAbsolute(m_xdpPath, {}, 0, x, y); } } void RemoteDesktopSession::keyboardKeycode(int key, bool press) { if (m_ei && m_keyboard) { ei_device_keyboard_key(m_keyboard, key, press); ei_device_frame(m_keyboard, ei_now(m_ei)); } else { iface->NotifyKeyboardKeycode(m_xdpPath, {}, key, press); } } void RemoteDesktopSession::keyboardKeysym(int sym, bool press) { if (m_ei && m_keyboard) { if (auto code = m_xkb->keycodeFromKeysym(sym)) { ei_device_keyboard_key(m_keyboard, *code, press); ei_device_frame(m_keyboard, ei_now(m_ei)); } else { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "failed to convert keysym" << sym; } } else { iface->NotifyKeyboardKeysym(m_xdpPath, {}, sym, press).waitForFinished(); } } WaylandRemoteInput::WaylandRemoteInput(QObject *parent) : AbstractRemoteInput(parent) { } bool WaylandRemoteInput::handlePacket(const NetworkPacket &np) { if (!s_session->isValid()) { qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Unable to handle remote input. RemoteDesktop portal not authenticated"; s_session->createSession(); return false; } const float dx = np.get(QStringLiteral("dx"), 0); const float dy = np.get(QStringLiteral("dy"), 0); const float x = np.get(QStringLiteral("x"), 0); const float y = np.get(QStringLiteral("y"), 0); const bool isSingleClick = np.get(QStringLiteral("singleclick"), false); const bool isDoubleClick = np.get(QStringLiteral("doubleclick"), false); const bool isMiddleClick = np.get(QStringLiteral("middleclick"), false); const bool isRightClick = np.get(QStringLiteral("rightclick"), false); const bool isSingleHold = np.get(QStringLiteral("singlehold"), false); const bool isSingleRelease = np.get(QStringLiteral("singlerelease"), false); const bool isScroll = np.get(QStringLiteral("scroll"), false); const QString key = np.get(QStringLiteral("key"), QLatin1String("")); const int specialKey = np.get(QStringLiteral("specialKey"), 0); if (isSingleClick || isDoubleClick || isMiddleClick || isRightClick || isSingleHold || isSingleRelease || isScroll || !key.isEmpty() || specialKey) { if (isSingleClick) { s_session->pointerButton(BTN_LEFT, true); s_session->pointerButton(BTN_LEFT, false); } else if (isDoubleClick) { s_session->pointerButton(BTN_LEFT, true); s_session->pointerButton(BTN_LEFT, false); s_session->pointerButton(BTN_LEFT, true); s_session->pointerButton(BTN_LEFT, false); } else if (isMiddleClick) { s_session->pointerButton(BTN_MIDDLE, true); s_session->pointerButton(BTN_MIDDLE, false); } else if (isRightClick) { s_session->pointerButton(BTN_RIGHT, true); s_session->pointerButton(BTN_RIGHT, false); } else if (isSingleHold) { // For drag'n drop s_session->pointerButton(BTN_LEFT, true); } else if (isSingleRelease) { s_session->pointerButton(BTN_LEFT, false); } else if (isScroll) { s_session->pointerAxis(dx, dy); } else if (specialKey || !key.isEmpty()) { bool ctrl = np.get(QStringLiteral("ctrl"), false); bool alt = np.get(QStringLiteral("alt"), false); bool shift = np.get(QStringLiteral("shift"), false); bool super = np.get(QStringLiteral("super"), false); if (ctrl) s_session->keyboardKeycode(KEY_LEFTCTRL, true); if (alt) s_session->keyboardKeycode(KEY_LEFTALT, true); if (shift) s_session->keyboardKeycode(KEY_LEFTSHIFT, true); if (super) s_session->keyboardKeycode(KEY_LEFTMETA, true); if (specialKey) { s_session->keyboardKeycode(SpecialKeysMap[specialKey], true); s_session->keyboardKeycode(SpecialKeysMap[specialKey], false); } else if (!key.isEmpty()) { for (const QChar character : key) { const auto keysym = xkb_utf32_to_keysym(character.toLower().unicode()); if (keysym != XKB_KEY_NoSymbol) { s_session->keyboardKeysym(keysym, true); s_session->keyboardKeysym(keysym, false); } else { qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "Cannot send character" << character; } } } if (ctrl) s_session->keyboardKeycode(KEY_LEFTCTRL, false); if (alt) s_session->keyboardKeycode(KEY_LEFTALT, false); if (shift) s_session->keyboardKeycode(KEY_LEFTSHIFT, false); if (super) s_session->keyboardKeycode(KEY_LEFTMETA, false); } } else { // Is a mouse move event if (dx || dy) s_session->pointerMotion(dx, dy); else if ((np.has(QStringLiteral("x")) || np.has(QStringLiteral("y")))) s_session->pointerMotionAbsolute(x, y); } return true; } #include "moc_waylandremoteinput.cpp"