You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
8.1 KiB
223 lines
8.1 KiB
#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") || 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<qulonglong> 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<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); |
|
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<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 |
|
}
|
|
|