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
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"); }
|
|
|