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