File indexing completed on 2024-04-21 03:56:29

0001 /*
0002     SPDX-FileCopyrightText: 2005-2006 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     SPDX-FileCopyrightText: 2016 Jan Grulich <jgrulich@redhat.com>
0006 
0007     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0008 */
0009 
0010 #include "notifybyportal.h"
0011 
0012 #include "debug_p.h"
0013 #include "knotification.h"
0014 #include "knotifyconfig.h"
0015 
0016 #include <QBuffer>
0017 #include <QDBusConnection>
0018 #include <QDBusConnectionInterface>
0019 #include <QDBusError>
0020 #include <QDBusMessage>
0021 #include <QDBusMetaType>
0022 #include <QDBusServiceWatcher>
0023 #include <QGuiApplication>
0024 #include <QHash>
0025 #include <QIcon>
0026 #include <QMap>
0027 #include <QPointer>
0028 
0029 #include <KConfigGroup>
0030 static const char portalDbusServiceName[] = "org.freedesktop.portal.Desktop";
0031 static const char portalDbusInterfaceName[] = "org.freedesktop.portal.Notification";
0032 static const char portalDbusPath[] = "/org/freedesktop/portal/desktop";
0033 
0034 class NotifyByPortalPrivate
0035 {
0036 public:
0037     struct PortalIcon {
0038         QString str;
0039         QDBusVariant data;
0040     };
0041 
0042     NotifyByPortalPrivate(NotifyByPortal *parent)
0043         : dbusServiceExists(false)
0044         , q(parent)
0045     {
0046     }
0047 
0048     /**
0049      * Sends notification to DBus "org.freedesktop.notifications" interface.
0050      * @param id knotify-sid identifier of notification
0051      * @param config notification data
0052      * @param update If true, will request the DBus service to update
0053                      the notification with new data from \c notification
0054      *               Otherwise will put new notification on screen
0055      * @return true for success or false if there was an error.
0056      */
0057     bool sendNotificationToPortal(KNotification *notification, const KNotifyConfig &config);
0058 
0059     /**
0060      * Sends request to close Notification with id to DBus "org.freedesktop.notifications" interface
0061      *  @param id knotify-side notification ID to close
0062      */
0063 
0064     void closePortalNotification(KNotification *notification);
0065     /**
0066      * Find the caption and the icon name of the application
0067      */
0068 
0069     void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName);
0070 
0071     /**
0072      * Specifies if DBus Notifications interface exists on session bus
0073      */
0074     bool dbusServiceExists;
0075 
0076     /*
0077      * As we communicate with the notification server over dbus
0078      * we use only ids, this is for fast KNotifications lookup
0079      */
0080     QHash<uint, QPointer<KNotification>> portalNotifications;
0081 
0082     /*
0083      * Holds the id that will be assigned to the next notification source
0084      * that will be created
0085      */
0086     uint nextId;
0087 
0088     NotifyByPortal *const q;
0089 };
0090 
0091 QDBusArgument &operator<<(QDBusArgument &argument, const NotifyByPortalPrivate::PortalIcon &icon)
0092 {
0093     argument.beginStructure();
0094     argument << icon.str << icon.data;
0095     argument.endStructure();
0096     return argument;
0097 }
0098 
0099 const QDBusArgument &operator>>(const QDBusArgument &argument, NotifyByPortalPrivate::PortalIcon &icon)
0100 {
0101     argument.beginStructure();
0102     argument >> icon.str >> icon.data;
0103     argument.endStructure();
0104     return argument;
0105 }
0106 
0107 Q_DECLARE_METATYPE(NotifyByPortalPrivate::PortalIcon)
0108 
0109 //---------------------------------------------------------------------------------------
0110 
0111 NotifyByPortal::NotifyByPortal(QObject *parent)
0112     : KNotificationPlugin(parent)
0113     , d(new NotifyByPortalPrivate(this))
0114 {
0115     // check if service already exists on plugin instantiation
0116     QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
0117     d->dbusServiceExists = interface && interface->isServiceRegistered(QString::fromLatin1(portalDbusServiceName));
0118 
0119     if (d->dbusServiceExists) {
0120         onServiceOwnerChanged(QString::fromLatin1(portalDbusServiceName), QString(), QStringLiteral("_")); // connect signals
0121     }
0122 
0123     // to catch register/unregister events from service in runtime
0124     QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this);
0125     watcher->setConnection(QDBusConnection::sessionBus());
0126     watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
0127     watcher->addWatchedService(QString::fromLatin1(portalDbusServiceName));
0128     connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &NotifyByPortal::onServiceOwnerChanged);
0129 }
0130 
0131 NotifyByPortal::~NotifyByPortal() = default;
0132 
0133 void NotifyByPortal::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
0134 {
0135     if (d->portalNotifications.contains(notification->id())) {
0136         // notification is already on the screen, do nothing
0137         finish(notification);
0138         return;
0139     }
0140 
0141     // check if Notifications DBus service exists on bus, use it if it does
0142     if (d->dbusServiceExists) {
0143         if (!d->sendNotificationToPortal(notification, notifyConfig)) {
0144             finish(notification); // an error occurred.
0145         }
0146     }
0147 }
0148 
0149 void NotifyByPortal::close(KNotification *notification)
0150 {
0151     if (d->dbusServiceExists) {
0152         d->closePortalNotification(notification);
0153     }
0154 }
0155 
0156 void NotifyByPortal::update(KNotification *notification, const KNotifyConfig &notifyConfig)
0157 {
0158     // TODO not supported by portals
0159     Q_UNUSED(notification);
0160     Q_UNUSED(notifyConfig);
0161 }
0162 
0163 void NotifyByPortal::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
0164 {
0165     Q_UNUSED(serviceName);
0166     // close all notifications we currently hold reference to
0167     for (KNotification *n : std::as_const(d->portalNotifications)) {
0168         if (n) {
0169             Q_EMIT finished(n);
0170         }
0171     }
0172 
0173     d->portalNotifications.clear();
0174 
0175     if (newOwner.isEmpty()) {
0176         d->dbusServiceExists = false;
0177     } else if (oldOwner.isEmpty()) {
0178         d->dbusServiceExists = true;
0179         d->nextId = 1;
0180 
0181         // connect to action invocation signals
0182         bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service
0183                                                                QString::fromLatin1(portalDbusPath),
0184                                                                QString::fromLatin1(portalDbusInterfaceName),
0185                                                                QStringLiteral("ActionInvoked"),
0186                                                                this,
0187                                                                SLOT(onPortalNotificationActionInvoked(QString, QString, QVariantList)));
0188         if (!connected) {
0189             qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to ActionInvoked dbus signal";
0190         }
0191     }
0192 }
0193 
0194 void NotifyByPortal::onPortalNotificationActionInvoked(const QString &id, const QString &action, const QVariantList &parameter)
0195 {
0196     Q_UNUSED(parameter);
0197 
0198     auto iter = d->portalNotifications.find(id.toUInt());
0199     if (iter == d->portalNotifications.end()) {
0200         return;
0201     }
0202 
0203     KNotification *n = *iter;
0204     if (n) {
0205         Q_EMIT actionInvoked(n->id(), action);
0206     } else {
0207         d->portalNotifications.erase(iter);
0208     }
0209 }
0210 
0211 void NotifyByPortalPrivate::getAppCaptionAndIconName(const KNotifyConfig &notifyConfig, QString *appCaption, QString *iconName)
0212 {
0213     *appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Name"));
0214     if (appCaption->isEmpty()) {
0215         *appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Comment"));
0216     }
0217     if (appCaption->isEmpty()) {
0218         *appCaption = notifyConfig.applicationName();
0219     }
0220 
0221     *iconName = notifyConfig.readEntry(QStringLiteral("IconName"));
0222     if (iconName->isEmpty()) {
0223         *iconName = notifyConfig.readGlobalEntry(QStringLiteral("IconName"));
0224     }
0225     if (iconName->isEmpty()) {
0226         *iconName = qGuiApp->windowIcon().name();
0227     }
0228     if (iconName->isEmpty()) {
0229         *iconName = notifyConfig.applicationName();
0230     }
0231 }
0232 
0233 bool NotifyByPortalPrivate::sendNotificationToPortal(KNotification *notification, const KNotifyConfig &notifyConfig_nocheck)
0234 {
0235     QDBusMessage dbusNotificationMessage;
0236     dbusNotificationMessage = QDBusMessage::createMethodCall(QString::fromLatin1(portalDbusServiceName),
0237                                                              QString::fromLatin1(portalDbusPath),
0238                                                              QString::fromLatin1(portalDbusInterfaceName),
0239                                                              QStringLiteral("AddNotification"));
0240 
0241     QVariantList args;
0242     // Will be used only with xdg-desktop-portal
0243     QVariantMap portalArgs;
0244 
0245     QString appCaption;
0246     QString iconName;
0247     getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName);
0248 
0249     // did the user override the icon name?
0250     if (!notification->iconName().isEmpty()) {
0251         iconName = notification->iconName();
0252     }
0253 
0254     QString title = notification->title().isEmpty() ? appCaption : notification->title();
0255     QString text = notification->text();
0256 
0257     if (notification->defaultAction()) {
0258         portalArgs.insert(QStringLiteral("default-action"), QStringLiteral("default"));
0259         portalArgs.insert(QStringLiteral("default-action-target"), QStringLiteral("0"));
0260     }
0261 
0262     QString priority;
0263     switch (notification->urgency()) {
0264     case KNotification::DefaultUrgency:
0265         break;
0266     case KNotification::LowUrgency:
0267         priority = QStringLiteral("low");
0268         break;
0269     case KNotification::NormalUrgency:
0270         priority = QStringLiteral("normal");
0271         break;
0272     case KNotification::HighUrgency:
0273         priority = QStringLiteral("high");
0274         break;
0275     case KNotification::CriticalUrgency:
0276         priority = QStringLiteral("urgent");
0277         break;
0278     }
0279 
0280     if (!priority.isEmpty()) {
0281         portalArgs.insert(QStringLiteral("priority"), priority);
0282     }
0283 
0284     // freedesktop.org spec defines action list to be list like
0285     // (act_id1, action1, act_id2, action2, ...)
0286     //
0287     // assign id's to actions like it's done in fillPopup() method
0288     // (i.e. starting from 1)
0289     QList<QVariantMap> buttons;
0290     buttons.reserve(notification->actions().count());
0291 
0292     const auto listActions = notification->actions();
0293     for (KNotificationAction *action : listActions) {
0294         QVariantMap button = {{QStringLiteral("action"), action->id()}, //
0295                               {QStringLiteral("label"), action->label()}};
0296         buttons << button;
0297     }
0298 
0299     qDBusRegisterMetaType<QList<QVariantMap>>();
0300     qDBusRegisterMetaType<PortalIcon>();
0301 
0302     if (!notification->pixmap().isNull()) {
0303         QByteArray pixmapData;
0304         QBuffer buffer(&pixmapData);
0305         buffer.open(QIODevice::WriteOnly);
0306         notification->pixmap().save(&buffer, "PNG");
0307         buffer.close();
0308 
0309         PortalIcon icon;
0310         icon.str = QStringLiteral("bytes");
0311         icon.data.setVariant(pixmapData);
0312         portalArgs.insert(QStringLiteral("icon"), QVariant::fromValue<PortalIcon>(icon));
0313     } else {
0314         // Use this for now for backwards compatibility, we can as well set the variant to be (sv) where the
0315         // string is keyword "themed" and the variant is an array of strings with icon names
0316         portalArgs.insert(QStringLiteral("icon"), iconName);
0317     }
0318 
0319     portalArgs.insert(QStringLiteral("title"), title);
0320     portalArgs.insert(QStringLiteral("body"), text);
0321     portalArgs.insert(QStringLiteral("buttons"), QVariant::fromValue<QList<QVariantMap>>(buttons));
0322 
0323     args.append(QString::number(nextId));
0324     args.append(portalArgs);
0325 
0326     dbusNotificationMessage.setArguments(args);
0327 
0328     QDBusPendingCall notificationCall = QDBusConnection::sessionBus().asyncCall(dbusNotificationMessage, -1);
0329 
0330     // If we are in sandbox we don't need to wait for returned notification id
0331     portalNotifications.insert(nextId++, notification);
0332 
0333     return true;
0334 }
0335 
0336 void NotifyByPortalPrivate::closePortalNotification(KNotification *notification)
0337 {
0338     uint id = portalNotifications.key(notification, 0);
0339 
0340     qCDebug(LOG_KNOTIFICATIONS) << "ID: " << id;
0341 
0342     if (id == 0) {
0343         qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id();
0344         return;
0345     }
0346 
0347     QDBusMessage m = QDBusMessage::createMethodCall(QString::fromLatin1(portalDbusServiceName),
0348                                                     QString::fromLatin1(portalDbusPath),
0349                                                     QString::fromLatin1(portalDbusInterfaceName),
0350                                                     QStringLiteral("RemoveNotification"));
0351     m.setArguments({QString::number(id)});
0352 
0353     // send(..) does not block
0354     bool queued = QDBusConnection::sessionBus().send(m);
0355 
0356     if (!queued) {
0357         qCWarning(LOG_KNOTIFICATIONS) << "Failed to queue dbus message for closing a notification";
0358     }
0359 }
0360 
0361 #include "moc_notifybyportal.cpp"