diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ec96c8d3f..7c8d15c56 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -46,11 +46,8 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") endif() endif() -if(NOT WIN32 AND NOT APPLE) - add_subdirectory(sendnotifications) -endif() - if(NOT APPLE) + add_subdirectory(sendnotifications) add_subdirectory(sftp) endif() diff --git a/plugins/mousepad/CMakeLists.txt b/plugins/mousepad/CMakeLists.txt index 5d54052c9..915b4c8a4 100644 --- a/plugins/mousepad/CMakeLists.txt +++ b/plugins/mousepad/CMakeLists.txt @@ -2,10 +2,8 @@ kdeconnect_add_plugin(kdeconnect_mousepad SOURCES mousepadplugin.cpp abstractrem if(UNIX AND NOT APPLE) target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp ${SRCS}) - target_sources(kdeconnect_mousepad PRIVATE ${wayland_SRCS}) target_link_libraries(kdeconnect_mousepad Wayland::Client Qt::WaylandClient PkgConfig::XkbCommon) - target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp) if (WITH_X11) find_package(LibFakeKey REQUIRED) diff --git a/plugins/sendnotifications/CMakeLists.txt b/plugins/sendnotifications/CMakeLists.txt index 8c9288183..9b5423dd1 100644 --- a/plugins/sendnotifications/CMakeLists.txt +++ b/plugins/sendnotifications/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources(kdeconnect_sendnotifications PRIVATE notificationslistener.cpp notifyingapplication.cpp ) + target_link_libraries(kdeconnect_sendnotifications kdeconnectcore Qt::DBus @@ -12,9 +13,16 @@ target_link_libraries(kdeconnect_sendnotifications Qt::Gui KF${QT_MAJOR_VERSION}::IconThemes KF${QT_MAJOR_VERSION}::ConfigCore - PkgConfig::GIO ) +if(WIN32) + target_sources(kdeconnect_sendnotifications PRIVATE windowsnotificationslistener.cpp) + target_link_libraries(kdeconnect_sendnotifications windowsapp) +else() + target_sources(kdeconnect_sendnotifications PRIVATE dbusnotificationslistener.cpp) + target_link_libraries(kdeconnect_sendnotifications PkgConfig::GIO) +endif() + # Config kdeconnect_add_kcm(kdeconnect_sendnotifications_config) diff --git a/plugins/sendnotifications/dbusnotificationslistener.cpp b/plugins/sendnotifications/dbusnotificationslistener.cpp new file mode 100644 index 000000000..479ac7d52 --- /dev/null +++ b/plugins/sendnotifications/dbusnotificationslistener.cpp @@ -0,0 +1,288 @@ +/** + * SPDX-FileCopyrightText: 2015 Holger Kaelberer + * SPDX-FileCopyrightText: 2023 Fushan Wen + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "dbusnotificationslistener.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "plugin_sendnotifications_debug.h" + +DBusNotificationsListener::DBusNotificationsListener(KdeConnectPlugin *aPlugin) + : NotificationsListener(aPlugin) +{ + setupDBusMonitor(); +} + +DBusNotificationsListener::~DBusNotificationsListener() +{ + g_dbus_connection_remove_filter(m_gdbusConnection, m_gdbusFilterId); + g_object_unref(m_gdbusConnection); +} + +void DBusNotificationsListener::setupDBusMonitor() +{ + GError *error = nullptr; + m_gdbusConnection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); + g_assert_no_error(error); + m_gdbusFilterId = g_dbus_connection_add_filter(m_gdbusConnection, DBusNotificationsListener::onMessageFiltered, this, nullptr); + + g_autoptr(GDBusMessage) msg = + g_dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.Monitoring", "BecomeMonitor"); + + GVariantBuilder *arrayBuilder = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(arrayBuilder, "s", "interface='org.freedesktop.Notifications'"); + g_variant_builder_add(arrayBuilder, "s", "member='Notify'"); + + g_dbus_message_set_body(msg, g_variant_new("(asu)", arrayBuilder, 0u)); + g_dbus_connection_send_message(m_gdbusConnection, msg, GDBusSendMessageFlags::G_DBUS_SEND_MESSAGE_FLAGS_NONE, nullptr, &error); + g_assert_no_error(error); +} + +bool DBusNotificationsListener::parseImageDataArgument(GVariant *argument, + int &width, + int &height, + int &rowStride, + int &bitsPerSample, + int &channels, + bool &hasAlpha, + QByteArray &imageData) const +{ + if (g_variant_n_children(argument) != 7) { + return false; + } + + g_autoptr(GVariant) variant; + + variant = g_variant_get_child_value(argument, 0); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { + return false; + } + width = g_variant_get_int32(variant); + + variant = g_variant_get_child_value(argument, 1); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { + return false; + } + height = g_variant_get_int32(variant); + + variant = g_variant_get_child_value(argument, 2); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { + return false; + } + rowStride = g_variant_get_int32(variant); + + variant = g_variant_get_child_value(argument, 3); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) { + return false; + } + hasAlpha = g_variant_get_boolean(variant); + + variant = g_variant_get_child_value(argument, 4); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { + return false; + } + bitsPerSample = g_variant_get_int32(variant); + + variant = g_variant_get_child_value(argument, 5); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { + return false; + } + channels = g_variant_get_int32(variant); + + variant = g_variant_get_child_value(argument, 6); + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_ARRAY)) { + return false; + } + imageData = g_variant_get_bytestring(variant); + + return true; +} + +QSharedPointer DBusNotificationsListener::iconForImageData(GVariant *argument) const +{ + int width, height, rowStride, bitsPerSample, channels; + bool hasAlpha; + QByteArray imageData; + + if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample, channels, hasAlpha, imageData)) + return QSharedPointer(); + + if (bitsPerSample != 8) { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unsupported image format:" + << "width=" << width << "height=" << height << "rowStride=" << rowStride + << "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha; + return QSharedPointer(); + } + + QImage image(reinterpret_cast(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32); + if (hasAlpha) { + image = image.rgbSwapped(); // RGBA --> ARGB + } + + QSharedPointer buffer = iconFromQImage(image); + if (!buffer) { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer"; + return QSharedPointer(); + } + + return buffer; +} + +QSharedPointer DBusNotificationsListener::iconForIconName(const QString &iconName) +{ + int size = KIconLoader::SizeEnormous; // use big size to allow for good + // quality on high-DPI mobile devices + QString iconPath = KIconLoader::global()->iconPath(iconName, -size, true); + if (!iconPath.isEmpty()) { + if (!iconPath.endsWith(QLatin1String(".png")) && KIconLoader::global()->theme()->name() != QLatin1String("hicolor")) { + // try falling back to hicolor theme: + KIconTheme hicolor(QStringLiteral("hicolor")); + if (hicolor.isValid()) { + iconPath = hicolor.iconPath(iconName + QStringLiteral(".png"), size, KIconLoader::MatchBest); + // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Found non-png icon in default theme trying fallback to hicolor:" << iconPath; + } + } + } + + if (iconPath.endsWith(QLatin1String(".png"))) + return QSharedPointer(new QFile(iconPath)); + return QSharedPointer(); +} + +GDBusMessage *DBusNotificationsListener::onMessageFiltered(GDBusConnection *, GDBusMessage *msg, int, void *parent) +{ + static unsigned id = 0; + if (!msg) { + return msg; + } + + const gchar *interface = g_dbus_message_get_interface(msg); + if (!interface || strcmp(interface, "org.freedesktop.Notifications")) { + // The first message will be from org.freedesktop.DBus.Monitoring + return msg; + } + const gchar *member = g_dbus_message_get_member(msg); + if (!member || strcmp(member, "Notify")) { + // Even if member is set, the monitor will still notify messages from other members. + return nullptr; + } + + g_autoptr(GVariant) bodyVariant = g_dbus_message_get_body(msg); + Q_ASSERT(bodyVariant); + + // Data order and types: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html + g_autoptr(GVariant) variant = g_variant_get_child_value(bodyVariant, 0); + const QString appName = QString::fromUtf8(g_variant_get_string(variant, nullptr)); + // skip our own notifications + auto listener = static_cast(parent); + + variant = g_variant_get_child_value(bodyVariant, 2); + const QString appIcon = QString::fromUtf8(g_variant_get_string(variant, nullptr)); + if (!listener->checkApplicationName(appName, std::reference_wrapper(appIcon))) { + return nullptr; + } + + variant = g_variant_get_child_value(bodyVariant, 7); + const auto timeout = g_variant_get_int32(variant); + if (timeout > 0 && listener->m_plugin->config()->getBool(QStringLiteral("generalPersistent"), false)) { + return nullptr; + } + + variant = g_variant_get_child_value(bodyVariant, 6); + std::unordered_map hints; + GVariantIter *iter; + const gchar *key; + GVariant *value = nullptr; + g_variant_get(variant, "a{sv}", &iter); + while (g_variant_iter_loop(iter, "{sv}", &key, &value)) { + hints.emplace(QString::fromUtf8(key), value); + } + g_variant_iter_free(iter); + + QScopeGuard cleanupHints([&hints] { + for (auto &[_, hint] : hints) { + g_variant_unref(hint); + } + }); + + int urgency = -1; + if (auto it = hints.find(QStringLiteral("urgency")); it != hints.end()) { + if (g_variant_is_of_type(it->second, G_VARIANT_TYPE_BYTE)) { + urgency = g_variant_get_byte(it->second); + } + } + if (urgency > -1 && urgency < listener->m_plugin->config()->getInt(QStringLiteral("generalUrgency"), 0)) + return nullptr; + + variant = g_variant_get_child_value(bodyVariant, 3); + QString ticker = QString::fromUtf8(g_variant_get_string(variant, nullptr)); + + variant = g_variant_get_child_value(bodyVariant, 4); + const QString body = QString::fromUtf8(g_variant_get_string(variant, nullptr)); + + if (!body.isEmpty() && listener->m_plugin->config()->getBool(QStringLiteral("generalIncludeBody"), true)) { + ticker += QStringLiteral(": ") + body; + } + + if (listener->checkIsInBlacklist(appName, ticker)) { + return nullptr; + } + + variant = g_variant_get_child_value(bodyVariant, 1); + const unsigned replacesId = g_variant_get_uint32(variant); + + // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Sending notification from" << appName << ":" < 0 ? replacesId : ++id)}, + {QStringLiteral("appName"), appName}, + {QStringLiteral("ticker"), ticker}, + {QStringLiteral("isClearable"), timeout == 0}}); // KNotifications are persistent if + // timeout == 0, for other notifications + // clearability is pointless + + // sync any icon data? + if (listener->m_plugin->config()->getBool(QStringLiteral("generalSynchronizeIcons"), true)) { + QSharedPointer iconSource; + // try different image sources according to priorities in notifications- + // spec version 1.2: + if (auto it = hints.find(QStringLiteral("image-data")); it != hints.end()) { + iconSource = listener->iconForImageData(it->second); + } else if (auto it = hints.find(QStringLiteral("image_data")); it != hints.end()) { // 1.1 backward compatibility + iconSource = listener->iconForImageData(it->second); + } else if (auto it = hints.find(QStringLiteral("image-path")); it != hints.end()) { + iconSource = iconForIconName(QString::fromUtf8(g_variant_get_string(it->second, nullptr))); + } else if (auto it = hints.find(QStringLiteral("image_path")); it != hints.end()) { // 1.1 backward compatibility + iconSource = iconForIconName(QString::fromUtf8(g_variant_get_string(it->second, nullptr))); + } else if (!appIcon.isEmpty()) { + iconSource = iconForIconName(appIcon); + } else if (auto it = hints.find(QStringLiteral("icon_data")); it != hints.end()) { // < 1.1 backward compatibility + iconSource = listener->iconForImageData(it->second); + } + + if (iconSource) + np.setPayload(iconSource, iconSource->size()); + } + + listener->m_plugin->sendPacket(np); + + qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Got notification appName=" << appName << "replacesId=" << replacesId << "appIcon=" << appIcon + << "summary=" << ticker << "body=" << body << "hints=" << hints.size() << "urgency=" << urgency + << "timeout=" << timeout; + + return nullptr; +} diff --git a/plugins/sendnotifications/dbusnotificationslistener.h b/plugins/sendnotifications/dbusnotificationslistener.h new file mode 100644 index 000000000..60554b025 --- /dev/null +++ b/plugins/sendnotifications/dbusnotificationslistener.h @@ -0,0 +1,41 @@ +/** + * SPDX-FileCopyrightText: 2023 Fushan Wen + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "notificationslistener.h" + +#include + +class DBusNotificationsListener : public NotificationsListener +{ + Q_OBJECT + +public: + explicit DBusNotificationsListener(KdeConnectPlugin *aPlugin); + ~DBusNotificationsListener() override; + +private: + void setupDBusMonitor(); + + // virtual helper function to make testing possible (QDBusArgument can not + // be injected without making a DBUS-call): + virtual bool parseImageDataArgument(GVariant *argument, + int &width, + int &height, + int &rowStride, + int &bitsPerSample, + int &channels, + bool &hasAlpha, + QByteArray &imageData) const; + QSharedPointer iconForImageData(GVariant *argument) const; + static QSharedPointer iconForIconName(const QString &iconName); + + static GDBusMessage *onMessageFiltered(GDBusConnection *connection, GDBusMessage *msg, int incoming, void *parent); + + GDBusConnection *m_gdbusConnection = nullptr; + unsigned m_gdbusFilterId = 0; +}; diff --git a/plugins/sendnotifications/notificationslistener.cpp b/plugins/sendnotifications/notificationslistener.cpp index 2a37893e1..12d874d4b 100644 --- a/plugins/sendnotifications/notificationslistener.cpp +++ b/plugins/sendnotifications/notificationslistener.cpp @@ -5,28 +5,18 @@ */ #include "notificationslistener.h" -#include +#include +#include +#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include - -#include #include -#include - #include "notifyingapplication.h" + #include "plugin_sendnotifications_debug.h" -#include "sendnotificationsplugin.h" NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin) : QObject(aPlugin) @@ -36,22 +26,6 @@ NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin) qRegisterMetaTypeStreamOperators("NotifyingApplication"); #endif - GError *error = nullptr; - m_gdbusConnection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); - g_assert_no_error(error); - m_gdbusFilterId = g_dbus_connection_add_filter(m_gdbusConnection, NotificationsListener::onMessageFiltered, this, nullptr); - - g_autoptr(GDBusMessage) msg = - g_dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.Monitoring", "BecomeMonitor"); - - GVariantBuilder *arrayBuilder = g_variant_builder_new(G_VARIANT_TYPE("as")); - g_variant_builder_add(arrayBuilder, "s", "interface='org.freedesktop.Notifications'"); - g_variant_builder_add(arrayBuilder, "s", "member='Notify'"); - - g_dbus_message_set_body(msg, g_variant_new("(asu)", arrayBuilder, 0u)); - g_dbus_connection_send_message(m_gdbusConnection, msg, GDBusSendMessageFlags::G_DBUS_SEND_MESSAGE_FLAGS_NONE, nullptr, &error); - g_assert_no_error(error); - setTranslatedAppName(); loadApplications(); @@ -61,8 +35,6 @@ NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin) NotificationsListener::~NotificationsListener() { qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Destroying NotificationsListener"; - g_dbus_connection_remove_filter(m_gdbusConnection, m_gdbusFilterId); - g_object_unref(m_gdbusConnection); } void NotificationsListener::setTranslatedAppName() @@ -94,88 +66,45 @@ void NotificationsListener::loadApplications() // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Loaded" << applications.size() << " applications"; } -bool NotificationsListener::parseImageDataArgument(GVariant *argument, - int &width, - int &height, - int &rowStride, - int &bitsPerSample, - int &channels, - bool &hasAlpha, - QByteArray &imageData) const +bool NotificationsListener::checkApplicationName(const QString &appName, std::optional> iconName) { - if (g_variant_n_children(argument) != 7) { + if (m_translatedAppName == appName) { return false; } - g_autoptr(GVariant) variant; + auto it = m_applications.constFind(appName); + if (it == m_applications.cend()) { + // new application -> add to config + NotifyingApplication app; + app.name = appName; + if (iconName.has_value()) { + app.icon = iconName.value(); + } + app.active = true; + m_applications.insert(app.name, app); + // update config: + QVariantList list; + for (const auto &a : std::as_const(m_applications)) { + list << QVariant::fromValue(a); + } + m_plugin->config()->setList(QStringLiteral("applications"), list); - variant = g_variant_get_child_value(argument, 0); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { - return false; + return true; + } else { + return it->active; } - width = g_variant_get_int32(variant); - - variant = g_variant_get_child_value(argument, 1); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { - return false; - } - height = g_variant_get_int32(variant); - - variant = g_variant_get_child_value(argument, 2); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { - return false; - } - rowStride = g_variant_get_int32(variant); - - variant = g_variant_get_child_value(argument, 3); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) { - return false; - } - hasAlpha = g_variant_get_boolean(variant); - - variant = g_variant_get_child_value(argument, 4); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { - return false; - } - bitsPerSample = g_variant_get_int32(variant); - - variant = g_variant_get_child_value(argument, 5); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) { - return false; - } - channels = g_variant_get_int32(variant); - - variant = g_variant_get_child_value(argument, 6); - if (g_variant_is_of_type(variant, G_VARIANT_TYPE_ARRAY)) { - return false; - } - imageData = g_variant_get_bytestring(variant); - - return true; } -QSharedPointer NotificationsListener::iconForImageData(GVariant *argument) const +bool NotificationsListener::checkIsInBlacklist(const QString &appName, const QString &content) { - int width, height, rowStride, bitsPerSample, channels; - bool hasAlpha; - QByteArray imageData; - - if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample, channels, hasAlpha, imageData)) - return QSharedPointer(); - - if (bitsPerSample != 8) { - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unsupported image format:" - << "width=" << width << "height=" << height << "rowStride=" << rowStride - << "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha; - return QSharedPointer(); - } - - QImage image(reinterpret_cast(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32); - if (hasAlpha) - image = image.rgbSwapped(); // RGBA --> ARGB + auto appIt = m_applications.constFind(appName); + return appIt->blacklistExpression.isValid() && !appIt->blacklistExpression.pattern().isEmpty() && appIt->blacklistExpression.match(content).hasMatch(); +} +QSharedPointer NotificationsListener::iconFromQImage(const QImage &image) const +{ QSharedPointer buffer = QSharedPointer(new QBuffer); - if (!buffer || !buffer->open(QIODevice::WriteOnly) || !image.save(buffer.data(), "PNG")) { + if (!buffer->open(QIODevice::WriteOnly) && !image.save(buffer.data(), "PNG")) { qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer"; return QSharedPointer(); } @@ -183,171 +112,4 @@ QSharedPointer NotificationsListener::iconForImageData(GVariant *argu return buffer; } -QSharedPointer NotificationsListener::iconForIconName(const QString &iconName) -{ - int size = KIconLoader::SizeEnormous; // use big size to allow for good - // quality on high-DPI mobile devices - QString iconPath = KIconLoader::global()->iconPath(iconName, -size, true); - if (!iconPath.isEmpty()) { - if (!iconPath.endsWith(QLatin1String(".png")) && KIconLoader::global()->theme()->name() != QLatin1String("hicolor")) { - // try falling back to hicolor theme: - KIconTheme hicolor(QStringLiteral("hicolor")); - if (hicolor.isValid()) { - iconPath = hicolor.iconPath(iconName + QStringLiteral(".png"), size, KIconLoader::MatchBest); - // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Found non-png icon in default theme trying fallback to hicolor:" << iconPath; - } - } - } - - if (iconPath.endsWith(QLatin1String(".png"))) - return QSharedPointer(new QFile(iconPath)); - return QSharedPointer(); -} - -GDBusMessage *NotificationsListener::onMessageFiltered(GDBusConnection *, GDBusMessage *msg, int, void *parent) -{ - static unsigned id = 0; - if (!msg) { - return msg; - } - - const gchar *interface = g_dbus_message_get_interface(msg); - if (!interface || strcmp(interface, "org.freedesktop.Notifications")) { - // The first message will be from org.freedesktop.DBus.Monitoring - return msg; - } - const gchar *member = g_dbus_message_get_member(msg); - if (!member || strcmp(member, "Notify")) { - // Even if member is set, the monitor will still notify messages from other members. - return nullptr; - } - - g_autoptr(GVariant) bodyVariant = g_dbus_message_get_body(msg); - Q_ASSERT(bodyVariant); - - // Data order and types: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html - g_autoptr(GVariant) variant = g_variant_get_child_value(bodyVariant, 0); - const QString appName = QString::fromUtf8(g_variant_get_string(variant, nullptr)); - // skip our own notifications - auto listener = static_cast(parent); - - if (appName == listener->m_translatedAppName) { - return nullptr; - } - - variant = g_variant_get_child_value(bodyVariant, 2); - const QString appIcon = QString::fromUtf8(g_variant_get_string(variant, nullptr)); - - NotifyingApplication app; - if (!listener->m_applications.contains(appName)) { - // new application -> add to config - app.name = appName; - app.icon = appIcon; - app.active = true; - app.blacklistExpression = QRegularExpression(); - listener->m_applications.insert(app.name, app); - // update config: - QVariantList list; - for (const auto &a : std::as_const(listener->m_applications)) - list << QVariant::fromValue(a); - listener->m_plugin->config()->setList(QStringLiteral("applications"), list); - // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Added new application to config:" << app; - } else { - app = listener->m_applications.value(appName); - } - - if (!app.active) { - return nullptr; - } - - variant = g_variant_get_child_value(bodyVariant, 7); - const auto timeout = g_variant_get_int32(variant); - if (timeout > 0 && listener->m_plugin->config()->getBool(QStringLiteral("generalPersistent"), false)) { - return nullptr; - } - - variant = g_variant_get_child_value(bodyVariant, 6); - std::unordered_map hints; - GVariantIter *iter; - const gchar *key; - GVariant *value = nullptr; - g_variant_get(variant, "a{sv}", &iter); - while (g_variant_iter_loop(iter, "{sv}", &key, &value)) { - hints.emplace(QString::fromUtf8(key), value); - } - g_variant_iter_free(iter); - - QScopeGuard cleanupHints([&hints] { - for (auto &[_, hint] : hints) { - g_variant_unref(hint); - } - }); - - int urgency = -1; - if (auto it = hints.find(QStringLiteral("urgency")); it != hints.end()) { - if (g_variant_is_of_type(it->second, G_VARIANT_TYPE_BYTE)) { - urgency = g_variant_get_byte(it->second); - } - } - if (urgency > -1 && urgency < listener->m_plugin->config()->getInt(QStringLiteral("generalUrgency"), 0)) - return nullptr; - - variant = g_variant_get_child_value(bodyVariant, 3); - QString ticker = QString::fromUtf8(g_variant_get_string(variant, nullptr)); - - variant = g_variant_get_child_value(bodyVariant, 4); - const QString body = QString::fromUtf8(g_variant_get_string(variant, nullptr)); - - if (!body.isEmpty() && listener->m_plugin->config()->getBool(QStringLiteral("generalIncludeBody"), true)) { - ticker += QStringLiteral(": ") + body; - } - - if (app.blacklistExpression.isValid() && !app.blacklistExpression.pattern().isEmpty() && app.blacklistExpression.match(ticker).hasMatch()) { - return nullptr; - } - - variant = g_variant_get_child_value(bodyVariant, 1); - const unsigned replacesId = g_variant_get_uint32(variant); - - // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Sending notification from" << appName << ":" < 0 ? replacesId : ++id)}, - {QStringLiteral("appName"), appName}, - {QStringLiteral("ticker"), ticker}, - {QStringLiteral("isClearable"), timeout == 0}}); // KNotifications are persistent if - // timeout == 0, for other notifications - // clearability is pointless - - // sync any icon data? - if (listener->m_plugin->config()->getBool(QStringLiteral("generalSynchronizeIcons"), true)) { - QSharedPointer iconSource; - // try different image sources according to priorities in notifications- - // spec version 1.2: - if (auto it = hints.find(QStringLiteral("image-data")); it != hints.end()) { - iconSource = listener->iconForImageData(it->second); - } else if (auto it = hints.find(QStringLiteral("image_data")); it != hints.end()) { // 1.1 backward compatibility - iconSource = listener->iconForImageData(it->second); - } else if (auto it = hints.find(QStringLiteral("image-path")); it != hints.end()) { - iconSource = iconForIconName(QString::fromUtf8(g_variant_get_string(it->second, nullptr))); - } else if (auto it = hints.find(QStringLiteral("image_path")); it != hints.end()) { // 1.1 backward compatibility - iconSource = iconForIconName(QString::fromUtf8(g_variant_get_string(it->second, nullptr))); - } else if (!appIcon.isEmpty()) { - iconSource = iconForIconName(appIcon); - } else if (auto it = hints.find(QStringLiteral("icon_data")); it != hints.end()) { // < 1.1 backward compatibility - iconSource = listener->iconForImageData(it->second); - } - - if (iconSource) - np.setPayload(iconSource, iconSource->size()); - } - - listener->m_plugin->sendPacket(np); - - qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Got notification appName=" << appName << "replacesId=" << replacesId << "appIcon=" << appIcon - << "summary=" << ticker << "body=" << body << "hints=" << hints.size() << "urgency=" << urgency - << "timeout=" << timeout; - - return nullptr; -} - #include "moc_notificationslistener.cpp" diff --git a/plugins/sendnotifications/notificationslistener.h b/plugins/sendnotifications/notificationslistener.h index 7a4631b50..29ab84fd8 100644 --- a/plugins/sendnotifications/notificationslistener.h +++ b/plugins/sendnotifications/notificationslistener.h @@ -6,18 +6,18 @@ #pragma once -#include -#include -#include -#include -#include +#include -#include +#include + +#include class KdeConnectPlugin; class Notification; struct NotifyingApplication; +#define PACKET_TYPE_NOTIFICATION QStringLiteral("kdeconnect.notification") + class NotificationsListener : public QObject { Q_OBJECT @@ -27,31 +27,18 @@ public: ~NotificationsListener() override; protected: + bool checkApplicationName(const QString &appName, std::optional> iconName = std::nullopt); + bool checkIsInBlacklist(const QString &appName, const QString &content); + QSharedPointer iconFromQImage(const QImage &image) const; + KdeConnectPlugin *m_plugin; - QHash m_applications; - - // virtual helper function to make testing possible (QDBusArgument can not - // be injected without making a DBUS-call): - virtual bool parseImageDataArgument(GVariant *argument, - int &width, - int &height, - int &rowStride, - int &bitsPerSample, - int &channels, - bool &hasAlpha, - QByteArray &imageData) const; - QSharedPointer iconForImageData(GVariant *argument) const; - static QSharedPointer iconForIconName(const QString &iconName); - - static GDBusMessage *onMessageFiltered(GDBusConnection *connection, GDBusMessage *msg, int incoming, void *parent); private Q_SLOTS: void loadApplications(); private: void setTranslatedAppName(); - QString m_translatedAppName; - GDBusConnection *m_gdbusConnection = nullptr; - unsigned m_gdbusFilterId = 0; + QHash m_applications; + QString m_translatedAppName; }; diff --git a/plugins/sendnotifications/sendnotificationsplugin.cpp b/plugins/sendnotifications/sendnotificationsplugin.cpp index 44b807978..151a3331b 100644 --- a/plugins/sendnotifications/sendnotificationsplugin.cpp +++ b/plugins/sendnotifications/sendnotificationsplugin.cpp @@ -6,7 +6,11 @@ #include "sendnotificationsplugin.h" -#include "notificationslistener.h" +#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) +#include "dbusnotificationslistener.h" +#elif defined(Q_OS_WIN) +#include "windowsnotificationslistener.h" +#endif #include @@ -15,7 +19,11 @@ K_PLUGIN_CLASS_WITH_JSON(SendNotificationsPlugin, "kdeconnect_sendnotifications. SendNotificationsPlugin::SendNotificationsPlugin(QObject *parent, const QVariantList &args) : KdeConnectPlugin(parent, args) { - notificationsListener = new NotificationsListener(this); +#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) + notificationsListener = new DBusNotificationsListener(this); +#elif defined(Q_OS_WIN) + notificationsListener = new WindowsNotificationsListener(this); +#endif } SendNotificationsPlugin::~SendNotificationsPlugin() diff --git a/plugins/sendnotifications/sendnotificationsplugin.h b/plugins/sendnotifications/sendnotificationsplugin.h index 563b0f672..32124e797 100644 --- a/plugins/sendnotifications/sendnotificationsplugin.h +++ b/plugins/sendnotifications/sendnotificationsplugin.h @@ -8,8 +8,6 @@ #include "core/kdeconnectplugin.h" -#define PACKET_TYPE_NOTIFICATION QStringLiteral("kdeconnect.notification") - /* * This class is just a proxy for NotificationsDbusInterface * because it can not inherit from QDBusAbstractAdaptor and diff --git a/plugins/sendnotifications/windowsnotificationslistener.cpp b/plugins/sendnotifications/windowsnotificationslistener.cpp new file mode 100644 index 000000000..9e3913626 --- /dev/null +++ b/plugins/sendnotifications/windowsnotificationslistener.cpp @@ -0,0 +1,124 @@ +/** + * SPDX-FileCopyrightText: 2023 Fushan Wen + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "windowsnotificationslistener.h" + +#include + +#include + +#include +#include +#include + +#include "plugin_sendnotifications_debug.h" + +using namespace winrt; +using namespace Windows::ApplicationModel; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Notifications; +using namespace Windows::UI::Notifications::Management; +using namespace Windows::Storage::Streams; + +WindowsNotificationsListener::WindowsNotificationsListener(KdeConnectPlugin *aPlugin) + : NotificationsListener(aPlugin) +{ + setupWindowsUserNotificationListener(); +} + +WindowsNotificationsListener::~WindowsNotificationsListener() +{ + UserNotificationListener::Current().NotificationChanged(m_notificationEventToken); +} + +void WindowsNotificationsListener::setupWindowsUserNotificationListener() +{ + // Register the notification listener + const UserNotificationListenerAccessStatus accessStatus = UserNotificationListener::Current().RequestAccessAsync().get(); + if (accessStatus != UserNotificationListenerAccessStatus::Allowed) { + return; + } + + // Register the event handler for notifications + m_notificationEventToken = UserNotificationListener::Current().NotificationChanged({this, &WindowsNotificationsListener::onNotificationChanged}); +} + +void WindowsNotificationsListener::onNotificationChanged(const UserNotificationListener &sender, const UserNotificationChangedEventArgs &args) +{ + // Get the notification from the event arguments + const UserNotificationChangedKind changeKind = args.ChangeKind(); + if (changeKind == UserNotificationChangedKind::Removed) { + return; + } + + const UserNotification userNotification = sender.GetNotification(args.UserNotificationId()); + if (!userNotification) { + return; + } + + const AppDisplayInfo appDisplayInfo = userNotification.AppInfo().DisplayInfo(); + const std::wstring_view appDisplayName = appDisplayInfo.DisplayName(); + const QString appName = QString::fromWCharArray(appDisplayName.data(), appDisplayName.size()); + if (!checkApplicationName(appName)) { + return; + } + + const winrt::Windows::UI::Notifications::Notification notification = userNotification.Notification(); + const NotificationBinding toastBinding = notification.Visual().GetBinding(KnownNotificationBindings::ToastGeneric()); + if (!toastBinding) { + return; + } + const auto textElements = toastBinding.GetTextElements(); + if (textElements.Size() == 0) { + return; + } + + std::wstring ticker = static_cast(textElements.GetAt(0).Text()); + if (m_plugin->config()->getBool(QStringLiteral("generalIncludeBody"), true)) { + for (unsigned i = 1; i < textElements.Size(); ++i) { + ticker += L"\n" + textElements.GetAt(i).Text(); + } + } + + const QString content = QString::fromStdWString(ticker); + if (checkIsInBlacklist(appName, content)) { + return; + } + + static unsigned id = 0; + NetworkPacket np(PACKET_TYPE_NOTIFICATION, + {{QStringLiteral("id"), id++}, + {QStringLiteral("appName"), appName}, + {QStringLiteral("ticker"), content}, + {QStringLiteral("isClearable"), true}}); // A Windows notification is always clearable + + if (m_plugin->config()->getBool(QStringLiteral("generalSynchronizeIcons"), true)) { + const RandomAccessStreamReference appLogoStreamReference = appDisplayInfo.GetLogo(Size(64, 64)); + if (appLogoStreamReference) { // Can be false when Package.appxmanifest doesn't contain icons + // Read the logo stream into a buffer + IRandomAccessStreamWithContentType logoStream = appLogoStreamReference.OpenReadAsync().get(); // TODO Port to coroutine + DataReader reader(logoStream); + reader.LoadAsync(static_cast(logoStream.Size())).get(); + std::vector bufferArray; + bufferArray.resize(reader.UnconsumedBufferLength()); + if (!bufferArray.empty()) { + reader.ReadBytes({bufferArray.data(), bufferArray.data() + bufferArray.size()}); + + QImage image; + if (image.loadFromData(bufferArray.data(), bufferArray.size())) { + // Write the logo buffer to the QIODevice + QSharedPointer iconSource = iconFromQImage(image); + if (iconSource) { + np.setPayload(iconSource, iconSource->size()); + } + } + } + } + } + + m_plugin->sendPacket(np); +} diff --git a/plugins/sendnotifications/windowsnotificationslistener.h b/plugins/sendnotifications/windowsnotificationslistener.h new file mode 100644 index 000000000..948cc24b5 --- /dev/null +++ b/plugins/sendnotifications/windowsnotificationslistener.h @@ -0,0 +1,29 @@ +/** + * SPDX-FileCopyrightText: 2023 Fushan Wen + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "notificationslistener.h" + +#include +#include +#include + +class WindowsNotificationsListener : public NotificationsListener +{ + Q_OBJECT + +public: + explicit WindowsNotificationsListener(KdeConnectPlugin *aPlugin); + ~WindowsNotificationsListener() override; + +private: + void setupWindowsUserNotificationListener(); + void onNotificationChanged(const winrt::Windows::UI::Notifications::Management::UserNotificationListener &sender, + const winrt::Windows::UI::Notifications::UserNotificationChangedEventArgs &args); + + winrt::event_token m_notificationEventToken; +};