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

source code of knotifications/src/notifybyportal.cpp