f1843cb492
Better patch to replace !218. - Auto and quick detection of previous D-Bus instance; - Remove private D-Bus compile definition, only use it on macOS without an existing D-Bus instance; - Safe reboot after crashes because the indicator is not relating on the kdeconnectd to run a D-Bus session; - Safe exit after clicking on `Quit` in the systray. More details in commit logs: Only enable private D-Bus on macOS because the other platforms do not need them. The app should be able to easily detect the session bus from the env DBUS_LAUNCHD_SESSION_BUS_SOCKET from launchd through launchctl. Because https://gitlab.freedesktop.org/dbus/dbus/-/blob/master/dbus/dbus-sysdeps-unix.c#L4392 shows that it is the only probing method on macOS with launchd. The D-Bus session bus can be easily found from launchd/launchctl with DBUS_LAUNCHD_SESSION_BUS_SOCKET env. It can be an external one (installed from HomeBrew) or an internal one (launched by a previous instance followed by a crash). The indicator helper on macOS can now automatically detect whether we can use a potentially (with launchd/launchctl env set, or KDE Connect macOS private_bus_address set) existed and usable session bus. If previous bus is usable, just try to launch the kdeconnectd with us. Otherwise, launch a private D-Bus daemon, export the launchd/launchctl env, and run a kdeconnectd instance. Everything works better and quicker now :)
265 lines
12 KiB
C++
265 lines
12 KiB
C++
/**
|
|
* SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
*/
|
|
#include "notificationslistener.h"
|
|
|
|
#include <QDBusInterface>
|
|
#include <QDBusArgument>
|
|
#include <QtDebug>
|
|
#include <QStandardPaths>
|
|
#include <QImage>
|
|
#include <KConfig>
|
|
#include <KConfigGroup>
|
|
#include <kiconloader.h>
|
|
#include <kicontheme.h>
|
|
|
|
#include <core/device.h>
|
|
#include <core/kdeconnectplugin.h>
|
|
|
|
#include <dbushelper.h>
|
|
|
|
#include "sendnotificationsplugin.h"
|
|
#include "plugin_sendnotification_debug.h"
|
|
#include "notifyingapplication.h"
|
|
|
|
//In older Qt released, qAsConst isnt available
|
|
#include "qtcompat_p.h"
|
|
|
|
NotificationsListener::NotificationsListener(KdeConnectPlugin* aPlugin)
|
|
: QDBusAbstractAdaptor(aPlugin),
|
|
m_plugin(aPlugin)
|
|
{
|
|
qRegisterMetaTypeStreamOperators<NotifyingApplication>("NotifyingApplication");
|
|
|
|
bool ret = QDBusConnection::sessionBus()
|
|
.registerObject(QStringLiteral("/org/freedesktop/Notifications"),
|
|
this,
|
|
QDBusConnection::ExportScriptableContents);
|
|
if (!ret)
|
|
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION)
|
|
<< "Error registering notifications listener for device"
|
|
<< m_plugin->device()->name() << ":"
|
|
<< QDBusConnection::sessionBus().lastError();
|
|
else
|
|
qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION)
|
|
<< "Registered notifications listener for device"
|
|
<< m_plugin->device()->name();
|
|
|
|
QDBusInterface iface(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"),
|
|
QStringLiteral("org.freedesktop.DBus"));
|
|
iface.call(QStringLiteral("AddMatch"),
|
|
QStringLiteral("interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'"));
|
|
|
|
setTranslatedAppName();
|
|
loadApplications();
|
|
|
|
connect(m_plugin->config(), &KdeConnectPluginConfig::configChanged, this, &NotificationsListener::loadApplications);
|
|
}
|
|
|
|
NotificationsListener::~NotificationsListener()
|
|
{
|
|
qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Destroying NotificationsListener";
|
|
QDBusInterface iface(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"),
|
|
QStringLiteral("org.freedesktop.DBus"));
|
|
QDBusMessage res = iface.call(QStringLiteral("RemoveMatch"),
|
|
QStringLiteral("interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'"));
|
|
QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/org/freedesktop/Notifications"));
|
|
}
|
|
|
|
void NotificationsListener::setTranslatedAppName()
|
|
{
|
|
QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/kdeconnect.notifyrc"), QStandardPaths::LocateFile);
|
|
if (filePath.isEmpty()) {
|
|
qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "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()
|
|
{
|
|
m_applications.clear();
|
|
const QVariantList list = m_plugin->config()->getList(QStringLiteral("applications"));
|
|
for (const auto& a : list) {
|
|
NotifyingApplication app = a.value<NotifyingApplication>();
|
|
if (!m_applications.contains(app.name)) {
|
|
m_applications.insert(app.name, app);
|
|
}
|
|
}
|
|
//qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Loaded" << applications.size() << " applications";
|
|
}
|
|
|
|
bool NotificationsListener::parseImageDataArgument(const QVariant& argument,
|
|
int& width, int& height,
|
|
int& rowStride, int& bitsPerSample,
|
|
int& channels, bool& hasAlpha,
|
|
QByteArray& imageData) const
|
|
{
|
|
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> 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_SENDNOTIFICATION) << "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<QBuffer> buffer = QSharedPointer<QBuffer>(new QBuffer);
|
|
if (!buffer || !buffer->open(QIODevice::WriteOnly) ||
|
|
!image.save(buffer.data(), "PNG")) {
|
|
qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Could not initialize image buffer";
|
|
return QSharedPointer<QIODevice>();
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
QSharedPointer<QIODevice> NotificationsListener::iconForIconName(const QString& iconName) const
|
|
{
|
|
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_SENDNOTIFICATION) << "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>();
|
|
}
|
|
|
|
uint NotificationsListener::Notify(const QString& appName, uint replacesId,
|
|
const QString& appIcon,
|
|
const QString& summary, const QString& body,
|
|
const QStringList& actions,
|
|
const QVariantMap& hints, int timeout)
|
|
{
|
|
static int id = 0;
|
|
Q_UNUSED(actions);
|
|
|
|
//qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Got notification appName=" << appName << "replacesId=" << replacesId << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout;
|
|
|
|
// skip our own notifications
|
|
if (appName == m_translatedAppName)
|
|
return 0;
|
|
|
|
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 : qAsConst(m_applications))
|
|
list << QVariant::fromValue<NotifyingApplication>(a);
|
|
m_plugin->config()->setList(QStringLiteral("applications"), list);
|
|
//qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Added new application to config:" << app;
|
|
} else {
|
|
app = m_applications.value(appName);
|
|
}
|
|
|
|
if (!app.active)
|
|
return 0;
|
|
|
|
if (timeout > 0 && m_plugin->config()->getBool(QStringLiteral("generalPersistent"), false))
|
|
return 0;
|
|
|
|
int urgency = -1;
|
|
if (hints.contains(QStringLiteral("urgency"))) {
|
|
bool ok;
|
|
urgency = hints[QStringLiteral("urgency")].toInt(&ok);
|
|
if (!ok)
|
|
urgency = -1;
|
|
}
|
|
if (urgency > -1 && urgency < m_plugin->config()->getInt(QStringLiteral("generalUrgency"), 0))
|
|
return 0;
|
|
|
|
QString ticker = summary;
|
|
if (!body.isEmpty() && m_plugin->config()->getBool(QStringLiteral("generalIncludeBody"), true))
|
|
ticker += QStringLiteral(": ") + body;
|
|
|
|
if (app.blacklistExpression.isValid() &&
|
|
!app.blacklistExpression.pattern().isEmpty() &&
|
|
app.blacklistExpression.match(ticker).hasMatch())
|
|
return 0;
|
|
|
|
//qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "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 (m_plugin->config()->getBool(QStringLiteral("generalSynchronizeIcons"), true)) {
|
|
QSharedPointer<QIODevice> iconSource;
|
|
// try different image sources according to priorities in notifications-
|
|
// spec version 1.2:
|
|
if (hints.contains(QStringLiteral("image-data")))
|
|
iconSource = iconForImageData(hints[QStringLiteral("image-data")]);
|
|
else if (hints.contains(QStringLiteral("image_data"))) // 1.1 backward compatibility
|
|
iconSource = iconForImageData(hints[QStringLiteral("image_data")]);
|
|
else if (hints.contains(QStringLiteral("image-path")))
|
|
iconSource = iconForIconName(hints[QStringLiteral("image-path")].toString());
|
|
else if (hints.contains(QStringLiteral("image_path"))) // 1.1 backward compatibility
|
|
iconSource = iconForIconName(hints[QStringLiteral("image_path")].toString());
|
|
else if (!appIcon.isEmpty())
|
|
iconSource = iconForIconName(appIcon);
|
|
else if (hints.contains(QStringLiteral("icon_data"))) // < 1.1 backward compatibility
|
|
iconSource = iconForImageData(hints[QStringLiteral("icon_data")]);
|
|
|
|
if (iconSource)
|
|
np.setPayload(iconSource, iconSource->size());
|
|
}
|
|
|
|
m_plugin->sendPacket(np);
|
|
|
|
return (replacesId > 0 ? replacesId : id);
|
|
}
|