File indexing completed on 2024-03-24 03:59: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"