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 }