diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp index 2e44094..ef9c047 100644 --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -1,366 +1,365 @@ /* This file is part of the KDE libraries Copyright (C) 2005 Olivier Goffart Copyright (C) 2013-2015 Martin Klapetek Copyright (C) 2017 Eike Hein This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "knotificationmanager_p.h" #include "knotification.h" #include #include #include #include #include #include #include #ifdef QT_DBUS_LIB #include #include #endif #include "knotifyconfig.h" #include "knotificationplugin.h" #include "notifybylogfile.h" #include "notifybytaskbar.h" #include "notifybyexecute.h" #if defined(Q_OS_ANDROID) - #include "notifybyandroid.h" +#include "notifybyandroid.h" #elif defined(Q_OS_WIN) - #include "notifybysnore.h" +#include "notifybysnore.h" #else - #include "notifybypopup.h" - #include "notifybyportal.h" +#include "notifybypopup.h" +#include "notifybyportal.h" #endif #include "debug_p.h" #if defined(HAVE_CANBERRA) #include "notifybyaudio_canberra.h" #elif defined(HAVE_PHONON4QT5) #include "notifybyaudio_phonon.h" #endif #ifdef HAVE_SPEECH #include "notifybytts.h" #endif typedef QHash Dict; struct Q_DECL_HIDDEN KNotificationManager::Private { QHash notifications; QHash notifyPlugins; // incremental ids for notifications int notifyIdCounter; QStringList dirtyConfigCache; bool portalDBusServiceExists = false; }; class KNotificationManagerSingleton { public: KNotificationManager instance; }; Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self) KNotificationManager *KNotificationManager::self() { return &s_self()->instance; } KNotificationManager::KNotificationManager() : d(new Private) { d->notifyIdCounter = 0; qDeleteAll(d->notifyPlugins); d->notifyPlugins.clear(); #ifdef QT_DBUS_LIB const bool inSandbox = QFileInfo::exists(QLatin1String("/.flatpak-info")) || qEnvironmentVariableIsSet("SNAP"); if (inSandbox) { QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop")); } QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/Config"), QStringLiteral("org.kde.knotification"), QStringLiteral("reparseConfiguration"), this, SLOT(reparseConfiguration(QString))); #endif } KNotificationManager::~KNotificationManager() { delete d; } KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action) { KNotificationPlugin *plugin = d->notifyPlugins.value(action); // We already loaded a plugin for this action. if (plugin) { return plugin; } auto addPlugin = [this](KNotificationPlugin *plugin) { d->notifyPlugins[plugin->optionName()] = plugin; connect(plugin, &KNotificationPlugin::finished, this, &KNotificationManager::notifyPluginFinished); connect(plugin, &KNotificationPlugin::actionInvoked, this, &KNotificationManager::notificationActivated); }; // Load plugin. // We have a series of built-ins up first, and fall back to trying // to instantiate an externally supplied plugin. if (action == QLatin1String("Popup")) { - #if defined(Q_OS_ANDROID) +#if defined(Q_OS_ANDROID) plugin = new NotifyByAndroid(this); - #elif defined(Q_OS_WIN) +#elif defined(Q_OS_WIN) plugin = new NotifyBySnore(this); - #else - if (d->inSandbox && d->portalDBusServiceExists) { - plugin = new NotifyByPortal(this); - } - else { - plugin = new NotifyByPopup(this); +#else + if (d->portalDBusServiceExists) { + plugin = new NotifyByPortal(this); + } else { + plugin = new NotifyByPopup(this); } - #endif +#endif addPlugin(plugin); } else if (action == QLatin1String("Taskbar")) { plugin = new NotifyByTaskbar(this); addPlugin(plugin); } else if (action == QLatin1String("Sound")) { - #if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA) +#if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA) plugin = new NotifyByAudio(this); addPlugin(plugin); - #endif +#endif } else if (action == QLatin1String("Execute")) { plugin = new NotifyByExecute(this); addPlugin(plugin); } else if (action == QLatin1String("Logfile")) { plugin = new NotifyByLogfile(this); addPlugin(plugin); } else if (action == QLatin1String("TTS")) { #ifdef HAVE_SPEECH plugin = new NotifyByTTS(this); addPlugin(plugin); #endif } else { bool pluginFound = false; QList plugins = KPluginLoader::instantiatePlugins(QStringLiteral("knotification/notifyplugins"), [&action, &pluginFound](const KPluginMetaData &data) { // KPluginLoader::instantiatePlugins loops over the plugins it // found and calls this function to determine whether to // instantiate them. We use a `pluginFound` var outside the // lambda to break out of the loop once we got a match. // The reason we can't just use KPluginLoader::findPlugins, // loop over the meta data and instantiate only one plugin // is because the X-KDE-KNotification-OptionName field is // optional (see TODO note below) and the matching plugin // may be among the plugins which don't have it. if (pluginFound) { return false; } const QJsonObject &rawData = data.rawData(); // This field is new-ish and optional. If it's not set we always // instantiate the plugin, unless we already got a match. // TODO KF6: Require X-KDE-KNotification-OptionName be set and // reject plugins without it. if (rawData.contains(QStringLiteral("X-KDE-KNotification-OptionName"))) { if (rawData.value(QStringLiteral("X-KDE-KNotification-OptionName")) == action) { pluginFound = true; } else { return false; } } return true; }, this); for (QObject *pluginObj : qAsConst(plugins)) { KNotificationPlugin *notifyPlugin = qobject_cast(pluginObj); if (notifyPlugin) { // We try to avoid unnecessary instantiations (see above), but // when they happen keep the resulting plugins around. addPlugin(notifyPlugin); // Get ready to return the plugin we got asked for. if (notifyPlugin->optionName() == action) { plugin = notifyPlugin; } } else { // Not our/valid plugin, so delete the created object. pluginObj->deleteLater(); } } } return plugin; } void KNotificationManager::notifyPluginFinished(KNotification *notification) { if (!notification || !d->notifications.contains(notification->id())) { return; } notification->deref(); } void KNotificationManager::notificationActivated(int id, int action) { if (d->notifications.contains(id)) { qCDebug(LOG_KNOTIFICATIONS) << id << " " << action; KNotification *n = d->notifications[id]; n->activate(action); close(id); } } void KNotificationManager::notificationClosed() { KNotification *notification = qobject_cast(sender()); if (!notification) { return; } // We cannot do d->notifications.find(notification->id()); here because the // notification->id() is -1 or -2 at this point, so we need to look for value for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) { if (iter.value() == notification) { d->notifications.erase(iter); break; } } } void KNotificationManager::close(int id, bool force) { if (force || d->notifications.contains(id)) { KNotification *n = d->notifications.value(id); qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id; // Find plugins that are actually acting on this notification // call close() only on those, otherwise each KNotificationPlugin::close() // will call finish() which may close-and-delete the KNotification object // before it finishes calling close on all the other plugins. // For example: Action=Popup is a single actions but there is 5 loaded // plugins, calling close() on the second would already close-and-delete // the notification KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId()); QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action")); const auto listActions = notifyActions.split(QLatin1Char('|')); for (const QString &action : listActions) { if (!d->notifyPlugins.contains(action)) { qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; continue; } d->notifyPlugins[action]->close(n); } } } int KNotificationManager::notify(KNotification *n) { KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId()); if (d->dirtyConfigCache.contains(n->appName())) { notifyConfig.reparseSingleConfiguration(n->appName()); d->dirtyConfigCache.removeOne(n->appName()); } const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action")); if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) { // this will cause KNotification closing itself fast n->ref(); n->deref(); return -1; } d->notifications.insert(d->notifyIdCounter, n); // TODO KF6 d-pointer KNotifyConfig and add this there if (n->urgency() == KNotification::DefaultUrgency) { const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency")); if (urgency == QLatin1String("Low")) { n->setUrgency(KNotification::LowUrgency); } else if (urgency == QLatin1String("Normal")) { n->setUrgency(KNotification::NormalUrgency); } else if (urgency == QLatin1String("High")) { n->setUrgency(KNotification::HighUrgency); } else if (urgency == QLatin1String("Critical")) { n->setUrgency(KNotification::CriticalUrgency); } } const auto actionsList = notifyActions.split(QLatin1Char('|')); for (const QString &action : actionsList) { KNotificationPlugin *notifyPlugin = pluginForAction(action); if (!notifyPlugin) { qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; continue; } n->ref(); qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName(); notifyPlugin->notify(n, ¬ifyConfig); } connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed); return d->notifyIdCounter++; } void KNotificationManager::update(KNotification *n) { KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId()); for (KNotificationPlugin *p : qAsConst(d->notifyPlugins)) { p->update(n, ¬ifyConfig); } } void KNotificationManager::reemit(KNotification *n) { notify(n); } void KNotificationManager::reparseConfiguration(const QString &app) { if (!d->dirtyConfigCache.contains(app)) { d->dirtyConfigCache << app; } } #include "moc_knotificationmanager_p.cpp" diff --git a/src/notifybysnore.cpp b/src/notifybysnore.cpp index b12d044..48bed40 100644 --- a/src/notifybysnore.cpp +++ b/src/notifybysnore.cpp @@ -1,189 +1,189 @@ /* Copyright (C) 2019 Piyush Aggarwal This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "notifybysnore.h" #include "knotification.h" #include "knotifyconfig.h" #include "debug_p.h" #include #include -#include #include #include #include -#include - - #include - #include #include #include #include +#include #include +/** + * Be sure to have a shortcut installed in Windows Start Menu by SnoreToast + * The syntax is - + * ./SnoreToast.exe -install + * + * appID : use as-is from your app's QCoreApplication::applicationName() when installing the shortcut. + * NOTE: Install the shortcut in Windows Start Menu. + */ + NotifyBySnore::NotifyBySnore(QObject* parent) : KNotificationPlugin(parent) { - iconDir = new QTemporaryDir(); - server = new QLocalServer(); - server->listen(qApp->applicationName()); + server.listen(QString::fromStdString(QCryptographicHash::hash(qApp->applicationDirPath().toUtf8(), \ + QCryptographicHash::Md5 ).toHex().toStdString()).left(5)); + // ^ can be increased from 5 to N for lesser collisions - QObject::connect(server, &QLocalServer::newConnection, server, [this]() { - auto sock = server->nextPendingConnection(); + QObject::connect(&server, &QLocalServer::newConnection, &server, [this]() { + auto sock = server.nextPendingConnection(); sock->waitForReadyRead(); const QByteArray rawData = sock->readAll(); + sock->close(); const QString data = QString::fromWCharArray(reinterpret_cast(rawData.constData()), rawData.size() / sizeof(wchar_t)); QMap map; for (const auto &str : data.split(QStringLiteral(";"))) { const auto index = str.indexOf(QStringLiteral("=")); map.insert(str.mid(0, index), str.mid(index + 1)); } const auto action = map[QStringLiteral("action")]; const auto id = map[QStringLiteral("notificationId")].toInt(); - + KNotification *notification = nullptr; + const auto it = m_notifications.find(id); + if (it != m_notifications.end()) { + notification = it.value(); + } const auto snoreAction = SnoreToastActions::getAction(action.toStdWString()); - qCDebug(LOG_KNOTIFICATIONS) << "The notification ID is : " << QString::number(id); - switch(snoreAction){ - case SnoreToastActions::Actions::Clicked :{ - qCDebug(LOG_KNOTIFICATIONS) << " User clicked on the toast."; - break; - } - - case SnoreToastActions::Actions::Hidden :{ - qCDebug(LOG_KNOTIFICATIONS) << "The toast got hidden."; - break; - } - case SnoreToastActions::Actions::Dismissed :{ - qCDebug(LOG_KNOTIFICATIONS) << "User dismissed the toast."; - break; - } - case SnoreToastActions::Actions::Timedout :{ - qCDebug(LOG_KNOTIFICATIONS) << "The toast timed out."; - break; + qCDebug(LOG_KNOTIFICATIONS) << "The notification ID is : " << id; + switch (snoreAction) { + case SnoreToastActions::Actions::Clicked : + qCDebug(LOG_KNOTIFICATIONS) << " User clicked on the toast."; + if (notification) { + close(notification); } - case SnoreToastActions::Actions::ButtonClicked :{ - qCDebug(LOG_KNOTIFICATIONS) << " User clicked a button on the toast."; - const auto button = map[QStringLiteral("button")]; - QStringList s = m_notifications.value(id)->actions(); - int actionNum = s.indexOf(button) + 1; // QStringList starts with index 0 but not actions - emit actionInvoked(id, actionNum); - break; - } - case SnoreToastActions::Actions::TextEntered :{ - qCDebug(LOG_KNOTIFICATIONS) << " User entered some text in the toast."; - break; - } - default:{ - qCDebug(LOG_KNOTIFICATIONS) << "Something weird happened to the toast."; - break; + break; + case SnoreToastActions::Actions::Hidden : + qCDebug(LOG_KNOTIFICATIONS) << "The toast got hidden."; + break; + case SnoreToastActions::Actions::Dismissed : + qCDebug(LOG_KNOTIFICATIONS) << "User dismissed the toast."; + break; + case SnoreToastActions::Actions::Timedout : + qCDebug(LOG_KNOTIFICATIONS) << "The toast timed out."; + break; + case SnoreToastActions::Actions::ButtonClicked :{ + qCDebug(LOG_KNOTIFICATIONS) << " User clicked a button on the toast."; + const auto button = map[QStringLiteral("button")]; + QStringList s = m_notifications.value(id)->actions(); + int actionNum = s.indexOf(button) + 1; // QStringList starts with index 0 but not actions + emit actionInvoked(id, actionNum); + break;} + case SnoreToastActions::Actions::TextEntered : + qCDebug(LOG_KNOTIFICATIONS) << " User entered some text in the toast."; + break; + default: + qCDebug(LOG_KNOTIFICATIONS) << "Unexpected behaviour with the toast."; + if (notification) { + close(notification); } + break; } -}); + }); } NotifyBySnore::~NotifyBySnore() { - server->close(); - iconDir->remove(); + server.close(); + iconDir.remove(); } void NotifyBySnore::notify(KNotification *notification, KNotifyConfig *config) { if (m_notifications.constFind(notification->id()) != m_notifications.constEnd()) { qCDebug(LOG_KNOTIFICATIONS) << "Duplicate notification with ID: " << notification->id() << " ignored."; return; } QProcess *proc = new QProcess(); - QStringList arguments; - QString iconPath(QStringLiteral("")); - + QString iconPath; arguments << QStringLiteral("-t"); - if(!notification->title().isEmpty()){ + if (!notification->title().isEmpty()) { arguments << notification->title(); } - else{ + else { arguments << qApp->applicationDisplayName(); } - arguments << QStringLiteral("-m") << notification->text(); - - if(!notification->pixmap().isNull()){ - iconPath.append(iconDir->path() + QStringLiteral("/") - + QString::number(notification->id()) + QStringLiteral(".png")); + if (!notification->pixmap().isNull()) { + iconPath = iconDir.path() + QStringLiteral("/") + + QString::number(notification->id()) + QStringLiteral(".png"); notification->pixmap().save(iconPath, "PNG"); arguments << QStringLiteral("-p") << iconPath; } - arguments << QStringLiteral("-appID") << qApp->applicationName() - << QStringLiteral("-id") << QString::number(notification->id()) - << QStringLiteral("-pipename") << server->fullServerName(); - - if (!notification->actions().isEmpty()){ + << QStringLiteral("-id") << QString::number(notification->id()) + << QStringLiteral("-pipename") << server.fullServerName(); + + if (!notification->actions().isEmpty()) { arguments << QStringLiteral("-b") << notification->actions().join(QStringLiteral(";")); } - - qDebug() << arguments; + qCDebug(LOG_KNOTIFICATIONS) << arguments; + m_notifications.insert(notification->id(), notification); proc->start(program, arguments); connect(proc, QOverload::of(&QProcess::finished), - [=](int exitCode, QProcess::ExitStatus exitStatus){ - proc->close(); - QFile file(iconPath); - file.remove(); + [=](int exitCode, QProcess::ExitStatus exitStatus){ + proc->deleteLater(); }); } -void NotifyBySnore::close(KNotification* notification) +void NotifyBySnore::close(KNotification *notification) { const auto it = m_notifications.find(notification->id()); if (it == m_notifications.end()) { return; } - qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast closing notification with ID: "<< notification->id(); + qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast closing notification with ID: " << notification->id(); QProcess *proc = new QProcess(); QStringList arguments; arguments << QStringLiteral("-close") << QString::number(notification->id()) - << QStringLiteral("-appID") << qApp->applicationName(); + << QStringLiteral("-appID") << qApp->applicationName(); proc->start(program, arguments); if (it.value()) { finish(it.value()); } - arguments.clear(); m_notifications.erase(it); connect(proc, QOverload::of(&QProcess::finished), [=](int exitCode, QProcess::ExitStatus exitStatus){ - proc->close(); + proc->deleteLater(); + delete proc; }); - } void NotifyBySnore::update(KNotification *notification, KNotifyConfig *config) { close(notification); notify(notification, config); -} \ No newline at end of file +} diff --git a/src/notifybysnore.h b/src/notifybysnore.h index 7b48b05..8ad37aa 100644 --- a/src/notifybysnore.h +++ b/src/notifybysnore.h @@ -1,50 +1,49 @@ /* Copyright (C) 2019 Piyush Aggarwal This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef NOTIFYBYSNORE_H #define NOTIFYBYSNORE_H #include "knotificationplugin.h" #include #include #include #include #include - /** Windows notification backend - inspired by Android notification backend. */ class NotifyBySnore : public KNotificationPlugin { Q_OBJECT public: explicit NotifyBySnore(QObject *parent = nullptr); ~NotifyBySnore() override; QString optionName() override { return QStringLiteral("Popup"); } void notify(KNotification *notification, KNotifyConfig *config) override; void close(KNotification * notification) override; void update(KNotification *notification, KNotifyConfig *config) override; private: QMap> m_notifications; QString program = QStringLiteral("SnoreToast.exe"); - QLocalServer *server; - QTemporaryDir *iconDir; + QLocalServer server; + QTemporaryDir iconDir; }; #endif // NOTIFYBYSNORE_H