1 /**
2 * SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de>
3 *
4 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6 #include "notificationslistener.h"
7
8 #include <QDBusInterface>
9 #include <QDBusArgument>
10 #include <QtDebug>
11 #include <QStandardPaths>
12 #include <QImage>
13 #include <KConfig>
14 #include <KConfigGroup>
15 #include <kiconloader.h>
16 #include <kicontheme.h>
17
18 #include <core/device.h>
19 #include <core/kdeconnectplugin.h>
20
21 #include <dbushelper.h>
22
23 #include "sendnotificationsplugin.h"
24 #include "plugin_sendnotification_debug.h"
25 #include "notifyingapplication.h"
26
27 //In older Qt released, qAsConst isnt available
28 #include "qtcompat_p.h"
29
NotificationsListener(KdeConnectPlugin * aPlugin)30 NotificationsListener::NotificationsListener(KdeConnectPlugin* aPlugin)
31 : QDBusAbstractAdaptor(aPlugin),
32 m_plugin(aPlugin)
33 {
34 qRegisterMetaTypeStreamOperators<NotifyingApplication>("NotifyingApplication");
35
36 bool ret = DBusHelper::sessionBus()
37 .registerObject(QStringLiteral("/org/freedesktop/Notifications"),
38 this,
39 QDBusConnection::ExportScriptableContents);
40 if (!ret)
41 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION)
42 << "Error registering notifications listener for device"
43 << m_plugin->device()->name() << ":"
44 << DBusHelper::sessionBus().lastError();
45 else
46 qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION)
47 << "Registered notifications listener for device"
48 << m_plugin->device()->name();
49
50 QDBusInterface iface(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"),
51 QStringLiteral("org.freedesktop.DBus"));
52 iface.call(QStringLiteral("AddMatch"),
53 QStringLiteral("interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'"));
54
55 setTranslatedAppName();
56 loadApplications();
57
58 connect(m_plugin->config(), &KdeConnectPluginConfig::configChanged, this, &NotificationsListener::loadApplications);
59 }
60
~NotificationsListener()61 NotificationsListener::~NotificationsListener()
62 {
63 qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Destroying NotificationsListener";
64 QDBusInterface iface(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"),
65 QStringLiteral("org.freedesktop.DBus"));
66 QDBusMessage res = iface.call(QStringLiteral("RemoveMatch"),
67 QStringLiteral("interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'"));
68 DBusHelper::sessionBus().unregisterObject(QStringLiteral("/org/freedesktop/Notifications"));
69 }
70
setTranslatedAppName()71 void NotificationsListener::setTranslatedAppName()
72 {
73 QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/kdeconnect.notifyrc"), QStandardPaths::LocateFile);
74 if (filePath.isEmpty()) {
75 qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Couldn't find kdeconnect.notifyrc to hide kdeconnect notifications on the devices. Using default name.";
76 m_translatedAppName = QStringLiteral("KDE Connect");
77 return;
78 }
79
80 KConfig config(filePath, KConfig::OpenFlag::SimpleConfig);
81 KConfigGroup globalgroup(&config, QStringLiteral("Global"));
82 m_translatedAppName = globalgroup.readEntry(QStringLiteral("Name"), QStringLiteral("KDE Connect"));
83 }
84
loadApplications()85 void NotificationsListener::loadApplications()
86 {
87 m_applications.clear();
88 const QVariantList list = m_plugin->config()->getList(QStringLiteral("applications"));
89 for (const auto& a : list) {
90 NotifyingApplication app = a.value<NotifyingApplication>();
91 if (!m_applications.contains(app.name)) {
92 m_applications.insert(app.name, app);
93 }
94 }
95 //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Loaded" << applications.size() << " applications";
96 }
97
parseImageDataArgument(const QVariant & argument,int & width,int & height,int & rowStride,int & bitsPerSample,int & channels,bool & hasAlpha,QByteArray & imageData) const98 bool NotificationsListener::parseImageDataArgument(const QVariant& argument,
99 int& width, int& height,
100 int& rowStride, int& bitsPerSample,
101 int& channels, bool& hasAlpha,
102 QByteArray& imageData) const
103 {
104 if (!argument.canConvert<QDBusArgument>())
105 return false;
106 const QDBusArgument dbusArg = argument.value<QDBusArgument>();
107 dbusArg.beginStructure();
108 dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample
109 >> channels >> imageData;
110 dbusArg.endStructure();
111 return true;
112 }
113
iconForImageData(const QVariant & argument) const114 QSharedPointer<QIODevice> NotificationsListener::iconForImageData(const QVariant& argument) const
115 {
116 int width, height, rowStride, bitsPerSample, channels;
117 bool hasAlpha;
118 QByteArray imageData;
119
120 if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample,
121 channels, hasAlpha, imageData))
122 return QSharedPointer<QIODevice>();
123
124 if (bitsPerSample != 8) {
125 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Unsupported image format:"
126 << "width=" << width
127 << "height=" << height
128 << "rowStride=" << rowStride
129 << "bitsPerSample=" << bitsPerSample
130 << "channels=" << channels
131 << "hasAlpha=" << hasAlpha;
132 return QSharedPointer<QIODevice>();
133 }
134
135 QImage image(reinterpret_cast<uchar*>(imageData.data()), width, height, rowStride,
136 hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
137 if (hasAlpha)
138 image = image.rgbSwapped(); // RGBA --> ARGB
139
140 QSharedPointer<QBuffer> buffer = QSharedPointer<QBuffer>(new QBuffer);
141 if (!buffer || !buffer->open(QIODevice::WriteOnly) ||
142 !image.save(buffer.data(), "PNG")) {
143 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Could not initialize image buffer";
144 return QSharedPointer<QIODevice>();
145 }
146
147 return buffer;
148 }
149
iconForIconName(const QString & iconName) const150 QSharedPointer<QIODevice> NotificationsListener::iconForIconName(const QString& iconName) const
151 {
152 int size = KIconLoader::SizeEnormous; // use big size to allow for good
153 // quality on high-DPI mobile devices
154 QString iconPath = KIconLoader::global()->iconPath(iconName, -size, true);
155 if (!iconPath.isEmpty()) {
156 if (!iconPath.endsWith(QLatin1String(".png")) &&
157 KIconLoader::global()->theme()->name() != QLatin1String("hicolor")) {
158 // try falling back to hicolor theme:
159 KIconTheme hicolor(QStringLiteral("hicolor"));
160 if (hicolor.isValid()) {
161 iconPath = hicolor.iconPath(iconName + QStringLiteral(".png"), size, KIconLoader::MatchBest);
162 //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Found non-png icon in default theme trying fallback to hicolor:" << iconPath;
163 }
164 }
165 }
166
167 if (iconPath.endsWith(QLatin1String(".png")))
168 return QSharedPointer<QIODevice>(new QFile(iconPath));
169 return QSharedPointer<QIODevice>();
170 }
171
Notify(const QString & appName,uint replacesId,const QString & appIcon,const QString & summary,const QString & body,const QStringList & actions,const QVariantMap & hints,int timeout)172 uint NotificationsListener::Notify(const QString& appName, uint replacesId,
173 const QString& appIcon,
174 const QString& summary, const QString& body,
175 const QStringList& actions,
176 const QVariantMap& hints, int timeout)
177 {
178 static int id = 0;
179 Q_UNUSED(actions);
180
181 //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Got notification appName=" << appName << "replacesId=" << replacesId << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout;
182
183 // skip our own notifications
184 if (appName == m_translatedAppName)
185 return 0;
186
187 NotifyingApplication app;
188 if (!m_applications.contains(appName)) {
189 // new application -> add to config
190 app.name = appName;
191 app.icon = appIcon;
192 app.active = true;
193 app.blacklistExpression = QRegularExpression();
194 m_applications.insert(app.name, app);
195 // update config:
196 QVariantList list;
197 for (const auto& a : qAsConst(m_applications))
198 list << QVariant::fromValue<NotifyingApplication>(a);
199 m_plugin->config()->setList(QStringLiteral("applications"), list);
200 //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Added new application to config:" << app;
201 } else {
202 app = m_applications.value(appName);
203 }
204
205 if (!app.active)
206 return 0;
207
208 if (timeout > 0 && m_plugin->config()->getBool(QStringLiteral("generalPersistent"), false))
209 return 0;
210
211 int urgency = -1;
212 if (hints.contains(QStringLiteral("urgency"))) {
213 bool ok;
214 urgency = hints[QStringLiteral("urgency")].toInt(&ok);
215 if (!ok)
216 urgency = -1;
217 }
218 if (urgency > -1 && urgency < m_plugin->config()->getInt(QStringLiteral("generalUrgency"), 0))
219 return 0;
220
221 QString ticker = summary;
222 if (!body.isEmpty() && m_plugin->config()->getBool(QStringLiteral("generalIncludeBody"), true))
223 ticker += QStringLiteral(": ") + body;
224
225 if (app.blacklistExpression.isValid() &&
226 !app.blacklistExpression.pattern().isEmpty() &&
227 app.blacklistExpression.match(ticker).hasMatch())
228 return 0;
229
230 //qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATION) << "Sending notification from" << appName << ":" <<ticker << "; appIcon=" << appIcon;
231 NetworkPacket np(PACKET_TYPE_NOTIFICATION, {
232 {QStringLiteral("id"), QString::number(replacesId > 0 ? replacesId : ++id)},
233 {QStringLiteral("appName"), appName},
234 {QStringLiteral("ticker"), ticker},
235 {QStringLiteral("isClearable"), timeout == 0}
236 }); // KNotifications are persistent if
237 // timeout == 0, for other notifications
238 // clearability is pointless
239
240 // sync any icon data?
241 if (m_plugin->config()->getBool(QStringLiteral("generalSynchronizeIcons"), true)) {
242 QSharedPointer<QIODevice> iconSource;
243 // try different image sources according to priorities in notifications-
244 // spec version 1.2:
245 if (hints.contains(QStringLiteral("image-data")))
246 iconSource = iconForImageData(hints[QStringLiteral("image-data")]);
247 else if (hints.contains(QStringLiteral("image_data"))) // 1.1 backward compatibility
248 iconSource = iconForImageData(hints[QStringLiteral("image_data")]);
249 else if (hints.contains(QStringLiteral("image-path")))
250 iconSource = iconForIconName(hints[QStringLiteral("image-path")].toString());
251 else if (hints.contains(QStringLiteral("image_path"))) // 1.1 backward compatibility
252 iconSource = iconForIconName(hints[QStringLiteral("image_path")].toString());
253 else if (!appIcon.isEmpty())
254 iconSource = iconForIconName(appIcon);
255 else if (hints.contains(QStringLiteral("icon_data"))) // < 1.1 backward compatibility
256 iconSource = iconForImageData(hints[QStringLiteral("icon_data")]);
257
258 if (iconSource)
259 np.setPayload(iconSource, iconSource->size());
260 }
261
262 m_plugin->sendPacket(np);
263
264 return (replacesId > 0 ? replacesId : id);
265 }
266