File indexing completed on 2024-04-28 04:57:10
0001 /** 0002 * SPDX-FileCopyrightText: 2015 Holger Kaelberer <holger.k@elberer.de> 0003 * SPDX-FileCopyrightText: 2018 Richard Liebscher <richard.liebscher@gmail.com> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "dbusnotificationslistener.h" 0009 0010 #include <limits> 0011 0012 #include <QBuffer> 0013 #include <QFile> 0014 #include <QImage> 0015 0016 #include <kiconloader.h> 0017 #include <kicontheme.h> 0018 0019 #include "plugin_sendnotifications_debug.h" 0020 #include <core/kdeconnectplugin.h> 0021 #include <core/kdeconnectpluginconfig.h> 0022 0023 namespace 0024 { 0025 // https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html 0026 inline constexpr const char *NOTIFY_SIGNATURE = "susssasa{sv}i"; 0027 inline constexpr const char *IMAGE_DATA_SIGNATURE = "iiibiiay"; 0028 0029 QString becomeMonitor(DBusConnection *conn, const char *match) 0030 { 0031 // message 0032 DBusMessage *msg = dbus_message_new_method_call(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_MONITORING, "BecomeMonitor"); 0033 Q_ASSERT(msg != nullptr); 0034 0035 // arguments 0036 const char *matches[] = {match}; 0037 const char **matches_ = matches; 0038 dbus_uint32_t flags = 0; 0039 0040 bool success = dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &matches_, 1, DBUS_TYPE_UINT32, &flags, DBUS_TYPE_INVALID); 0041 if (!success) { 0042 dbus_message_unref(msg); 0043 return QStringLiteral("Failed to call dbus_message_append_args"); 0044 } 0045 0046 // send 0047 // TODO: wait and check for error: dbus_connection_send_with_reply_and_block 0048 success = dbus_connection_send(conn, msg, nullptr); 0049 if (!success) { 0050 dbus_message_unref(msg); 0051 return QStringLiteral("Failed to call dbus_connection_send"); 0052 } 0053 0054 dbus_message_unref(msg); 0055 0056 return QString(); 0057 } 0058 0059 extern "C" DBusHandlerResult handleMessageFromC(DBusConnection *, DBusMessage *message, void *user_data) 0060 { 0061 auto *self = static_cast<DBusNotificationsListenerThread *>(user_data); 0062 if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "Notify")) { 0063 self->handleNotifyCall(message); 0064 } 0065 // Monitors must not allow libdbus to reply to messages, so we eat the message. 0066 return DBUS_HANDLER_RESULT_HANDLED; 0067 } 0068 0069 unsigned nextUnsigned(DBusMessageIter *iter) 0070 { 0071 Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_UINT32); 0072 DBusBasicValue value; 0073 dbus_message_iter_get_basic(iter, &value); 0074 dbus_message_iter_next(iter); 0075 return value.u32; 0076 } 0077 0078 int nextInt(DBusMessageIter *iter) 0079 { 0080 Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_INT32); 0081 DBusBasicValue value; 0082 dbus_message_iter_get_basic(iter, &value); 0083 dbus_message_iter_next(iter); 0084 return value.i32; 0085 } 0086 0087 QString nextString(DBusMessageIter *iter) 0088 { 0089 Q_ASSERT(dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING); 0090 DBusBasicValue value; 0091 dbus_message_iter_get_basic(iter, &value); 0092 dbus_message_iter_next(iter); 0093 return QString::fromUtf8(value.str); 0094 } 0095 0096 QStringList nextStringList(DBusMessageIter *iter) 0097 { 0098 DBusMessageIter sub; 0099 dbus_message_iter_recurse(iter, &sub); 0100 dbus_message_iter_next(iter); 0101 0102 QStringList list; 0103 while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { 0104 list.append(nextString(&sub)); 0105 } 0106 return list; 0107 } 0108 0109 QVariant nextVariant(DBusMessageIter *iter) 0110 { 0111 int type = dbus_message_iter_get_arg_type(iter); 0112 if (type != DBUS_TYPE_VARIANT) { 0113 return QVariant(); 0114 } 0115 0116 DBusMessageIter sub; 0117 dbus_message_iter_recurse(iter, &sub); 0118 dbus_message_iter_next(iter); 0119 0120 type = dbus_message_iter_get_arg_type(&sub); 0121 if (dbus_type_is_basic(type)) { 0122 DBusBasicValue value; 0123 dbus_message_iter_get_basic(&sub, &value); 0124 switch (type) { 0125 case DBUS_TYPE_BOOLEAN: 0126 return QVariant(value.bool_val); 0127 case DBUS_TYPE_INT16: 0128 return QVariant(value.i16); 0129 case DBUS_TYPE_INT32: 0130 return QVariant(value.i32); 0131 case DBUS_TYPE_INT64: 0132 return QVariant((qlonglong)value.i64); 0133 case DBUS_TYPE_UINT16: 0134 return QVariant(value.u16); 0135 case DBUS_TYPE_UINT32: 0136 return QVariant(value.u32); 0137 case DBUS_TYPE_UINT64: 0138 return QVariant((qulonglong)value.u64); 0139 case DBUS_TYPE_BYTE: 0140 return QVariant(value.byt); 0141 case DBUS_TYPE_DOUBLE: 0142 return QVariant(value.dbl); 0143 case DBUS_TYPE_STRING: 0144 return QVariant(QString::fromUtf8(value.str)); 0145 case DBUS_STRUCT_BEGIN_CHAR: { 0146 } 0147 default: 0148 break; 0149 } 0150 } 0151 0152 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unimplemented conversation of type" << QChar(type) << type; 0153 0154 return QVariant(); 0155 } 0156 0157 static QVariantMap nextVariantMap(DBusMessageIter *iter) 0158 { 0159 DBusMessageIter sub; 0160 dbus_message_iter_recurse(iter, &sub); 0161 dbus_message_iter_next(iter); 0162 0163 QVariantMap map; 0164 while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { 0165 DBusMessageIter entry; 0166 dbus_message_iter_recurse(&sub, &entry); 0167 dbus_message_iter_next(&sub); 0168 QString key = nextString(&entry); 0169 QVariant value = nextVariant(&entry); 0170 map.insert(key, value); 0171 } 0172 return map; 0173 } 0174 } 0175 0176 void DBusNotificationsListenerThread::run() 0177 { 0178 DBusError err = DBUS_ERROR_INIT; 0179 m_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &err); 0180 if (dbus_error_is_set(&err)) { 0181 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "D-Bus connection failed" << err.message; 0182 dbus_error_free(&err); 0183 return; 0184 } 0185 0186 Q_ASSERT(m_connection != nullptr); 0187 0188 dbus_connection_set_route_peer_messages(m_connection, true); 0189 dbus_connection_set_exit_on_disconnect(m_connection, false); 0190 dbus_connection_add_filter(m_connection, handleMessageFromC, this, nullptr); 0191 0192 QString error = becomeMonitor(m_connection, 0193 "interface='org.freedesktop.Notifications'," 0194 "member='Notify'"); 0195 0196 if (!error.isEmpty()) { 0197 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).noquote() << "Failed to become a DBus monitor." 0198 << "No notifictions will be sent. Error:" << error; 0199 } 0200 0201 // wake up every minute to see if we are still connected 0202 while (m_connection != nullptr) { 0203 dbus_connection_read_write_dispatch(m_connection, 60 * 1000); 0204 } 0205 0206 deleteLater(); 0207 } 0208 0209 void DBusNotificationsListenerThread::stop() 0210 { 0211 if (m_connection) { 0212 dbus_connection_close(m_connection); 0213 dbus_connection_unref(m_connection); 0214 m_connection = nullptr; 0215 } 0216 } 0217 0218 void DBusNotificationsListenerThread::handleNotifyCall(DBusMessage *message) 0219 { 0220 DBusMessageIter iter; 0221 dbus_message_iter_init(message, &iter); 0222 0223 if (!dbus_message_has_signature(message, NOTIFY_SIGNATURE)) { 0224 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS).nospace() 0225 << "Call to Notify has wrong signature. Expected " << NOTIFY_SIGNATURE << ", got " << dbus_message_get_signature(message); 0226 return; 0227 } 0228 0229 QString appName = nextString(&iter); 0230 uint replacesId = nextUnsigned(&iter); 0231 QString appIcon = nextString(&iter); 0232 QString summary = nextString(&iter); 0233 QString body = nextString(&iter); 0234 QStringList actions = nextStringList(&iter); 0235 QVariantMap hints = nextVariantMap(&iter); 0236 int timeout = nextInt(&iter); 0237 0238 Q_EMIT notificationReceived(appName, replacesId, appIcon, summary, body, actions, hints, timeout); 0239 } 0240 0241 DBusNotificationsListener::DBusNotificationsListener(KdeConnectPlugin *aPlugin) 0242 : NotificationsListener(aPlugin) 0243 , m_thread(new DBusNotificationsListenerThread) 0244 { 0245 connect(m_thread, &DBusNotificationsListenerThread::notificationReceived, this, &DBusNotificationsListener::onNotify); 0246 m_thread->start(); 0247 } 0248 0249 DBusNotificationsListener::~DBusNotificationsListener() 0250 { 0251 m_thread->stop(); 0252 m_thread->quit(); 0253 } 0254 0255 void DBusNotificationsListener::onNotify(const QString &appName, 0256 uint replacesId, 0257 const QString &appIcon, 0258 const QString &summary, 0259 const QString &body, 0260 const QStringList &actions, 0261 const QVariantMap &hints, 0262 int timeout) 0263 { 0264 Q_UNUSED(actions); 0265 0266 // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Got notification appName=" << appName << "replacesId=" << replacesId 0267 // << "appIcon=" << appIcon << "summary=" << summary << "body=" << body << "actions=" << actions << "hints=" << hints << "timeout=" << timeout; 0268 0269 auto *config = m_plugin->config(); 0270 if (timeout > 0 && config->getBool(QStringLiteral("generalPersistent"), false)) { 0271 return; 0272 } 0273 0274 if (!checkApplicationName(appName, appIcon)) { 0275 return; 0276 } 0277 0278 int urgency = -1; 0279 auto urgencyHint = hints.constFind(QStringLiteral("urgency")); 0280 if (urgencyHint != hints.cend()) { 0281 bool ok = false; 0282 urgency = urgencyHint->toInt(&ok); 0283 if (!ok) { 0284 urgency = -1; 0285 } 0286 } 0287 if (urgency > -1 && urgency < config->getInt(QStringLiteral("generalUrgency"), 0)) { 0288 return; 0289 } 0290 0291 if (summary.isEmpty()) { 0292 return; 0293 } 0294 0295 const bool includeBody = config->getBool(QStringLiteral("generalIncludeBody"), true); 0296 0297 QString ticker = summary; 0298 if (!body.isEmpty() && includeBody) { 0299 ticker += QLatin1String(": ") + body; 0300 } 0301 0302 if (checkIsInBlacklist(appName, ticker)) { 0303 return; 0304 } 0305 0306 // qCDebug(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Sending notification from" << appName << ":" <<ticker << "; appIcon=" << appIcon; 0307 0308 static unsigned id = 0; 0309 if (id == std::numeric_limits<unsigned>::max()) { 0310 id = 0; 0311 } 0312 NetworkPacket np(PACKET_TYPE_NOTIFICATION, 0313 { 0314 {QStringLiteral("id"), replacesId > 0 ? replacesId : id++}, 0315 {QStringLiteral("appName"), appName}, 0316 {QStringLiteral("ticker"), ticker}, 0317 {QStringLiteral("isClearable"), timeout == -1}, 0318 {QStringLiteral("title"), summary}, 0319 {QStringLiteral("silent"), false}, 0320 }); 0321 0322 if (!body.isEmpty() && includeBody) { 0323 np.set(QStringLiteral("text"), body); 0324 } 0325 0326 // Only send icon on first notify (replacesId == 0) 0327 if (config->getBool(QStringLiteral("generalSynchronizeIcons"), true) && replacesId == 0) { 0328 QSharedPointer<QIODevice> iconSource; 0329 // try different image sources according to priorities in notifications-spec version 1.2: 0330 auto it = hints.constFind(QStringLiteral("image-data")); 0331 if (it != hints.cend() || (it = hints.constFind(QStringLiteral("image_data"))) != hints.cend()) { 0332 iconSource = iconForImageData(it.value()); 0333 } else if ((it = hints.constFind(QStringLiteral("image-path"))) != hints.cend() 0334 || (it = hints.constFind(QStringLiteral("image_path"))) != hints.cend()) { 0335 iconSource = iconForIconName(it.value().toString()); 0336 } else if (!appIcon.isEmpty()) { 0337 iconSource = iconForIconName(appIcon); 0338 } else if ((it = hints.constFind(QStringLiteral("icon_data"))) != hints.cend()) { 0339 iconSource = iconForImageData(it.value()); 0340 } 0341 if (iconSource) { 0342 np.setPayload(iconSource, iconSource->size()); 0343 } 0344 } 0345 0346 m_plugin->sendPacket(np); 0347 } 0348 0349 bool DBusNotificationsListener::parseImageDataArgument(const QVariant &argument, 0350 int &width, 0351 int &height, 0352 int &rowStride, 0353 int &bitsPerSample, 0354 int &channels, 0355 bool &hasAlpha, 0356 QByteArray &imageData) const 0357 { 0358 // FIXME 0359 // if (!argument.canConvert<QDBusArgument>()) { 0360 // return false; 0361 // } 0362 // const QDBusArgument dbusArg = argument.value<QDBusArgument>(); 0363 // dbusArg.beginStructure(); 0364 // dbusArg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> imageData; 0365 // dbusArg.endStructure(); 0366 return true; 0367 } 0368 0369 QSharedPointer<QIODevice> DBusNotificationsListener::iconForImageData(const QVariant &argument) const 0370 { 0371 int width, height, rowStride, bitsPerSample, channels; 0372 bool hasAlpha; 0373 QByteArray imageData; 0374 0375 if (!parseImageDataArgument(argument, width, height, rowStride, bitsPerSample, channels, hasAlpha, imageData)) 0376 return QSharedPointer<QIODevice>(); 0377 0378 if (bitsPerSample != 8) { 0379 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Unsupported image format:" 0380 << "width=" << width << "height=" << height << "rowStride=" << rowStride 0381 << "bitsPerSample=" << bitsPerSample << "channels=" << channels << "hasAlpha=" << hasAlpha; 0382 return QSharedPointer<QIODevice>(); 0383 } 0384 0385 QImage image(reinterpret_cast<uchar *>(imageData.data()), width, height, rowStride, hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32); 0386 if (hasAlpha) { 0387 image = std::move(image).rgbSwapped(); // RGBA --> ARGB 0388 } 0389 0390 QSharedPointer<QIODevice> buffer = iconFromQImage(image); 0391 if (!buffer) { 0392 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not initialize image buffer"; 0393 return QSharedPointer<QIODevice>(); 0394 } 0395 0396 return buffer; 0397 } 0398 0399 QSharedPointer<QIODevice> DBusNotificationsListener::iconForIconName(const QString &iconName) const 0400 { 0401 int size = KIconLoader::SizeHuge; // use big size to allow for good quality on high-DPI mobile devices 0402 QString iconPath = iconName; 0403 if (!QFile::exists(iconName)) { 0404 const KIconTheme *iconTheme = KIconLoader::global()->theme(); 0405 if (iconTheme) { 0406 iconPath = iconTheme->iconPath(iconName + QLatin1String(".png"), size, KIconLoader::MatchBest); 0407 if (iconPath.isEmpty()) { 0408 iconPath = iconTheme->iconPath(iconName + QLatin1String(".svg"), size, KIconLoader::MatchBest); 0409 if (iconPath.isEmpty()) { 0410 iconPath = iconTheme->iconPath(iconName + QLatin1String(".svgz"), size, KIconLoader::MatchBest); 0411 } 0412 } 0413 } else { 0414 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "KIconLoader has no theme set"; 0415 } 0416 } 0417 if (iconPath.isEmpty()) { 0418 qCWarning(KDECONNECT_PLUGIN_SENDNOTIFICATIONS) << "Could not find notification icon:" << iconName; 0419 return QSharedPointer<QIODevice>(); 0420 } else if (iconPath.endsWith(QLatin1String(".png"))) { 0421 return QSharedPointer<QIODevice>(new QFile(iconPath)); 0422 } else { 0423 // TODO: cache icons 0424 return iconFromQImage(QImage(iconPath)); 0425 } 0426 } 0427 0428 #include "moc_dbusnotificationslistener.cpp"