summaryrefslogtreecommitdiff
path: root/csrc/services/notification-service.c
diff options
context:
space:
mode:
Diffstat (limited to 'csrc/services/notification-service.c')
-rw-r--r--csrc/services/notification-service.c1833
1 files changed, 1833 insertions, 0 deletions
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 <json-glib/json-glib.h>
+#include <gtk/gtk.h>
+
+#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( &notifications ),
+ 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 ) );
+}