From 1af3d14f3f995645a0de08a54b244c19840709e9 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Thu, 19 Jun 2025 23:17:58 +0200 Subject: init hs project --- csrc/services/notification-service.c | 1833 ++++++++++++++++++++++++++++++++++ 1 file changed, 1833 insertions(+) create mode 100644 csrc/services/notification-service.c (limited to 'csrc/services/notification-service.c') diff --git a/csrc/services/notification-service.c b/csrc/services/notification-service.c new file mode 100644 index 0000000..1a085f0 --- /dev/null +++ b/csrc/services/notification-service.c @@ -0,0 +1,1833 @@ +#include "services/notification-service.h" +#include "dbus/notifications.h" +#include "utils.h" +#include +#include + +#define DEFAULT_TIMEOUT 3000 + +// +// FoobarNotificationUrgency: +// +// The urgency of a notification as provided by the application that sent it. +// + +G_DEFINE_ENUM_TYPE( + FoobarNotificationUrgency, + foobar_notification_urgency, + G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT, "loss-of-comfort" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, "loss-of-money" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE, "loss-of-life" ) ) + +// +// FoobarNotificationAction: +// +// A labeled action for a notification that can be invoked by the notification daemon (us). +// + +struct _FoobarNotificationAction +{ + GObject parent_instance; + FoobarNotification* notification; + gchar* id; + gchar* label; +}; + +enum +{ + ACTION_PROP_ID = 1, + ACTION_PROP_LABEL, + N_ACTION_PROPS, +}; + +static GParamSpec* action_props[N_ACTION_PROPS] = { 0 }; + +static void foobar_notification_action_class_init ( FoobarNotificationActionClass* klass ); +static void foobar_notification_action_init ( FoobarNotificationAction* self ); +static void foobar_notification_action_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_action_finalize ( GObject* object ); +static FoobarNotificationAction* foobar_notification_action_new ( FoobarNotification* notification ); +static void foobar_notification_action_set_id ( FoobarNotificationAction* self, + gchar const* value ); +static void foobar_notification_action_set_label ( FoobarNotificationAction* self, + gchar const* value ); + +G_DEFINE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, G_TYPE_OBJECT ) + +// +// FoobarNotification: +// +// A notification received by the notification daemon (us). +// + +struct _FoobarNotification +{ + GObject parent_instance; + guint timeout_id; + FoobarNotificationService* service; + guint id; + GPtrArray* actions; + gchar* app_entry; + gchar* app_name; + gchar* body; + gchar* summary; + GdkPixbuf* image; + gchar* image_path; + gchar* image_data; + gboolean is_dismissed; + gboolean is_resident; + gboolean is_transient; + GDateTime* time; + gint64 timeout; + FoobarNotificationUrgency urgency; +}; + +enum +{ + NOTIFICATION_PROP_ID = 1, + NOTIFICATION_PROP_APP_ENTRY, + NOTIFICATION_PROP_APP_NAME, + NOTIFICATION_PROP_BODY, + NOTIFICATION_PROP_SUMMARY, + NOTIFICATION_PROP_IMAGE, + NOTIFICATION_PROP_IS_DISMISSED, + NOTIFICATION_PROP_IS_RESIDENT, + NOTIFICATION_PROP_IS_TRANSIENT, + NOTIFICATION_PROP_TIME, + NOTIFICATION_PROP_TIMEOUT, + NOTIFICATION_PROP_URGENCY, + N_NOTIFICATION_PROPS, +}; + +static GParamSpec* notification_props[N_NOTIFICATION_PROPS] = { 0 }; + +static void foobar_notification_class_init ( FoobarNotificationClass* klass ); +static void foobar_notification_init ( FoobarNotification* self ); +static void foobar_notification_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_finalize ( GObject* object ); +static FoobarNotification* foobar_notification_new ( FoobarNotificationService* service ); +static gchar const* foobar_notification_get_image_path ( FoobarNotification* self ); +static gchar const* foobar_notification_get_image_data ( FoobarNotification* self ); +static void foobar_notification_set_id ( FoobarNotification* self, + guint value ); +static void foobar_notification_set_app_entry ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_app_name ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_body ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_summary ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_image_from_path( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_image_from_data( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_image ( FoobarNotification* self, + GdkPixbuf* value ); +static void foobar_notification_set_dismissed ( FoobarNotification* self, + gboolean value ); +static void foobar_notification_set_resident ( FoobarNotification* self, + gboolean value ); +static void foobar_notification_set_transient ( FoobarNotification* self, + gboolean value ); +static void foobar_notification_set_time ( FoobarNotification* self, + GDateTime* value ); +static void foobar_notification_set_timeout ( FoobarNotification* self, + gint64 value ); +static void foobar_notification_set_urgency ( FoobarNotification* self, + FoobarNotificationUrgency value ); +static void foobar_notification_add_action ( FoobarNotification* self, + FoobarNotificationAction* action ); +static void foobar_notification_free_action ( gpointer action ); +static gboolean foobar_notification_handle_timeout ( gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNotification, foobar_notification, G_TYPE_OBJECT ) + +// +// FoobarNotificationService: +// +// Service acting as the notification daemon, receiving incoming notifications from other applications. The service also +// persistently saves previously received notifications until they are closed. +// + +struct _FoobarNotificationService +{ + GObject parent_instance; + GListStore* notifications; + GtkSortListModel* sorted_notifications; + GtkFilterListModel* popup_notifications; + FoobarNotifications* skeleton; + guint bus_owner_id; + guint next_id; + gchar* cache_path; + GMutex write_cache_mutex; +}; + +enum +{ + PROP_NOTIFICATIONS = 1, + PROP_POPUP_NOTIFICATIONS, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_notification_service_class_init ( FoobarNotificationServiceClass* klass ); +static void foobar_notification_service_init ( FoobarNotificationService* self ); +static void foobar_notification_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_service_finalize ( GObject* object ); +static void foobar_notification_service_read_cache ( FoobarNotificationService* self ); +static void foobar_notification_service_write_cache ( FoobarNotificationService* self ); +static void foobar_notification_service_write_cache_cb ( GObject* object, + GAsyncResult* result, + gpointer userdata ); +static void foobar_notification_service_write_cache_async ( FoobarNotificationService* self, + GPtrArray* notifications, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata ); +static gboolean foobar_notification_service_write_cache_finish ( FoobarNotificationService* self, + GAsyncResult* result, + GError** error ); +static void foobar_notification_service_write_cache_thread ( GTask* task, + gpointer source_object, + gpointer task_data, + GCancellable* cancellable ); +static void foobar_notification_service_handle_bus_acquired ( GDBusConnection* connection, + gchar const* name, + gpointer userdata ); +static void foobar_notification_service_handle_bus_lost ( GDBusConnection* connection, + gchar const* name, + gpointer userdata ); +static gboolean foobar_notification_service_handle_notify ( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gchar const* app_name, + guint replaces_id, + gchar const* image, + gchar const* summary, + gchar const* body, + gchar const* const* actions, + GVariant* hints, + gint expiration, + gpointer userdata ); +static gboolean foobar_notification_service_handle_close_notification ( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + guint id, + gpointer userdata ); +static gboolean foobar_notification_service_handle_get_capabilities ( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_notification_service_handle_get_server_information( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_notification_service_popup_filter_func ( gpointer item, + gpointer userdata ); +static gint foobar_notification_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Notification Action +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for notification actions. +// +void foobar_notification_action_class_init( FoobarNotificationActionClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_action_get_property; + object_klass->finalize = foobar_notification_action_finalize; + + action_props[ACTION_PROP_ID] = g_param_spec_string( + "id", + "ID", + "Textual ID of the action within the notification.", + NULL, + G_PARAM_READABLE ); + action_props[ACTION_PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "Human-readable title for the action.", + NULL, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_ACTION_PROPS, action_props ); +} + +// +// Instance initialization for notification actions. +// +void foobar_notification_action_init( FoobarNotificationAction* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_action_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNotificationAction* self = (FoobarNotificationAction*)object; + + switch ( prop_id ) + { + case ACTION_PROP_ID: + g_value_set_string( value, foobar_notification_action_get_id( self ) ); + break; + case ACTION_PROP_LABEL: + g_value_set_string( value, foobar_notification_action_get_label( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for notification actions. +// +void foobar_notification_action_finalize( GObject* object ) +{ + FoobarNotificationAction* self = (FoobarNotificationAction*)object; + + g_clear_pointer( &self->id, g_free ); + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_notification_action_parent_class )->finalize( object ); +} + +// +// Create a new notification action that is owned by the given notification (captured as an unowned reference). +// +FoobarNotificationAction* foobar_notification_action_new( FoobarNotification* notification ) +{ + FoobarNotificationAction* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_ACTION, NULL ); + self->notification = notification; + return self; +} + +// +// Get the textual ID of the action within the notification. +// +gchar const* foobar_notification_action_get_id( FoobarNotificationAction* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL ); + return self->id; +} + +// +// Get a human-readable title for the action. +// +gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL ); + return self->label; +} + +// +// Update the textual ID of the action within the notification. +// +void foobar_notification_action_set_id( + FoobarNotificationAction* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); + + if ( g_strcmp0( self->id, value ) ) + { + g_clear_pointer( &self->id, g_free ); + self->id = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_ID] ); + } +} + +// +// Set a human-readable title for the action. +// +void foobar_notification_action_set_label( + FoobarNotificationAction* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_LABEL] ); + } +} + +// +// Invoke this action by emitting a signal that can be handled by its sender application. +// +void foobar_notification_action_invoke( FoobarNotificationAction* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); + + if ( self->notification && self->notification->service ) + { + FoobarNotification* notification = self->notification; + FoobarNotificationService* service = notification->service; + + foobar_notifications_emit_action_invoked( + service->skeleton, + foobar_notification_get_id( notification ), + foobar_notification_action_get_id( self ) ); + + if ( !foobar_notification_is_resident( notification ) ) + { + foobar_notification_close( notification ); + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Notification +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for notifications. +// +void foobar_notification_class_init( FoobarNotificationClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_get_property; + object_klass->finalize = foobar_notification_finalize; + + notification_props[NOTIFICATION_PROP_ID] = g_param_spec_uint( + "id", + "ID", + "Numeric ID of the notification.", + 0, + UINT_MAX, + 0, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_APP_ENTRY] = g_param_spec_string( + "app-entry", + "App Entry", + "The desktop file of the notification's application.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_APP_NAME] = g_param_spec_string( + "app-name", + "App Name", + "Human-readable name of the application.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_BODY] = g_param_spec_string( + "body", + "Body", + "Main content of the notification.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_SUMMARY] = g_param_spec_string( + "summary", + "Summary", + "Brief summary of the notification's content.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IMAGE] = g_param_spec_object( + "image", + "Image", + "An image icon for the notification.", + GDK_TYPE_PIXBUF, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IS_DISMISSED] = g_param_spec_boolean( + "is-dismissed", + "Is Dismissed", + "Indicates that the notification was dismissed or the timeout has passed.", + FALSE, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IS_RESIDENT] = g_param_spec_boolean( + "is-resident", + "Is Resident", + "Indicates that the notification is not automatically removed once an action is invoked.", + FALSE, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IS_TRANSIENT] = g_param_spec_boolean( + "is-transient", + "Is Transient", + "Indicates that the persistence capability should be bypassed.", + FALSE, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_TIME] = g_param_spec_boxed( + "time", + "Time", + "The timestamp at which the notification was received.", + G_TYPE_DATE_TIME, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_TIMEOUT] = g_param_spec_int64( + "timeout", + "Timeout", + "Timeout after which the notification auto-closes in milliseconds.", + 0, + INT64_MAX, + 0, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_URGENCY] = g_param_spec_enum( + "urgency", + "Urgency", + "The urgency state of the notification.", + FOOBAR_TYPE_NOTIFICATION_URGENCY, + FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_NOTIFICATION_PROPS, notification_props ); +} + +// +// Instance initialization for notifications. +// +void foobar_notification_init( FoobarNotification* self ) +{ + self->actions = g_ptr_array_new_with_free_func( foobar_notification_free_action ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNotification* self = (FoobarNotification*)object; + + switch ( prop_id ) + { + case NOTIFICATION_PROP_ID: + g_value_set_int64( value, foobar_notification_get_id( self ) ); + break; + case NOTIFICATION_PROP_APP_ENTRY: + g_value_set_string( value, foobar_notification_get_app_entry( self ) ); + break; + case NOTIFICATION_PROP_APP_NAME: + g_value_set_string( value, foobar_notification_get_app_name( self ) ); + break; + case NOTIFICATION_PROP_BODY: + g_value_set_string( value, foobar_notification_get_body( self ) ); + break; + case NOTIFICATION_PROP_SUMMARY: + g_value_set_string( value, foobar_notification_get_summary( self ) ); + break; + case NOTIFICATION_PROP_IMAGE: + g_value_set_object( value, foobar_notification_get_image( self ) ); + break; + case NOTIFICATION_PROP_IS_DISMISSED: + g_value_set_boolean( value, foobar_notification_is_dismissed( self ) ); + break; + case NOTIFICATION_PROP_IS_RESIDENT: + g_value_set_boolean( value, foobar_notification_is_resident( self ) ); + break; + case NOTIFICATION_PROP_IS_TRANSIENT: + g_value_set_boolean( value, foobar_notification_is_transient( self ) ); + break; + case NOTIFICATION_PROP_TIME: + g_value_set_boxed( value, foobar_notification_get_time( self ) ); + break; + case NOTIFICATION_PROP_TIMEOUT: + g_value_set_int64( value, foobar_notification_get_timeout( self ) ); + break; + case NOTIFICATION_PROP_URGENCY: + g_value_set_enum( value, foobar_notification_get_urgency( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance initialization for notifications. +// +void foobar_notification_finalize( GObject* object ) +{ + FoobarNotification* self = (FoobarNotification*)object; + + g_clear_object( &self->image ); + g_clear_pointer( &self->actions, g_ptr_array_unref ); + g_clear_pointer( &self->app_entry, g_free ); + g_clear_pointer( &self->app_name, g_free ); + g_clear_pointer( &self->body, g_free ); + g_clear_pointer( &self->summary, g_free ); + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_pointer( &self->time, g_date_time_unref ); + + G_OBJECT_CLASS( foobar_notification_parent_class )->finalize( object ); +} + +// +// Create a new notification that is owned by the given service (captured as an unowned reference). +// +FoobarNotification* foobar_notification_new( FoobarNotificationService* service ) +{ + FoobarNotification* self = g_object_new( FOOBAR_TYPE_NOTIFICATION, NULL ); + self->service = service; + return self; +} + +// +// Numeric ID of the notification. +// +guint foobar_notification_get_id( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 ); + return self->id; +} + +// +// A list of actions which can be invoked by notifying the notification's sender. +// +FoobarNotificationAction** foobar_notification_get_actions( + FoobarNotification* self, + guint* out_count ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + if ( out_count ) { *out_count = self->actions->len; } + return (FoobarNotificationAction**)self->actions->pdata; +} + +// +// The desktop file of the notification's application. +// +gchar const* foobar_notification_get_app_entry( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->app_entry; +} + +// +// Human-readable name of the application. +// +gchar const* foobar_notification_get_app_name( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->app_name; +} + +// +// Main content of the notification. +// +gchar const* foobar_notification_get_body( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->body; +} + +// +// Brief summary of the notification's content. +// +gchar const* foobar_notification_get_summary( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->summary; +} + +// +// The source path for the notification's image if it was loaded from a file. +// +gchar const* foobar_notification_get_image_path( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->image_path; +} + +// +// The base64-encoded data for the notification's image if it was loaded from data. +// +gchar const* foobar_notification_get_image_data( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->image_data; +} + +// +// An image icon for the notification. +// +GdkPixbuf* foobar_notification_get_image( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->image; +} + +// +// Indicates that the notification was dismissed or the timeout has passed. +// +gboolean foobar_notification_is_dismissed( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); + return self->is_dismissed; +} + +// +// Indicates that the notification is not automatically removed once an action is invoked. +// +gboolean foobar_notification_is_resident( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); + return self->is_resident; +} + +// +// Indicates that the persistence capability should be bypassed. +// +gboolean foobar_notification_is_transient( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); + return self->is_transient; +} + +// +// The timestamp at which the notification was received. +// +GDateTime* foobar_notification_get_time( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->time; +} + +// +// Timeout after which the notification auto-closes in milliseconds. +// +gint64 foobar_notification_get_timeout( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 ); + return self->timeout; +} + +// +// The urgency state of the notification. +// +FoobarNotificationUrgency foobar_notification_get_urgency( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY ); + return self->urgency; +} + +// +// Numeric ID of the notification. +// +void foobar_notification_set_id( + FoobarNotification* self, + guint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->id != value ) + { + self->id = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_ID] ); + } +} + +// +// The desktop file of the notification's application. +// +void foobar_notification_set_app_entry( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->app_entry, value ) ) + { + g_clear_pointer( &self->app_entry, g_free ); + self->app_entry = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_ENTRY] ); + } +} + +// +// Human-readable name of the application. +// +void foobar_notification_set_app_name( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->app_name, value ) ) + { + g_clear_pointer( &self->app_name, g_free ); + self->app_name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_NAME] ); + } +} + +// +// Main content of the notification. +// +void foobar_notification_set_body( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->body, value ) ) + { + g_clear_pointer( &self->body, g_free ); + self->body = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_BODY] ); + } +} + +// +// Brief summary of the notification's content. +// +void foobar_notification_set_summary( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->summary, value ) ) + { + g_clear_pointer( &self->summary, g_free ); + self->summary = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_SUMMARY] ); + } +} + +// +// Update the notification's image by trying to load a file at the provided path. +// +void foobar_notification_set_image_from_path( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->image_path, value ) ) + { + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_object( &self->image ); + self->image_path = g_strdup( value ); + + g_autoptr( GError ) error = NULL; + self->image = gdk_pixbuf_new_from_file( self->image_path, &error ); + if ( !self->image ) + { + g_warning( "Invalid image path for notification: %s", error->message ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); + } +} + +// +// Update the notification's image by trying to load base64-encoded data. +// +void foobar_notification_set_image_from_data( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->image_data, value ) ) + { + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_object( &self->image ); + self->image_data = g_strdup( value ); + + gsize data_len; + guchar* data = g_base64_decode( self->image_data, &data_len ); + g_autoptr( GError ) error = NULL; + g_autoptr( GInputStream ) input = g_memory_input_stream_new_from_data( data, (gssize)data_len, g_free ); + self->image = gdk_pixbuf_new_from_stream( input, NULL, &error ); + if ( !self->image ) + { + g_warning( "Invalid image data for notification: %s", error->message ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); + } +} + +// +// Update the notification's image to a custom pixbuf. +// +static void foobar_notification_set_image( + FoobarNotification* self, + GdkPixbuf* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->image != value ) + { + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_object( &self->image ); + + if ( value ) + { + self->image = g_object_ref( value ); + g_autoptr( GError ) error = NULL; + gchar* data; + gsize data_len; + if ( gdk_pixbuf_save_to_buffer( self->image, &data, &data_len, "png", &error, NULL ) ) + { + self->image_data = g_base64_encode( (guchar const*)data, data_len ); + } + else + { + g_warning( "Unable to save image for notification: %s", error->message ); + g_clear_object( &self->image ); + } + } + + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); + } +} + +// +// Indicates that the notification was dismissed or the timeout has passed. +// +void foobar_notification_set_dismissed( + FoobarNotification* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = !!value; + if ( self->is_dismissed != value ) + { + self->is_dismissed = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_DISMISSED] ); + + if ( self->service ) + { + gtk_filter_changed( + gtk_filter_list_model_get_filter( self->service->popup_notifications ), + value ? GTK_FILTER_CHANGE_MORE_STRICT : GTK_FILTER_CHANGE_LESS_STRICT ); + } + } +} + +// +// Indicates that the notification is not automatically removed once an action is invoked. +// +void foobar_notification_set_resident( + FoobarNotification* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = !!value; + if ( self->is_resident != value ) + { + self->is_resident = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_RESIDENT] ); + } +} + +// +// Indicates that the persistence capability should be bypassed. +// +void foobar_notification_set_transient( + FoobarNotification* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = !!value; + if ( self->is_transient != value ) + { + self->is_transient = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_TRANSIENT] ); + } +} + +// +// The timestamp at which the notification was received. +// +void foobar_notification_set_time( + FoobarNotification* self, + GDateTime* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->time != value ) + { + if ( self->time ) { g_date_time_unref( self->time ); } + self->time = g_date_time_ref( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIME] ); + } +} + +// +// Timeout after which the notification auto-closes in milliseconds. +// +void foobar_notification_set_timeout( + FoobarNotification* self, + gint64 value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = MAX( value, 0 ); + if ( self->timeout != value ) + { + self->timeout = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIMEOUT] ); + } +} + +// +// The urgency state of the notification. +// +void foobar_notification_set_urgency( + FoobarNotification* self, + FoobarNotificationUrgency value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->urgency != value ) + { + self->urgency = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_URGENCY] ); + } +} + +// +// Register an action for the notification. +// +void foobar_notification_add_action( + FoobarNotification* self, + FoobarNotificationAction* action ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + g_return_if_fail( action->notification == self ); + + g_ptr_array_add( self->actions, g_object_ref( action ) ); +} + +// +// Block the notification from automatically being dismissed. +// +void foobar_notification_block_timeout( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + g_clear_handle_id( &self->timeout_id, g_source_remove ); +} + +// +// If not already started, start the timeout to automatically dismiss the notification after it's timeout. +// +void foobar_notification_resume_timeout( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( !self->is_dismissed && !self->timeout_id ) + { + self->timeout_id = g_timeout_add_full( + G_NOTIFICATION_PRIORITY_NORMAL, + self->timeout, + foobar_notification_handle_timeout, + g_object_ref( self ), + g_object_unref ); + } +} + +// +// Manually dismiss the notification, setting "is-dismissed" to TRUE. +// +void foobar_notification_dismiss( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + foobar_notification_set_dismissed( self, TRUE ); +} + +// +// Close the notification, removing it from the parent service's list. +// +void foobar_notification_close( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->service ) + { + FoobarNotificationService* service = self->service; + self->service = NULL; + + guint id = foobar_notification_get_id( self ); + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( service->notifications ) ); ++i ) + { + if ( g_list_model_get_item( G_LIST_MODEL( service->notifications ), i ) == self ) + { + g_list_store_remove( service->notifications, i ); + foobar_notification_service_write_cache( service ); + break; + } + } + + foobar_notifications_emit_notification_closed( service->skeleton, id, 3 ); + } +} + +// +// Called when an action is removed from a notification, resetting its weak reference to the notification. +// +void foobar_notification_free_action( gpointer action ) +{ + FoobarNotificationAction* object = (FoobarNotificationAction*)action; + object->notification = NULL; + g_object_unref( object ); +} + +// +// Called after the notification's timeout has elapsed. +// +gboolean foobar_notification_handle_timeout( gpointer userdata ) +{ + FoobarNotification* notification = (FoobarNotification*)userdata; + + foobar_notification_dismiss( notification ); + notification->timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the notification service. +// +void foobar_notification_service_class_init( FoobarNotificationServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_service_get_property; + object_klass->finalize = foobar_notification_service_finalize; + + props[PROP_NOTIFICATIONS] = g_param_spec_object( + "notifications", + "Notifications", + "Sorted list of all notifications.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + props[PROP_POPUP_NOTIFICATIONS] = g_param_spec_object( + "popup-notifications", + "Popup Notifications", + "Sorted list of all visible notifications (i.e. notifications that are not dismissed).", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the notification service. +// +void foobar_notification_service_init( FoobarNotificationService* self ) +{ + self->notifications = g_list_store_new( FOOBAR_TYPE_NOTIFICATION ); + self->cache_path = foobar_get_cache_path( "notifications.json" ); + g_mutex_init( &self->write_cache_mutex ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_notification_service_sort_func, NULL, NULL ); + self->sorted_notifications = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->notifications ) ), + GTK_SORTER( sorter ) ); + + GtkCustomFilter* popup_filter = gtk_custom_filter_new( foobar_notification_service_popup_filter_func, NULL, NULL ); + self->popup_notifications = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->sorted_notifications ) ), + GTK_FILTER( popup_filter ) ); + + foobar_notification_service_read_cache( self ); + + self->bus_owner_id = g_bus_own_name( + G_BUS_TYPE_SESSION, + "org.freedesktop.Notifications", + G_BUS_NAME_OWNER_FLAGS_NONE, + foobar_notification_service_handle_bus_acquired, + NULL, + foobar_notification_service_handle_bus_lost, + self, + NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)object; + + switch ( prop_id ) + { + case PROP_NOTIFICATIONS: + g_value_set_object( value, foobar_notification_service_get_notifications( self ) ); + break; + case PROP_POPUP_NOTIFICATIONS: + g_value_set_object( value, foobar_notification_service_get_popup_notifications( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the notification service. +// +void foobar_notification_service_finalize( GObject* object ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)object; + + if ( self->skeleton ) { g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->skeleton ) ); } + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i ) + { + FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); + notification->service = NULL; + } + + g_clear_object( &self->popup_notifications ); + g_clear_object( &self->sorted_notifications ); + g_clear_object( &self->notifications ); + g_clear_object( &self->skeleton ); + g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name ); + g_clear_pointer( &self->cache_path, g_free ); + + g_mutex_clear( &self->write_cache_mutex ); + + G_OBJECT_CLASS( foobar_notification_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new notification service instance. +// +FoobarNotificationService* foobar_notification_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_NOTIFICATION_SERVICE, NULL ); +} + +// +// Get a sorted list of all notifications. +// +GListModel* foobar_notification_service_get_notifications( FoobarNotificationService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->sorted_notifications ); +} + +// +// Get a sorted list of all visible notifications (i.e. notifications that are not dismissed). +// +GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->popup_notifications ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called after the process has asynchronously acquired the notification bus. +// +// This is where we can export notification daemon functionality using the corresponding DBus skeleton object. +// +void foobar_notification_service_handle_bus_acquired( + GDBusConnection* connection, + gchar const* name, + gpointer userdata ) +{ + (void)name; + FoobarNotificationService* self = (FoobarNotificationService*)userdata; + + self->skeleton = foobar_notifications_skeleton_new( ); + g_signal_connect( + self->skeleton, + "handle-notify", + G_CALLBACK( foobar_notification_service_handle_notify ), + self ); + g_signal_connect( + self->skeleton, + "handle-close-notification", + G_CALLBACK( foobar_notification_service_handle_close_notification ), + self ); + g_signal_connect( + self->skeleton, + "handle-get-capabilities", + G_CALLBACK( foobar_notification_service_handle_get_capabilities ), + self ); + g_signal_connect( + self->skeleton, + "handle-get-server-information", + G_CALLBACK( foobar_notification_service_handle_get_server_information ), + self ); + + g_autoptr( GError ) error = NULL; + if ( !g_dbus_interface_skeleton_export( + G_DBUS_INTERFACE_SKELETON( self->skeleton ), + connection, + "/org/freedesktop/Notifications", + &error ) ) + { + g_warning( "Unable to export notifications interface: %s", error->message ); + } +} + +// +// Called after the process has failed to acquire the notification bus. +// +void foobar_notification_service_handle_bus_lost( + GDBusConnection* connection, + gchar const* name, + gpointer userdata ) +{ + (void)connection; + (void)name; + (void)userdata; + + g_autofree gchar* other_name = NULL; + g_autoptr( FoobarNotifications ) proxy = foobar_notifications_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + NULL, + NULL ); + + if ( proxy ) + { + foobar_notifications_call_get_server_information_sync( + proxy, + &other_name, + NULL, + NULL, + NULL, + NULL, + NULL ); + } + + g_warning( + "Unable to register notification daemon because %s is already running.", + other_name ? other_name : "(unknown)" ); +} + +// +// DBus skeleton callback for the "Notify" method. +// +gboolean foobar_notification_service_handle_notify( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gchar const* app_name, + guint replaces_id, + gchar const* image, + gchar const* summary, + gchar const* body, + gchar const* const* actions, + GVariant* hints, + gint expiration, + gpointer userdata ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)userdata; + + g_autoptr( GDateTime ) time = g_date_time_new_now_local( ); + g_autoptr( FoobarNotification ) notification = foobar_notification_new( self ); + foobar_notification_set_id( notification, replaces_id ? replaces_id : self->next_id++ ); + foobar_notification_set_app_name( notification, app_name ); + foobar_notification_set_summary( notification, summary ); + foobar_notification_set_body( notification, body ); + foobar_notification_set_time( notification, time ); + foobar_notification_set_timeout( notification, expiration != -1 ? expiration : DEFAULT_TIMEOUT ); + if ( image && *image ) { foobar_notification_set_image_from_path( notification, image ); } + + if ( hints ) + { + // Process hints to set some more properties for the notification. + + gsize hint_count = g_variant_n_children( hints ); + for ( gsize i = 0; i < hint_count; ++i ) + { + g_autoptr( GVariant ) hint = g_variant_get_child_value( hints, i ); + g_autoptr( GVariant ) key = g_variant_get_child_value( hint, 0 ); + g_autoptr( GVariant ) value_wrapper = g_variant_get_child_value( hint, 1 ); + g_autoptr( GVariant ) value = g_variant_get_variant( value_wrapper ); + gchar const* key_str = g_variant_get_string( key, NULL ); + + if ( !g_strcmp0( key_str, "desktop-entry" ) ) + { + foobar_notification_set_app_entry( notification, g_variant_get_string( value, NULL ) ); + } + else if ( !g_strcmp0( key_str, "resident" ) ) + { + foobar_notification_set_resident( notification, g_variant_get_boolean( value ) ); + } + else if ( !g_strcmp0( key_str, "transient" ) ) + { + foobar_notification_set_transient( notification, g_variant_get_boolean( value ) ); + } + else if ( !g_strcmp0( key_str, "urgency" ) ) + { + foobar_notification_set_urgency( notification, g_variant_get_byte( value ) ); + } + + if ( !foobar_notification_get_image( notification ) ) + { + if ( !g_strcmp0( key_str, "image-path" ) || + !g_strcmp0( key_str, "image_path" ) ) + { + foobar_notification_set_image_from_path( notification, g_variant_get_string( value, NULL ) ); + } + else if ( !g_strcmp0( key_str, "image-data" ) || + !g_strcmp0( key_str, "image_data" ) || + !g_strcmp0( key_str, "icon_data" ) ) + { + gint32 width, height, row_stride, bits_per_sample, channels; + gboolean has_alpha; + g_variant_get( + value, + "(iiibiiay)", + &width, + &height, + &row_stride, + &has_alpha, + &bits_per_sample, + &channels, + NULL ); + + g_autoptr( GVariant ) data_variant = g_variant_get_child_value( value, 6 ); + gsize data_len; + guint8 const* data = g_variant_get_fixed_array( data_variant, &data_len, sizeof( guint8 ) ); + g_autoptr( GBytes ) bytes = g_bytes_new( data, data_len ); + + g_autoptr( GdkPixbuf ) pixbuf = gdk_pixbuf_new_from_bytes( + bytes, + GDK_COLORSPACE_RGB, + has_alpha, + bits_per_sample, + width, + height, + row_stride ); + foobar_notification_set_image( notification, pixbuf ); + } + } + } + } + + if ( actions ) + { + // Actions are stored in a flat array containing the label followed by the ID of the action. + + while ( *actions ) + { + gchar const* label = *actions++; + gchar const* id = *actions++; + if ( !id ) + { + g_warning( "Action missing id: %s", label ); + break; + } + + g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification ); + foobar_notification_action_set_id( action, id ); + foobar_notification_action_set_label( action, label ); + foobar_notification_add_action( notification, action ); + } + } + + // Add the notification to the list and start the timeout. + + g_list_store_append( self->notifications, notification ); + foobar_notification_resume_timeout( notification ); + foobar_notification_service_write_cache( self ); + + foobar_notifications_complete_notify( iface, invocation, foobar_notification_get_id( notification ) ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "CloseNotification" method. +// +gboolean foobar_notification_service_handle_close_notification( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + guint id, + gpointer userdata ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)userdata; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i ) + { + FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); + if ( foobar_notification_get_id( notification ) == id ) + { + foobar_notification_close( notification ); + break; + } + } + + foobar_notifications_complete_close_notification( iface, invocation ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "GetCapabilities" method. +// +gboolean foobar_notification_service_handle_get_capabilities( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + (void)userdata; + + gchar const* capabilities[] = + { + "action-icons", + "actions", + "body", + "body-hyperlinks", + "body-markup", + "icon-static", + "persistence", + NULL + }; + foobar_notifications_complete_get_capabilities( iface, invocation, capabilities ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "GetServerInformation" method. +// +gboolean foobar_notification_service_handle_get_server_information( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + (void)userdata; + + foobar_notifications_complete_get_server_information( + iface, + invocation, + "Foobar", + "Hannes Schulze", + "1.0.0", + "1.2" ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Synchronously read the cache file at self->cache_path, populating self->notifications and initializing self->next_id. +// +void foobar_notification_service_read_cache( FoobarNotificationService* self ) +{ + g_autoptr( GError ) error = NULL; + + if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) ) + { + g_autoptr( JsonParser ) parser = json_parser_new( ); + if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) ) + { + g_warning( "Unable to read cached notifications: %s", error->message ); + return; + } + + JsonNode* root_node = json_parser_get_root( parser ); + JsonArray* root_array = json_node_get_array( root_node ); + for ( guint i = 0; i < json_array_get_length( root_array ); ++i ) + { + JsonObject* notification_object = json_array_get_object_element( root_array, i ); + g_autoptr( FoobarNotification ) notification = foobar_notification_new( self ); + + foobar_notification_set_dismissed( notification, TRUE ); + + guint id = json_object_get_int_member( notification_object, "id" ); + foobar_notification_set_id( notification, id ); + + gchar const* app_entry = json_object_get_string_member_with_default( notification_object, "app-entry", NULL ); + foobar_notification_set_app_entry( notification, app_entry ); + + gchar const* app_name = json_object_get_string_member_with_default( notification_object, "app-name", NULL ); + foobar_notification_set_app_name( notification, app_name ); + + gchar const* body = json_object_get_string_member_with_default( notification_object, "body", NULL ); + foobar_notification_set_body( notification, body ); + + gchar const* summary = json_object_get_string_member_with_default( notification_object, "summary", NULL ); + foobar_notification_set_summary( notification, summary ); + + gchar const* image_path = json_object_get_string_member_with_default( notification_object, "image-path", NULL ); + gchar const* image_data = json_object_get_string_member_with_default( notification_object, "image-data", NULL ); + if ( image_path ) { foobar_notification_set_image_from_path( notification, image_path ); } + else if ( image_data ) { foobar_notification_set_image_from_data( notification, image_data ); } + + gboolean is_resident = json_object_get_boolean_member( notification_object, "is-resident" ); + foobar_notification_set_resident( notification, is_resident ); + + gchar const* time_str = json_object_get_string_member_with_default( notification_object, "time", NULL ); + GTimeZone* tz = g_time_zone_new_local( ); + GDateTime* time = time_str ? g_date_time_new_from_iso8601( time_str, tz ) : NULL; + foobar_notification_set_time( notification, time ); + g_time_zone_unref( tz ); + if ( time ) { g_date_time_unref( time ); } + + gint64 timeout = json_object_get_int_member( notification_object, "timeout" ); + foobar_notification_set_timeout( notification, timeout ); + + FoobarNotificationUrgency urgency = json_object_get_int_member( notification_object, "urgency" ); + foobar_notification_set_urgency( notification, urgency ); + + JsonArray* actions_array = json_object_get_array_member( notification_object, "actions" ); + for ( guint j = 0; j < json_array_get_length( actions_array ); ++j ) + { + JsonObject* action_object = json_array_get_object_element( actions_array, j ); + g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification ); + + gchar const* action_id = json_object_get_string_member_with_default( action_object, "id", NULL ); + foobar_notification_action_set_id( action, action_id ); + + gchar const* label = json_object_get_string_member_with_default( action_object, "label", NULL ); + foobar_notification_action_set_label( action, label ); + + foobar_notification_add_action( notification, action ); + } + + g_list_store_append( self->notifications, notification ); + self->next_id = MAX( self->next_id, id + 1 ); + } + } +} + +// +// Asynchronously start writing the cache file at self->cache_path. +// +void foobar_notification_service_write_cache( FoobarNotificationService* self ) +{ + guint count = g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); + g_autoptr( GPtrArray ) notifications = g_ptr_array_new_full( count, g_object_unref ); + for ( guint i = 0; i < count; ++i ) + { + FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); + if ( !foobar_notification_is_transient( notification ) ) + { + g_ptr_array_add( notifications, g_object_ref( notification ) ); + } + } + + foobar_notification_service_write_cache_async( + self, + g_steal_pointer( ¬ifications ), + NULL, + foobar_notification_service_write_cache_cb, + NULL ); +} + +// +// Callback invoked when the cache was written successfully or failed. +// +void foobar_notification_service_write_cache_cb( + GObject* object, + GAsyncResult* result, + gpointer userdata ) +{ + (void)userdata; + FoobarNotificationService* self = (FoobarNotificationService*)object; + + g_autoptr( GError ) error = NULL; + if ( !foobar_notification_service_write_cache_finish( self, result, &error ) ) + { + g_warning( "Unable to write cached notifications: %s", error->message ); + } +} + +// +// Asynchronously write the cache file at self->cache_path, using the provided snapshot of the list. +// +void foobar_notification_service_write_cache_async( + FoobarNotificationService* self, + GPtrArray* notifications, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata ) +{ + g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata ); + g_task_set_name( task, "write-notification-cache" ); + g_task_set_task_data( task, notifications, (GDestroyNotify)g_ptr_array_unref ); + g_task_run_in_thread( task, foobar_notification_service_write_cache_thread ); +} + +// +// Get the asynchronous result for writing the cache file, returning TRUE on success, or FALSE on error. +// +gboolean foobar_notification_service_write_cache_finish( + FoobarNotificationService* self, + GAsyncResult* result, + GError** error ) +{ + (void)self; + + return g_task_propagate_boolean( G_TASK( result ), error ); +} + +// +// Task implementation for foobar_notification_service_write_cache_async, invoked on a background thread. +// +void foobar_notification_service_write_cache_thread( + GTask* task, + gpointer source_object, + gpointer task_data, + GCancellable* cancellable ) +{ + (void)cancellable; + FoobarNotificationService* self = (FoobarNotificationService*)source_object; + GPtrArray* notifications = (GPtrArray*)task_data; + + if ( !self->cache_path ) + { + g_task_return_boolean( task, TRUE ); + return; + } + + g_autoptr( GError ) error = NULL; + + g_autoptr( JsonBuilder ) builder = json_builder_new( ); + json_builder_begin_array( builder ); + for ( guint i = 0; i < notifications->len; ++i ) + { + // Only init-only properties are read here so no extra synchronization is needed. + + FoobarNotification* notification = g_ptr_array_index( notifications, i ); + + json_builder_begin_object( builder ); + + json_builder_set_member_name( builder, "id" ); + json_builder_add_int_value( builder, foobar_notification_get_id( notification ) ); + + json_builder_set_member_name( builder, "app-entry" ); + json_builder_add_string_value( builder, foobar_notification_get_app_entry( notification ) ); + + json_builder_set_member_name( builder, "app-name" ); + json_builder_add_string_value( builder, foobar_notification_get_app_name( notification ) ); + + json_builder_set_member_name( builder, "body" ); + json_builder_add_string_value( builder, foobar_notification_get_body( notification ) ); + + json_builder_set_member_name( builder, "summary" ); + json_builder_add_string_value( builder, foobar_notification_get_summary( notification ) ); + + json_builder_set_member_name( builder, "image-path" ); + json_builder_add_string_value( builder, foobar_notification_get_image_path( notification ) ); + + json_builder_set_member_name( builder, "image-data" ); + json_builder_add_string_value( builder, foobar_notification_get_image_data( notification ) ); + + json_builder_set_member_name( builder, "is-resident" ); + json_builder_add_boolean_value( builder, foobar_notification_is_resident( notification ) ); + + GDateTime* time = foobar_notification_get_time( notification ); + gchar* time_str = time ? g_date_time_format_iso8601( time ) : NULL; + json_builder_set_member_name( builder, "time" ); + json_builder_add_string_value( builder, time_str ); + g_free( time_str ); + + json_builder_set_member_name( builder, "timeout" ); + json_builder_add_int_value( builder, foobar_notification_get_timeout( notification ) ); + + json_builder_set_member_name( builder, "urgency" ); + json_builder_add_int_value( builder, foobar_notification_get_urgency( notification ) ); + + guint action_count; + FoobarNotificationAction** actions = foobar_notification_get_actions( notification, &action_count ); + json_builder_set_member_name( builder, "actions" ); + json_builder_begin_array( builder ); + for ( guint j = 0; j < action_count; ++j ) + { + FoobarNotificationAction* action = actions[j]; + + json_builder_begin_object( builder ); + + json_builder_set_member_name( builder, "id" ); + json_builder_add_string_value( builder, foobar_notification_action_get_id( action ) ); + + json_builder_set_member_name( builder, "label" ); + json_builder_add_string_value( builder, foobar_notification_action_get_label( action ) ); + + json_builder_end_object( builder ); + } + json_builder_end_array( builder ); + + json_builder_end_object( builder ); + } + json_builder_end_array( builder ); + + g_autoptr( JsonNode ) root_node = json_builder_get_root( builder ); + g_autoptr( JsonGenerator ) generator = json_generator_new( ); + json_generator_set_root( generator, root_node ); + + g_mutex_lock( &self->write_cache_mutex ); + gboolean success = json_generator_to_file( generator, self->cache_path, &error ); + g_mutex_unlock( &self->write_cache_mutex ); + if ( !success ) + { + g_task_return_error( task, g_steal_pointer( &error ) ); + return; + } + + g_task_return_boolean( task, TRUE ); +} + +// +// Filtering callback for the list of pop-up notifications. +// +gboolean foobar_notification_service_popup_filter_func( + gpointer item, + gpointer userdata ) +{ + (void)userdata; + + FoobarNotification* notification = item; + return !foobar_notification_is_dismissed( notification ); +} + +// +// Sorting callback for the list of notifications. +// +// Notifications are sorted by timestamp in descending order. +// +gint foobar_notification_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarNotification* notification_a = (FoobarNotification*)item_a; + FoobarNotification* notification_b = (FoobarNotification*)item_b; + return -g_date_time_compare( + foobar_notification_get_time( notification_a ), + foobar_notification_get_time( notification_b ) ); +} -- cgit v1.3.1