plugins/sendnotifications: add support for Windows
This commit is contained in:
parent
cf92bacf02
commit
c4ce19e9cd
10 changed files with 733 additions and 461 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
427
plugins/sendnotifications/dbusnotificationslistener.cpp
Normal file
427
plugins/sendnotifications/dbusnotificationslistener.cpp
Normal file
|
@ -0,0 +1,427 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
|
||||
* SPDX-FileCopyrightText: 2018 Richard Liebscher <richard.liebscher@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "dbusnotificationslistener.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
|
||||
#include <kiconloader.h>
|
||||
#include <kicontheme.h>
|
||||
|
||||
#include "plugin_sendnotifications_debug.h"
|
||||
#include <core/kdeconnectplugin.h>
|
||||
#include <core/kdeconnectpluginconfig.h>
|
||||
|
||||
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<DBusNotificationsListenerThread *>(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 << ":" <<ticker << "; appIcon=" << appIcon;
|
||||
|
||||
static unsigned id = 0;
|
||||
if (id == std::numeric_limits<unsigned>::max()) {
|
||||
id = 0;
|
||||
}
|
||||
NetworkPacket np(PACKET_TYPE_NOTIFICATION,
|
||||
{
|
||||
{QStringLiteral("id"), replacesId > 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<QIODevice> 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<QDBusArgument>()) {
|
||||
// return false;
|
||||
// }
|
||||
// const QDBusArgument dbusArg = argument.value<QDBusArgument>();
|
||||
// dbusArg.beginStructure();
|
||||
// dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData;
|
||||
// dbusArg.endStructure();
|
||||
return true;
|
||||
}
|
||||
|
||||
QSharedPointer<QIODevice> 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<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) 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<QIODevice>();
|
||||
} else if (iconPath.endsWith(QLatin1String(".png"))) {
|
||||
return QSharedPointer<QIODevice>(new QFile(iconPath));
|
||||
} else {
|
||||
// TODO: cache icons
|
||||
return iconFromQImage(QImage(iconPath));
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_dbusnotificationslistener.cpp"
|
58
plugins/sendnotifications/dbusnotificationslistener.h
Normal file
58
plugins/sendnotifications/dbusnotificationslistener.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
|
||||
* SPDX-FileCopyrightText: 2018 Richard Liebscher <richard.liebscher@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "notificationslistener.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include <dbus/dbus.h>
|
||||
|
||||
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<DBusConnection *> 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<QIODevice> iconForImageData(const QVariant &argument) const;
|
||||
QSharedPointer<QIODevice> iconForIconName(const QString &iconName) const;
|
||||
QSharedPointer<QIODevice> pngFromImage() const;
|
||||
|
||||
DBusNotificationsListenerThread *m_thread = nullptr;
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
|
||||
* SPDX-FileCopyrightText: 2018 Richard Liebscher <richard.liebscher@gmail.com>
|
||||
*
|
||||
* 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 <QBuffer>
|
||||
#include <QDBusArgument>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QImage>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <core/kdeconnectplugin.h>
|
||||
#include <core/kdeconnectpluginconfig.h>
|
||||
|
||||
#include <kiconloader.h>
|
||||
#include <kicontheme.h>
|
||||
#include <KConfig>
|
||||
#include <KConfigGroup>
|
||||
|
||||
#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<NotificationsListenerThread *>(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 <core/kdeconnectplugin.h>
|
||||
#include <core/kdeconnectpluginconfig.h>
|
||||
|
||||
NotificationsListener::NotificationsListener(KdeConnectPlugin *aPlugin)
|
||||
: QObject(aPlugin)
|
||||
, m_plugin(aPlugin)
|
||||
, m_thread(new NotificationsListenerThread())
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
qRegisterMetaTypeStreamOperators<NotifyingApplication>("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<std::reference_wrapper<const QString>> iconName)
|
||||
{
|
||||
if (!argument.canConvert<QDBusArgument>())
|
||||
if (m_translatedAppName == appName) {
|
||||
return false;
|
||||
const QDBusArgument dbusArg = argument.value<QDBusArgument>();
|
||||
dbusArg.beginStructure();
|
||||
dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData;
|
||||
dbusArg.endStructure();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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<QIODevice> NotificationsListener::iconFromQImage(const QImage &image) const
|
||||
|
@ -292,180 +107,8 @@ QSharedPointer<QIODevice> NotificationsListener::iconFromQImage(const QImage &im
|
|||
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer";
|
||||
return QSharedPointer<QIODevice>();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
QSharedPointer<QIODevice> 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<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> 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<QIODevice>();
|
||||
} else if (iconPath.endsWith(QLatin1String(".png"))) {
|
||||
return QSharedPointer<QIODevice>(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<NotifyingApplication>(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 << ":" <<ticker << "; appIcon=" << appIcon;
|
||||
|
||||
bool silent = false;
|
||||
static unsigned id = 0;
|
||||
NetworkPacket np(PACKET_TYPE_NOTIFICATION,
|
||||
{
|
||||
{QStringLiteral("id"), QString::number(replacesId > 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<QIODevice> 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"
|
||||
|
|
|
@ -1,42 +1,22 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
|
||||
* SPDX-FileCopyrightText: 2018 Richard Liebscher <richard.liebscher@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QHash>
|
||||
#include <QIODevice>
|
||||
#include <QSet>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <atomic>
|
||||
#include <dbus/dbus.h>
|
||||
|
||||
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<DBusConnection *> 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<QString, NotifyingApplication> 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<QIODevice> iconForImageData(const QVariant &argument) const;
|
||||
QSharedPointer<QIODevice> iconForIconName(const QString &iconName) const;
|
||||
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;
|
||||
|
||||
private Q_SLOTS:
|
||||
void loadApplications();
|
||||
void onNotify(const QString &, uint, const QString &, const QString &, const QString &, const QStringList &, const QVariantMap &, int);
|
||||
|
||||
private:
|
||||
QSharedPointer<QIODevice> pngFromImage();
|
||||
NotificationsListenerThread *m_thread;
|
||||
void setTranslatedAppName();
|
||||
|
||||
QHash<QString, NotifyingApplication> m_applications;
|
||||
QString m_translatedAppName;
|
||||
};
|
||||
|
|
|
@ -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 <windows.h>
|
||||
|
||||
#include <appmodel.h> // GetCurrentPackageFullName
|
||||
#endif
|
||||
#include <KPluginFactory>
|
||||
|
||||
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<wchar_t, 100> buffer;
|
||||
if (GetCurrentPackageFullName(&bufferLength, buffer.data()) == ERROR_SUCCESS) {
|
||||
notificationsListener = new WindowsNotificationsListener(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SendNotificationsPlugin::~SendNotificationsPlugin()
|
||||
|
|
|
@ -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
|
||||
|
|
129
plugins/sendnotifications/windowsnotificationslistener.cpp
Normal file
129
plugins/sendnotifications/windowsnotificationslistener.cpp
Normal file
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* 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;
|
||||
if (id == std::numeric_limits<unsigned>::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<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);
|
||||
}
|
||||
|
||||
#include "moc_windowsnotificationslistener.cpp"
|
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