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 }