diff --git a/README.md b/README.md
index f9ebf9e..219d209 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,9 @@ daemon, as defined in the [org.freedesktop.Notifications][fdo-spec] Desktop
Specification. These notifications can be used to inform the user about an event
or display some form of information without getting in the user's way.
+It is also a simple wrapper to send cross-desktop Notifications for sandboxed
+applications using the [XDG Portal Notification API][portal].
+
## Notice
For GLib based applications the [GNotification][gnotif] API should be used
@@ -14,3 +17,4 @@ instead.
[fdo-spec]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html
[gnotif]: https://docs.gtk.org/gio/class.Notification.html
+[portal]: https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.Notification.xml
diff --git a/docs/reference/libnotify-sections.txt b/docs/reference/libnotify-sections.txt
index f78a33a..f7229c6 100644
--- a/docs/reference/libnotify-sections.txt
+++ b/docs/reference/libnotify-sections.txt
@@ -4,6 +4,7 @@ NOTIFY_EXPIRES_DEFAULT
NOTIFY_EXPIRES_NEVER
NotifyNotification
NotifyNotification
+NotifyClosedReason
NotifyUrgency
NotifyActionCallback
NOTIFY_ACTION_CALLBACK
diff --git a/libnotify/internal.h b/libnotify/internal.h
index 741d002..4d7ceb0 100644
--- a/libnotify/internal.h
+++ b/libnotify/internal.h
@@ -26,6 +26,10 @@
#define NOTIFY_DBUS_CORE_INTERFACE "org.freedesktop.Notifications"
#define NOTIFY_DBUS_CORE_OBJECT "/org/freedesktop/Notifications"
+#define NOTIFY_PORTAL_DBUS_NAME "org.freedesktop.portal.Desktop"
+#define NOTIFY_PORTAL_DBUS_CORE_INTERFACE "org.freedesktop.portal.Notification"
+#define NOTIFY_PORTAL_DBUS_CORE_OBJECT "/org/freedesktop/portal/desktop"
+
G_BEGIN_DECLS
GDBusProxy * _notify_get_proxy (GError **error);
@@ -36,6 +40,14 @@ gint _notify_notification_get_timeout (const NotifyNotific
gboolean _notify_notification_has_nondefault_actions (const NotifyNotification *n);
gboolean _notify_check_spec_version (int major, int minor);
+const char * _notify_get_snap_name (void);
+const char * _notify_get_snap_path (void);
+const char * _notify_get_snap_app (void);
+
+const char * _notify_get_flatpak_app (void);
+
+gboolean _notify_uses_portal_notifications (void);
+
G_END_DECLS
#endif /* _LIBNOTIFY_INTERNAL_H_ */
diff --git a/libnotify/notification.c b/libnotify/notification.c
index 18d7203..c8c7010 100644
--- a/libnotify/notification.c
+++ b/libnotify/notification.c
@@ -53,6 +53,7 @@
static void notify_notification_class_init (NotifyNotificationClass *klass);
static void notify_notification_init (NotifyNotification *sp);
static void notify_notification_finalize (GObject *object);
+static void notify_notification_dispose (GObject *object);
typedef struct
{
@@ -70,12 +71,9 @@ struct _NotifyNotificationPrivate
char *body;
char *activation_token;
- const char *snap_path;
- const char *snap_name;
- char *snap_app;
-
/* NULL to use icon data. Anything else to have server lookup icon */
char *icon_name;
+ GdkPixbuf *icon_pixbuf;
/*
* -1 = use server default
@@ -83,6 +81,7 @@ struct _NotifyNotificationPrivate
* > 0 = Number of milliseconds before we timeout
*/
gint timeout;
+ guint portal_timeout_id;
GSList *actions;
GHashTable *action_map;
@@ -154,6 +153,7 @@ notify_notification_class_init (NotifyNotificationClass *klass)
object_class->constructor = notify_notification_constructor;
object_class->get_property = notify_notification_get_property;
object_class->set_property = notify_notification_set_property;
+ object_class->dispose = notify_notification_dispose;
object_class->finalize = notify_notification_finalize;
/**
@@ -238,9 +238,9 @@ notify_notification_class_init (NotifyNotificationClass *klass)
g_param_spec_int ("closed-reason",
"Closed Reason",
"The reason code for why the notification was closed",
- -1,
+ NOTIFY_CLOSED_REASON_UNSET,
G_MAXINT32,
- -1,
+ NOTIFY_CLOSED_REASON_UNSET,
G_PARAM_READABLE
| G_PARAM_STATIC_NAME
| G_PARAM_STATIC_NICK
@@ -356,110 +356,12 @@ destroy_pair (CallbackPair *pair)
g_free (pair);
}
-static void
-maybe_initialize_snap (NotifyNotification *obj)
-{
- NotifyNotificationPrivate *priv = obj->priv;
- gchar *cgroup_contents = NULL;
-
- priv->snap_path = g_getenv ("SNAP");
- if (priv->snap_path == NULL)
- return;
-
- if (*priv->snap_path == '\0' ||
- !strchr (priv->snap_path, G_DIR_SEPARATOR)) {
- priv->snap_path = NULL;
- return;
- }
-
- priv->snap_name = g_getenv ("SNAP_NAME");
- if (priv->snap_name && *priv->snap_name == '\0') {
- priv->snap_name = NULL;
- }
-
- if (g_file_get_contents ("/proc/self/cgroup", &cgroup_contents,
- NULL, NULL)) {
- gchar **lines = g_strsplit (cgroup_contents, "\n", -1);
- gchar *found_snap_name = NULL;
- gint i;
-
- for (i = 0; lines[i]; ++i) {
- gchar **parts = g_strsplit (lines[i], ":", 3);
- gchar *basename;
- gchar **ns;
- guint ns_length;
-
- if (g_strv_length (parts) != 3) {
- g_strfreev (parts);
- continue;
- }
-
- basename = g_path_get_basename (parts[2]);
- g_strfreev (parts);
-
- if (!basename) {
- continue;
- }
-
- ns = g_strsplit (basename, ".", -1);
- ns_length = g_strv_length (ns);
- g_free (basename);
-
- if (ns_length < 2 || !g_str_equal (ns[0], "snap")) {
- g_strfreev (ns);
- continue;
- }
-
- if (priv->snap_name == NULL) {
- g_free (found_snap_name);
- found_snap_name = g_strdup (ns[1]);
- }
-
- if (ns_length < 3) {
- g_strfreev (ns);
- continue;
- }
-
- if (priv->snap_name == NULL) {
- priv->snap_name = found_snap_name;
- found_snap_name = NULL;
- }
-
- if (g_str_equal (ns[1], priv->snap_name)) {
- priv->snap_app = g_strdup (ns[2]);
- g_strfreev (ns);
- break;
- }
-
- g_strfreev (ns);
- }
-
- if (priv->snap_name == NULL && found_snap_name != NULL) {
- priv->snap_name = found_snap_name;
- found_snap_name = NULL;
- }
-
- g_strfreev (lines);
- g_free (found_snap_name);
- }
-
- if (priv->snap_app == NULL) {
- priv->snap_app = g_strdup (priv->snap_name);
- }
-
- g_debug ("SNAP path: %s", priv->snap_path);
- g_debug ("SNAP name: %s", priv->snap_name);
- g_debug ("SNAP app: %s", priv->snap_app);
-
- g_free (cgroup_contents);
-}
-
static void
notify_notification_init (NotifyNotification *obj)
{
obj->priv = g_new0 (NotifyNotificationPrivate, 1);
obj->priv->timeout = NOTIFY_EXPIRES_DEFAULT;
- obj->priv->closed_reason = -1;
+ obj->priv->closed_reason = NOTIFY_CLOSED_REASON_UNSET;
obj->priv->hints = g_hash_table_new_full (g_str_hash,
g_str_equal,
g_free,
@@ -469,8 +371,22 @@ notify_notification_init (NotifyNotification *obj)
g_str_equal,
g_free,
(GDestroyNotify) destroy_pair);
+}
- maybe_initialize_snap (obj);
+static void
+notify_notification_dispose (GObject *object)
+{
+ NotifyNotification *obj = NOTIFY_NOTIFICATION (object);
+ NotifyNotificationPrivate *priv = obj->priv;
+
+ if (priv->portal_timeout_id) {
+ g_source_remove (priv->portal_timeout_id);
+ priv->portal_timeout_id = 0;
+ }
+
+ g_clear_object (&priv->icon_pixbuf);
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
@@ -487,7 +403,6 @@ notify_notification_finalize (GObject *object)
g_free (priv->body);
g_free (priv->icon_name);
g_free (priv->activation_token);
- g_free (priv->snap_app);
if (priv->actions != NULL) {
g_slist_foreach (priv->actions, (GFunc) g_free, NULL);
@@ -510,6 +425,18 @@ notify_notification_finalize (GObject *object)
G_OBJECT_CLASS (parent_class)->finalize (object);
}
+static gboolean
+maybe_warn_portal_unsupported_feature (const char *feature_name)
+{
+ if (!_notify_uses_portal_notifications ()) {
+ return FALSE;
+ }
+
+ g_message ("%s is not available when using Portal Notifications",
+ feature_name);
+ return TRUE;
+}
+
/**
* notify_notification_new:
* @summary: The required summary text.
@@ -596,7 +523,6 @@ static gchar *
try_prepend_snap_desktop (NotifyNotification *notification,
const gchar *desktop)
{
- NotifyNotificationPrivate *priv = notification->priv;
gchar *ret = NULL;
/*
@@ -604,11 +530,11 @@ try_prepend_snap_desktop (NotifyNotification *notification,
* ${SNAP_NAME}_; snap .desktop files are in the format
* ${SNAP_NAME}_desktop_file_name
*/
- ret = try_prepend_path (desktop, priv->snap_path);
+ ret = try_prepend_path (desktop, _notify_get_snap_path ());
- if (ret == NULL && priv->snap_name != NULL &&
+ if (ret == NULL && _notify_get_snap_name () != NULL &&
strchr (desktop, G_DIR_SEPARATOR) == NULL) {
- ret = g_strdup_printf ("%s_%s", priv->snap_name, desktop);
+ ret = g_strdup_printf ("%s_%s", _notify_get_snap_name (), desktop);
}
return ret;
@@ -619,7 +545,7 @@ try_prepend_snap (NotifyNotification *notification,
const gchar *value)
{
/* hardcoded paths to icons might be relocated under $SNAP */
- return try_prepend_path (value, notification->priv->snap_path);
+ return try_prepend_path (value, _notify_get_snap_path ());
}
@@ -698,6 +624,80 @@ notify_notification_update (NotifyNotification *notification,
return TRUE;
}
+static char *
+get_portal_notification_id (NotifyNotification *notification)
+{
+ char *app_id;
+ char *notification_id;
+
+ g_assert (_notify_uses_portal_notifications ());
+
+ if (_notify_get_snap_name ()) {
+ app_id = g_strdup_printf ("snap.%s_%s",
+ _notify_get_snap_name (),
+ _notify_get_snap_app ());
+ } else {
+ app_id = g_strdup_printf ("flatpak.%s",
+ _notify_get_flatpak_app ());
+ }
+
+ notification_id = g_strdup_printf ("libnotify-%s-%s-%u",
+ app_id,
+ notify_get_app_name (),
+ notification->priv->id);
+
+ g_free (app_id);
+
+ return notification_id;
+}
+
+static gboolean
+activate_action (NotifyNotification *notification,
+ const gchar *action)
+{
+ CallbackPair *pair;
+
+ pair = g_hash_table_lookup (notification->priv->action_map, action);
+
+ if (!pair) {
+ return FALSE;
+ }
+
+ /* Some clients have assumed it is safe to unref the
+ * Notification at the end of their NotifyActionCallback
+ * so we add a temporary ref until we're done with it.
+ */
+ g_object_ref (notification);
+
+ notification->priv->activating = TRUE;
+ pair->cb (notification, (char *) action, pair->user_data);
+ notification->priv->activating = FALSE;
+ g_free (notification->priv->activation_token);
+ notification->priv->activation_token = NULL;
+
+ g_object_unref (notification);
+
+ return TRUE;
+}
+
+static gboolean
+close_notification (NotifyNotification *notification,
+ NotifyClosedReason reason)
+{
+ if (notification->priv->closed_reason != NOTIFY_CLOSED_REASON_UNSET ||
+ reason == NOTIFY_CLOSED_REASON_UNSET) {
+ return FALSE;
+ }
+
+ g_object_ref (G_OBJECT (notification));
+ notification->priv->closed_reason = reason;
+ g_signal_emit (notification, signals[SIGNAL_CLOSED], 0);
+ notification->priv->id = 0;
+ g_object_unref (G_OBJECT (notification));
+
+ return TRUE;
+}
+
static void
proxy_g_signal_cb (GDBusProxy *proxy,
const char *sender_name,
@@ -705,8 +705,12 @@ proxy_g_signal_cb (GDBusProxy *proxy,
GVariant *parameters,
NotifyNotification *notification)
{
+ const char *interface;
+
g_return_if_fail (NOTIFY_IS_NOTIFICATION (notification));
+ interface = g_dbus_proxy_get_interface_name (proxy);
+
if (g_strcmp0 (signal_name, "NotificationClosed") == 0 &&
g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) {
guint32 id, reason;
@@ -715,43 +719,21 @@ proxy_g_signal_cb (GDBusProxy *proxy,
if (id != notification->priv->id)
return;
- g_object_ref (G_OBJECT (notification));
- notification->priv->closed_reason = reason;
- g_signal_emit (notification, signals[SIGNAL_CLOSED], 0);
- notification->priv->id = 0;
- g_object_unref (G_OBJECT (notification));
+ close_notification (notification, reason);
} else if (g_strcmp0 (signal_name, "ActionInvoked") == 0 &&
+ g_str_equal (interface, NOTIFY_DBUS_CORE_INTERFACE) &&
g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(us)"))) {
guint32 id;
const char *action;
- CallbackPair *pair;
g_variant_get (parameters, "(u&s)", &id, &action);
if (id != notification->priv->id)
return;
- pair = (CallbackPair *) g_hash_table_lookup (notification->priv->action_map,
- action);
-
- if (pair == NULL) {
- if (g_ascii_strcasecmp (action, "default")) {
- g_warning ("Received unknown action %s", action);
- }
- } else {
- /* Some clients have assumed it is safe to unref the
- * Notification at the end of their NotifyActionCallback
- * so we add a temporary ref until we're done with it.
- */
- g_object_ref (notification);
-
- notification->priv->activating = TRUE;
- pair->cb (notification, (char *) action, pair->user_data);
- notification->priv->activating = FALSE;
- g_free (notification->priv->activation_token);
- notification->priv->activation_token = NULL;
-
- g_object_unref (notification);
+ if (!activate_action (notification, action) &&
+ g_ascii_strcasecmp (action, "default")) {
+ g_warning ("Received unknown action %s", action);
}
} else if (g_strcmp0 (signal_name, "ActivationToken") == 0 &&
g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(us)"))) {
@@ -765,9 +747,306 @@ proxy_g_signal_cb (GDBusProxy *proxy,
g_free (notification->priv->activation_token);
notification->priv->activation_token = g_strdup (activation_token);
+ } else if (g_str_equal (signal_name, "ActionInvoked") &&
+ g_str_equal (interface, NOTIFY_PORTAL_DBUS_CORE_INTERFACE) &&
+ g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ssav)"))) {
+ char *notification_id;
+ const char *id;
+ const char *action;
+ GVariant *parameter;
+
+ g_variant_get (parameters, "(&s&s@av)", &id, &action, ¶meter);
+ g_variant_unref (parameter);
+
+ notification_id = get_portal_notification_id (notification);
+
+ if (!g_str_equal (notification_id, id)) {
+ g_free (notification_id);
+ return;
+ }
+
+ if (!activate_action (notification, action) &&
+ g_str_equal (action, "default-action") &&
+ !_notify_get_snap_app ()) {
+ g_warning ("Received unknown action %s", action);
+ }
+
+ close_notification (notification, NOTIFY_CLOSED_REASON_DISMISSED);
+
+ g_free (notification_id);
+ } else {
+ g_debug ("Unhandled signal '%s.%s'", interface, signal_name);
}
}
+static gboolean
+remove_portal_notification (GDBusProxy *proxy,
+ NotifyNotification *notification,
+ NotifyClosedReason reason,
+ GError **error)
+{
+ GVariant *ret;
+ gchar *notification_id;
+
+ if (notification->priv->portal_timeout_id) {
+ g_source_remove (notification->priv->portal_timeout_id);
+ notification->priv->portal_timeout_id = 0;
+ }
+
+ notification_id = get_portal_notification_id (notification);
+
+ ret = g_dbus_proxy_call_sync (proxy,
+ "RemoveNotification",
+ g_variant_new ("(s)", notification_id),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ error);
+
+ g_free (notification_id);
+
+ if (!ret) {
+ return FALSE;
+ }
+
+ close_notification (notification, reason);
+
+ g_variant_unref (ret);
+
+ return TRUE;
+}
+
+static gboolean
+on_portal_timeout (gpointer data)
+{
+ NotifyNotification *notification = data;
+ GDBusProxy *proxy;
+
+ notification->priv->portal_timeout_id = 0;
+
+ proxy = _notify_get_proxy (NULL);
+ if (proxy == NULL) {
+ return FALSE;
+ }
+
+ remove_portal_notification (proxy, notification,
+ NOTIFY_CLOSED_REASON_EXPIRED, NULL);
+ return FALSE;
+}
+
+static GIcon *
+get_notification_gicon (NotifyNotification *notification,
+ GError **error)
+{
+ NotifyNotificationPrivate *priv = notification->priv;
+ GFileInputStream *input;
+ GFile *file = NULL;
+ GIcon *gicon = NULL;
+
+ if (priv->icon_pixbuf) {
+ return G_ICON (g_object_ref (priv->icon_pixbuf));
+ }
+
+ if (!priv->icon_name) {
+ return NULL;
+ }
+
+ if (strstr (priv->icon_name, "://")) {
+ file = g_file_new_for_uri (priv->icon_name);
+ } else if (g_file_test (priv->icon_name, G_FILE_TEST_EXISTS)) {
+ file = g_file_new_for_path (priv->icon_name);
+ } else {
+ gicon = g_themed_icon_new (priv->icon_name);
+ }
+
+ if (!file) {
+ return gicon;
+ }
+
+ input = g_file_read (file, NULL, error);
+
+ if (input) {
+ GByteArray *bytes_array = g_byte_array_new ();
+ guint8 buf[1024];
+
+ while (TRUE) {
+ gssize read;
+
+ read = g_input_stream_read (G_INPUT_STREAM (input),
+ buf,
+ G_N_ELEMENTS (buf),
+ NULL, NULL);
+
+ if (read > 0) {
+ g_byte_array_append (bytes_array, buf, read);
+ } else {
+ if (read < 0) {
+ g_byte_array_unref (bytes_array);
+ bytes_array = NULL;
+ }
+
+ break;
+ }
+ }
+
+ if (bytes_array && bytes_array->len) {
+ GBytes *bytes;
+
+ bytes = g_byte_array_free_to_bytes (bytes_array);
+ bytes_array = NULL;
+
+ gicon = g_bytes_icon_new (bytes);
+ } else if (bytes_array) {
+ g_byte_array_unref (bytes_array);
+ }
+ }
+
+ g_clear_object (&input);
+ g_clear_object (&file);
+
+ return gicon;
+}
+
+static gboolean
+add_portal_notification (GDBusProxy *proxy,
+ NotifyNotification *notification,
+ GError **error)
+{
+ GIcon *icon;
+ GVariant *urgency;
+ GVariant *ret;
+ GVariantBuilder builder;
+ NotifyNotificationPrivate *priv = notification->priv;
+ GError *local_error = NULL;
+ static guint32 portal_notification_count = 0;
+ char *notification_id;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
+
+ g_variant_builder_add (&builder, "{sv}", "title",
+ g_variant_new_string (priv->summary ? priv->summary : ""));
+ g_variant_builder_add (&builder, "{sv}", "body",
+ g_variant_new_string (priv->body ? priv->body : ""));
+
+ if (g_hash_table_lookup (priv->action_map, "default")) {
+ g_variant_builder_add (&builder, "{sv}", "default-action",
+ g_variant_new_string ("default"));
+ } else if (g_hash_table_lookup (priv->action_map, "DEFAULT")) {
+ g_variant_builder_add (&builder, "{sv}", "default-action",
+ g_variant_new_string ("DEFAULT"));
+ } else if (_notify_get_snap_app ()) {
+ /* In the snap case we may need to ensure that a default-action
+ * is set to ensure that we will use the FDO notification daemon
+ * and won't fallback to GTK one, as app-id won't match.
+ * See: https://github.com/flatpak/xdg-desktop-portal/issues/769
+ */
+ g_variant_builder_add (&builder, "{sv}", "default-action",
+ g_variant_new_string ("snap-fake-default-action"));
+ }
+
+ if (priv->has_nondefault_actions) {
+ GVariantBuilder buttons;
+ GSList *l;
+
+ g_variant_builder_init (&buttons, G_VARIANT_TYPE ("aa{sv}"));
+
+ for (l = priv->actions; l && l->next; l = l->next->next) {
+ GVariantBuilder button;
+ const char *action;
+ const char *label;
+
+ g_variant_builder_init (&button, G_VARIANT_TYPE_VARDICT);
+
+ action = l->data;
+ label = l->next->data;
+
+ g_variant_builder_add (&button, "{sv}", "action",
+ g_variant_new_string (action));
+ g_variant_builder_add (&button, "{sv}", "label",
+ g_variant_new_string (label));
+
+ g_variant_builder_add (&buttons, "@a{sv}",
+ g_variant_builder_end (&button));
+ }
+
+ g_variant_builder_add (&builder, "{sv}", "buttons",
+ g_variant_builder_end (&buttons));
+ }
+
+ urgency = g_hash_table_lookup (notification->priv->hints, "urgency");
+ if (urgency) {
+ switch (g_variant_get_byte (urgency)) {
+ case NOTIFY_URGENCY_LOW:
+ g_variant_builder_add (&builder, "{sv}", "priority",
+ g_variant_new_string ("low"));
+ break;
+ case NOTIFY_URGENCY_NORMAL:
+ g_variant_builder_add (&builder, "{sv}", "priority",
+ g_variant_new_string ("normal"));
+ break;
+ case NOTIFY_URGENCY_CRITICAL:
+ g_variant_builder_add (&builder, "{sv}", "priority",
+ g_variant_new_string ("urgent"));
+ break;
+ default:
+ g_warn_if_reached ();
+ }
+ }
+
+ icon = get_notification_gicon (notification, &local_error);
+ if (icon) {
+ GVariant *serialized_icon = g_icon_serialize (icon);
+
+ g_variant_builder_add (&builder, "{sv}", "icon",
+ serialized_icon);
+ g_variant_unref (serialized_icon);
+ g_clear_object (&icon);
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
+
+ if (!priv->id) {
+ priv->id = ++portal_notification_count;
+ } else if (priv->closed_reason == NOTIFY_CLOSED_REASON_UNSET) {
+ remove_portal_notification (proxy, notification,
+ NOTIFY_CLOSED_REASON_UNSET, NULL);
+ }
+
+ notification_id = get_portal_notification_id (notification);
+
+ ret = g_dbus_proxy_call_sync (proxy,
+ "AddNotification",
+ g_variant_new ("(s@a{sv})",
+ notification_id,
+ g_variant_builder_end (&builder)),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ error);
+
+ if (priv->portal_timeout_id) {
+ g_source_remove (priv->portal_timeout_id);
+ priv->portal_timeout_id = 0;
+ }
+
+ g_free (notification_id);
+
+ if (!ret) {
+ return FALSE;
+ }
+
+ if (priv->timeout > 0) {
+ priv->portal_timeout_id = g_timeout_add (priv->timeout,
+ on_portal_timeout,
+ notification);
+ }
+
+ g_variant_unref (ret);
+
+ return TRUE;
+}
+
/**
* notify_notification_show:
* @notification: The notification.
@@ -789,9 +1068,7 @@ notify_notification_show (NotifyNotification *notification,
GHashTableIter iter;
gpointer key, data;
GVariant *result;
-#ifdef GLIB_VERSION_2_32
GApplication *application = NULL;
-#endif
g_return_val_if_fail (notification != NULL, FALSE);
g_return_val_if_fail (NOTIFY_IS_NOTIFICATION (notification), FALSE);
@@ -815,6 +1092,10 @@ notify_notification_show (NotifyNotification *notification,
notification);
}
+ if (_notify_uses_portal_notifications ()) {
+ return add_portal_notification (proxy, notification, error);
+ }
+
g_variant_builder_init (&actions_builder, G_VARIANT_TYPE ("as"));
for (l = priv->actions; l != NULL; l = l->next) {
g_variant_builder_add (&actions_builder, "s", l->data);
@@ -831,13 +1112,13 @@ notify_notification_show (NotifyNotification *notification,
g_variant_new_int64 (getpid ()));
}
- if (priv->snap_app &&
+ if (_notify_get_snap_app () &&
g_hash_table_lookup (priv->hints, "desktop-entry") == NULL) {
gchar *snap_desktop;
snap_desktop = g_strdup_printf ("%s_%s",
- priv->snap_name,
- priv->snap_app);
+ _notify_get_snap_name (),
+ _notify_get_snap_app ());
g_debug ("Using desktop entry: %s", snap_desktop);
g_variant_builder_add (&hints_builder, "{sv}",
@@ -845,8 +1126,7 @@ notify_notification_show (NotifyNotification *notification,
g_variant_new_take_string (snap_desktop));
}
-#ifdef GLIB_VERSION_2_32
- if (!priv->snap_app) {
+ if (!_notify_get_snap_app ()) {
application = g_application_get_default ();
}
@@ -861,7 +1141,6 @@ notify_notification_show (NotifyNotification *notification,
g_variant_new_string (application_id));
}
}
-#endif
/* TODO: make this nonblocking */
result = g_dbus_proxy_call_sync (proxy,
@@ -940,6 +1219,10 @@ notify_notification_set_category (NotifyNotification *notification,
g_return_if_fail (notification != NULL);
g_return_if_fail (NOTIFY_IS_NOTIFICATION (notification));
+ if (maybe_warn_portal_unsupported_feature ("Category")) {
+ return;
+ }
+
if (category != NULL && category[0] != '\0') {
notify_notification_set_hint_string (notification,
"category",
@@ -1017,11 +1300,18 @@ notify_notification_set_image_from_pixbuf (NotifyNotification *notification,
hint_name = "icon_data";
}
+ g_clear_object (¬ification->priv->icon_pixbuf);
+
if (pixbuf == NULL) {
notify_notification_set_hint (notification, hint_name, NULL);
return;
}
+ if (_notify_uses_portal_notifications ()) {
+ notification->priv->icon_pixbuf = g_object_ref (pixbuf);
+ return;
+ }
+
g_object_get (pixbuf,
"width", &width,
"height", &height,
@@ -1078,7 +1368,7 @@ maybe_parse_snap_hint_value (NotifyNotification *notification,
{
StringParserFunc parse_func = NULL;
- if (!notification->priv->snap_path)
+ if (!_notify_get_snap_path ())
return value;
if (g_strcmp0 (key, "desktop-entry") == 0) {
@@ -1145,6 +1435,10 @@ notify_notification_set_app_name (NotifyNotification *notification,
{
g_return_if_fail (NOTIFY_IS_NOTIFICATION (notification));
+ if (maybe_warn_portal_unsupported_feature ("App Name")) {
+ return;
+ }
+
g_free (notification->priv->app_name);
notification->priv->app_name = g_strdup (app_name);
@@ -1385,6 +1679,7 @@ notify_notification_add_action (NotifyNotification *notification,
/**
* notify_notification_get_activation_token:
+ * @notification: The notification.
*
* If an an action is currently being activated, return the activation token.
* This function is intended to be used in a #NotifyActionCallback to get
@@ -1399,11 +1694,9 @@ const char *
notify_notification_get_activation_token (NotifyNotification *notification)
{
g_return_val_if_fail (NOTIFY_IS_NOTIFICATION (notification), NULL);
+ g_return_val_if_fail (notification->priv->activating, NULL);
- if (notification->priv->activating)
- return notification->priv->activation_token;
-
- return NULL;
+ return notification->priv->activation_token;
}
gboolean
@@ -1442,6 +1735,12 @@ notify_notification_close (NotifyNotification *notification,
return FALSE;
}
+ if (_notify_uses_portal_notifications ()) {
+ return remove_portal_notification (proxy, notification,
+ NOTIFY_CLOSED_REASON_API_REQUEST,
+ error);
+ }
+
/* FIXME: make this nonblocking! */
result = g_dbus_proxy_call_sync (proxy,
"CloseNotification",
@@ -1466,13 +1765,17 @@ notify_notification_close (NotifyNotification *notification,
* Returns the closed reason code for the notification. This is valid only
* after the "closed" signal is emitted.
*
- * Returns: The closed reason code.
+ * Since version 0.8.0 the returned value is of type #NotifyClosedReason.
+ *
+ * Returns: An integer representing the closed reason code
+ * (Since 0.8.0 it's also a #NotifyClosedReason).
*/
gint
notify_notification_get_closed_reason (const NotifyNotification *notification)
{
- g_return_val_if_fail (notification != NULL, -1);
- g_return_val_if_fail (NOTIFY_IS_NOTIFICATION (notification), -1);
+ g_return_val_if_fail (notification != NULL, NOTIFY_CLOSED_REASON_UNSET);
+ g_return_val_if_fail (NOTIFY_IS_NOTIFICATION (notification),
+ NOTIFY_CLOSED_REASON_UNSET);
return notification->priv->closed_reason;
}
diff --git a/libnotify/notification.h b/libnotify/notification.h
index ffc960f..f0c8e69 100644
--- a/libnotify/notification.h
+++ b/libnotify/notification.h
@@ -55,6 +55,11 @@ typedef struct _NotifyNotification NotifyNotification;
typedef struct _NotifyNotificationClass NotifyNotificationClass;
typedef struct _NotifyNotificationPrivate NotifyNotificationPrivate;
+/**
+ * NotifyNotification:
+ *
+ * A passive pop-up notification.
+ */
struct _NotifyNotification
{
/*< private >*/
@@ -88,11 +93,34 @@ typedef enum
} NotifyUrgency;
+
+/**
+ * NotifyClosedReason:
+ * @NOTIFY_CLOSED_REASON_UNSET: Notification not closed.
+ * @NOTIFY_CLOSED_REASON_EXPIRED: Timeout has expired.
+ * @NOTIFY_CLOSED_REASON_DISMISSED: It has been dismissed by the user.
+ * @NOTIFY_CLOSED_REASON_API_REQUEST: It has been closed by a call to
+ * notify_notification_close().
+ * @NOTIFY_CLOSED_REASON_UNDEFIEND: Closed by undefined/reserved reasons.
+ *
+ * The reason for which the notification has been closed.
+ *
+ * Since: 0.8.0
+ */
+typedef enum
+{
+ NOTIFY_CLOSED_REASON_UNSET = -1,
+ NOTIFY_CLOSED_REASON_EXPIRED = 1,
+ NOTIFY_CLOSED_REASON_DISMISSED = 2,
+ NOTIFY_CLOSED_REASON_API_REQUEST = 3,
+ NOTIFY_CLOSED_REASON_UNDEFIEND = 4,
+} NotifyClosedReason;
+
/**
* NotifyActionCallback:
- * @notification:
- * @action:
- * @user_data:
+ * @notification: a #NotifyActionCallback notification
+ * @action: (transfer none): The activated action name
+ * @user_data: (nullable) (transfer none): User provided data
*
* An action callback function.
*/
diff --git a/libnotify/notify.c b/libnotify/notify.c
index ce5a97b..362ba3d 100644
--- a/libnotify/notify.c
+++ b/libnotify/notify.c
@@ -43,10 +43,14 @@
static gboolean _initted = FALSE;
static char *_app_name = NULL;
+static char *_snap_name = NULL;
+static char *_snap_app = NULL;
+static char *_flatpak_app = NULL;
static GDBusProxy *_proxy = NULL;
static GList *_active_notifications = NULL;
static int _spec_version_major = 0;
static int _spec_version_minor = 0;
+static int _portal_version = 0;
gboolean
_notify_check_spec_version (int major,
@@ -74,6 +78,26 @@ _notify_get_server_info (char **ret_name,
return FALSE;
}
+ if (_notify_uses_portal_notifications ()) {
+ if (ret_name) {
+ *ret_name = g_strdup ("Portal Notification");
+ }
+
+ if (ret_vendor) {
+ *ret_vendor = g_strdup ("Freedesktop");
+ }
+
+ if (ret_version) {
+ *ret_version = g_strdup_printf ("%u", _portal_version);
+ }
+
+ if (ret_spec_version) {
+ *ret_spec_version = g_strdup ("1.2");
+ }
+
+ return TRUE;
+ }
+
result = g_dbus_proxy_call_sync (proxy,
"GetServerInformation",
g_variant_new ("()"),
@@ -119,6 +143,18 @@ _notify_update_spec_version (GError **error)
return TRUE;
}
+static gboolean
+set_app_name (const char *app_name)
+{
+ g_return_val_if_fail (app_name != NULL, FALSE);
+ g_return_val_if_fail (*app_name != '\0', FALSE);
+
+ g_free (_app_name);
+ _app_name = g_strdup (app_name);
+
+ return TRUE;
+}
+
/**
* notify_set_app_name:
@@ -130,46 +166,267 @@ _notify_update_spec_version (GError **error)
void
notify_set_app_name (const char *app_name)
{
- g_free (_app_name);
- _app_name = g_strdup (app_name);
+ set_app_name (app_name);
}
/**
* notify_init:
- * @app_name: The name of the application initializing libnotify.
+ * @app_name: (nullable): The name of the application initializing libnotify.
*
* Initialized libnotify. This must be called before any other functions.
*
+ * Starting from 0.8, if the provided @app_name is %NULL, libnotify will
+ * try to figure it out from the running application.
+ * Before it was not allowed, and was causing libnotify not to be initialized.
+ *
* Returns: %TRUE if successful, or %FALSE on error.
*/
gboolean
notify_init (const char *app_name)
{
- g_return_val_if_fail (app_name != NULL, FALSE);
- g_return_val_if_fail (*app_name != '\0', FALSE);
-
if (_initted)
return TRUE;
-#ifdef GLIB_VERSION_2_32
- if (app_name == NULL && g_application_get_default ()) {
- GApplication *application = g_application_get_default ();
+ if (app_name == NULL) {
+ GApplication *application;
- app_name = g_application_get_application_id (application);
+ app_name = _notify_get_snap_app ();
+ if (app_name == NULL) {
+ app_name = _notify_get_flatpak_app ();
+ }
+
+ if (app_name == NULL &&
+ (application = g_application_get_default ())) {
+ app_name = g_application_get_application_id (application);
+ }
}
-#endif
- notify_set_app_name (app_name);
-
-#ifndef GLIB_VERSION_2_36
- g_type_init ();
-#endif
+ if (!set_app_name (app_name)) {
+ return FALSE;
+ }
_initted = TRUE;
return TRUE;
}
+static void
+_initialize_snap_names (void)
+{
+ gchar *cgroup_contents = NULL;
+ gchar *found_snap_name = NULL;
+ gchar **lines;
+ gint i;
+
+ if (!g_file_get_contents ("/proc/self/cgroup", &cgroup_contents,
+ NULL, NULL)) {
+ g_free (cgroup_contents);
+ return;
+ }
+
+ lines = g_strsplit (cgroup_contents, "\n", -1);
+ g_free (cgroup_contents);
+
+ for (i = 0; lines[i]; ++i) {
+ gchar **parts = g_strsplit (lines[i], ":", 3);
+ gchar *basename;
+ gchar **ns;
+ guint ns_length;
+
+ if (g_strv_length (parts) != 3) {
+ g_strfreev (parts);
+ continue;
+ }
+
+ basename = g_path_get_basename (parts[2]);
+ g_strfreev (parts);
+
+ if (!basename) {
+ continue;
+ }
+
+ ns = g_strsplit (basename, ".", -1);
+ ns_length = g_strv_length (ns);
+ g_free (basename);
+
+ if (ns_length < 2 || !g_str_equal (ns[0], "snap")) {
+ g_strfreev (ns);
+ continue;
+ }
+
+ if (_snap_name == NULL) {
+ g_free (found_snap_name);
+ found_snap_name = g_strdup (ns[1]);
+ }
+
+ if (ns_length < 3) {
+ g_strfreev (ns);
+ continue;
+ }
+
+ if (_snap_name == NULL) {
+ _snap_name = found_snap_name;
+ found_snap_name = NULL;
+ g_debug ("SNAP name: %s", _snap_name);
+ }
+
+ if (g_str_equal (ns[1], _snap_name)) {
+ _snap_app = g_strdup (ns[2]);
+ g_strfreev (ns);
+ break;
+ }
+
+ g_strfreev (ns);
+ }
+
+ if (_snap_name == NULL && found_snap_name != NULL) {
+ _snap_name = found_snap_name;
+ found_snap_name = NULL;
+ g_debug ("SNAP name: %s", _snap_name);
+ }
+
+ if (_snap_app == NULL) {
+ _snap_app = g_strdup (_snap_name);
+ }
+
+ g_debug ("SNAP app: %s", _snap_app);
+
+ g_strfreev (lines);
+ g_free (found_snap_name);
+}
+
+const char *
+_notify_get_snap_path (void)
+{
+ static const char *snap_path = NULL;
+ static gsize snap_path_set = FALSE;
+
+ if (g_once_init_enter (&snap_path_set)) {
+ snap_path = g_getenv ("SNAP");
+
+ if (!snap_path || *snap_path == '\0' ||
+ !strchr (snap_path, G_DIR_SEPARATOR)) {
+ snap_path = NULL;
+ } else {
+ g_debug ("SNAP path: %s", snap_path);
+ }
+
+ g_once_init_leave (&snap_path_set, TRUE);
+ }
+
+ return snap_path;
+}
+
+const char *
+_notify_get_snap_name (void)
+{
+ static gsize snap_name_set = FALSE;
+
+ if (g_once_init_enter (&snap_name_set)) {
+ if (!_snap_name) {
+ const char *snap_name_env = g_getenv ("SNAP_NAME");
+
+ if (!snap_name_env || *snap_name_env == '\0')
+ snap_name_env = NULL;
+
+ _snap_name = g_strdup (snap_name_env);
+ g_debug ("SNAP name: %s", _snap_name);
+ }
+
+ g_once_init_leave (&snap_name_set, TRUE);
+ }
+
+ return _snap_name;
+}
+
+const char *
+_notify_get_snap_app (void)
+{
+ static gsize snap_app_set = FALSE;
+
+ if (g_once_init_enter (&snap_app_set)) {
+ _initialize_snap_names ();
+ g_once_init_leave (&snap_app_set, TRUE);
+ }
+
+ return _snap_app;
+}
+
+const char *
+_notify_get_flatpak_app (void)
+{
+ static gsize flatpak_app_set = FALSE;
+
+ if (g_once_init_enter (&flatpak_app_set)) {
+ GKeyFile *info = g_key_file_new ();
+
+ if (g_key_file_load_from_file (info, "/.flatpak-info",
+ G_KEY_FILE_NONE, NULL)) {
+ const char *group = "Application";
+
+ if (g_key_file_has_group (info, "Runtime")) {
+ group = "Runtime";
+ }
+
+ _flatpak_app = g_key_file_get_string (info, group,
+ "name", NULL);
+ }
+
+ g_key_file_free (info);
+ g_once_init_leave (&flatpak_app_set, TRUE);
+ }
+
+ return _flatpak_app;
+}
+
+static gboolean
+_notify_is_running_under_flatpak (void)
+{
+ return !!_notify_get_flatpak_app ();
+}
+
+static gboolean
+_notify_is_running_under_snap (void)
+{
+ return !!_notify_get_snap_app ();
+}
+
+static gboolean
+_notify_is_running_in_sandbox (void)
+{
+ static gsize use_portal = 0;
+ enum {
+ IGNORE_PORTAL = 1,
+ TRY_USE_PORTAL = 2,
+ FORCE_PORTAL = 3
+ };
+
+ if (g_once_init_enter (&use_portal)) {
+ if (G_UNLIKELY (g_getenv ("NOTIFY_IGNORE_PORTAL"))) {
+ g_once_init_leave (&use_portal, IGNORE_PORTAL);
+ } else if (G_UNLIKELY (g_getenv ("NOTIFY_FORCE_PORTAL"))) {
+ g_once_init_leave (&use_portal, FORCE_PORTAL);
+ } else {
+ g_once_init_leave (&use_portal, TRY_USE_PORTAL);
+ }
+ }
+
+ if (use_portal == IGNORE_PORTAL) {
+ return FALSE;
+ }
+
+ return use_portal == FORCE_PORTAL ||
+ _notify_is_running_under_flatpak () ||
+ _notify_is_running_under_snap ();
+}
+
+gboolean
+_notify_uses_portal_notifications (void)
+{
+ return _portal_version != 0;
+}
+
+
/**
* notify_get_app_name:
*
@@ -219,6 +476,15 @@ notify_uninit (void)
_proxy = NULL;
}
+ g_free (_snap_name);
+ _snap_name = NULL;
+
+ g_free (_snap_app);
+ _snap_app = NULL;
+
+ g_free (_flatpak_app);
+ _flatpak_app = NULL;
+
_initted = FALSE;
}
@@ -235,6 +501,46 @@ notify_is_initted (void)
return _initted;
}
+GDBusProxy *
+_get_portal_proxy (GError **error)
+{
+ GError *local_error = NULL;
+ GDBusProxy *proxy;
+ GVariant *res;
+
+ proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ NOTIFY_PORTAL_DBUS_NAME,
+ NOTIFY_PORTAL_DBUS_CORE_OBJECT,
+ NOTIFY_PORTAL_DBUS_CORE_INTERFACE,
+ NULL,
+ &local_error);
+
+ if (proxy == NULL) {
+ g_debug ("Failed to get portal proxy: %s", local_error->message);
+ g_clear_error (&local_error);
+
+ return NULL;
+ }
+
+ res = g_dbus_proxy_get_cached_property (proxy, "version");
+ if (!res) {
+ g_object_unref (proxy);
+ return NULL;
+ }
+
+ _portal_version = g_variant_get_uint32 (res);
+ g_assert (_portal_version > 0);
+
+ g_warning ("Running in confined mode, using Portal notifications. "
+ "Some features and hints won't be supported");
+
+ g_variant_unref (res);
+
+ return proxy;
+}
+
/*
* _notify_get_proxy:
* @error: (allow-none): a location to store a #GError, or %NULL
@@ -250,6 +556,14 @@ _notify_get_proxy (GError **error)
if (_proxy != NULL)
return _proxy;
+ if (_notify_is_running_in_sandbox ()) {
+ _proxy = _get_portal_proxy (error);
+
+ if (_proxy != NULL) {
+ goto out;
+ }
+ }
+
_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
NULL,
@@ -258,6 +572,8 @@ _notify_get_proxy (GError **error)
NOTIFY_DBUS_CORE_INTERFACE,
NULL,
error);
+
+out:
if (_proxy == NULL) {
return NULL;
}
@@ -295,6 +611,15 @@ notify_get_server_caps (void)
return NULL;
}
+ if (_notify_uses_portal_notifications ()) {
+ list = g_list_prepend (list, g_strdup ("actions"));
+ list = g_list_prepend (list, g_strdup ("body"));
+ list = g_list_prepend (list, g_strdup ("body-images"));
+ list = g_list_prepend (list, g_strdup ("icon-static"));
+
+ return list;
+ }
+
result = g_dbus_proxy_call_sync (proxy,
"GetCapabilities",
g_variant_new ("()"),
diff --git a/meson.build b/meson.build
index 7abea3b..05a5385 100644
--- a/meson.build
+++ b/meson.build
@@ -1,6 +1,6 @@
project('libnotify',
'c',
- version: '0.7.12',
+ version: '0.8.0',
meson_version: '>= 0.47.0')
gnome = import('gnome')
@@ -36,7 +36,7 @@ man1dir = join_paths(prefix, get_option('mandir'), 'man1')
libnotify_deps = []
extra_deps = []
-glib_req_version = '>= 2.26.0'
+glib_req_version = '>= 2.38.0'
gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0')
glib_dep = dependency('glib-2.0', version: glib_req_version)
diff --git a/tests/test-error.c b/tests/test-error.c
index 66d7675..e18428b 100644
--- a/tests/test-error.c
+++ b/tests/test-error.c
@@ -29,10 +29,6 @@ main ()
{
NotifyNotification *n;
-#ifndef GLIB_VERSION_2_36
- g_type_init ();
-#endif
-
notify_init ("Error Handling");
n = notify_notification_new ("Summary", "Content", NULL);
diff --git a/tests/test-replace.c b/tests/test-replace.c
index 5af0c27..eea6409 100644
--- a/tests/test-replace.c
+++ b/tests/test-replace.c
@@ -28,10 +28,6 @@ main ()
GError *error;
error = NULL;
-#ifndef GLIB_VERSION_2_36
- g_type_init ();
-#endif
-
notify_init ("Replace Test");
n = notify_notification_new ("Summary",
diff --git a/tools/notify-send.c b/tools/notify-send.c
index 926cd11..367dfb0 100644
--- a/tools/notify-send.c
+++ b/tools/notify-send.c
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#define N_(x) (x)
@@ -148,6 +149,19 @@ handle_closed (NotifyNotification *notify,
g_main_loop_quit (loop);
}
+static gboolean
+on_sigint (gpointer data)
+{
+ NotifyNotification *notification = data;
+
+ g_printerr ("Wait cancelled, closing notification\n");
+
+ notify_notification_close (notification, NULL);
+ g_main_loop_quit (loop);
+
+ return FALSE;
+}
+
static void
handle_action (NotifyNotification *notify,
char *action,
@@ -203,6 +217,10 @@ main (int argc, char *argv[])
static char **n_text = NULL;
static char **hints = NULL;
static char **actions = NULL;
+ static char *server_name = NULL;
+ static char *server_vendor = NULL;
+ static char *server_version = NULL;
+ static char *server_spec_version = NULL;
static gboolean print_id = FALSE;
static gint notification_id = 0;
static gboolean do_version = FALSE;
@@ -265,10 +283,6 @@ main (int argc, char *argv[])
setlocale (LC_ALL, "");
-#ifndef GLIB_VERSION_2_36
- g_type_init ();
-#endif
-
g_set_prgname (argv[0]);
g_log_set_always_fatal (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
@@ -310,6 +324,18 @@ main (int argc, char *argv[])
if (!notify_init ("notify-send"))
exit (1);
+ notify_get_server_info (&server_name,
+ &server_vendor,
+ &server_version,
+ &server_spec_version);
+
+ g_debug ("Using sever %s %s, v%s - Supporting Notification Spec %s",
+ server_name, server_vendor, server_version, server_spec_version);
+ g_free (server_name);
+ g_free (server_vendor);
+ g_free (server_version);
+ g_free (server_spec_version);
+
notify = g_object_new (NOTIFY_TYPE_NOTIFICATION,
"summary", summary,
"body", body,
@@ -442,6 +468,7 @@ main (int argc, char *argv[])
}
if (wait) {
+ g_unix_signal_add (SIGINT, on_sigint, notify);
loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (loop);
g_main_loop_unref (loop);