FdoNotificationDaemon: Actually associate a Source with an app

Actually link a notification source with an app instead of just to
its app name and PID, which in many cases don't really identify an
app. E.g. for portal applications the PID points to the
xdg-desktop-portal.

Use the app when ever possible but keep using the app name and PID
as a fallback.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3173>
This commit is contained in:
Julian Sparber 2024-02-14 18:55:08 +01:00 committed by Florian Müllner
parent d63013f075
commit b1d2a5bac8

View File

@ -35,8 +35,9 @@ class FdoNotificationDaemon {
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(FdoNotificationsIface, this); this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(FdoNotificationsIface, this);
this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/Notifications'); this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/Notifications');
this._sources = []; this._sourcesForApp = new Map();
this._notifications = {}; this._sourceForPidAndName = new Map();
this._notifications = new Map();
this._nextNotificationId = 1; this._nextNotificationId = 1;
} }
@ -72,54 +73,69 @@ class FdoNotificationDaemon {
return null; return null;
} }
_lookupSource(title, pid) { _getApp(pid, appId, appName) {
for (let i = 0; i < this._sources.length; i++) { const appSys = Shell.AppSystem.get_default();
let source = this._sources[i]; let app;
if (source.pid === pid && source.initialTitle === title)
return source; app = Shell.WindowTracker.get_default().get_app_from_pid(pid);
} if (!app && appId)
return null; app = appSys.lookup_app(`${appId}.desktop`);
if (!app)
app = appSys.lookup_app(`${appName}.desktop`);
return app;
} }
// Returns the source associated with ndata.notification if it is set. // Returns the source associated with an app.
// If the existing or requested source is associated with a tray icon
// and passed in pid matches a pid of an existing source, the title
// match is ignored to enable representing a tray icon and notifications
// from the same application with a single source.
// //
// If no existing source is found, a new source is created as long as // If no existing source is found a new one is created.
// pid is provided. _getSourceForApp(sender, app) {
_getSource(title, pid, ndata, sender) { let source = this._sourcesForApp.get(app);
if (!pid && !(ndata && ndata.notification))
throw new Error('Either a pid or ndata.notification is needed');
// We use notification's source for the notifications we still have
// around that are getting replaced because we don't keep sources
// for transient notifications in this._sources, but we still want
// the notification associated with them to get replaced correctly.
if (ndata && ndata.notification)
return ndata.notification.source;
let source = this._lookupSource(title, pid);
if (source) if (source)
return source; return source;
const appId = ndata?.hints['desktop-entry']; source = new FdoNotificationDaemonSource(sender, app);
source = new FdoNotificationDaemonSource(title, pid, sender, appId);
this._sources.push(source); if (app) {
source.connect('destroy', () => { this._sourcesForApp.set(app, source);
let index = this._sources.indexOf(source); source.connect('destroy', () => {
if (index >= 0) this._sourcesForApp.delete(app);
this._sources.splice(index, 1); });
}); }
Main.messageTray.add(source);
return source;
}
// Returns the source associated with a pid and the app name.
//
// If no existing source is found, a new one is created.
_getSourceForPidAndName(sender, pid, appName) {
const key = `${pid}${appName}`;
let source = this._sourceForPidAndName.get(key);
if (source)
return source;
source = new FdoNotificationDaemonSource(sender, null);
// Only check whether we have a PID since it's enough to identify
// uniquely an app and "" is a valid app name.
if (pid) {
this._sourceForPidAndName.set(key, source);
source.connect('destroy', () => {
this._sourceForPidAndName.delete(key);
});
}
Main.messageTray.add(source); Main.messageTray.add(source);
return source; return source;
} }
NotifyAsync(params, invocation) { NotifyAsync(params, invocation) {
let [appName, replacesId, appIcon, summary, body, actions, hints, timeout] = params; let [appName, replacesId, appIcon, summary, body, actions, hints, timeout_] = params;
let id; let id;
for (let hint in hints) { for (let hint in hints) {
@ -143,42 +159,26 @@ class FdoNotificationDaemon {
hints['image-data'] = hints['icon_data']; hints['image-data'] = hints['icon_data'];
} }
const ndata = { let source, notification;
appName, if (replacesId !== 0 && this._notifications.has(replacesId)) {
appIcon, notification = this._notifications.get(replacesId);
summary, source = notification.source;
body, id = replacesId;
actions,
hints,
timeout,
};
if (replacesId !== 0 && this._notifications[replacesId]) {
ndata.id = id = replacesId;
ndata.notification = this._notifications[replacesId].notification;
} else { } else {
replacesId = 0; const sender = hints['x-shell-sender'];
ndata.id = id = this._nextNotificationId++; const pid = hints['x-shell-sender-pid'];
} const appId = hints['desktop-entry'];
this._notifications[id] = ndata; const app = this._getApp(pid, appId, appName);
const sender = hints['x-shell-sender']; id = this._nextNotificationId++;
const pid = hints['x-shell-sender-pid']; source = app
? this._getSourceForApp(sender, app)
: this._getSourceForPidAndName(sender, pid, appName);
let source = this._getSource(appName, pid, ndata, sender, null);
this._notifyForSource(source, ndata);
return invocation.return_value(GLib.Variant.new('(u)', [id]));
}
_notifyForSource(source, ndata) {
const {appIcon, summary, body, actions, hints} = ndata;
let {notification} = ndata;
if (notification == null) {
notification = new MessageTray.Notification(source); notification = new MessageTray.Notification(source);
ndata.notification = notification; this._notifications.set(id, notification);
notification.connect('destroy', (n, reason) => { notification.connect('destroy', (n, reason) => {
delete this._notifications[ndata.id]; this._notifications.delete(id);
let notificationClosedReason; let notificationClosedReason;
switch (reason) { switch (reason) {
case MessageTray.NotificationDestroyedReason.EXPIRED: case MessageTray.NotificationDestroyedReason.EXPIRED:
@ -191,7 +191,7 @@ class FdoNotificationDaemon {
notificationClosedReason = NotificationClosedReason.APP_CLOSED; notificationClosedReason = NotificationClosedReason.APP_CLOSED;
break; break;
} }
this._emitNotificationClosed(ndata.id, notificationClosedReason); this._emitNotificationClosed(id, notificationClosedReason);
}); });
} }
@ -216,8 +216,8 @@ class FdoNotificationDaemon {
hasDefaultAction = true; hasDefaultAction = true;
} else { } else {
notification.addAction(label, () => { notification.addAction(label, () => {
this._emitActivationToken(source, ndata.id); this._emitActivationToken(source, id);
this._emitActionInvoked(ndata.id, actionId); this._emitActionInvoked(id, actionId);
}); });
} }
} }
@ -225,8 +225,8 @@ class FdoNotificationDaemon {
if (hasDefaultAction) { if (hasDefaultAction) {
notification.connect('activated', () => { notification.connect('activated', () => {
this._emitActivationToken(source, ndata.id); this._emitActivationToken(source, id);
this._emitActionInvoked(ndata.id, 'default'); this._emitActionInvoked(id, 'default');
}); });
} else { } else {
notification.connect('activated', () => { notification.connect('activated', () => {
@ -257,16 +257,14 @@ class FdoNotificationDaemon {
// Only fallback to 'app-icon' when the source doesn't have a valid app // Only fallback to 'app-icon' when the source doesn't have a valid app
const sourceGIcon = source.app ? null : this._iconForNotificationData(appIcon); const sourceGIcon = source.app ? null : this._iconForNotificationData(appIcon);
source.processNotification(notification, sourceGIcon); source.processNotification(notification, appName, sourceGIcon);
return invocation.return_value(GLib.Variant.new('(u)', [id]));
} }
CloseNotification(id) { CloseNotification(id) {
let ndata = this._notifications[id]; const notification = this._notifications.get(id);
if (ndata) { notification?.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
if (ndata.notification)
ndata.notification.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
delete this._notifications[id];
}
} }
GetCapabilities() { GetCapabilities() {
@ -316,30 +314,15 @@ class FdoNotificationDaemon {
export const FdoNotificationDaemonSource = GObject.registerClass( export const FdoNotificationDaemonSource = GObject.registerClass(
class FdoNotificationDaemonSource extends MessageTray.Source { class FdoNotificationDaemonSource extends MessageTray.Source {
constructor(title, pid, sender, appId) { constructor(sender, app) {
const appSys = Shell.AppSystem.get_default();
let app;
app = Shell.WindowTracker.get_default().get_app_from_pid(pid);
if (!app && appId)
app = appSys.lookup_app(`${appId}.desktop`);
if (!app)
app = appSys.lookup_app(`${title}.desktop`);
// Use app name as title if available, instead of whatever is provided
// through libnotify (usually garbage)
super({ super({
title: app?.get_name() ?? title,
policy: MessageTray.NotificationPolicy.newForApp(app), policy: MessageTray.NotificationPolicy.newForApp(app),
}); });
this.pid = pid;
this.initialTitle = title;
this.app = app; this.app = app;
this._appName = null;
this._appIcon = null; this._appIcon = null;
if (sender) { if (sender) {
this._nameWatcherId = Gio.DBus.session.watch_name(sender, this._nameWatcherId = Gio.DBus.session.watch_name(sender,
Gio.BusNameWatcherFlags.NONE, Gio.BusNameWatcherFlags.NONE,
@ -360,13 +343,17 @@ class FdoNotificationDaemonSource extends MessageTray.Source {
this.destroy(); this.destroy();
} }
processNotification(notification, appIcon) { processNotification(notification, appName, appIcon) {
if (!this.app && appName) {
this._appName = appName;
this.notify('title');
}
if (!this.app && appIcon) { if (!this.app && appIcon) {
this._appIcon = appIcon; this._appIcon = appIcon;
this.notify('icon'); this.notify('icon');
} }
let tracker = Shell.WindowTracker.get_default(); let tracker = Shell.WindowTracker.get_default();
if (notification.resident && this.app && tracker.focus_app === this.app) if (notification.resident && this.app && tracker.focus_app === this.app)
this.pushNotification(notification); this.pushNotification(notification);
@ -397,6 +384,10 @@ class FdoNotificationDaemonSource extends MessageTray.Source {
super.destroy(); super.destroy();
} }
get title() {
return this.app?.get_name() ?? this._appName;
}
get icon() { get icon() {
return this.app?.get_icon() ?? this._appIcon; return this.app?.get_icon() ?? this._appIcon;
} }