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 <[email protected]>
5  SPDX-FileCopyrightText: 2017 Eike Hein <[email protected]>
6  SPDX-FileCopyrightText: 2022 Harald Sitter <[email protected]>
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 <KPluginMetaData>
18 #include <KSandbox>
19 #include <QFileInfo>
20 #include <QHash>
21 
22 #ifdef QT_DBUS_LIB
23 #include <QDBusConnection>
24 #include <QDBusConnectionInterface>
25 #endif
26 
27 #include "knotificationplugin.h"
28 #include "knotificationreplyaction.h"
29 #include "knotifyconfig.h"
30 
31 #include "notifybyexecute.h"
32 #include "notifybylogfile.h"
33 #include "notifybytaskbar.h"
34 
35 #if defined(Q_OS_ANDROID)
36 #include "notifybyandroid.h"
37 #elif defined(Q_OS_MACOS)
38 #include "notifybymacosnotificationcenter.h"
39 #elif defined(WITH_SNORETOAST)
40 #include "notifybysnore.h"
41 #else
42 #include "notifybypopup.h"
43 #include "notifybyportal.h"
44 #endif
45 #include "debug_p.h"
46 
47 #if defined(HAVE_CANBERRA)
48 #include "notifybyaudio_canberra.h"
49 #elif defined(HAVE_PHONON4QT5)
50 #include "notifybyaudio_phonon.h"
51 #endif
52 
53 #ifdef HAVE_SPEECH
54 #include "notifybytts.h"
55 #endif
56 
58 
59 struct Q_DECL_HIDDEN KNotificationManager::Private {
60  QHash<int, KNotification *> notifications;
62 
63  QStringList dirtyConfigCache;
64  bool portalDBusServiceExists = false;
65 };
66 
67 class KNotificationManagerSingleton
68 {
69 public:
70  KNotificationManager instance;
71 };
72 
73 Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self)
74 
75 KNotificationManager *KNotificationManager::self()
76 {
77  return &s_self()->instance;
78 }
79 
80 KNotificationManager::KNotificationManager()
81  : d(new Private)
82 {
83  qDeleteAll(d->notifyPlugins);
84  d->notifyPlugins.clear();
85 
86 #ifdef QT_DBUS_LIB
87  if (KSandbox::isInside()) {
89  d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop"));
90  }
91 
93  QStringLiteral("/Config"),
94  QStringLiteral("org.kde.knotification"),
95  QStringLiteral("reparseConfiguration"),
96  this,
98 #endif
99 }
100 
101 KNotificationManager::~KNotificationManager() = default;
102 
103 KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action)
104 {
105  KNotificationPlugin *plugin = d->notifyPlugins.value(action);
106 
107  // We already loaded a plugin for this action.
108  if (plugin) {
109  return plugin;
110  }
111 
112  auto addPlugin = [this](KNotificationPlugin *plugin) {
113  d->notifyPlugins[plugin->optionName()] = plugin;
114  connect(plugin, &KNotificationPlugin::finished, this, &KNotificationManager::notifyPluginFinished);
115  connect(plugin, &KNotificationPlugin::xdgActivationTokenReceived, this, &KNotificationManager::xdgActivationTokenReceived);
116  connect(plugin, &KNotificationPlugin::actionInvoked, this, &KNotificationManager::notificationActivated);
117  connect(plugin, &KNotificationPlugin::replied, this, &KNotificationManager::notificationReplied);
118  };
119 
120  // Load plugin.
121  // We have a series of built-ins up first, and fall back to trying
122  // to instantiate an externally supplied plugin.
123  if (action == QLatin1String("Popup")) {
124 #if defined(Q_OS_ANDROID)
125  plugin = new NotifyByAndroid(this);
126 #elif defined(WITH_SNORETOAST)
127  plugin = new NotifyBySnore(this);
128 #elif defined(Q_OS_MACOS)
129  plugin = new NotifyByMacOSNotificationCenter(this);
130 #else
131  if (d->portalDBusServiceExists) {
132  plugin = new NotifyByPortal(this);
133  } else {
134  plugin = new NotifyByPopup(this);
135  }
136 #endif
137  addPlugin(plugin);
138  } else if (action == QLatin1String("Taskbar")) {
139 #if !defined(Q_OS_ANDROID)
140  plugin = new NotifyByTaskbar(this);
141  addPlugin(plugin);
142 #endif
143  } else if (action == QLatin1String("Sound")) {
144 #if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA)
145  plugin = new NotifyByAudio(this);
146  addPlugin(plugin);
147 #endif
148  } else if (action == QLatin1String("Execute")) {
149 #if !defined(Q_OS_ANDROID)
150  plugin = new NotifyByExecute(this);
151  addPlugin(plugin);
152 #endif
153  } else if (action == QLatin1String("Logfile")) {
154  plugin = new NotifyByLogfile(this);
155  addPlugin(plugin);
156  } else if (action == QLatin1String("TTS")) {
157 #ifdef HAVE_SPEECH
158  plugin = new NotifyByTTS(this);
159  addPlugin(plugin);
160 #endif
161  } else {
162  bool pluginFound = false;
163 
164  std::function<bool(const KPluginMetaData &)> filter = [&action, &pluginFound](const KPluginMetaData &data) {
165  // KPluginMetaData::findPlugins loops over the plugins it
166  // finds and calls this function to determine whether to
167  // deliver them. We use a `pluginFound` var outside the
168  // lambda to break out of the loop once we got a match.
169  // The reason we can't just have KPluginMetaData::findPlugins,
170  // loop over the meta data and instantiate only one plugin
171  // is because the X-KDE-KNotification-OptionName field is
172  // optional (see TODO note below) and the matching plugin
173  // may be among the plugins which don't have it.
174  if (pluginFound) {
175  return false;
176  }
177 
178  const QJsonObject &rawData = data.rawData();
179 
180  // This field is new-ish and optional. If it's not set we always
181  // instantiate the plugin, unless we already got a match.
182  // TODO KF6: Require X-KDE-KNotification-OptionName be set and
183  // reject plugins without it.
184  if (rawData.contains(QLatin1String("X-KDE-KNotification-OptionName"))) {
185  if (rawData.value(QStringLiteral("X-KDE-KNotification-OptionName")) == action) {
186  pluginFound = true;
187  } else {
188  return false;
189  }
190  }
191 
192  return true;
193  };
194 
195  QPluginLoader loader;
196  const QVector<KPluginMetaData> listMetaData = KPluginMetaData::findPlugins(QStringLiteral("knotification/notifyplugins"), filter);
197  for (const KPluginMetaData &metadata : listMetaData) {
198  loader.setFileName(metadata.fileName());
199  QObject *pluginObj = loader.instance();
200  if (!pluginObj) {
201  qCWarning(LOG_KNOTIFICATIONS).nospace() << "Could not instantiate plugin \"" << metadata.fileName() << "\": " << loader.errorString();
202  continue;
203  }
204  KNotificationPlugin *notifyPlugin = qobject_cast<KNotificationPlugin *>(pluginObj);
205 
206  if (notifyPlugin) {
207  notifyPlugin->setParent(this);
208  // We try to avoid unnecessary instantiations (see above), but
209  // when they happen keep the resulting plugins around.
210  addPlugin(notifyPlugin);
211 
212  // Get ready to return the plugin we got asked for.
213  if (notifyPlugin->optionName() == action) {
214  plugin = notifyPlugin;
215  }
216  } else {
217  // Not our/valid plugin, so delete the created object.
218  pluginObj->deleteLater();
219  }
220  }
221  }
222 
223  return plugin;
224 }
225 
226 void KNotificationManager::notifyPluginFinished(KNotification *notification)
227 {
228  if (!notification || !d->notifications.contains(notification->id())) {
229  return;
230  }
231 
232  notification->deref();
233 }
234 
235 void KNotificationManager::notificationActivated(int id, int action)
236 {
237  if (d->notifications.contains(id)) {
238  qCDebug(LOG_KNOTIFICATIONS) << id << " " << action;
239  KNotification *n = d->notifications[id];
240  n->activate(action);
241 
242  // Resident actions delegate control over notification lifetime to the client
243  if (!n->hints().value(QStringLiteral("resident")).toBool()) {
244  close(id);
245  }
246  }
247 }
248 
249 void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token)
250 {
251  KNotification *n = d->notifications.value(id);
252  if (n) {
253  qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token;
254  n->d->xdgActivationToken = token;
255  Q_EMIT n->xdgActivationTokenChanged();
256  }
257 }
258 
259 void KNotificationManager::notificationReplied(int id, const QString &text)
260 {
261  if (KNotification *n = d->notifications.value(id)) {
262  if (auto *replyAction = n->replyAction()) {
263  // cannot really send out a "activate inline-reply" signal from plugin to manager
264  // so we instead assume empty reply is not supported and means normal invocation
265  if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
266  Q_EMIT replyAction->activated();
267  } else {
268  Q_EMIT replyAction->replied(text);
269  }
270  close(id);
271  }
272  }
273 }
274 
275 void KNotificationManager::notificationClosed()
276 {
277  KNotification *notification = qobject_cast<KNotification *>(sender());
278  if (!notification) {
279  return;
280  }
281  // We cannot do d->notifications.find(notification->id()); here because the
282  // notification->id() is -1 or -2 at this point, so we need to look for value
283  for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) {
284  if (iter.value() == notification) {
285  d->notifications.erase(iter);
286  break;
287  }
288  }
289 }
290 
291 void KNotificationManager::close(int id, bool force)
292 {
293  if (force || d->notifications.contains(id)) {
294  KNotification *n = d->notifications.value(id);
295  qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id;
296 
297  // Find plugins that are actually acting on this notification
298  // call close() only on those, otherwise each KNotificationPlugin::close()
299  // will call finish() which may close-and-delete the KNotification object
300  // before it finishes calling close on all the other plugins.
301  // For example: Action=Popup is a single actions but there is 5 loaded
302  // plugins, calling close() on the second would already close-and-delete
303  // the notification
304  KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId());
305  QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
306 
307  const auto listActions = notifyActions.split(QLatin1Char('|'));
308  for (const QString &action : listActions) {
309  if (!d->notifyPlugins.contains(action)) {
310  qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
311  continue;
312  }
313 
314  d->notifyPlugins[action]->close(n);
315  }
316  }
317 }
318 
319 void KNotificationManager::notify(KNotification *n)
320 {
321  KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId());
322 
323  if (d->dirtyConfigCache.contains(n->appName())) {
324  notifyConfig.reparseSingleConfiguration(n->appName());
325  d->dirtyConfigCache.removeOne(n->appName());
326  }
327 
328  const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
329 
330  if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) {
331  // this will cause KNotification closing itself fast
332  n->ref();
333  n->deref();
334  return;
335  }
336 
337  d->notifications.insert(n->id(), n);
338 
339  // TODO KF6 d-pointer KNotifyConfig and add this there
340  if (n->urgency() == KNotification::DefaultUrgency) {
341  const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency"));
342  if (urgency == QLatin1String("Low")) {
343  n->setUrgency(KNotification::LowUrgency);
344  } else if (urgency == QLatin1String("Normal")) {
345  n->setUrgency(KNotification::NormalUrgency);
346  } else if (urgency == QLatin1String("High")) {
347  n->setUrgency(KNotification::HighUrgency);
348  } else if (urgency == QLatin1String("Critical")) {
349  n->setUrgency(KNotification::CriticalUrgency);
350  }
351  }
352 
353  const auto actionsList = notifyActions.split(QLatin1Char('|'));
354 
355  // Make sure all plugins can ref the notification
356  // otherwise a plugin may finish and deref before everyone got a chance to ref
357  for (const QString &action : actionsList) {
358  KNotificationPlugin *notifyPlugin = pluginForAction(action);
359 
360  if (!notifyPlugin) {
361  qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
362  continue;
363  }
364 
365  n->ref();
366  }
367 
368  for (const QString &action : actionsList) {
369  KNotificationPlugin *notifyPlugin = pluginForAction(action);
370 
371  if (!notifyPlugin) {
372  qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
373  continue;
374  }
375 
376  qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName();
377  notifyPlugin->notify(n, &notifyConfig);
378  }
379 
380  connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed);
381 }
382 
383 void KNotificationManager::update(KNotification *n)
384 {
385  KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId());
386 
387  for (KNotificationPlugin *p : std::as_const(d->notifyPlugins)) {
388  p->update(n, &notifyConfig);
389  }
390 }
391 
392 void KNotificationManager::reemit(KNotification *n)
393 {
394  notify(n);
395 }
396 
397 void KNotificationManager::reparseConfiguration(const QString &app)
398 {
399  if (!d->dirtyConfigCache.contains(app)) {
400  d->dirtyConfigCache << app;
401  }
402 }
403 
404 #include "moc_knotificationmanager_p.cpp"
bool connect(const QString &service, const QString &path, const QString &interface, const QString &name, QObject *receiver, const char *slot)
KNotificationReplyAction * replyAction() const
void setFileName(const QString &fileName)
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QDBusReply< bool > isServiceRegistered(const QString &serviceName) const const
const QList< QKeySequence > & close()
void setUrgency(Urgency urgency)
Sets the urgency of the notification.
virtual QString optionName()=0
return the name of this plugin.
void reparseConfiguration(const QString &componentName)
bool contains(const QString &key) const const
Urgency urgency
Sets the urgency of the notification.
Definition: knotification.h:84
Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) class ControlPrivate
QDBusConnection sender()
void deleteLater()
QVariantMap hints
Definition: knotification.h:98
void finished(KNotification *notification)
the presentation is finished.
QDBusConnection sessionBus()
void deref()
Remove a reference made with ref().
bool isEmpty() const const
virtual void notify(KNotification *notification, KNotifyConfig *notifyConfig)=0
This function is called when the notification is sent.
void closed()
Emitted when the notification is closed.
QFuture< void > filter(Sequence &sequence, KeepFunctor filterFunction)
void activate(unsigned int action=0)
Activate the action specified action If the action is zero, then the default action is activated.
QJsonValue value(const QString &key) const const
QDBusConnectionInterface * interface() const const
static QVector< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter, KPluginMetaDataOption option)
QString appName() const
void setParent(QObject *parent)
KCOREADDONS_EXPORT bool isInside()
abstract class for KNotification actions
QObject * instance()
ContextList contexts() const
void ref()
The notification will automatically be closed if all presentations are finished.
void actionInvoked(int id, int action)
emit this signal if one action was invoked
@ UseRegularAction
Add the reply action as regular button.
void xdgActivationTokenChanged()
Emitted when xdgActivationToken changes.
QString eventId
Set the event id, if not already passed to the constructor.
Definition: knotification.h:39
QString errorString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 03:49:15 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.