File indexing completed on 2024-04-28 07:45:18

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 <QFileInfo>
0018 #include <QHash>
0019 
0020 #ifdef QT_DBUS_LIB
0021 #include <QDBusConnection>
0022 #include <QDBusConnectionInterface>
0023 #endif
0024 
0025 #include "knotificationplugin.h"
0026 #include "knotificationreplyaction.h"
0027 #include "knotifyconfig.h"
0028 
0029 #if defined(Q_OS_ANDROID)
0030 #include "notifybyandroid.h"
0031 #elif defined(Q_OS_MACOS)
0032 #include "notifybymacosnotificationcenter.h"
0033 #elif defined(WITH_SNORETOAST)
0034 #include "notifybysnore.h"
0035 #else
0036 #include "notifybypopup.h"
0037 #include "notifybyportal.h"
0038 #endif
0039 #include "debug_p.h"
0040 
0041 #if defined(HAVE_CANBERRA)
0042 #include "notifybyaudio.h"
0043 #endif
0044 
0045 typedef QHash<QString, QString> Dict;
0046 
0047 struct Q_DECL_HIDDEN KNotificationManager::Private {
0048     QHash<int, KNotification *> notifications;
0049     QHash<QString, KNotificationPlugin *> notifyPlugins;
0050 
0051     QStringList dirtyConfigCache;
0052     bool portalDBusServiceExists = false;
0053 };
0054 
0055 class KNotificationManagerSingleton
0056 {
0057 public:
0058     KNotificationManager instance;
0059 };
0060 
0061 Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self)
0062 
0063 KNotificationManager *KNotificationManager::self()
0064 {
0065     return &s_self()->instance;
0066 }
0067 
0068 KNotificationManager::KNotificationManager()
0069     : d(new Private)
0070 {
0071     qDeleteAll(d->notifyPlugins);
0072     d->notifyPlugins.clear();
0073 
0074 #ifdef QT_DBUS_LIB
0075     if (isInsideSandbox()) {
0076         QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
0077         d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop"));
0078     }
0079 
0080     QDBusConnection::sessionBus().connect(QString(),
0081                                           QStringLiteral("/Config"),
0082                                           QStringLiteral("org.kde.knotification"),
0083                                           QStringLiteral("reparseConfiguration"),
0084                                           this,
0085                                           SLOT(reparseConfiguration(QString)));
0086 #endif
0087 }
0088 
0089 KNotificationManager::~KNotificationManager() = default;
0090 
0091 KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action)
0092 {
0093     KNotificationPlugin *plugin = d->notifyPlugins.value(action);
0094 
0095     // We already loaded a plugin for this action.
0096     if (plugin) {
0097         return plugin;
0098     }
0099 
0100     auto addPlugin = [this](KNotificationPlugin *plugin) {
0101         d->notifyPlugins[plugin->optionName()] = plugin;
0102         connect(plugin, &KNotificationPlugin::finished, this, &KNotificationManager::notifyPluginFinished);
0103         connect(plugin, &KNotificationPlugin::xdgActivationTokenReceived, this, &KNotificationManager::xdgActivationTokenReceived);
0104         connect(plugin, &KNotificationPlugin::actionInvoked, this, &KNotificationManager::notificationActivated);
0105         connect(plugin, &KNotificationPlugin::replied, this, &KNotificationManager::notificationReplied);
0106     };
0107 
0108     // Load plugin.
0109     // We have a series of built-ins up first, and fall back to trying
0110     // to instantiate an externally supplied plugin.
0111     if (action == QLatin1String("Popup")) {
0112 #if defined(Q_OS_ANDROID)
0113         plugin = new NotifyByAndroid(this);
0114 #elif defined(WITH_SNORETOAST)
0115         plugin = new NotifyBySnore(this);
0116 #elif defined(Q_OS_MACOS)
0117         plugin = new NotifyByMacOSNotificationCenter(this);
0118 #else
0119         if (d->portalDBusServiceExists) {
0120             plugin = new NotifyByPortal(this);
0121         } else {
0122             plugin = new NotifyByPopup(this);
0123         }
0124 #endif
0125         addPlugin(plugin);
0126     } else if (action == QLatin1String("Sound")) {
0127 #if defined(HAVE_CANBERRA)
0128         plugin = new NotifyByAudio(this);
0129         addPlugin(plugin);
0130 #endif
0131     }
0132 
0133     return plugin;
0134 }
0135 
0136 void KNotificationManager::notifyPluginFinished(KNotification *notification)
0137 {
0138     if (!notification || !d->notifications.contains(notification->id())) {
0139         return;
0140     }
0141 
0142     notification->deref();
0143 }
0144 
0145 void KNotificationManager::notificationActivated(int id, const QString &actionId)
0146 {
0147     if (d->notifications.contains(id)) {
0148         qCDebug(LOG_KNOTIFICATIONS) << id << " " << actionId;
0149         KNotification *n = d->notifications[id];
0150         n->activate(actionId);
0151 
0152         // Resident actions delegate control over notification lifetime to the client
0153         if (!n->hints().value(QStringLiteral("resident")).toBool()) {
0154             close(id);
0155         }
0156     }
0157 }
0158 
0159 void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token)
0160 {
0161     KNotification *n = d->notifications.value(id);
0162     if (n) {
0163         qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token;
0164         n->d->xdgActivationToken = token;
0165         Q_EMIT n->xdgActivationTokenChanged();
0166     }
0167 }
0168 
0169 void KNotificationManager::notificationReplied(int id, const QString &text)
0170 {
0171     if (KNotification *n = d->notifications.value(id)) {
0172         if (auto *replyAction = n->replyAction()) {
0173             // cannot really send out a "activate inline-reply" signal from plugin to manager
0174             // so we instead assume empty reply is not supported and means normal invocation
0175             if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
0176                 Q_EMIT replyAction->activated();
0177             } else {
0178                 Q_EMIT replyAction->replied(text);
0179             }
0180             close(id);
0181         }
0182     }
0183 }
0184 
0185 void KNotificationManager::notificationClosed()
0186 {
0187     KNotification *notification = qobject_cast<KNotification *>(sender());
0188     if (!notification) {
0189         return;
0190     }
0191     // We cannot do d->notifications.find(notification->id()); here because the
0192     // notification->id() is -1 or -2 at this point, so we need to look for value
0193     for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) {
0194         if (iter.value() == notification) {
0195             d->notifications.erase(iter);
0196             break;
0197         }
0198     }
0199 }
0200 
0201 void KNotificationManager::close(int id)
0202 {
0203     if (d->notifications.contains(id)) {
0204         KNotification *n = d->notifications.value(id);
0205         qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id;
0206 
0207         // Find plugins that are actually acting on this notification
0208         // call close() only on those, otherwise each KNotificationPlugin::close()
0209         // will call finish() which may close-and-delete the KNotification object
0210         // before it finishes calling close on all the other plugins.
0211         // For example: Action=Popup is a single actions but there is 5 loaded
0212         // plugins, calling close() on the second would already close-and-delete
0213         // the notification
0214         KNotifyConfig notifyConfig(n->appName(), n->eventId());
0215         QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
0216 
0217         const auto listActions = notifyActions.split(QLatin1Char('|'));
0218         for (const QString &action : listActions) {
0219             if (!d->notifyPlugins.contains(action)) {
0220                 qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
0221                 continue;
0222             }
0223 
0224             d->notifyPlugins[action]->close(n);
0225         }
0226     }
0227 }
0228 
0229 void KNotificationManager::notify(KNotification *n)
0230 {
0231     KNotifyConfig notifyConfig(n->appName(), n->eventId());
0232 
0233     if (d->dirtyConfigCache.contains(n->appName())) {
0234         notifyConfig.reparseSingleConfiguration(n->appName());
0235         d->dirtyConfigCache.removeOne(n->appName());
0236     }
0237 
0238     if (!notifyConfig.isValid()) {
0239         qCWarning(LOG_KNOTIFICATIONS) << "No event config could be found for event id" << n->eventId() << "under notifyrc file for app" << n->appName();
0240     }
0241 
0242     const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
0243 
0244     if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) {
0245         // this will cause KNotification closing itself fast
0246         n->ref();
0247         n->deref();
0248         return;
0249     }
0250 
0251     d->notifications.insert(n->id(), n);
0252 
0253     // TODO KF6 d-pointer KNotifyConfig and add this there
0254     if (n->urgency() == KNotification::DefaultUrgency) {
0255         const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency"));
0256         if (urgency == QLatin1String("Low")) {
0257             n->setUrgency(KNotification::LowUrgency);
0258         } else if (urgency == QLatin1String("Normal")) {
0259             n->setUrgency(KNotification::NormalUrgency);
0260         } else if (urgency == QLatin1String("High")) {
0261             n->setUrgency(KNotification::HighUrgency);
0262         } else if (urgency == QLatin1String("Critical")) {
0263             n->setUrgency(KNotification::CriticalUrgency);
0264         }
0265     }
0266 
0267     const auto actionsList = notifyActions.split(QLatin1Char('|'));
0268 
0269     // Make sure all plugins can ref the notification
0270     // otherwise a plugin may finish and deref before everyone got a chance to ref
0271     for (const QString &action : actionsList) {
0272         KNotificationPlugin *notifyPlugin = pluginForAction(action);
0273 
0274         if (!notifyPlugin) {
0275             qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
0276             continue;
0277         }
0278 
0279         n->ref();
0280     }
0281 
0282     for (const QString &action : actionsList) {
0283         KNotificationPlugin *notifyPlugin = pluginForAction(action);
0284 
0285         if (!notifyPlugin) {
0286             qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
0287             continue;
0288         }
0289 
0290         qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName();
0291         notifyPlugin->notify(n, notifyConfig);
0292     }
0293 
0294     connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed);
0295 }
0296 
0297 void KNotificationManager::update(KNotification *n)
0298 {
0299     KNotifyConfig notifyConfig(n->appName(), n->eventId());
0300 
0301     for (KNotificationPlugin *p : std::as_const(d->notifyPlugins)) {
0302         p->update(n, notifyConfig);
0303     }
0304 }
0305 
0306 void KNotificationManager::reemit(KNotification *n)
0307 {
0308     notify(n);
0309 }
0310 
0311 void KNotificationManager::reparseConfiguration(const QString &app)
0312 {
0313     if (!d->dirtyConfigCache.contains(app)) {
0314         d->dirtyConfigCache << app;
0315     }
0316 }
0317 
0318 bool KNotificationManager::isInsideSandbox()
0319 {
0320     // logic is taken from KSandbox::isInside()
0321     static const bool isFlatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
0322     static const bool isSnap = qEnvironmentVariableIsSet("SNAP");
0323 
0324     return isFlatpak || isSnap;
0325 }
0326 
0327 #include "moc_knotificationmanager_p.cpp"