From 8275f6e90d60daa31c5641ab695903c842b117fc Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Mon, 6 Apr 2026 12:44:16 -0400 Subject: [PATCH] Add D-Bus notification driver Use the core and portal notification implementations to dispatch system notifications. --- src/notification/unix/SDL_dbusnotification.c | 1581 ++++++++++++++++++ src/video/wayland/SDL_waylandutil.c | 5 + src/video/wayland/SDL_waylandwindow.c | 24 + test/testnotification.c | 1 - 4 files changed, 1610 insertions(+), 1 deletion(-) create mode 100644 src/notification/unix/SDL_dbusnotification.c diff --git a/src/notification/unix/SDL_dbusnotification.c b/src/notification/unix/SDL_dbusnotification.c new file mode 100644 index 0000000000..18a7c175d7 --- /dev/null +++ b/src/notification/unix/SDL_dbusnotification.c @@ -0,0 +1,1581 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "../../core/linux/SDL_dbus.h" +#include "../../core/unix/SDL_appid.h" +#include "../../events/SDL_notificationevents_c.h" +#include "../../io/SDL_iostream_c.h" +#include "../../video/SDL_surface_c.h" +#include + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_MEMFD_CREATE +#include +#endif + +#define NOTIFICATION_PORTAL_NODE "org.freedesktop.portal.Desktop" +#define NOTIFICATION_PORTAL_PATH "/org/freedesktop/portal/desktop" +#define NOTIFICATION_PORTAL_INTERFACE "org.freedesktop.portal.Notification" + +#define NOTIFICATION_CORE_NODE "org.freedesktop.Notifications" +#define NOTIFICATION_CORE_PATH "/org/freedesktop/Notifications" +#define NOTIFICATION_CORE_INTERFACE "org.freedesktop.Notifications" + +#define NOTIFICATION_ACTION_SIGNAL_NAME "ActionInvoked" +#define NOTIFICATION_CLOSED_SIGNAL_NAME "NotificationClosed" +#define NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "ActivationToken" + +#define SDL_NOTIFICATION_PREAMBLE "SDL_LocalNotification-" + +#define ACTIVATION_TOKEN_LIFETIME SDL_SECONDS_TO_NS(1) + +static Uint64 activation_token_time_ns; +static char *activation_token; + +static char *icon_uri; +static Uint64 session_id; + +static Uint32 core_id_list[32]; +static Uint32 core_id_count; + +static Uint32 interface_version; + +static bool core_interface_initialized; +static bool portal_interface_initialized; + +static void GetRandom(void *dst, size_t size) +{ + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + fd = open("/dev/random", O_RDONLY); + } + if (fd >= 0) { + while (read(fd, dst, size) != size) { + } + close(fd); + } else { + size_t written = 0; + + while (written < size) { + const Uint64 tval = SDL_GetTicksNS(); + const size_t towrite = SDL_min(size - written, sizeof(tval)); + SDL_memcpy((Uint8 *)dst + written, &tval, towrite); + written += towrite; + } + } +} + +static bool AppendOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *type, const char *key, const void *value) +{ + DBusMessageIter options_pair, options_value; + + if (!dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + return false; + } + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, type, &options_value)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_value, (int)type[0], value)) { + return false; + } + if (!dbus->message_iter_close_container(&options_pair, &options_value)) { + return false; + } + return (bool)dbus->message_iter_close_container(options, &options_pair); +} + +static bool AppendStringOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value) +{ + DBusMessageIter options_pair, options_value; + + if (!dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + return false; + } + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &options_value)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_value, DBUS_TYPE_STRING, &value)) { + return false; + } + if (!dbus->message_iter_close_container(&options_pair, &options_value)) { + return false; + } + return (bool)dbus->message_iter_close_container(options, &options_pair); +} + +static bool AppendTargetString(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key) +{ + char target_buf[128]; + const char *target_val = target_buf; + DBusMessageIter options_pair, target_variant, target_string; + + SDL_snprintf(target_buf, sizeof(target_buf), "%" SDL_PRIu64, session_id); + + if (!dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + return false; + } + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, DBUS_TYPE_VARIANT_AS_STRING, &target_variant)) { + return false; + } + if (!dbus->message_iter_open_container(&target_variant, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &target_string)) { + return false; + } + if (!dbus->message_iter_append_basic(&target_string, DBUS_TYPE_STRING, &target_val)) { + return false; + } + if (!dbus->message_iter_close_container(&target_variant, &target_string)) { + return false; + } + if (!dbus->message_iter_close_container(&options_pair, &target_variant)) { + return false; + } + return (bool)dbus->message_iter_close_container(options, &options_pair); +} + +static void RemoveIDFromListAtIndex(Uint32 index) +{ + --core_id_count; + if (index < core_id_count) { + SDL_memmove(&core_id_list[index], &core_id_list[index + 1], (core_id_count - index) * sizeof(Uint32)); + } +} + +static bool HasDesktopFile(const char *app_id) +{ + char path[PATH_MAX]; + struct stat st; + + if (!app_id) { + return NULL; + } + + const char *xdg_data_home = SDL_getenv("XDG_DATA_HOME"); + if (xdg_data_home) { + SDL_snprintf(path, SDL_arraysize(path), "%s/applications/%s.desktop", xdg_data_home, app_id); + if (stat(path, &st) == 0) { + return true; + } + } else { + xdg_data_home = SDL_getenv("HOME"); + if (xdg_data_home) { + SDL_snprintf(path, SDL_arraysize(path), "%s/.local/share/applications/%s.desktop", xdg_data_home, app_id); + if (stat(path, &st) == 0) { + return true; + } + } + } + + if (xdg_data_home) { + SDL_snprintf(path, SDL_arraysize(path), "%s/.local/share/applications/%s.desktop", xdg_data_home, app_id); + if (stat(path, &st) == 0) { + return true; + } + } + + SDL_snprintf(path, SDL_arraysize(path), "/usr/share/local/applications/%s.desktop", app_id); + if (stat(path, &st) == 0) { + return true; + } + + SDL_snprintf(path, SDL_arraysize(path), "/usr/share/applications/%s.desktop", app_id); + if (stat(path, &st) == 0) { + return true; + } + + return false; +} + +// org.freedesktop.Notifications, used when not running inside a container. +static DBusHandlerResult CoreNotificationFilter(DBusConnection *conn, DBusMessage *msg, void *data) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + + if (dbus->message_is_signal(msg, NOTIFICATION_CORE_NODE, NOTIFICATION_ACTION_SIGNAL_NAME)) { + DBusMessageIter signal_iter; // variant_iter; + const char *button = NULL; + Uint32 id = 0; + bool own_id = false; + + if (!dbus->message_iter_init(msg, &signal_iter)) { + goto not_our_signal; + } + + // Check if the parameters are what we expect + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &id); + + // See if this signal is for this client. + for (Uint32 i = 0; i < core_id_count; ++i) { + if (id == core_id_list[i]) { + RemoveIDFromListAtIndex(i); + own_id = true; + break; + } + } + if (!own_id) { + goto not_our_signal; + } + + if (!dbus->message_iter_next(&signal_iter)) { + goto not_our_signal; + } + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &button); + if (button) { + SDL_SendNotificationAction(id, button); + } + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus->message_is_signal(msg, NOTIFICATION_CORE_NODE, NOTIFICATION_CLOSED_SIGNAL_NAME)) { + DBusMessageIter signal_iter; // variant_iter; + Uint32 id = 0, reason = 0; + + dbus->message_iter_init(msg, &signal_iter); + // Check if the parameters are what we expect + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &id); + + if (!dbus->message_iter_next(&signal_iter)) { + goto not_our_signal; + } + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &reason); + if (id && reason) { + for (Uint32 i = 0; i < core_id_count; ++i) { + if (core_id_list[i] == id) { + RemoveIDFromListAtIndex(i); + return DBUS_HANDLER_RESULT_HANDLED; + } + } + } + } else if (dbus->message_is_signal(msg, NOTIFICATION_CORE_NODE, NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME)) { + DBusMessageIter signal_iter; // variant_iter; + const char *token = NULL; + Uint32 id = 0; + + dbus->message_iter_init(msg, &signal_iter); + // Check if the parameters are what we expect + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &id); + + if (!dbus->message_iter_next(&signal_iter)) { + goto not_our_signal; + } + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &token); + if (id && token) { + for (Uint32 i = 0; i < core_id_count; ++i) { + if (core_id_list[i] == id) { + SDL_free(activation_token); + activation_token = SDL_strdup(token); + activation_token_time_ns = SDL_GetTicksNS(); + return DBUS_HANDLER_RESULT_HANDLED; + } + } + } + } + +not_our_signal: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static bool SetCoreImage(SDL_DBusContext *dbus, DBusMessageIter *iterInit, SDL_Surface *surface) +{ + DBusMessageIter iterEntry, iterValue; + DBusMessageIter iter, array; + const dbus_bool_t alpha = true; + Sint32 bpp = 8; + + if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_DICT_ENTRY, NULL, &iterEntry)) { + return false; + } + + const char *key = "image-data"; + if (!dbus->message_iter_append_basic(&iterEntry, DBUS_TYPE_STRING, &key)) { + return false; + } + + if (!dbus->message_iter_open_container(&iterEntry, DBUS_TYPE_VARIANT, "(iiibiiay)", &iterValue)) { + return false; + } + + if (!dbus->message_iter_open_container(&iterValue, DBUS_TYPE_STRUCT, NULL, &iter)) { + return false; + } + + // Width + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &surface->w)) { + return false; + } + + // Height + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &surface->h)) { + return false; + } + + // Pitch + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &surface->pitch)) { + return false; + } + + // Alpha yes/no + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_BOOLEAN, &alpha)) { + return false; + } + + // BPP + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &bpp)) { + return false; + } + + // Channels (always 4 with alpha) + bpp = 4; + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &bpp)) { + return false; + } + + // Raw image bytes + if (!dbus->message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "y", &array)) { + return false; + } + + const Uint8 *pixels = surface->pixels; + for (int i = 0; i < surface->pitch * surface->h; i++) { + if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_BYTE, &pixels[i])) { + return false; + } + } + + if (!dbus->message_iter_close_container(&iter, &array)) { + return false; + } + if (!dbus->message_iter_close_container(&iterValue, &iter)) { + return false; + } + if (!dbus->message_iter_close_container(&iterEntry, &iterValue)) { + return false; + } + if (!dbus->message_iter_close_container(iterInit, &iterEntry)) { + return false; + } + + return true; +} + +static bool SetCoreHints(SDL_DBusContext *dbus, DBusMessageIter *iterInit, SDL_PropertiesID props) +{ + DBusMessageIter iterDict; + const char *app_id = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING); + const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default"); + SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL); + SDL_NotificationPriority priority = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL); + const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false); + + if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_ARRAY, "{sv}", &iterDict)) { + return false; + } + + Uint8 dbus_priority; + + switch (priority) { + case SDL_NOTIFICATION_PRIORITY_NORMAL: + case SDL_NOTIFICATION_PRIORITY_HIGH: + default: + dbus_priority = 1; + break; + case SDL_NOTIFICATION_PRIORITY_LOW: + dbus_priority = 0; + break; + case SDL_NOTIFICATION_PRIORITY_CRITICAL: + dbus_priority = 2; + break; + } + + if (!AppendOption(dbus, &iterDict, DBUS_TYPE_BYTE_AS_STRING, "urgency", &dbus_priority)) { + return false; + } + + if (app_id) { + if (HasDesktopFile(app_id)) { + if (!AppendOption(dbus, &iterDict, DBUS_TYPE_STRING_AS_STRING, "desktop-entry", &app_id)) { + return false; + } + } + } + + const dbus_bool_t db_transient = transient; + if (!AppendOption(dbus, &iterDict, DBUS_TYPE_BOOLEAN_AS_STRING, "transient", &db_transient)) { + return false; + } + + if (SDL_strcmp(sound, "default") == 0) { + if (!AppendStringOption(dbus, &iterDict, "sound-name", "dialog-information")) { + return false; + } + } else if (SDL_strcmp(sound, "silent") != 0) { + char sound_path[PATH_MAX]; + if (realpath(sound, sound_path)) { + if (!AppendStringOption(dbus, &iterDict, "sound-file", sound_path)) { + return false; + } + } else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Notification sound '%s' not found", sound); + + // Play the default if the custom sound is not found. + if (!AppendStringOption(dbus, &iterDict, "sound-name", "dialog-information")) { + return false; + } + } + } + + if (image) { + SDL_Surface *image_surface = image; + if (image->format != SDL_PIXELFORMAT_ABGR8888) { + image_surface = SDL_ConvertSurface(image, SDL_PIXELFORMAT_ABGR8888); + } + + SetCoreImage(dbus, &iterDict, image_surface); + + if (image_surface != image) { + SDL_DestroySurface(image_surface); + } + } + + if (!dbus->message_iter_close_container(iterInit, &iterDict)) { + return false; + } + + return true; +} + +static bool InitCoreSignalListener(SDL_DBusContext *dbus) +{ + static bool interface_unavailable = false; + if (interface_unavailable) { + return false; + } + + // Query the server information to see if the notification interface is available. + DBusMessage *msg = dbus->message_new_method_call(NOTIFICATION_CORE_NODE, NOTIFICATION_CORE_PATH, NOTIFICATION_CORE_INTERFACE, "GetServerInformation"); + if (msg == NULL) { + return false; + } + DBusMessage *reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, -1, NULL); + dbus->message_unref(msg); + if (!reply) { + // Mark the interface as unavailable. + interface_unavailable = true; + return false; + } + dbus->message_unref(reply); + + if (!session_id) { + GetRandom(&session_id, sizeof(session_id)); + } + + DBusError error; + dbus->error_init(&error); + + dbus->bus_add_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'", + &error); + if (dbus->error_is_set(&error)) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message); + dbus->error_free(&error); + return false; + } + + dbus->bus_add_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_CLOSED_SIGNAL_NAME "'", + &error); + if (dbus->error_is_set(&error)) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message); + dbus->error_free(&error); + goto unregister_action; + } + + dbus->bus_add_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "'", + &error); + if (dbus->error_is_set(&error)) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message); + dbus->error_free(&error); + goto unregister_closed; + } + dbus->error_free(&error); + + if (dbus->connection_add_filter(dbus->session_conn, &CoreNotificationFilter, NULL, NULL)) { + dbus->connection_flush(dbus->session_conn); + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Registered DBus portal notification filter"); + } else { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus notification filter: %s", error.message); + goto unregister_token; + } + + core_interface_initialized = true; + return true; + + // On failure, undo all registrations. +unregister_token: + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "'", + NULL); + +unregister_closed: + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_CLOSED_SIGNAL_NAME "'", + NULL); + +unregister_action: + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'", + NULL); + + return false; +} + +static const char *GetIconURI() +{ + if (icon_uri) { + return icon_uri; + } + + char full_path[PATH_MAX]; + SDL_PropertiesID props = SDL_GetGlobalProperties(); + const char *icon = SDL_GetStringProperty(props, SDL_PROP_GLOBAL_NOTIFICATION_HEADER_ICON_STRING, NULL); + if (icon && realpath(icon, full_path)) { + size_t len = SDL_strlen(full_path) + 8; + icon_uri = SDL_malloc(len); + if (icon_uri) { + SDL_strlcpy(icon_uri, "file://", len); + SDL_strlcat(icon_uri, full_path, len); + } + } + + return icon_uri; +} + +static SDL_NotificationID ShowCoreNotification(SDL_DBusContext *dbus, SDL_PropertiesID props) +{ + DBusConnection *conn = dbus->session_conn; + DBusMessage *msg = NULL; + DBusMessageIter iter, array; + const char *tmpstr = NULL; + const Sint32 timeout = -1; + Uint32 message_id = 0; + bool error_set = false; + + const SDL_PropertiesID replaces = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0); + const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL); + const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, NULL); + const SDL_NotificationAction *actions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL); + const int num_actions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0); + + // Call org.freedesktop.Notifications.Notify() + msg = dbus->message_new_method_call(NOTIFICATION_CORE_NODE, NOTIFICATION_CORE_PATH, NOTIFICATION_CORE_INTERFACE, "Notify"); + if (msg == NULL) { + goto failure; + } + + dbus->message_iter_init_append(msg, &iter); + // App ID + tmpstr = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING); + if (!tmpstr) { + SDL_GetAppID(); + } + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &tmpstr)) { + goto failure; + } + + // Replaces id + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &replaces)) { + goto failure; + } + + // Icon URI + tmpstr = GetIconURI(); + if (!tmpstr) { + tmpstr = ""; + } + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &tmpstr)) { + goto failure; + } + + // Summary + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &title)) { + goto failure; + } + + // Body + tmpstr = message ? message : ""; + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &message)) { + goto failure; + } + + { + // Actions + if (!dbus->message_iter_open_container(&iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &array)) { + goto failure; + } + + // Add the default action + tmpstr = "default"; + if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &tmpstr)) { + goto failure; + } + if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &tmpstr)) { + goto failure; + } + + // Add the actions + if (actions) { + for (int i = 0; i < num_actions; ++i) { + if (actions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) { + if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &actions[i].button.action_id)) { + goto failure; + } + if (!dbus->message_iter_append_basic(&array, DBUS_TYPE_STRING, &actions[i].button.action_label)) { + goto failure; + } + } + } + } + if (!dbus->message_iter_close_container(&iter, &array)) { + goto failure; + } + } + + // Hints + if (!SetCoreHints(dbus, &iter, props)) { + goto failure; + } + + // Timeout + if (!dbus->message_iter_append_basic(&iter, DBUS_TYPE_INT32, &timeout)) { + goto failure; + } + + { + DBusMessageIter reply_iter; + DBusError error; + + dbus->error_init(&error); + DBusMessage *reply = dbus->connection_send_with_reply_and_block(conn, msg, -1, &error); + if (!reply) { + if (error.message) { + SDL_SetError("Notification failed: %s", error.message); + error_set = true; + } + dbus->error_free(&error); + goto failure; + } + + dbus->error_free(&error); + dbus->message_unref(msg); + + if (!dbus->message_iter_init(reply, &reply_iter)) { + goto failure; + } + if (dbus->message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_UINT32) { + dbus->message_unref(reply); + goto failure; + } + dbus->message_iter_get_basic(&reply_iter, &message_id); + dbus->message_unref(reply); + } + + if (core_id_count == SDL_arraysize(core_id_list)) { + RemoveIDFromListAtIndex(0); + } + core_id_list[core_id_count++] = message_id; + + return message_id; + +failure: + if (msg) { + dbus->message_unref(msg); + } + if (!error_set) { + SDL_SetError("Failed to dispatch org.freedesktop.Notifications request (Out of memory?)"); + } + return 0; +} + +static bool RemoveCoreNotification(SDL_DBusContext *dbus, SDL_NotificationID id) +{ + if (!id) { + return SDL_InvalidParamError("id"); + } + + DBusMessageIter iter; + bool ret = false; + + // Call org.freedesktop.Notifications.CloseNotification() + DBusMessage *msg = dbus->message_new_method_call(NOTIFICATION_CORE_NODE, NOTIFICATION_CORE_PATH, NOTIFICATION_CORE_INTERFACE, "CloseNotification"); + if (!msg) { + return SDL_OutOfMemory(); + } + + dbus->message_iter_init_append(msg, &iter); + if (dbus->message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &id)) { + ret = (bool)dbus->connection_send(dbus->session_conn, msg, NULL); + if (!ret) { + SDL_SetError("Failed to send notification removal request"); + } + } else { + ret = SDL_OutOfMemory(); + } + + dbus->message_unref(msg); + return ret; +} + +// org.freedesktop.portal.Notification interface, used when running in a Flatpak or SNAP container +static DBusHandlerResult PortalNotificationFilter(DBusConnection *conn, DBusMessage *msg, void *data) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + + if (dbus->message_is_signal(msg, NOTIFICATION_PORTAL_INTERFACE, NOTIFICATION_ACTION_SIGNAL_NAME)) { + DBusMessageIter signal_iter; //, variant_iter; + const char *str = NULL; + + if (!dbus->message_iter_init(msg, &signal_iter)) { + goto not_our_signal; + } + + // Check if the parameters are what we expect + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &str); + + // Parse the ID. + if (SDL_strncmp(str, SDL_NOTIFICATION_PREAMBLE, sizeof(SDL_NOTIFICATION_PREAMBLE) - 1) != 0) { + goto not_our_signal; + } + const Uint32 id = (Uint32)SDL_strtoul(str + sizeof(SDL_NOTIFICATION_PREAMBLE), NULL, 10); + if (!id) { + goto not_our_signal; + } + + if (!dbus->message_iter_next(&signal_iter)) { + goto not_our_signal; + } + if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) { + goto not_our_signal; + } + dbus->message_iter_get_basic(&signal_iter, &str); + + // Check for the target and optional XDG activation parameter. + const char *target = NULL, *token = NULL; + + if (dbus->message_iter_next(&signal_iter) && dbus->message_iter_get_arg_type(&signal_iter) == DBUS_TYPE_ARRAY) { + DBusMessageIter param_iter; + dbus->message_iter_recurse(&signal_iter, ¶m_iter); + + // The order of parameters in the array is defined: first the target, then the activation ID. + if (dbus->message_iter_get_arg_type(¶m_iter) == DBUS_TYPE_VARIANT) { + DBusMessageIter target_variant, target_string; + + dbus->message_iter_recurse(¶m_iter, &target_variant); + if (dbus->message_iter_get_arg_type(&target_variant) == DBUS_TYPE_VARIANT) { + dbus->message_iter_recurse(&target_variant, &target_string); + if (dbus->message_iter_get_arg_type(&target_string) == DBUS_TYPE_STRING) { + dbus->message_iter_get_basic(&target_string, &target); + } + } + dbus->message_iter_next(¶m_iter); + } + + // System properties array. + if (dbus->message_iter_get_arg_type(¶m_iter) == DBUS_TYPE_ARRAY) { + DBusMessageIter pdata_iter; + + dbus->message_iter_recurse(¶m_iter, &pdata_iter); + while (dbus->message_iter_get_arg_type(&pdata_iter) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_entry_iter; + + // Enter the dictionary entry. + dbus->message_iter_recurse(¶m_iter, &dict_entry_iter); + + // Get the key + const char *key = NULL; + dbus->message_iter_get_basic(&dict_entry_iter, &key); + + // Get the activation token string. + if (SDL_strcmp(key, "activation-token") == 0) { + DBusMessageIter dict_val_iter; + // Enter the value variant. + dbus->message_iter_recurse(&dict_entry_iter, &dict_val_iter); + + if (dbus->message_iter_get_arg_type(&dict_val_iter) == DBUS_TYPE_STRING) { + dbus->message_iter_get_basic(&dict_val_iter, &token); + } + + // Found the activation token, nothing else to do. + break; + } + + dbus->message_iter_next(&pdata_iter); + } + } + } + + if (target && SDL_strtoull(target, NULL, 10) == session_id) { + if (token) { + SDL_free(activation_token); + activation_token = SDL_strdup(token); + activation_token_time_ns = SDL_GetTicksNS(); + } + SDL_SendNotificationAction(id, str); + + return DBUS_HANDLER_RESULT_HANDLED; + } + } + +not_our_signal: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static bool SetPortalImage(SDL_DBusContext *dbus, DBusMessageIter *iterInit, SDL_Surface *surface) +{ + static const char *fd_string = "file-descriptor"; + static const char *key = "icon"; + DBusMessageIter options_pair, variant_iter, struct_iter; + SDL_IOStream *png = NULL; + int fd = -1; + bool ret = false; + +#ifdef HAVE_MEMFD_CREATE + /* Version 2 of the portal interface wants images passed as a sealable file descriptor, + * which is only possible with memfd_create(). + */ + if (interface_version >= 2) { + fd = memfd_create("SDL_NotificationImage", MFD_ALLOW_SEALING); + if (fd >= 0) { + png = SDL_IOFromFD(fd, true); + if (!png) { + close(fd); + fd = -1; + } + } + } +#endif + + if (!png) { + png = SDL_IOFromDynamicMem(); + } + if (!png) { + return false; + } + if (!SDL_SavePNG_IO(surface, png, false)) { + goto done; + } + const Sint64 size = SDL_GetIOSize(png); + + if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + goto done; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + goto done; + } + if (fd >= 0) { + DBusMessageIter fd_variant_iter; + + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "(sv)", &variant_iter)) { + goto done; + } + if (!dbus->message_iter_open_container(&variant_iter, DBUS_TYPE_STRUCT, NULL, &struct_iter)) { + goto done; + } + if (!dbus->message_iter_append_basic(&struct_iter, DBUS_TYPE_STRING, &fd_string)) { + goto done; + } + if (!dbus->message_iter_open_container(&struct_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_UNIX_FD_AS_STRING, &fd_variant_iter)) { + goto done; + } + if (!dbus->message_iter_append_basic(&fd_variant_iter, DBUS_TYPE_UNIX_FD, &fd)) { + goto done; + } + if (!dbus->message_iter_close_container(&struct_iter, &fd_variant_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&variant_iter, &struct_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&options_pair, &variant_iter)) { + goto done; + } + } else { + DBusMessageIter array_iter, byte_array_iter; + const char *bytes_string = "bytes"; + + SDL_PropertiesID io_props = SDL_GetIOProperties(png); + Uint8 *png_ptr = SDL_GetPointerProperty(io_props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL); + + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "(sv)", &variant_iter)) { + goto done; + } + if (!dbus->message_iter_open_container(&variant_iter, DBUS_TYPE_STRUCT, NULL, &struct_iter)) { + goto done; + } + if (!dbus->message_iter_append_basic(&struct_iter, DBUS_TYPE_STRING, &bytes_string)) { + goto done; + } + if (!dbus->message_iter_open_container(&struct_iter, DBUS_TYPE_VARIANT, "ay", &byte_array_iter)) { + goto done; + } + if (!dbus->message_iter_open_container(&byte_array_iter, DBUS_TYPE_ARRAY, "y", &array_iter)) { + goto done; + } + + for (Sint64 i = 0; i < size; ++i) { + if (!dbus->message_iter_append_basic(&array_iter, DBUS_TYPE_BYTE, &png_ptr[i])) { + goto done; + } + } + + if (!dbus->message_iter_close_container(&byte_array_iter, &array_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&struct_iter, &byte_array_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&variant_iter, &struct_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&options_pair, &variant_iter)) { + goto done; + } + } + ret = (bool)dbus->message_iter_close_container(iterInit, &options_pair); + +done: + + SDL_CloseIO(png); + return ret; +} + +#ifdef HAVE_MEMFD_CREATE +static bool SetFileSize(int fd, off_t size) +{ +#ifdef HAVE_POSIX_FALLOCATE + sigset_t set, old_set; + int ret; + + /* SIGALRM can potentially block a large posix_fallocate() operation + * from succeeding, so block it. + */ + sigemptyset(&set); + sigaddset(&set, SIGALRM); + sigprocmask(SIG_BLOCK, &set, &old_set); + + do { + ret = posix_fallocate(fd, 0, size); + } while (ret == EINTR); + + sigprocmask(SIG_SETMASK, &old_set, NULL); + + if (ret == 0) { + return true; + } else if (ret != EINVAL && errno != EOPNOTSUPP) { + return false; + } +#endif + + if (ftruncate(fd, size) < 0) { + return false; + } + return true; +} +#endif + +static bool SetPortalSound(SDL_DBusContext *dbus, DBusMessageIter *iterInit, const char *sound) +{ + static const char *key = "sound"; + bool ret = true; + + if (SDL_strcmp(sound, "default") == 0) { + return AppendStringOption(dbus, iterInit, key, "default"); + } else if (SDL_strcmp(sound, "silent") == 0) { + return true; + } + + // Passing sound files to a portal must be done via a sealable file descriptor, which is only possible with memfd_create. +#ifdef HAVE_MEMFD_CREATE + static const char *fd_string = "file-descriptor"; + DBusMessageIter options_pair, variant_iter, struct_iter, fd_variant_iter; + struct stat st; + int mem_fd = -1; + ret = false; + + if (stat(sound, &st) != 0) { + // Log an error if the sound file can't be found, but this is not fatal. + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Notification sound file '%s' not found", sound); + return AppendStringOption(dbus, iterInit, key, "default"); + } + // Copy the sound file to a memfd. + const int read_fd = open(sound, O_RDONLY | O_CLOEXEC); + if (read_fd >= 0) { + mem_fd = memfd_create(sound, MFD_ALLOW_SEALING | MFD_CLOEXEC); + if (mem_fd < 0) { + close(read_fd); + return false; + } + } else { + // Log an error if the sound file can't be opened, but this is not fatal. + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Notification sound file '%s' cannot be opened; check file permissions", sound); + return AppendStringOption(dbus, iterInit, key, "default"); + } + + if (!SetFileSize(mem_fd, st.st_size)) { + return false; + } + + // Map the memfd, so the data can be read directly into it. + void *data = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, 0); + if (data == MAP_FAILED) { + close(read_fd); + close(mem_fd); + return false; + } + + const ssize_t res = read(read_fd, data, st.st_size); + munmap(data, st.st_size); + close(read_fd); + + if (res != st.st_size) { + close(mem_fd); + return false; + } + + // Set the tuple values (sv). + if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + goto done; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + goto done; + } + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "(sv)", &variant_iter)) { + goto done; + } + if (!dbus->message_iter_open_container(&variant_iter, DBUS_TYPE_STRUCT, NULL, &struct_iter)) { + goto done; + } + if (!dbus->message_iter_append_basic(&struct_iter, DBUS_TYPE_STRING, &fd_string)) { + goto done; + } + if (!dbus->message_iter_open_container(&struct_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_UNIX_FD_AS_STRING, &fd_variant_iter)) { + goto done; + } + if (!dbus->message_iter_append_basic(&fd_variant_iter, DBUS_TYPE_UNIX_FD, &mem_fd)) { + goto done; + } + if (!dbus->message_iter_close_container(&struct_iter, &fd_variant_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&variant_iter, &struct_iter)) { + goto done; + } + if (!dbus->message_iter_close_container(&options_pair, &variant_iter)) { + goto done; + } + ret = dbus->message_iter_close_container(iterInit, &options_pair); + +done: + if (mem_fd >= 0) { + close(mem_fd); + } +#endif + + return ret; +} + +static bool AddPortalActions(SDL_DBusContext *dbus, DBusMessageIter *iterInit, const SDL_NotificationAction *actions, int num_actions) +{ + DBusMessageIter options_pair, options_value, button_array, properties_array; + const char *key = "buttons"; + + if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + return false; + } + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "aa{sv}", &options_value)) { + return false; + } + if (!dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "a{sv}", &button_array)) { + return false; + } + + for (int i = 0; i < num_actions; ++i) { + if (actions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) { + if (!dbus->message_iter_open_container(&button_array, DBUS_TYPE_ARRAY, "{sv}", &properties_array)) { + return false; + } + + if (!AppendStringOption(dbus, &properties_array, "action", actions[i].button.action_id)) { + return false; + } + if (!AppendStringOption(dbus, &properties_array, "label", actions[i].button.action_label)) { + return false; + } + if (!AppendTargetString(dbus, &properties_array, "target")) { + return false; + } + + if (!dbus->message_iter_close_container(&button_array, &properties_array)) { + return false; + } + } + } + + if (!dbus->message_iter_close_container(&options_value, &button_array)) { + return false; + } + if (!dbus->message_iter_close_container(&options_pair, &options_value)) { + return false; + } + if (!dbus->message_iter_close_container(iterInit, &options_pair)) { + return false; + } + + return true; +} + +static bool SetPortalDisplayHints(SDL_DBusContext *dbus, DBusMessageIter *iterInit, SDL_PropertiesID props) +{ + DBusMessageIter options_pair, options_value, var_struct, string_array; + const char *key = "display-hint"; + const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false); + + if (!dbus->message_iter_open_container(iterInit, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair)) { + return false; + } + if (!dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key)) { + return false; + } + if (!dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "(as)", &options_value)) { + return false; + } + if (!dbus->message_iter_open_container(&options_value, DBUS_TYPE_STRUCT, NULL, &var_struct)) { + return false; + } + if (!dbus->message_iter_open_container(&var_struct, DBUS_TYPE_ARRAY, "s", &string_array)) { + return false; + } + + if (transient) { + const char *val = "transient"; + if (!dbus->message_iter_append_basic(&string_array, DBUS_TYPE_STRING, &val)) { + return false; + } + } + + if (!dbus->message_iter_close_container(&var_struct, &string_array)) { + return false; + } + if (!dbus->message_iter_close_container(&options_value, &var_struct)) { + return false; + } + if (!dbus->message_iter_close_container(&options_pair, &options_value)) { + return false; + } + return (bool)dbus->message_iter_close_container(iterInit, &options_pair); +} + +static bool InitPortalSignalListener(SDL_DBusContext *dbus) +{ + static bool interface_unavailable = false; + if (interface_unavailable) { + return false; + } + + if (!SDL_DBus_QueryProperty(NULL, + NOTIFICATION_PORTAL_NODE, NOTIFICATION_PORTAL_PATH, NOTIFICATION_PORTAL_INTERFACE, + "version", DBUS_TYPE_UINT32, &interface_version)) { + // Mark the interface as unavailable. + interface_unavailable = true; + return false; + } + + if (!session_id) { + GetRandom(&session_id, sizeof(session_id)); + } + + DBusError error; + dbus->error_init(&error); + + dbus->bus_add_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_PORTAL_INTERFACE "'," + "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'", + &error); + if (dbus->error_is_set(&error)) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Failed to register DBus portal notification filter: %s", error.message); + dbus->error_free(&error); + return false; + } + dbus->error_free(&error); + + if (dbus->connection_add_filter(dbus->session_conn, &PortalNotificationFilter, NULL, NULL)) { + dbus->connection_flush(dbus->session_conn); + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Registered DBus portal notification filter"); + } else { + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_PORTAL_INTERFACE "'," + "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'", + NULL); + return false; + } + + portal_interface_initialized = true; + return true; +} + +static SDL_NotificationID ShowPortalNotification(SDL_DBusContext *dbus, SDL_PropertiesID props) +{ + DBusConnection *conn = dbus->session_conn; + DBusMessage *msg = NULL; + DBusMessageIter iter, array; + bool error_set = false; + + const SDL_PropertiesID replaces = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0); + const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL); + const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, NULL); + const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default"); + const SDL_NotificationPriority priority = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL); + SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL); + const SDL_NotificationAction *actions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL); + const int num_actions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0); + + // Call Notification.AddNotification() + msg = dbus->message_new_method_call(NOTIFICATION_PORTAL_NODE, NOTIFICATION_PORTAL_PATH, NOTIFICATION_PORTAL_INTERFACE, "AddNotification"); + if (msg == NULL) { + goto failure; + } + + dbus->message_iter_init_append(msg, &iter); + + // Notification ID + Uint32 new_id = 0; + if (!replaces) { + GetRandom(&new_id, sizeof(new_id)); + } else { + new_id = replaces; + } + + { + char id_str[128]; + SDL_snprintf(id_str, SDL_arraysize(id_str), SDL_NOTIFICATION_PREAMBLE "%" SDL_PRIu32, new_id); + const char *id = id_str; + dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &id); + } + + // Parameters + dbus->message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array); + if (!AppendStringOption(dbus, &array, "title", title)) { + goto failure; + } + if (!AppendStringOption(dbus, &array, "body", message)) { + goto failure; + } + if (!AppendStringOption(dbus, &array, "default-action", "default")) { + goto failure; + } + if (!AppendTargetString(dbus, &array, "default-action-target")) { + goto failure; + } + + if (!SetPortalSound(dbus, &array, sound)) { + goto failure; + } + + const char *priority_str; + switch (priority) { + case SDL_NOTIFICATION_PRIORITY_NORMAL: + default: + priority_str = "normal"; + break; + case SDL_NOTIFICATION_PRIORITY_LOW: + priority_str = "low"; + break; + case SDL_NOTIFICATION_PRIORITY_HIGH: + priority_str = "high"; + break; + case SDL_NOTIFICATION_PRIORITY_CRITICAL: + priority_str = "urgent"; + break; + } + + if (!AppendStringOption(dbus, &array, "priority", priority_str)) { + goto failure; + } + if (!SetPortalDisplayHints(dbus, &array, props)) { + goto failure; + } + + if (image) { + SDL_Surface *image_surface = image; + if (image_surface->format != SDL_PIXELFORMAT_ABGR8888) { + image_surface = SDL_ConvertSurface(image_surface, SDL_PIXELFORMAT_ABGR8888); + } + + const bool res = SetPortalImage(dbus, &array, image_surface); + + if (image_surface != image) { + SDL_DestroySurface(image_surface); + } + + if (!res) { + goto failure; + } + } + + if (actions && num_actions) { + if (!AddPortalActions(dbus, &array, actions, num_actions)) { + goto failure; + } + } + + if (!dbus->message_iter_close_container(&iter, &array)) { + goto failure; + } + + { + DBusError err; + dbus->error_init(&err); + DBusMessage *reply = dbus->connection_send_with_reply_and_block(conn, msg, -1, &err); + dbus->message_unref(msg); + + if (!reply) { + if (err.message) { + SDL_SetError("Notification failed: %s", err.message); + error_set = true; + } + dbus->message_unref(reply); + dbus->error_free(&err); + goto failure; + } + + dbus->message_unref(reply); + return new_id; + } + +failure: + if (msg) { + dbus->message_unref(msg); + } + if (!error_set) { + SDL_SetError("Failed to dispatch org.freedesktop.portal.Notification request (Out of memory?)"); + } + return 0; +} + +static bool RemovePortalNotification(SDL_DBusContext *dbus, SDL_NotificationID id) +{ + if (!id) { + return SDL_InvalidParamError("id"); + } + + char id_str[128]; + DBusMessageIter iter; + bool ret = false; + + // Call org.freedesktop.Notifications.CloseNotification() + DBusMessage *msg = dbus->message_new_method_call(NOTIFICATION_PORTAL_NODE, NOTIFICATION_PORTAL_PATH, NOTIFICATION_PORTAL_INTERFACE, "RemoveNotification"); + if (msg == NULL) { + return SDL_OutOfMemory(); + } + + dbus->message_iter_init_append(msg, &iter); + + SDL_snprintf(id_str, SDL_arraysize(id_str), SDL_NOTIFICATION_PREAMBLE "%" SDL_PRIu32, id); + const char *cstr = id_str; + if (dbus->message_iter_append_basic(&iter, DBUS_TYPE_STRING, &cstr)) { + ret = (bool)dbus->connection_send(dbus->session_conn, msg, NULL); + if (!ret) { + SDL_SetError("Failed to send notification removal request"); + } + } else { + ret = SDL_OutOfMemory(); + } + + dbus->message_unref(msg); + return ret; +} + +static bool CheckInitNotifications(SDL_DBusContext *dbus) +{ + bool ret = false; + if (!dbus || !dbus->session_conn) { + return SDL_SetError("D-Bus not available"); + } + + if (core_interface_initialized || portal_interface_initialized) { + return true; + } + + if (SDL_GetSandbox() != SDL_SANDBOX_NONE) { + ret = InitPortalSignalListener(dbus); + if (!ret) { + ret = InitCoreSignalListener(dbus); + } + } else { + ret = InitCoreSignalListener(dbus); + if (!ret) { + ret = InitPortalSignalListener(dbus); + } + } + + return ret ? ret : SDL_SetError("Notification interface not available"); +} + +SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + + if (!CheckInitNotifications(dbus)) { + return 0; + } + + // The portal is only used if inside a container, or the app association can be wrong. + if (portal_interface_initialized) { + return ShowPortalNotification(dbus, props); + } else if (core_interface_initialized) { + return ShowCoreNotification(dbus, props); + } + + return 0; +} + +bool SDL_RemoveNotification(SDL_NotificationID notification) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + + if (!CheckInitNotifications(dbus)) { + return false; + } + + if (portal_interface_initialized) { + return RemovePortalNotification(dbus, notification); + } else if (core_interface_initialized) { + return RemoveCoreNotification(dbus, notification); + } + + return false; +} + +void SDL_CleanupNotifications() +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + + if (dbus && dbus->session_conn) { + DBusConnection *conn = dbus->session_conn; + + if (portal_interface_initialized) { + dbus->connection_remove_filter(conn, &PortalNotificationFilter, NULL); + dbus->bus_remove_match(conn, + "type='signal', interface='" NOTIFICATION_PORTAL_INTERFACE "'," + "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'", + NULL); + } + if (core_interface_initialized) { + dbus->connection_remove_filter(conn, &CoreNotificationFilter, NULL); + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_ACTION_SIGNAL_NAME "'", + NULL); + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_CLOSED_SIGNAL_NAME "'", + NULL); + dbus->bus_remove_match(dbus->session_conn, + "type='signal', interface='" NOTIFICATION_CORE_INTERFACE "'," + "member='" NOTIFICATION_ACTIVATION_TOKEN_SIGNAL_NAME "'", + NULL); + } + dbus->connection_flush(conn); + + portal_interface_initialized = false; + core_interface_initialized = false; + } + + SDL_free(icon_uri); + icon_uri = NULL; + + SDL_free(activation_token); + activation_token = NULL; +} + +bool SDL_RequestNotificationPermission(void) +{ + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + + // No API (yet) to ask permission; just make sure that notifications are available. + return CheckInitNotifications(dbus); +} + +#ifdef SDL_VIDEO_DRIVER_WAYLAND +const char *SDL_GetNotificationActivationToken() +{ + // Track the lifetime to avoid returning a stale token. + if (activation_token && SDL_GetTicksNS() - activation_token_time_ns < ACTIVATION_TOKEN_LIFETIME) { + activation_token_time_ns = 0; + return activation_token; + } + + return NULL; +} +#endif // SDL_VIDEO_DRIVER_WAYLAND diff --git a/src/video/wayland/SDL_waylandutil.c b/src/video/wayland/SDL_waylandutil.c index d6461cb071..0732e7501a 100644 --- a/src/video/wayland/SDL_waylandutil.c +++ b/src/video/wayland/SDL_waylandutil.c @@ -26,6 +26,8 @@ #include "SDL_waylandutil.h" #include "xdg-activation-v1-client-protocol.h" +#include "../../notification/SDL_notification_c.h" + #define WAYLAND_HANDLE_PREFIX "wayland:" typedef struct Wayland_ActivationParams @@ -66,6 +68,9 @@ bool Wayland_GetActivationTokenForExport(SDL_VideoDevice *_this, char **token, c } const char *xdg_activation_token = SDL_getenv("XDG_ACTIVATION_TOKEN"); + if (!xdg_activation_token) { + xdg_activation_token = SDL_GetNotificationActivationToken(); + } if (xdg_activation_token) { *token = SDL_strdup(xdg_activation_token); if (!*token) { diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index dda72ce311..1d38bee979 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -28,6 +28,7 @@ #include "../SDL_sysvideo.h" #include "../../events/SDL_events_c.h" #include "../../core/unix/SDL_appid.h" +#include "../../notification/SDL_notification_c.h" #include "../SDL_egl_c.h" #include "SDL_waylandevents_c.h" #include "SDL_waylandmouse.h" @@ -2564,6 +2565,29 @@ static void Wayland_activate_window(SDL_VideoData *data, SDL_WindowData *target_ void Wayland_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window) { + SDL_VideoData *viddata = _this->internal; + SDL_WindowData *wind = window->internal; + + if (viddata->activation_manager) { + /* Check for an activation token, in case the window is being + * raised in response to a system notification. + * + * Note that we don't check for empty strings, as that is still + * considered a valid activation token! + */ + const char *activation_token = SDL_GetNotificationActivationToken(); + if (activation_token) { + xdg_activation_v1_activate(viddata->activation_manager, + activation_token, + wind->surface); + + // Clear this variable, per the protocol's request. + SDL_unsetenv_unsafe("XDG_ACTIVATION_TOKEN"); + return; + } + } + + // No token? Try to activate the window via an event serial. Wayland_activate_window(_this->internal, window->internal, true); } diff --git a/test/testnotification.c b/test/testnotification.c index e1a75391a6..75fc8f5053 100644 --- a/test/testnotification.c +++ b/test/testnotification.c @@ -29,7 +29,6 @@ static SDL_NotificationAction actions[] = { { .button = { SDL_NOTIFICATION_ACTION_TYPE_BUTTON, "action_1", "OK" } }, { .button = { SDL_NOTIFICATION_ACTION_TYPE_BUTTON, "action_2", "Cancel" } } }; -static SDL_NotificationAction *action_array[SDL_arraysize(actions) + 1]; static bool transient; static int sound;