| /* |
| * Copyright © 2013 Lars Uebernickel |
| * |
| * SPDX-License-Identifier: LGPL-2.1-or-later |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General |
| * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. |
| * |
| * Authors: Lars Uebernickel <lars@uebernic.de> |
| */ |
| |
| #include "config.h" |
| |
| #include "gnotificationbackend.h" |
| |
| #include "gapplication.h" |
| #include "giomodule-priv.h" |
| #include "gnotification-private.h" |
| #include "gdbusconnection.h" |
| #include "gdbusnamewatching.h" |
| #include "gactiongroup.h" |
| #include "gaction.h" |
| #include "gthemedicon.h" |
| #include "gfileicon.h" |
| #include "gfile.h" |
| #include "gdbusutils.h" |
| |
| #define G_TYPE_FDO_NOTIFICATION_BACKEND (g_fdo_notification_backend_get_type ()) |
| #define G_FDO_NOTIFICATION_BACKEND(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_FDO_NOTIFICATION_BACKEND, GFdoNotificationBackend)) |
| |
| typedef struct _GFdoNotificationBackend GFdoNotificationBackend; |
| typedef GNotificationBackendClass GFdoNotificationBackendClass; |
| |
| struct _GFdoNotificationBackend |
| { |
| GNotificationBackend parent; |
| |
| guint bus_name_id; |
| |
| guint notify_subscription; |
| GSList *notifications; |
| }; |
| |
| GType g_fdo_notification_backend_get_type (void); |
| |
| G_DEFINE_TYPE_WITH_CODE (GFdoNotificationBackend, g_fdo_notification_backend, G_TYPE_NOTIFICATION_BACKEND, |
| _g_io_modules_ensure_extension_points_registered (); |
| g_io_extension_point_implement (G_NOTIFICATION_BACKEND_EXTENSION_POINT_NAME, |
| g_define_type_id, "freedesktop", 0)) |
| |
| typedef struct |
| { |
| GFdoNotificationBackend *backend; |
| gchar *id; |
| guint32 notify_id; |
| gchar *default_action; /* (nullable) (owned) */ |
| GVariant *default_action_target; /* (nullable) (owned), not floating */ |
| } FreedesktopNotification; |
| |
| static void |
| freedesktop_notification_free (gpointer data) |
| { |
| FreedesktopNotification *n = data; |
| |
| g_object_unref (n->backend); |
| g_free (n->id); |
| g_free (n->default_action); |
| if (n->default_action_target) |
| g_variant_unref (n->default_action_target); |
| |
| g_slice_free (FreedesktopNotification, n); |
| } |
| |
| static FreedesktopNotification * |
| freedesktop_notification_new (GFdoNotificationBackend *backend, |
| const gchar *id, |
| GNotification *notification) |
| { |
| FreedesktopNotification *n; |
| |
| n = g_slice_new0 (FreedesktopNotification); |
| n->backend = g_object_ref (backend); |
| n->id = g_strdup (id); |
| n->notify_id = 0; |
| g_notification_get_default_action (notification, |
| &n->default_action, |
| &n->default_action_target); |
| |
| return n; |
| } |
| |
| static FreedesktopNotification * |
| g_fdo_notification_backend_find_notification (GFdoNotificationBackend *backend, |
| const gchar *id) |
| { |
| GSList *it; |
| |
| for (it = backend->notifications; it != NULL; it = it->next) |
| { |
| FreedesktopNotification *n = it->data; |
| if (g_str_equal (n->id, id)) |
| return n; |
| } |
| |
| return NULL; |
| } |
| |
| static FreedesktopNotification * |
| g_fdo_notification_backend_find_notification_by_notify_id (GFdoNotificationBackend *backend, |
| guint32 id) |
| { |
| GSList *it; |
| |
| for (it = backend->notifications; it != NULL; it = it->next) |
| { |
| FreedesktopNotification *n = it->data; |
| if (n->notify_id == id) |
| return n; |
| } |
| |
| return NULL; |
| } |
| |
| static gboolean |
| activate_action (GFdoNotificationBackend *backend, |
| const gchar *name, |
| GVariant *parameter) |
| { |
| GNotificationBackend *g_backend = G_NOTIFICATION_BACKEND (backend); |
| |
| /* Callers should not provide a floating variant here */ |
| g_assert (parameter == NULL || !g_variant_is_floating (parameter)); |
| |
| if (name != NULL && |
| g_str_has_prefix (name, "app.")) |
| { |
| const GVariantType *parameter_type = NULL; |
| const gchar *action_name = name + strlen ("app."); |
| |
| /* @name and @parameter come as untrusted input over D-Bus, so validate them first */ |
| if (g_action_group_query_action (G_ACTION_GROUP (g_backend->application), |
| action_name, NULL, ¶meter_type, |
| NULL, NULL, NULL) && |
| ((parameter_type == NULL && parameter == NULL) || |
| (parameter_type != NULL && parameter != NULL && g_variant_is_of_type (parameter, parameter_type)))) |
| { |
| g_action_group_activate_action (G_ACTION_GROUP (g_backend->application), action_name, parameter); |
| return TRUE; |
| } |
| } |
| else if (name == NULL) |
| { |
| g_application_activate (g_backend->application); |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| static void |
| notify_signal (GDBusConnection *connection, |
| const gchar *sender_name, |
| const gchar *object_path, |
| const gchar *interface_name, |
| const gchar *signal_name, |
| GVariant *parameters, |
| gpointer user_data) |
| { |
| GFdoNotificationBackend *backend = user_data; |
| guint32 id = 0; |
| const gchar *action = NULL; |
| FreedesktopNotification *n; |
| gboolean notification_closed = TRUE; |
| |
| if (g_str_equal (signal_name, "NotificationClosed") && |
| g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) |
| { |
| g_variant_get (parameters, "(uu)", &id, NULL); |
| } |
| else if (g_str_equal (signal_name, "ActionInvoked") && |
| g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(us)"))) |
| { |
| g_variant_get (parameters, "(u&s)", &id, &action); |
| } |
| else |
| return; |
| |
| n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id); |
| if (n == NULL) |
| return; |
| |
| if (action) |
| { |
| if (g_str_equal (action, "default")) |
| { |
| if (!activate_action (backend, n->default_action, n->default_action_target)) |
| notification_closed = FALSE; |
| } |
| else |
| { |
| gchar *name = NULL; |
| GVariant *target = NULL; |
| |
| if (!g_action_parse_detailed_name (action, &name, &target, NULL) || |
| !activate_action (backend, name, target)) |
| notification_closed = FALSE; |
| |
| g_free (name); |
| g_clear_pointer (&target, g_variant_unref); |
| } |
| } |
| |
| /* Remove the notification, as it’s either been explicitly closed |
| * (`NotificationClosed` signal) or has been closed as a result of activating |
| * an action successfully. GLib doesn’t currently support the `resident` hint |
| * on notifications which would allow them to stay around after having an |
| * action invoked on them (see |
| * https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#idm45877717456448) |
| * |
| * First, get the notification again in case the action redrew it */ |
| if (notification_closed) |
| { |
| n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id); |
| if (n != NULL) |
| { |
| backend->notifications = g_slist_remove (backend->notifications, n); |
| freedesktop_notification_free (n); |
| } |
| } |
| } |
| |
| static void |
| name_vanished_handler_cb (GDBusConnection *connection, |
| const gchar *name, |
| gpointer user_data) |
| { |
| GFdoNotificationBackend *backend = user_data; |
| |
| if (backend->notifications) |
| { |
| g_slist_free_full (backend->notifications, freedesktop_notification_free); |
| backend->notifications = NULL; |
| } |
| } |
| |
| /* Converts a GNotificationPriority to an urgency level as defined by |
| * the freedesktop spec (0: low, 1: normal, 2: critical). |
| */ |
| static guchar |
| urgency_from_priority (GNotificationPriority priority) |
| { |
| switch (priority) |
| { |
| case G_NOTIFICATION_PRIORITY_LOW: |
| return 0; |
| |
| default: |
| case G_NOTIFICATION_PRIORITY_NORMAL: |
| case G_NOTIFICATION_PRIORITY_HIGH: |
| return 1; |
| |
| case G_NOTIFICATION_PRIORITY_URGENT: |
| return 2; |
| } |
| } |
| |
| static void |
| call_notify (GDBusConnection *con, |
| GApplication *app, |
| guint32 replace_id, |
| GNotification *notification, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| GVariantBuilder action_builder; |
| guint n_buttons; |
| guint i; |
| GVariantBuilder hints_builder; |
| GIcon *icon; |
| GVariant *parameters; |
| const gchar *app_name; |
| const gchar *body; |
| guchar urgency; |
| |
| g_variant_builder_init (&action_builder, G_VARIANT_TYPE_STRING_ARRAY); |
| if (g_notification_get_default_action (notification, NULL, NULL)) |
| { |
| g_variant_builder_add (&action_builder, "s", "default"); |
| g_variant_builder_add (&action_builder, "s", ""); |
| } |
| |
| n_buttons = g_notification_get_n_buttons (notification); |
| for (i = 0; i < n_buttons; i++) |
| { |
| gchar *label; |
| gchar *action; |
| GVariant *target; |
| gchar *detailed_name; |
| |
| g_notification_get_button (notification, i, &label, &action, &target); |
| detailed_name = g_action_print_detailed_name (action, target); |
| |
| /* Actions named 'default' collide with libnotify's naming of the |
| * default action. Rewriting them to something unique is enough, |
| * because those actions can never be activated (they aren't |
| * prefixed with 'app.'). |
| */ |
| if (g_str_equal (detailed_name, "default")) |
| { |
| g_free (detailed_name); |
| detailed_name = g_dbus_generate_guid (); |
| } |
| |
| g_variant_builder_add_value (&action_builder, g_variant_new_take_string (detailed_name)); |
| g_variant_builder_add_value (&action_builder, g_variant_new_take_string (label)); |
| |
| g_free (action); |
| if (target) |
| g_variant_unref (target); |
| } |
| |
| g_variant_builder_init (&hints_builder, G_VARIANT_TYPE ("a{sv}")); |
| g_variant_builder_add (&hints_builder, "{sv}", "desktop-entry", |
| g_variant_new_string (g_application_get_application_id (app))); |
| urgency = urgency_from_priority (g_notification_get_priority (notification)); |
| g_variant_builder_add (&hints_builder, "{sv}", "urgency", g_variant_new_byte (urgency)); |
| if (g_notification_get_category (notification)) |
| { |
| g_variant_builder_add (&hints_builder, "{sv}", "category", |
| g_variant_new_string (g_notification_get_category (notification))); |
| } |
| |
| icon = g_notification_get_icon (notification); |
| if (icon != NULL) |
| { |
| if (G_IS_FILE_ICON (icon)) |
| { |
| GFile *file; |
| |
| file = g_file_icon_get_file (G_FILE_ICON (icon)); |
| g_variant_builder_add (&hints_builder, "{sv}", "image-path", |
| g_variant_new_take_string (g_file_get_path (file))); |
| } |
| else if (G_IS_THEMED_ICON (icon)) |
| { |
| const gchar* const* icon_names = g_themed_icon_get_names(G_THEMED_ICON (icon)); |
| /* Take first name from GThemedIcon */ |
| g_variant_builder_add (&hints_builder, "{sv}", "image-path", |
| g_variant_new_string (icon_names[0])); |
| } |
| } |
| |
| app_name = g_get_application_name (); |
| body = g_notification_get_body (notification); |
| |
| parameters = g_variant_new ("(susssasa{sv}i)", |
| app_name ? app_name : "", |
| replace_id, |
| "", /* app icon */ |
| g_notification_get_title (notification), |
| body ? body : "", |
| &action_builder, |
| &hints_builder, |
| -1); /* expire_timeout */ |
| |
| g_dbus_connection_call (con, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", |
| "org.freedesktop.Notifications", "Notify", |
| parameters, G_VARIANT_TYPE ("(u)"), |
| G_DBUS_CALL_FLAGS_NONE, -1, NULL, |
| callback, user_data); |
| } |
| |
| static void |
| notification_sent (GObject *source_object, |
| GAsyncResult *result, |
| gpointer user_data) |
| { |
| FreedesktopNotification *n = user_data; |
| GVariant *val; |
| GError *error = NULL; |
| static gboolean warning_printed = FALSE; |
| |
| val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &error); |
| if (val) |
| { |
| GFdoNotificationBackend *backend = n->backend; |
| FreedesktopNotification *match; |
| |
| g_variant_get (val, "(u)", &n->notify_id); |
| g_variant_unref (val); |
| |
| match = g_fdo_notification_backend_find_notification_by_notify_id (backend, n->notify_id); |
| if (match != NULL) |
| { |
| backend->notifications = g_slist_remove (backend->notifications, match); |
| freedesktop_notification_free (match); |
| } |
| backend->notifications = g_slist_prepend (backend->notifications, n); |
| } |
| else |
| { |
| if (!warning_printed) |
| { |
| g_warning ("unable to send notifications through org.freedesktop.Notifications: %s", |
| error->message); |
| warning_printed = TRUE; |
| } |
| |
| freedesktop_notification_free (n); |
| g_error_free (error); |
| } |
| } |
| |
| static void |
| g_fdo_notification_backend_dispose (GObject *object) |
| { |
| GFdoNotificationBackend *backend = G_FDO_NOTIFICATION_BACKEND (object); |
| |
| if (backend->bus_name_id) |
| { |
| g_bus_unwatch_name (backend->bus_name_id); |
| backend->bus_name_id = 0; |
| } |
| |
| if (backend->notify_subscription) |
| { |
| GDBusConnection *session_bus; |
| |
| session_bus = G_NOTIFICATION_BACKEND (backend)->dbus_connection; |
| g_dbus_connection_signal_unsubscribe (session_bus, backend->notify_subscription); |
| backend->notify_subscription = 0; |
| } |
| |
| if (backend->notifications) |
| { |
| g_slist_free_full (backend->notifications, freedesktop_notification_free); |
| backend->notifications = NULL; |
| } |
| |
| G_OBJECT_CLASS (g_fdo_notification_backend_parent_class)->dispose (object); |
| } |
| |
| static gboolean |
| g_fdo_notification_backend_is_supported (void) |
| { |
| /* This is the fallback backend with the lowest priority. To avoid an |
| * unnecessary synchronous dbus call to check for |
| * org.freedesktop.Notifications, this function always succeeds. A |
| * warning will be printed when sending the first notification fails. |
| */ |
| return TRUE; |
| } |
| |
| static void |
| g_fdo_notification_backend_send_notification (GNotificationBackend *backend, |
| const gchar *id, |
| GNotification *notification) |
| { |
| GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend); |
| FreedesktopNotification *n, *tmp; |
| |
| if (self->bus_name_id == 0) |
| { |
| self->bus_name_id = g_bus_watch_name_on_connection (backend->dbus_connection, |
| "org.freedesktop.Notifications", |
| G_BUS_NAME_WATCHER_FLAGS_NONE, |
| NULL, |
| name_vanished_handler_cb, |
| backend, |
| NULL); |
| } |
| |
| if (self->notify_subscription == 0) |
| { |
| self->notify_subscription = |
| g_dbus_connection_signal_subscribe (backend->dbus_connection, |
| "org.freedesktop.Notifications", |
| "org.freedesktop.Notifications", NULL, |
| "/org/freedesktop/Notifications", NULL, |
| G_DBUS_SIGNAL_FLAGS_NONE, |
| notify_signal, backend, NULL); |
| } |
| |
| n = freedesktop_notification_new (self, id, notification); |
| |
| tmp = g_fdo_notification_backend_find_notification (self, id); |
| if (tmp) |
| n->notify_id = tmp->notify_id; |
| |
| call_notify (backend->dbus_connection, backend->application, n->notify_id, notification, notification_sent, n); |
| } |
| |
| static void |
| g_fdo_notification_backend_withdraw_notification (GNotificationBackend *backend, |
| const gchar *id) |
| { |
| GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend); |
| FreedesktopNotification *n; |
| |
| n = g_fdo_notification_backend_find_notification (self, id); |
| if (n) |
| { |
| if (n->notify_id > 0) |
| { |
| g_dbus_connection_call (backend->dbus_connection, |
| "org.freedesktop.Notifications", |
| "/org/freedesktop/Notifications", |
| "org.freedesktop.Notifications", "CloseNotification", |
| g_variant_new ("(u)", n->notify_id), NULL, |
| G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); |
| } |
| |
| self->notifications = g_slist_remove (self->notifications, n); |
| freedesktop_notification_free (n); |
| } |
| } |
| |
| static void |
| g_fdo_notification_backend_init (GFdoNotificationBackend *backend) |
| { |
| } |
| |
| static void |
| g_fdo_notification_backend_class_init (GFdoNotificationBackendClass *class) |
| { |
| GObjectClass *object_class = G_OBJECT_CLASS (class); |
| GNotificationBackendClass *backend_class = G_NOTIFICATION_BACKEND_CLASS (class); |
| |
| object_class->dispose = g_fdo_notification_backend_dispose; |
| |
| backend_class->is_supported = g_fdo_notification_backend_is_supported; |
| backend_class->send_notification = g_fdo_notification_backend_send_notification; |
| backend_class->withdraw_notification = g_fdo_notification_backend_withdraw_notification; |
| } |