File indexing completed on 2024-04-28 05:36:53

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 void ScreenCastSession::setPersistMode(ScreenCastPortal::PersistMode persistMode)
0191 {
0192     m_persistMode = persistMode;
0193 }
0194 
0195 ScreenCastPortal::CursorModes ScreenCastSession::cursorMode() const
0196 {
0197     return m_cursorMode;
0198 }
0199 
0200 void ScreenCastSession::setOptions(const QVariantMap &options)
0201 {
0202     m_multipleSources = options.value(QStringLiteral("multiple")).toBool();
0203     m_cursorMode = ScreenCastPortal::CursorModes(options.value(QStringLiteral("cursor_mode")).toUInt());
0204     m_types = ScreenCastPortal::SourceType(options.value(QStringLiteral("types")).toUInt());
0205 
0206     if (m_types == 0) {
0207         m_types = ScreenCastPortal::Monitor;
0208     }
0209 }
0210 
0211 RemoteDesktopSession::RemoteDesktopSession(QObject *parent, const QString &appId, const QString &path)
0212     : ScreenCastSession(parent, appId, path, QStringLiteral("krfb"))
0213     , m_screenSharingEnabled(false)
0214 {
0215     connect(this, &RemoteDesktopSession::closed, this, [this] {
0216         if (m_acquired) {
0217             WaylandIntegration::acquireStreamingInput(false);
0218         }
0219     });
0220 }
0221 
0222 RemoteDesktopSession::~RemoteDesktopSession()
0223 {
0224 }
0225 
0226 void RemoteDesktopSession::setOptions(const QVariantMap &options)
0227 {
0228 }
0229 
0230 RemoteDesktopPortal::DeviceTypes RemoteDesktopSession::deviceTypes() const
0231 {
0232     return m_deviceTypes;
0233 }
0234 
0235 void RemoteDesktopSession::setDeviceTypes(RemoteDesktopPortal::DeviceTypes deviceTypes)
0236 {
0237     m_deviceTypes = deviceTypes;
0238 }
0239 
0240 bool RemoteDesktopSession::screenSharingEnabled() const
0241 {
0242     return m_screenSharingEnabled;
0243 }
0244 
0245 void RemoteDesktopSession::setScreenSharingEnabled(bool enabled)
0246 {
0247     if (m_screenSharingEnabled == enabled) {
0248         return;
0249     }
0250 
0251     m_screenSharingEnabled = enabled;
0252 }
0253 
0254 void RemoteDesktopSession::acquireStreamingInput()
0255 {
0256     WaylandIntegration::acquireStreamingInput(true);
0257     m_acquired = true;
0258 }
0259 
0260 void RemoteDesktopSession::refreshDescription()
0261 {
0262     m_item->setTitle(i18nc("SNI title that indicates there's a process remotely controlling the system", "Remote Desktop"));
0263     m_item->setToolTipTitle(m_item->title());
0264     setDescription(RemoteDesktopDialog::buildDescription(m_appId, deviceTypes(), screenSharingEnabled()));
0265 }
0266 
0267 void ScreenCastSession::setStreams(const WaylandIntegration::Streams &streams)
0268 {
0269     Q_ASSERT(!streams.isEmpty());
0270     m_streams = streams;
0271 
0272     m_item->setStandardActionsEnabled(false);
0273     if (qobject_cast<RemoteDesktopSession *>(this)) {
0274         refreshDescription();
0275     } else {
0276         const bool isWindow = m_streams[0].map[QLatin1String("source_type")] == ScreenCastPortal::Window;
0277         m_item->setToolTipSubTitle(i18ncp("%1 number of screens, %2 the app that receives them",
0278                                           "Sharing contents to %2",
0279                                           "%1 video streams to %2",
0280                                           m_streams.count(),
0281                                           Utils::applicationName(m_appId)));
0282         m_item->setTitle(i18nc("SNI title that indicates there's a process seeing our windows or screens", "Screen casting"));
0283         if (isWindow) {
0284             m_item->setOverlayIconByName(QStringLiteral("window"));
0285         } else {
0286             m_item->setOverlayIconByName(QStringLiteral("monitor"));
0287         }
0288     }
0289     m_item->setToolTipIconByName(m_item->overlayIconName());
0290     m_item->setToolTipTitle(m_item->title());
0291 
0292     for (const auto &s : streams) {
0293         connect(s.stream, &ScreencastingStream::closed, this, &ScreenCastSession::streamClosed);
0294         connect(s.stream, &ScreencastingStream::failed, this, [this](const QString &error) {
0295             qCWarning(XdgDesktopPortalKdeScreenCast) << "ScreenCast session failed" << error;
0296             streamClosed();
0297         });
0298     }
0299     m_item->setStatus(KStatusNotifierItem::Active);
0300 }
0301 
0302 void ScreenCastSession::setDescription(const QString &description)
0303 {
0304     m_item->setToolTipSubTitle(description);
0305 }
0306 
0307 void ScreenCastSession::streamClosed()
0308 {
0309     ScreencastingStream *stream = qobject_cast<ScreencastingStream *>(sender());
0310     auto it = std::remove_if(m_streams.begin(), m_streams.end(), [stream](const WaylandIntegration::Stream &s) {
0311         return s.stream == stream;
0312     });
0313     m_streams.erase(it, m_streams.end());
0314 
0315     if (m_streams.isEmpty()) {
0316         close();
0317     }
0318 }
0319 
0320 GlobalShortcutsSession::GlobalShortcutsSession(QObject *parent, const QString &appId, const QString &path)
0321     : Session(parent, appId, path)
0322     , m_token(path.mid(path.lastIndexOf('/') + 1))
0323     , m_globalAccelInterface(
0324           new KGlobalAccelInterface(QStringLiteral("org.kde.kglobalaccel"), QStringLiteral("/kglobalaccel"), QDBusConnection::sessionBus(), this))
0325     , m_component(new KGlobalAccelComponentInterface(m_globalAccelInterface->service(),
0326                                                      m_globalAccelInterface->getComponent(componentName()).value().path(),
0327                                                      m_globalAccelInterface->connection(),
0328                                                      this))
0329 {
0330     qDBusRegisterMetaType<KGlobalShortcutInfo>();
0331     qDBusRegisterMetaType<QList<KGlobalShortcutInfo>>();
0332     qDBusRegisterMetaType<QKeySequence>();
0333     qDBusRegisterMetaType<QList<QKeySequence>>();
0334 
0335     connect(m_globalAccelInterface,
0336             &KGlobalAccelInterface::yourShortcutsChanged,
0337             this,
0338             [this](const QStringList &actionId, const QList<QKeySequence> &newKeys) {
0339                 Q_UNUSED(newKeys);
0340                 if (actionId[KGlobalAccel::ComponentUnique] == componentName()) {
0341                     m_shortcuts[actionId[KGlobalAccel::ActionUnique]]->setShortcuts(newKeys);
0342                     Q_EMIT shortcutsChanged();
0343                 }
0344             });
0345     connect(m_component,
0346             &KGlobalAccelComponentInterface::globalShortcutPressed,
0347             this,
0348             [this](const QString &componentUnique, const QString &actionUnique, qlonglong timestamp) {
0349                 if (componentUnique != componentName()) {
0350                     return;
0351                 }
0352 
0353                 Q_EMIT shortcutActivated(actionUnique, timestamp);
0354             });
0355     connect(m_component,
0356             &KGlobalAccelComponentInterface::globalShortcutReleased,
0357             this,
0358             [this](const QString &componentUnique, const QString &actionUnique, qlonglong timestamp) {
0359                 if (componentUnique != componentName()) {
0360                     return;
0361                 }
0362 
0363                 Q_EMIT shortcutDeactivated(actionUnique, timestamp);
0364             });
0365 }
0366 
0367 GlobalShortcutsSession::~GlobalShortcutsSession() = default;
0368 
0369 void GlobalShortcutsSession::restoreActions(const QVariant &shortcutsVariant)
0370 {
0371     auto arg = shortcutsVariant.value<QDBusArgument>();
0372 
0373     static const auto ShortcutsSignature = QDBusMetaType::typeToSignature(QMetaType(qMetaTypeId<Shortcuts>()));
0374     if (arg.currentSignature() != QLatin1String(ShortcutsSignature)) {
0375         qCWarning(XdgSessionKdeSession) << "Wrong global shortcuts type, should be " << ShortcutsSignature << "instead of " << arg.currentSignature();
0376         return;
0377     }
0378 
0379     const Shortcuts shortcuts = qdbus_cast<Shortcuts>(arg);
0380     const QList<KGlobalShortcutInfo> shortcutInfos = m_component->allShortcutInfos();
0381     QHash<QString, KGlobalShortcutInfo> shortcutInfosByName;
0382     shortcutInfosByName.reserve(shortcutInfos.size());
0383     for (const auto &shortcutInfo : shortcutInfos) {
0384         shortcutInfosByName[shortcutInfo.uniqueName()] = shortcutInfo;
0385     }
0386 
0387     for (const auto &shortcut : shortcuts) {
0388         const QString description = shortcut.second["description"].toString();
0389         if (description.isEmpty() || shortcut.first.isEmpty()) {
0390             qCWarning(XdgSessionKdeSession) << "Shortcut without name or description" << shortcut.first << "for" << componentName();
0391             continue;
0392         }
0393 
0394         QAction *&action = m_shortcuts[shortcut.first];
0395         if (!action) {
0396             action = new QAction(this);
0397         }
0398         action->setProperty("componentName", componentName());
0399         action->setProperty("componentDisplayName", componentName());
0400         action->setObjectName(shortcut.first);
0401         action->setText(description);
0402         const auto itShortcut = shortcutInfosByName.constFind(shortcut.first);
0403         if (itShortcut != shortcutInfosByName.constEnd()) {
0404             action->setShortcuts(itShortcut->keys());
0405         } else {
0406             const auto preferredShortcut = XdgShortcut::parse(shortcut.second["preferred_trigger"].toString());
0407             if (preferredShortcut) {
0408                 action->setShortcut(preferredShortcut.value());
0409             }
0410         }
0411         KGlobalAccel::self()->setGlobalShortcut(action, action->shortcuts());
0412 
0413         shortcutInfosByName.remove(shortcut.first);
0414     }
0415 
0416     // We can forget the shortcuts that aren't around anymore
0417     while (!shortcutInfosByName.isEmpty()) {
0418         const QString shortcutName = shortcutInfosByName.begin().key();
0419         auto action = m_shortcuts.take(shortcutName);
0420         KGlobalAccel::self()->removeAllShortcuts(action);
0421         shortcutInfosByName.erase(shortcutInfosByName.begin());
0422     }
0423 
0424     Q_ASSERT(m_shortcuts.size() == shortcuts.size());
0425 }
0426 
0427 QVariant GlobalShortcutsSession::shortcutDescriptionsVariant() const
0428 {
0429     QDBusArgument retVar;
0430     retVar << shortcutDescriptions();
0431     return QVariant::fromValue(retVar);
0432 }
0433 
0434 Shortcuts GlobalShortcutsSession::shortcutDescriptions() const
0435 {
0436     Shortcuts ret;
0437     for (auto it = m_shortcuts.cbegin(), itEnd = m_shortcuts.cend(); it != itEnd; ++it) {
0438         QStringList triggers;
0439         triggers.reserve((*it)->shortcuts().size());
0440         const auto shortcuts = (*it)->shortcuts();
0441         for (const auto &shortcut : shortcuts) {
0442             triggers += shortcut.toString(QKeySequence::NativeText);
0443         }
0444 
0445         ret.append({it.key(),
0446                     QVariantMap{
0447                         {QStringLiteral("description"), (*it)->text()},
0448                         {QStringLiteral("trigger_description"), triggers.join(i18n(", "))},
0449                     }});
0450     }
0451     Q_ASSERT(ret.size() == m_shortcuts.size());
0452     return ret;
0453 }