File indexing completed on 2024-04-14 03:54:13
0001 /* 0002 SPDX-FileCopyrightText: 2005-2009 Olivier Goffart <ogoffart at kde.org> 0003 SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com> 0004 SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0007 */ 0008 0009 #include "notifybypopup.h" 0010 0011 #include "debug_p.h" 0012 #include "imageconverter.h" 0013 #include "knotification.h" 0014 #include "knotificationreplyaction.h" 0015 0016 #include <QBuffer> 0017 #include <QDBusConnection> 0018 #include <QGuiApplication> 0019 #include <QHash> 0020 #include <QIcon> 0021 #include <QMutableListIterator> 0022 #include <QPointer> 0023 #include <QUrl> 0024 0025 #include <KConfigGroup> 0026 0027 NotifyByPopup::NotifyByPopup(QObject *parent) 0028 : KNotificationPlugin(parent) 0029 , m_dbusInterface(QStringLiteral("org.freedesktop.Notifications"), QStringLiteral("/org/freedesktop/Notifications"), QDBusConnection::sessionBus()) 0030 { 0031 m_dbusServiceCapCacheDirty = true; 0032 0033 connect(&m_dbusInterface, &org::freedesktop::Notifications::ActionInvoked, this, &NotifyByPopup::onNotificationActionInvoked); 0034 connect(&m_dbusInterface, &org::freedesktop::Notifications::ActivationToken, this, &NotifyByPopup::onNotificationActionTokenReceived); 0035 0036 // TODO can we check if this actually worked? 0037 // probably not as this just does a DBus filter which will work but the signal might still get caught in apparmor :/ 0038 connect(&m_dbusInterface, &org::freedesktop::Notifications::NotificationReplied, this, &NotifyByPopup::onNotificationReplied); 0039 0040 connect(&m_dbusInterface, &org::freedesktop::Notifications::NotificationClosed, this, &NotifyByPopup::onNotificationClosed); 0041 } 0042 0043 NotifyByPopup::~NotifyByPopup() 0044 { 0045 if (!m_notificationQueue.isEmpty()) { 0046 qCWarning(LOG_KNOTIFICATIONS) << "Had queued notifications on destruction. Was the eventloop running?"; 0047 } 0048 } 0049 0050 void NotifyByPopup::notify(KNotification *notification, const KNotifyConfig ¬ifyConfig) 0051 { 0052 if (m_dbusServiceCapCacheDirty) { 0053 // if we don't have the server capabilities yet, we need to query for them first; 0054 // as that is an async dbus operation, we enqueue the notification and process them 0055 // when we receive dbus reply with the server capabilities 0056 m_notificationQueue.append(qMakePair(notification, notifyConfig)); 0057 queryPopupServerCapabilities(); 0058 } else { 0059 if (!sendNotificationToServer(notification, notifyConfig)) { 0060 finish(notification); // an error occurred. 0061 } 0062 } 0063 } 0064 0065 void NotifyByPopup::update(KNotification *notification, const KNotifyConfig ¬ifyConfig) 0066 { 0067 sendNotificationToServer(notification, notifyConfig, true); 0068 } 0069 0070 void NotifyByPopup::close(KNotification *notification) 0071 { 0072 QMutableListIterator<QPair<KNotification *, KNotifyConfig>> iter(m_notificationQueue); 0073 while (iter.hasNext()) { 0074 auto &item = iter.next(); 0075 if (item.first == notification) { 0076 iter.remove(); 0077 } 0078 } 0079 0080 uint id = m_notifications.key(notification, 0); 0081 0082 if (id == 0) { 0083 qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id(); 0084 return; 0085 } 0086 0087 m_dbusInterface.CloseNotification(id); 0088 } 0089 0090 void NotifyByPopup::onNotificationActionTokenReceived(uint notificationId, const QString &xdgActivationToken) 0091 { 0092 auto iter = m_notifications.find(notificationId); 0093 if (iter == m_notifications.end()) { 0094 return; 0095 } 0096 0097 KNotification *n = *iter; 0098 if (n) { 0099 Q_EMIT xdgActivationTokenReceived(n->id(), xdgActivationToken); 0100 } 0101 } 0102 0103 void NotifyByPopup::onNotificationActionInvoked(uint notificationId, const QString &actionKey) 0104 { 0105 auto iter = m_notifications.find(notificationId); 0106 if (iter == m_notifications.end()) { 0107 return; 0108 } 0109 0110 KNotification *n = *iter; 0111 if (n) { 0112 if (actionKey == QLatin1String("inline-reply") && n->replyAction()) { 0113 Q_EMIT replied(n->id(), QString()); 0114 } else { 0115 Q_EMIT actionInvoked(n->id(), actionKey); 0116 } 0117 } else { 0118 m_notifications.erase(iter); 0119 } 0120 } 0121 0122 void NotifyByPopup::onNotificationClosed(uint dbus_id, uint reason) 0123 { 0124 auto iter = m_notifications.find(dbus_id); 0125 if (iter == m_notifications.end()) { 0126 return; 0127 } 0128 KNotification *n = *iter; 0129 m_notifications.remove(dbus_id); 0130 0131 if (n) { 0132 Q_EMIT finished(n); 0133 // The popup bubble is the only user facing part of a notification, 0134 // if the user closes the popup, it means he wants to get rid 0135 // of the notification completely, including playing sound etc 0136 // Therefore we close the KNotification completely after closing 0137 // the popup, but only if the reason is 2, which means "user closed" 0138 if (reason == 2) { 0139 n->close(); 0140 } 0141 } 0142 } 0143 0144 void NotifyByPopup::onNotificationReplied(uint notificationId, const QString &text) 0145 { 0146 auto iter = m_notifications.find(notificationId); 0147 if (iter == m_notifications.end()) { 0148 return; 0149 } 0150 0151 KNotification *n = *iter; 0152 if (n) { 0153 if (n->replyAction()) { 0154 Q_EMIT replied(n->id(), text); 0155 } 0156 } else { 0157 m_notifications.erase(iter); 0158 } 0159 } 0160 0161 void NotifyByPopup::getAppCaptionAndIconName(const KNotifyConfig ¬ifyConfig, QString *appCaption, QString *iconName) 0162 { 0163 *appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Name")); 0164 if (appCaption->isEmpty()) { 0165 *appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Comment")); 0166 } 0167 if (appCaption->isEmpty()) { 0168 *appCaption = notifyConfig.applicationName(); 0169 } 0170 0171 *iconName = notifyConfig.readEntry(QStringLiteral("IconName")); 0172 if (iconName->isEmpty()) { 0173 *iconName = notifyConfig.readGlobalEntry(QStringLiteral("IconName")); 0174 } 0175 if (iconName->isEmpty()) { 0176 *iconName = qGuiApp->windowIcon().name(); 0177 } 0178 if (iconName->isEmpty()) { 0179 *iconName = notifyConfig.applicationName(); 0180 } 0181 } 0182 0183 bool NotifyByPopup::sendNotificationToServer(KNotification *notification, const KNotifyConfig ¬ifyConfig_nocheck, bool update) 0184 { 0185 uint updateId = m_notifications.key(notification, 0); 0186 0187 if (update) { 0188 if (updateId == 0) { 0189 // we have nothing to update; the notification we're trying to update 0190 // has been already closed 0191 return false; 0192 } 0193 } 0194 0195 QString appCaption; 0196 QString iconName; 0197 getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName); 0198 0199 // did the user override the icon name? 0200 if (!notification->iconName().isEmpty()) { 0201 iconName = notification->iconName(); 0202 } 0203 0204 QString title = notification->title().isEmpty() ? appCaption : notification->title(); 0205 QString text = notification->text(); 0206 0207 if (!m_popupServerCapabilities.contains(QLatin1String("body-markup"))) { 0208 text = stripRichText(text); 0209 } 0210 0211 QVariantMap hintsMap; 0212 0213 // freedesktop.org spec defines action list to be list like 0214 // (act_id1, action1, act_id2, action2, ...) 0215 // 0216 // assign id's to actions like it's done in fillPopup() method 0217 // (i.e. starting from 1) 0218 QStringList actionList; 0219 if (m_popupServerCapabilities.contains(QLatin1String("actions"))) { 0220 if (notification->defaultAction()) { 0221 actionList.append(QStringLiteral("default")); 0222 actionList.append(notification->defaultAction()->label()); 0223 } 0224 int actId = 0; 0225 const auto listActions = notification->actions(); 0226 for (const KNotificationAction *action : listActions) { 0227 actId++; 0228 actionList.append(action->id()); 0229 actionList.append(action->label()); 0230 } 0231 0232 if (auto *replyAction = notification->replyAction()) { 0233 const bool supportsInlineReply = m_popupServerCapabilities.contains(QLatin1String("inline-reply")); 0234 0235 if (supportsInlineReply || replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) { 0236 actionList.append(QStringLiteral("inline-reply")); 0237 actionList.append(replyAction->label()); 0238 0239 if (supportsInlineReply) { 0240 if (!replyAction->placeholderText().isEmpty()) { 0241 hintsMap.insert(QStringLiteral("x-kde-reply-placeholder-text"), replyAction->placeholderText()); 0242 } 0243 if (!replyAction->submitButtonText().isEmpty()) { 0244 hintsMap.insert(QStringLiteral("x-kde-reply-submit-button-text"), replyAction->submitButtonText()); 0245 } 0246 if (replyAction->submitButtonIconName().isEmpty()) { 0247 hintsMap.insert(QStringLiteral("x-kde-reply-submit-button-icon-name"), replyAction->submitButtonIconName()); 0248 } 0249 } 0250 } 0251 } 0252 } 0253 0254 // Add the application name to the hints. 0255 // According to freedesktop.org spec, the app_name is supposed to be the application's "pretty name" 0256 // but in some places it's handy to know the application name itself 0257 if (!notification->appName().isEmpty()) { 0258 hintsMap[QStringLiteral("x-kde-appname")] = notification->appName(); 0259 } 0260 0261 if (!notification->eventId().isEmpty()) { 0262 hintsMap[QStringLiteral("x-kde-eventId")] = notification->eventId(); 0263 } 0264 0265 if (notification->flags() & KNotification::SkipGrouping) { 0266 hintsMap[QStringLiteral("x-kde-skipGrouping")] = 1; 0267 } 0268 0269 QString desktopFileName = QGuiApplication::desktopFileName(); 0270 if (!desktopFileName.isEmpty()) { 0271 // handle apps which set the desktopFileName property with filename suffix, 0272 // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) 0273 if (desktopFileName.endsWith(QLatin1String(".desktop"))) { 0274 desktopFileName.chop(8); 0275 } 0276 hintsMap[QStringLiteral("desktop-entry")] = desktopFileName; 0277 } 0278 0279 int urgency = -1; 0280 switch (notification->urgency()) { 0281 case KNotification::DefaultUrgency: 0282 break; 0283 case KNotification::LowUrgency: 0284 urgency = 0; 0285 break; 0286 case KNotification::NormalUrgency: 0287 Q_FALLTHROUGH(); 0288 // freedesktop.org m_notifications only know low, normal, critical 0289 case KNotification::HighUrgency: 0290 urgency = 1; 0291 break; 0292 case KNotification::CriticalUrgency: 0293 urgency = 2; 0294 break; 0295 } 0296 0297 if (urgency > -1) { 0298 hintsMap[QStringLiteral("urgency")] = urgency; 0299 } 0300 0301 const QVariantMap hints = notification->hints(); 0302 for (auto it = hints.constBegin(); it != hints.constEnd(); ++it) { 0303 hintsMap[it.key()] = it.value(); 0304 } 0305 0306 // FIXME - re-enable/fix 0307 // let's see if we've got an image, and store the image in the hints map 0308 if (!notification->pixmap().isNull()) { 0309 QByteArray pixmapData; 0310 QBuffer buffer(&pixmapData); 0311 buffer.open(QIODevice::WriteOnly); 0312 notification->pixmap().save(&buffer, "PNG"); 0313 buffer.close(); 0314 hintsMap[QStringLiteral("image_data")] = ImageConverter::variantForImage(QImage::fromData(pixmapData)); 0315 } 0316 0317 // Persistent => 0 == infinite timeout 0318 // CloseOnTimeout => -1 == let the server decide 0319 int timeout = (notification->flags() & KNotification::Persistent) ? 0 : -1; 0320 0321 const QDBusPendingReply<uint> reply = m_dbusInterface.Notify(appCaption, updateId, iconName, title, text, actionList, hintsMap, timeout); 0322 0323 // parent is set to the notification so that no-one ever accesses a dangling pointer on the notificationObject property 0324 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, notification); 0325 0326 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, notification](QDBusPendingCallWatcher *watcher) { 0327 watcher->deleteLater(); 0328 QDBusPendingReply<uint> reply = *watcher; 0329 m_notifications.insert(reply.argumentAt<0>(), notification); 0330 }); 0331 0332 return true; 0333 } 0334 0335 void NotifyByPopup::queryPopupServerCapabilities() 0336 { 0337 if (!m_dbusServiceCapCacheDirty) { 0338 return; 0339 } 0340 0341 QDBusPendingReply<QStringList> call = m_dbusInterface.GetCapabilities(); 0342 0343 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call); 0344 0345 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { 0346 watcher->deleteLater(); 0347 const QDBusPendingReply<QStringList> reply = *watcher; 0348 const QStringList capabilities = reply.argumentAt<0>(); 0349 m_popupServerCapabilities = capabilities; 0350 m_dbusServiceCapCacheDirty = false; 0351 0352 // re-run notify() on all enqueued m_notifications 0353 for (const QPair<KNotification *, KNotifyConfig> ¬i : std::as_const(m_notificationQueue)) { 0354 notify(noti.first, noti.second); 0355 } 0356 0357 m_notificationQueue.clear(); 0358 }); 0359 } 0360 0361 #include "moc_notifybypopup.cpp"