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 QT_DBUS_LIB |
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 | #else |
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 QT_DBUS_LIB |
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 | #else |
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 | |