diff --git a/autotests/client/test_idle.cpp b/autotests/client/test_idle.cpp index dd41964..18169c9 100644 --- a/autotests/client/test_idle.cpp +++ b/autotests/client/test_idle.cpp @@ -1,260 +1,289 @@ /******************************************************************** Copyright 2016 Martin Gräßlin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ // Qt #include // client #include "../../src/client/connection_thread.h" #include "../../src/client/event_queue.h" #include "../../src/client/idle.h" #include "../../src/client/registry.h" #include "../../src/client/seat.h" // server #include "../../src/server/display.h" #include "../../src/server/idle_interface.h" #include "../../src/server/seat_interface.h" using namespace KWayland::Client; using namespace KWayland::Server; class IdleTest : public QObject { Q_OBJECT private Q_SLOTS: void init(); void cleanup(); void testTimeout(); void testSimulateUserActivity(); + void testServerSimulateUserActivity(); void testIdleInhibit(); void testIdleInhibitBlocksTimeout(); private: Display *m_display = nullptr; SeatInterface *m_seatInterface = nullptr; IdleInterface *m_idleInterface = nullptr; ConnectionThread *m_connection = nullptr; QThread *m_thread = nullptr; EventQueue *m_queue = nullptr; Seat *m_seat = nullptr; Idle *m_idle = nullptr; }; static const QString s_socketName = QStringLiteral("kwayland-test-idle-0"); void IdleTest::init() { delete m_display; m_display = new Display(this); m_display->setSocketName(s_socketName); m_display->start(); QVERIFY(m_display->isRunning()); m_display->createShm(); m_seatInterface = m_display->createSeat(); m_seatInterface->setName(QStringLiteral("seat0")); m_seatInterface->create(); m_idleInterface = m_display->createIdle(); m_idleInterface->create(); // setup connection m_connection = new KWayland::Client::ConnectionThread; QSignalSpy connectedSpy(m_connection, &ConnectionThread::connected); QVERIFY(connectedSpy.isValid()); m_connection->setSocketName(s_socketName); m_thread = new QThread(this); m_connection->moveToThread(m_thread); m_thread->start(); m_connection->initConnection(); QVERIFY(connectedSpy.wait()); m_queue = new EventQueue(this); m_queue->setup(m_connection); Registry registry; QSignalSpy interfacesAnnouncedSpy(®istry, &Registry::interfacesAnnounced); QVERIFY(interfacesAnnouncedSpy.isValid()); registry.setEventQueue(m_queue); registry.create(m_connection); QVERIFY(registry.isValid()); registry.setup(); QVERIFY(interfacesAnnouncedSpy.wait()); m_seat = registry.createSeat(registry.interface(Registry::Interface::Seat).name, registry.interface(Registry::Interface::Seat).version, this); QVERIFY(m_seat->isValid()); m_idle = registry.createIdle(registry.interface(Registry::Interface::Idle).name, registry.interface(Registry::Interface::Idle).version, this); QVERIFY(m_idle->isValid()); } void IdleTest::cleanup() { #define CLEANUP(variable) \ if (variable) { \ delete variable; \ variable = nullptr; \ } CLEANUP(m_idle) CLEANUP(m_seat) CLEANUP(m_queue) if (m_connection) { m_connection->deleteLater(); m_connection = nullptr; } if (m_thread) { m_thread->quit(); m_thread->wait(); delete m_thread; m_thread = nullptr; } CLEANUP(m_idleInterface) CLEANUP(m_seatInterface) CLEANUP(m_display) #undef CLEANUP } void IdleTest::testTimeout() { // this test verifies the basic functionality of a timeout, that it gets fired // and that it resumes from idle, etc. QScopedPointer timeout(m_idle->getTimeout(1, m_seat)); QVERIFY(timeout->isValid()); QSignalSpy idleSpy(timeout.data(), &IdleTimeout::idle); QVERIFY(idleSpy.isValid()); QSignalSpy resumedFormIdleSpy(timeout.data(), &IdleTimeout::resumeFromIdle); QVERIFY(resumedFormIdleSpy.isValid()); // we requested a timeout of 1 msec, but the minimum the server sets is 5 sec QVERIFY(!idleSpy.wait(500)); // the default of 5 sec will now pass QVERIFY(idleSpy.wait()); // simulate some activity QVERIFY(resumedFormIdleSpy.isEmpty()); m_seatInterface->setTimestamp(1); QVERIFY(resumedFormIdleSpy.wait()); timeout.reset(); m_connection->flush(); m_display->dispatchEvents(); } void IdleTest::testSimulateUserActivity() { // this test verifies that simulate user activity doesn't fire the timer QScopedPointer timeout(m_idle->getTimeout(6000, m_seat)); QVERIFY(timeout->isValid()); QSignalSpy idleSpy(timeout.data(), &IdleTimeout::idle); QVERIFY(idleSpy.isValid()); QSignalSpy resumedFormIdleSpy(timeout.data(), &IdleTimeout::resumeFromIdle); QVERIFY(resumedFormIdleSpy.isValid()); m_connection->flush(); QTest::qWait(4000); timeout->simulateUserActivity(); // waiting default five sec should fail QVERIFY(!idleSpy.wait()); // another 2 sec should fire QVERIFY(idleSpy.wait(2000)); // now simulating user activity should emit a resumedFromIdle QVERIFY(resumedFormIdleSpy.isEmpty()); timeout->simulateUserActivity(); QVERIFY(resumedFormIdleSpy.wait()); timeout.reset(); m_connection->flush(); m_display->dispatchEvents(); } +void IdleTest::testServerSimulateUserActivity() +{ + // this test verifies that simulate user activity doesn't fire the timer + QScopedPointer timeout(m_idle->getTimeout(6000, m_seat)); + QVERIFY(timeout->isValid()); + QSignalSpy idleSpy(timeout.data(), &IdleTimeout::idle); + QVERIFY(idleSpy.isValid()); + QSignalSpy resumedFormIdleSpy(timeout.data(), &IdleTimeout::resumeFromIdle); + QVERIFY(resumedFormIdleSpy.isValid()); + m_connection->flush(); + + QTest::qWait(4000); + m_idleInterface->simulateUserActivity(); + // waiting default five sec should fail + QVERIFY(!idleSpy.wait()); + // another 2 sec should fire + QVERIFY(idleSpy.wait(2000)); + + // now simulating user activity should emit a resumedFromIdle + QVERIFY(resumedFormIdleSpy.isEmpty()); + m_idleInterface->simulateUserActivity(); + QVERIFY(resumedFormIdleSpy.wait()); + + timeout.reset(); + m_connection->flush(); + m_display->dispatchEvents(); +} + void IdleTest::testIdleInhibit() { QCOMPARE(m_idleInterface->isInhibited(), false); QSignalSpy idleInhibitedSpy(m_idleInterface, &IdleInterface::inhibitedChanged); QVERIFY(idleInhibitedSpy.isValid()); m_idleInterface->inhibit(); QCOMPARE(m_idleInterface->isInhibited(), true); QCOMPARE(idleInhibitedSpy.count(), 1); m_idleInterface->inhibit(); QCOMPARE(m_idleInterface->isInhibited(), true); QCOMPARE(idleInhibitedSpy.count(), 1); m_idleInterface->uninhibit(); QCOMPARE(m_idleInterface->isInhibited(), true); QCOMPARE(idleInhibitedSpy.count(), 1); m_idleInterface->uninhibit(); QCOMPARE(m_idleInterface->isInhibited(), false); QCOMPARE(idleInhibitedSpy.count(), 2); } void IdleTest::testIdleInhibitBlocksTimeout() { // this test verifies that a timeout does not fire when the system is inhibited // so first inhibit QCOMPARE(m_idleInterface->isInhibited(), false); m_idleInterface->inhibit(); QScopedPointer timeout(m_idle->getTimeout(1, m_seat)); QVERIFY(timeout->isValid()); QSignalSpy idleSpy(timeout.data(), &IdleTimeout::idle); QVERIFY(idleSpy.isValid()); QSignalSpy resumedFormIdleSpy(timeout.data(), &IdleTimeout::resumeFromIdle); QVERIFY(resumedFormIdleSpy.isValid()); // we requested a timeout of 1 msec, but the minimum the server sets is 5 sec QVERIFY(!idleSpy.wait(500)); // the default of 5 sec won't pass QVERIFY(!idleSpy.wait()); // simulate some activity QVERIFY(resumedFormIdleSpy.isEmpty()); m_seatInterface->setTimestamp(1); // resume from idle should not fire QVERIFY(!resumedFormIdleSpy.wait()); // let's uninhibit m_idleInterface->uninhibit(); QCOMPARE(m_idleInterface->isInhibited(), false); // we requested a timeout of 1 msec, but the minimum the server sets is 5 sec QVERIFY(!idleSpy.wait(500)); // the default of 5 sec will now pass QVERIFY(idleSpy.wait()); // if we inhibit now it will trigger a resume from idle QVERIFY(resumedFormIdleSpy.isEmpty()); m_idleInterface->inhibit(); QVERIFY(resumedFormIdleSpy.wait()); // let's wait again just to verify that also inhibit for already existing IdleTimeout works QVERIFY(!idleSpy.wait(500)); QVERIFY(!idleSpy.wait()); QCOMPARE(idleSpy.count(), 1); timeout.reset(); m_connection->flush(); m_display->dispatchEvents(); } QTEST_GUILESS_MAIN(IdleTest) #include "test_idle.moc" diff --git a/src/server/idle_interface.cpp b/src/server/idle_interface.cpp index cf27a1a..1fec113 100644 --- a/src/server/idle_interface.cpp +++ b/src/server/idle_interface.cpp @@ -1,260 +1,268 @@ /******************************************************************** Copyright 2015 Martin Gräßlin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #include "idle_interface.h" #include "display.h" #include "global_p.h" #include "resource_p.h" #include "seat_interface.h" #include #include #include namespace KWayland { namespace Server { class IdleInterface::Private : public Global::Private { public: Private(IdleInterface *q, Display *d); int inhibitCount = 0; + QVector idleTimeouts; private: void bind(wl_client *client, uint32_t version, uint32_t id) override; static void getIdleTimeoutCallback(wl_client *client, wl_resource *resource, uint32_t id, wl_resource *seat, uint32_t timeout); static void unbind(wl_resource *resource); static Private *cast(wl_resource *r) { return reinterpret_cast(wl_resource_get_user_data(r)); } IdleInterface *q; static const struct org_kde_kwin_idle_interface s_interface; static const quint32 s_version; }; class IdleTimeoutInterface::Private : public Resource::Private { public: Private(SeatInterface *seat, IdleTimeoutInterface *q, IdleInterface *manager, wl_resource *parentResource); ~Private(); void setup(quint32 timeout); + void simulateUserActivity(); + SeatInterface *seat; QTimer *timer = nullptr; private: static void simulateUserActivityCallback(wl_client *client, wl_resource *resource); IdleTimeoutInterface *q_func() { return reinterpret_cast(q); } static const struct org_kde_kwin_idle_timeout_interface s_interface; }; const quint32 IdleInterface::Private::s_version = 1; #ifndef DOXYGEN_SHOULD_SKIP_THIS const struct org_kde_kwin_idle_interface IdleInterface::Private::s_interface = { getIdleTimeoutCallback }; #endif IdleInterface::Private::Private(IdleInterface *q, Display *d) : Global::Private(d, &org_kde_kwin_idle_interface, s_version) , q(q) { } void IdleInterface::Private::getIdleTimeoutCallback(wl_client *client, wl_resource *resource, uint32_t id, wl_resource *seat, uint32_t timeout) { Private *p = cast(resource); SeatInterface *s = SeatInterface::get(seat); Q_ASSERT(s); IdleTimeoutInterface *idleTimeout = new IdleTimeoutInterface(s, p->q, resource); idleTimeout->create(p->display->getConnection(client), wl_resource_get_version(resource), id); if (!idleTimeout->resource()) { wl_resource_post_no_memory(resource); delete idleTimeout; return; } + p->idleTimeouts << idleTimeout; + QObject::connect(idleTimeout, &IdleTimeoutInterface::aboutToBeUnbound, p->q, + std::bind(&QVector::removeOne, p->idleTimeouts, idleTimeout)); idleTimeout->d_func()->setup(timeout); } void IdleInterface::Private::bind(wl_client *client, uint32_t version, uint32_t id) { auto c = display->getConnection(client); wl_resource *resource = c->createResource(&org_kde_kwin_idle_interface, qMin(version, s_version), id); if (!resource) { wl_client_post_no_memory(client); return; } wl_resource_set_implementation(resource, &s_interface, this, unbind); // TODO: should we track? } void IdleInterface::Private::unbind(wl_resource *resource) { Q_UNUSED(resource) } IdleInterface::IdleInterface(Display *display, QObject *parent) : Global(new Private(this, display), parent) { } IdleInterface::~IdleInterface() = default; void IdleInterface::inhibit() { Q_D(); d->inhibitCount++; if (d->inhibitCount == 1) { emit inhibitedChanged(); } } void IdleInterface::uninhibit() { Q_D(); d->inhibitCount--; if (d->inhibitCount == 0) { emit inhibitedChanged(); } } bool IdleInterface::isInhibited() const { Q_D(); return d->inhibitCount > 0; } +void IdleInterface::simulateUserActivity() +{ + Q_D(); + for (auto i : qAsConst(d->idleTimeouts)) { + i->d_func()->simulateUserActivity(); + } +} + IdleInterface::Private *IdleInterface::d_func() const { return reinterpret_cast(d.data()); } #ifndef DOXYGEN_SHOULD_SKIP_THIS const struct org_kde_kwin_idle_timeout_interface IdleTimeoutInterface::Private::s_interface = { resourceDestroyedCallback, simulateUserActivityCallback }; #endif IdleTimeoutInterface::Private::Private(SeatInterface *seat, IdleTimeoutInterface *q, IdleInterface *manager, wl_resource *parentResource) : Resource::Private(q, manager, parentResource, &org_kde_kwin_idle_timeout_interface, &s_interface) , seat(seat) { } IdleTimeoutInterface::Private::~Private() = default; void IdleTimeoutInterface::Private::simulateUserActivityCallback(wl_client *client, wl_resource *resource) { Q_UNUSED(client); Private *p = reinterpret_cast(wl_resource_get_user_data(resource)); - if (!p->timer) { + p->simulateUserActivity(); +} + +void IdleTimeoutInterface::Private::simulateUserActivity() +{ + if (!timer) { // not yet configured return; } - if (qobject_cast(p->global)->isInhibited()) { + if (qobject_cast(global)->isInhibited()) { // ignored while inhibited return; } - if (!p->timer->isActive() && p->resource) { - org_kde_kwin_idle_timeout_send_resumed(p->resource); + if (!timer->isActive() && resource) { + org_kde_kwin_idle_timeout_send_resumed(resource); } - p->timer->start(); + timer->start(); } void IdleTimeoutInterface::Private::setup(quint32 timeout) { if (timer) { return; } timer = new QTimer(q); timer->setSingleShot(true); // less than 5 sec is not idle by definition timer->setInterval(qMax(timeout, 5000u)); QObject::connect(timer, &QTimer::timeout, q, [this] { if (resource) { org_kde_kwin_idle_timeout_send_idle(resource); } } ); if (qobject_cast(global)->isInhibited()) { // don't start if inhibited return; } timer->start(); } IdleTimeoutInterface::IdleTimeoutInterface(SeatInterface *seat, IdleInterface *parent, wl_resource *parentResource) : Resource(new Private(seat, this, parent, parentResource)) { connect(seat, &SeatInterface::timestampChanged, this, [this] { Q_D(); - if (!d->timer) { - // not yet configured - return; - } - if (qobject_cast(d->global)->isInhibited()) { - // ignored while inhibited - return; - } - if (!d->timer->isActive() && d->resource) { - org_kde_kwin_idle_timeout_send_resumed(d->resource); - } - d->timer->start(); + d->simulateUserActivity(); } ); connect(parent, &IdleInterface::inhibitedChanged, this, [this] { Q_D(); if (!d->timer) { // not yet configured return; } if (qobject_cast(d->global)->isInhibited()) { if (!d->timer->isActive() && d->resource) { org_kde_kwin_idle_timeout_send_resumed(d->resource); } d->timer->stop(); } else { d->timer->start(); } } ); } IdleTimeoutInterface::~IdleTimeoutInterface() = default; IdleTimeoutInterface::Private *IdleTimeoutInterface::d_func() const { return reinterpret_cast(d.data()); } } } diff --git a/src/server/idle_interface.h b/src/server/idle_interface.h index b9f73fd..15bb0a5 100644 --- a/src/server/idle_interface.h +++ b/src/server/idle_interface.h @@ -1,131 +1,140 @@ /******************************************************************** Copyright 2015 Martin Gräßlin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #ifndef KWAYLAND_SERVER_IDLE_INTERFACE_H #define KWAYLAND_SERVER_IDLE_INTERFACE_H #include #include "global.h" #include "resource.h" namespace KWayland { namespace Server { class Display; class SeatInterface; /** * @brief Global representing the org_kde_kwin_idle interface. * * The IdleInterface allows to register callbacks which are invoked if there has * not been any user activity (no input) for a specified time span on a seat. * * A client can bind an idle timeout for a SeatInterface and through that register * an idle timeout. The complete interaction is handled internally, thus the API * user only needs to create the IdleInterface in order to provide this feature. * * This interface is useful for clients as it allows them to perform power management, * chat applications might want to set to away after no user input for some time, etc. * * Of course this exposes the global input usage to all clients. Normally clients don't * know whether the input devices are used, only if their surfaces have focus. With this * interface it is possible to notice that there are input events. A server should consider * this to decide whether it wants to provide this feature! * * @since 5.4 **/ class KWAYLANDSERVER_EXPORT IdleInterface : public Global { Q_OBJECT public: virtual ~IdleInterface(); /** * Inhibits the IdleInterface. While inhibited no IdleTimeoutInterface interface gets * notified about an idle timeout. * * This can be used to inhibit power management, screen locking, etc. directly from * Compositor side. * * To resume idle timeouts invoke @link{uninhibit}. It is possible to invoke inhibit several * times, in that case uninhibit needs to called the same amount as inhibit has been called. * @see uninhibit * @see isInhibited * @see inhibitedChanged * @since 5.41 **/ void inhibit(); /** * Unhibits the IdleInterface. The idle timeouts are only restarted if uninhibit has been * called the same amount as inhibit. * * @see inhibit * @see isInhibited * @see inhibitedChanged * @since 5.41 **/ void uninhibit(); /** * @returns Whether idle timeouts are currently inhibited * @see inhibit * @see uninhibit * @see inhibitedChanged * @since 5.41 **/ bool isInhibited() const; + /** + * Calling this method allows the Compositor to simulate user activity. + * This means the same action is performed as if the user interacted with + * an input device on the SeatInterface. + * Idle timeouts are resumed and the idle time gets restarted. + * @since 5.42 + **/ + void simulateUserActivity(); + Q_SIGNALS: /** * Emitted when the system gets inhibited or uninhibited. * @see inhibit * @see uninhibit * @see isInhibited * @since 5.41 **/ void inhibitedChanged(); private: explicit IdleInterface(Display *display, QObject *parent = nullptr); friend class Display; class Private; Private *d_func() const; }; // TODO: KF6 make private class class KWAYLANDSERVER_EXPORT IdleTimeoutInterface : public Resource { Q_OBJECT public: virtual ~IdleTimeoutInterface(); private: explicit IdleTimeoutInterface(SeatInterface *seat, IdleInterface *parent, wl_resource *parentResource); friend class IdleInterface; class Private; Private *d_func() const; }; } } #endif