diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ imageconverter.cpp #needed to marshal images for sending over dbus by NotifyByPopup notifybypopupgrowl.cpp notifybyexecute.cpp + notifybyflatpak.cpp notifybylogfile.cpp notifybytaskbar.cpp ${knotifications_QM_LOADER} diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include "knotifyconfig.h" @@ -34,6 +36,7 @@ #include "notifybylogfile.h" #include "notifybytaskbar.h" #include "notifybyexecute.h" +#include "notifybyflatpak.h" #include "debug_p.h" #ifdef HAVE_PHONON4QT5 @@ -74,7 +77,29 @@ d->notifyIdCounter = 0; qDeleteAll(d->notifyPlugins); d->notifyPlugins.clear(); - addPlugin(new NotifyByPopup(this)); + + bool inSandbox = false; + bool portalDBusServiceExists = false; + + if (!qEnvironmentVariableIsEmpty("XDG_RUNTIME_DIR")) { + const QString runtimeDir = qgetenv("XDG_RUNTIME_DIR"); + if (!runtimeDir.isEmpty()) { + inSandbox = QFileInfo::exists(runtimeDir + QLatin1String("/flatpak-info")); + } + } + + if (inSandbox) { + QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); + portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop")); + } + + // If we are running in sandbox and flatpak portal dbus service is available send popup notifications + // through the portal + if (inSandbox && portalDBusServiceExists) { + addPlugin(new NotifyByFlatpak(this)); + } else { + addPlugin(new NotifyByPopup(this)); + } addPlugin(new NotifyByExecute(this)); addPlugin(new NotifyByLogfile(this)); diff --git a/src/notifybyflatpak.h b/src/notifybyflatpak.h new file mode 100644 --- /dev/null +++ b/src/notifybyflatpak.h @@ -0,0 +1,63 @@ +/* + Copyright (C) 2005-2006 by Olivier Goffart + Copyright (C) 2008 by Dmitry Suzdalev + Copyright (C) 2014 by Martin Klapetek + Copyright (C) 2016 Jan Grulich + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . + */ + +#ifndef NOTIFYBYFLATPAK_H +#define NOTIFYBYFLATPAK_H + +#include "knotificationplugin.h" + +#include +#include + +class KNotification; +class NotifyByFlatpakPrivate; + +class NotifyByFlatpak : public KNotificationPlugin +{ + Q_OBJECT +public: + NotifyByFlatpak(QObject *parent = 0l); + virtual ~NotifyByFlatpak(); + + QString optionName() Q_DECL_OVERRIDE { return QStringLiteral("Popup"); } + void notify(KNotification *notification, KNotifyConfig *notifyConfig) Q_DECL_OVERRIDE; + void close(KNotification *notification) Q_DECL_OVERRIDE; + void update(KNotification *notification, KNotifyConfig *config) Q_DECL_OVERRIDE; + +private Q_SLOTS: + + // slot to catch appearance or dissapearance of org.freedesktop.Desktop DBus service + void onServiceOwnerChanged(const QString &, const QString &, const QString &); + + void onPortalNotificationActionInvoked(const QString &, const QString &, const QVariantList &); + +private: + // TODO KF6, replace current public notify/update + void notify(KNotification *notification, const KNotifyConfig ¬ifyConfig); + void update(KNotification *notification, const KNotifyConfig ¬ifyConfig); + + NotifyByFlatpakPrivate * const d; +}; + +#endif + diff --git a/src/notifybyflatpak.cpp b/src/notifybyflatpak.cpp new file mode 100644 --- /dev/null +++ b/src/notifybyflatpak.cpp @@ -0,0 +1,299 @@ +/* + Copyright (C) 2005-2006 by Olivier Goffart + Copyright (C) 2008 by Dmitry Suzdalev + Copyright (C) 2014 by Martin Klapetek + Copyright (C) 2016 Jan Grulich + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . + */ + +#include "notifybyflatpak.h" + +#include "knotifyconfig.h" +#include "knotification.h" +#include "debug_p.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +static const char portalDbusServiceName[] = "org.freedesktop.portal.Desktop"; +static const char portalDbusInterfaceName[] = "org.freedesktop.portal.Notification"; +static const char portalDbusPath[] = "/org/freedesktop/portal/desktop"; + +class NotifyByFlatpakPrivate { +public: + NotifyByFlatpakPrivate(NotifyByFlatpak *parent) : dbusServiceExists(false), q(parent) {} + + /** + * Sends notification to DBus "org.freedesktop.notifications" interface. + * @param id knotify-sid identifier of notification + * @param config notification data + * @param update If true, will request the DBus service to update + the notification with new data from \c notification + * Otherwise will put new notification on screen + * @return true for success or false if there was an error. + */ + bool sendNotificationToPortal(KNotification *notification, const KNotifyConfig &config); + + /** + * Sends request to close Notification with id to DBus "org.freedesktop.notifications" interface + * @param id knotify-side notification ID to close + */ + + void closePortalNotification(KNotification *notification); + /** + * Find the caption and the icon name of the application + */ + + void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName); + + /** + * Specifies if DBus Notifications interface exists on session bus + */ + bool dbusServiceExists; + + /* + * As we communicate with the notification server over dbus + * we use only ids, this is for fast KNotifications lookup + */ + QHash> flatpakNotifications; + + /* + * Holds the id that will be assigned to the next notification source + * that will be created + */ + uint nextId; + + NotifyByFlatpak * const q; +}; + +//--------------------------------------------------------------------------------------- + +NotifyByFlatpak::NotifyByFlatpak(QObject *parent) + : KNotificationPlugin(parent), + d(new NotifyByFlatpakPrivate(this)) +{ + // check if service already exists on plugin instantiation + QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); + d->dbusServiceExists = interface && interface->isServiceRegistered(portalDbusServiceName); + + if (d->dbusServiceExists) { + onServiceOwnerChanged(portalDbusServiceName, QString(), QStringLiteral("_")); //connect signals + } + + // to catch register/unregister events from service in runtime + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this); + watcher->setConnection(QDBusConnection::sessionBus()); + watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); + watcher->addWatchedService(portalDbusServiceName); + connect(watcher,&QDBusServiceWatcher::serviceOwnerChanged, this, &NotifyByFlatpak::onServiceOwnerChanged); +} + + +NotifyByFlatpak::~NotifyByFlatpak() +{ + delete d; +} + +void NotifyByFlatpak::notify(KNotification *notification, KNotifyConfig *notifyConfig) +{ + notify(notification, *notifyConfig); +} + +void NotifyByFlatpak::notify(KNotification *notification, const KNotifyConfig ¬ifyConfig) +{ + if (d->flatpakNotifications.contains(notification->id())) { + // notification is already on the screen, do nothing + finish(notification); + return; + } + + // check if Notifications DBus service exists on bus, use it if it does + if (d->dbusServiceExists) { + if (!d->sendNotificationToPortal(notification, notifyConfig)) { + finish(notification); //an error ocurred. + } + } +} + +void NotifyByFlatpak::close(KNotification *notification) +{ + if (d->dbusServiceExists) { + d->closePortalNotification(notification); + } +} + +void NotifyByFlatpak::update(KNotification *notification, KNotifyConfig *notifyConfig) +{ + // TODO not supported by portals + Q_UNUSED(notification); + Q_UNUSED(notifyConfig); +} + +void NotifyByFlatpak::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(serviceName); + // close all notifications we currently hold reference to + Q_FOREACH (KNotification *n, d->flatpakNotifications) { + if (n) { + emit finished(n); + } + } + + d->flatpakNotifications.clear(); + + if (newOwner.isEmpty()) { + d->dbusServiceExists = false; + } else if (oldOwner.isEmpty()) { + d->dbusServiceExists = true; + d->nextId = 1; + + // connect to action invocation signals + bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service + portalDbusPath, + portalDbusInterfaceName, + QStringLiteral("ActionInvoked"), + this, + SLOT(onPortalNotificationActionInvoked(QString,QString,QVariantList))); + if (!connected) { + qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to ActionInvoked dbus signal"; + } + } +} + +void NotifyByFlatpak::onPortalNotificationActionInvoked(const QString &id, const QString &action, const QVariantList ¶meter) +{ + Q_UNUSED(parameter); + + auto iter = d->flatpakNotifications.find(id.toUInt()); + if (iter == d->flatpakNotifications.end()) { + return; + } + + KNotification *n = *iter; + if (n) { + emit actionInvoked(n->id(), action.toUInt()); + } else { + d->flatpakNotifications.erase(iter); + } +} + +void NotifyByFlatpakPrivate::getAppCaptionAndIconName(const KNotifyConfig ¬ifyConfig, QString *appCaption, QString *iconName) +{ + KConfigGroup globalgroup(&(*notifyConfig.eventsfile), QStringLiteral("Global")); + *appCaption = globalgroup.readEntry("Name", globalgroup.readEntry("Comment", notifyConfig.appname)); + + KConfigGroup eventGroup(&(*notifyConfig.eventsfile), QStringLiteral("Event/%1").arg(notifyConfig.eventid)); + if (eventGroup.hasKey("IconName")) { + *iconName = eventGroup.readEntry("IconName", notifyConfig.appname); + } else { + *iconName = globalgroup.readEntry("IconName", notifyConfig.appname); + } +} + +bool NotifyByFlatpakPrivate::sendNotificationToPortal(KNotification *notification, const KNotifyConfig ¬ifyConfig_nocheck) +{ + QDBusMessage dbusNotificationMessage; + dbusNotificationMessage = QDBusMessage::createMethodCall(portalDbusServiceName, + portalDbusPath, + portalDbusInterfaceName, + QStringLiteral("AddNotification")); + + QVariantList args; + // Will be used only with flatpak portal + QVariantMap portalArgs; + + QString appCaption; + QString iconName; + getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName); + + //did the user override the icon name? + if (!notification->iconName().isEmpty()) { + iconName = notification->iconName(); + } + + QString title = notification->title().isEmpty() ? appCaption : notification->title(); + QString text = notification->text(); + + // galago spec defines action list to be list like + // (act_id1, action1, act_id2, action2, ...) + // + // assign id's to actions like it's done in fillPopup() method + // (i.e. starting from 1) + QList buttons; + buttons.reserve(notification->actions().count()); + + int actId = 0; + Q_FOREACH (const QString &actionName, notification->actions()) { + actId++; + QVariantMap button = { + {QStringLiteral("action"), QString::number(actId)}, + {QStringLiteral("label"), actionName} + }; + buttons << button; + } + + qDBusRegisterMetaType >(); + portalArgs.insert(QStringLiteral("icon"), iconName); + portalArgs.insert(QStringLiteral("title"), title); + portalArgs.insert(QStringLiteral("body"), text); + portalArgs.insert(QStringLiteral("buttons"), QVariant::fromValue >(buttons)); + + args.append(QString::number(nextId)); + args.append(portalArgs); + + dbusNotificationMessage.setArguments(args); + + QDBusPendingCall notificationCall = QDBusConnection::sessionBus().asyncCall(dbusNotificationMessage, -1); + + // If we are in sandbox we don't need to wait for returned notification id + flatpakNotifications.insert(nextId++, notification); + + return true; +} + +void NotifyByFlatpakPrivate::closePortalNotification(KNotification *notification) +{ + uint id = flatpakNotifications.key(notification, 0); + + qCDebug(LOG_KNOTIFICATIONS) << "ID: " << id; + + if (id == 0) { + qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id(); + return; + } + + QDBusMessage m = QDBusMessage::createMethodCall(portalDbusServiceName, + portalDbusPath, + portalDbusInterfaceName, + QStringLiteral("RemoveNotification")); + m.setArguments({QString::number(id)}); + + // send(..) does not block + bool queued = QDBusConnection::sessionBus().send(m); + + if (!queued) { + qCWarning(LOG_KNOTIFICATIONS) << "Failed to queue dbus message for closing a notification"; + } +}