File indexing completed on 2024-04-28 16:55:48

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Red Hat Inc
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  *
0006  * SPDX-FileCopyrightText: 2018 Jan Grulich <jgrulich@redhat.com>
0007  */
0008 
0009 #include "session.h"
0010 #include "desktopportal.h"
0011 #include "session_debug.h"
0012 #include "xdgshortcut.h"
0013 
0014 #include <QDBusConnection>
0015 #include <QDBusMessage>
0016 #include <QDBusPendingCall>
0017 #include <QDBusPendingCallWatcher>
0018 #include <QLoggingCategory>
0019 #include <screencast_debug.h>
0020 
0021 #include <KGlobalAccel>
0022 #include <KLocalizedString>
0023 #include <KStatusNotifierItem>
0024 
0025 #include "kglobalaccel_component_interface.h"
0026 #include "kglobalaccel_interface.h"
0027 #include "remotedesktopdialog.h"
0028 #include "utils.h"
0029 #include <QMenu>
0030 
0031 static QMap<QString, Session *> sessionList;
0032 
0033 Session::Session(QObject *parent, const QString &appId, const QString &path)
0034     : QDBusVirtualObject(parent)
0035     , m_appId(appId)
0036     , m_path(path)
0037 {
0038 }
0039 
0040 Session::~Session()
0041 {
0042 }
0043 
0044 bool Session::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
0045 {
0046     Q_UNUSED(connection);
0047 
0048     if (message.path() != m_path) {
0049         return false;
0050     }
0051 
0052     /* Check to make sure we're getting properties on our interface */
0053     if (message.type() != QDBusMessage::MessageType::MethodCallMessage) {
0054         return false;
0055     }
0056 
0057     qCDebug(XdgSessionKdeSession) << message.interface();
0058     qCDebug(XdgSessionKdeSession) << message.member();
0059     qCDebug(XdgSessionKdeSession) << message.path();
0060 
0061     if (message.interface() == QLatin1String("org.freedesktop.impl.portal.Session")) {
0062         if (message.member() == QLatin1String("Close")) {
0063             close();
0064             QDBusMessage reply = message.createReply();
0065             return connection.send(reply);
0066         }
0067     } else if (message.interface() == QLatin1String("org.freedesktop.DBus.Properties")) {
0068         if (message.member() == QLatin1String("Get")) {
0069             if (message.arguments().count() == 2) {
0070                 const QString interface = message.arguments().at(0).toString();
0071                 const QString property = message.arguments().at(1).toString();
0072 
0073                 if (interface == QLatin1String("org.freedesktop.impl.portal.Session") && property == QLatin1String("version")) {
0074                     QList<QVariant> arguments;
0075                     arguments << 1;
0076 
0077                     QDBusMessage reply = message.createReply();
0078                     reply.setArguments(arguments);
0079                     return connection.send(reply);
0080                 }
0081             }
0082         }
0083     }
0084 
0085     return false;
0086 }
0087 
0088 QString Session::introspect(const QString &path) const
0089 {
0090     QString nodes;
0091 
0092     if (path.startsWith(QLatin1String("/org/freedesktop/portal/desktop/session/"))) {
0093         nodes = QStringLiteral(
0094             "<interface name=\"org.freedesktop.impl.portal.Session\">"
0095             "    <method name=\"Close\">"
0096             "    </method>"
0097             "<signal name=\"Closed\">"
0098             "</signal>"
0099             "<property name=\"version\" type=\"u\" access=\"read\"/>"
0100             "</interface>");
0101     }
0102 
0103     qCDebug(XdgSessionKdeSession) << nodes;
0104 
0105     return nodes;
0106 }
0107 
0108 bool Session::close()
0109 {
0110     QDBusMessage reply = QDBusMessage::createSignal(m_path, QStringLiteral("org.freedesktop.impl.portal.Session"), QStringLiteral("Closed"));
0111     const bool result = QDBusConnection::sessionBus().send(reply);
0112 
0113     Q_EMIT closed();
0114 
0115     sessionList.remove(m_path);
0116     QDBusConnection::sessionBus().unregisterObject(m_path);
0117 
0118     deleteLater();
0119 
0120     return result;
0121 }
0122 
0123 Session *Session::createSession(QObject *parent, SessionType type, const QString &appId, const QString &path)
0124 {
0125     QDBusConnection sessionBus = QDBusConnection::sessionBus();
0126 
0127     Session *session = nullptr;
0128     switch (type) {
0129     case ScreenCast:
0130         session = new ScreenCastSession(parent, appId, path, QStringLiteral("media-record"));
0131         break;
0132     case RemoteDesktop:
0133         session = new RemoteDesktopSession(parent, appId, path);
0134         break;
0135     case GlobalShortcuts:
0136         session = new GlobalShortcutsSession(parent, appId, path);
0137         break;
0138     }
0139 
0140     if (sessionBus.registerVirtualObject(path, session, QDBusConnection::VirtualObjectRegisterOption::SubPath)) {
0141         connect(session, &Session::closed, [session, path]() {
0142             sessionList.remove(path);
0143             QDBusConnection::sessionBus().unregisterObject(path);
0144             session->deleteLater();
0145         });
0146         sessionList.insert(path, session);
0147         return session;
0148     } else {
0149         qCDebug(XdgSessionKdeSession) << sessionBus.lastError().message();
0150         qCDebug(XdgSessionKdeSession) << "Failed to register session object: " << path;
0151         session->deleteLater();
0152         return nullptr;
0153     }
0154 }
0155 
0156 Session *Session::getSession(const QString &sessionHandle)
0157 {
0158     return sessionList.value(sessionHandle);
0159 }
0160 
0161 ScreenCastSession::ScreenCastSession(QObject *parent, const QString &appId, const QString &path, const QString &iconName)
0162     : Session(parent, appId, path)
0163     , m_item(new KStatusNotifierItem(this))
0164 {
0165     m_item->setStandardActionsEnabled(false);
0166     m_item->setIconByName(iconName);
0167 
0168     auto menu = new QMenu;
0169     auto stopAction = new QAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18nc("@action:inmenu stops screen/window sharing", "End"));
0170     connect(stopAction, &QAction::triggered, this, &Session::close);
0171     connect(m_item, &KStatusNotifierItem::activateRequested, menu, &QMenu::show);
0172     menu->addAction(stopAction);
0173     m_item->setContextMenu(menu);
0174 }
0175 
0176 ScreenCastSession::~ScreenCastSession()
0177 {
0178 }
0179 
0180 bool ScreenCastSession::multipleSources() const
0181 {
0182     return m_multipleSources;
0183 }
0184 
0185 ScreenCastPortal::SourceType ScreenCastSession::types() const
0186 {
0187     return m_types;
0188 }
0189 
0190 ScreenCastPortal::CursorModes ScreenCastSession::cursorMode() const
0191 {
0192     return m_cursorMode;
0193 }
0194 
0195 void ScreenCastSession::setOptions(const QVariantMap &options)
0196 {
0197     m_multipleSources = options.value(QStringLiteral("multiple")).toBool();
0198     m_cursorMode = ScreenCastPortal::CursorModes(options.value(QStringLiteral("cursor_mode")).toUInt());
0199     m_persistMode = ScreenCastPortal::PersistMode(options.value("persist_mode").toUInt());
0200     m_restoreData = options.value(QStringLiteral("restore_data"));
0201     m_types = ScreenCastPortal::SourceType(options.value(QStringLiteral("types")).toUInt());
0202 
0203     if (m_types == 0) {
0204         m_types = ScreenCastPortal::Monitor;
0205     }
0206 }
0207 
0208 RemoteDesktopSession::RemoteDesktopSession(QObject *parent, const QString &appId, const QString &path)
0209     : ScreenCastSession(parent, appId, path, QStringLiteral("krfb"))
0210     , m_screenSharingEnabled(false)
0211 {
0212     connect(this, &RemoteDesktopSession::closed, this, [this] {
0213         if (m_acquired) {
0214             WaylandIntegration::acquireStreamingInput(false);
0215         }
0216     });
0217 }
0218 
0219 RemoteDesktopSession::~RemoteDesktopSession()
0220 {
0221 }
0222 
0223 RemoteDesktopPortal::DeviceTypes RemoteDesktopSession::deviceTypes() const
0224 {
0225     return m_deviceTypes;
0226 }
0227 
0228 void RemoteDesktopSession::setDeviceTypes(RemoteDesktopPortal::DeviceTypes deviceTypes)
0229 {
0230     m_deviceTypes = deviceTypes;
0231 }
0232 
0233 bool RemoteDesktopSession::screenSharingEnabled() const
0234 {
0235     return m_screenSharingEnabled;
0236 }
0237 
0238 void RemoteDesktopSession::setScreenSharingEnabled(bool enabled)
0239 {
0240     if (m_screenSharingEnabled == enabled) {
0241         return;
0242     }
0243 
0244     m_screenSharingEnabled = enabled;
0245 }
0246 
0247 void RemoteDesktopSession::acquireStreamingInput()
0248 {
0249     WaylandIntegration::acquireStreamingInput(true);
0250     m_acquired = true;
0251 }
0252 
0253 void RemoteDesktopSession::refreshDescription()
0254 {
0255     m_item->setTitle(i18nc("SNI title that indicates there's a process remotely controlling the system", "Remote Desktop"));
0256     m_item->setToolTipTitle(m_item->title());
0257     setDescription(RemoteDesktopDialog::buildDescription(m_appId, deviceTypes(), screenSharingEnabled()));
0258 }
0259 
0260 void ScreenCastSession::setStreams(const WaylandIntegration::Streams &streams)
0261 {
0262     Q_ASSERT(!streams.isEmpty());
0263     m_streams = streams;
0264 
0265     m_item->setStandardActionsEnabled(false);
0266     if (qobject_cast<RemoteDesktopSession *>(this)) {
0267         refreshDescription();
0268     } else {
0269         const bool isWindow = m_streams[0].map[QLatin1String("source_type")] == ScreenCastPortal::Window;
0270         m_item->setToolTipSubTitle(i18ncp("%1 number of screens, %2 the app that receives them",
0271                                           "Sharing contents to %2",
0272                                           "%1 video streams to %2",
0273                                           m_streams.count(),
0274                                           Utils::applicationName(m_appId)));
0275         m_item->setTitle(i18nc("SNI title that indicates there's a process seeing our windows or screens", "Screen casting"));
0276         if (isWindow) {
0277             m_item->setOverlayIconByName(QStringLiteral("window"));
0278         } else {
0279             m_item->setOverlayIconByName(QStringLiteral("monitor"));
0280         }
0281     }
0282     m_item->setToolTipIconByName(m_item->overlayIconName());
0283     m_item->setToolTipTitle(m_item->title());
0284 
0285     for (const auto &s : streams) {
0286         connect(s.stream, &ScreencastingStream::closed, this, &ScreenCastSession::streamClosed);
0287         connect(s.stream, &ScreencastingStream::failed, this, [this](const QString &error) {
0288             qCWarning(XdgDesktopPortalKdeScreenCast) << "ScreenCast session failed" << error;
0289             streamClosed();
0290         });
0291     }
0292     m_item->setStatus(KStatusNotifierItem::Active);
0293 }
0294 
0295 void ScreenCastSession::setDescription(const QString &description)
0296 {
0297     m_item->setToolTipSubTitle(description);
0298 }
0299 
0300 void ScreenCastSession::streamClosed()
0301 {
0302     ScreencastingStream *stream = qobject_cast<ScreencastingStream *>(sender());
0303     auto it = std::remove_if(m_streams.begin(), m_streams.end(), [stream](const WaylandIntegration::Stream &s) {
0304         return s.stream == stream;
0305     });
0306     m_streams.erase(it, m_streams.end());
0307 
0308     if (m_streams.isEmpty()) {
0309         close();
0310     }
0311 }
0312 
0313 GlobalShortcutsSession::GlobalShortcutsSession(QObject *parent, const QString &appId, const QString &path)
0314     : Session(parent, appId, path)
0315     , m_token(path.mid(path.lastIndexOf('/') + 1))
0316     , m_globalAccelInterface(
0317           new KGlobalAccelInterface(QStringLiteral("org.kde.kglobalaccel"), QStringLiteral("/kglobalaccel"), QDBusConnection::sessionBus(), this))
0318     , m_component(new KGlobalAccelComponentInterface(m_globalAccelInterface->service(),
0319                                                      m_globalAccelInterface->getComponent(componentName()).value().path(),
0320                                                      m_globalAccelInterface->connection(),
0321                                                      this))
0322 {
0323     qDBusRegisterMetaType<KGlobalShortcutInfo>();
0324     qDBusRegisterMetaType<QList<KGlobalShortcutInfo>>();
0325     qDBusRegisterMetaType<QKeySequence>();
0326     qDBusRegisterMetaType<QList<QKeySequence>>();
0327 
0328     connect(m_globalAccelInterface,
0329             &KGlobalAccelInterface::yourShortcutsChanged,
0330             this,
0331             [this](const QStringList &actionId, const QList<QKeySequence> &newKeys) {
0332                 Q_UNUSED(newKeys);
0333                 if (actionId[KGlobalAccel::ComponentUnique] == componentName()) {
0334                     m_shortcuts[actionId[KGlobalAccel::ActionUnique]]->setShortcuts(newKeys);
0335                     Q_EMIT shortcutsChanged();
0336                 }
0337             });
0338     connect(m_component,
0339             &KGlobalAccelComponentInterface::globalShortcutPressed,
0340             this,
0341             [this](const QString &componentUnique, const QString &actionUnique, qlonglong timestamp) {
0342                 if (componentUnique != componentName()) {
0343                     return;
0344                 }
0345 
0346                 Q_EMIT shortcutActivated(actionUnique, timestamp);
0347             });
0348     connect(m_component,
0349             &KGlobalAccelComponentInterface::globalShortcutReleased,
0350             this,
0351             [this](const QString &componentUnique, const QString &actionUnique, qlonglong timestamp) {
0352                 if (componentUnique != componentName()) {
0353                     return;
0354                 }
0355 
0356                 Q_EMIT shortcutDeactivated(actionUnique, timestamp);
0357             });
0358 }
0359 
0360 GlobalShortcutsSession::~GlobalShortcutsSession() = default;
0361 
0362 void GlobalShortcutsSession::restoreActions(const QVariant &shortcutsVariant)
0363 {
0364     auto arg = shortcutsVariant.value<QDBusArgument>();
0365 
0366 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0367     static const auto ShortcutsSignature = QDBusMetaType::typeToSignature(qMetaTypeId<Shortcuts>());
0368 #else
0369     static const auto ShortcutsSignature = QDBusMetaType::typeToSignature(QMetaType(qMetaTypeId<Shortcuts>()));
0370 #endif
0371     if (arg.currentSignature() != QLatin1String(ShortcutsSignature)) {
0372         qCWarning(XdgSessionKdeSession) << "Wrong global shortcuts type, should be " << ShortcutsSignature << "instead of " << arg.currentSignature();
0373         return;
0374     }
0375 
0376     const Shortcuts shortcuts = qdbus_cast<Shortcuts>(arg);
0377     const QList<KGlobalShortcutInfo> shortcutInfos = m_component->allShortcutInfos();
0378     QHash<QString, KGlobalShortcutInfo> shortcutInfosByName;
0379     shortcutInfosByName.reserve(shortcutInfos.size());
0380     for (const auto &shortcutInfo : shortcutInfos) {
0381         shortcutInfosByName[shortcutInfo.uniqueName()] = shortcutInfo;
0382     }
0383 
0384     for (const auto &shortcut : shortcuts) {
0385         const QString description = shortcut.second["description"].toString();
0386         if (description.isEmpty() || shortcut.first.isEmpty()) {
0387             qCWarning(XdgSessionKdeSession) << "Shortcut without name or description" << shortcut.first << "for" << componentName();
0388             continue;
0389         }
0390 
0391         QAction *&action = m_shortcuts[shortcut.first];
0392         if (!action) {
0393             action = new QAction(this);
0394         }
0395         action->setProperty("componentName", componentName());
0396         action->setProperty("componentDisplayName", componentName());
0397         action->setObjectName(shortcut.first);
0398         action->setText(description);
0399         const auto itShortcut = shortcutInfosByName.constFind(shortcut.first);
0400         if (itShortcut != shortcutInfosByName.constEnd()) {
0401             action->setShortcuts(itShortcut->keys());
0402         } else {
0403             const auto preferredShortcut = XdgShortcut::parse(shortcut.second["preferred_trigger"].toString());
0404             if (preferredShortcut) {
0405                 action->setShortcut(preferredShortcut.value());
0406             }
0407         }
0408         KGlobalAccel::self()->setGlobalShortcut(action, action->shortcuts());
0409 
0410         shortcutInfosByName.remove(shortcut.first);
0411     }
0412 
0413     // We can forget the shortcuts that aren't around anymore
0414     while (!shortcutInfosByName.isEmpty()) {
0415         const QString shortcutName = shortcutInfosByName.begin().key();
0416         auto action = m_shortcuts.take(shortcutName);
0417         KGlobalAccel::self()->removeAllShortcuts(action);
0418         shortcutInfosByName.erase(shortcutInfosByName.begin());
0419     }
0420 
0421     Q_ASSERT(m_shortcuts.size() == shortcuts.size());
0422 }
0423 
0424 QVariant GlobalShortcutsSession::shortcutDescriptionsVariant() const
0425 {
0426     QDBusArgument retVar;
0427     retVar << shortcutDescriptions();
0428     return QVariant::fromValue(retVar);
0429 }
0430 
0431 Shortcuts GlobalShortcutsSession::shortcutDescriptions() const
0432 {
0433     Shortcuts ret;
0434     for (auto it = m_shortcuts.cbegin(), itEnd = m_shortcuts.cend(); it != itEnd; ++it) {
0435         QStringList triggers;
0436         triggers.reserve((*it)->shortcuts().size());
0437         const auto shortcuts = (*it)->shortcuts();
0438         for (const auto &shortcut : shortcuts) {
0439             triggers += shortcut.toString(QKeySequence::NativeText);
0440         }
0441 
0442         ret.append({it.key(),
0443                     QVariantMap{
0444                         {QStringLiteral("description"), (*it)->text()},
0445                         {QStringLiteral("trigger_description"), triggers.join(i18n(", "))},
0446                     }});
0447     }
0448     Q_ASSERT(ret.size() == m_shortcuts.size());
0449     return ret;
0450 }