#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 ) ); }