diff --git a/gio/tests/gnotification-server.c b/gio/tests/gnotification-server.c index e82d9c166..e05479b74 100644 --- a/gio/tests/gnotification-server.c +++ b/gio/tests/gnotification-server.c @@ -22,6 +22,7 @@ #include "gnotification-server.h" #include +#include typedef GObjectClass GNotificationServerClass; @@ -33,10 +34,16 @@ struct _GNotificationServer guint name_owner_id; guint object_id; + gchar *backend_name; + guint backend_version; + guint is_running; /* app_ids -> hashtables of notification ids -> a{sv} */ GHashTable *applications; + + /* notification -> unix_fd_list */ + GHashTable *unix_fd_lists; }; G_DEFINE_TYPE (GNotificationServer, g_notification_server, G_TYPE_OBJECT) @@ -44,9 +51,27 @@ G_DEFINE_TYPE (GNotificationServer, g_notification_server, G_TYPE_OBJECT) enum { PROP_0, - PROP_IS_RUNNING + PROP_IS_RUNNING, + PROP_BACKEND_NAME, + PROP_BACKEND_VERSION }; +static const gchar * +get_bus_name (GNotificationServer *server) { + if (g_strcmp0 (server->backend_name, "portal") == 0) + return "org.freedesktop.portal.Desktop"; + + return "org.gtk.Notifications"; +} + +static const gchar * +get_object_path (GNotificationServer *server) { + if (g_strcmp0 (server->backend_name, "portal") == 0) + return "/org/freedesktop/portal/desktop"; + + return "/org/gtk/Notifications"; +} + static GDBusInterfaceInfo * org_gtk_Notifications_get_interface (void) { @@ -85,6 +110,52 @@ org_gtk_Notifications_get_interface (void) return iface_info; } +static GDBusInterfaceInfo * +org_freedesktop_portal_notification_get_interface (void) +{ + static GDBusInterfaceInfo *iface_info; + + if (iface_info == NULL) + { + GDBusNodeInfo *info; + GError *error = NULL; + + info = g_dbus_node_info_new_for_xml ( + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "", &error); + + if (info == NULL) + g_error ("%s", error->message); + + iface_info = g_dbus_node_info_lookup_interface (info, "org.freedesktop.portal.Notification"); + g_assert (iface_info); + + g_dbus_interface_info_ref (iface_info); + g_dbus_node_info_unref (info); + } + + return iface_info; +} + +static GDBusInterfaceInfo * +get_interface (GNotificationServer *server) +{ + if (g_strcmp0 (server->backend_name, "portal") == 0) + return org_freedesktop_portal_notification_get_interface (); + + return org_gtk_Notifications_get_interface (); +} + static void g_notification_server_notification_added (GNotificationServer *server, const gchar *app_id, @@ -116,6 +187,8 @@ g_notification_server_notification_removed (GNotificationServer *server, notifications = g_hash_table_lookup (server->applications, app_id); if (notifications) { + g_hash_table_remove (server->unix_fd_lists, + g_hash_table_lookup (notifications, notification_id)); g_hash_table_remove (notifications, notification_id); if (g_hash_table_size (notifications) == 0) g_hash_table_remove (server->applications, app_id); @@ -164,56 +237,74 @@ org_gtk_Notifications_method_call (GDBusConnection *connection, } static void -g_notification_server_dispose (GObject *object) +org_freedesktop_portal_notification_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) { - GNotificationServer *server = G_NOTIFICATION_SERVER (object); + GNotificationServer *server = user_data; - g_notification_server_stop (server); - - g_clear_pointer (&server->applications, g_hash_table_unref); - g_clear_object (&server->connection); - - G_OBJECT_CLASS (g_notification_server_parent_class)->dispose (object); -} - -static void -g_notification_server_get_property (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec) -{ - GNotificationServer *server = G_NOTIFICATION_SERVER (object); - - switch (property_id) + if (g_str_equal (method_name, "AddNotification")) { - case PROP_IS_RUNNING: - g_value_set_boolean (value, server->is_running); - break; + const gchar *notification_id; + g_autoptr(GVariant) notification = NULL; + GUnixFDList* fd_list; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + fd_list = g_dbus_message_get_unix_fd_list (g_dbus_method_invocation_get_message (invocation)); + + g_variant_get (parameters, "(&s@a{sv})", ¬ification_id, ¬ification); + + if (fd_list) + g_hash_table_replace (server->unix_fd_lists, g_variant_ref (notification), g_object_ref (fd_list)); + + g_notification_server_notification_added (server, "", notification_id, notification); + g_dbus_method_invocation_return_value (invocation, NULL); + } + else if (g_str_equal (method_name, "RemoveNotification")) + { + const gchar *notification_id; + + g_variant_get (parameters, "(&s)", ¬ification_id); + g_notification_server_notification_removed (server, "", notification_id); + g_dbus_method_invocation_return_value (invocation, NULL); + } + else + { + g_dbus_method_invocation_return_dbus_error (invocation, "UnknownMethod", "No such method"); } } - -static void -g_notification_server_class_init (GNotificationServerClass *class) +static GVariant * +org_freedesktop_portal_notification_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) { - GObjectClass *object_class = G_OBJECT_CLASS (class); + GNotificationServer *server = user_data; - object_class->get_property = g_notification_server_get_property; - object_class->dispose = g_notification_server_dispose; + if (g_strcmp0 (property_name, "version") == 0) + return g_variant_new_uint32 (server->backend_version); - g_object_class_install_property (object_class, PROP_IS_RUNNING, - g_param_spec_boolean ("is-running", "", "", FALSE, - G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + return NULL; +} - g_signal_new ("notification-received", G_TYPE_NOTIFICATION_SERVER, G_SIGNAL_RUN_FIRST, - 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 3, - G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT); - g_signal_new ("notification-removed", G_TYPE_NOTIFICATION_SERVER, G_SIGNAL_RUN_FIRST, - 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 2, - G_TYPE_STRING, G_TYPE_STRING); +static GDBusInterfaceVTable +get_vtable (GNotificationServer *server) +{ + const GDBusInterfaceVTable vtable = { + (g_strcmp0 (server->backend_name, "portal") == 0) ? org_freedesktop_portal_notification_method_call : org_gtk_Notifications_method_call, + (g_strcmp0 (server->backend_name, "portal") == 0) ? org_freedesktop_portal_notification_get_property : NULL, + NULL, { 0 } + }; + + return vtable; } static void @@ -221,13 +312,11 @@ g_notification_server_bus_acquired (GDBusConnection *connection, const gchar *name, gpointer user_data) { - const GDBusInterfaceVTable vtable = { - org_gtk_Notifications_method_call, NULL, NULL, { 0 } - }; GNotificationServer *server = user_data; + const GDBusInterfaceVTable vtable = get_vtable (server); - server->object_id = g_dbus_connection_register_object (connection, "/org/gtk/Notifications", - org_gtk_Notifications_get_interface (), + server->object_id = g_dbus_connection_register_object (connection, get_object_path (server), + get_interface (server), &vtable, server, NULL, NULL); /* register_object only fails if the same object is exported more than once */ @@ -261,13 +350,15 @@ g_notification_server_name_lost (GDBusConnection *connection, } static void -g_notification_server_init (GNotificationServer *server) +g_notification_server_constructed (GObject *object) { - server->applications = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, (GDestroyNotify) g_hash_table_unref); + + GNotificationServer *server = G_NOTIFICATION_SERVER (object); + + G_OBJECT_CLASS (g_notification_server_parent_class)->constructed (object); server->name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, - "org.gtk.Notifications", + get_bus_name (server), G_BUS_NAME_OWNER_FLAGS_NONE, g_notification_server_bus_acquired, g_notification_server_name_acquired, @@ -275,10 +366,119 @@ g_notification_server_init (GNotificationServer *server) server, NULL); } -GNotificationServer * -g_notification_server_new (void) +static void +g_notification_server_dispose (GObject *object) { - return g_object_new (G_TYPE_NOTIFICATION_SERVER, NULL); + GNotificationServer *server = G_NOTIFICATION_SERVER (object); + + g_notification_server_stop (server); + + g_clear_pointer (&server->applications, g_hash_table_unref); + g_clear_pointer (&server->unix_fd_lists, g_hash_table_unref); + g_clear_object (&server->connection); + g_clear_pointer (&server->backend_name, g_free); + + G_OBJECT_CLASS (g_notification_server_parent_class)->dispose (object); +} + +static void +g_notification_server_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GNotificationServer *server = G_NOTIFICATION_SERVER (object); + + switch (property_id) + { + case PROP_BACKEND_NAME: + server->backend_name = g_value_dup_string (value); + break; + case PROP_BACKEND_VERSION: + server->backend_version = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +g_notification_server_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GNotificationServer *server = G_NOTIFICATION_SERVER (object); + + switch (property_id) + { + case PROP_IS_RUNNING: + g_value_set_boolean (value, server->is_running); + break; + case PROP_BACKEND_NAME: + g_value_set_string (value, server->backend_name); + break; + case PROP_BACKEND_VERSION: + g_value_set_uint (value, server->backend_version); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +g_notification_server_class_init (GNotificationServerClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->set_property = g_notification_server_set_property; + object_class->get_property = g_notification_server_get_property; + object_class->constructed = g_notification_server_constructed; + object_class->dispose = g_notification_server_dispose; + + g_object_class_install_property (object_class, PROP_IS_RUNNING, + g_param_spec_boolean ("is-running", "", "", FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_BACKEND_NAME, + g_param_spec_string ("backend-name", "", "", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_BACKEND_VERSION, + g_param_spec_uint ("backend-version", "", "", 0, G_MAXUINT32, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_signal_new ("notification-received", G_TYPE_NOTIFICATION_SERVER, G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 3, + G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT); + + g_signal_new ("notification-removed", G_TYPE_NOTIFICATION_SERVER, G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 2, + G_TYPE_STRING, G_TYPE_STRING); +} + +static void +g_notification_server_init (GNotificationServer *server) +{ + server->applications = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_hash_table_unref); + + server->unix_fd_lists = g_hash_table_new_full (g_direct_hash, g_direct_equal, + (GDestroyNotify) g_variant_unref, + (GDestroyNotify) g_object_unref); + server->backend_version = 0; +} + +GNotificationServer * +g_notification_server_new (const gchar *backend_name, + guint backend_version) +{ + return g_object_new (G_TYPE_NOTIFICATION_SERVER, + "backend-name", backend_name, + "backend-version", backend_version, + NULL); } void @@ -321,6 +521,16 @@ g_notification_server_list_applications (GNotificationServer *server) return (gchar **) g_hash_table_get_keys_as_array (server->applications, NULL); } +GUnixFDList * +g_notification_server_get_unix_fd_list_for_notification (GNotificationServer *server, + GVariant *notification) +{ + g_return_val_if_fail (G_IS_NOTIFICATION_SERVER (server), NULL); + g_return_val_if_fail (notification != NULL, NULL); + + return g_hash_table_lookup (server->unix_fd_lists, notification); +} + gchar ** g_notification_server_list_notifications (GNotificationServer *server, const gchar *app_id) diff --git a/gio/tests/gnotification-server.h b/gio/tests/gnotification-server.h index 207cc8f7a..fb1c734cb 100644 --- a/gio/tests/gnotification-server.h +++ b/gio/tests/gnotification-server.h @@ -23,6 +23,7 @@ #define __G_NOTIFICATION_SERVER_H__ #include +#include #define G_TYPE_NOTIFICATION_SERVER (g_notification_server_get_type ()) #define G_NOTIFICATION_SERVER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_NOTIFICATION_SERVER, GNotificationServer)) @@ -32,12 +33,15 @@ typedef struct _GNotificationServer GNotificationServer; GType g_notification_server_get_type (void); -GNotificationServer * g_notification_server_new (void); +GNotificationServer * g_notification_server_new (const gchar *backend_name, + guint backend_version); void g_notification_server_stop (GNotificationServer *server); gboolean g_notification_server_get_is_running (GNotificationServer *server); +GUnixFDList * g_notification_server_get_unix_fd_list_for_notification (GNotificationServer *server, + GVariant *notification); gchar ** g_notification_server_list_applications (GNotificationServer *server); gchar ** g_notification_server_list_notifications (GNotificationServer *server, diff --git a/gio/tests/gnotification.c b/gio/tests/gnotification.c index dbc63e523..c74ed0ce1 100644 --- a/gio/tests/gnotification.c +++ b/gio/tests/gnotification.c @@ -148,7 +148,7 @@ basic (void) loop = g_main_loop_new (NULL, FALSE); - server = g_notification_server_new (); + server = g_notification_server_new ("gtk", 1); g_signal_connect (server, "notification-received", G_CALLBACK (notification_received), &received_count); g_signal_connect (server, "notification-removed", G_CALLBACK (notification_removed), &removed_count); g_signal_connect (server, "notify::is-running", G_CALLBACK (server_notify_is_running), loop); diff --git a/gio/tests/meson.build b/gio/tests/meson.build index ba04916c3..e4d81a692 100644 --- a/gio/tests/meson.build +++ b/gio/tests/meson.build @@ -528,6 +528,9 @@ if host_machine.system() != 'windows' 'gnotification' : { 'extra_sources' : [extra_sources, 'gnotification-server.c'], }, + 'portal-notification-backend' : { + 'extra_sources' : [extra_sources, 'gnotification-server.c'], + }, 'gdbus-test-codegen-old' : { 'source' : 'gdbus-test-codegen.c', 'extra_sources' : [extra_sources, gdbus_test_codegen_generated, gdbus_test_codegen_generated_interface_info], diff --git a/gio/tests/portal-notification-backend.c b/gio/tests/portal-notification-backend.c new file mode 100644 index 000000000..7eaa38ee0 --- /dev/null +++ b/gio/tests/portal-notification-backend.c @@ -0,0 +1,598 @@ +/* + * Copyright © 2013 Lars Uebernickel + * Copyright © 2024 GNOME Foundation Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + * + * Authors: Lars Uebernickel + * Julian Sparber + */ + +#include +#include + +#include "gnotification-server.h" +#include "gdbus-sessionbus.h" + +#define TEST_DATA "some test data" + +struct _GNotification +{ + GObject parent; + + gchar *title; + gchar *body; + gchar *markup_body; + GIcon *icon; + GNotificationSound *sound; + GNotificationPriority priority; + gchar *category; + GNotificationDisplayHintFlags display_hint; + GPtrArray *buttons; + gchar *default_action; + GVariant *default_action_target; +}; + +typedef enum +{ + SOUND_TYPE_DEFAULT, + SOUND_TYPE_FILE, + SOUND_TYPE_BYTES, + SOUND_TYPE_CUSTOM, +} SoundType; + +struct _GNotificationSound +{ + GObject parent; + + SoundType sound_type; + union { + GFile *file; + GBytes *bytes; + struct { + gchar *action; + GVariant *target; + } custom; + }; +}; + +typedef struct +{ + gchar *label; + gchar *purpose; + gchar *action_name; + GVariant *target; +} Button; + +typedef struct +{ + gchar *id; + GNotification *notification; + GMainLoop *loop; +} TestData; + +static void +test_data_free (gpointer pointer) +{ + TestData *data = pointer; + + g_clear_pointer (&data->id, g_free); + g_clear_object (&data->notification); + g_main_loop_unref (data->loop); +} + +static void +send_and_wait (TestData *data, + GApplication *application, + const gchar *id, + GNotification *notification) +{ + g_clear_pointer (&data->id, g_free); + data->id = g_strdup (id); + g_set_object (&data->notification, notification); + + g_application_send_notification (application, id, notification); + + while (g_main_context_iteration (g_main_loop_get_context (data->loop), TRUE)) + { + if (data->id == NULL && data->notification == NULL) + { + break; + } + } +} + +static void +send_and_wait_finish (TestData *data) +{ + g_clear_pointer (&data->id, g_free); + g_clear_object (&data->notification); + + g_main_context_wakeup (g_main_loop_get_context (data->loop)); +} + +static GFile * +get_test_file (void) { + GFile *file = NULL; + g_autoptr(GFileIOStream) iostream = NULL; + GOutputStream *stream = NULL; + + file = g_file_new_tmp ("notification-testXXXXXX", &iostream, NULL); + stream = g_io_stream_get_output_stream (G_IO_STREAM (iostream)); + g_output_stream_write_all (stream, TEST_DATA, strlen (TEST_DATA), NULL, NULL, NULL); + g_output_stream_close (stream, NULL, NULL); + + return file; +} + +static void +activate_app (GApplication *application, + gpointer user_data) +{ + TestData *data = user_data; + g_autoptr(GNotification) notification = NULL; + g_autoptr(GIcon) icon = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GNotificationSound) sound = NULL; + g_autoptr(GFile) file = NULL; + + bytes = g_bytes_new_static (TEST_DATA, strlen (TEST_DATA)); + file = get_test_file (); + + notification = g_notification_new ("Test"); + send_and_wait (data, application, "test1", notification); + + notification = g_notification_new ("Test2"); + send_and_wait (data, application, "test2", notification); + + g_application_withdraw_notification (application, "test1"); + + notification = g_notification_new ("Test3"); + send_and_wait (data, application, "test3", notification); + + notification = g_notification_new ("Test4"); + icon = g_themed_icon_new ("i-c-o-n"); + g_notification_set_icon (notification, icon); + g_clear_object (&icon); + g_notification_set_body (notification, "body"); + g_notification_set_body_with_markup (notification, "markup-body"); + g_notification_set_priority (notification, G_NOTIFICATION_PRIORITY_URGENT); + g_notification_set_default_action_and_target (notification, "app.action", "i", 42); + g_notification_add_button_with_purpose_and_target_value (notification, + "label", + "x-gnome.purpose", + "app.action2", + g_variant_new_string ("bla")); + g_notification_set_category (notification, "x-gnome.category"); + g_notification_set_display_hint_flags (notification, G_NOTIFICATION_DISPLAY_HINT_TRANSIENT); + send_and_wait (data, application, "test4", notification); + + notification = g_notification_new ("Test5"); + icon = g_file_icon_new (file); + g_notification_set_icon (notification, icon); + g_clear_object (&icon); + send_and_wait (data, application, "test5", notification); + + notification = g_notification_new ("Test6"); + icon = g_bytes_icon_new (bytes); + g_notification_set_icon (notification, icon); + g_clear_object (&icon); + send_and_wait (data, application, "test6", notification); + + notification = g_notification_new ("Test7"); + sound = g_notification_sound_new_default (); + g_notification_set_sound (notification, sound); + g_clear_object (&sound); + send_and_wait (data, application, "test7", notification); + + notification = g_notification_new ("Test8"); + sound = g_notification_sound_new_from_file (file); + g_notification_set_sound (notification, sound); + g_clear_object (&sound); + send_and_wait (data, application, "test8", notification); + + notification = g_notification_new ("Test9"); + sound = g_notification_sound_new_from_bytes (bytes); + g_notification_set_sound (notification, sound); + g_clear_object (&sound); + send_and_wait (data, application, "test9", notification); + + notification = g_notification_new ("Test10"); + sound = g_notification_sound_new_custom ("app.play-custom-sound", g_variant_new_string ("some target")); + g_notification_set_sound (notification, sound); + g_clear_object (&sound); + send_and_wait (data, application, "test10", notification); + + send_and_wait (data, application, NULL, notification); + + g_dbus_connection_flush_sync (g_application_get_dbus_connection (application), NULL, NULL); + + g_assert_true (g_file_delete (file, NULL, NULL)); + g_main_loop_quit (data->loop); +} + +static void +notification_received (GNotificationServer *server, + const gchar *app_id, + const gchar *notification_id, + GVariant *notification, + gpointer user_data) +{ + TestData *exp_data = user_data; + struct _GNotification *exp_notification; + + g_assert_nonnull (exp_data); + exp_notification = (struct _GNotification *)exp_data->notification; + g_assert_nonnull (exp_notification); + + if (exp_data->id) + g_assert_cmpstr (exp_data->id, ==, notification_id); + else + g_assert_true (g_dbus_is_guid (notification_id)); + + if (exp_notification->title) + { + const gchar *title; + g_assert_true (g_variant_lookup (notification, "title", "&s", &title)); + g_assert_cmpstr (title, ==, exp_notification->title); + } + + if (exp_notification->body && !exp_notification->markup_body) + { + const gchar *body; + g_assert_true (g_variant_lookup (notification, "body", "&s", &body)); + g_assert_cmpstr (body, ==, exp_notification->body); + } + + if (exp_notification->markup_body) + { + const gchar *body; + g_assert_true (g_variant_lookup (notification, "markup-body", "&s", &body)); + g_assert_cmpstr (body, ==, exp_notification->markup_body); + } + + if (exp_notification->icon) + { + g_autoptr(GVariant) serialized_icon = NULL; + + serialized_icon = g_variant_lookup_value (notification, "icon", NULL); + if (G_IS_THEMED_ICON (exp_notification->icon)) + { + g_autoptr(GIcon) icon = NULL; + + icon = g_icon_deserialize (serialized_icon); + g_assert_true (g_icon_equal (exp_notification->icon, icon)); + } + else + { + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GBytes) exp_bytes = NULL; + GUnixFDList *fd_list; + int fd; + int fd_id; + gchar *key; + g_autoptr(GVariant) handle = NULL; + g_autoptr(GMappedFile) mapped = NULL; + + g_assert_true (g_variant_is_of_type (serialized_icon, G_VARIANT_TYPE("(sv)"))); + g_variant_get (serialized_icon, "(&sv)", &key, &handle); + g_assert_cmpstr (key, ==, "file-descriptor"); + + fd_list = g_notification_server_get_unix_fd_list_for_notification (server, notification); + g_assert_nonnull (fd_list); + fd_id = g_variant_get_handle (handle); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >, -1); + + mapped = g_mapped_file_new_from_fd (fd, FALSE, &error); + g_assert_no_error (error); + bytes = g_mapped_file_get_bytes (mapped); + + if (G_IS_BYTES_ICON (exp_notification->icon)) + { + exp_bytes = g_bytes_ref (g_bytes_icon_get_bytes (G_BYTES_ICON (exp_notification->icon))); + } + else if (G_IS_FILE_ICON (exp_notification->icon)) + { + GFile *file; + + file = g_file_icon_get_file (G_FILE_ICON (exp_notification->icon)); + exp_bytes = g_file_load_bytes (file, NULL, NULL, &error); + g_assert_no_error (error); + } + g_assert_true (g_bytes_equal (exp_bytes, bytes)); + } + } + + if (exp_notification->sound) + { + struct _GNotificationSound *exp_sound = (struct _GNotificationSound *)exp_notification->sound; + g_autoptr(GVariant) serialized_sound = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) bytes = NULL; + + serialized_sound = g_variant_lookup_value (notification, "sound", NULL); + if (exp_sound->sound_type == SOUND_TYPE_FILE || exp_sound->sound_type == SOUND_TYPE_BYTES) + { + GUnixFDList *fd_list; + int fd; + int fd_id; + gchar *key; + g_autoptr(GVariant) handle = NULL; + g_autoptr(GMappedFile) mapped = NULL; + + g_assert_true (g_variant_is_of_type (serialized_sound, G_VARIANT_TYPE("(sv)"))); + g_variant_get (serialized_sound, "(&sv)", &key, &handle); + g_assert_cmpstr (key, ==, "file-descriptor"); + + fd_list = g_notification_server_get_unix_fd_list_for_notification (server, notification); + g_assert_nonnull (fd_list); + fd_id = g_variant_get_handle (handle); + fd = g_unix_fd_list_get (fd_list, fd_id, &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >, -1); + + mapped = g_mapped_file_new_from_fd (fd, FALSE, &error); + g_assert_no_error (error); + bytes = g_mapped_file_get_bytes (mapped); + } + else if (exp_sound->sound_type == SOUND_TYPE_DEFAULT) + { + const char *key; + + g_assert_true (g_variant_is_of_type (serialized_sound, G_VARIANT_TYPE("s"))); + key = g_variant_get_string (serialized_sound, NULL); + g_assert_cmpstr (key, ==, "default"); + } + else if (exp_sound->sound_type == SOUND_TYPE_CUSTOM) + { + /* The portal uses a button with a specific purpose for custom sound action */ + } + else + { + g_assert_not_reached (); + } + + if (exp_sound->sound_type == SOUND_TYPE_FILE) + { + g_autoptr(GBytes) exp_bytes = NULL; + exp_bytes = g_file_load_bytes (exp_sound->file, NULL, NULL, &error); + g_assert_no_error (error); + g_assert_true (g_bytes_equal (exp_bytes, bytes)); + } + else if (exp_sound->sound_type == SOUND_TYPE_BYTES) + { + g_assert_true (g_bytes_equal (exp_sound->bytes, bytes)); + } + } + else + { + const char *key; + g_autoptr(GVariant) serialized_sound = NULL; + + serialized_sound = g_variant_lookup_value (notification, "sound", NULL); + if (serialized_sound) + { + g_assert_true (g_variant_is_of_type (serialized_sound, G_VARIANT_TYPE("s"))); + key = g_variant_get_string (serialized_sound, NULL); + + g_assert_cmpstr (key, ==, "silent"); + } + } + + if (exp_notification->priority) + { + g_autoptr(GEnumClass) enum_class = NULL; + GEnumValue *enum_value; + const gchar *priority = NULL; + g_assert_true (g_variant_lookup (notification, "priority", "&s", &priority)); + + enum_class = g_type_class_ref (G_TYPE_NOTIFICATION_PRIORITY); + g_assert_nonnull (enum_class); + enum_value = g_enum_get_value_by_nick (enum_class, priority); + g_assert_nonnull (enum_value); + + g_assert_true ((GNotificationPriority) enum_value->value == exp_notification->priority); + } + + if (exp_notification->display_hint) + { + g_autoptr(GFlagsClass) flags_class = NULL; + GNotificationDisplayHintFlags display_hint = G_NOTIFICATION_DISPLAY_HINT_UPDATE; + const gchar** flags = NULL; + gsize i; + g_assert_true (g_variant_lookup (notification, "display-hint", "^a&s", &flags)); + + flags_class = g_type_class_ref (G_TYPE_NOTIFICATION_DISPLAY_HINT_FLAGS); + g_assert_nonnull (flags_class); + + for (i = 0; flags[i]; i++) + { + GFlagsValue *flags_value; + + if (g_strcmp0 (flags[i], "show-as-new") == 0) + { + display_hint &= ~G_NOTIFICATION_DISPLAY_HINT_UPDATE; + continue; + } + + flags_value = g_flags_get_value_by_nick (flags_class, flags[i]); + g_assert_nonnull (flags_value); + display_hint |= flags_value->value; + } + + g_assert_true (display_hint == exp_notification->display_hint); + } + + if (exp_notification->category) + { + const gchar *category; + g_assert_true (g_variant_lookup (notification, "category", "&s", &category)); + g_assert_cmpstr (category, ==, exp_notification->category); + } + + if (exp_notification->default_action) + { + const gchar *default_action; + g_assert_true (g_variant_lookup (notification, "default-action", "&s", &default_action)); + g_assert_cmpstr (default_action, ==, exp_notification->default_action); + } + + if (exp_notification->default_action_target) + { + g_autoptr(GVariant) default_action_target = NULL; + default_action_target = g_variant_lookup_value (notification, "default-action-target", NULL); + g_assert_true (g_variant_equal (default_action_target, exp_notification->default_action_target)); + } + + // Custom sound is a special system button for the portal + if ((exp_notification->buttons && exp_notification->buttons->len > 0) || + (exp_notification->sound && exp_notification->sound->sound_type == SOUND_TYPE_CUSTOM)) + { + gsize i; + g_autoptr(GVariant) buttons = NULL; + buttons = g_variant_lookup_value (notification, "buttons", G_VARIANT_TYPE("aa{sv}")); + g_assert_nonnull (buttons); + + for (i = 0; i < g_variant_n_children (buttons); i++) + { + Button *exp_button; + g_autoptr(GVariant) button = NULL; + const gchar *label = NULL; + const gchar *purpose = NULL; + const gchar *action_name = NULL; + g_autoptr(GVariant) action_target = NULL; + + button = g_variant_get_child_value (buttons, i); + g_assert_nonnull (button); + + if (g_variant_lookup (button, "purpose", "&s", &purpose) && + g_strcmp0 (purpose, "system.custom-alert") == 0) + { + g_assert_nonnull (exp_notification->sound); + g_assert_false (g_variant_lookup (button, "label", "&s", &label)); + + g_assert_true (g_variant_lookup (button, "action", "&s", &action_name)); + g_assert_cmpstr (action_name, ==, exp_notification->sound->custom.action); + + action_target = g_variant_lookup_value (button, "target", NULL); + g_assert_true (g_variant_equal (action_target, exp_notification->sound->custom.target)); + + continue; + } + exp_button = (Button*)g_ptr_array_index (exp_notification->buttons, i); + g_assert_nonnull (exp_button); + + if (exp_button->label) + { + g_assert_true (g_variant_lookup (button, "label", "&s", &label)); + g_assert_cmpstr (label, ==, exp_button->label); + } + + if (exp_button->purpose) + { + g_assert_true (g_variant_lookup (button, "purpose", "&s", &purpose)); + g_assert_cmpstr (purpose, ==, exp_button->purpose); + } + + if (exp_button->action_name) + { + g_assert_true (g_variant_lookup (button, "action", "&s", &action_name)); + g_assert_cmpstr (action_name, ==, exp_button->action_name); + } + + action_target = g_variant_lookup_value (button, "target", NULL); + g_assert_true (g_variant_equal (action_target, exp_button->target)); + } + } + + send_and_wait_finish (exp_data); +} + +static void +notification_removed (GNotificationServer *server, + const gchar *app_id, + const gchar *notification_id, + gpointer user_data) +{ + gint *count = user_data; + + g_assert_cmpstr (notification_id, ==, "test1"); + + (*count)++; +} + +static void +server_notify_is_running (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GNotificationServer *server = G_NOTIFICATION_SERVER (object); + GApplication *app; + + g_assert_true (g_notification_server_get_is_running (server)); + + app = g_application_new ("org.gtk.TestApplication", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect (app, "activate", G_CALLBACK (activate_app), user_data); + + g_application_run (app, 0, NULL); + + g_object_unref (app); +} + +static void +basic (void) +{ + TestData *data; + GNotificationServer *server; + GMainLoop *loop; + gint removed_count = 0; + + session_bus_up (); + + loop = g_main_loop_new (NULL, FALSE); + + data = g_new0 (TestData, 1); + data->loop = g_main_loop_ref (loop); + + server = g_notification_server_new ("portal", 2); + g_signal_connect (server, "notification-received", G_CALLBACK (notification_received), data); + g_signal_connect (server, "notification-removed", G_CALLBACK (notification_removed), &removed_count); + g_signal_connect (server, "notify::is-running", G_CALLBACK (server_notify_is_running), data); + + g_main_loop_run (loop); + + test_data_free (data); + + g_assert_cmpint (removed_count, ==, 1); + + g_object_unref (server); + g_main_loop_unref (loop); + session_bus_down (); +} + +int main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_setenv ("GIO_USE_PORTALS", "1", TRUE); + + g_test_add_func ("/portal-notification-backend/basic", basic); + + return g_test_run (); +}