1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2005 Olivier Goffart <ogoffart at kde.org>
4 SPDX-FileCopyrightText: 2013-2015 Martin Klapetek <mklapetek@kde.org>
5 SPDX-FileCopyrightText: 2017 Eike Hein <hein@kde.org>
6 SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.0-only
9*/
10
11#include "knotification.h"
12#include "knotification_p.h"
13#include "knotificationmanager_p.h"
14
15#include <config-knotifications.h>
16
17#include <QFileInfo>
18#include <QHash>
19
20#ifdef HAVE_DBUS
21#include <QDBusConnection>
22#include <QDBusConnectionInterface>
23#endif
24
25#include "knotificationplugin.h"
26#include "knotificationreplyaction.h"
27#include "knotifyconfig.h"
28
29#if defined(Q_OS_ANDROID)
30#include "notifybyandroid.h"
31#elif defined(Q_OS_MACOS)
32#include "notifybymacosnotificationcenter.h"
33#elif defined(WITH_SNORETOAST)
34#include "notifybysnore.h"
35#elif defined(HAVE_DBUS)
36#include "notifybypopup.h"
37#include "notifybyportal.h"
38#endif
39#include "debug_p.h"
40
41#if defined(HAVE_CANBERRA)
42#include "notifybyaudio.h"
43#endif
44
45typedef QHash<QString, QString> Dict;
46
47struct Q_DECL_HIDDEN KNotificationManager::Private {
48 QHash<int, KNotification *> notifications;
49 QHash<QString, KNotificationPlugin *> notifyPlugins;
50
51 QStringList dirtyConfigCache;
52 bool portalDBusServiceExists = false;
53};
54
55class KNotificationManagerSingleton
56{
57public:
58 KNotificationManager instance;
59};
60
61Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self)
62
63KNotificationManager *KNotificationManager::self()
64{
65 return &s_self()->instance;
66}
67
68KNotificationManager::KNotificationManager()
69 : d(new Private)
70{
71 qDeleteAll(c: d->notifyPlugins);
72 d->notifyPlugins.clear();
73
74#ifdef HAVE_DBUS
75 if (isInsideSandbox()) {
76 QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
77 d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop"));
78 }
79
80 QDBusConnection::sessionBus().connect(service: QString(),
81 QStringLiteral("/Config"),
82 QStringLiteral("org.kde.knotification"),
83 QStringLiteral("reparseConfiguration"),
84 receiver: this,
85 SLOT(reparseConfiguration(QString)));
86#endif
87}
88
89KNotificationManager::~KNotificationManager() = default;
90
91KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action)
92{
93 KNotificationPlugin *plugin = d->notifyPlugins.value(key: action);
94
95 // We already loaded a plugin for this action.
96 if (plugin) {
97 return plugin;
98 }
99
100 auto addPlugin = [this](KNotificationPlugin *plugin) {
101 d->notifyPlugins[plugin->optionName()] = plugin;
102 connect(sender: plugin, signal: &KNotificationPlugin::finished, context: this, slot: &KNotificationManager::notifyPluginFinished);
103 connect(sender: plugin, signal: &KNotificationPlugin::xdgActivationTokenReceived, context: this, slot: &KNotificationManager::xdgActivationTokenReceived);
104 connect(sender: plugin, signal: &KNotificationPlugin::actionInvoked, context: this, slot: &KNotificationManager::notificationActivated);
105 connect(sender: plugin, signal: &KNotificationPlugin::replied, context: this, slot: &KNotificationManager::notificationReplied);
106 };
107
108 // Load plugin.
109 // We have a series of built-ins up first, and fall back to trying
110 // to instantiate an externally supplied plugin.
111 if (action == QLatin1String("Popup")) {
112#if defined(Q_OS_ANDROID)
113 plugin = new NotifyByAndroid(this);
114#elif defined(WITH_SNORETOAST)
115 plugin = new NotifyBySnore(this);
116#elif defined(Q_OS_MACOS)
117 plugin = new NotifyByMacOSNotificationCenter(this);
118#elif defined(HAVE_DBUS)
119 if (d->portalDBusServiceExists) {
120 plugin = new NotifyByPortal(this);
121 } else {
122 plugin = new NotifyByPopup(this);
123 }
124#endif
125 addPlugin(plugin);
126 } else if (action == QLatin1String("Sound")) {
127#if defined(HAVE_CANBERRA)
128 plugin = new NotifyByAudio(this);
129 addPlugin(plugin);
130#endif
131 }
132
133 return plugin;
134}
135
136void KNotificationManager::notifyPluginFinished(KNotification *notification)
137{
138 if (!notification || !d->notifications.contains(key: notification->id())) {
139 return;
140 }
141
142 notification->deref();
143}
144
145void KNotificationManager::notificationActivated(int id, const QString &actionId)
146{
147 if (KNotification *n = d->notifications.value(key: id)) {
148 qCDebug(LOG_KNOTIFICATIONS) << id << " " << actionId;
149 n->activate(action: actionId);
150 }
151
152 // we must look up again, as n->activate goes into application code where anything can happen
153 if (KNotification *n = d->notifications.value(key: id)) {
154 // Resident actions delegate control over notification lifetime to the client
155 if (!n->hints().value(QStringLiteral("resident")).toBool()) {
156 close(id);
157 }
158 }
159}
160
161void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token)
162{
163 KNotification *n = d->notifications.value(key: id);
164 if (n) {
165 qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token;
166 n->d->xdgActivationToken = token;
167 Q_EMIT n->xdgActivationTokenChanged();
168 }
169}
170
171void KNotificationManager::notificationReplied(int id, const QString &text)
172{
173 if (KNotification *n = d->notifications.value(key: id)) {
174 if (auto *replyAction = n->replyAction()) {
175 // cannot really send out a "activate inline-reply" signal from plugin to manager
176 // so we instead assume empty reply is not supported and means normal invocation
177 if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
178 Q_EMIT replyAction->activated();
179 } else {
180 Q_EMIT replyAction->replied(text);
181 }
182 close(id);
183 }
184 }
185}
186
187void KNotificationManager::notificationClosed()
188{
189 KNotification *notification = qobject_cast<KNotification *>(object: sender());
190 if (!notification) {
191 return;
192 }
193 // We cannot do d->notifications.find(notification->id()); here because the
194 // notification->id() is -1 or -2 at this point, so we need to look for value
195 for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) {
196 if (iter.value() == notification) {
197 d->notifications.erase(it: iter);
198 break;
199 }
200 }
201}
202
203void KNotificationManager::close(int id)
204{
205 if (d->notifications.contains(key: id)) {
206 KNotification *n = d->notifications.value(key: id);
207 qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id;
208
209 // Find plugins that are actually acting on this notification
210 // call close() only on those, otherwise each KNotificationPlugin::close()
211 // will call finish() which may close-and-delete the KNotification object
212 // before it finishes calling close on all the other plugins.
213 // For example: Action=Popup is a single actions but there is 5 loaded
214 // plugins, calling close() on the second would already close-and-delete
215 // the notification
216 KNotifyConfig notifyConfig(n->appName(), n->eventId());
217 QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
218
219 const auto listActions = notifyActions.split(sep: QLatin1Char('|'));
220 for (const QString &action : listActions) {
221 if (!d->notifyPlugins.contains(key: action)) {
222 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
223 continue;
224 }
225
226 d->notifyPlugins[action]->close(notification: n);
227 }
228 }
229}
230
231void KNotificationManager::notify(KNotification *n)
232{
233 KNotifyConfig notifyConfig(n->appName(), n->eventId());
234
235 if (d->dirtyConfigCache.contains(str: n->appName())) {
236 notifyConfig.reparseSingleConfiguration(app: n->appName());
237 d->dirtyConfigCache.removeOne(t: n->appName());
238 }
239
240 if (!notifyConfig.isValid()) {
241 qCWarning(LOG_KNOTIFICATIONS) << "No event config could be found for event id" << n->eventId() << "under notifyrc file for app" << n->appName();
242 }
243
244 const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
245
246 if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) {
247 // this will cause KNotification closing itself fast
248 n->ref();
249 n->deref();
250 return;
251 }
252
253 d->notifications.insert(key: n->id(), value: n);
254
255 // TODO KF6 d-pointer KNotifyConfig and add this there
256 if (n->urgency() == KNotification::DefaultUrgency) {
257 const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency"));
258 if (urgency == QLatin1String("Low")) {
259 n->setUrgency(KNotification::LowUrgency);
260 } else if (urgency == QLatin1String("Normal")) {
261 n->setUrgency(KNotification::NormalUrgency);
262 } else if (urgency == QLatin1String("High")) {
263 n->setUrgency(KNotification::HighUrgency);
264 } else if (urgency == QLatin1String("Critical")) {
265 n->setUrgency(KNotification::CriticalUrgency);
266 }
267 }
268
269 const auto actionsList = notifyActions.split(sep: QLatin1Char('|'));
270
271 // Make sure all plugins can ref the notification
272 // otherwise a plugin may finish and deref before everyone got a chance to ref
273 for (const QString &action : actionsList) {
274 KNotificationPlugin *notifyPlugin = pluginForAction(action);
275
276 if (!notifyPlugin) {
277 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
278 continue;
279 }
280
281 n->ref();
282 }
283
284 for (const QString &action : actionsList) {
285 KNotificationPlugin *notifyPlugin = pluginForAction(action);
286
287 if (!notifyPlugin) {
288 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
289 continue;
290 }
291
292 qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName();
293 notifyPlugin->notify(notification: n, notifyConfig);
294 }
295
296 connect(sender: n, signal: &KNotification::closed, context: this, slot: &KNotificationManager::notificationClosed);
297}
298
299void KNotificationManager::update(KNotification *n)
300{
301 KNotifyConfig notifyConfig(n->appName(), n->eventId());
302
303 for (KNotificationPlugin *p : std::as_const(t&: d->notifyPlugins)) {
304 p->update(notification: n, notifyConfig);
305 }
306}
307
308void KNotificationManager::reemit(KNotification *n)
309{
310 notify(n);
311}
312
313void KNotificationManager::reparseConfiguration(const QString &app)
314{
315 if (!d->dirtyConfigCache.contains(str: app)) {
316 d->dirtyConfigCache << app;
317 }
318}
319
320bool KNotificationManager::isInsideSandbox()
321{
322 // logic is taken from KSandbox::isInside()
323 static const bool isFlatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
324 static const bool isSnap = qEnvironmentVariableIsSet(varName: "SNAP");
325
326 return isFlatpak || isSnap;
327}
328
329#include "moc_knotificationmanager_p.cpp"
330

source code of knotifications/src/knotificationmanager.cpp