| 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 <mklapetek@kde.org> |
| 5 | SPDX-FileCopyrightText: 2017 Eike Hein <hein@kde.org> |
| 6 | SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org> |
| 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 <QFileInfo> |
| 18 | #include <QHash> |
| 19 | |
| 20 | #ifdef HAVE_DBUS |
| 21 | #include <QDBusConnection> |
| 22 | #include <QDBusConnectionInterface> |
| 23 | #endif |
| 24 | |
| 25 | #include "knotificationplugin.h" |
| 26 | #include "knotificationreplyaction.h" |
| 27 | #include "knotifyconfig.h" |
| 28 | |
| 29 | #if defined(Q_OS_ANDROID) |
| 30 | #include "notifybyandroid.h" |
| 31 | #elif defined(Q_OS_MACOS) |
| 32 | #include "notifybymacosnotificationcenter.h" |
| 33 | #elif defined(WITH_SNORETOAST) |
| 34 | #include "notifybysnore.h" |
| 35 | #elif defined(HAVE_DBUS) |
| 36 | #include "notifybypopup.h" |
| 37 | #include "notifybyportal.h" |
| 38 | #endif |
| 39 | #include "debug_p.h" |
| 40 | |
| 41 | #if defined(HAVE_CANBERRA) |
| 42 | #include "notifybyaudio.h" |
| 43 | #endif |
| 44 | |
| 45 | typedef QHash<QString, QString> Dict; |
| 46 | |
| 47 | struct Q_DECL_HIDDEN KNotificationManager::Private { |
| 48 | QHash<int, KNotification *> notifications; |
| 49 | QHash<QString, KNotificationPlugin *> notifyPlugins; |
| 50 | |
| 51 | QStringList dirtyConfigCache; |
| 52 | bool portalDBusServiceExists = false; |
| 53 | }; |
| 54 | |
| 55 | class KNotificationManagerSingleton |
| 56 | { |
| 57 | public: |
| 58 | KNotificationManager instance; |
| 59 | }; |
| 60 | |
| 61 | Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self) |
| 62 | |
| 63 | KNotificationManager *KNotificationManager::self() |
| 64 | { |
| 65 | return &s_self()->instance; |
| 66 | } |
| 67 | |
| 68 | KNotificationManager::KNotificationManager() |
| 69 | : d(new Private) |
| 70 | { |
| 71 | qDeleteAll(c: d->notifyPlugins); |
| 72 | d->notifyPlugins.clear(); |
| 73 | |
| 74 | #ifdef HAVE_DBUS |
| 75 | if (isInsideSandbox()) { |
| 76 | QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); |
| 77 | d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop" )); |
| 78 | } |
| 79 | |
| 80 | QDBusConnection::sessionBus().connect(service: QString(), |
| 81 | QStringLiteral("/Config" ), |
| 82 | QStringLiteral("org.kde.knotification" ), |
| 83 | QStringLiteral("reparseConfiguration" ), |
| 84 | receiver: this, |
| 85 | SLOT(reparseConfiguration(QString))); |
| 86 | #endif |
| 87 | } |
| 88 | |
| 89 | KNotificationManager::~KNotificationManager() = default; |
| 90 | |
| 91 | KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action) |
| 92 | { |
| 93 | KNotificationPlugin *plugin = d->notifyPlugins.value(key: action); |
| 94 | |
| 95 | // We already loaded a plugin for this action. |
| 96 | if (plugin) { |
| 97 | return plugin; |
| 98 | } |
| 99 | |
| 100 | auto addPlugin = [this](KNotificationPlugin *plugin) { |
| 101 | d->notifyPlugins[plugin->optionName()] = plugin; |
| 102 | connect(sender: plugin, signal: &KNotificationPlugin::finished, context: this, slot: &KNotificationManager::notifyPluginFinished); |
| 103 | connect(sender: plugin, signal: &KNotificationPlugin::xdgActivationTokenReceived, context: this, slot: &KNotificationManager::xdgActivationTokenReceived); |
| 104 | connect(sender: plugin, signal: &KNotificationPlugin::actionInvoked, context: this, slot: &KNotificationManager::notificationActivated); |
| 105 | connect(sender: plugin, signal: &KNotificationPlugin::replied, context: this, slot: &KNotificationManager::notificationReplied); |
| 106 | }; |
| 107 | |
| 108 | // Load plugin. |
| 109 | // We have a series of built-ins up first, and fall back to trying |
| 110 | // to instantiate an externally supplied plugin. |
| 111 | if (action == QLatin1String("Popup" )) { |
| 112 | #if defined(Q_OS_ANDROID) |
| 113 | plugin = new NotifyByAndroid(this); |
| 114 | #elif defined(WITH_SNORETOAST) |
| 115 | plugin = new NotifyBySnore(this); |
| 116 | #elif defined(Q_OS_MACOS) |
| 117 | plugin = new NotifyByMacOSNotificationCenter(this); |
| 118 | #elif defined(HAVE_DBUS) |
| 119 | if (d->portalDBusServiceExists) { |
| 120 | plugin = new NotifyByPortal(this); |
| 121 | } else { |
| 122 | plugin = new NotifyByPopup(this); |
| 123 | } |
| 124 | #endif |
| 125 | addPlugin(plugin); |
| 126 | } else if (action == QLatin1String("Sound" )) { |
| 127 | #if defined(HAVE_CANBERRA) |
| 128 | plugin = new NotifyByAudio(this); |
| 129 | addPlugin(plugin); |
| 130 | #endif |
| 131 | } |
| 132 | |
| 133 | return plugin; |
| 134 | } |
| 135 | |
| 136 | void KNotificationManager::notifyPluginFinished(KNotification *notification) |
| 137 | { |
| 138 | if (!notification || !d->notifications.contains(key: notification->id())) { |
| 139 | return; |
| 140 | } |
| 141 | |
| 142 | notification->deref(); |
| 143 | } |
| 144 | |
| 145 | void KNotificationManager::notificationActivated(int id, const QString &actionId) |
| 146 | { |
| 147 | if (d->notifications.contains(key: id)) { |
| 148 | qCDebug(LOG_KNOTIFICATIONS) << id << " " << actionId; |
| 149 | KNotification *n = d->notifications[id]; |
| 150 | n->activate(action: actionId); |
| 151 | |
| 152 | // Resident actions delegate control over notification lifetime to the client |
| 153 | if (!n->hints().value(QStringLiteral("resident" )).toBool()) { |
| 154 | close(id); |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token) |
| 160 | { |
| 161 | KNotification *n = d->notifications.value(key: id); |
| 162 | if (n) { |
| 163 | qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token; |
| 164 | n->d->xdgActivationToken = token; |
| 165 | Q_EMIT n->xdgActivationTokenChanged(); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | void KNotificationManager::notificationReplied(int id, const QString &text) |
| 170 | { |
| 171 | if (KNotification *n = d->notifications.value(key: id)) { |
| 172 | if (auto *replyAction = n->replyAction()) { |
| 173 | // cannot really send out a "activate inline-reply" signal from plugin to manager |
| 174 | // so we instead assume empty reply is not supported and means normal invocation |
| 175 | if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) { |
| 176 | Q_EMIT replyAction->activated(); |
| 177 | } else { |
| 178 | Q_EMIT replyAction->replied(text); |
| 179 | } |
| 180 | close(id); |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | void KNotificationManager::notificationClosed() |
| 186 | { |
| 187 | KNotification *notification = qobject_cast<KNotification *>(object: sender()); |
| 188 | if (!notification) { |
| 189 | return; |
| 190 | } |
| 191 | // We cannot do d->notifications.find(notification->id()); here because the |
| 192 | // notification->id() is -1 or -2 at this point, so we need to look for value |
| 193 | for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) { |
| 194 | if (iter.value() == notification) { |
| 195 | d->notifications.erase(it: iter); |
| 196 | break; |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | void KNotificationManager::close(int id) |
| 202 | { |
| 203 | if (d->notifications.contains(key: id)) { |
| 204 | KNotification *n = d->notifications.value(key: id); |
| 205 | qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id; |
| 206 | |
| 207 | // Find plugins that are actually acting on this notification |
| 208 | // call close() only on those, otherwise each KNotificationPlugin::close() |
| 209 | // will call finish() which may close-and-delete the KNotification object |
| 210 | // before it finishes calling close on all the other plugins. |
| 211 | // For example: Action=Popup is a single actions but there is 5 loaded |
| 212 | // plugins, calling close() on the second would already close-and-delete |
| 213 | // the notification |
| 214 | KNotifyConfig notifyConfig(n->appName(), n->eventId()); |
| 215 | QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action" )); |
| 216 | |
| 217 | const auto listActions = notifyActions.split(sep: QLatin1Char('|')); |
| 218 | for (const QString &action : listActions) { |
| 219 | if (!d->notifyPlugins.contains(key: action)) { |
| 220 | qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | d->notifyPlugins[action]->close(notification: n); |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | void KNotificationManager::notify(KNotification *n) |
| 230 | { |
| 231 | KNotifyConfig notifyConfig(n->appName(), n->eventId()); |
| 232 | |
| 233 | if (d->dirtyConfigCache.contains(str: n->appName())) { |
| 234 | notifyConfig.reparseSingleConfiguration(app: n->appName()); |
| 235 | d->dirtyConfigCache.removeOne(t: n->appName()); |
| 236 | } |
| 237 | |
| 238 | if (!notifyConfig.isValid()) { |
| 239 | qCWarning(LOG_KNOTIFICATIONS) << "No event config could be found for event id" << n->eventId() << "under notifyrc file for app" << n->appName(); |
| 240 | } |
| 241 | |
| 242 | const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action" )); |
| 243 | |
| 244 | if (notifyActions.isEmpty() || notifyActions == QLatin1String("None" )) { |
| 245 | // this will cause KNotification closing itself fast |
| 246 | n->ref(); |
| 247 | n->deref(); |
| 248 | return; |
| 249 | } |
| 250 | |
| 251 | d->notifications.insert(key: n->id(), value: n); |
| 252 | |
| 253 | // TODO KF6 d-pointer KNotifyConfig and add this there |
| 254 | if (n->urgency() == KNotification::DefaultUrgency) { |
| 255 | const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency" )); |
| 256 | if (urgency == QLatin1String("Low" )) { |
| 257 | n->setUrgency(KNotification::LowUrgency); |
| 258 | } else if (urgency == QLatin1String("Normal" )) { |
| 259 | n->setUrgency(KNotification::NormalUrgency); |
| 260 | } else if (urgency == QLatin1String("High" )) { |
| 261 | n->setUrgency(KNotification::HighUrgency); |
| 262 | } else if (urgency == QLatin1String("Critical" )) { |
| 263 | n->setUrgency(KNotification::CriticalUrgency); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | const auto actionsList = notifyActions.split(sep: QLatin1Char('|')); |
| 268 | |
| 269 | // Make sure all plugins can ref the notification |
| 270 | // otherwise a plugin may finish and deref before everyone got a chance to ref |
| 271 | for (const QString &action : actionsList) { |
| 272 | KNotificationPlugin *notifyPlugin = pluginForAction(action); |
| 273 | |
| 274 | if (!notifyPlugin) { |
| 275 | qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; |
| 276 | continue; |
| 277 | } |
| 278 | |
| 279 | n->ref(); |
| 280 | } |
| 281 | |
| 282 | for (const QString &action : actionsList) { |
| 283 | KNotificationPlugin *notifyPlugin = pluginForAction(action); |
| 284 | |
| 285 | if (!notifyPlugin) { |
| 286 | qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; |
| 287 | continue; |
| 288 | } |
| 289 | |
| 290 | qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName(); |
| 291 | notifyPlugin->notify(notification: n, notifyConfig); |
| 292 | } |
| 293 | |
| 294 | connect(sender: n, signal: &KNotification::closed, context: this, slot: &KNotificationManager::notificationClosed); |
| 295 | } |
| 296 | |
| 297 | void KNotificationManager::update(KNotification *n) |
| 298 | { |
| 299 | KNotifyConfig notifyConfig(n->appName(), n->eventId()); |
| 300 | |
| 301 | for (KNotificationPlugin *p : std::as_const(t&: d->notifyPlugins)) { |
| 302 | p->update(notification: n, notifyConfig); |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | void KNotificationManager::reemit(KNotification *n) |
| 307 | { |
| 308 | notify(n); |
| 309 | } |
| 310 | |
| 311 | void KNotificationManager::reparseConfiguration(const QString &app) |
| 312 | { |
| 313 | if (!d->dirtyConfigCache.contains(str: app)) { |
| 314 | d->dirtyConfigCache << app; |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | bool KNotificationManager::isInsideSandbox() |
| 319 | { |
| 320 | // logic is taken from KSandbox::isInside() |
| 321 | static const bool isFlatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info" )); |
| 322 | static const bool isSnap = qEnvironmentVariableIsSet(varName: "SNAP" ); |
| 323 | |
| 324 | return isFlatpak || isSnap; |
| 325 | } |
| 326 | |
| 327 | #include "moc_knotificationmanager_p.cpp" |
| 328 | |