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.
 
 
 
 
 
 

235 lines
12 KiB

#include "ptt_portal.h"
#include "../../lib/debug_config.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusServiceWatcher>
#include <QDBusVariant>
#include <QUuid>
static const QString kService = "org.freedesktop.portal.Desktop";
static const QString kPath = "/org/freedesktop/portal/desktop";
static const QString kInterface = "org.freedesktop.portal.GlobalShortcuts";
static const QString kRequestInterface = "org.freedesktop.portal.Request";
static const QString kSessionInterface = "org.freedesktop.portal.Session";
QDBusArgument& operator<<(QDBusArgument& arg, const PttPortalShortcut& shortcut) {
arg.beginStructure(); arg << shortcut.id << shortcut.properties; arg.endStructure(); return arg;
}
const QDBusArgument& operator>>(const QDBusArgument& arg, PttPortalShortcut& shortcut) {
arg.beginStructure(); arg >> shortcut.id >> shortcut.properties; arg.endStructure(); return arg;
}
PttPortal::PttPortal(QObject* parent, const QString& sessionToken) : QObject(parent), m_sessionToken(sessionToken) {
qDBusRegisterMetaType<PttPortalShortcut>();
qDBusRegisterMetaType<PttPortalShortcuts>();
m_requestTimer.setSingleShot(true);
connect(&m_requestTimer, &QTimer::timeout, this, [this]() { fail("request timed out"); });
auto* watcher = new QDBusServiceWatcher(kService, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration, this);
connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this]() { fail("portal service disappeared"); });
auto bus = QDBusConnection::sessionBus();
bool ok = bus.connect(kService, kPath, kInterface, "Activated", this,
SLOT(activated(QDBusObjectPath,QString,qulonglong,QVariantMap)));
ok &= bus.connect(kService, kPath, kInterface, "Deactivated", this,
SLOT(deactivated(QDBusObjectPath,QString,qulonglong,QVariantMap)));
ok &= bus.connect(kService, kPath, kInterface, "ShortcutsChanged", this,
SLOT(shortcutsChanged(QDBusObjectPath,PttPortalShortcuts)));
if (!ok) fail("cannot subscribe to portal signals");
}
PttPortal::~PttPortal() { closeSession(); }
bool PttPortal::busy() const {
return m_state == State::Checking || m_state == State::Creating || m_state == State::Listing || m_state == State::Binding;
}
void PttPortal::setStatus(const QString& text) { m_status = text; emit statusChanged(); }
void PttPortal::initialize() {
if (busy() || m_state == State::Ready || m_state == State::Unbound) return;
m_state = State::Checking;
setStatus("Подключение глобальных хоткеев…");
auto message = QDBusMessage::createMethodCall(kService, kPath, "org.freedesktop.DBus.Properties", "Get");
message.setArguments({kInterface, "version"});
auto* watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
uint epoch = m_epoch;
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, epoch]() {
QDBusPendingReply<QDBusVariant> reply = *watcher;
watcher->deleteLater();
if (epoch != m_epoch || m_state != State::Checking) return;
if (reply.isError()) { fail(reply.error().message()); return; }
uint version = reply.value().variant().toUInt();
if (version < 2) { fail(QString("GlobalShortcuts version %1; version 2 required").arg(version)); return; }
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT portal: connected version=%u", version);
m_state = State::Creating;
// Стабильный session token сохраняет идентичность обычного приложения в KDE без sandbox app_id.
request("CreateSession", {}, {{"session_handle_token", m_sessionToken}});
});
}
void PttPortal::request(const QString& method, const QVariantList& arguments, QVariantMap options) {
auto bus = QDBusConnection::sessionBus();
QString sender = bus.baseService().mid(1).replace('.', '_');
QString token = "utun_ptt_" + QUuid::createUuid().toString(QUuid::Id128);
m_request = kPath + "/request/" + sender + "/" + token;
if (!bus.connect(kService, m_request, kRequestInterface, "Response", this, SLOT(response(uint,QVariantMap,QDBusMessage)))) {
fail("cannot subscribe to request response"); return;
}
options.insert("handle_token", token);
auto message = QDBusMessage::createMethodCall(kService, kPath, kInterface, method);
QVariantList args = arguments; args << options; message.setArguments(args);
QString expected = m_request;
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: %s request=%s session=%s", qPrintable(method), qPrintable(expected), qPrintable(m_session));
// Binding может ждать действий пользователя; остальные операции ограничены 30 секундами.
if (m_state != State::Binding) m_requestTimer.start(30000);
auto* watcher = new QDBusPendingCallWatcher(bus.asyncCall(message), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, expected]() {
QDBusPendingReply<QDBusObjectPath> reply = *watcher;
watcher->deleteLater();
if (m_request != expected) return; // Response уже обработан или сессия закрыта
if (reply.isError()) { fail(reply.error().message()); return; }
if (reply.value().path() != expected) fail("portal returned unexpected request path");
});
}
void PttPortal::disconnectRequest() {
m_requestTimer.stop();
if (m_request.isEmpty()) return;
QDBusConnection::sessionBus().disconnect(kService, m_request, kRequestInterface, "Response", this, SLOT(response(uint,QVariantMap,QDBusMessage)));
m_request.clear();
}
void PttPortal::response(uint code, const QVariantMap& results, const QDBusMessage& message) {
if (m_request.isEmpty() || message.path() != m_request) return;
State state = m_state;
disconnectRequest();
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: response state=%d code=%u", int(state), code);
if (code != 0) {
fail(QString("request %1: response=%2%3").arg(int(state)).arg(code).arg(code == 1 ? " (cancelled)" : ""));
if (code == 1) setStatus("Назначение отменено. Нажмите кнопку, чтобы повторить.");
return;
}
if (state == State::Creating) {
m_session = results.value("session_handle").toString();
if (!m_session.startsWith(kPath + "/session/")) { fail("invalid session handle"); return; }
if (!QDBusConnection::sessionBus().connect(kService, m_session, kSessionInterface, "Closed", this, SLOT(sessionClosed()))) {
fail("cannot subscribe to session closure"); return;
}
m_state = State::Listing;
request("ListShortcuts", {QVariant::fromValue(QDBusObjectPath(m_session))}, {});
} else if (state == State::Listing) {
auto shortcuts = qdbus_cast<PttPortalShortcuts>(results.value("shortcuts"));
updateDescriptions(shortcuts);
QSet<QString> known;
for (const auto& shortcut : shortcuts) if (isPttId(shortcut.id)) known.insert(shortcut.id);
m_configureAfterBind = known.size() == 3 && m_configureRequested;
m_state = State::Unbound;
if (!known.isEmpty() || m_configureRequested) bind();
else setStatus("Назначьте сочетание в системных настройках.");
} else if (state == State::Binding) {
m_state = State::Ready;
updateDescriptions(qdbus_cast<PttPortalShortcuts>(results.value("shortcuts")));
setStatus("Глобальные хоткеи подключены.");
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT portal: bindings ready session=%s", qPrintable(m_session));
// Первое BindShortcuts само открывает окно назначения, повторное Configure — отдельный вызов.
m_configureRequested = false;
if (m_configureAfterBind) { m_configureAfterBind = false; openConfiguration(); }
} else fail("unexpected request response");
}
bool PttPortal::isPttId(const QString& id) { return id == "ptt1" || id == "ptt2" || id == "ptt3"; }
void PttPortal::bind() {
PttPortalShortcuts shortcuts;
for (int i = 1; i <= 3; i++) shortcuts << PttPortalShortcut{QString("ptt%1").arg(i), {{"description", QString("Рация: передача %1").arg(i)}}};
m_state = State::Binding;
setStatus("Назначение глобальных хоткеев…");
request("BindShortcuts", {QVariant::fromValue(QDBusObjectPath(m_session)), QVariant::fromValue(shortcuts), m_parentWindow}, {});
}
void PttPortal::configure(const QString& parentWindow) {
m_parentWindow = parentWindow;
if (m_state == State::Ready) { openConfiguration(); return; }
m_configureRequested = true;
if (m_state == State::Unbound) bind();
else initialize();
}
void PttPortal::openConfiguration() {
auto message = QDBusMessage::createMethodCall(kService, kPath, kInterface, "ConfigureShortcuts");
message.setArguments({QVariant::fromValue(QDBusObjectPath(m_session)), m_parentWindow, QVariantMap{}});
auto* watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
uint epoch = m_epoch;
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, epoch]() {
QDBusPendingReply<> reply = *watcher;
watcher->deleteLater();
if (epoch != m_epoch) return;
if (reply.isError()) { fail(reply.error().message()); return; }
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "PTT portal: system configuration opened");
});
}
void PttPortal::updateDescriptions(const PttPortalShortcuts& shortcuts) {
m_descriptions = {"", "", ""};
for (const auto& shortcut : shortcuts) {
if (!isPttId(shortcut.id)) continue;
int index = shortcut.id.right(1).toInt() - 1;
m_descriptions[index] = shortcut.properties.value("trigger_description").toString();
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: %s trigger=%s", qPrintable(shortcut.id), qPrintable(m_descriptions[index]));
}
emit descriptionsChanged();
}
void PttPortal::activated(const QDBusObjectPath& session, const QString& id, qulonglong, const QVariantMap&) {
if (session.path() != m_session || m_state != State::Ready || !isPttId(id) || m_inhibited) return;
m_active.insert(id); updateTalking();
}
void PttPortal::deactivated(const QDBusObjectPath& session, const QString& id, qulonglong, const QVariantMap&) {
if (session.path() != m_session || !isPttId(id)) return;
m_active.remove(id); updateTalking();
}
void PttPortal::shortcutsChanged(const QDBusObjectPath& session, const PttPortalShortcuts& shortcuts) {
if (session.path() != m_session) return;
m_active.clear(); updateTalking(); updateDescriptions(shortcuts);
}
void PttPortal::setInhibited(bool inhibited) {
m_inhibited = inhibited;
if (inhibited) { m_active.clear(); updateTalking(); }
}
void PttPortal::updateTalking() {
bool talking = !m_inhibited && !m_active.isEmpty();
if (m_talking == talking) return;
m_talking = talking;
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "PTT portal: transmission %s active_shortcuts=%d", talking ? "started" : "stopped", int(m_active.size()));
emit talkingChanged(talking);
}
void PttPortal::closeSession() {
m_epoch++;
auto bus = QDBusConnection::sessionBus();
if (!m_request.isEmpty()) {
auto close = QDBusMessage::createMethodCall(kService, m_request, kRequestInterface, "Close");
close.setAutoStartService(false);
bus.asyncCall(close);
disconnectRequest();
}
if (!m_session.isEmpty()) {
bus.disconnect(kService, m_session, kSessionInterface, "Closed", this, SLOT(sessionClosed()));
auto close = QDBusMessage::createMethodCall(kService, m_session, kSessionInterface, "Close");
close.setAutoStartService(false);
bus.asyncCall(close);
m_session.clear();
}
}
void PttPortal::fail(const QString& detail) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "PTT portal: %s", qPrintable(detail));
closeSession();
m_active.clear(); updateTalking();
m_configureRequested = m_configureAfterBind = false;
m_state = State::Failed;
m_descriptions = {"", "", ""}; emit descriptionsChanged();
setStatus("Глобальные хоткеи недоступны. Повторите назначение; подробности в логе radio.");
}
void PttPortal::sessionClosed() { fail("portal session closed"); }