1/*
2 SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
3 SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
4 SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
5 SPDX-FileCopyrightText: 2016 Jan Grulich <jgrulich@redhat.com>
6
7 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
8*/
9
10#include "notifybyportal.h"
11
12#include "debug_p.h"
13#include "knotification.h"
14#include "knotifyconfig.h"
15
16#include <QBuffer>
17#include <QDBusConnection>
18#include <QDBusConnectionInterface>
19#include <QDBusError>
20#include <QDBusMessage>
21#include <QDBusMetaType>
22#include <QDBusServiceWatcher>
23#include <QGuiApplication>
24#include <QHash>
25#include <QIcon>
26#include <QMap>
27#include <QPointer>
28
29#include <KConfigGroup>
30static const char portalDbusServiceName[] = "org.freedesktop.portal.Desktop";
31static const char portalDbusInterfaceName[] = "org.freedesktop.portal.Notification";
32static const char portalDbusPath[] = "/org/freedesktop/portal/desktop";
33
34class NotifyByPortalPrivate
35{
36public:
37 struct PortalIcon {
38 QString str;
39 QDBusVariant data;
40 };
41
42 NotifyByPortalPrivate(NotifyByPortal *parent)
43 : dbusServiceExists(false)
44 , q(parent)
45 {
46 }
47
48 /**
49 * Sends notification to DBus "org.freedesktop.notifications" interface.
50 * @param id knotify-sid identifier of notification
51 * @param config notification data
52 * @param update If true, will request the DBus service to update
53 the notification with new data from \c notification
54 * Otherwise will put new notification on screen
55 * @return true for success or false if there was an error.
56 */
57 bool sendNotificationToPortal(KNotification *notification, const KNotifyConfig &config);
58
59 /**
60 * Sends request to close Notification with id to DBus "org.freedesktop.notifications" interface
61 * @param id knotify-side notification ID to close
62 */
63
64 void closePortalNotification(KNotification *notification);
65 /**
66 * Find the caption and the icon name of the application
67 */
68
69 void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName);
70
71 /**
72 * Specifies if DBus Notifications interface exists on session bus
73 */
74 bool dbusServiceExists;
75
76 /*
77 * As we communicate with the notification server over dbus
78 * we use only ids, this is for fast KNotifications lookup
79 */
80 QHash<uint, QPointer<KNotification>> portalNotifications;
81
82 /*
83 * Holds the id that will be assigned to the next notification source
84 * that will be created
85 */
86 uint nextId;
87
88 NotifyByPortal *const q;
89};
90
91QDBusArgument &operator<<(QDBusArgument &argument, const NotifyByPortalPrivate::PortalIcon &icon)
92{
93 argument.beginStructure();
94 argument << icon.str << icon.data;
95 argument.endStructure();
96 return argument;
97}
98
99const QDBusArgument &operator>>(const QDBusArgument &argument, NotifyByPortalPrivate::PortalIcon &icon)
100{
101 argument.beginStructure();
102 argument >> icon.str >> icon.data;
103 argument.endStructure();
104 return argument;
105}
106
107Q_DECLARE_METATYPE(NotifyByPortalPrivate::PortalIcon)
108
109//---------------------------------------------------------------------------------------
110
111NotifyByPortal::NotifyByPortal(QObject *parent)
112 : KNotificationPlugin(parent)
113 , d(new NotifyByPortalPrivate(this))
114{
115 // check if service already exists on plugin instantiation
116 QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
117 d->dbusServiceExists = interface && interface->isServiceRegistered(serviceName: QString::fromLatin1(ba: portalDbusServiceName));
118
119 if (d->dbusServiceExists) {
120 onServiceOwnerChanged(QString::fromLatin1(ba: portalDbusServiceName), QString(), QStringLiteral("_")); // connect signals
121 }
122
123 // to catch register/unregister events from service in runtime
124 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this);
125 watcher->setConnection(QDBusConnection::sessionBus());
126 watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
127 watcher->addWatchedService(newService: QString::fromLatin1(ba: portalDbusServiceName));
128 connect(sender: watcher, signal: &QDBusServiceWatcher::serviceOwnerChanged, context: this, slot: &NotifyByPortal::onServiceOwnerChanged);
129}
130
131NotifyByPortal::~NotifyByPortal() = default;
132
133void NotifyByPortal::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
134{
135 if (d->portalNotifications.contains(key: notification->id())) {
136 // notification is already on the screen, do nothing
137 finish(notification);
138 return;
139 }
140
141 // check if Notifications DBus service exists on bus, use it if it does
142 if (d->dbusServiceExists) {
143 if (!d->sendNotificationToPortal(notification, config: notifyConfig)) {
144 finish(notification); // an error occurred.
145 }
146 }
147}
148
149void NotifyByPortal::close(KNotification *notification)
150{
151 if (d->dbusServiceExists) {
152 d->closePortalNotification(notification);
153 }
154}
155
156void NotifyByPortal::update(KNotification *notification, const KNotifyConfig &notifyConfig)
157{
158 // TODO not supported by portals
159 Q_UNUSED(notification);
160 Q_UNUSED(notifyConfig);
161}
162
163void NotifyByPortal::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
164{
165 Q_UNUSED(serviceName);
166 // close all notifications we currently hold reference to
167 for (KNotification *n : std::as_const(t&: d->portalNotifications)) {
168 if (n) {
169 Q_EMIT finished(notification: n);
170 }
171 }
172
173 d->portalNotifications.clear();
174
175 if (newOwner.isEmpty()) {
176 d->dbusServiceExists = false;
177 } else if (oldOwner.isEmpty()) {
178 d->dbusServiceExists = true;
179 d->nextId = 1;
180
181 // connect to action invocation signals
182 bool connected = QDBusConnection::sessionBus().connect(service: QString(), // from any service
183 path: QString::fromLatin1(ba: portalDbusPath),
184 interface: QString::fromLatin1(ba: portalDbusInterfaceName),
185 QStringLiteral("ActionInvoked"),
186 receiver: this,
187 SLOT(onPortalNotificationActionInvoked(QString, QString, QVariantList)));
188 if (!connected) {
189 qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to ActionInvoked dbus signal";
190 }
191 }
192}
193
194void NotifyByPortal::onPortalNotificationActionInvoked(const QString &id, const QString &action, const QVariantList &parameter)
195{
196 Q_UNUSED(parameter);
197
198 auto iter = d->portalNotifications.find(key: id.toUInt());
199 if (iter == d->portalNotifications.end()) {
200 return;
201 }
202
203 KNotification *n = *iter;
204 if (n) {
205 Q_EMIT actionInvoked(id: n->id(), action);
206 } else {
207 d->portalNotifications.erase(it: iter);
208 }
209}
210
211void NotifyByPortalPrivate::getAppCaptionAndIconName(const KNotifyConfig &notifyConfig, QString *appCaption, QString *iconName)
212{
213 *appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Name"));
214 if (appCaption->isEmpty()) {
215 *appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Comment"));
216 }
217 if (appCaption->isEmpty()) {
218 *appCaption = notifyConfig.applicationName();
219 }
220
221 *iconName = notifyConfig.readEntry(QStringLiteral("IconName"));
222 if (iconName->isEmpty()) {
223 *iconName = notifyConfig.readGlobalEntry(QStringLiteral("IconName"));
224 }
225 if (iconName->isEmpty()) {
226 *iconName = qGuiApp->windowIcon().name();
227 }
228 if (iconName->isEmpty()) {
229 *iconName = notifyConfig.applicationName();
230 }
231}
232
233bool NotifyByPortalPrivate::sendNotificationToPortal(KNotification *notification, const KNotifyConfig &notifyConfig_nocheck)
234{
235 QDBusMessage dbusNotificationMessage;
236 dbusNotificationMessage = QDBusMessage::createMethodCall(destination: QString::fromLatin1(ba: portalDbusServiceName),
237 path: QString::fromLatin1(ba: portalDbusPath),
238 interface: QString::fromLatin1(ba: portalDbusInterfaceName),
239 QStringLiteral("AddNotification"));
240
241 QVariantList args;
242 // Will be used only with xdg-desktop-portal
243 QVariantMap portalArgs;
244
245 QString appCaption;
246 QString iconName;
247 getAppCaptionAndIconName(notifyConfig: notifyConfig_nocheck, appCaption: &appCaption, iconName: &iconName);
248
249 // did the user override the icon name?
250 if (!notification->iconName().isEmpty()) {
251 iconName = notification->iconName();
252 }
253
254 QString title = notification->title().isEmpty() ? appCaption : notification->title();
255 QString text = notification->text();
256
257 if (notification->defaultAction()) {
258 portalArgs.insert(QStringLiteral("default-action"), QStringLiteral("default"));
259 portalArgs.insert(QStringLiteral("default-action-target"), QStringLiteral("0"));
260 }
261
262 QString priority;
263 switch (notification->urgency()) {
264 case KNotification::DefaultUrgency:
265 break;
266 case KNotification::LowUrgency:
267 priority = QStringLiteral("low");
268 break;
269 case KNotification::NormalUrgency:
270 priority = QStringLiteral("normal");
271 break;
272 case KNotification::HighUrgency:
273 priority = QStringLiteral("high");
274 break;
275 case KNotification::CriticalUrgency:
276 priority = QStringLiteral("urgent");
277 break;
278 }
279
280 if (!priority.isEmpty()) {
281 portalArgs.insert(QStringLiteral("priority"), value: priority);
282 }
283
284 // freedesktop.org spec defines action list to be list like
285 // (act_id1, action1, act_id2, action2, ...)
286 //
287 // assign id's to actions like it's done in fillPopup() method
288 // (i.e. starting from 1)
289 QList<QVariantMap> buttons;
290 buttons.reserve(asize: notification->actions().count());
291
292 const auto listActions = notification->actions();
293 for (KNotificationAction *action : listActions) {
294 QVariantMap button = {{QStringLiteral("action"), action->id()}, //
295 {QStringLiteral("label"), action->label()}};
296 buttons << button;
297 }
298
299 qDBusRegisterMetaType<QList<QVariantMap>>();
300 qDBusRegisterMetaType<PortalIcon>();
301
302 if (!notification->pixmap().isNull()) {
303 QByteArray pixmapData;
304 QBuffer buffer(&pixmapData);
305 buffer.open(openMode: QIODevice::WriteOnly);
306 notification->pixmap().save(device: &buffer, format: "PNG");
307 buffer.close();
308
309 PortalIcon icon;
310 icon.str = QStringLiteral("bytes");
311 icon.data.setVariant(pixmapData);
312 portalArgs.insert(QStringLiteral("icon"), value: QVariant::fromValue<PortalIcon>(value: icon));
313 } else {
314 // Use this for now for backwards compatibility, we can as well set the variant to be (sv) where the
315 // string is keyword "themed" and the variant is an array of strings with icon names
316 portalArgs.insert(QStringLiteral("icon"), value: iconName);
317 }
318
319 portalArgs.insert(QStringLiteral("title"), value: title);
320 portalArgs.insert(QStringLiteral("body"), value: text);
321 portalArgs.insert(QStringLiteral("buttons"), value: QVariant::fromValue<QList<QVariantMap>>(value: buttons));
322
323 args.append(t: QString::number(nextId));
324 args.append(t: portalArgs);
325
326 dbusNotificationMessage.setArguments(args);
327
328 QDBusPendingCall notificationCall = QDBusConnection::sessionBus().asyncCall(message: dbusNotificationMessage, timeout: -1);
329
330 // If we are in sandbox we don't need to wait for returned notification id
331 portalNotifications.insert(key: nextId++, value: notification);
332
333 return true;
334}
335
336void NotifyByPortalPrivate::closePortalNotification(KNotification *notification)
337{
338 uint id = portalNotifications.key(value: notification, defaultKey: 0);
339
340 qCDebug(LOG_KNOTIFICATIONS) << "ID: " << id;
341
342 if (id == 0) {
343 qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id();
344 return;
345 }
346
347 QDBusMessage m = QDBusMessage::createMethodCall(destination: QString::fromLatin1(ba: portalDbusServiceName),
348 path: QString::fromLatin1(ba: portalDbusPath),
349 interface: QString::fromLatin1(ba: portalDbusInterfaceName),
350 QStringLiteral("RemoveNotification"));
351 m.setArguments({QString::number(id)});
352
353 // send(..) does not block
354 bool queued = QDBusConnection::sessionBus().send(message: m);
355
356 if (!queued) {
357 qCWarning(LOG_KNOTIFICATIONS) << "Failed to queue dbus message for closing a notification";
358 }
359}
360
361#include "moc_notifybyportal.cpp"
362

source code of knotifications/src/notifybyportal.cpp