Browse Source

Add desktop user activity monitoring with platform fallbacks

master
evgeny 4 days ago
parent
commit
40b8e1a740
  1. 213
      tools/chatgui/src/user_activity_monitor.cpp
  2. 41
      tools/chatgui/src/user_activity_monitor.h

213
tools/chatgui/src/user_activity_monitor.cpp

@ -0,0 +1,213 @@
#include "user_activity_monitor.h"
#include "debug_ui.h"
#include <QApplication>
#include <QEvent>
#include <QGuiApplication>
#include <cmath>
#include <limits>
#ifdef UTUN_ACTIVITY_DBUS
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#endif
#ifdef UTUN_ACTIVITY_KIDLETIME
#include <KIdleTime>
#endif
#ifdef Q_OS_WIN
#include <qt_windows.h>
#elif defined(Q_OS_MACOS)
#include <ApplicationServices/ApplicationServices.h>
#elif defined(UTUN_ACTIVITY_X11)
#include <X11/Xlib.h>
#include <X11/extensions/scrnsaver.h>
#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<Display*>(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") ||
(platform == "xcb" && 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();
#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::Wheel:
case QEvent::KeyPress:
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<qulonglong> reply = *watcher;
watcher->deleteLater();
if (!m_watching || generation != m_generation) 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 (elapsedMs == 0) GUI_DEBUG("activity: GNOME system idle monitor ready");
if (reply.value() <= qulonglong(std::numeric_limits<qint64>::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);
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<qint64>::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<Display*>(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
}

41
tools/chatgui/src/user_activity_monitor.h

@ -0,0 +1,41 @@
/* Первая активность после start(): системный idle time или события приложения при ограничениях ОС.
* stop() отменяет наблюдение и запоздалые D-Bus ответы. Все вызовы и сигнал — в GUI-потоке. */
#pragma once
#include <QObject>
#include <QElapsedTimer>
#include <QTimer>
class UserActivityMonitor : public QObject {
Q_OBJECT
public:
explicit UserActivityMonitor(QObject* parent = nullptr);
~UserActivityMonitor() override;
void start();
void stop();
signals:
void activity();
protected:
bool eventFilter(QObject* object, QEvent* event) override;
virtual qint64 systemIdleTime();
private:
void poll();
void checkIdleTime(qint64 idleMs, qint64 elapsedMs);
void useLocalActivity();
void startWaylandFallback();
QTimer m_pollTimer;
QElapsedTimer m_elapsed;
bool m_watching = false;
bool m_localWarningLogged = false;
bool m_dbusPending = false;
bool m_useGnome = false;
bool m_gnomeUnavailable = false;
bool m_useKIdleTime = false;
quint64 m_generation = 0;
void* m_display = nullptr;
bool m_x11Checked = false;
};
Loading…
Cancel
Save