QortalOS Brooklyn for Raspberry Pi 4
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

547 lines
20 KiB

/*
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <[email protected]>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "server_p.h"
#include "debug.h"
#include "notificationmanageradaptor.h"
#include "notificationsadaptor.h"
#include "notification_p.h"
#include "server.h"
#include "serverinfo.h"
#include "utils_p.h"
#include <QDBusConnection>
#include <QDBusServiceWatcher>
#include <KConfigGroup>
#include <KService>
#include <KSharedConfig>
#include <KUser>
using namespace NotificationManager;
ServerPrivate::ServerPrivate(QObject *parent)
: QObject(parent)
, m_inhibitionWatcher(new QDBusServiceWatcher(this))
, m_notificationWatchers(new QDBusServiceWatcher(this))
{
m_inhibitionWatcher->setConnection(QDBusConnection::sessionBus());
m_inhibitionWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
connect(m_inhibitionWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &ServerPrivate::onInhibitionServiceUnregistered);
m_notificationWatchers->setConnection(QDBusConnection::sessionBus());
m_notificationWatchers->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
connect(m_notificationWatchers, &QDBusServiceWatcher::serviceUnregistered, [=](const QString &service) {
m_notificationWatchers->removeWatchedService(service);
});
}
ServerPrivate::~ServerPrivate() = default;
QString ServerPrivate::notificationServiceName()
{
return QStringLiteral("org.freedesktop.Notifications");
}
QString ServerPrivate::notificationServicePath()
{
return QStringLiteral("/org/freedesktop/Notifications");
}
QString ServerPrivate::notificationServiceInterface()
{
return notificationServiceName();
}
ServerInfo *ServerPrivate::currentOwner() const
{
if (!m_currentOwner) {
m_currentOwner.reset(new ServerInfo());
}
return m_currentOwner.data();
}
bool ServerPrivate::init()
{
if (m_valid) {
return true;
}
new NotificationsAdaptor(this);
new NotificationManagerAdaptor(this);
if (!m_dbusObjectValid) { // if already registered, don't fail here
m_dbusObjectValid = QDBusConnection::sessionBus().registerObject(notificationServicePath(), this);
}
if (!m_dbusObjectValid) {
qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification DBus object";
return false;
}
// Only the "dbus master" (effectively plasmashell) should be the true owner of notifications
const bool master = Utils::isDBusMaster();
QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface();
if (!master) {
// NOTE this connects to whether the application lost ownership of given service
// This is not a wildcard listener for all unregistered services on the bus!
connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost, Qt::UniqueConnection);
}
auto registration = dbusIface->registerService(notificationServiceName(),
master ? QDBusConnectionInterface::ReplaceExistingService : QDBusConnectionInterface::DontQueueService,
master ? QDBusConnectionInterface::DontAllowReplacement : QDBusConnectionInterface::AllowReplacement);
if (registration.value() != QDBusConnectionInterface::ServiceRegistered) {
qCWarning(NOTIFICATIONMANAGER) << "Failed to register Notification service on DBus";
return false;
}
connect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged, Qt::UniqueConnection);
qCDebug(NOTIFICATIONMANAGER) << "Registered Notification service on DBus";
KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Notifications"));
const bool broadcastsEnabled = config.readEntry("ListenForBroadcasts", false);
if (broadcastsEnabled) {
qCDebug(NOTIFICATIONMANAGER) << "Notification server is configured to listen for broadcasts";
// NOTE Keep disconnect() call in onServiceOwnershipLost in sync if you change this!
QDBusConnection::systemBus().connect({},
{},
QStringLiteral("org.kde.BroadcastNotifications"),
QStringLiteral("Notify"),
this,
SLOT(onBroadcastNotification(QMap<QString, QVariant>)));
}
m_valid = true;
Q_EMIT validChanged();
return true;
}
uint ServerPrivate::Notify(const QString &app_name,
uint replaces_id,
const QString &app_icon,
const QString &summary,
const QString &body,
const QStringList &actions,
const QVariantMap &hints,
int timeout)
{
const bool wasReplaced = replaces_id > 0;
uint notificationId = 0;
if (wasReplaced) {
notificationId = replaces_id;
} else {
// Avoid wrapping around to 0 in case of overflow
if (!m_highestNotificationId) {
++m_highestNotificationId;
}
notificationId = m_highestNotificationId;
++m_highestNotificationId;
}
Notification notification(notificationId);
notification.setDBusService(message().service());
notification.setSummary(summary);
notification.setBody(body);
notification.setApplicationName(app_name);
notification.setActions(actions);
notification.setTimeout(timeout);
// might override some of the things we set above (like application name)
notification.d->processHints(hints);
// If we didn't get a pixmap, load the app_icon instead
if (notification.d->image.isNull()) {
notification.setIcon(app_icon);
}
uint pid = 0;
if (notification.desktopEntry().isEmpty() || notification.applicationName().isEmpty()) {
if (notification.desktopEntry().isEmpty() && notification.applicationName().isEmpty()) {
qCInfo(NOTIFICATIONMANAGER) << "Notification from service" << message().service()
<< "didn't contain any identification information, this is an application bug!";
}
QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
if (pidReply.isValid()) {
pid = pidReply.value();
}
}
// No desktop entry? Try to read the BAMF_DESKTOP_FILE_HINT in the environment of snaps
if (notification.desktopEntry().isEmpty() && pid > 0) {
const QString desktopEntry = Utils::desktopEntryFromPid(pid);
if (!desktopEntry.isEmpty()) {
qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from desktop entry" << desktopEntry;
notification.setDesktopEntry(desktopEntry);
// No application name? Set it to the service name, which is nicer than the process name fallback below
// Also if the title looks like it's just the desktop entry, use the nicer service name
if (notification.applicationName().isEmpty() || notification.applicationName() == desktopEntry) {
KService::Ptr service = KService::serviceByDesktopName(desktopEntry);
if (service) {
notification.setApplicationName(service->name());
}
}
}
}
// No application name? Try to figure out the process name using the sender's PID
if (notification.applicationName().isEmpty() && pid > 0) {
const QString processName = Utils::processNameFromPid(pid);
if (!processName.isEmpty()) {
qCDebug(NOTIFICATIONMANAGER) << "Resolved notification to be from process name" << processName;
notification.setApplicationName(processName);
}
}
// If multiple identical notifications are sent in quick succession, refuse the request
if (m_lastNotification.applicationName() == notification.applicationName() && m_lastNotification.summary() == notification.summary()
&& m_lastNotification.body() == notification.body() && m_lastNotification.desktopEntry() == notification.desktopEntry()
&& m_lastNotification.eventId() == notification.eventId() && m_lastNotification.actionNames() == notification.actionNames()
&& m_lastNotification.urls() == notification.urls() && m_lastNotification.created().msecsTo(notification.created()) < 1000) {
qCDebug(NOTIFICATIONMANAGER) << "Discarding excess notification creation request";
sendErrorReply(QStringLiteral("org.freedesktop.Notifications.Error.ExcessNotificationGeneration"),
QStringLiteral("Created too many similar notifications in quick succession"));
return 0;
}
m_lastNotification = notification;
if (wasReplaced) {
notification.resetUpdated();
Q_EMIT static_cast<Server *>(parent())->notificationReplaced(replaces_id, notification);
} else {
Q_EMIT static_cast<Server *>(parent())->notificationAdded(notification);
}
// currently we dispatch all notification, this is ugly
// TODO: come up with proper authentication/user selection
for (const QString &service : m_notificationWatchers->watchedServices()) {
QDBusMessage msg = QDBusMessage::createMethodCall(service,
QStringLiteral("/NotificationWatcher"),
QStringLiteral("org.kde.NotificationWatcher"),
QStringLiteral("Notify"));
msg.setArguments({notificationId,
notification.applicationName(),
replaces_id,
notification.applicationIconName(),
notification.summary(),
// we pass raw body data since this data goes through another sanitization
// in WatchedNotificationsModel when notification object is created.
notification.rawBody(),
actions,
hints,
notification.timeout()});
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
}
return notificationId;
}
void ServerPrivate::CloseNotification(uint id)
{
for (const QString &service : m_notificationWatchers->watchedServices()) {
QDBusMessage msg = QDBusMessage::createMethodCall(service,
QStringLiteral("/NotificationWatcher"),
QStringLiteral("org.kde.NotificationWatcher"),
QStringLiteral("CloseNotification"));
msg.setArguments({id});
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
}
// spec says "If the notification no longer exists, an empty D-BUS Error message is sent back."
static_cast<Server *>(parent())->closeNotification(id, Server::CloseReason::Revoked);
}
QStringList ServerPrivate::GetCapabilities() const
{
// should this be configurable somehow so the UI can tell what it implements?
return QStringList{QStringLiteral("body"),
QStringLiteral("body-hyperlinks"),
QStringLiteral("body-markup"),
QStringLiteral("body-images"),
QStringLiteral("icon-static"),
QStringLiteral("actions"),
QStringLiteral("persistence"),
QStringLiteral("inline-reply"),
QStringLiteral("x-kde-urls"),
QStringLiteral("x-kde-origin-name"),
QStringLiteral("x-kde-display-appname"),
QStringLiteral("inhibitions")};
}
QString ServerPrivate::GetServerInformation(QString &vendor, QString &version, QString &specVersion) const
{
vendor = QStringLiteral("KDE");
version = QLatin1String(PROJECT_VERSION);
specVersion = QStringLiteral("1.2");
return QStringLiteral("Plasma");
}
void ServerPrivate::onBroadcastNotification(const QMap<QString, QVariant> &properties)
{
qCDebug(NOTIFICATIONMANAGER) << "Received broadcast notification";
const auto currentUserId = KUserId::currentEffectiveUserId().nativeId();
// a QVariantList with ints arrives as QDBusArgument here, using a QStringList for simplicity
const QStringList &userIds = properties.value(QStringLiteral("uids")).toStringList();
if (!userIds.isEmpty()) {
auto it = std::find_if(userIds.constBegin(), userIds.constEnd(), [currentUserId](const QVariant &id) {
bool ok;
auto uid = id.toString().toLongLong(&ok);
return ok && uid == currentUserId;
});
if (it == userIds.constEnd()) {
qCDebug(NOTIFICATIONMANAGER) << "It is not meant for us, ignoring";
return;
}
}
bool ok;
int timeout = properties.value(QStringLiteral("timeout")).toInt(&ok);
if (!ok) {
timeout = -1; // -1 = server default, 0 would be "persistent"
}
Notify(properties.value(QStringLiteral("appName")).toString(),
0, // replaces_id
properties.value(QStringLiteral("appIcon")).toString(),
properties.value(QStringLiteral("summary")).toString(),
properties.value(QStringLiteral("body")).toString(),
{}, // no actions
properties.value(QStringLiteral("hints")).toMap(),
timeout);
}
uint ServerPrivate::add(const Notification &notification)
{
// TODO check if notification with ID already exists and signal update instead
if (notification.id() == 0) {
++m_highestNotificationId;
notification.d->id = m_highestNotificationId;
Q_EMIT static_cast<Server *>(parent())->notificationAdded(notification);
} else {
Q_EMIT static_cast<Server *>(parent())->notificationReplaced(notification.id(), notification);
}
return notification.id();
}
void ServerPrivate::sendReplyText(const QString &dbusService, uint notificationId, const QString &text, Notifications::InvokeBehavior behavior)
{
if (dbusService.isEmpty()) {
qCWarning(NOTIFICATIONMANAGER) << "Sending notification reply text for notification" << notificationId << "untargeted";
}
QDBusMessage msg =
QDBusMessage::createTargetedSignal(dbusService, notificationServicePath(), notificationServiceName(), QStringLiteral("NotificationReplied"));
msg.setArguments({notificationId, text});
QDBusConnection::sessionBus().send(msg);
if (behavior & Notifications::Close) {
Q_EMIT CloseNotification(notificationId);
}
}
uint ServerPrivate::Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints)
{
const QString dbusService = message().service();
QString applicationName = desktop_entry;
qCDebug(NOTIFICATIONMANAGER) << "Request inhibit from service" << dbusService << "which is" << desktop_entry << "with reason" << reason;
// xdg-desktop-portal forwards appId only for sandboxed apps it can trust
// Resolve it to process name here to at least have something, even if that means showing "xdg-desktop-portal-kde is currently..."
if (desktop_entry.isEmpty()) {
QDBusReply<uint> pidReply = connection().interface()->servicePid(message().service());
if (pidReply.isValid()) {
const auto pid = pidReply.value();
const QString processName = Utils::processNameFromPid(pid);
if (!processName.isEmpty()) {
qCDebug(NOTIFICATIONMANAGER) << "Resolved inhibition to be from process name" << processName;
applicationName = processName;
}
}
} else {
KService::Ptr service = KService::serviceByDesktopName(desktop_entry);
if (service) {
applicationName = service->name();
}
}
if (applicationName.isEmpty()) {
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("No meaningful desktop_entry provided"));
return 0;
}
m_inhibitionWatcher->addWatchedService(dbusService);
++m_highestInhibitionCookie;
const bool oldExternalInhibited = externalInhibited();
m_externalInhibitions.insert(m_highestInhibitionCookie, {desktop_entry, applicationName, reason, hints});
m_inhibitionServices.insert(m_highestInhibitionCookie, dbusService);
if (externalInhibited() != oldExternalInhibited) {
Q_EMIT externalInhibitedChanged();
}
Q_EMIT externalInhibitionsChanged();
return m_highestInhibitionCookie;
}
void ServerPrivate::onServiceOwnershipLost(const QString &serviceName)
{
if (serviceName != notificationServiceName()) {
return;
}
qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service";
disconnect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceUnregistered, this, &ServerPrivate::onServiceOwnershipLost);
disconnect(this, &ServerPrivate::inhibitedChanged, this, &ServerPrivate::onInhibitedChanged);
QDBusConnection::systemBus().disconnect({},
{},
QStringLiteral("org.kde.BroadcastNotifications"),
QStringLiteral("Notify"),
this,
SLOT(onBroadcastNotification(QMap<QString, QVariant>)));
m_valid = false;
Q_EMIT validChanged();
Q_EMIT serviceOwnershipLost();
}
void ServerPrivate::onInhibitionServiceUnregistered(const QString &serviceName)
{
qCDebug(NOTIFICATIONMANAGER) << "Inhibition service unregistered" << serviceName;
const QList<uint> cookies = m_inhibitionServices.keys(serviceName);
if (cookies.isEmpty()) {
qCInfo(NOTIFICATIONMANAGER) << "Unknown inhibition service unregistered" << serviceName;
return;
}
// We do lookups in there again...
for (uint cookie : cookies) {
UnInhibit(cookie);
}
}
void ServerPrivate::onInhibitedChanged()
{
// Q_EMIT DBus change signal...
QDBusMessage signal =
QDBusMessage::createSignal(notificationServicePath(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged"));
signal.setArguments({
notificationServiceInterface(),
QVariantMap{
// updated
{QStringLiteral("Inhibited"), inhibited()},
},
QStringList() // invalidated
});
QDBusConnection::sessionBus().send(signal);
}
void ServerPrivate::UnInhibit(uint cookie)
{
qCDebug(NOTIFICATIONMANAGER) << "Request release inhibition for cookie" << cookie;
const QString service = m_inhibitionServices.value(cookie);
if (service.isEmpty()) {
qCInfo(NOTIFICATIONMANAGER) << "Requested to release inhibition with cookie" << cookie << "that doesn't exist";
// TODO if called from dbus raise error
return;
}
m_inhibitionWatcher->removeWatchedService(service);
m_externalInhibitions.remove(cookie);
m_inhibitionServices.remove(cookie);
if (m_externalInhibitions.isEmpty()) {
Q_EMIT externalInhibitedChanged();
}
Q_EMIT externalInhibitionsChanged();
}
QList<Inhibition> ServerPrivate::externalInhibitions() const
{
return m_externalInhibitions.values();
}
bool ServerPrivate::inhibited() const
{
return m_inhibited;
}
void ServerPrivate::setInhibited(bool inhibited)
{
if (m_inhibited != inhibited) {
m_inhibited = inhibited;
Q_EMIT inhibitedChanged();
}
}
bool ServerPrivate::externalInhibited() const
{
return !m_externalInhibitions.isEmpty();
}
void ServerPrivate::clearExternalInhibitions()
{
if (m_externalInhibitions.isEmpty()) {
return;
}
m_inhibitionWatcher->setWatchedServices(QStringList()); // remove all watches
m_inhibitionServices.clear();
m_externalInhibitions.clear();
Q_EMIT externalInhibitedChanged();
Q_EMIT externalInhibitionsChanged();
}
void ServerPrivate::RegisterWatcher()
{
m_notificationWatchers->addWatchedService(message().service());
}
void ServerPrivate::UnRegisterWatcher()
{
m_notificationWatchers->removeWatchedService(message().service());
}
void ServerPrivate::InvokeAction(uint id, const QString &actionKey)
{
Q_EMIT ActionInvoked(id, actionKey);
}