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