plugins/sendnotifications: add support for Windows
This commit is contained in:
parent
47cb369e01
commit
a189a5edd8
11 changed files with 546 additions and 306 deletions
|
@ -46,11 +46,8 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NOT WIN32 AND NOT APPLE)
|
|
||||||
add_subdirectory(sendnotifications)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT APPLE)
|
if(NOT APPLE)
|
||||||
|
add_subdirectory(sendnotifications)
|
||||||
add_subdirectory(sftp)
|
add_subdirectory(sftp)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,8 @@ kdeconnect_add_plugin(kdeconnect_mousepad SOURCES mousepadplugin.cpp abstractrem
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp ${SRCS})
|
target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp ${SRCS})
|
||||||
|
|
||||||
target_sources(kdeconnect_mousepad PRIVATE ${wayland_SRCS})
|
target_sources(kdeconnect_mousepad PRIVATE ${wayland_SRCS})
|
||||||
target_link_libraries(kdeconnect_mousepad Wayland::Client Qt::WaylandClient PkgConfig::XkbCommon)
|
target_link_libraries(kdeconnect_mousepad Wayland::Client Qt::WaylandClient PkgConfig::XkbCommon)
|
||||||
target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp)
|
|
||||||
|
|
||||||
if (WITH_X11)
|
if (WITH_X11)
|
||||||
find_package(LibFakeKey REQUIRED)
|
find_package(LibFakeKey REQUIRED)
|
||||||
|
|
|
@ -5,6 +5,7 @@ target_sources(kdeconnect_sendnotifications PRIVATE
|
||||||
notificationslistener.cpp
|
notificationslistener.cpp
|
||||||
notifyingapplication.cpp
|
notifyingapplication.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(kdeconnect_sendnotifications
|
target_link_libraries(kdeconnect_sendnotifications
|
||||||
kdeconnectcore
|
kdeconnectcore
|
||||||
Qt::DBus
|
Qt::DBus
|
||||||
|
@ -12,9 +13,16 @@ target_link_libraries(kdeconnect_sendnotifications
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
KF${QT_MAJOR_VERSION}::IconThemes
|
KF${QT_MAJOR_VERSION}::IconThemes
|
||||||
KF${QT_MAJOR_VERSION}::ConfigCore
|
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
|
# Config
|
||||||
kdeconnect_add_kcm(kdeconnect_sendnotifications_config)
|
kdeconnect_add_kcm(kdeconnect_sendnotifications_config)
|
||||||
|
|
||||||
|
|
288
plugins/sendnotifications/dbusnotificationslistener.cpp
Normal file
288
plugins/sendnotifications/dbusnotificationslistener.cpp
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
|
||||||
|
* SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dbusnotificationslistener.h"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <QDBusArgument>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QScopeGuard>
|
||||||
|
#include <dbushelper.h>
|
||||||
|
|
||||||
|
#include <kiconloader.h>
|
||||||
|
#include <kicontheme.h>
|
||||||
|
|
||||||
|
#include <core/kdeconnectplugin.h>
|
||||||
|
|
||||||
|
#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<QIODevice> 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<QIODevice>();
|
||||||
|
|
||||||
|
if (bitsPerSample != 8) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unsupported image format:"
|
||||||
|
<< "width=" << width << "height=" << height << "rowStride=" << rowStride
|
||||||
|
<< "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha;
|
||||||
|
return QSharedPointer<QIODevice>();
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image(reinterpret_cast<uchar *>(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
||||||
|
if (hasAlpha) {
|
||||||
|
image = image.rgbSwapped(); // RGBA --> ARGB
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<QIODevice> buffer = iconFromQImage(image);
|
||||||
|
if (!buffer) {
|
||||||
|
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer";
|
||||||
|
return QSharedPointer<QIODevice>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<QIODevice> 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<QIODevice>(new QFile(iconPath));
|
||||||
|
return QSharedPointer<QIODevice>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<DBusNotificationsListener *>(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<const QString>(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<QString, GVariant *> 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 << ":" <<ticker << "; appIcon=" << appIcon;
|
||||||
|
NetworkPacket np(PACKET_TYPE_NOTIFICATION,
|
||||||
|
{{QStringLiteral("id"), QString::number(replacesId > 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<QIODevice> 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;
|
||||||
|
}
|
41
plugins/sendnotifications/dbusnotificationslistener.h
Normal file
41
plugins/sendnotifications/dbusnotificationslistener.h
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "notificationslistener.h"
|
||||||
|
|
||||||
|
#include <gio/gio.h>
|
||||||
|
|
||||||
|
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<QIODevice> iconForImageData(GVariant *argument) const;
|
||||||
|
static QSharedPointer<QIODevice> iconForIconName(const QString &iconName);
|
||||||
|
|
||||||
|
static GDBusMessage *onMessageFiltered(GDBusConnection *connection, GDBusMessage *msg, int incoming, void *parent);
|
||||||
|
|
||||||
|
GDBusConnection *m_gdbusConnection = nullptr;
|
||||||
|
unsigned m_gdbusFilterId = 0;
|
||||||
|
};
|
|
@ -5,28 +5,18 @@
|
||||||
*/
|
*/
|
||||||
#include "notificationslistener.h"
|
#include "notificationslistener.h"
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <QBuffer>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
#include <KConfig>
|
#include <KConfig>
|
||||||
#include <KConfigGroup>
|
#include <KConfigGroup>
|
||||||
#include <QDBusArgument>
|
|
||||||
#include <QDBusInterface>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QScopeGuard>
|
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QtDebug>
|
|
||||||
|
|
||||||
#include <kiconloader.h>
|
|
||||||
#include <kicontheme.h>
|
|
||||||
|
|
||||||
#include <core/device.h>
|
|
||||||
#include <core/kdeconnectplugin.h>
|
#include <core/kdeconnectplugin.h>
|
||||||
|
|
||||||
#include <dbushelper.h>
|
|
||||||
|
|
||||||
#include "notifyingapplication.h"
|
#include "notifyingapplication.h"
|
||||||
|
|
||||||
#include "plugin_sendnotifications_debug.h"
|
#include "plugin_sendnotifications_debug.h"
|
||||||
#include "sendnotificationsplugin.h"
|
|
||||||
|
|
||||||
NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin)
|
NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin)
|
||||||
: QObject(aPlugin)
|
: QObject(aPlugin)
|
||||||
|
@ -36,22 +26,6 @@ NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin)
|
||||||
qRegisterMetaTypeStreamOperators<NotifyingApplication>("NotifyingApplication");
|
qRegisterMetaTypeStreamOperators<NotifyingApplication>("NotifyingApplication");
|
||||||
#endif
|
#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();
|
setTranslatedAppName();
|
||||||
loadApplications();
|
loadApplications();
|
||||||
|
|
||||||
|
@ -61,8 +35,6 @@ NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin)
|
||||||
NotificationsListener::~NotificationsListener()
|
NotificationsListener::~NotificationsListener()
|
||||||
{
|
{
|
||||||
qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Destroying NotificationsListener";
|
qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Destroying NotificationsListener";
|
||||||
g_dbus_connection_remove_filter(m_gdbusConnection, m_gdbusFilterId);
|
|
||||||
g_object_unref(m_gdbusConnection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationsListener::setTranslatedAppName()
|
void NotificationsListener::setTranslatedAppName()
|
||||||
|
@ -94,88 +66,45 @@ void NotificationsListener::loadApplications()
|
||||||
// qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Loaded" << applications.size() << " applications";
|
// qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Loaded" << applications.size() << " applications";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NotificationsListener::parseImageDataArgument(GVariant *argument,
|
bool NotificationsListener::checkApplicationName(const QString &appName, std::optional<std::reference_wrapper<const QString>> iconName)
|
||||||
int &width,
|
|
||||||
int &height,
|
|
||||||
int &rowStride,
|
|
||||||
int &bitsPerSample,
|
|
||||||
int &channels,
|
|
||||||
bool &hasAlpha,
|
|
||||||
QByteArray &imageData) const
|
|
||||||
{
|
{
|
||||||
if (g_variant_n_children(argument) != 7) {
|
if (m_translatedAppName == appName) {
|
||||||
return false;
|
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<NotifyingApplication>(a);
|
||||||
|
}
|
||||||
|
m_plugin->config()->setList(QStringLiteral("applications"), list);
|
||||||
|
|
||||||
variant = g_variant_get_child_value(argument, 0);
|
return true;
|
||||||
if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) {
|
} else {
|
||||||
return false;
|
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<QIODevice> NotificationsListener::iconForImageData(GVariant *argument) const
|
bool NotificationsListener::checkIsInBlacklist(const QString &appName, const QString &content)
|
||||||
{
|
{
|
||||||
int width, height, rowStride, bitsPerSample, channels;
|
auto appIt = m_applications.constFind(appName);
|
||||||
bool hasAlpha;
|
return appIt->blacklistExpression.isValid() && !appIt->blacklistExpression.pattern().isEmpty() && appIt->blacklistExpression.match(content).hasMatch();
|
||||||
QByteArray imageData;
|
}
|
||||||
|
|
||||||
if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample, channels, hasAlpha, imageData))
|
|
||||||
return QSharedPointer<QIODevice>();
|
|
||||||
|
|
||||||
if (bitsPerSample != 8) {
|
|
||||||
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unsupported image format:"
|
|
||||||
<< "width=" << width << "height=" << height << "rowStride=" << rowStride
|
|
||||||
<< "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha;
|
|
||||||
return QSharedPointer<QIODevice>();
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage image(reinterpret_cast<uchar *>(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
|
|
||||||
if (hasAlpha)
|
|
||||||
image = image.rgbSwapped(); // RGBA --> ARGB
|
|
||||||
|
|
||||||
|
QSharedPointer<QIODevice> NotificationsListener::iconFromQImage(const QImage &image) const
|
||||||
|
{
|
||||||
QSharedPointer<QBuffer> buffer = QSharedPointer<QBuffer>(new QBuffer);
|
QSharedPointer<QBuffer> buffer = QSharedPointer<QBuffer>(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";
|
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer";
|
||||||
return QSharedPointer<QIODevice>();
|
return QSharedPointer<QIODevice>();
|
||||||
}
|
}
|
||||||
|
@ -183,171 +112,4 @@ QSharedPointer<QIODevice> NotificationsListener::iconForImageData(GVariant *argu
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSharedPointer<QIODevice> 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<QIODevice>(new QFile(iconPath));
|
|
||||||
return QSharedPointer<QIODevice>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<NotificationsListener *>(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<NotifyingApplication>(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<QString, GVariant *> 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 << ":" <<ticker << "; appIcon=" << appIcon;
|
|
||||||
NetworkPacket np(PACKET_TYPE_NOTIFICATION,
|
|
||||||
{{QStringLiteral("id"), QString::number(replacesId > 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<QIODevice> 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"
|
#include "moc_notificationslistener.cpp"
|
||||||
|
|
|
@ -6,18 +6,18 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <optional>
|
||||||
#include <QDBusAbstractAdaptor>
|
|
||||||
#include <QDBusArgument>
|
|
||||||
#include <QFile>
|
|
||||||
#include <core/device.h>
|
|
||||||
|
|
||||||
#include <gio/gio.h>
|
#include <QIODevice>
|
||||||
|
|
||||||
|
#include <core/device.h>
|
||||||
|
|
||||||
class KdeConnectPlugin;
|
class KdeConnectPlugin;
|
||||||
class Notification;
|
class Notification;
|
||||||
struct NotifyingApplication;
|
struct NotifyingApplication;
|
||||||
|
|
||||||
|
#define PACKET_TYPE_NOTIFICATION QStringLiteral("kdeconnect.notification")
|
||||||
|
|
||||||
class NotificationsListener : public QObject
|
class NotificationsListener : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -27,31 +27,18 @@ public:
|
||||||
~NotificationsListener() override;
|
~NotificationsListener() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
bool checkApplicationName(const QString &appName, std::optional<std::reference_wrapper<const QString>> iconName = std::nullopt);
|
||||||
|
bool checkIsInBlacklist(const QString &appName, const QString &content);
|
||||||
|
QSharedPointer<QIODevice> iconFromQImage(const QImage &image) const;
|
||||||
|
|
||||||
KdeConnectPlugin *m_plugin;
|
KdeConnectPlugin *m_plugin;
|
||||||
QHash<QString, NotifyingApplication> 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<QIODevice> iconForImageData(GVariant *argument) const;
|
|
||||||
static QSharedPointer<QIODevice> iconForIconName(const QString &iconName);
|
|
||||||
|
|
||||||
static GDBusMessage *onMessageFiltered(GDBusConnection *connection, GDBusMessage *msg, int incoming, void *parent);
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void loadApplications();
|
void loadApplications();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setTranslatedAppName();
|
void setTranslatedAppName();
|
||||||
QString m_translatedAppName;
|
|
||||||
|
|
||||||
GDBusConnection *m_gdbusConnection = nullptr;
|
QHash<QString, NotifyingApplication> m_applications;
|
||||||
unsigned m_gdbusFilterId = 0;
|
QString m_translatedAppName;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
|
|
||||||
#include "sendnotificationsplugin.h"
|
#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 <KPluginFactory>
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
@ -15,7 +19,11 @@ K_PLUGIN_CLASS_WITH_JSON(SendNotificationsPlugin, "kdeconnect_sendnotifications.
|
||||||
SendNotificationsPlugin::SendNotificationsPlugin(QObject *parent, const QVariantList &args)
|
SendNotificationsPlugin::SendNotificationsPlugin(QObject *parent, const QVariantList &args)
|
||||||
: KdeConnectPlugin(parent, 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()
|
SendNotificationsPlugin::~SendNotificationsPlugin()
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
#include "core/kdeconnectplugin.h"
|
#include "core/kdeconnectplugin.h"
|
||||||
|
|
||||||
#define PACKET_TYPE_NOTIFICATION QStringLiteral("kdeconnect.notification")
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This class is just a proxy for NotificationsDbusInterface
|
* This class is just a proxy for NotificationsDbusInterface
|
||||||
* because it can not inherit from QDBusAbstractAdaptor and
|
* because it can not inherit from QDBusAbstractAdaptor and
|
||||||
|
|
124
plugins/sendnotifications/windowsnotificationslistener.cpp
Normal file
124
plugins/sendnotifications/windowsnotificationslistener.cpp
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "windowsnotificationslistener.h"
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
#include <core/kdeconnectplugin.h>
|
||||||
|
|
||||||
|
#include <winrt/Windows.ApplicationModel.h>
|
||||||
|
#include <winrt/Windows.Foundation.Collections.h>
|
||||||
|
#include <winrt/Windows.Storage.Streams.h>
|
||||||
|
|
||||||
|
#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<std::wstring>(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<uint32_t>(logoStream.Size())).get();
|
||||||
|
std::vector<unsigned char> 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<QIODevice> iconSource = iconFromQImage(image);
|
||||||
|
if (iconSource) {
|
||||||
|
np.setPayload(iconSource, iconSource->size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_plugin->sendPacket(np);
|
||||||
|
}
|
29
plugins/sendnotifications/windowsnotificationslistener.h
Normal file
29
plugins/sendnotifications/windowsnotificationslistener.h
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "notificationslistener.h"
|
||||||
|
|
||||||
|
#include <winrt/Windows.Foundation.h>
|
||||||
|
#include <winrt/Windows.UI.Notifications.Management.h>
|
||||||
|
#include <winrt/Windows.UI.Notifications.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
Loading…
Reference in a new issue