KNotifications

knotificationmanager.cpp
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#else
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
46
47struct Q_DECL_HIDDEN KNotificationManager::Private {
48 QHash<int, KNotification *> notifications;
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(d->notifyPlugins);
72 d->notifyPlugins.clear();
73
74#ifdef HAVE_DBUS
75 if (isInsideSandbox()) {
77 d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop"));
78 }
79
81 QStringLiteral("/Config"),
82 QStringLiteral("org.kde.knotification"),
83 QStringLiteral("reparseConfiguration"),
84 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(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(plugin, &KNotificationPlugin::finished, this, &KNotificationManager::notifyPluginFinished);
103 connect(plugin, &KNotificationPlugin::xdgActivationTokenReceived, this, &KNotificationManager::xdgActivationTokenReceived);
104 connect(plugin, &KNotificationPlugin::actionInvoked, this, &KNotificationManager::notificationActivated);
105 connect(plugin, &KNotificationPlugin::replied, this, &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#else
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(notification->id())) {
139 return;
140 }
141
142 notification->deref();
143}
144
145void KNotificationManager::notificationActivated(int id, const QString &actionId)
146{
147 if (d->notifications.contains(id)) {
148 qCDebug(LOG_KNOTIFICATIONS) << id << " " << actionId;
149 KNotification *n = d->notifications[id];
150 n->activate(actionId);
151
152 // Resident actions delegate control over notification lifetime to the client
153 if (!n->hints().value(QStringLiteral("resident")).toBool()) {
154 close(id);
155 }
156 }
157}
158
159void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token)
160{
161 KNotification *n = d->notifications.value(id);
162 if (n) {
163 qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token;
164 n->d->xdgActivationToken = token;
165 Q_EMIT n->xdgActivationTokenChanged();
166 }
167}
168
169void KNotificationManager::notificationReplied(int id, const QString &text)
170{
171 if (KNotification *n = d->notifications.value(id)) {
172 if (auto *replyAction = n->replyAction()) {
173 // cannot really send out a "activate inline-reply" signal from plugin to manager
174 // so we instead assume empty reply is not supported and means normal invocation
175 if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
176 Q_EMIT replyAction->activated();
177 } else {
178 Q_EMIT replyAction->replied(text);
179 }
180 close(id);
181 }
182 }
183}
184
185void KNotificationManager::notificationClosed()
186{
187 KNotification *notification = qobject_cast<KNotification *>(sender());
188 if (!notification) {
189 return;
190 }
191 // We cannot do d->notifications.find(notification->id()); here because the
192 // notification->id() is -1 or -2 at this point, so we need to look for value
193 for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) {
194 if (iter.value() == notification) {
195 d->notifications.erase(iter);
196 break;
197 }
198 }
199}
200
201void KNotificationManager::close(int id)
202{
203 if (d->notifications.contains(id)) {
204 KNotification *n = d->notifications.value(id);
205 qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id;
206
207 // Find plugins that are actually acting on this notification
208 // call close() only on those, otherwise each KNotificationPlugin::close()
209 // will call finish() which may close-and-delete the KNotification object
210 // before it finishes calling close on all the other plugins.
211 // For example: Action=Popup is a single actions but there is 5 loaded
212 // plugins, calling close() on the second would already close-and-delete
213 // the notification
214 KNotifyConfig notifyConfig(n->appName(), n->eventId());
215 QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
216
217 const auto listActions = notifyActions.split(QLatin1Char('|'));
218 for (const QString &action : listActions) {
219 if (!d->notifyPlugins.contains(action)) {
220 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
221 continue;
222 }
223
224 d->notifyPlugins[action]->close(n);
225 }
226 }
227}
228
229void KNotificationManager::notify(KNotification *n)
230{
231 KNotifyConfig notifyConfig(n->appName(), n->eventId());
232
233 if (d->dirtyConfigCache.contains(n->appName())) {
234 notifyConfig.reparseSingleConfiguration(n->appName());
235 d->dirtyConfigCache.removeOne(n->appName());
236 }
237
238 if (!notifyConfig.isValid()) {
239 qCWarning(LOG_KNOTIFICATIONS) << "No event config could be found for event id" << n->eventId() << "under notifyrc file for app" << n->appName();
240 }
241
242 const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
243
244 if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) {
245 // this will cause KNotification closing itself fast
246 n->ref();
247 n->deref();
248 return;
249 }
250
251 d->notifications.insert(n->id(), n);
252
253 // TODO KF6 d-pointer KNotifyConfig and add this there
254 if (n->urgency() == KNotification::DefaultUrgency) {
255 const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency"));
256 if (urgency == QLatin1String("Low")) {
257 n->setUrgency(KNotification::LowUrgency);
258 } else if (urgency == QLatin1String("Normal")) {
259 n->setUrgency(KNotification::NormalUrgency);
260 } else if (urgency == QLatin1String("High")) {
261 n->setUrgency(KNotification::HighUrgency);
262 } else if (urgency == QLatin1String("Critical")) {
263 n->setUrgency(KNotification::CriticalUrgency);
264 }
265 }
266
267 const auto actionsList = notifyActions.split(QLatin1Char('|'));
268
269 // Make sure all plugins can ref the notification
270 // otherwise a plugin may finish and deref before everyone got a chance to ref
271 for (const QString &action : actionsList) {
272 KNotificationPlugin *notifyPlugin = pluginForAction(action);
273
274 if (!notifyPlugin) {
275 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
276 continue;
277 }
278
279 n->ref();
280 }
281
282 for (const QString &action : actionsList) {
283 KNotificationPlugin *notifyPlugin = pluginForAction(action);
284
285 if (!notifyPlugin) {
286 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
287 continue;
288 }
289
290 qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName();
291 notifyPlugin->notify(n, notifyConfig);
292 }
293
294 connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed);
295}
296
297void KNotificationManager::update(KNotification *n)
298{
299 KNotifyConfig notifyConfig(n->appName(), n->eventId());
300
301 for (KNotificationPlugin *p : std::as_const(d->notifyPlugins)) {
302 p->update(n, notifyConfig);
303 }
304}
305
306void KNotificationManager::reemit(KNotification *n)
307{
308 notify(n);
309}
310
311void KNotificationManager::reparseConfiguration(const QString &app)
312{
313 if (!d->dirtyConfigCache.contains(app)) {
314 d->dirtyConfigCache << app;
315 }
316}
317
318bool KNotificationManager::isInsideSandbox()
319{
320 // logic is taken from KSandbox::isInside()
321 static const bool isFlatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
322 static const bool isSnap = qEnvironmentVariableIsSet("SNAP");
323
324 return isFlatpak || isSnap;
325}
326
327#include "moc_knotificationmanager_p.cpp"
abstract class for KNotification actions
virtual QString optionName()=0
return the name of this plugin.
virtual void notify(KNotification *notification, const KNotifyConfig &notifyConfig)=0
This function is called when the notification is sent.
void actionInvoked(int id, const QString &action)
emit this signal if one action was invoked
void finished(KNotification *notification)
the presentation is finished.
@ UseRegularAction
Add the reply action as regular button.
KNotification is the main class for creating notifications.
KNotificationReplyAction * replyAction() const
void closed()
Emitted when the notification is closed.
QVariantMap hints
QString eventId
Set the event id, if not already passed to the constructor.
Urgency urgency
Sets the urgency of the notification.
QString appName() const
void xdgActivationTokenChanged()
Emitted when xdgActivationToken changes.
void setUrgency(Urgency urgency)
Sets the urgency of the notification.
Represent the configuration for an event.
KCOREADDONS_EXPORT bool isFlatpak()
KCOREADDONS_EXPORT bool isSnap()
KGuiItem close()
bool connect(const QString &service, const QString &path, const QString &interface, const QString &name, QObject *receiver, const char *slot)
QDBusConnectionInterface * interface() const const
QDBusConnection sessionBus()
QDBusReply< bool > isServiceRegistered(const QString &serviceName) const const
bool exists() const const
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jun 14 2024 11:50:24 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.