Browse Source

Add D-Bus notification driver

Use the core and portal notification implementations to dispatch system notifications.
Frank Praznik 2 tháng trước cách đây
mục cha
commit
8275f6e90d

+ 1581 - 0
src/notification/unix/SDL_dbusnotification.c

@@ -0,0 +1,1581 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  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 <SDL3/SDL_iostream.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifdef HAVE_MEMFD_CREATE
+#include <sys/mman.h>
+#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, &param_iter);
+
+            // The order of parameters in the array is defined: first the target, then the activation ID.
+            if (dbus->message_iter_get_arg_type(&param_iter) == DBUS_TYPE_VARIANT) {
+                DBusMessageIter target_variant, target_string;
+
+                dbus->message_iter_recurse(&param_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(&param_iter);
+            }
+
+            // System properties array.
+            if (dbus->message_iter_get_arg_type(&param_iter) == DBUS_TYPE_ARRAY) {
+                DBusMessageIter pdata_iter;
+
+                dbus->message_iter_recurse(&param_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(&param_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

+ 5 - 0
src/video/wayland/SDL_waylandutil.c

@@ -26,6 +26,8 @@
 #include "SDL_waylandutil.h"
 #include "SDL_waylandutil.h"
 #include "xdg-activation-v1-client-protocol.h"
 #include "xdg-activation-v1-client-protocol.h"
 
 
+#include "../../notification/SDL_notification_c.h"
+
 #define WAYLAND_HANDLE_PREFIX "wayland:"
 #define WAYLAND_HANDLE_PREFIX "wayland:"
 
 
 typedef struct Wayland_ActivationParams
 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");
     const char *xdg_activation_token = SDL_getenv("XDG_ACTIVATION_TOKEN");
+    if (!xdg_activation_token) {
+        xdg_activation_token = SDL_GetNotificationActivationToken();
+    }
     if (xdg_activation_token) {
     if (xdg_activation_token) {
         *token = SDL_strdup(xdg_activation_token);
         *token = SDL_strdup(xdg_activation_token);
         if (!*token) {
         if (!*token) {

+ 24 - 0
src/video/wayland/SDL_waylandwindow.c

@@ -28,6 +28,7 @@
 #include "../SDL_sysvideo.h"
 #include "../SDL_sysvideo.h"
 #include "../../events/SDL_events_c.h"
 #include "../../events/SDL_events_c.h"
 #include "../../core/unix/SDL_appid.h"
 #include "../../core/unix/SDL_appid.h"
+#include "../../notification/SDL_notification_c.h"
 #include "../SDL_egl_c.h"
 #include "../SDL_egl_c.h"
 #include "SDL_waylandevents_c.h"
 #include "SDL_waylandevents_c.h"
 #include "SDL_waylandmouse.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)
 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);
     Wayland_activate_window(_this->internal, window->internal, true);
 }
 }
 
 

+ 0 - 1
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_1", "OK" } },
     { .button = { SDL_NOTIFICATION_ACTION_TYPE_BUTTON, "action_2", "Cancel" } }
     { .button = { SDL_NOTIFICATION_ACTION_TYPE_BUTTON, "action_2", "Cancel" } }
 };
 };
-static SDL_NotificationAction *action_array[SDL_arraysize(actions) + 1];
 
 
 static bool transient;
 static bool transient;
 static int sound;
 static int sound;