/* * libwatchfish - library with common functionality for SailfishOS smartwatch connector programs. * Copyright (C) 2015 Javier S. Pedro * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include "notification.h" #include "notificationmonitor.h" #include "notificationmonitor_p.h" namespace watchfish { Q_LOGGING_CATEGORY(notificationMonitorCat, "watchfish-NotificationMonitor") QDebug operator<<(QDebug &debug, const ProtoNotification &proto) { QDebugStateSaver saver(debug); Q_UNUSED(saver); debug.nospace() << "Notification(appId=" << proto.appId << ", appName=" << proto.appName << ", summary=" << proto.summary << ", body=" << proto.body << ", appIcon=" << proto.appIcon << ", hints=" << proto.hints << ", timeout=" << proto.expireTimeout << ", actions=" << proto.actions << ")"; return debug; } NotificationMonitorPrivate::NotificationMonitorPrivate(NotificationMonitor *q) : q_ptr(q) { DBusError error = DBUS_ERROR_INIT; _conn = dbus_bus_get_private(DBUS_BUS_SESSION, &error); if (!_conn) { qCWarning(notificationMonitorCat) << "Could not connect to the session bus"; return; } dbus_connection_set_exit_on_disconnect(_conn, FALSE); dbus_connection_set_watch_functions(_conn, busWatchAdd, busWatchRemove, busWatchToggle, this, NULL); addMatchRule("type='method_call',interface='org.freedesktop.Notifications',member='Notify',eavesdrop='true'"); addMatchRule("type='method_return',sender='org.freedesktop.Notifications',eavesdrop='true'"); addMatchRule("type='signal',sender='org.freedesktop.Notifications',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications',member='NotificationClosed'"); dbus_bool_t result = dbus_connection_add_filter(_conn, busMessageFilter, this, NULL); if (!result) { qCWarning(notificationMonitorCat) << "Could not add filter"; } qCDebug(notificationMonitorCat) << "Starting notification monitor"; } NotificationMonitorPrivate::~NotificationMonitorPrivate() { QMap::iterator it = _notifs.begin(); while (it != _notifs.end()) { delete it.value(); } #if 0 /* No need to remove match rules since we're closing the connection. */ removeMatchRule("type='method_call',interface='org.freedesktop.Notifications',member='Notify',eavesdrop='true'"); removeMatchRule("type='method_return',sender='org.freedesktop.Notifications',eavesdrop='true'"); removeMatchRule("type='signal',sender='org.freedesktop.Notifications',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications',member='NotificationClosed'"); #endif dbus_connection_remove_filter(_conn, busMessageFilter, this); dbus_connection_close(_conn); dbus_connection_unref(_conn); } void NotificationMonitorPrivate::processIncomingNotification(quint32 id, const ProtoNotification &proto) { Q_Q(NotificationMonitor); qCDebug(notificationMonitorCat) << "Incoming notification" << id << proto; Notification *n = _notifs.value(id, 0); bool is_new_notification = !n; if (is_new_notification) { n = new Notification(id, q); } n->setAppId(proto.appId); n->setAppName(proto.appName); n->setSummary(proto.summary); n->setBody(proto.body); n->setIcon(proto.appIcon); n->setTransient(proto.hints.value("transient", "false") == "true"); // Handle nemo specific stuff QDateTime timestamp = QDateTime::fromString(proto.hints["x-nemo-timestamp"], Qt::ISODate); if (timestamp.isValid()) { n->setTimestamp(timestamp); } else if (is_new_notification) { n->setTimestamp(QDateTime::currentDateTime()); } n->setCategory(proto.hints.value("category")); n->setPreviewSummary(proto.hints.value("x-nemo-preview-summary")); n->setPreviewBody(proto.hints.value("x-nemo-preview-body")); // Nemo D-Bus actions... for (int i = 0; i < proto.actions.size(); i += 2) { const QString &actionName = proto.actions[i]; QString hintName = QString("x-nemo-remote-action-%1").arg(actionName); QString remote = proto.hints.value(hintName); QStringList remoteParts = remote.split(' '); if (remoteParts.size() >= 4) { n->addDBusAction(actionName, remoteParts[0], remoteParts[1], remoteParts[2], remoteParts[3], remoteParts.mid(4)); } } if (is_new_notification) { _notifs.insert(id, n); emit q->notification(n); } } void NotificationMonitorPrivate::processCloseNotification(quint32 id, quint32 reason) { qCDebug(notificationMonitorCat) << "Close notification" << id << reason; Notification *n = _notifs.value(id, 0); if (n) { _notifs.remove(id); emit n->closed(static_cast(reason)); n->deleteLater(); } else { qCDebug(notificationMonitorCat) << " but it is not found"; } } void NotificationMonitorPrivate::sendMessageWithString(const char *service, const char *path, const char *iface, const char *method, const char *arg) { DBusMessage *msg = dbus_message_new_method_call(service, path, iface, method); Q_ASSERT(msg); dbus_message_set_no_reply(msg, TRUE); dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg, DBUS_TYPE_INVALID); dbus_connection_send(_conn, msg, NULL); dbus_message_unref(msg); } void NotificationMonitorPrivate::addMatchRule(const char *rule) { sendMessageWithString("org.freedesktop.DBus", "/", "org.freedesktop.DBus", "AddMatch", rule); } void NotificationMonitorPrivate::removeMatchRule(const char *rule) { sendMessageWithString("org.freedesktop.DBus", "/", "org.freedesktop.DBus", "RemoveMatch", rule); } ProtoNotification NotificationMonitorPrivate::parseNotifyCall(DBusMessage *msg) const { ProtoNotification proto; DBusMessageIter iter, sub; const char *app_name, *app_icon, *summary, *body; quint32 replaces_id; qint32 expire_timeout; if (strcmp(dbus_message_get_signature(msg), "susssasa{sv}i") != 0) { qCWarning(notificationMonitorCat) << "Invalid signature"; return proto; } dbus_message_iter_init(msg, &iter); Q_ASSERT(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING); dbus_message_iter_get_basic(&iter, &app_name); dbus_message_iter_next(&iter); Q_ASSERT(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_UINT32); dbus_message_iter_get_basic(&iter, &replaces_id); dbus_message_iter_next(&iter); Q_ASSERT(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING); dbus_message_iter_get_basic(&iter, &app_icon); dbus_message_iter_next(&iter); dbus_message_iter_get_basic(&iter, &summary); dbus_message_iter_next(&iter); dbus_message_iter_get_basic(&iter, &body); dbus_message_iter_next(&iter); // Add basic notification information proto.appName = QString::fromUtf8(app_name); proto.appIcon = QString::fromUtf8(app_icon); proto.summary = QString::fromUtf8(summary); proto.body = QString::fromUtf8(body); dbus_message_iter_recurse(&iter, &sub); while (dbus_message_iter_get_arg_type(&sub) == DBUS_TYPE_STRING) { const char *action; dbus_message_iter_get_basic(&sub, &action); proto.actions.append(QString::fromUtf8(action)); dbus_message_iter_next(&sub); } dbus_message_iter_next(&iter); // Parse extended information QHash hints; dbus_message_iter_recurse(&iter, &sub); while (dbus_message_iter_get_arg_type(&sub) == DBUS_TYPE_DICT_ENTRY) { DBusMessageIter entry, value; const char *key; dbus_message_iter_recurse(&sub, &entry); dbus_message_iter_get_basic(&entry, &key); dbus_message_iter_next(&entry); dbus_message_iter_recurse(&entry, &value); if (dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_STRING) { const char *s; dbus_message_iter_get_basic(&value, &s); hints.insert(key, QString::fromUtf8(s)); } dbus_message_iter_next(&sub); } dbus_message_iter_next(&iter); Q_ASSERT(dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INT32); dbus_message_iter_get_basic(&iter, &expire_timeout); proto.expireTimeout = expire_timeout; if (hints.contains("x-nemo-owner")) { proto.appId = hints["x-nemo-owner"]; if (proto.appName.isEmpty()) { // Lookup application name via .desktop file proto.appName = getAppName(proto.appId); // Otherwise just use the appId if (proto.appName.isEmpty()) { proto.appName = proto.appId; } } } // Lookup category info and merge it with the notification info if found. if (hints.contains("category")) { proto.hints = getCategoryInfo(hints["category"]); proto.hints.unite(hints); } else { proto.hints = hints; } return proto; } QHash NotificationMonitorPrivate::getCategoryInfo(const QString &category) const { bool in_cache = _categoryCache.contains(category); bool needs_check = !in_cache || _categoryCache[category].lastCheckTime.secsTo(QDateTime::currentDateTime()) > CATEGORY_REFRESH_CHECK_TIME; if (needs_check) { QFileInfo finfo(QString("%1/%2.conf").arg(CATEGORY_DEFINITION_FILE_DIRECTORY, category)); if (finfo.exists()) { NotificationCategoryCacheEntry &entry = _categoryCache[category]; if (!in_cache || finfo.lastModified() > entry.lastReadTime) { QSettings settings(finfo.absoluteFilePath(), QSettings::IniFormat); entry.data.clear(); foreach (const QString &key, settings.allKeys()) { entry.data[key] = settings.value(key).toString(); } entry.lastReadTime = finfo.lastModified(); } entry.lastCheckTime = QDateTime::currentDateTime(); return entry.data; } else { qCWarning(notificationMonitorCat) << "Notification category" << category << "does not exist"; _categoryCache.remove(category); return QHash(); } } else { return _categoryCache[category].data; } } QString NotificationMonitorPrivate::getAppName(const QString &id) const { bool in_cache = _appNameCache.contains(id); bool needs_check = !in_cache || _appNameCache[id].lastCheckTime.secsTo(QDateTime::currentDateTime()) > DESKTOP_REFRESH_CHECK_TIME; if (needs_check) { AppNameCacheEntry &entry = _appNameCache[id]; QFileInfo finfo(QString("%1/%2.desktop").arg(DESKTOP_FILE_DIRECTORY, id)); if (finfo.exists()) { if (!in_cache || finfo.lastModified() > entry.lastReadTime) { QSettings settings(finfo.absoluteFilePath(), QSettings::IniFormat); settings.beginGroup("Desktop Entry"); entry.name = settings.value("Name").toString(); // TODO Localization entry.lastReadTime = finfo.lastModified(); } } else { qCWarning(notificationMonitorCat) << "Desktop file for" << id << "does not exist"; // Cache negative results, since they might be common... } entry.lastCheckTime = QDateTime::currentDateTime(); return entry.name; } else { return _appNameCache[id].name; } } dbus_bool_t NotificationMonitorPrivate::busWatchAdd(DBusWatch *watch, void *data) { NotificationMonitorPrivate *self = static_cast(data); NotificationMonitor *monitor = self->q_func(); int socket = dbus_watch_get_socket(watch); int flags = dbus_watch_get_flags(watch); QSocketNotifier::Type type; switch (flags) { case DBUS_WATCH_READABLE: type = QSocketNotifier::Read; break; case DBUS_WATCH_WRITABLE: type = QSocketNotifier::Write; break; default: qCWarning(notificationMonitorCat) << "Can't add this type of watch" << flags; return FALSE; } QSocketNotifier *notifier = new QSocketNotifier(socket, type, monitor); dbus_watch_set_data(watch, notifier, NULL); notifier->setEnabled(dbus_watch_get_enabled(watch)); notifier->setProperty("dbus-watch", QVariant::fromValue(watch)); notifier->connect(notifier, SIGNAL(activated(int)), self, SLOT(handleBusSocketActivated())); return TRUE; } void NotificationMonitorPrivate::busWatchRemove(DBusWatch *watch, void *data) { QSocketNotifier *notifier = static_cast(dbus_watch_get_data(watch)); Q_UNUSED(data); delete notifier; } void NotificationMonitorPrivate::busWatchToggle(DBusWatch *watch, void *data) { QSocketNotifier *notifier = static_cast(dbus_watch_get_data(watch)); Q_UNUSED(data); notifier->setEnabled(dbus_watch_get_enabled(watch)); } DBusHandlerResult NotificationMonitorPrivate::busMessageFilter(DBusConnection *conn, DBusMessage *msg, void *user_data) { NotificationMonitorPrivate *self = static_cast(user_data); DBusError error = DBUS_ERROR_INIT; Q_UNUSED(conn); switch (dbus_message_get_type(msg)) { case DBUS_MESSAGE_TYPE_METHOD_CALL: if (dbus_message_is_method_call(msg, "org.freedesktop.Notifications", "Notify")) { quint32 serial = dbus_message_get_serial(msg); ProtoNotification proto = self->parseNotifyCall(msg); self->_pendingConfirmation.insert(serial, proto); } break; case DBUS_MESSAGE_TYPE_METHOD_RETURN: if (self->_pendingConfirmation.contains(dbus_message_get_reply_serial(msg))) { quint32 id; if (dbus_message_get_args(msg, &error, DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID)) { ProtoNotification proto = self->_pendingConfirmation.take(dbus_message_get_reply_serial(msg)); self->processIncomingNotification(id, proto); } else { qCWarning(notificationMonitorCat) << "Could not parse notification method return"; } } break; case DBUS_MESSAGE_TYPE_SIGNAL: if (dbus_message_is_signal(msg, "org.freedesktop.Notifications", "NotificationClosed")) { quint32 id, reason; if (dbus_message_get_args(msg, &error, DBUS_TYPE_UINT32, &id, DBUS_TYPE_UINT32, &reason, DBUS_TYPE_INVALID)) { self->processCloseNotification(id, reason); } else { qCWarning(notificationMonitorCat) << "Failed to parse notification signal arguments"; } } break; } return DBUS_HANDLER_RESULT_HANDLED; } void NotificationMonitorPrivate::handleBusSocketActivated() { QSocketNotifier *notifier = static_cast(sender()); DBusWatch *watch = static_cast(notifier->property("dbus-watch").value()); dbus_watch_handle(watch, dbus_watch_get_flags(watch)); while (dbus_connection_get_dispatch_status(_conn) == DBUS_DISPATCH_DATA_REMAINS) { dbus_connection_dispatch(_conn); } } NotificationMonitor::NotificationMonitor(QObject *parent) : QObject(parent), d_ptr(new NotificationMonitorPrivate(this)) { } NotificationMonitor::~NotificationMonitor() { delete d_ptr; } }