File indexing completed on 2024-04-28 11:43:48

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