File indexing completed on 2024-04-28 11:43:48
0001 /* 0002 SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org> 0003 SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com> 0004 SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org> 0005 SPDX-FileCopyrightText: 2016 Jan Grulich <jgrulich@redhat.com> 0006 0007 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0008 */ 0009 0010 #include "notifybyportal.h" 0011 0012 #include "debug_p.h" 0013 #include "knotification.h" 0014 #include "knotifyconfig.h" 0015 0016 #include <QBuffer> 0017 #include <QDBusConnection> 0018 #include <QDBusConnectionInterface> 0019 #include <QDBusError> 0020 #include <QDBusMessage> 0021 #include <QDBusMetaType> 0022 #include <QDBusServiceWatcher> 0023 #include <QMap> 0024 0025 #include <KConfigGroup> 0026 static const char portalDbusServiceName[] = "org.freedesktop.portal.Desktop"; 0027 static const char portalDbusInterfaceName[] = "org.freedesktop.portal.Notification"; 0028 static const char portalDbusPath[] = "/org/freedesktop/portal/desktop"; 0029 0030 class NotifyByPortalPrivate 0031 { 0032 public: 0033 struct PortalIcon { 0034 QString str; 0035 QDBusVariant data; 0036 }; 0037 0038 NotifyByPortalPrivate(NotifyByPortal *parent) 0039 : dbusServiceExists(false) 0040 , q(parent) 0041 { 0042 } 0043 0044 /** 0045 * Sends notification to DBus "org.freedesktop.notifications" interface. 0046 * @param id knotify-sid identifier of notification 0047 * @param config notification data 0048 * @param update If true, will request the DBus service to update 0049 the notification with new data from \c notification 0050 * Otherwise will put new notification on screen 0051 * @return true for success or false if there was an error. 0052 */ 0053 bool sendNotificationToPortal(KNotification *notification, const KNotifyConfig &config); 0054 0055 /** 0056 * Sends request to close Notification with id to DBus "org.freedesktop.notifications" interface 0057 * @param id knotify-side notification ID to close 0058 */ 0059 0060 void closePortalNotification(KNotification *notification); 0061 /** 0062 * Find the caption and the icon name of the application 0063 */ 0064 0065 void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName); 0066 0067 /** 0068 * Specifies if DBus Notifications interface exists on session bus 0069 */ 0070 bool dbusServiceExists; 0071 0072 /* 0073 * As we communicate with the notification server over dbus 0074 * we use only ids, this is for fast KNotifications lookup 0075 */ 0076 QHash<uint, QPointer<KNotification>> portalNotifications; 0077 0078 /* 0079 * Holds the id that will be assigned to the next notification source 0080 * that will be created 0081 */ 0082 uint nextId; 0083 0084 NotifyByPortal *const q; 0085 }; 0086 0087 QDBusArgument &operator<<(QDBusArgument &argument, const NotifyByPortalPrivate::PortalIcon &icon) 0088 { 0089 argument.beginStructure(); 0090 argument << icon.str << icon.data; 0091 argument.endStructure(); 0092 return argument; 0093 } 0094 0095 const QDBusArgument &operator>>(const QDBusArgument &argument, NotifyByPortalPrivate::PortalIcon &icon) 0096 { 0097 argument.beginStructure(); 0098 argument >> icon.str >> icon.data; 0099 argument.endStructure(); 0100 return argument; 0101 } 0102 0103 Q_DECLARE_METATYPE(NotifyByPortalPrivate::PortalIcon) 0104 0105 //--------------------------------------------------------------------------------------- 0106 0107 NotifyByPortal::NotifyByPortal(QObject *parent) 0108 : KNotificationPlugin(parent) 0109 , d(new NotifyByPortalPrivate(this)) 0110 { 0111 // check if service already exists on plugin instantiation 0112 QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); 0113 d->dbusServiceExists = interface && interface->isServiceRegistered(QString::fromLatin1(portalDbusServiceName)); 0114 0115 if (d->dbusServiceExists) { 0116 onServiceOwnerChanged(QString::fromLatin1(portalDbusServiceName), QString(), QStringLiteral("_")); // connect signals 0117 } 0118 0119 // to catch register/unregister events from service in runtime 0120 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this); 0121 watcher->setConnection(QDBusConnection::sessionBus()); 0122 watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); 0123 watcher->addWatchedService(QString::fromLatin1(portalDbusServiceName)); 0124 connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &NotifyByPortal::onServiceOwnerChanged); 0125 } 0126 0127 NotifyByPortal::~NotifyByPortal() = default; 0128 0129 void NotifyByPortal::notify(KNotification *notification, KNotifyConfig *notifyConfig) 0130 { 0131 notify(notification, *notifyConfig); 0132 } 0133 0134 void NotifyByPortal::notify(KNotification *notification, const KNotifyConfig ¬ifyConfig) 0135 { 0136 if (d->portalNotifications.contains(notification->id())) { 0137 // notification is already on the screen, do nothing 0138 finish(notification); 0139 return; 0140 } 0141 0142 // check if Notifications DBus service exists on bus, use it if it does 0143 if (d->dbusServiceExists) { 0144 if (!d->sendNotificationToPortal(notification, notifyConfig)) { 0145 finish(notification); // an error occurred. 0146 } 0147 } 0148 } 0149 0150 void NotifyByPortal::close(KNotification *notification) 0151 { 0152 if (d->dbusServiceExists) { 0153 d->closePortalNotification(notification); 0154 } 0155 } 0156 0157 void NotifyByPortal::update(KNotification *notification, KNotifyConfig *notifyConfig) 0158 { 0159 // TODO not supported by portals 0160 Q_UNUSED(notification); 0161 Q_UNUSED(notifyConfig); 0162 } 0163 0164 void NotifyByPortal::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) 0165 { 0166 Q_UNUSED(serviceName); 0167 // close all notifications we currently hold reference to 0168 for (KNotification *n : std::as_const(d->portalNotifications)) { 0169 if (n) { 0170 Q_EMIT finished(n); 0171 } 0172 } 0173 0174 d->portalNotifications.clear(); 0175 0176 if (newOwner.isEmpty()) { 0177 d->dbusServiceExists = false; 0178 } else if (oldOwner.isEmpty()) { 0179 d->dbusServiceExists = true; 0180 d->nextId = 1; 0181 0182 // connect to action invocation signals 0183 bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service 0184 QString::fromLatin1(portalDbusPath), 0185 QString::fromLatin1(portalDbusInterfaceName), 0186 QStringLiteral("ActionInvoked"), 0187 this, 0188 SLOT(onPortalNotificationActionInvoked(QString, QString, QVariantList))); 0189 if (!connected) { 0190 qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to ActionInvoked dbus signal"; 0191 } 0192 } 0193 } 0194 0195 void NotifyByPortal::onPortalNotificationActionInvoked(const QString &id, const QString &action, const QVariantList ¶meter) 0196 { 0197 Q_UNUSED(parameter); 0198 0199 auto iter = d->portalNotifications.find(id.toUInt()); 0200 if (iter == d->portalNotifications.end()) { 0201 return; 0202 } 0203 0204 KNotification *n = *iter; 0205 if (n) { 0206 Q_EMIT actionInvoked(n->id(), action.toUInt()); 0207 } else { 0208 d->portalNotifications.erase(iter); 0209 } 0210 } 0211 0212 void NotifyByPortalPrivate::getAppCaptionAndIconName(const KNotifyConfig ¬ifyConfig, QString *appCaption, QString *iconName) 0213 { 0214 KConfigGroup globalgroup(&(*notifyConfig.eventsfile), QStringLiteral("Global")); 0215 *appCaption = globalgroup.readEntry("Name", globalgroup.readEntry("Comment", notifyConfig.appname)); 0216 0217 KConfigGroup eventGroup(&(*notifyConfig.eventsfile), QStringLiteral("Event/%1").arg(notifyConfig.eventid)); 0218 if (eventGroup.hasKey("IconName")) { 0219 *iconName = eventGroup.readEntry("IconName", notifyConfig.appname); 0220 } else { 0221 *iconName = globalgroup.readEntry("IconName", notifyConfig.appname); 0222 } 0223 } 0224 0225 bool NotifyByPortalPrivate::sendNotificationToPortal(KNotification *notification, const KNotifyConfig ¬ifyConfig_nocheck) 0226 { 0227 QDBusMessage dbusNotificationMessage; 0228 dbusNotificationMessage = QDBusMessage::createMethodCall(QString::fromLatin1(portalDbusServiceName), 0229 QString::fromLatin1(portalDbusPath), 0230 QString::fromLatin1(portalDbusInterfaceName), 0231 QStringLiteral("AddNotification")); 0232 0233 QVariantList args; 0234 // Will be used only with xdg-desktop-portal 0235 QVariantMap portalArgs; 0236 0237 QString appCaption; 0238 QString iconName; 0239 getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName); 0240 0241 // did the user override the icon name? 0242 if (!notification->iconName().isEmpty()) { 0243 iconName = notification->iconName(); 0244 } 0245 0246 QString title = notification->title().isEmpty() ? appCaption : notification->title(); 0247 QString text = notification->text(); 0248 0249 if (!notification->defaultAction().isEmpty()) { 0250 portalArgs.insert(QStringLiteral("default-action"), notification->defaultAction()); 0251 portalArgs.insert(QStringLiteral("default-action-target"), QStringLiteral("0")); 0252 } 0253 0254 QString priority; 0255 switch (notification->urgency()) { 0256 case KNotification::DefaultUrgency: 0257 break; 0258 case KNotification::LowUrgency: 0259 priority = QStringLiteral("low"); 0260 break; 0261 case KNotification::NormalUrgency: 0262 priority = QStringLiteral("normal"); 0263 break; 0264 case KNotification::HighUrgency: 0265 priority = QStringLiteral("high"); 0266 break; 0267 case KNotification::CriticalUrgency: 0268 priority = QStringLiteral("urgent"); 0269 break; 0270 } 0271 0272 if (!priority.isEmpty()) { 0273 portalArgs.insert(QStringLiteral("priority"), priority); 0274 } 0275 0276 // freedesktop.org spec defines action list to be list like 0277 // (act_id1, action1, act_id2, action2, ...) 0278 // 0279 // assign id's to actions like it's done in fillPopup() method 0280 // (i.e. starting from 1) 0281 QList<QVariantMap> buttons; 0282 buttons.reserve(notification->actions().count()); 0283 0284 int actId = 0; 0285 const auto listActions = notification->actions(); 0286 for (const QString &actionName : listActions) { 0287 actId++; 0288 QVariantMap button = {{QStringLiteral("action"), QString::number(actId)}, // 0289 {QStringLiteral("label"), actionName}}; 0290 buttons << button; 0291 } 0292 0293 qDBusRegisterMetaType<QList<QVariantMap>>(); 0294 qDBusRegisterMetaType<PortalIcon>(); 0295 0296 if (!notification->pixmap().isNull()) { 0297 QByteArray pixmapData; 0298 QBuffer buffer(&pixmapData); 0299 buffer.open(QIODevice::WriteOnly); 0300 notification->pixmap().save(&buffer, "PNG"); 0301 buffer.close(); 0302 0303 PortalIcon icon; 0304 icon.str = QStringLiteral("bytes"); 0305 icon.data.setVariant(pixmapData); 0306 portalArgs.insert(QStringLiteral("icon"), QVariant::fromValue<PortalIcon>(icon)); 0307 } else { 0308 // Use this for now for backwards compatibility, we can as well set the variant to be (sv) where the 0309 // string is keyword "themed" and the variant is an array of strings with icon names 0310 portalArgs.insert(QStringLiteral("icon"), iconName); 0311 } 0312 0313 portalArgs.insert(QStringLiteral("title"), title); 0314 portalArgs.insert(QStringLiteral("body"), text); 0315 portalArgs.insert(QStringLiteral("buttons"), QVariant::fromValue<QList<QVariantMap>>(buttons)); 0316 0317 args.append(QString::number(nextId)); 0318 args.append(portalArgs); 0319 0320 dbusNotificationMessage.setArguments(args); 0321 0322 QDBusPendingCall notificationCall = QDBusConnection::sessionBus().asyncCall(dbusNotificationMessage, -1); 0323 0324 // If we are in sandbox we don't need to wait for returned notification id 0325 portalNotifications.insert(nextId++, notification); 0326 0327 return true; 0328 } 0329 0330 void NotifyByPortalPrivate::closePortalNotification(KNotification *notification) 0331 { 0332 uint id = portalNotifications.key(notification, 0); 0333 0334 qCDebug(LOG_KNOTIFICATIONS) << "ID: " << id; 0335 0336 if (id == 0) { 0337 qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id(); 0338 return; 0339 } 0340 0341 QDBusMessage m = QDBusMessage::createMethodCall(QString::fromLatin1(portalDbusServiceName), 0342 QString::fromLatin1(portalDbusPath), 0343 QString::fromLatin1(portalDbusInterfaceName), 0344 QStringLiteral("RemoveNotification")); 0345 m.setArguments({QString::number(id)}); 0346 0347 // send(..) does not block 0348 bool queued = QDBusConnection::sessionBus().send(m); 0349 0350 if (!queued) { 0351 qCWarning(LOG_KNOTIFICATIONS) << "Failed to queue dbus message for closing a notification"; 0352 } 0353 } 0354 0355 #include "moc_notifybyportal.cpp"