diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0819adc..11109b1 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,7 +17,7 @@ jobs: - name: Install the build tools run: | pacman -Syu --noconfirm --needed git base-devel cmake pkgconf \ - qt6-base qt6-declarative layer-shell-qt kidletime ki18n \ + qt6-base qt6-declarative layer-shell-qt ki18n \ opencv pam systemd-libs gettext scdoc shellcheck desktop-file-utils - uses: actions/checkout@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c526cd..8112d59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: git make cmake gcc-c++ gettext scdoc tar curl rpm-build rpm-sign systemd-rpm-macros \ 'pkgconfig(systemd)' 'pkgconfig(libsystemd)' pam-devel opencv-devel \ qt6-qtbase-devel qt6-qtbase-private-devel qt6-qtdeclarative-devel qt6-qtwayland-devel \ - layer-shell-qt-devel kf6-kidletime-devel kf6-ki18n-devel + layer-shell-qt-devel kf6-ki18n-devel - uses: actions/checkout@v7 diff --git a/CMakeLists.txt b/CMakeLists.txt index abde265..74bceb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,7 +136,6 @@ if(PFU_BUILD_AGENT) set(QT_NO_PRIVATE_MODULE_WARNING ON) find_package(Qt6 REQUIRED COMPONENTS GuiPrivate WaylandClientPrivate) find_package(LayerShellQt REQUIRED) - find_package(KF6IdleTime REQUIRED) find_package(KF6I18n REQUIRED) qt_add_executable(plasma-face-unlock-agent @@ -147,6 +146,7 @@ if(PFU_BUILD_AGENT) src/agent/bubblewindow.cpp src/agent/bubblecontroller.cpp src/agent/lockcontroller.cpp + src/agent/inputwatcher.cpp src/agent/enrollcontroller.cpp ) target_include_directories(plasma-face-unlock-agent PRIVATE src/agent) @@ -155,7 +155,8 @@ if(PFU_BUILD_AGENT) endif() qt6_generate_wayland_protocol_client_sources(plasma-face-unlock-agent - FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml) + FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/protocols/ext-idle-notify-v1.xml) # The QML files sit next to the module's qmldir in the resources, so they # see each other (and the Theme singleton) without importing anything. @@ -189,7 +190,7 @@ if(PFU_BUILD_AGENT) Qt6::Gui Qt6::GuiPrivate Qt6::Quick Qt6::DBus Qt6::Network Qt6::WaylandClient Qt6::WaylandClientPrivate LayerShellQt::Interface - KF6::IdleTime KF6::I18n KF6::I18nQml + KF6::I18n KF6::I18nQml pfu_common ) install(TARGETS plasma-face-unlock-agent DESTINATION ${PFU_LIBEXECDIR}) diff --git a/README.md b/README.md index 7c24156..8f4e6e2 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ sudo make install ``` Needs CMake, a C++20 compiler, Qt 6 (Core, DBus, Network, Gui, Quick, WaylandClient), LayerShellQt, -KIdleTime, KI18n, OpenCV 4.5.4 or newer with the DNN module, Linux-PAM and libsystemd. Optionally +KI18n, OpenCV 4.5.4 or newer with the DNN module, Linux-PAM and libsystemd. Optionally `msgfmt` (gettext) for translations and `scdoc` for the man page. Supports `PREFIX` and `DESTDIR`. `make check` runs syntax checks and shellcheck. diff --git a/doc/plasma-face-unlock.1.scd b/doc/plasma-face-unlock.1.scd index de8e312..817ea0b 100644 --- a/doc/plasma-face-unlock.1.scd +++ b/doc/plasma-face-unlock.1.scd @@ -171,7 +171,9 @@ The agent watches for the screen to lock (org.freedesktop.ScreenSaver) and scans when somebody comes back: - on any key or mouse movement after the screen locked, once 1.5 seconds have - passed (the key that locked it does not count); + passed (the key that locked it does not count). Enter on the empty password + field is a key like any other. This works while a video or an app keeps the + screen on, too (ext_idle_notifier_v1, input notification); - when the machine wakes from sleep; - right after locking, if *Look right after the screen locks* is on. Off by default: whoever locks their screen on purpose is usually still in front of @@ -256,7 +258,7 @@ _$XDG_RUNTIME_DIR/plasma-face-unlock/agent.socket_ # REQUIREMENTS KDE Plasma 6 on Wayland, a camera, OpenCV 4.5.4 or newer with its DNN module, -Qt 6, LayerShellQt, KIdleTime, KI18n, systemd, polkit and Linux-PAM. +Qt 6, LayerShellQt, KI18n, systemd, polkit and Linux-PAM. # SEE ALSO diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index e5c482d..577cdb3 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -11,7 +11,7 @@ arch=('x86_64' 'aarch64') url="https://github.com/LoonixTools/plasma-face-unlock" license=('GPL-3.0-or-later' 'MIT' 'Apache-2.0') depends=('bash' 'coreutils' 'gawk' 'grep' 'sed' 'gettext' 'systemd' 'systemd-libs' 'pam' 'polkit' - 'opencv' 'qt6-base' 'qt6-declarative' 'layer-shell-qt' 'kidletime' 'ki18n' 'kscreenlocker') + 'opencv' 'qt6-base' 'qt6-declarative' 'layer-shell-qt' 'ki18n' 'kscreenlocker') makedepends=('cmake' 'scdoc') install="${pkgname}.install" source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz" diff --git a/packaging/rpm/plasma-face-unlock.spec b/packaging/rpm/plasma-face-unlock.spec index 7a45ae4..3e12757 100644 --- a/packaging/rpm/plasma-face-unlock.spec +++ b/packaging/rpm/plasma-face-unlock.spec @@ -34,7 +34,6 @@ BuildRequires: cmake(Qt6WaylandClient) BuildRequires: qt6-qtbase-private-devel BuildRequires: qt6-qtwayland-devel BuildRequires: cmake(LayerShellQt) -BuildRequires: cmake(KF6IdleTime) BuildRequires: cmake(KF6I18n) Requires: bash >= 4.2 diff --git a/protocols/ext-idle-notify-v1.xml b/protocols/ext-idle-notify-v1.xml new file mode 100644 index 0000000..db7d9c1 --- /dev/null +++ b/protocols/ext-idle-notify-v1.xml @@ -0,0 +1,131 @@ + + + + Copyright © 2015 Martin Gräßlin + Copyright © 2022 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This interface allows clients to monitor user idle status. + + After binding to this global, clients can create ext_idle_notification_v1 + objects to get notified when the user is idle for a given amount of time. + + + + + Destroy the manager object. All objects created via this interface + remain valid. + + + + + + Create a new idle notification object. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + Create a new idle notification object to track input from the + user, such as keyboard and mouse movement. Because this object is + meant to track user input alone, it ignores idle inhibitors. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + This interface is used by the compositor to send idle notification events + to clients. + + Initially the notification object is not idle. The notification object + becomes idle when no user activity has happened for at least the timeout + duration, starting from the creation of the notification object. User + activity may include input events or a presence sensor, but is + compositor-specific. + + How this notification responds to idle inhibitors depends on how + it was constructed. If constructed from the + get_idle_notification request, then if an idle inhibitor is + active (e.g. another client has created a zwp_idle_inhibitor_v1 + on a visible surface), the compositor must not make the + notification object idle. However, if constructed from the + get_input_idle_notification request, then idle inhibitors are + ignored, and only input from the user, e.g. from a keyboard or + mouse, counts as activity. + + When the notification object becomes idle, an idled event is sent. When + user activity starts again, the notification object stops being idle, + a resumed event is sent and the timeout is restarted. + + + + + Destroy the notification object. + + + + + + This event is sent when the notification object becomes idle. + + It's a compositor protocol error to send this event twice without a + resumed event in-between. + + + + + + This event is sent when the notification object stops being idle. + + It's a compositor protocol error to send this event twice without an + idled event in-between. It's a compositor protocol error to send this + event prior to any idled event. + + + + diff --git a/src/agent/inputwatcher.cpp b/src/agent/inputwatcher.cpp new file mode 100644 index 0000000..5d01760 --- /dev/null +++ b/src/agent/inputwatcher.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "inputwatcher.h" + +#include +#include +#include + +#include + +#include "qwayland-ext-idle-notify-v1.h" + +class IdleNotifier : public QWaylandClientExtensionTemplate, public QtWayland::ext_idle_notifier_v1 +{ +public: + IdleNotifier() + : QWaylandClientExtensionTemplate(2) + { + initialize(); + } + ~IdleNotifier() override + { + if (isActive()) { + destroy(); + } + } +}; + +class IdleNotification : public QtWayland::ext_idle_notification_v1 +{ +public: + IdleNotification(::ext_idle_notification_v1 *object, std::function resumed) + : QtWayland::ext_idle_notification_v1(object) + , m_resumed(std::move(resumed)) + { + } + ~IdleNotification() override + { + destroy(); + } + +protected: + // Only ever sent after idled: calm first, then this. + void ext_idle_notification_v1_resumed() override + { + if (!m_fired) { + m_fired = true; + m_resumed(); + } + } + +private: + std::function m_resumed; + bool m_fired = false; +}; + +InputWatcher::InputWatcher(QObject *parent) + : QObject(parent) + , m_notifier(std::make_unique()) +{ +} + +InputWatcher::~InputWatcher() = default; + +void InputWatcher::watch(int calmMs) +{ + m_notification.reset(); + auto *wayland = qGuiApp->nativeInterface(); + if (!m_notifier->isActive() || !wayland || !wayland->seat()) { + qWarning("no ext_idle_notifier_v1, so no telling when somebody comes back"); + return; + } + // Version 1 has only the notification that inhibitors hold back. + ::ext_idle_notification_v1 *object = m_notifier->QWaylandClientExtension::version() >= 2 + ? m_notifier->get_input_idle_notification(uint32_t(calmMs), wayland->seat()) + : m_notifier->get_idle_notification(uint32_t(calmMs), wayland->seat()); + // Queued: not from inside the Wayland event, which the receiver might + // answer by watching anew and so destroying the notification. + m_notification = std::make_unique(object, [this] { + QMetaObject::invokeMethod(this, &InputWatcher::input, Qt::QueuedConnection); + }); +} + +void InputWatcher::stop() +{ + m_notification.reset(); +} diff --git a/src/agent/inputwatcher.h b/src/agent/inputwatcher.h new file mode 100644 index 0000000..b637cbe --- /dev/null +++ b/src/agent/inputwatcher.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// Somebody touching the machine: a key, a click, the mouse, the touchpad. +// +// From ext_idle_notifier_v1, like KIdleTime, but with the input notification +// of version 2. KIdleTime's own honours idle inhibitors: with a video playing +// or an app keeping the screen on, no key reached it and the lock screen never +// scanned. + +#pragma once + +#include + +#include + +class IdleNotifier; +class IdleNotification; + +class InputWatcher : public QObject +{ + Q_OBJECT +public: + explicit InputWatcher(QObject *parent = nullptr); + ~InputWatcher() override; + + // Emit input() once, for the first input after nothing was touched for + // calmMs. 0: the next input. Replaces what was watched before. + void watch(int calmMs); + void stop(); + +Q_SIGNALS: + void input(); + +private: + std::unique_ptr m_notifier; + std::unique_ptr m_notification; +}; diff --git a/src/agent/lockcontroller.cpp b/src/agent/lockcontroller.cpp index 56f2665..654eac2 100644 --- a/src/agent/lockcontroller.cpp +++ b/src/agent/lockcontroller.cpp @@ -6,8 +6,6 @@ #include "daemonclient.h" #include "userconfig.h" -#include - #include #include #include @@ -38,7 +36,10 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb , m_config(config) { m_armTimer.setSingleShot(true); - connect(&m_armTimer, &QTimer::timeout, this, &LockController::arm); + connect(&m_armTimer, &QTimer::timeout, this, [this] { + arm(0); + }); + connect(&m_input, &InputWatcher::input, this, &LockController::onResume); QDBusConnection session = QDBusConnection::sessionBus(); session.connect(QStringLiteral("org.freedesktop.ScreenSaver"), @@ -55,17 +56,6 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb this, SLOT(onPrepareForSleep(bool))); - KIdleTime *idle = KIdleTime::instance(); - connect(idle, &KIdleTime::resumingFromIdle, this, &LockController::onResume); - connect(idle, qOverload(&KIdleTime::timeoutReached), this, [this, idle](int id, int) { - if (id != m_idleId) { - return; - } - idle->removeIdleTimeout(id); - m_idleId = -1; - arm(); - }); - // Started while the screen is already locked (the agent restarted). const QDBusMessage get = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), @@ -86,16 +76,10 @@ void LockController::onActiveChanged(bool active) if (active == m_locked) { return; } - KIdleTime *idle = KIdleTime::instance(); - if (!active) { m_locked = false; m_armTimer.stop(); - if (m_idleId >= 0) { - idle->removeIdleTimeout(m_idleId); - m_idleId = -1; - } - idle->stopCatchingResumeEvent(); + m_input.stop(); if (m_scan) { m_scan->abort(); m_scan->deleteLater(); @@ -148,12 +132,12 @@ void LockController::onPrepareForSleep(bool sleeping) } } -void LockController::arm() +void LockController::arm(int calmMs) { if (!m_locked || m_stopped || !m_config->scanOnWake()) { return; } - KIdleTime::instance()->catchNextResumeEvent(); + m_input.watch(calmMs); } void LockController::onResume() @@ -221,9 +205,7 @@ void LockController::onScanFinished(const QJsonObject &result) m_armTimer.start(GraceAfterLockMs); return; } - if (m_idleId < 0) { - m_idleId = KIdleTime::instance()->addIdleTimeout(CalmBeforeRetryMs); - } + arm(CalmBeforeRetryMs); } void LockController::unlock() diff --git a/src/agent/lockcontroller.h b/src/agent/lockcontroller.h index 02e63d0..eb871db 100644 --- a/src/agent/lockcontroller.h +++ b/src/agent/lockcontroller.h @@ -19,6 +19,8 @@ #pragma once +#include "inputwatcher.h" + #include #include #include @@ -44,7 +46,8 @@ private Q_SLOTS: void onPrepareForSleep(bool sleeping); private: - void arm(); + // Scan at the next input after calmMs without any. + void arm(int calmMs); void onResume(); void startScan(const QString &why); void onScanFinished(const QJsonObject &result); @@ -58,5 +61,5 @@ private: QElapsedTimer m_lockedFor; QPointer m_scan; QTimer m_armTimer; - int m_idleId = -1; + InputWatcher m_input; };