mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
fix: forward activation token from libnotify on notification click (#50669)
* feat: forward activation token from libnotify notification clicks When a notification action is clicked on Linux, retrieve the activation token from libnotify (if available) via dlsym and set it using `base::nix::SetActivationToken()`. This enables proper window focus handling under Wayland, where the compositor requires a valid activation token to grant focus to the application. The `notify_notification_get_activation_token` symbol is resolved at runtime to maintain compatibility with older libnotify versions that do not expose this API. Co-authored-by: Bohdan Tkachenko <bohdan@tkachenko.dev> * refactor: simplify libnotify soname loading and activation token lookup Replace the chained Load() calls with a loop over a constexpr array of sonames, and inline the lazy EnsureActivationTokenFunc() into Initialize() since it is only called once and the library handle is already known at that point. Co-authored-by: Bohdan Tkachenko <bohdan@tkachenko.dev> --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Bohdan Tkachenko <bohdan@tkachenko.dev>
This commit is contained in:
@@ -4,12 +4,16 @@
|
||||
|
||||
#include "shell/browser/notifications/linux/libnotify_notification.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "base/containers/flat_set.h"
|
||||
#include "base/files/file_enumerator.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/nix/xdg_util.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/process/process_handle.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
@@ -50,6 +54,9 @@ bool NotifierSupportsActions() {
|
||||
return HasCapability("actions");
|
||||
}
|
||||
|
||||
using GetActivationTokenFunc = const char* (*)(NotifyNotification*);
|
||||
GetActivationTokenFunc g_get_activation_token = nullptr;
|
||||
|
||||
void log_and_clear_error(GError* error, const char* context) {
|
||||
LOG(ERROR) << context << ": domain=" << error->domain
|
||||
<< " code=" << error->code << " message=\"" << error->message
|
||||
@@ -61,18 +68,40 @@ void log_and_clear_error(GError* error, const char* context) {
|
||||
|
||||
// static
|
||||
bool LibnotifyNotification::Initialize() {
|
||||
if (!GetLibNotifyLoader().Load("libnotify.so.4") && // most common one
|
||||
!GetLibNotifyLoader().Load("libnotify.so.5") &&
|
||||
!GetLibNotifyLoader().Load("libnotify.so.1") &&
|
||||
!GetLibNotifyLoader().Load("libnotify.so")) {
|
||||
constexpr std::array kLibnotifySonames = {
|
||||
"libnotify.so.4",
|
||||
"libnotify.so.5",
|
||||
"libnotify.so.1",
|
||||
"libnotify.so",
|
||||
};
|
||||
|
||||
const char* loaded_soname = nullptr;
|
||||
for (const char* soname : kLibnotifySonames) {
|
||||
if (GetLibNotifyLoader().Load(soname)) {
|
||||
loaded_soname = soname;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!loaded_soname) {
|
||||
LOG(WARNING) << "Unable to find libnotify; notifications disabled";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GetLibNotifyLoader().notify_is_initted() &&
|
||||
!GetLibNotifyLoader().notify_init(GetApplicationName().c_str())) {
|
||||
LOG(WARNING) << "Unable to initialize libnotify; notifications disabled";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safe to cache the symbol after dlclose(handle) because libnotify remains
|
||||
// loaded via GetLibNotifyLoader() for the process lifetime.
|
||||
if (void* handle = dlopen(loaded_soname, RTLD_LAZY)) {
|
||||
g_get_activation_token = reinterpret_cast<GetActivationTokenFunc>(
|
||||
dlsym(handle, "notify_notification_get_activation_token"));
|
||||
dlclose(handle);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -192,6 +221,14 @@ void LibnotifyNotification::OnNotificationView(NotifyNotification* notification,
|
||||
gpointer user_data) {
|
||||
LibnotifyNotification* that = static_cast<LibnotifyNotification*>(user_data);
|
||||
DCHECK(that);
|
||||
|
||||
if (g_get_activation_token) {
|
||||
const char* token = g_get_activation_token(notification);
|
||||
if (token && *token) {
|
||||
base::nix::SetActivationToken(std::string(token));
|
||||
}
|
||||
}
|
||||
|
||||
that->NotificationClicked();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { app } from 'electron/main';
|
||||
import { expect } from 'chai';
|
||||
import * as dbus from 'dbus-native';
|
||||
|
||||
import { once } from 'node:events';
|
||||
import * as path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
@@ -25,7 +26,7 @@ const skip = process.platform !== 'linux' ||
|
||||
!process.env.DBUS_SESSION_BUS_ADDRESS;
|
||||
|
||||
ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
let mock: any, Notification, getCalls: any, reset: any;
|
||||
let mock: any, Notification: any, getCalls: any, emitSignal: any, reset: any;
|
||||
const realAppName = app.name;
|
||||
const realAppVersion = app.getVersion();
|
||||
const appName = 'api-notification-dbus-spec';
|
||||
@@ -45,7 +46,17 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
const getInterface = promisify(service.getInterface.bind(service));
|
||||
mock = await getInterface(path, iface);
|
||||
getCalls = promisify(mock.GetCalls.bind(mock));
|
||||
emitSignal = promisify(mock.EmitSignal.bind(mock));
|
||||
reset = promisify(mock.Reset.bind(mock));
|
||||
|
||||
// Override GetCapabilities to include "actions" so that libnotify
|
||||
// registers the "default" action callback on notifications.
|
||||
const addMethod = promisify(mock.AddMethod.bind(mock));
|
||||
await addMethod(
|
||||
serviceName, 'GetCapabilities', '', 'as',
|
||||
'ret = ["body", "body-markup", "icon-static", "image/svg+xml", ' +
|
||||
'"private-synchronous", "append", "private-icon-only", "truncation", "actions"]'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
@@ -122,7 +133,7 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
app_icon: '',
|
||||
title: 'title',
|
||||
body: 'body',
|
||||
actions: [],
|
||||
actions: ['default', 'View'],
|
||||
hints: {
|
||||
append: 'true',
|
||||
image_data: [3, 3, 12, true, 8, 4, Buffer.from([255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 76, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 38, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0])],
|
||||
@@ -133,4 +144,30 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActivationToken on notification click', () => {
|
||||
it('should emit click when ActionInvoked is sent by the daemon', async () => {
|
||||
const n = new Notification({ title: 'activation-token-test', body: 'test' });
|
||||
const clicked = once(n, 'click');
|
||||
|
||||
n.show();
|
||||
|
||||
// getCalls returns all D-Bus method calls. The mock assigns sequential
|
||||
// notification IDs starting at 1 for each Notify call.
|
||||
const calls = await getCalls();
|
||||
const notifyCalls = calls.filter((c: any) => c[1] === 'Notify');
|
||||
const notificationId = notifyCalls.length;
|
||||
|
||||
// Simulate the notification daemon emitting ActivationToken (FDN 1.2)
|
||||
// followed by ActionInvoked for a "default" click.
|
||||
emitSignal(
|
||||
'org.freedesktop.Notifications', 'ActivationToken',
|
||||
'us', [['u', notificationId], ['s', 'test-activation-token']]);
|
||||
emitSignal(
|
||||
'org.freedesktop.Notifications', 'ActionInvoked',
|
||||
'us', [['u', notificationId], ['s', 'default']]);
|
||||
|
||||
await clicked;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user