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 &notifyConfig)
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 &notifyConfig)
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 &notifyConfig, 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 &notifyConfig_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> &noti : 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"