diff --git a/libnotificationmanager/CMakeLists.txt b/libnotificationmanager/CMakeLists.txt --- a/libnotificationmanager/CMakeLists.txt +++ b/libnotificationmanager/CMakeLists.txt @@ -8,6 +8,7 @@ set(notificationmanager_LIB_SRCS server.cpp server_p.cpp + serverinfo.cpp settings.cpp mirroredscreenstracker.cpp notifications.cpp diff --git a/libnotificationmanager/declarative/notificationmanagerplugin.cpp b/libnotificationmanager/declarative/notificationmanagerplugin.cpp --- a/libnotificationmanager/declarative/notificationmanagerplugin.cpp +++ b/libnotificationmanager/declarative/notificationmanagerplugin.cpp @@ -23,6 +23,7 @@ #include "notifications.h" #include "job.h" #include "server.h" +#include "serverinfo.h" #include "settings.h" #include @@ -40,4 +41,5 @@ QQmlEngine::setObjectOwnership(&Server::self(), QQmlEngine::CppOwnership); return &Server::self(); }); + qmlRegisterUncreatableType(uri, 1, 0, "ServerInfo", QStringLiteral("Can only access ServerInfo via Server")); } diff --git a/libnotificationmanager/server.h b/libnotificationmanager/server.h --- a/libnotificationmanager/server.h +++ b/libnotificationmanager/server.h @@ -29,6 +29,7 @@ class Notification; +class ServerInfo; class ServerPrivate; /** @@ -40,6 +41,24 @@ { Q_OBJECT + /** + * Whether the notification service could be registered. + * Call @c init() to register. + */ + Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) + + /** + * Information about the current owner of the Notification service. + * + * This can be used to tell the user which application is currently + * owning the service in case service registration failed. + * + * This is never null, even if there is no notification service running. + * + * @since 5.18 + */ + Q_PROPERTY(NotificationManager::ServerInfo *currentOwner READ currentOwner CONSTANT) + /** * Whether notifications are currently inhibited. * @@ -77,6 +96,12 @@ */ bool isValid() const; + /** + * Information about the current owner of the Notification service. + * @since 5.18 + */ + ServerInfo *currentOwner() const; + /** * Whether notifications are currently inhibited. * @since 5.17 @@ -134,6 +159,14 @@ uint add(const Notification ¬ification); Q_SIGNALS: + /** + * Emitted when the notification service validity changes, + * because it sucessfully registered the service or lost + * ownership of it. + * @since 5.18 + */ + void validChanged(); + /** * Emitted when a notification was added. * This is emitted regardless of any filtering rules or user settings. diff --git a/libnotificationmanager/server.cpp b/libnotificationmanager/server.cpp --- a/libnotificationmanager/server.cpp +++ b/libnotificationmanager/server.cpp @@ -34,6 +34,7 @@ : QObject(parent) , d(new ServerPrivate(this)) { + connect(d.data(), &ServerPrivate::validChanged, this, &Server::validChanged); connect(d.data(), &ServerPrivate::inhibitedChanged, this, [this] { emit inhibitedChanged(inhibited()); }); @@ -62,6 +63,11 @@ return d->m_valid; } +ServerInfo *Server::currentOwner() const +{ + return d->currentOwner(); +} + void Server::closeNotification(uint notificationId, CloseReason reason) { emit notificationRemoved(notificationId, reason); diff --git a/libnotificationmanager/server_p.h b/libnotificationmanager/server_p.h --- a/libnotificationmanager/server_p.h +++ b/libnotificationmanager/server_p.h @@ -39,6 +39,8 @@ namespace NotificationManager { +class ServerInfo; + class Q_DECL_HIDDEN ServerPrivate : public QObject, protected QDBusContext { Q_OBJECT @@ -71,17 +73,24 @@ void NotificationClosed(uint id, uint reason); void ActionInvoked(uint id, const QString &actionKey); + void validChanged(); + void inhibitedChanged(); void externalInhibitedChanged(); void externalInhibitionsChanged(); void serviceOwnershipLost(); public: // stuff used by public class + friend class ServerInfo; + static QString notificationServiceName(); + bool init(); uint add(const Notification ¬ification); + ServerInfo *currentOwner() const; + // Server only handles external application inhibitions but we still want the Inhibited property // expose the actual inhibition state for applications to check. void setInhibited(bool inhibited); @@ -97,7 +106,13 @@ void onBroadcastNotification(const QMap &properties); private: - void onServiceUnregistered(const QString &serviceName); + void onServiceOwnershipLost(const QString &serviceName); + void onInhibitionServiceUnregistered(const QString &serviceName); + void onInhibitedChanged(); // emit DBus change signal + + bool m_dbusObjectValid = false; + + mutable QScopedPointer m_currentOwner; QDBusServiceWatcher *m_inhibitionWatcher = nullptr; uint m_highestInhibitionCookie = 0; diff --git a/libnotificationmanager/server_p.cpp b/libnotificationmanager/server_p.cpp --- a/libnotificationmanager/server_p.cpp +++ b/libnotificationmanager/server_p.cpp @@ -28,6 +28,7 @@ #include "notification_p.h" #include "server.h" +#include "serverinfo.h" #include "utils_p.h" @@ -47,80 +48,79 @@ { m_inhibitionWatcher->setConnection(QDBusConnection::sessionBus()); m_inhibitionWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); - connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ServerPrivate::onServiceUnregistered); + connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ServerPrivate::onInhibitionServiceUnregistered); } ServerPrivate::~ServerPrivate() = default; +QString ServerPrivate::notificationServiceName() +{ + return QStringLiteral("org.freedesktop.Notifications"); +} + +ServerInfo *ServerPrivate::currentOwner() const +{ + if (!m_currentOwner) { + m_currentOwner.reset(new ServerInfo()); + } + + return m_currentOwner.data(); +} + bool ServerPrivate::init() { if (m_valid) { return true; } new NotificationsAdaptor(this); - if (!QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/freedesktop/Notifications"), this)) { + if (!m_dbusObjectValid) { // if already registered, don't fail here + m_dbusObjectValid = QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/freedesktop/Notifications"), this); + } + + if (!m_dbusObjectValid) { qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification DBus object"; return false; } // Only the "dbus master" (effectively plasmashell) should be the true owner of notifications const bool master = Utils::isDBusMaster(); - const QString notificationService = QStringLiteral("org.freedesktop.Notifications"); - QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface(); if (!master) { - connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, [=](const QString &serviceName) { - if (serviceName == notificationService) { - qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service"; - emit serviceOwnershipLost(); - } - }); + // NOTE this connects to whether the application lost ownership of given service + // This is not a wildcard listener for all unregistered services on the bus! + connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost, Qt::UniqueConnection); } - auto registration = dbusIface->registerService(notificationService, + auto registration = dbusIface->registerService(notificationServiceName(), master ? QDBusConnectionInterface::ReplaceExistingService : QDBusConnectionInterface::DontQueueService, master ? QDBusConnectionInterface::DontAllowReplacement : QDBusConnectionInterface::AllowReplacement ); if (registration.value() != QDBusConnectionInterface::ServiceRegistered) { qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification service on DBus"; return false; } - connect(this, &ServerPrivate::inhibitedChanged, this, [this] { - // emit DBus change signal... - QDBusMessage signal = QDBusMessage::createSignal( - QStringLiteral("/org/freedesktop/Notifications"), - QStringLiteral("org.freedesktop.DBus.Properties"), - QStringLiteral("PropertiesChanged") - ); - - signal.setArguments({ - QStringLiteral("org.freedesktop.Notifications"), - QVariantMap{ // updated - {QStringLiteral("Inhibited"), inhibited()}, - }, - QStringList() // invalidated - }); - - QDBusConnection::sessionBus().send(signal); - }); + connect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged, Qt::UniqueConnection); qCDebug(NOTIFICATIONMANAGER) << "Registered Notification service on DBus"; KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Notifications")); const bool broadcastsEnabled = config.readEntry("ListenForBroadcasts", false); if (broadcastsEnabled) { qCDebug(NOTIFICATIONMANAGER) << "Notification server is configured to listen for broadcasts"; + // NOTE Keep disconnect() call in onServiceOwnershipLost in sync if you change this! QDBusConnection::systemBus().connect({}, {}, QStringLiteral("org.kde.BroadcastNotifications"), QStringLiteral("Notify"), this, SLOT(onBroadcastNotification(QMap))); } m_valid = true; + emit validChanged(); + return true; } @@ -342,7 +342,28 @@ return m_highestInhibitionCookie; } -void ServerPrivate::onServiceUnregistered(const QString &serviceName) +void ServerPrivate::onServiceOwnershipLost(const QString &serviceName) +{ + if (serviceName != notificationServiceName()) { + return; + } + + qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service"; + + disconnect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceUnregistered, + this, &ServerPrivate::onServiceOwnershipLost); + disconnect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged); + + QDBusConnection::systemBus().disconnect({}, {}, QStringLiteral("org.kde.BroadcastNotifications"), + QStringLiteral("Notify"), this, SLOT(onBroadcastNotification(QMap))); + + m_valid = false; + + emit validChanged(); + emit serviceOwnershipLost(); +} + +void ServerPrivate::onInhibitionServiceUnregistered(const QString &serviceName) { qCDebug(NOTIFICATIONMANAGER) << "Inhibition service unregistered" << serviceName; @@ -356,6 +377,26 @@ UnInhibit(cookie); } +void ServerPrivate::onInhibitedChanged() +{ + // emit DBus change signal... + QDBusMessage signal = QDBusMessage::createSignal( + QStringLiteral("/org/freedesktop/Notifications"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + signal.setArguments({ + QStringLiteral("org.freedesktop.Notifications"), + QVariantMap{ // updated + {QStringLiteral("Inhibited"), inhibited()}, + }, + QStringList() // invalidated + }); + + QDBusConnection::sessionBus().send(signal); +} + void ServerPrivate::UnInhibit(uint cookie) { qCDebug(NOTIFICATIONMANAGER) << "Request release inhibition for cookie" << cookie; diff --git a/libnotificationmanager/serverinfo.h b/libnotificationmanager/serverinfo.h new file mode 100644 --- /dev/null +++ b/libnotificationmanager/serverinfo.h @@ -0,0 +1,83 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * 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 . + */ + +#pragma once + +#include "notificationmanager_export.h" + +#include +#include +#include + +namespace NotificationManager +{ + +/** + * @short Information about the notification server + * + * Provides information such as vendor, name, version of the notification server. + * + * @author Kai Uwe Broulik + **/ +class NOTIFICATIONMANAGER_EXPORT ServerInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + + Q_PROPERTY(QString vendor READ vendor NOTIFY vendorChanged) + + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + + Q_PROPERTY(QString version READ version NOTIFY versionChanged) + + Q_PROPERTY(QString specVersion READ specVersion NOTIFY specVersionChanged) + +public: + explicit ServerInfo(QObject *parent = nullptr); + ~ServerInfo() override; + + enum class Status { + Unknown = -1, + NotRunning, + Running + }; + Q_ENUM(Status) + + Status status() const; + QString vendor() const; + QString name() const; + QString version() const; + QString specVersion() const; + +Q_SIGNALS: + void statusChanged(Status status); + void vendorChanged(const QString &vendor); + void nameChanged(const QString &name); + void versionChanged(const QString &version); + void specVersionChanged(const QString &specVersion); + +private: + class Private; + QScopedPointer d; + +}; + +} // namespace NotificationManager diff --git a/libnotificationmanager/serverinfo.cpp b/libnotificationmanager/serverinfo.cpp new file mode 100644 --- /dev/null +++ b/libnotificationmanager/serverinfo.cpp @@ -0,0 +1,176 @@ +/* + * Copyright 2018 Kai Uwe Broulik + * + * 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 "serverinfo.h" + +#include "server_p.h" // for notificationServiceName + +#include "debug.h" + +#include +#include +#include +#include +#include + +using namespace NotificationManager; + +class Q_DECL_HIDDEN ServerInfo::Private +{ +public: + Private(ServerInfo *q); + ~Private(); + + void setStatus(ServerInfo::Status status); + void setServerInformation(const QString &vendor, + const QString &name, + const QString &version, + const QString &specVersion); + + void updateServerInformation(); + + ServerInfo *q; + + ServerInfo::Status status = ServerInfo::Status::Unknown; + + QString vendor; + QString name; + QString version; + QString specVersion; +}; + +ServerInfo::Private::Private(ServerInfo *q) + : q(q) +{ + +} + +ServerInfo::Private::~Private() = default; + +void ServerInfo::Private::setStatus(ServerInfo::Status status) +{ + if (this->status != status) { + this->status = status; + emit q->statusChanged(status); + } +} + +void ServerInfo::Private::setServerInformation(const QString &vendor, + const QString &name, + const QString &version, + const QString &specVersion) +{ + if (this->vendor != vendor) { + this->vendor = vendor; + emit q->vendorChanged(vendor); + } + if (this->name != name) { + this->name = name; + emit q->nameChanged(name); + } + if (this->version != version) { + this->version = version; + emit q->versionChanged(version); + } + if (this->specVersion != specVersion) { + this->specVersion = specVersion; + emit q->specVersionChanged(specVersion); + } +} + +void ServerInfo::Private::updateServerInformation() +{ + // Check whether the service is running to avoid DBus-activating plasma_waitforname and getting stuck there. + if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerPrivate::notificationServiceName())) { + setStatus(ServerInfo::Status::NotRunning); + setServerInformation({}, {}, {}, {}); + return; + } + + QDBusMessage msg = QDBusMessage::createMethodCall(ServerPrivate::notificationServiceName(), + QStringLiteral("/org/freedesktop/Notifications"), + QStringLiteral("org.freedesktop.Notifications"), + QStringLiteral("GetServerInformation")); + auto call = QDBusConnection::sessionBus().asyncCall(msg); + + auto *watcher = new QDBusPendingCallWatcher(call, q); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + watcher->deleteLater(); + + if (reply.isError()) { + qCWarning(NOTIFICATIONMANAGER) << "Failed to determine notification server information" << reply.error().message(); + // Should this still be "Running" as technically it is? + // But if it is not even responding to this properly, who knows what it'll to with an actual notification + setStatus(Status::Unknown); + setServerInformation({}, {}, {}, {}); + return; + } + + const QString name = reply.argumentAt(0).toString(); + const QString vendor = reply.argumentAt(1).toString(); + const QString version = reply.argumentAt(2).toString(); + const QString specVersion = reply.argumentAt(3).toString(); + + setServerInformation(vendor, name, version, specVersion); + setStatus(ServerInfo::Status::Running); + }); +} + +ServerInfo::ServerInfo(QObject *parent) + : QObject(parent) + , d(new Private(this)) +{ + auto *watcher = new QDBusServiceWatcher(ServerPrivate::notificationServiceName(), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this]() { + d->updateServerInformation(); + }); + + d->updateServerInformation(); +} + +ServerInfo::~ServerInfo() = default; + +ServerInfo::Status ServerInfo::status() const +{ + return d->status; +} + +QString ServerInfo::vendor() const +{ + return d->vendor; +} + +QString ServerInfo::name() const +{ + return d->name; +} + +QString ServerInfo::version() const +{ + return d->version; +} + +QString ServerInfo::specVersion() const +{ + return d->specVersion; +}