diff --git a/libnotificationmanager/notification.cpp b/libnotificationmanager/notification.cpp index ad7042c60..1dd437f00 100644 --- a/libnotificationmanager/notification.cpp +++ b/libnotificationmanager/notification.cpp @@ -1,745 +1,755 @@ /* * Copyright 2008 Dmitry Suzdalev * Copyright 2017 David Edmundson * Copyright 2018-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 . */ #include "notification.h" #include "notification_p.h" #include "notifications.h" #include #include #include #include #include #include #include #include #include #include #include #include "debug.h" using namespace NotificationManager; Notification::Private::Private() { } Notification::Private::~Private() = default; QString Notification::Private::sanitize(const QString &text) { // replace all \ns with
QString t = text; t.replace(QLatin1String("\n"), QStringLiteral("
")); // Now remove all inner whitespace (\ns are already
s) t = t.simplified(); // Finally, check if we don't have multiple
s following, // can happen for example when "\n \n" is sent, this replaces // all
s in succsession with just one t.replace(QRegularExpression(QStringLiteral("
\\s*
(\\s|
)*")), QLatin1String("
")); // This fancy RegExp escapes every occurrence of & since QtQuick Text will blatantly cut off // text where it finds a stray ampersand. // Only &{apos, quot, gt, lt, amp}; as well as { character references will be allowed t.replace(QRegularExpression(QStringLiteral("&(?!(?:apos|quot|[gl]t|amp);|#)")), QLatin1String("&")); // Don't bother adding some HTML structure if the body is now empty if (t.isEmpty()) { return t; } QXmlStreamReader r(QStringLiteral("") + t + QStringLiteral("")); QString result; QXmlStreamWriter out(&result); const QVector allowedTags = {"b", "i", "u", "img", "a", "html", "br", "table", "tr", "td"}; out.writeStartDocument(); while (!r.atEnd()) { r.readNext(); if (r.tokenType() == QXmlStreamReader::StartElement) { const QString name = r.name().toString(); if (!allowedTags.contains(name)) { continue; } out.writeStartElement(name); if (name == QLatin1String("img")) { auto src = r.attributes().value("src").toString(); auto alt = r.attributes().value("alt").toString(); const QUrl url(src); if (url.isLocalFile()) { out.writeAttribute(QStringLiteral("src"), src); } else { //image denied for security reasons! Do not copy the image src here! } out.writeAttribute(QStringLiteral("alt"), alt); } if (name == QLatin1Char('a')) { out.writeAttribute(QStringLiteral("href"), r.attributes().value("href").toString()); } } if (r.tokenType() == QXmlStreamReader::EndElement) { const QString name = r.name().toString(); if (!allowedTags.contains(name)) { continue; } out.writeEndElement(); } if (r.tokenType() == QXmlStreamReader::Characters) { const auto text = r.text().toString(); out.writeCharacters(text); //this auto escapes chars -> HTML entities } } out.writeEndDocument(); if (r.hasError()) { qCWarning(NOTIFICATIONMANAGER) << "Notification to send to backend contains invalid XML: " << r.errorString() << "line" << r.lineNumber() << "col" << r.columnNumber(); } // The Text.StyledText format handles only html3.2 stuff and ' is html4 stuff // so we need to replace it here otherwise it will not render at all. result.replace(QLatin1String("'"), QChar('\'')); return result; } QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgument &arg) { int width, height, rowStride, hasAlpha, bitsPerSample, channels; QByteArray pixels; char* ptr; char* end; arg.beginStructure(); arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels; arg.endStructure(); #define SANITY_CHECK(condition) \ if (!(condition)) { \ qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \ return QImage(); \ } SANITY_CHECK(width > 0); SANITY_CHECK(width < 2048); SANITY_CHECK(height > 0); SANITY_CHECK(height < 2048); SANITY_CHECK(rowStride > 0); #undef SANITY_CHECK auto copyLineRGB32 = [](QRgb* dst, const char* src, int width) { const char* end = src + width * 3; for (; src != end; ++dst, src+=3) { *dst = qRgb(src[0], src[1], src[2]); } }; auto copyLineARGB32 = [](QRgb* dst, const char* src, int width) { const char* end = src + width * 4; for (; src != end; ++dst, src+=4) { *dst = qRgba(src[0], src[1], src[2], src[3]); } }; QImage::Format format = QImage::Format_Invalid; void (*fcn)(QRgb*, const char*, int) = nullptr; if (bitsPerSample == 8) { if (channels == 4) { format = QImage::Format_ARGB32; fcn = copyLineARGB32; } else if (channels == 3) { format = QImage::Format_RGB32; fcn = copyLineRGB32; } } if (format == QImage::Format_Invalid) { qCWarning(NOTIFICATIONMANAGER) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")"; return QImage(); } QImage image(width, height, format); ptr = pixels.data(); end = ptr + pixels.length(); for (int y=0; y end) { qCWarning(NOTIFICATIONMANAGER) << "Image data is incomplete. y:" << y << "height:" << height; break; } fcn((QRgb*)image.scanLine(y), ptr, width); } return image; } void Notification::Private::sanitizeImage(QImage &image) { if (image.isNull()) { return; } const QSize max = maximumImageSize(); if (image.size().width() > max.width() || image.size().height() > max.height()) { image = image.scaled(max, Qt::KeepAspectRatio, Qt::SmoothTransformation); } } void Notification::Private::loadImagePath(const QString &path) { // image_path and appIcon should either be a URL with file scheme or the name of a themed icon. // We're lenient and also allow local paths. image = QImage(); // clear icon.clear(); QUrl imageUrl; if (path.startsWith(QLatin1Char('/'))) { imageUrl = QUrl::fromLocalFile(path); } else if (path.contains(QLatin1Char('/'))) { // bad heuristic to detect a URL imageUrl = QUrl(path); if (!imageUrl.isLocalFile()) { qCDebug(NOTIFICATIONMANAGER) << "Refused to load image from" << path << "which isn't a valid local location."; return; } } if (!imageUrl.isValid()) { // try icon path instead; icon = path; return; } QImageReader reader(imageUrl.toLocalFile()); reader.setAutoTransform(true); const QSize imageSize = reader.size(); if (imageSize.isValid() && (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height())) { const QSize thumbnailSize = imageSize.scaled(maximumImageSize(), Qt::KeepAspectRatio); reader.setScaledSize(thumbnailSize); } image = reader.read(); } QString Notification::Private::defaultComponentName() { // NOTE Keep in sync with KNotification return QStringLiteral("plasma_workspace"); } QSize Notification::Private::maximumImageSize() { return QSize(256, 256); } KService::Ptr Notification::Private::serviceForDesktopEntry(const QString &desktopEntry) { KService::Ptr service; if (desktopEntry.startsWith(QLatin1Char('/'))) { service = KService::serviceByDesktopPath(desktopEntry); } else { service = KService::serviceByDesktopName(desktopEntry); } if (!service) { const QString lowerDesktopEntry = desktopEntry.toLower(); service = KService::serviceByDesktopName(lowerDesktopEntry); } // Try if it's a renamed flatpak if (!service) { const QString desktopId = desktopEntry + QLatin1String(".desktop"); // HACK Querying for XDG lists in KServiceTypeTrader does not work, do it manually const auto services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and exist [X-Flatpak-RenamedFrom]")); for (auto it = services.constBegin(); it != services.constEnd() && !service; ++it) { const QVariant renamedFrom = (*it)->property(QStringLiteral("X-Flatpak-RenamedFrom"), QVariant::String); const auto names = renamedFrom.toString().split(QChar(';')); for (const QString &name : names) { if (name == desktopId) { service = *it; break; } } } } return service; } void Notification::Private::setDesktopEntry(const QString &desktopEntry) { QString serviceName; configurableService = false; KService::Ptr service = serviceForDesktopEntry(desktopEntry); if (service) { this->desktopEntry = service->desktopEntryName(); serviceName = service->name(); applicationIconName = service->icon(); configurableService = !service->noDisplay(); } const bool isDefaultEvent = (notifyRcName == defaultComponentName()); configurableNotifyRc = false; if (!notifyRcName.isEmpty()) { // Check whether the application actually has notifications we can configure KConfig config(notifyRcName + QStringLiteral(".notifyrc"), KConfig::NoGlobals); config.addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/") + notifyRcName + QStringLiteral(".notifyrc"))); KConfigGroup globalGroup(&config, "Global"); const QString iconName = globalGroup.readEntry("IconName"); // For default events we try to show the application name from the desktop entry if possible // This will have us show e.g. "Dr Konqi" instead of generic "Plasma Desktop" if (isDefaultEvent && !serviceName.isEmpty()) { applicationName = serviceName; } // also only overwrite application icon name for non-default events (or if we don't have a service icon) if (!iconName.isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) { applicationIconName = iconName; } const QRegularExpression regexp(QStringLiteral("^Event/([^/]*)$")); configurableNotifyRc = !config.groupList().filter(regexp).isEmpty(); } } void Notification::Private::processHints(const QVariantMap &hints) { auto end = hints.end(); notifyRcName = hints.value(QStringLiteral("x-kde-appname")).toString(); setDesktopEntry(hints.value(QStringLiteral("desktop-entry")).toString()); // Special override for KDE Connect since the notification is sent by kdeconnectd // but actually comes from a different app on the phone const QString applicationDisplayName = hints.value(QStringLiteral("x-kde-display-appname")).toString(); if (!applicationDisplayName.isEmpty()) { applicationName = applicationDisplayName; } originName = hints.value(QStringLiteral("x-kde-origin-name")).toString(); eventId = hints.value(QStringLiteral("x-kde-eventId")).toString(); bool ok; const int urgency = hints.value(QStringLiteral("urgency")).toInt(&ok); // DBus type is actually "byte" if (ok) { // FIXME use separate enum again switch (urgency) { case 0: setUrgency(Notifications::LowUrgency); break; case 1: setUrgency(Notifications::NormalUrgency); break; case 2: setUrgency(Notifications::CriticalUrgency); break; } } userActionFeedback = hints.value(QStringLiteral("x-kde-user-action-feedback")).toBool(); if (userActionFeedback) { // A confirmation of an explicit user interaction is assumed to have been seen by the user. read = true; } urls = QUrl::fromStringList(hints.value(QStringLiteral("x-kde-urls")).toStringList()); replyPlaceholderText = hints.value(QStringLiteral("x-kde-reply-placeholder-text")).toString(); replySubmitButtonText = hints.value(QStringLiteral("x-kde-reply-submit-button-text")).toString(); replySubmitButtonIconName = hints.value(QStringLiteral("x-kde-reply-submit-button-icon-name")).toString(); // Underscored hints was in use in version 1.1 of the spec but has been // replaced by dashed hints in version 1.2. We need to support it for // users of the 1.2 version of the spec. auto it = hints.find(QStringLiteral("image-data")); if (it == end) { it = hints.find(QStringLiteral("image_data")); } if (it == end) { // This hint was in use in version 1.0 of the spec but has been // replaced by "image_data" in version 1.1. We need to support it for // users of the 1.0 version of the spec. it = hints.find(QStringLiteral("icon_data")); } if (it != end) { image = decodeNotificationSpecImageHint(it->value()); } if (image.isNull()) { it = hints.find(QStringLiteral("image-path")); if (it == end) { it = hints.find(QStringLiteral("image_path")); } if (it != end) { loadImagePath(it->toString()); } } sanitizeImage(image); } void Notification::Private::setUrgency(Notifications::Urgency urgency) { this->urgency = urgency; // Critical notifications must not time out // TODO should we really imply this here and not on the view side? // are there usecases for critical but can expire? // "critical updates available"? if (urgency == Notifications::CriticalUrgency) { timeout = 0; } } Notification::Notification(uint id) : d(new Private()) { d->id = id; d->created = QDateTime::currentDateTimeUtc(); } Notification::Notification(const Notification &other) : d(new Private(*other.d)) { } Notification::Notification(Notification &&other) : d(other.d) { other.d = nullptr; } Notification &Notification::operator=(const Notification &other) { d = new Private(*other.d); return *this; } Notification &Notification::operator=(Notification &&other) { d = other.d; other.d = nullptr; return *this; } Notification::~Notification() { delete d; } uint Notification::id() const { return d->id; } +QString Notification::dBusService() const +{ + return d->dBusService; +} + +void Notification::setDBusService(const QString &dBusService) +{ + d->dBusService = dBusService; +} + QDateTime Notification::created() const { return d->created; } QDateTime Notification::updated() const { return d->updated; } void Notification::resetUpdated() { d->updated = QDateTime::currentDateTimeUtc(); } bool Notification::read() const { return d->read; } void Notification::setRead(bool read) { d->read = read; } QString Notification::summary() const { return d->summary; } void Notification::setSummary(const QString &summary) { d->summary = summary; } QString Notification::body() const { return d->body; } void Notification::setBody(const QString &body) { d->body = Private::sanitize(body.trimmed()); } QString Notification::icon() const { return d->icon; } void Notification::setIcon(const QString &icon) { d->loadImagePath(icon); Private::sanitizeImage(d->image); } QImage Notification::image() const { return d->image; } void Notification::setImage(const QImage &image) { d->image = image; } QString Notification::desktopEntry() const { return d->desktopEntry; } void Notification::setDesktopEntry(const QString &desktopEntry) { d->setDesktopEntry(desktopEntry); } QString Notification::notifyRcName() const { return d->notifyRcName; } QString Notification::eventId() const { return d->eventId; } QString Notification::applicationName() const { return d->applicationName; } void Notification::setApplicationName(const QString &applicationName) { d->applicationName = applicationName; } QString Notification::applicationIconName() const { return d->applicationIconName; } void Notification::setApplicationIconName(const QString &applicationIconName) { d->applicationIconName = applicationIconName; } QString Notification::originName() const { return d->originName; } QStringList Notification::actionNames() const { return d->actionNames; } QStringList Notification::actionLabels() const { return d->actionLabels; } bool Notification::hasDefaultAction() const { return d->hasDefaultAction; } QString Notification::defaultActionLabel() const { return d->defaultActionLabel; } void Notification::setActions(const QStringList &actions) { if (actions.count() % 2 != 0) { qCWarning(NOTIFICATIONMANAGER) << "List of actions must contain an even number of items, tried to set actions to" << actions; return; } d->hasDefaultAction = false; d->hasConfigureAction = false; d->hasReplyAction = false; QStringList names; QStringList labels; for (int i = 0; i < actions.count(); i += 2) { const QString &name = actions.at(i); const QString &label = actions.at(i + 1); if (!d->hasDefaultAction && name == QLatin1String("default")) { d->hasDefaultAction = true; d->defaultActionLabel = label; continue; } if (!d->hasConfigureAction && name == QLatin1String("settings")) { d->hasConfigureAction = true; d->configureActionLabel = label; continue; } if (!d->hasReplyAction && name == QLatin1String("inline-reply")) { d->hasReplyAction = true; d->replyActionLabel = label; continue; } names << name; labels << label; } d->actionNames = names; d->actionLabels = labels; } QList Notification::urls() const { return d->urls; } void Notification::setUrls(const QList &urls) { d->urls = urls; } Notifications::Urgency Notification::urgency() const { return d->urgency; } bool Notification::userActionFeedback() const { return d->userActionFeedback; } int Notification::timeout() const { return d->timeout; } void Notification::setTimeout(int timeout) { d->timeout = timeout; } bool Notification::configurable() const { return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService; } QString Notification::configureActionLabel() const { return d->configureActionLabel; } bool Notification::hasReplyAction() const { return d->hasReplyAction; } QString Notification::replyActionLabel() const { return d->replyActionLabel; } QString Notification::replyPlaceholderText() const { return d->replyPlaceholderText; } QString Notification::replySubmitButtonText() const { return d->replySubmitButtonText; } QString Notification::replySubmitButtonIconName() const { return d->replySubmitButtonIconName; } bool Notification::expired() const { return d->expired; } void Notification::setExpired(bool expired) { d->expired = expired; } bool Notification::dismissed() const { return d->dismissed; } void Notification::setDismissed(bool dismissed) { d->dismissed = dismissed; } void Notification::processHints(const QVariantMap &hints) { d->processHints(hints); } diff --git a/libnotificationmanager/notification.h b/libnotificationmanager/notification.h index 36da3afc1..9403f0743 100644 --- a/libnotificationmanager/notification.h +++ b/libnotificationmanager/notification.h @@ -1,135 +1,138 @@ /* * Copyright 2018-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 #include #include #include #include #include "notifications.h" #include "notificationmanager_export.h" namespace NotificationManager { /** * @short Represents a single notification * * @author Kai Uwe Broulik **/ class NOTIFICATIONMANAGER_EXPORT Notification { public: explicit Notification(uint id = 0); Notification(const Notification &other); Notification(Notification &&other) Q_DECL_NOEXCEPT; Notification &operator=(const Notification &other); Notification &operator=(Notification &&other) Q_DECL_NOEXCEPT; virtual ~Notification(); uint id() const; + QString dBusService() const; + void setDBusService(const QString &dBusService); + QDateTime created() const; QDateTime updated() const; void resetUpdated(); bool read() const; void setRead(bool read); QString summary() const; void setSummary(const QString &summary); QString body() const; void setBody(const QString &body); QString icon() const; void setIcon(const QString &icon); QImage image() const; void setImage(const QImage &image); QString desktopEntry() const; void setDesktopEntry(const QString &desktopEntry); QString notifyRcName() const; QString eventId() const; QString applicationName() const; void setApplicationName(const QString &applicationName); QString applicationIconName() const; void setApplicationIconName(const QString &applicationIconName); QString originName() const; // should we group the two into a QPair or something? QStringList actionNames() const; QStringList actionLabels() const; bool hasDefaultAction() const; QString defaultActionLabel() const; void setActions(const QStringList &actions); QList urls() const; void setUrls(const QList &urls); // FIXME use separate enum again Notifications::Urgency urgency() const; void setUrgency(Notifications::Urgency urgency); bool userActionFeedback() const; int timeout() const; void setTimeout(int timeout); bool configurable() const; QString configureActionLabel() const; bool hasReplyAction() const; QString replyActionLabel() const; QString replyPlaceholderText() const; QString replySubmitButtonText() const; QString replySubmitButtonIconName() const; bool expired() const; void setExpired(bool expired); bool dismissed() const; void setDismissed(bool dismissed); void processHints(const QVariantMap &hints); private: friend class NotificationsModel; friend class ServerPrivate; class Private; Private *d; }; } // namespace NotificationManager diff --git a/libnotificationmanager/notification_p.h b/libnotificationmanager/notification_p.h index 7d812d39f..92165f316 100644 --- a/libnotificationmanager/notification_p.h +++ b/libnotificationmanager/notification_p.h @@ -1,107 +1,109 @@ /* * Copyright 2018-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 #include #include #include #include #include #include #include #include "notifications.h" namespace NotificationManager { class Q_DECL_HIDDEN Notification::Private { public: Private(); ~Private(); static QString sanitize(const QString &text); static QImage decodeNotificationSpecImageHint(const QDBusArgument &arg); static void sanitizeImage(QImage &image); void loadImagePath(const QString &path); static QString defaultComponentName(); static QSize maximumImageSize(); static KService::Ptr serviceForDesktopEntry(const QString &desktopEntry); void setDesktopEntry(const QString &desktopEntry); void processHints(const QVariantMap &hints); void setUrgency(Notifications::Urgency urgency); uint id = 0; + // Bus name of the creator/sender + QString dBusService; QDateTime created; QDateTime updated; bool read = false; QString summary; QString body; // Can be theme icon name or path QString icon; QImage image; QString applicationName; QString desktopEntry; bool configurableService = false; QString serviceName; // "Name" field in KService from desktopEntry QString applicationIconName; QString originName; QStringList actionNames; QStringList actionLabels; bool hasDefaultAction = false; QString defaultActionLabel; bool hasConfigureAction = false; QString configureActionLabel; bool configurableNotifyRc = false; QString notifyRcName; QString eventId; bool hasReplyAction = false; QString replyActionLabel; QString replyPlaceholderText; QString replySubmitButtonText; QString replySubmitButtonIconName; QList urls; bool userActionFeedback = false; Notifications::Urgency urgency = Notifications::NormalUrgency; int timeout = -1; bool expired = false; bool dismissed = false; }; } // namespace NotificationManager diff --git a/libnotificationmanager/notificationsmodel.cpp b/libnotificationmanager/notificationsmodel.cpp index 3ae1a0835..fa46e2774 100644 --- a/libnotificationmanager/notificationsmodel.cpp +++ b/libnotificationmanager/notificationsmodel.cpp @@ -1,530 +1,530 @@ /* * Copyright 2018-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 . */ #include "notificationsmodel.h" #include "debug.h" #include "server.h" #include "notifications.h" #include "notification.h" #include "notification_p.h" #include #include #include #include #include #include static const int s_notificationsLimit = 1000; using namespace NotificationManager; class Q_DECL_HIDDEN NotificationsModel::Private { public: explicit Private(NotificationsModel *q); ~Private(); void onNotificationAdded(const Notification ¬ification); void onNotificationReplaced(uint replacedId, const Notification ¬ification); void onNotificationRemoved(uint notificationId, Server::CloseReason reason); void setupNotificationTimeout(const Notification ¬ification); int rowOfNotification(uint id) const; NotificationsModel *q; QVector notifications; // Fallback timeout to ensure all notifications expire eventually // otherwise when it isn't shown to the user and doesn't expire // an app might wait indefinitely for the notification to do so QHash notificationTimeouts; QDateTime lastRead; }; NotificationsModel::Private::Private(NotificationsModel *q) : q(q) , lastRead(QDateTime::currentDateTimeUtc()) { } NotificationsModel::Private::~Private() { qDeleteAll(notificationTimeouts); notificationTimeouts.clear(); } void NotificationsModel::Private::onNotificationAdded(const Notification ¬ification) { // Once we reach a certain insane number of notifications discard some old ones // as we keep pixmaps around etc if (notifications.count() >= s_notificationsLimit) { const int cleanupCount = s_notificationsLimit / 2; qCDebug(NOTIFICATIONMANAGER) << "Reached the notification limit of" << s_notificationsLimit << ", discarding the oldest" << cleanupCount << "notifications"; q->beginRemoveRows(QModelIndex(), 0, cleanupCount - 1); for (int i = 0 ; i < cleanupCount; ++i) { notifications.removeAt(0); // TODO close gracefully? } q->endRemoveRows(); } setupNotificationTimeout(notification); q->beginInsertRows(QModelIndex(), notifications.count(), notifications.count()); notifications.append(std::move(notification)); q->endInsertRows(); } void NotificationsModel::Private::onNotificationReplaced(uint replacedId, const Notification ¬ification) { const int row = rowOfNotification(replacedId); if (row == -1) { qCWarning(NOTIFICATIONMANAGER) << "Trying to replace notification with id" << replacedId << "which doesn't exist, creating a new one. This is an application bug!"; onNotificationAdded(notification); return; } setupNotificationTimeout(notification); notifications[row] = notification; const QModelIndex idx = q->index(row, 0); emit q->dataChanged(idx, idx); } void NotificationsModel::Private::onNotificationRemoved(uint removedId, Server::CloseReason reason) { const int row = rowOfNotification(removedId); if (row == -1) { return; } q->stopTimeout(removedId); // When a notification expired, keep it around in the history and mark it as such if (reason == Server::CloseReason::Expired) { const QModelIndex idx = q->index(row, 0); Notification ¬ification = notifications[row]; notification.setExpired(true); // Since the notification is "closed" it cannot have any actions // unless it is "resident" which we don't support notification.setActions(QStringList()); emit q->dataChanged(idx, idx, { Notifications::ExpiredRole, // TODO only emit those if actually changed? Notifications::ActionNamesRole, Notifications::ActionLabelsRole, Notifications::HasDefaultActionRole, Notifications::DefaultActionLabelRole, Notifications::ConfigurableRole }); return; } // Otherwise if explicitly closed by either user or app, remove it q->beginRemoveRows(QModelIndex(), row, row); notifications.removeAt(row); q->endRemoveRows(); } void NotificationsModel::Private::setupNotificationTimeout(const Notification ¬ification) { if (notification.timeout() == 0) { // In case it got replaced by a persistent notification q->stopTimeout(notification.id()); return; } QTimer *timer = notificationTimeouts.value(notification.id()); if (!timer) { timer = new QTimer(); timer->setSingleShot(true); connect(timer, &QTimer::timeout, q, [this, timer] { const uint id = timer->property("notificationId").toUInt(); q->expire(id); }); notificationTimeouts.insert(notification.id(), timer); } timer->stop(); timer->setProperty("notificationId", notification.id()); timer->setInterval(60000 /*1min*/ + (notification.timeout() == -1 ? 120000 /*2min, max configurable default timeout*/ : notification.timeout())); timer->start(); } int NotificationsModel::Private::rowOfNotification(uint id) const { auto it = std::find_if(notifications.constBegin(), notifications.constEnd(), [id](const Notification &item) { return item.id() == id; }); if (it == notifications.constEnd()) { return -1; } return std::distance(notifications.constBegin(), it); } NotificationsModel::NotificationsModel() : QAbstractListModel(nullptr) , d(new Private(this)) { connect(&Server::self(), &Server::notificationAdded, this, [this](const Notification ¬ification) { d->onNotificationAdded(notification); }); connect(&Server::self(), &Server::notificationReplaced, this, [this](uint replacedId, const Notification ¬ification) { d->onNotificationReplaced(replacedId, notification); }); connect(&Server::self(), &Server::notificationRemoved, this, [this](uint removedId, Server::CloseReason reason) { d->onNotificationRemoved(removedId, reason); }); connect(&Server::self(), &Server::serviceOwnershipLost, this, [this] { // Expire all notifications as we're defunct now const auto notifications = d->notifications; for (const Notification ¬ification : notifications) { if (!notification.expired()) { d->onNotificationRemoved(notification.id(), Server::CloseReason::Expired); } } }); Server::self().init(); } NotificationsModel::~NotificationsModel() = default; NotificationsModel::Ptr NotificationsModel::createNotificationsModel() { static QWeakPointer s_instance; if (!s_instance) { QSharedPointer ptr(new NotificationsModel()); s_instance = ptr.toWeakRef(); return ptr; } return s_instance.toStrongRef(); } QDateTime NotificationsModel::lastRead() const { return d->lastRead; } void NotificationsModel::setLastRead(const QDateTime &lastRead) { if (d->lastRead != lastRead) { d->lastRead = lastRead; emit lastReadChanged(); } } QVariant NotificationsModel::data(const QModelIndex &index, int role) const { if (!checkIndex(index)) { return QVariant(); } const Notification ¬ification = d->notifications.at(index.row()); switch (role) { case Notifications::IdRole: return notification.id(); case Notifications::TypeRole: return Notifications::NotificationType; case Notifications::CreatedRole: if (notification.created().isValid()) { return notification.created(); } break; case Notifications::UpdatedRole: if (notification.updated().isValid()) { return notification.updated(); } break; case Notifications::SummaryRole: return notification.summary(); case Notifications::BodyRole: return notification.body(); case Notifications::IconNameRole: if (notification.image().isNull()) { return notification.icon(); } break; case Notifications::ImageRole: if (!notification.image().isNull()) { return notification.image(); } break; case Notifications::DesktopEntryRole: return notification.desktopEntry(); case Notifications::NotifyRcNameRole: return notification.notifyRcName(); case Notifications::ApplicationNameRole: return notification.applicationName(); case Notifications::ApplicationIconNameRole: return notification.applicationIconName(); case Notifications::OriginNameRole: return notification.originName(); case Notifications::ActionNamesRole: return notification.actionNames(); case Notifications::ActionLabelsRole: return notification.actionLabels(); case Notifications::HasDefaultActionRole: return notification.hasDefaultAction(); case Notifications::DefaultActionLabelRole: return notification.defaultActionLabel(); case Notifications::UrlsRole: return QVariant::fromValue(notification.urls()); case Notifications::UrgencyRole: return static_cast(notification.urgency()); case Notifications::UserActionFeedbackRole: return notification.userActionFeedback(); case Notifications::TimeoutRole: return notification.timeout(); case Notifications::ClosableRole: return true; case Notifications::ConfigurableRole: return notification.configurable(); case Notifications::ConfigureActionLabelRole: return notification.configureActionLabel(); case Notifications::ExpiredRole: return notification.expired(); case Notifications::ReadRole: return notification.read(); case Notifications::HasReplyActionRole: return notification.hasReplyAction(); case Notifications::ReplyActionLabelRole: return notification.replyActionLabel(); case Notifications::ReplyPlaceholderTextRole: return notification.replyPlaceholderText(); case Notifications::ReplySubmitButtonTextRole: return notification.replySubmitButtonText(); case Notifications::ReplySubmitButtonIconNameRole: return notification.replySubmitButtonIconName(); } return QVariant(); } bool NotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!checkIndex(index)) { return false; } Notification ¬ification = d->notifications[index.row()]; switch (role) { case Notifications::ReadRole: if (value.toBool() != notification.read()) { notification.setRead(value.toBool()); return true; } break; } return false; } int NotificationsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return d->notifications.count(); } void NotificationsModel::expire(uint notificationId) { if (d->rowOfNotification(notificationId) > -1) { Server::self().closeNotification(notificationId, Server::CloseReason::Expired); } } void NotificationsModel::close(uint notificationId) { if (d->rowOfNotification(notificationId) > -1) { Server::self().closeNotification(notificationId, Server::CloseReason::DismissedByUser); } } void NotificationsModel::configure(uint notificationId) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (notification.d->hasConfigureAction) { Server::self().invokeAction(notificationId, QStringLiteral("settings")); // FIXME make a static Notification::configureActionName() or something return; } if (!notification.desktopEntry().isEmpty() || !notification.notifyRcName().isEmpty()) { configure(notification.desktopEntry(), notification.notifyRcName(), notification.eventId()); return; } qCWarning(NOTIFICATIONMANAGER) << "Trying to configure notification" << notificationId << "which isn't configurable"; } void NotificationsModel::configure(const QString &desktopEntry, const QString ¬ifyRcName, const QString &eventId) { // TODO would be nice to just have a signal but since NotificationsModel is shared, // if we connect to this from Notifications you would get a signal in every instance // and potentialy open the config dialog multiple times. QStringList args; if (!desktopEntry.isEmpty()) { args.append(QStringLiteral("--desktop-entry")); args.append(desktopEntry); } if (!notifyRcName.isEmpty()) { args.append(QStringLiteral("--notifyrc")); args.append(notifyRcName); } if (!eventId.isEmpty()) { args.append(QStringLiteral("--event-id")); args.append(eventId); } QProcess::startDetached(QStringLiteral("kcmshell5"), { QStringLiteral("notifications"), QStringLiteral("--args"), KShell::joinArgs(args) }); } void NotificationsModel::invokeDefaultAction(uint notificationId) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.hasDefaultAction()) { qCWarning(NOTIFICATIONMANAGER) << "Trying to invoke default action on notification" << notificationId << "which doesn't have one"; return; } Server::self().invokeAction(notificationId, QStringLiteral("default")); // FIXME make a static Notification::defaultActionName() or something } void NotificationsModel::invokeAction(uint notificationId, const QString &actionName) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.actionNames().contains(actionName)) { qCWarning(NOTIFICATIONMANAGER) << "Trying to invoke action" << actionName << "on notification" << notificationId << "which it doesn't have"; return; } Server::self().invokeAction(notificationId, actionName); } void NotificationsModel::reply(uint notificationId, const QString &text) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.hasReplyAction()) { qCWarning(NOTIFICATIONMANAGER) << "Trying to reply to a notification which doesn't have a reply action"; return; } - Server::self().reply(notificationId, text); + Server::self().reply(notification.dBusService(), notificationId, text); } void NotificationsModel::startTimeout(uint notificationId) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.timeout() || notification.expired()) { return; } d->setupNotificationTimeout(notification); } void NotificationsModel::stopTimeout(uint notificationId) { delete d->notificationTimeouts.take(notificationId); } void NotificationsModel::clear(Notifications::ClearFlags flags) { if (d->notifications.isEmpty()) { return; } // Tries to remove a contiguous group if possible as the likely case is // you have n unread notifications at the end of the list, we don't want to // remove and signal each item individually QVector> clearQueue; QPair clearRange{-1, -1}; for (int i = d->notifications.count() - 1; i >= 0; --i) { const Notification ¬ification = d->notifications.at(i); bool clear = (flags.testFlag(Notifications::ClearExpired) && notification.expired()); if (clear) { if (clearRange.second == -1) { clearRange.second = i; } clearRange.first = i; } else { if (clearRange.first != -1) { clearQueue.append(clearRange); clearRange.first = -1; clearRange.second = -1; } } } if (clearRange.first != -1) { clearQueue.append(clearRange); clearRange.first = -1; clearRange.second = -1; } for (const auto &range : clearQueue) { beginRemoveRows(QModelIndex(), range.first, range.second); for (int i = range.second; i >= range.first; --i) { d->notifications.removeAt(i); } endRemoveRows(); } } diff --git a/libnotificationmanager/server.cpp b/libnotificationmanager/server.cpp index cce480712..df5fc33b1 100644 --- a/libnotificationmanager/server.cpp +++ b/libnotificationmanager/server.cpp @@ -1,133 +1,133 @@ /* * 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 "server.h" #include "server_p.h" #include "notification.h" #include "notification_p.h" #include "debug.h" #include using namespace NotificationManager; Server::Server(QObject *parent) : QObject(parent) , d(new ServerPrivate(this)) { connect(d.data(), &ServerPrivate::validChanged, this, &Server::validChanged); connect(d.data(), &ServerPrivate::inhibitedChanged, this, [this] { emit inhibitedChanged(inhibited()); }); connect(d.data(), &ServerPrivate::externalInhibitedChanged, this, [this] { emit inhibitedByApplicationChanged(inhibitedByApplication()); }); connect(d.data(), &ServerPrivate::externalInhibitionsChanged, this, &Server::inhibitionApplicationsChanged); connect(d.data(), &ServerPrivate::serviceOwnershipLost, this, &Server::serviceOwnershipLost); } Server::~Server() = default; Server &Server::self() { static Server s_self; return s_self; } bool Server::init() { return d->init(); } bool Server::isValid() const { return d->m_valid; } ServerInfo *Server::currentOwner() const { return d->currentOwner(); } void Server::closeNotification(uint notificationId, CloseReason reason) { emit notificationRemoved(notificationId, reason); emit d->NotificationClosed(notificationId, static_cast(reason)); // tell on DBus } void Server::invokeAction(uint notificationId, const QString &actionName) { emit d->ActionInvoked(notificationId, actionName); } -void Server::reply(uint notificationId, const QString &text) +void Server::reply(const QString &dbusService, uint notificationId, const QString &text) { - emit d->NotificationReplied(notificationId, text); + d->sendReplyText(dbusService, notificationId, text); } uint Server::add(const Notification ¬ification) { return d->add(notification); } bool Server::inhibited() const { return d->inhibited(); } void Server::setInhibited(bool inhibited) { d->setInhibited(inhibited); } bool Server::inhibitedByApplication() const { return d->externalInhibited(); } QStringList Server::inhibitionApplications() const { QStringList applications; const auto inhibitions = d->externalInhibitions(); applications.reserve(inhibitions.count()); for (const auto &inhibition : inhibitions) { applications.append(!inhibition.applicationName.isEmpty() ? inhibition.applicationName : inhibition.desktopEntry); } return applications; } QStringList Server::inhibitionReasons() const { QStringList reasons; const auto inhibitions = d->externalInhibitions(); reasons.reserve(inhibitions.count()); for (const auto &inhibition : inhibitions) { reasons.append(inhibition.reason); } return reasons; } void Server::clearInhibitions() { d->clearExternalInhibitions(); } diff --git a/libnotificationmanager/server.h b/libnotificationmanager/server.h index 7b8e40a3f..039fb310b 100644 --- a/libnotificationmanager/server.h +++ b/libnotificationmanager/server.h @@ -1,232 +1,233 @@ /* * 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 . */ #pragma once #include #include "notificationmanager_export.h" namespace NotificationManager { class Notification; class ServerInfo; class ServerPrivate; /** * @short A notification DBus server * * @author Kai Uwe Broulik **/ class NOTIFICATIONMANAGER_EXPORT Server : public QObject { 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. * * This is what is announced to other applicatons on the bus. * * @note This does not keep track of inhibitions on its own, * you need to calculate this yourself and update the property accordingly. */ Q_PROPERTY(bool inhibited READ inhibited WRITE setInhibited NOTIFY inhibitedChanged) public: ~Server() override; /** * The reason a notification was closed */ enum class CloseReason { Expired = 1, ///< The notification timed out DismissedByUser = 2, ///< The user explicitly closed or acknowledged the notification Revoked = 3 ///< The notification was revoked by the issuing app because it is no longer relevant }; Q_ENUM(CloseReason) static Server &self(); /** * Registers the Notification Service on DBus. * * @return true if it succeeded, false otherwise. */ bool init(); /** * Whether the notification service could be registered */ 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 */ bool inhibited() const; /** * Whether notifications are currently effectively inhibited. * * @note You need to keep track of inhibitions and call this * yourself when appropriate. * @since 5.17 */ void setInhibited(bool inhibited); /** * Whether an application requested to inhibit notifications. */ bool inhibitedByApplication() const; // should we return a struct or pair or something? QStringList inhibitionApplications() const; QStringList inhibitionReasons() const; /** * Remove all inhibitions. * * @note The applications are not explicitly informed about this. */ void clearInhibitions(); /** * Sends a notification closed event * * @param id The notification ID * @param reason The reason why it was closed */ void closeNotification(uint id, CloseReason reason); /** * Sends an action invocation request * * @param id The notification ID * @param actionName The name of the action, e.g. "Action 1", or "default" */ void invokeAction(uint id, const QString &actionName); /** * Sends a notification reply text * + * @param dbusService The bus name of the receiving application * @param id The notification ID * @param text The reply message text * @since 5.18 */ - void reply(uint id, const QString &text); + void reply(const QString &dbusService, uint id, const QString &text); /** * Adds a notification * * @note The notification isn't actually broadcast * but just emitted locally. * * @return the ID of the notification */ 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. * @param notification The notification */ void notificationAdded(const Notification ¬ification); /** * Emitted when a notification is supposed to be updated * This is emitted regardless of any filtering rules or user settings. * @param replacedId The ID of the notification it replaces * @param notification The new notification to use instead */ void notificationReplaced(uint replacedId, const Notification ¬ification); /** * Emitted when a notification got removed (closed) * @param id The notification ID * @param reason The reason why it was closed */ void notificationRemoved(uint id, CloseReason reason); /** * Emitted when the inhibited state changed. */ void inhibitedChanged(bool inhibited); /** * Emitted when inhibitions by application have been changed. * Becomes true as soon as there is one inhibition and becomes * false again when all inhibitions have been lifted. * @since 5.17 */ void inhibitedByApplicationChanged(bool inhibited); /** * Emitted when the list of applications holding a notification * inhibition changes. * Normally you would only want to listen do @c inhibitedChanged */ void inhibitionApplicationsChanged(); /** * Emitted when the ownership of the Notification DBus Service is lost. */ void serviceOwnershipLost(); private: explicit Server(QObject *parent = nullptr); Q_DISABLE_COPY(Server) // FIXME we also need to disable move and other stuff? QScopedPointer d; }; } // namespace NotificationManager diff --git a/libnotificationmanager/server_p.cpp b/libnotificationmanager/server_p.cpp index 3411d5382..b8751ace0 100644 --- a/libnotificationmanager/server_p.cpp +++ b/libnotificationmanager/server_p.cpp @@ -1,459 +1,484 @@ /* * Copyright 2018-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 . */ #include "server_p.h" #include "debug.h" #include "notificationsadaptor.h" #include "notification.h" #include "notification_p.h" #include "server.h" #include "serverinfo.h" #include "utils_p.h" #include #include #include #include #include #include using namespace NotificationManager; ServerPrivate::ServerPrivate(QObject *parent) : QObject(parent) , m_inhibitionWatcher(new QDBusServiceWatcher(this)) { m_inhibitionWatcher->setConnection(QDBusConnection::sessionBus()); m_inhibitionWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ServerPrivate::onInhibitionServiceUnregistered); } ServerPrivate::~ServerPrivate() = default; QString ServerPrivate::notificationServiceName() { return QStringLiteral("org.freedesktop.Notifications"); } +QString ServerPrivate::notificationServicePath() +{ + return QStringLiteral("/org/freedesktop/Notifications"); +} + +QString ServerPrivate::notificationServiceInterface() +{ + return notificationServiceName(); +} + 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 (!m_dbusObjectValid) { // if already registered, don't fail here - m_dbusObjectValid = QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/freedesktop/Notifications"), this); + m_dbusObjectValid = QDBusConnection::sessionBus().registerObject(notificationServicePath(), 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(); QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface(); if (!master) { // 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(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, &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; } uint ServerPrivate::Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout) { const bool wasReplaced = replaces_id > 0; uint notificationId = 0; if (wasReplaced) { notificationId = replaces_id; } else { // Avoid wrapping around to 0 in case of overflow if (!m_highestNotificationId) { ++m_highestNotificationId; } notificationId = m_highestNotificationId; ++m_highestNotificationId; } Notification notification(notificationId); + notification.setDBusService(message().service()); notification.setSummary(summary); notification.setBody(body); notification.setApplicationName(app_name); notification.setActions(actions); notification.setTimeout(timeout); // might override some of the things we set above (like application name) notification.d->processHints(hints); // If we didn't get a pixmap, load the app_icon instead if (notification.d->image.isNull()) { notification.setIcon(app_icon); } uint pid = 0; if (notification.desktopEntry().isEmpty() || notification.applicationName().isEmpty()) { if (notification.desktopEntry().isEmpty() && notification.applicationName().isEmpty()) { qCInfo(NOTIFICATIONMANAGER) << "Notification from service" << message().service() << "didn't contain any identification information, this is an application bug!"; } QDBusReply pidReply = connection().interface()->servicePid(message().service()); if (pidReply.isValid()) { pid = pidReply.value(); } } // No desktop entry? Try to read the BAMF_DESKTOP_FILE_HINT in the environment of snaps if (notification.desktopEntry().isEmpty() && pid > 0) { const QString desktopEntry = Utils::desktopEntryFromPid(pid); if (!desktopEntry.isEmpty()) { qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from desktop entry" << desktopEntry; notification.setDesktopEntry(desktopEntry); } } // No application name? Try to figure out the process name using the sender's PID if (notification.applicationName().isEmpty() && pid > 0) { const QString processName = Utils::processNameFromPid(pid); if (!processName.isEmpty()) { qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from process name" << processName; notification.setApplicationName(processName); } } // If multiple identical notifications are sent in quick succession, refuse the request if (m_lastNotification.applicationName() == notification.applicationName() && m_lastNotification.summary() == notification.summary() && m_lastNotification.body() == notification.body() && m_lastNotification.desktopEntry() == notification.desktopEntry() && m_lastNotification.eventId() == notification.eventId() && m_lastNotification.actionNames() == notification.actionNames() && m_lastNotification.urls() == notification.urls() && m_lastNotification.created().msecsTo(notification.created()) < 1000) { qCDebug(NOTIFICATIONMANAGER) << "Discarding excess notification creation request"; sendErrorReply(QStringLiteral("org.freedesktop.Notifications.Error.ExcessNotificationGeneration"), QStringLiteral("Created too many similar notifications in quick succession")); return 0; } m_lastNotification = notification; if (wasReplaced) { notification.resetUpdated(); emit static_cast(parent())->notificationReplaced(replaces_id, notification); } else { emit static_cast(parent())->notificationAdded(notification); } return notificationId; } void ServerPrivate::CloseNotification(uint id) { // spec says "If the notification no longer exists, an empty D-BUS Error message is sent back." static_cast(parent())->closeNotification(id, Server::CloseReason::Revoked); } QStringList ServerPrivate::GetCapabilities() const { // should this be configurable somehow so the UI can tell what it implements? return QStringList{ QStringLiteral("body"), QStringLiteral("body-hyperlinks"), QStringLiteral("body-markup"), QStringLiteral("body-images"), QStringLiteral("icon-static"), QStringLiteral("actions"), QStringLiteral("inline-reply"), QStringLiteral("x-kde-urls"), QStringLiteral("x-kde-origin-name"), QStringLiteral("x-kde-display-appname"), QStringLiteral("inhibitions") }; } QString ServerPrivate::GetServerInformation(QString &vendor, QString &version, QString &specVersion) const { vendor = QStringLiteral("KDE"); version = QLatin1String(PROJECT_VERSION); specVersion = QStringLiteral("1.2"); return QStringLiteral("Plasma"); } void ServerPrivate::onBroadcastNotification(const QMap &properties) { qCDebug(NOTIFICATIONMANAGER) << "Received broadcast notification"; const auto currentUserId = KUserId::currentEffectiveUserId().nativeId(); // a QVariantList with ints arrives as QDBusArgument here, using a QStringList for simplicity const QStringList &userIds = properties.value(QStringLiteral("uids")).toStringList(); if (!userIds.isEmpty()) { auto it = std::find_if(userIds.constBegin(), userIds.constEnd(), [currentUserId](const QVariant &id) { bool ok; auto uid = id.toString().toLongLong(&ok); return ok && uid == currentUserId; }); if (it == userIds.constEnd()) { qCDebug(NOTIFICATIONMANAGER) << "It is not meant for us, ignoring"; return; } } bool ok; int timeout = properties.value(QStringLiteral("timeout")).toInt(&ok); if (!ok) { timeout = -1; // -1 = server default, 0 would be "persistent" } Notify( properties.value(QStringLiteral("appName")).toString(), 0, // replaces_id properties.value(QStringLiteral("appIcon")).toString(), properties.value(QStringLiteral("summary")).toString(), properties.value(QStringLiteral("body")).toString(), {}, // no actions properties.value(QStringLiteral("hints")).toMap(), timeout ); } uint ServerPrivate::add(const Notification ¬ification) { // TODO check if notification with ID already exists and signal update instead if (notification.id() == 0) { ++m_highestNotificationId; notification.d->id = m_highestNotificationId; emit static_cast(parent())->notificationAdded(notification); } else { emit static_cast(parent())->notificationReplaced(notification.id(), notification); } return notification.id(); } +void ServerPrivate::sendReplyText(const QString &dbusService, uint notificationId, const QString &text) +{ + if (dbusService.isEmpty()) { + qCWarning(NOTIFICATIONMANAGER) << "Sending notification reply text for notification" << notificationId << "untargeted"; + } + + QDBusMessage msg = QDBusMessage::createTargetedSignal(dbusService, + notificationServicePath(), + notificationServiceName(), + QStringLiteral("NotificationReplied")); + msg.setArguments({notificationId, text}); + QDBusConnection::sessionBus().send(msg); +} + uint ServerPrivate::Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints) { const QString dbusService = message().service(); qCDebug(NOTIFICATIONMANAGER) << "Request inhibit from service" << dbusService << "which is" << desktop_entry << "with reason" << reason; if (desktop_entry.isEmpty()) { // TODO return error return 0; } KService::Ptr service = KService::serviceByDesktopName(desktop_entry); QString applicationName; if (service) { // should we check for this and error if it didn't find a service? applicationName = service->name(); } m_inhibitionWatcher->addWatchedService(dbusService); ++m_highestInhibitionCookie; const bool oldExternalInhibited = externalInhibited(); m_externalInhibitions.insert(m_highestInhibitionCookie, { desktop_entry, applicationName, reason, hints }); m_inhibitionServices.insert(m_highestInhibitionCookie, dbusService); if (externalInhibited() != oldExternalInhibited) { emit externalInhibitedChanged(); } emit externalInhibitionsChanged(); return m_highestInhibitionCookie; } 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; const QList cookies = m_inhibitionServices.keys(serviceName); if (cookies.isEmpty()) { qCInfo(NOTIFICATIONMANAGER) << "Unknown inhibition service unregistered" << serviceName; return; } // We do lookups in there again... for (uint cookie : cookies) { UnInhibit(cookie); } } void ServerPrivate::onInhibitedChanged() { // emit DBus change signal... QDBusMessage signal = QDBusMessage::createSignal( - QStringLiteral("/org/freedesktop/Notifications"), + notificationServicePath(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged") ); signal.setArguments({ - QStringLiteral("org.freedesktop.Notifications"), + notificationServiceInterface(), QVariantMap{ // updated {QStringLiteral("Inhibited"), inhibited()}, }, QStringList() // invalidated }); QDBusConnection::sessionBus().send(signal); } void ServerPrivate::UnInhibit(uint cookie) { qCDebug(NOTIFICATIONMANAGER) << "Request release inhibition for cookie" << cookie; const QString service = m_inhibitionServices.value(cookie); if (service.isEmpty()) { qCInfo(NOTIFICATIONMANAGER) << "Requested to release inhibition with cookie" << cookie << "that doesn't exist"; // TODO if called from dbus raise error return; } m_inhibitionWatcher->removeWatchedService(service); m_externalInhibitions.remove(cookie); m_inhibitionServices.remove(cookie); if (m_externalInhibitions.isEmpty()) { emit externalInhibitedChanged(); } emit externalInhibitionsChanged(); } QList ServerPrivate::externalInhibitions() const { return m_externalInhibitions.values(); } bool ServerPrivate::inhibited() const { return m_inhibited; } void ServerPrivate::setInhibited(bool inhibited) { if (m_inhibited != inhibited) { m_inhibited = inhibited; emit inhibitedChanged(); } } bool ServerPrivate::externalInhibited() const { return !m_externalInhibitions.isEmpty(); } void ServerPrivate::clearExternalInhibitions() { if (m_externalInhibitions.isEmpty()) { return; } m_inhibitionWatcher->setWatchedServices(QStringList()); // remove all watches m_inhibitionServices.clear(); m_externalInhibitions.clear(); emit externalInhibitedChanged(); emit externalInhibitionsChanged(); } diff --git a/libnotificationmanager/server_p.h b/libnotificationmanager/server_p.h index a6e72aa39..4ab68a642 100644 --- a/libnotificationmanager/server_p.h +++ b/libnotificationmanager/server_p.h @@ -1,130 +1,134 @@ /* * Copyright 2018-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 #include #include "notification.h" class QDBusServiceWatcher; struct Inhibition { QString desktopEntry; QString applicationName; //QString applicationIconName; QString reason; QVariantMap hints; }; namespace NotificationManager { class ServerInfo; class Q_DECL_HIDDEN ServerPrivate : public QObject, protected QDBusContext { Q_OBJECT // DBus // Inhibitions Q_PROPERTY(bool Inhibited READ inhibited) public: ServerPrivate(QObject *parent); ~ServerPrivate() override; // DBus uint Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout); void CloseNotification(uint id); QStringList GetCapabilities() const; QString GetServerInformation(QString &vendor, QString &version, QString &specVersion) const; // Inhibitions uint Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints); void UnInhibit(uint cookie); bool inhibited() const; // property getter Q_SIGNALS: // DBus void NotificationClosed(uint id, uint reason); void ActionInvoked(uint id, const QString &actionKey); // non-standard + // This is manually emitted as targeted signal in sendReplyText() void NotificationReplied(uint id, const QString &text); void validChanged(); void inhibitedChanged(); void externalInhibitedChanged(); void externalInhibitionsChanged(); void serviceOwnershipLost(); public: // stuff used by public class friend class ServerInfo; static QString notificationServiceName(); + static QString notificationServicePath(); + static QString notificationServiceInterface(); bool init(); uint add(const Notification ¬ification); + void sendReplyText(const QString &dbusService, uint notificationId, const QString &text); 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); bool externalInhibited() const; QList externalInhibitions() const; void clearExternalInhibitions(); bool m_valid = false; uint m_highestNotificationId = 1; private slots: void onBroadcastNotification(const QMap &properties); private: 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; QHash m_externalInhibitions; QHash m_inhibitionServices; bool m_inhibited = false; Notification m_lastNotification; }; } // namespace NotificationManager