From c4ce19e9cd4b8d046c1643e018c6a65ef285673e Mon Sep 17 00:00:00 2001 From: Fushan Wen Date: Mon, 31 Jul 2023 01:05:15 +0800 Subject: [PATCH] plugins/sendnotifications: add support for Windows --- plugins/CMakeLists.txt | 5 +- plugins/sendnotifications/CMakeLists.txt | 11 +- .../dbusnotificationslistener.cpp | 427 ++++++++++++++++ .../dbusnotificationslistener.h | 58 +++ .../notificationslistener.cpp | 465 ++---------------- .../sendnotifications/notificationslistener.h | 50 +- .../sendnotificationsplugin.cpp | 18 +- .../sendnotificationsplugin.h | 2 - .../windowsnotificationslistener.cpp | 129 +++++ .../windowsnotificationslistener.h | 29 ++ 10 files changed, 733 insertions(+), 461 deletions(-) create mode 100644 plugins/sendnotifications/dbusnotificationslistener.cpp create mode 100644 plugins/sendnotifications/dbusnotificationslistener.h create mode 100644 plugins/sendnotifications/windowsnotificationslistener.cpp create mode 100644 plugins/sendnotifications/windowsnotificationslistener.h 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/sendnotifications/CMakeLists.txt b/plugins/sendnotifications/CMakeLists.txt index 13bc0fc1c..dfc648a55 100644 --- a/plugins/sendnotifications/CMakeLists.txt +++ b/plugins/sendnotifications/CMakeLists.txt @@ -5,19 +5,26 @@ target_sources(kdeconnect_sendnotifications PRIVATE notificationslistener.cpp notifyingapplication.cpp ) + target_link_libraries(kdeconnect_sendnotifications kdeconnectcore - Qt::DBus KF${QT_MAJOR_VERSION}::I18n Qt::Gui KF${QT_MAJOR_VERSION}::IconThemes KF${QT_MAJOR_VERSION}::ConfigCore - ${DBus_LIBRARIES} ) + +if(WIN32) +target_sources(kdeconnect_sendnotifications PRIVATE windowsnotificationslistener.cpp) +target_link_libraries(kdeconnect_sendnotifications runtimeobject windowsapp) +else() +target_sources(kdeconnect_sendnotifications PRIVATE dbusnotificationslistener.cpp) +target_link_libraries(kdeconnect_sendnotifications ${DBus_LIBRARIES}) target_include_directories(kdeconnect_sendnotifications SYSTEM PRIVATE ${DBus_INCLUDE_DIRS} ) +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..b3fe47e37 --- /dev/null +++ b/plugins/sendnotifications/dbusnotificationslistener.cpp @@ -0,0 +1,427 @@ +/** + * SPDX-FileCopyrightText: 2015 Holger Kaelberer + * SPDX-FileCopyrightText: 2018 Richard Liebscher + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "dbusnotificationslistener.h" + +#include + +#include +#include +#include + +#include +#include + +#include "plugin_sendnotifications_debug.h" +#include +#include + +namespace { +// https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html +inline constexpr const char *NOTIFY_SIGNATURE = "susssasa{sv}i"; +inline constexpr const char *IMAGE_DATA_SIGNATURE = "iiibiiay"; + +QString becomeMonitor(DBusConnection *conn, const char *match) +{ + // message + DBusMessage *msg = dbus_message_new_method_call(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_MONITORING, "BecomeMonitor"); + Q_ASSERT(msg != nullptr); + + // arguments + const char *matches[] = {match}; + const char **matches_ = matches; + dbus_uint32_t flags = 0; + + bool success = dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &matches_, 1, DBUS_TYPE_UINT32, &flags, DBUS_TYPE_INVALID); + if (!success) { + dbus_message_unref(msg); + return QStringLiteral("Failed to call dbus_message_append_args"); + } + + // send + // TODO: wait and check for error: dbus_connection_send_with_reply_and_block + success = dbus_connection_send(conn, msg, nullptr); + if (!success) { + dbus_message_unref(msg); + return QStringLiteral("Failed to call dbus_connection_send"); + } + + dbus_message_unref(msg); + + return QString(); +} + +extern "C" DBusHandlerResult handleMessageFromC(DBusConnection *, DBusMessage *message, void *user_data) +{ + auto *self = static_cast(user_data); + if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "Notify")) { + self->handleNotifyCall(message); + } + // Monitors must not allow libdbus to reply to messages, so we eat the message. + return DBUS_HANDLER_RESULT_HANDLED; +} + +unsigned nextUnsigned(DBusMessageIter *iter) +{ + Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_UINT32); + DBusBasicValue value; + dbus_message_iter_get_basic(iter, &value); + dbus_message_iter_next(iter); + return value.u32; +} + +int nextInt(DBusMessageIter *iter) +{ + Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_INT32); + DBusBasicValue value; + dbus_message_iter_get_basic(iter, &value); + dbus_message_iter_next(iter); + return value.i32; +} + +QString nextString(DBusMessageIter *iter) +{ + Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING); + DBusBasicValue value; + dbus_message_iter_get_basic(iter, &value); + dbus_message_iter_next(iter); + return QString::fromUtf8(value.str); +} + +QStringList nextStringList(DBusMessageIter *iter) +{ + DBusMessageIter sub; + dbus_message_iter_recurse(iter, &sub); + dbus_message_iter_next(iter); + + QStringList list; + while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { + list.append(nextString(&sub)); + } + return list; +} + +QVariant nextVariant(DBusMessageIter *iter) +{ + int type = dbus_message_iter_get_arg_type(iter); + if (type != DBUS_TYPE_VARIANT) { + return QVariant(); + } + + DBusMessageIter sub; + dbus_message_iter_recurse(iter, &sub); + dbus_message_iter_next(iter); + + type = dbus_message_iter_get_arg_type(&sub); + if (dbus_type_is_basic(type)) { + DBusBasicValue value; + dbus_message_iter_get_basic(&sub, &value); + switch (type) { + case DBUS_TYPE_BOOLEAN: + return QVariant(value.bool_val); + case DBUS_TYPE_INT16: + return QVariant(value.i16); + case DBUS_TYPE_INT32: + return QVariant(value.i32); + case DBUS_TYPE_INT64: + return QVariant((qlonglong)value.i64); + case DBUS_TYPE_UINT16: + return QVariant(value.u16); + case DBUS_TYPE_UINT32: + return QVariant(value.u32); + case DBUS_TYPE_UINT64: + return QVariant((qulonglong)value.u64); + case DBUS_TYPE_BYTE: + return QVariant(value.byt); + case DBUS_TYPE_DOUBLE: + return QVariant(value.dbl); + case DBUS_TYPE_STRING: + return QVariant(QString::fromUtf8(value.str)); + case DBUS_STRUCT_BEGIN_CHAR: { + + } + default: + break; + } + } + + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unimplemented conversation of type" << QChar(type) << type; + + return QVariant(); +} + +static QVariantMap nextVariantMap(DBusMessageIter *iter) +{ + DBusMessageIter sub; + dbus_message_iter_recurse(iter, &sub); + dbus_message_iter_next(iter); + + QVariantMap map; + while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { + DBusMessageIter entry; + dbus_message_iter_recurse(&sub, &entry); + dbus_message_iter_next(&sub); + QString key = nextString(&entry); + QVariant value = nextVariant(&entry); + map.insert(key, value); + } + return map; +} +} + +void DBusNotificationsListenerThread::run() +{ + DBusError err = DBUS_ERROR_INIT; + m_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "D-Bus connection failed" << err.message; + dbus_error_free(&err); + return; + } + + Q_ASSERT(m_connection != nullptr); + + dbus_connection_set_route_peer_messages(m_connection, true); + dbus_connection_set_exit_on_disconnect(m_connection, false); + dbus_connection_add_filter(m_connection, handleMessageFromC, this, nullptr); + + QString error = becomeMonitor(m_connection, + "interface='org.freedesktop.Notifications'," + "member='Notify'"); + + if (!error.isEmpty()) { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).noquote() << "Failed to become a DBus monitor." + << "No notifictions will be sent. Error:" << error; + } + + // wake up every minute to see if we are still connected + while (m_connection != nullptr) { + dbus_connection_read_write_dispatch(m_connection, 60 * 1000); + } + + deleteLater(); +} + +void DBusNotificationsListenerThread::stop() +{ + if (m_connection) { + dbus_connection_close(m_connection); + dbus_connection_unref(m_connection); + m_connection = nullptr; + } +} + +void DBusNotificationsListenerThread::handleNotifyCall(DBusMessage *message) +{ + DBusMessageIter iter; + dbus_message_iter_init(message, &iter); + + if (!dbus_message_has_signature(message, NOTIFY_SIGNATURE)) { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).nospace() + << "Call to Notify has wrong signature. Expected " << NOTIFY_SIGNATURE << ", got " << dbus_message_get_signature(message); + return; + } + + QString appName = nextString(&iter); + uint replacesId = nextUnsigned(&iter); + QString appIcon = nextString(&iter); + QString summary = nextString(&iter); + QString body = nextString(&iter); + QStringList actions = nextStringList(&iter); + QVariantMap hints = nextVariantMap(&iter); + int timeout = nextInt(&iter); + + Q_EMIT notificationReceived(appName, replacesId, appIcon, summary, body, actions, hints, timeout); +} + +DBusNotificationsListener::DBusNotificationsListener(KdeConnectPlugin *aPlugin) + : NotificationsListener(aPlugin) + , m_thread(new DBusNotificationsListenerThread) +{ + connect(m_thread, &DBusNotificationsListenerThread::notificationReceived, this, &DBusNotificationsListener::onNotify); + m_thread->start(); +} + +DBusNotificationsListener::~DBusNotificationsListener() +{ + m_thread->stop(); + m_thread->quit(); +} + +void DBusNotificationsListener::onNotify(const QString &appName, + uint replacesId, + const QString &appIcon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout) +{ + Q_UNUSED(actions); + + // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Got notification appName=" << appName << "replacesId=" << replacesId + // << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout; + + auto *config = m_plugin->config(); + if (timeout > 0 && config->getBool(QStringLiteral("generalPersistent"), false)) { + return; + } + + if (!checkApplicationName(appName, appIcon)) { + return; + } + + int urgency = -1; + auto urgencyHint = hints.constFind(QStringLiteral("urgency")); + if (urgencyHint != hints.cend()) { + bool ok = false; + urgency = urgencyHint->toInt(&ok); + if (!ok) { + urgency = -1; + } + } + if (urgency > -1 && urgency < config->getInt(QStringLiteral("generalUrgency"), 0)) { + return; + } + + if (summary.isEmpty()) { + return; + } + + const bool includeBody = config->getBool(QStringLiteral("generalIncludeBody"), true); + + QString ticker = summary; + if (!body.isEmpty() && includeBody) { + ticker += QLatin1String(": ") + body; + } + + if (checkIsInBlacklist(appName, ticker)) { + return; + } + + // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Sending notification from" << appName << ":" < 0 ? replacesId : id++}, + {QStringLiteral("appName"), appName}, + {QStringLiteral("ticker"), ticker}, + {QStringLiteral("isClearable"), timeout == -1}, + {QStringLiteral("title"), summary}, + {QStringLiteral("silent"), false}, + }); + + if (!body.isEmpty() && includeBody) { + np.set(QStringLiteral("text"), body); + } + + // Only send icon on first notify (replacesId == 0) + if (config->getBool(QStringLiteral("generalSynchronizeIcons"), true) && replacesId == 0) { + QSharedPointer iconSource; + // try different image sources according to priorities in notifications-spec version 1.2: + auto it = hints.constFind(QStringLiteral("image-data")); + if (it != hints.cend() || (it = hints.constFind(QStringLiteral("image_data"))) != hints.cend()) { + iconSource = iconForImageData(it.value()); + } else if ((it = hints.constFind(QStringLiteral("image-path"))) != hints.cend() || (it = hints.constFind(QStringLiteral("image_path"))) != hints.cend()) { + iconSource = iconForIconName(it.value().toString()); + } else if (!appIcon.isEmpty()) { + iconSource = iconForIconName(appIcon); + } else if ((it = hints.constFind(QStringLiteral("icon_data"))) != hints.cend()) { + iconSource = iconForImageData(it.value()); + } + if (iconSource) { + np.setPayload(iconSource, iconSource->size()); + } + } + + m_plugin->sendPacket(np); +} + +bool DBusNotificationsListener::parseImageDataArgument(const QVariant &argument, + int &width, + int &height, + int &rowStride, + int &bitsPerSample, + int &channels, + bool &hasAlpha, + QByteArray &imageData) const +{ + // FIXME + // if (!argument.canConvert()) { + // return false; + // } + // const QDBusArgument dbusArg = argument.value(); + // dbusArg.beginStructure(); + // dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData; + // dbusArg.endStructure(); + return true; +} + +QSharedPointer DBusNotificationsListener::iconForImageData(const QVariant &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) const +{ + int size = KIconLoader::SizeHuge; // use big size to allow for good quality on high-DPI mobile devices + QString iconPath = iconName; + if (!QFile::exists(iconName)) { + const KIconTheme *iconTheme = KIconLoader::global()->theme(); + if (iconTheme) { + iconPath = iconTheme->iconPath(iconName + QLatin1String(".png"), size, KIconLoader::MatchBest); + if (iconPath.isEmpty()) { + iconPath = iconTheme->iconPath(iconName + QLatin1String(".svg"), size, KIconLoader::MatchBest); + if (iconPath.isEmpty()) { + iconPath = iconTheme->iconPath(iconName + QLatin1String(".svgz"), size, KIconLoader::MatchBest); + } + } + } else { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "KIconLoader has no theme set"; + } + } + if (iconPath.isEmpty()) { + qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not find notification icon:" << iconName; + return QSharedPointer(); + } else if (iconPath.endsWith(QLatin1String(".png"))) { + return QSharedPointer(new QFile(iconPath)); + } else { + // TODO: cache icons + return iconFromQImage(QImage(iconPath)); + } +} + +#include "moc_dbusnotificationslistener.cpp" diff --git a/plugins/sendnotifications/dbusnotificationslistener.h b/plugins/sendnotifications/dbusnotificationslistener.h new file mode 100644 index 000000000..fe24d7c2e --- /dev/null +++ b/plugins/sendnotifications/dbusnotificationslistener.h @@ -0,0 +1,58 @@ +/** + * SPDX-FileCopyrightText: 2015 Holger Kaelberer + * SPDX-FileCopyrightText: 2018 Richard Liebscher + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "notificationslistener.h" + +#include + +#include + +#include + +class DBusNotificationsListenerThread : public QThread +{ + Q_OBJECT + +public: + void run() override; + void stop(); + void handleNotifyCall(DBusMessage *message); + +Q_SIGNALS: + void notificationReceived(const QString &, uint, const QString &, const QString &, const QString &, const QStringList &, const QVariantMap &, int); + +private: + std::atomic m_connection = nullptr; +}; + +class DBusNotificationsListener : public NotificationsListener +{ + Q_OBJECT + +public: + explicit DBusNotificationsListener(KdeConnectPlugin *aPlugin); + ~DBusNotificationsListener() override; + +private: + void onNotify(const QString &, uint, const QString &, const QString &, const QString &, const QStringList &, const QVariantMap &, int); + + bool parseImageDataArgument(const QVariant &argument, + int &width, + int &height, + int &rowStride, + int &bitsPerSample, + int &channels, + bool &hasAlpha, + QByteArray &imageData) const; + QSharedPointer iconForImageData(const QVariant &argument) const; + QSharedPointer iconForIconName(const QString &iconName) const; + QSharedPointer pngFromImage() const; + + DBusNotificationsListenerThread *m_thread = nullptr; +}; diff --git a/plugins/sendnotifications/notificationslistener.cpp b/plugins/sendnotifications/notificationslistener.cpp index c59b34458..e873c170f 100644 --- a/plugins/sendnotifications/notificationslistener.cpp +++ b/plugins/sendnotifications/notificationslistener.cpp @@ -1,6 +1,5 @@ /** * SPDX-FileCopyrightText: 2015 Holger Kaelberer - * SPDX-FileCopyrightText: 2018 Richard Liebscher * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -8,251 +7,50 @@ #include "notificationslistener.h" #include -#include -#include -#include #include +#include -#include -#include - -#include -#include +#include +#include #include "notifyingapplication.h" #include "plugin_sendnotifications_debug.h" -#include "sendnotificationsplugin.h" - -const char *NOTIFY_SIGNATURE = "susssasa{sv}i"; - -QString becomeMonitor(DBusConnection *conn, const char *match) -{ - // message - DBusMessage *msg = dbus_message_new_method_call(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_MONITORING, "BecomeMonitor"); - Q_ASSERT(msg != nullptr); - - // arguments - const char *matches[] = {match}; - const char **matches_ = matches; - dbus_uint32_t flags = 0; - - bool success = dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &matches_, 1, DBUS_TYPE_UINT32, &flags, DBUS_TYPE_INVALID); - if (!success) { - dbus_message_unref(msg); - return QStringLiteral("Failed to call dbus_message_append_args"); - } - - // send - // TODO: wait and check for error: dbus_connection_send_with_reply_and_block - success = dbus_connection_send(conn, msg, nullptr); - if (!success) { - dbus_message_unref(msg); - return QStringLiteral("Failed to call dbus_connection_send"); - } - - dbus_message_unref(msg); - - return QString(); -} - -extern "C" DBusHandlerResult handleMessageFromC(DBusConnection *, DBusMessage *message, void *user_data) -{ - auto *self = static_cast(user_data); - if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "Notify")) { - self->handleNotifyCall(message); - } - // Monitors must not allow libdbus to reply to messages, so we eat the message. - return DBUS_HANDLER_RESULT_HANDLED; -} - -void NotificationsListenerThread::stop() -{ - if (m_connection) { - dbus_connection_close(m_connection); - dbus_connection_unref(m_connection); - m_connection = nullptr; - } -} - -void NotificationsListenerThread::run() -{ - DBusError err = DBUS_ERROR_INIT; - m_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &err); - if (dbus_error_is_set(&err)) { - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "D-Bus connection failed" << err.message; - dbus_error_free(&err); - return; - } - - Q_ASSERT(m_connection != nullptr); - - dbus_connection_set_route_peer_messages(m_connection, true); - dbus_connection_set_exit_on_disconnect(m_connection, false); - dbus_connection_add_filter(m_connection, handleMessageFromC, this, nullptr); - - QString error = becomeMonitor(m_connection, - "interface='org.freedesktop.Notifications'," - "member='Notify'"); - - if (!error.isEmpty()) { - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).noquote() << "Failed to become a DBus monitor." - << "No notifictions will be sent. Error:" << error; - } - - // wake up every minute to see if we are still connected - while (m_connection != nullptr) { - dbus_connection_read_write_dispatch(m_connection, 60 * 1000); - } - - deleteLater(); -} - -static unsigned nextUnsigned(DBusMessageIter *iter) -{ - Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_UINT32); - DBusBasicValue value; - dbus_message_iter_get_basic(iter, &value); - dbus_message_iter_next(iter); - return value.u32; -} - -static int nextInt(DBusMessageIter *iter) -{ - Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_INT32); - DBusBasicValue value; - dbus_message_iter_get_basic(iter, &value); - dbus_message_iter_next(iter); - return value.i32; -} - -static QString nextString(DBusMessageIter *iter) -{ - Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING); - DBusBasicValue value; - dbus_message_iter_get_basic(iter, &value); - dbus_message_iter_next(iter); - return QString::fromUtf8(value.str); -} - -static QStringList nextStringList(DBusMessageIter *iter) -{ - DBusMessageIter sub; - dbus_message_iter_recurse(iter, &sub); - dbus_message_iter_next(iter); - - QStringList list; - while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { - list.append(nextString(&sub)); - } - return list; -} - -static QVariant nextVariant(DBusMessageIter *iter) -{ - int type = dbus_message_iter_get_arg_type(iter); - if (type != DBUS_TYPE_VARIANT) - return QVariant(); - - DBusMessageIter sub; - dbus_message_iter_recurse(iter, &sub); - dbus_message_iter_next(iter); - - type = dbus_message_iter_get_arg_type(&sub); - if (dbus_type_is_basic(type)) { - DBusBasicValue value; - dbus_message_iter_get_basic(&sub, &value); - switch (type) { - case DBUS_TYPE_BOOLEAN: - return QVariant(value.bool_val); - case DBUS_TYPE_INT16: - return QVariant(value.i16); - case DBUS_TYPE_INT32: - return QVariant(value.i32); - case DBUS_TYPE_INT64: - return QVariant((qlonglong)value.i64); - case DBUS_TYPE_UINT16: - return QVariant(value.u16); - case DBUS_TYPE_UINT32: - return QVariant(value.u32); - case DBUS_TYPE_UINT64: - return QVariant((qulonglong)value.u64); - case DBUS_TYPE_BYTE: - return QVariant(value.byt); - case DBUS_TYPE_DOUBLE: - return QVariant(value.dbl); - case DBUS_TYPE_STRING: - return QVariant(QString::fromUtf8(value.str)); - } - } - - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unimplemented conversation of type" << QChar(type) << type; - - return QVariant(); -} - -static QVariantMap nextVariantMap(DBusMessageIter *iter) -{ - DBusMessageIter sub; - dbus_message_iter_recurse(iter, &sub); - dbus_message_iter_next(iter); - - QVariantMap map; - while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { - DBusMessageIter entry; - dbus_message_iter_recurse(&sub, &entry); - dbus_message_iter_next(&sub); - QString key = nextString(&entry); - QVariant value = nextVariant(&entry); - map.insert(key, value); - } - return map; -} - -void NotificationsListenerThread::handleNotifyCall(DBusMessage *message) -{ - DBusMessageIter iter; - dbus_message_iter_init(message, &iter); - - if (!dbus_message_has_signature(message, NOTIFY_SIGNATURE)) { - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).nospace() - << "Call to Notify has wrong signature. Expected " << NOTIFY_SIGNATURE << ", got " << dbus_message_get_signature(message); - return; - } - - QString appName = nextString(&iter); - uint replacesId = nextUnsigned(&iter); - QString appIcon = nextString(&iter); - QString summary = nextString(&iter); - QString body = nextString(&iter); - QStringList actions = nextStringList(&iter); - QVariantMap hints = nextVariantMap(&iter); - int timeout = nextInt(&iter); - - Q_EMIT notificationReceived(appName, replacesId, appIcon, summary, body, actions, hints, timeout); -} +#include +#include NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin) : QObject(aPlugin) , m_plugin(aPlugin) - , m_thread(new NotificationsListenerThread()) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) qRegisterMetaTypeStreamOperators("NotifyingApplication"); #endif + setTranslatedAppName(); loadApplications(); connect(m_plugin->config(), &KdeConnectPluginConfig::configChanged, this, &NotificationsListener::loadApplications); - - connect(m_thread, &NotificationsListenerThread::notificationReceived, this, &NotificationsListener::onNotify); - - m_thread->start(); } NotificationsListener::~NotificationsListener() { - m_thread->stop(); - m_thread->quit(); + qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Destroying NotificationsListener"; +} + +void NotificationsListener::setTranslatedAppName() +{ + QString filePath = + QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/kdeconnect.notifyrc"), QStandardPaths::LocateFile); + if (filePath.isEmpty()) { + qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) + << "Couldn't find kdeconnect.notifyrc to hide kdeconnect notifications on the devices. Using default name."; + m_translatedAppName = QStringLiteral("KDE Connect"); + return; + } + + KConfig config(filePath, KConfig::OpenFlag::SimpleConfig); + KConfigGroup globalgroup(&config, QStringLiteral("Global")); + m_translatedAppName = globalgroup.readEntry(QStringLiteral("Name"), QStringLiteral("KDE Connect")); } void NotificationsListener::loadApplications() @@ -267,22 +65,39 @@ void NotificationsListener::loadApplications() // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Loaded" << m_applications.size() << " applications"; } -bool NotificationsListener::parseImageDataArgument(const QVariant &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 (!argument.canConvert()) + if (m_translatedAppName == appName) { return false; - const QDBusArgument dbusArg = argument.value(); - dbusArg.beginStructure(); - dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData; - dbusArg.endStructure(); - return true; + } + + 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); + + return true; + } else { + return it->active; + } +} + +bool NotificationsListener::checkIsInBlacklist(const QString &appName, const QString &content) +{ + 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 @@ -292,180 +107,8 @@ QSharedPointer NotificationsListener::iconFromQImage(const QImage &im qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer"; return QSharedPointer(); } - return buffer; -} - -QSharedPointer NotificationsListener::iconForImageData(const QVariant &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 NotificationsListener::iconForIconName(const QString &iconName) const -{ - int size = KIconLoader::SizeHuge; // use big size to allow for good quality on high-DPI mobile devices - QString iconPath = iconName; - if (!QFile::exists(iconName)) { - const KIconTheme *iconTheme = KIconLoader::global()->theme(); - if (iconTheme) { - iconPath = iconTheme->iconPath(iconName + QLatin1String(".png"), size, KIconLoader::MatchBest); - if (iconPath.isEmpty()) { - iconPath = iconTheme->iconPath(iconName + QLatin1String(".svg"), size, KIconLoader::MatchBest); - if (iconPath.isEmpty()) { - iconPath = iconTheme->iconPath(iconName + QLatin1String(".svgz"), size, KIconLoader::MatchBest); - } - } - } else { - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "KIconLoader has no theme set"; - } - } - if (iconPath.isEmpty()) { - qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not find notification icon:" << iconName; - return QSharedPointer(); - } else if (iconPath.endsWith(QLatin1String(".png"))) { - return QSharedPointer(new QFile(iconPath)); - } else { - // TODO: cache icons - return iconFromQImage(QImage(iconPath)); - } -} - -void NotificationsListener::onNotify(const QString &appName, - uint replacesId, - const QString &appIcon, - const QString &summary, - const QString &body, - const QStringList &actions, - const QVariantMap &hints, - int timeout) -{ - Q_UNUSED(actions); - - // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Got notification appName=" << appName << "replacesId=" << replacesId - // << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout; - - if (appName == QStringLiteral("KDE Connect")) { - qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Ignoring my own notification"; - return; - } - - auto *config = m_plugin->config(); - - NotifyingApplication app; - if (!m_applications.contains(appName)) { - // new application -> add to config - app.name = appName; - app.icon = appIcon; - app.active = true; - app.blacklistExpression = QRegularExpression(); - m_applications.insert(app.name, app); - // update config - QVariantList list; - for (const auto &a : std::as_const(m_applications)) - list << QVariant::fromValue(a); - config->setList(QStringLiteral("applications"), list); - // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Added new application to config:" << app; - } else { - app = m_applications.value(appName); - } - - if (!app.active) { - return; - } - - if (timeout > 0 && config->getBool(QStringLiteral("generalPersistent"), false)) { - return; - } - - int urgency = -1; - auto urgencyHint = hints.find(QStringLiteral("urgency")); - if (urgencyHint != hints.end()) { - bool ok; - urgency = urgencyHint->toInt(&ok); - if (!ok) - urgency = -1; - } - if (urgency > -1 && urgency < config->getInt(QStringLiteral("generalUrgency"), 0)) { - return; - } - - if (summary.isEmpty()) { - return; - } - - const bool includeBody = config->getBool(QStringLiteral("generalIncludeBody"), true); - - QString ticker = summary; - if (!body.isEmpty() && includeBody) { - ticker += QStringLiteral(": ") + body; - } - - if (app.blacklistExpression.isValid() && !app.blacklistExpression.pattern().isEmpty() && app.blacklistExpression.match(ticker).hasMatch()) { - return; - } - - // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Sending notification from" << appName << ":" < 0 ? replacesId : ++id)}, - {QStringLiteral("appName"), appName}, - {QStringLiteral("ticker"), ticker}, - {QStringLiteral("isClearable"), timeout == -1}, - {QStringLiteral("title"), summary}, - {QStringLiteral("silent"), silent}, - }); - - if (!body.isEmpty() && includeBody) { - np.set(QStringLiteral("text"), body); - } - - // Only send icon on first notify (replacesId == 0) - if (config->getBool(QStringLiteral("generalSynchronizeIcons"), true) && replacesId == 0) { - QSharedPointer iconSource; - // try different image sources according to priorities in notifications-spec version 1.2: - auto it = hints.find(QStringLiteral("image-data")); - if (it != hints.end() || (it = hints.find(QStringLiteral("image_data"))) != hints.end()) { - iconSource = iconForImageData(it.value()); - } else if ((it = hints.find(QStringLiteral("image-path"))) != hints.end() || (it = hints.find(QStringLiteral("image_path"))) != hints.end()) { - iconSource = iconForIconName(it.value().toString()); - } else if (!appIcon.isEmpty()) { - iconSource = iconForIconName(appIcon); - } else if ((it = hints.find(QStringLiteral("icon_data"))) != hints.end()) { - iconSource = iconForImageData(it.value()); - } - if (iconSource) { - np.setPayload(iconSource, iconSource->size()); - } - } - - m_plugin->sendPacket(np); -} - #include "moc_notificationslistener.cpp" diff --git a/plugins/sendnotifications/notificationslistener.h b/plugins/sendnotifications/notificationslistener.h index 5c9f87d43..e93258605 100644 --- a/plugins/sendnotifications/notificationslistener.h +++ b/plugins/sendnotifications/notificationslistener.h @@ -1,42 +1,22 @@ /** * SPDX-FileCopyrightText: 2015 Holger Kaelberer - * SPDX-FileCopyrightText: 2018 Richard Liebscher * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #pragma once +#include + #include #include -#include #include -#include -#include -#include class KdeConnectPlugin; - struct NotifyingApplication; #define PACKET_TYPE_NOTIFICATION QStringLiteral("kdeconnect.notification") -class NotificationsListenerThread : public QThread -{ - Q_OBJECT -public: - void run() override; - void stop(); - void handleNotifyCall(DBusMessage *message); - -Q_SIGNALS: - void notificationReceived(const QString &, uint, const QString &, const QString &, const QString &, const QStringList &, const QVariantMap &, int); - -private: - std::atomic m_connection = nullptr; -}; - -// TODO: make a singleton, shared for all devices class NotificationsListener : public QObject { Q_OBJECT @@ -46,28 +26,18 @@ public: ~NotificationsListener() override; protected: - 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(const QVariant &argument, - int &width, - int &height, - int &rowStride, - int &bitsPerSample, - int &channels, - bool &hasAlpha, - QByteArray &imageData) const; - QSharedPointer iconForImageData(const QVariant &argument) const; - QSharedPointer iconForIconName(const QString &iconName) const; + 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; + private Q_SLOTS: void loadApplications(); - void onNotify(const QString &, uint, const QString &, const QString &, const QString &, const QStringList &, const QVariantMap &, int); private: - QSharedPointer pngFromImage(); - NotificationsListenerThread *m_thread; + void setTranslatedAppName(); + + QHash m_applications; + QString m_translatedAppName; }; diff --git a/plugins/sendnotifications/sendnotificationsplugin.cpp b/plugins/sendnotifications/sendnotificationsplugin.cpp index 44b807978..fc2ab25a1 100644 --- a/plugins/sendnotifications/sendnotificationsplugin.cpp +++ b/plugins/sendnotifications/sendnotificationsplugin.cpp @@ -6,8 +6,14 @@ #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" +#include +#include // GetCurrentPackageFullName +#endif #include K_PLUGIN_CLASS_WITH_JSON(SendNotificationsPlugin, "kdeconnect_sendnotifications.json") @@ -15,7 +21,15 @@ 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) + std::uint32_t bufferLength = 100; + std::array buffer; + if (GetCurrentPackageFullName(&bufferLength, buffer.data()) == ERROR_SUCCESS) { + 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..137709e9a --- /dev/null +++ b/plugins/sendnotifications/windowsnotificationslistener.cpp @@ -0,0 +1,129 @@ +/** + * 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; + if (id == std::numeric_limits::max()) { + 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); +} + +#include "moc_windowsnotificationslistener.cpp" 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; +};