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, ¬ifyConfig); 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, ¬ifyConfig); 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"