#include "user_activity_monitor.h" #include "debug_ui.h" #include #include #include #include #include #ifdef UTUN_ACTIVITY_DBUS #include #include #include #include #endif #ifdef UTUN_ACTIVITY_KIDLETIME #include #endif #ifdef Q_OS_WIN #include #elif defined(Q_OS_MACOS) #include #elif defined(UTUN_ACTIVITY_X11) #include #include #undef KeyPress #undef KeyRelease #endif UserActivityMonitor::UserActivityMonitor(QObject* parent) : QObject(parent) { m_pollTimer.setInterval(250); connect(&m_pollTimer, &QTimer::timeout, this, &UserActivityMonitor::poll); } UserActivityMonitor::~UserActivityMonitor() { stop(); #ifdef UTUN_ACTIVITY_X11 if (m_display) XCloseDisplay(static_cast(m_display)); #endif GUI_DEBUG("activity: monitor destroyed"); } void UserActivityMonitor::start() { stop(); m_watching = true; m_elapsed.start(); qApp->installEventFilter(this); const QString platform = QGuiApplication::platformName(); const bool wayland = platform.startsWith("wayland") || qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland"; #ifdef UTUN_ACTIVITY_DBUS if (wayland && !m_gnomeUnavailable) { m_useGnome = true; m_pollTimer.start(); poll(); return; } #endif if (wayland) { startWaylandFallback(); return; } if (systemIdleTime() >= 0) { GUI_DEBUG("activity: watching system input platform=%s poll_ms=%d", qPrintable(platform), m_pollTimer.interval()); m_pollTimer.start(); } else useLocalActivity(); } void UserActivityMonitor::stop() { ++m_generation; m_pollTimer.stop(); if (m_watching && qApp) qApp->removeEventFilter(this); #ifdef UTUN_ACTIVITY_KIDLETIME if (m_useKIdleTime) KIdleTime::instance()->stopCatchingResumeEvent(); disconnect(m_kIdleConnection); #endif m_watching = false; m_dbusPending = false; m_useGnome = false; m_useKIdleTime = false; } bool UserActivityMonitor::eventFilter(QObject*, QEvent* event) { if (!m_watching || !event->spontaneous()) return false; switch (event->type()) { case QEvent::MouseMove: case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: case QEvent::MouseButtonDblClick: case QEvent::Wheel: case QEvent::KeyPress: case QEvent::KeyRelease: case QEvent::TouchBegin: case QEvent::TouchUpdate: case QEvent::TabletMove: case QEvent::TabletPress: GUI_DEBUG("activity: application input event=%d elapsed_ms=%lld", int(event->type()), (long long)m_elapsed.elapsed()); stop(); emit activity(); break; default: break; } return false; } void UserActivityMonitor::checkIdleTime(qint64 idleMs, qint64 elapsedMs) { if (!m_watching || idleMs < 0 || idleMs >= elapsedMs) return; GUI_DEBUG("activity: system input idle_ms=%lld elapsed_ms=%lld", (long long)idleMs, (long long)elapsedMs); stop(); emit activity(); } void UserActivityMonitor::poll() { #ifdef UTUN_ACTIVITY_DBUS if (m_useGnome) { if (m_dbusPending) return; auto message = QDBusMessage::createMethodCall("org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", "org.gnome.Mutter.IdleMonitor", "GetIdletime"); message.setAutoStartService(false); auto* watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message, 1000), this); const quint64 generation = m_generation; const qint64 elapsedMs = m_elapsed.elapsed(); m_dbusPending = true; connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, generation, elapsedMs] { QDBusPendingReply reply = *watcher; watcher->deleteLater(); if (!m_watching || generation != m_generation) { GUI_DEBUG("activity: ignored obsolete GNOME reply generation=%llu current=%llu watching=%d", (unsigned long long)generation, (unsigned long long)m_generation, int(m_watching)); return; } m_dbusPending = false; if (reply.isError()) { GUI_WARN("activity: GNOME idle monitor unavailable: %s", qPrintable(reply.error().message())); m_gnomeUnavailable = true; m_useGnome = false; m_pollTimer.stop(); startWaylandFallback(); return; } if (!m_gnomeReadyLogged) { GUI_DEBUG("activity: GNOME system idle monitor ready"); m_gnomeReadyLogged = true; } if (reply.value() <= qulonglong(std::numeric_limits::max())) checkIdleTime(qint64(reply.value()), elapsedMs); else GUI_WARN("activity: GNOME idle time out of range: %llu", (unsigned long long)reply.value()); }); return; } #endif const qint64 elapsedMs = m_elapsed.elapsed(); const qint64 idleMs = systemIdleTime(); if (idleMs < 0) { m_pollTimer.stop(); useLocalActivity(); return; } checkIdleTime(idleMs, elapsedMs); } void UserActivityMonitor::startWaylandFallback() { #ifdef UTUN_ACTIVITY_KIDLETIME if (QGuiApplication::platformName().startsWith("wayland")) { auto* idle = KIdleTime::instance(); const int timeout = idle->addIdleTimeout(0); if (timeout) { idle->removeIdleTimeout(timeout); m_kIdleConnection = connect(idle, &KIdleTime::resumingFromIdle, this, [this] { if (!m_watching || !m_useKIdleTime) return; GUI_DEBUG("activity: Wayland compositor reported user activity"); stop(); emit activity(); }); m_useKIdleTime = true; idle->catchNextResumeEvent(); GUI_DEBUG("activity: watching Wayland resume through KIdleTime (subject to compositor idle inhibitors)"); return; } GUI_WARN("activity: KIdleTime has no available compositor backend"); } #endif useLocalActivity(); } void UserActivityMonitor::useLocalActivity() { if (!m_localWarningLogged) { GUI_WARN("activity: global input unavailable platform=%s session=%s; using ChatGUI input and manual dismissal", qPrintable(QGuiApplication::platformName()), qPrintable(qEnvironmentVariable("XDG_SESSION_TYPE"))); m_localWarningLogged = true; } } qint64 UserActivityMonitor::systemIdleTime() { #ifdef Q_OS_WIN LASTINPUTINFO info = {sizeof(info), 0}; if (!GetLastInputInfo(&info)) { GUI_WARN("activity: GetLastInputInfo failed error=%lu", GetLastError()); return -1; } return DWORD(GetTickCount() - info.dwTime); #elif defined(Q_OS_MACOS) const double seconds = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType); if (!std::isfinite(seconds) || seconds < 0 || seconds > double(std::numeric_limits::max() / 1000)) { GUI_WARN("activity: invalid macOS idle time seconds=%g", seconds); return -1; } return qint64(seconds * 1000); #elif defined(UTUN_ACTIVITY_X11) if (QGuiApplication::platformName() != "xcb") return -1; if (!m_x11Checked) { m_x11Checked = true; auto* display = XOpenDisplay(nullptr); int eventBase = 0, errorBase = 0; if (!display) { GUI_WARN("activity: cannot open X11 display"); return -1; } if (!XScreenSaverQueryExtension(display, &eventBase, &errorBase)) { GUI_WARN("activity: X11 screen saver extension unavailable"); XCloseDisplay(display); return -1; } m_display = display; } if (!m_display) return -1; auto* display = static_cast(m_display); XScreenSaverInfo info{}; if (!XScreenSaverQueryInfo(display, DefaultRootWindow(display), &info)) { GUI_WARN("activity: XScreenSaverQueryInfo failed"); return -1; } return qint64(info.idle); #else return -1; #endif }