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