|
@@ -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, ¶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
|