File indexing completed on 2024-04-28 15:29:09

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2005 Olivier Goffart <ogoffart at kde.org>
0004     SPDX-FileCopyrightText: 2013-2015 Martin Klapetek <mklapetek@kde.org>
0005     SPDX-FileCopyrightText: 2017 Eike Hein <hein@kde.org>
0006     SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-only
0009 */
0010 
0011 #include "knotification.h"
0012 #include "knotification_p.h"
0013 #include "knotificationmanager_p.h"
0014 
0015 #include <config-knotifications.h>
0016 
0017 #include <KPluginMetaData>
0018 #include <KSandbox>
0019 #include <QFileInfo>
0020 #include <QHash>
0021 
0022 #ifdef QT_DBUS_LIB
0023 #include <QDBusConnection>
0024 #include <QDBusConnectionInterface>
0025 #endif
0026 
0027 #include "knotificationplugin.h"
0028 #include "knotificationreplyaction.h"
0029 #include "knotifyconfig.h"
0030 
0031 #include "notifybyexecute.h"
0032 #include "notifybylogfile.h"
0033 #include "notifybytaskbar.h"
0034 
0035 #if defined(Q_OS_ANDROID)
0036 #include "notifybyandroid.h"
0037 #elif defined(Q_OS_MACOS)
0038 #include "notifybymacosnotificationcenter.h"
0039 #elif defined(WITH_SNORETOAST)
0040 #include "notifybysnore.h"
0041 #else
0042 #include "notifybypopup.h"
0043 #include "notifybyportal.h"
0044 #endif
0045 #include "debug_p.h"
0046 
0047 #if defined(HAVE_CANBERRA)
0048 #include "notifybyaudio_canberra.h"
0049 #elif defined(HAVE_PHONON4QT5)
0050 #include "notifybyaudio_phonon.h"
0051 #endif
0052 
0053 #ifdef HAVE_SPEECH
0054 #include "notifybytts.h"
0055 #endif
0056 
0057 typedef QHash<QString, QString> Dict;
0058 
0059 struct Q_DECL_HIDDEN KNotificationManager::Private {
0060     QHash<int, KNotification *> notifications;
0061     QHash<QString, KNotificationPlugin *> notifyPlugins;
0062 
0063     QStringList dirtyConfigCache;
0064     bool portalDBusServiceExists = false;
0065 };
0066 
0067 class KNotificationManagerSingleton
0068 {
0069 public:
0070     KNotificationManager instance;
0071 };
0072 
0073 Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self)
0074 
0075 KNotificationManager *KNotificationManager::self()
0076 {
0077     return &s_self()->instance;
0078 }
0079 
0080 KNotificationManager::KNotificationManager()
0081     : d(new Private)
0082 {
0083     qDeleteAll(d->notifyPlugins);
0084     d->notifyPlugins.clear();
0085 
0086 #ifdef QT_DBUS_LIB
0087     if (KSandbox::isInside()) {
0088         QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
0089         d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop"));
0090     }
0091 
0092     QDBusConnection::sessionBus().connect(QString(),
0093                                           QStringLiteral("/Config"),
0094                                           QStringLiteral("org.kde.knotification"),
0095                                           QStringLiteral("reparseConfiguration"),
0096                                           this,
0097                                           SLOT(reparseConfiguration(QString)));
0098 #endif
0099 }
0100 
0101 KNotificationManager::~KNotificationManager() = default;
0102 
0103 KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action)
0104 {
0105     KNotificationPlugin *plugin = d->notifyPlugins.value(action);
0106 
0107     // We already loaded a plugin for this action.
0108     if (plugin) {
0109         return plugin;
0110     }
0111 
0112     auto addPlugin = [this](KNotificationPlugin *plugin) {
0113         d->notifyPlugins[plugin->optionName()] = plugin;
0114         connect(plugin, &KNotificationPlugin::finished, this, &KNotificationManager::notifyPluginFinished);
0115         connect(plugin, &KNotificationPlugin::xdgActivationTokenReceived, this, &KNotificationManager::xdgActivationTokenReceived);
0116         connect(plugin, &KNotificationPlugin::actionInvoked, this, &KNotificationManager::notificationActivated);
0117         connect(plugin, &KNotificationPlugin::replied, this, &KNotificationManager::notificationReplied);
0118     };
0119 
0120     // Load plugin.
0121     // We have a series of built-ins up first, and fall back to trying
0122     // to instantiate an externally supplied plugin.
0123     if (action == QLatin1String("Popup")) {
0124 #if defined(Q_OS_ANDROID)
0125         plugin = new NotifyByAndroid(this);
0126 #elif defined(WITH_SNORETOAST)
0127         plugin = new NotifyBySnore(this);
0128 #elif defined(Q_OS_MACOS)
0129         plugin = new NotifyByMacOSNotificationCenter(this);
0130 #else
0131         if (d->portalDBusServiceExists) {
0132             plugin = new NotifyByPortal(this);
0133         } else {
0134             plugin = new NotifyByPopup(this);
0135         }
0136 #endif
0137         addPlugin(plugin);
0138     } else if (action == QLatin1String("Taskbar")) {
0139 #if !defined(Q_OS_ANDROID)
0140         plugin = new NotifyByTaskbar(this);
0141         addPlugin(plugin);
0142 #endif
0143     } else if (action == QLatin1String("Sound")) {
0144 #if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA)
0145         plugin = new NotifyByAudio(this);
0146         addPlugin(plugin);
0147 #endif
0148     } else if (action == QLatin1String("Execute")) {
0149 #if !defined(Q_OS_ANDROID)
0150         plugin = new NotifyByExecute(this);
0151         addPlugin(plugin);
0152 #endif
0153     } else if (action == QLatin1String("Logfile")) {
0154         plugin = new NotifyByLogfile(this);
0155         addPlugin(plugin);
0156     } else if (action == QLatin1String("TTS")) {
0157 #ifdef HAVE_SPEECH
0158         plugin = new NotifyByTTS(this);
0159         addPlugin(plugin);
0160 #endif
0161     } else {
0162         bool pluginFound = false;
0163 
0164         std::function<bool(const KPluginMetaData &)> filter = [&action, &pluginFound](const KPluginMetaData &data) {
0165             // KPluginMetaData::findPlugins loops over the plugins it
0166             // finds and calls this function to determine whether to
0167             // deliver them. We use a `pluginFound` var outside the
0168             // lambda to break out of the loop once we got a match.
0169             // The reason we can't just have KPluginMetaData::findPlugins,
0170             // loop over the meta data and instantiate only one plugin
0171             // is because the X-KDE-KNotification-OptionName field is
0172             // optional (see TODO note below) and the matching plugin
0173             // may be among the plugins which don't have it.
0174             if (pluginFound) {
0175                 return false;
0176             }
0177 
0178             const QJsonObject &rawData = data.rawData();
0179 
0180             // This field is new-ish and optional. If it's not set we always
0181             // instantiate the plugin, unless we already got a match.
0182             // TODO KF6: Require X-KDE-KNotification-OptionName be set and
0183             // reject plugins without it.
0184             if (rawData.contains(QLatin1String("X-KDE-KNotification-OptionName"))) {
0185                 if (rawData.value(QStringLiteral("X-KDE-KNotification-OptionName")) == action) {
0186                     pluginFound = true;
0187                 } else {
0188                     return false;
0189                 }
0190             }
0191 
0192             return true;
0193         };
0194 
0195         QPluginLoader loader;
0196         const QVector<KPluginMetaData> listMetaData = KPluginMetaData::findPlugins(QStringLiteral("knotification/notifyplugins"), filter);
0197         for (const KPluginMetaData &metadata : listMetaData) {
0198             loader.setFileName(metadata.fileName());
0199             QObject *pluginObj = loader.instance();
0200             if (!pluginObj) {
0201                 qCWarning(LOG_KNOTIFICATIONS).nospace() << "Could not instantiate plugin \"" << metadata.fileName() << "\": " << loader.errorString();
0202                 continue;
0203             }
0204             KNotificationPlugin *notifyPlugin = qobject_cast<KNotificationPlugin *>(pluginObj);
0205 
0206             if (notifyPlugin) {
0207                 notifyPlugin->setParent(this);
0208                 // We try to avoid unnecessary instantiations (see above), but
0209                 // when they happen keep the resulting plugins around.
0210                 addPlugin(notifyPlugin);
0211 
0212                 // Get ready to return the plugin we got asked for.
0213                 if (notifyPlugin->optionName() == action) {
0214                     plugin = notifyPlugin;
0215                 }
0216             } else {
0217                 // Not our/valid plugin, so delete the created object.
0218                 pluginObj->deleteLater();
0219             }
0220         }
0221     }
0222 
0223     return plugin;
0224 }
0225 
0226 void KNotificationManager::notifyPluginFinished(KNotification *notification)
0227 {
0228     if (!notification || !d->notifications.contains(notification->id())) {
0229         return;
0230     }
0231 
0232     notification->deref();
0233 }
0234 
0235 void KNotificationManager::notificationActivated(int id, int action)
0236 {
0237     if (d->notifications.contains(id)) {
0238         qCDebug(LOG_KNOTIFICATIONS) << id << " " << action;
0239         KNotification *n = d->notifications[id];
0240         n->activate(action);
0241 
0242         // Resident actions delegate control over notification lifetime to the client
0243         if (!n->hints().value(QStringLiteral("resident")).toBool()) {
0244             close(id);
0245         }
0246     }
0247 }
0248 
0249 void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token)
0250 {
0251     KNotification *n = d->notifications.value(id);
0252     if (n) {
0253         qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token;
0254         n->d->xdgActivationToken = token;
0255         Q_EMIT n->xdgActivationTokenChanged();
0256     }
0257 }
0258 
0259 void KNotificationManager::notificationReplied(int id, const QString &text)
0260 {
0261     if (KNotification *n = d->notifications.value(id)) {
0262         if (auto *replyAction = n->replyAction()) {
0263             // cannot really send out a "activate inline-reply" signal from plugin to manager
0264             // so we instead assume empty reply is not supported and means normal invocation
0265             if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
0266                 Q_EMIT replyAction->activated();
0267             } else {
0268                 Q_EMIT replyAction->replied(text);
0269             }
0270             close(id);
0271         }
0272     }
0273 }
0274 
0275 void KNotificationManager::notificationClosed()
0276 {
0277     KNotification *notification = qobject_cast<KNotification *>(sender());
0278     if (!notification) {
0279         return;
0280     }
0281     // We cannot do d->notifications.find(notification->id()); here because the
0282     // notification->id() is -1 or -2 at this point, so we need to look for value
0283     for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) {
0284         if (iter.value() == notification) {
0285             d->notifications.erase(iter);
0286             break;
0287         }
0288     }
0289 }
0290 
0291 void KNotificationManager::close(int id, bool force)
0292 {
0293     if (force || d->notifications.contains(id)) {
0294         KNotification *n = d->notifications.value(id);
0295         qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id;
0296 
0297         // Find plugins that are actually acting on this notification
0298         // call close() only on those, otherwise each KNotificationPlugin::close()
0299         // will call finish() which may close-and-delete the KNotification object
0300         // before it finishes calling close on all the other plugins.
0301         // For example: Action=Popup is a single actions but there is 5 loaded
0302         // plugins, calling close() on the second would already close-and-delete
0303         // the notification
0304         KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId());
0305         QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
0306 
0307         const auto listActions = notifyActions.split(QLatin1Char('|'));
0308         for (const QString &action : listActions) {
0309             if (!d->notifyPlugins.contains(action)) {
0310                 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
0311                 continue;
0312             }
0313 
0314             d->notifyPlugins[action]->close(n);
0315         }
0316     }
0317 }
0318 
0319 void KNotificationManager::notify(KNotification *n)
0320 {
0321     KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId());
0322 
0323     if (d->dirtyConfigCache.contains(n->appName())) {
0324         notifyConfig.reparseSingleConfiguration(n->appName());
0325         d->dirtyConfigCache.removeOne(n->appName());
0326     }
0327 
0328     const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
0329 
0330     if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) {
0331         // this will cause KNotification closing itself fast
0332         n->ref();
0333         n->deref();
0334         return;
0335     }
0336 
0337     d->notifications.insert(n->id(), n);
0338 
0339     // TODO KF6 d-pointer KNotifyConfig and add this there
0340     if (n->urgency() == KNotification::DefaultUrgency) {
0341         const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency"));
0342         if (urgency == QLatin1String("Low")) {
0343             n->setUrgency(KNotification::LowUrgency);
0344         } else if (urgency == QLatin1String("Normal")) {
0345             n->setUrgency(KNotification::NormalUrgency);
0346         } else if (urgency == QLatin1String("High")) {
0347             n->setUrgency(KNotification::HighUrgency);
0348         } else if (urgency == QLatin1String("Critical")) {
0349             n->setUrgency(KNotification::CriticalUrgency);
0350         }
0351     }
0352 
0353     const auto actionsList = notifyActions.split(QLatin1Char('|'));
0354 
0355     // Make sure all plugins can ref the notification
0356     // otherwise a plugin may finish and deref before everyone got a chance to ref
0357     for (const QString &action : actionsList) {
0358         KNotificationPlugin *notifyPlugin = pluginForAction(action);
0359 
0360         if (!notifyPlugin) {
0361             qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
0362             continue;
0363         }
0364 
0365         n->ref();
0366     }
0367 
0368     for (const QString &action : actionsList) {
0369         KNotificationPlugin *notifyPlugin = pluginForAction(action);
0370 
0371         if (!notifyPlugin) {
0372             qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
0373             continue;
0374         }
0375 
0376         qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName();
0377         notifyPlugin->notify(n, &notifyConfig);
0378     }
0379 
0380     connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed);
0381 }
0382 
0383 void KNotificationManager::update(KNotification *n)
0384 {
0385     KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId());
0386 
0387     for (KNotificationPlugin *p : std::as_const(d->notifyPlugins)) {
0388         p->update(n, &notifyConfig);
0389     }
0390 }
0391 
0392 void KNotificationManager::reemit(KNotification *n)
0393 {
0394     notify(n);
0395 }
0396 
0397 void KNotificationManager::reparseConfiguration(const QString &app)
0398 {
0399     if (!d->dirtyConfigCache.contains(app)) {
0400         d->dirtyConfigCache << app;
0401     }
0402 }
0403 
0404 #include "moc_knotificationmanager_p.cpp"