1 | /* |
2 | * Copyright © 2013 Lars Uebernickel |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General |
15 | * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Authors: Lars Uebernickel <lars@uebernic.de> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gnotificationbackend.h" |
23 | |
24 | #include "gapplication.h" |
25 | #include "giomodule-priv.h" |
26 | #include "gnotification-private.h" |
27 | #include "gdbusconnection.h" |
28 | #include "gdbusnamewatching.h" |
29 | #include "gactiongroup.h" |
30 | #include "gaction.h" |
31 | #include "gthemedicon.h" |
32 | #include "gfileicon.h" |
33 | #include "gfile.h" |
34 | #include "gdbusutils.h" |
35 | |
36 | #define G_TYPE_FDO_NOTIFICATION_BACKEND (g_fdo_notification_backend_get_type ()) |
37 | #define G_FDO_NOTIFICATION_BACKEND(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_FDO_NOTIFICATION_BACKEND, GFdoNotificationBackend)) |
38 | |
39 | typedef struct _GFdoNotificationBackend GFdoNotificationBackend; |
40 | typedef GNotificationBackendClass GFdoNotificationBackendClass; |
41 | |
42 | struct _GFdoNotificationBackend |
43 | { |
44 | GNotificationBackend parent; |
45 | |
46 | guint bus_name_id; |
47 | |
48 | guint notify_subscription; |
49 | GSList *notifications; |
50 | }; |
51 | |
52 | GType g_fdo_notification_backend_get_type (void); |
53 | |
54 | G_DEFINE_TYPE_WITH_CODE (GFdoNotificationBackend, g_fdo_notification_backend, G_TYPE_NOTIFICATION_BACKEND, |
55 | _g_io_modules_ensure_extension_points_registered (); |
56 | g_io_extension_point_implement (G_NOTIFICATION_BACKEND_EXTENSION_POINT_NAME, |
57 | g_define_type_id, "freedesktop" , 0)) |
58 | |
59 | typedef struct |
60 | { |
61 | GFdoNotificationBackend *backend; |
62 | gchar *id; |
63 | guint32 notify_id; |
64 | gchar *default_action; |
65 | GVariant *default_action_target; |
66 | } FreedesktopNotification; |
67 | |
68 | static void |
69 | freedesktop_notification_free (gpointer data) |
70 | { |
71 | FreedesktopNotification *n = data; |
72 | |
73 | g_object_unref (object: n->backend); |
74 | g_free (mem: n->id); |
75 | g_free (mem: n->default_action); |
76 | if (n->default_action_target) |
77 | g_variant_unref (value: n->default_action_target); |
78 | |
79 | g_slice_free (FreedesktopNotification, n); |
80 | } |
81 | |
82 | static FreedesktopNotification * |
83 | freedesktop_notification_new (GFdoNotificationBackend *backend, |
84 | const gchar *id, |
85 | GNotification *notification) |
86 | { |
87 | FreedesktopNotification *n; |
88 | |
89 | n = g_slice_new0 (FreedesktopNotification); |
90 | n->backend = g_object_ref (backend); |
91 | n->id = g_strdup (str: id); |
92 | n->notify_id = 0; |
93 | g_notification_get_default_action (notification, |
94 | action: &n->default_action, |
95 | target: &n->default_action_target); |
96 | |
97 | return n; |
98 | } |
99 | |
100 | static FreedesktopNotification * |
101 | g_fdo_notification_backend_find_notification (GFdoNotificationBackend *backend, |
102 | const gchar *id) |
103 | { |
104 | GSList *it; |
105 | |
106 | for (it = backend->notifications; it != NULL; it = it->next) |
107 | { |
108 | FreedesktopNotification *n = it->data; |
109 | if (g_str_equal (v1: n->id, v2: id)) |
110 | return n; |
111 | } |
112 | |
113 | return NULL; |
114 | } |
115 | |
116 | static FreedesktopNotification * |
117 | g_fdo_notification_backend_find_notification_by_notify_id (GFdoNotificationBackend *backend, |
118 | guint32 id) |
119 | { |
120 | GSList *it; |
121 | |
122 | for (it = backend->notifications; it != NULL; it = it->next) |
123 | { |
124 | FreedesktopNotification *n = it->data; |
125 | if (n->notify_id == id) |
126 | return n; |
127 | } |
128 | |
129 | return NULL; |
130 | } |
131 | |
132 | static void |
133 | activate_action (GFdoNotificationBackend *backend, |
134 | const gchar *name, |
135 | GVariant *parameter) |
136 | { |
137 | GNotificationBackend *g_backend = G_NOTIFICATION_BACKEND (backend); |
138 | |
139 | if (name) |
140 | { |
141 | if (g_str_has_prefix (str: name, prefix: "app." )) |
142 | g_action_group_activate_action (G_ACTION_GROUP (g_backend->application), action_name: name + 4, parameter); |
143 | } |
144 | else |
145 | { |
146 | g_application_activate (application: g_backend->application); |
147 | } |
148 | } |
149 | |
150 | static void |
151 | notify_signal (GDBusConnection *connection, |
152 | const gchar *sender_name, |
153 | const gchar *object_path, |
154 | const gchar *interface_name, |
155 | const gchar *signal_name, |
156 | GVariant *parameters, |
157 | gpointer user_data) |
158 | { |
159 | GFdoNotificationBackend *backend = user_data; |
160 | guint32 id = 0; |
161 | const gchar *action = NULL; |
162 | FreedesktopNotification *n; |
163 | |
164 | if (g_str_equal (v1: signal_name, v2: "NotificationClosed" ) && |
165 | g_variant_is_of_type (value: parameters, G_VARIANT_TYPE ("(uu)" ))) |
166 | { |
167 | g_variant_get (value: parameters, format_string: "(uu)" , &id, NULL); |
168 | } |
169 | else if (g_str_equal (v1: signal_name, v2: "ActionInvoked" ) && |
170 | g_variant_is_of_type (value: parameters, G_VARIANT_TYPE ("(us)" ))) |
171 | { |
172 | g_variant_get (value: parameters, format_string: "(u&s)" , &id, &action); |
173 | } |
174 | else |
175 | return; |
176 | |
177 | n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id); |
178 | if (n == NULL) |
179 | return; |
180 | |
181 | if (action) |
182 | { |
183 | if (g_str_equal (v1: action, v2: "default" )) |
184 | { |
185 | activate_action (backend, name: n->default_action, parameter: n->default_action_target); |
186 | } |
187 | else |
188 | { |
189 | gchar *name; |
190 | GVariant *target; |
191 | |
192 | if (g_action_parse_detailed_name (detailed_name: action, action_name: &name, target_value: &target, NULL)) |
193 | { |
194 | activate_action (backend, name, parameter: target); |
195 | g_free (mem: name); |
196 | if (target) |
197 | g_variant_unref (value: target); |
198 | } |
199 | } |
200 | } |
201 | |
202 | /* Get the notification again in case the action redrew it */ |
203 | n = g_fdo_notification_backend_find_notification_by_notify_id (backend, id); |
204 | if (n != NULL) |
205 | { |
206 | backend->notifications = g_slist_remove (list: backend->notifications, data: n); |
207 | freedesktop_notification_free (data: n); |
208 | } |
209 | } |
210 | |
211 | static void |
212 | name_vanished_handler_cb (GDBusConnection *connection, |
213 | const gchar *name, |
214 | gpointer user_data) |
215 | { |
216 | GFdoNotificationBackend *backend = user_data; |
217 | |
218 | if (backend->notifications) |
219 | { |
220 | g_slist_free_full (list: backend->notifications, free_func: freedesktop_notification_free); |
221 | backend->notifications = NULL; |
222 | } |
223 | } |
224 | |
225 | /* Converts a GNotificationPriority to an urgency level as defined by |
226 | * the freedesktop spec (0: low, 1: normal, 2: critical). |
227 | */ |
228 | static guchar |
229 | urgency_from_priority (GNotificationPriority priority) |
230 | { |
231 | switch (priority) |
232 | { |
233 | case G_NOTIFICATION_PRIORITY_LOW: |
234 | return 0; |
235 | |
236 | default: |
237 | case G_NOTIFICATION_PRIORITY_NORMAL: |
238 | case G_NOTIFICATION_PRIORITY_HIGH: |
239 | return 1; |
240 | |
241 | case G_NOTIFICATION_PRIORITY_URGENT: |
242 | return 2; |
243 | } |
244 | } |
245 | |
246 | static void |
247 | call_notify (GDBusConnection *con, |
248 | GApplication *app, |
249 | guint32 replace_id, |
250 | GNotification *notification, |
251 | GAsyncReadyCallback callback, |
252 | gpointer user_data) |
253 | { |
254 | GVariantBuilder action_builder; |
255 | guint n_buttons; |
256 | guint i; |
257 | GVariantBuilder hints_builder; |
258 | GIcon *icon; |
259 | GVariant *parameters; |
260 | const gchar *body; |
261 | guchar urgency; |
262 | |
263 | g_variant_builder_init (builder: &action_builder, G_VARIANT_TYPE_STRING_ARRAY); |
264 | if (g_notification_get_default_action (notification, NULL, NULL)) |
265 | { |
266 | g_variant_builder_add (builder: &action_builder, format_string: "s" , "default" ); |
267 | g_variant_builder_add (builder: &action_builder, format_string: "s" , "" ); |
268 | } |
269 | |
270 | n_buttons = g_notification_get_n_buttons (notification); |
271 | for (i = 0; i < n_buttons; i++) |
272 | { |
273 | gchar *label; |
274 | gchar *action; |
275 | GVariant *target; |
276 | gchar *detailed_name; |
277 | |
278 | g_notification_get_button (notification, index: i, label: &label, action: &action, target: &target); |
279 | detailed_name = g_action_print_detailed_name (action_name: action, target_value: target); |
280 | |
281 | /* Actions named 'default' collide with libnotify's naming of the |
282 | * default action. Rewriting them to something unique is enough, |
283 | * because those actions can never be activated (they aren't |
284 | * prefixed with 'app.'). |
285 | */ |
286 | if (g_str_equal (v1: detailed_name, v2: "default" )) |
287 | { |
288 | g_free (mem: detailed_name); |
289 | detailed_name = g_dbus_generate_guid (); |
290 | } |
291 | |
292 | g_variant_builder_add_value (builder: &action_builder, value: g_variant_new_take_string (string: detailed_name)); |
293 | g_variant_builder_add_value (builder: &action_builder, value: g_variant_new_take_string (string: label)); |
294 | |
295 | g_free (mem: action); |
296 | if (target) |
297 | g_variant_unref (value: target); |
298 | } |
299 | |
300 | g_variant_builder_init (builder: &hints_builder, G_VARIANT_TYPE ("a{sv}" )); |
301 | g_variant_builder_add (builder: &hints_builder, format_string: "{sv}" , "desktop-entry" , |
302 | g_variant_new_string (string: g_application_get_application_id (application: app))); |
303 | urgency = urgency_from_priority (priority: g_notification_get_priority (notification)); |
304 | g_variant_builder_add (builder: &hints_builder, format_string: "{sv}" , "urgency" , g_variant_new_byte (value: urgency)); |
305 | icon = g_notification_get_icon (notification); |
306 | if (icon != NULL) |
307 | { |
308 | if (G_IS_FILE_ICON (icon)) |
309 | { |
310 | GFile *file; |
311 | |
312 | file = g_file_icon_get_file (G_FILE_ICON (icon)); |
313 | g_variant_builder_add (builder: &hints_builder, format_string: "{sv}" , "image-path" , |
314 | g_variant_new_take_string (string: g_file_get_path (file))); |
315 | } |
316 | else if (G_IS_THEMED_ICON (icon)) |
317 | { |
318 | const gchar* const* icon_names = g_themed_icon_get_names(G_THEMED_ICON (icon)); |
319 | /* Take first name from GThemedIcon */ |
320 | g_variant_builder_add (builder: &hints_builder, format_string: "{sv}" , "image-path" , |
321 | g_variant_new_string (string: icon_names[0])); |
322 | } |
323 | } |
324 | |
325 | body = g_notification_get_body (notification); |
326 | |
327 | parameters = g_variant_new (format_string: "(susssasa{sv}i)" , |
328 | "" , /* app name */ |
329 | replace_id, |
330 | "" , /* app icon */ |
331 | g_notification_get_title (notification), |
332 | body ? body : "" , |
333 | &action_builder, |
334 | &hints_builder, |
335 | -1); /* expire_timeout */ |
336 | |
337 | g_dbus_connection_call (connection: con, bus_name: "org.freedesktop.Notifications" , object_path: "/org/freedesktop/Notifications" , |
338 | interface_name: "org.freedesktop.Notifications" , method_name: "Notify" , |
339 | parameters, G_VARIANT_TYPE ("(u)" ), |
340 | flags: G_DBUS_CALL_FLAGS_NONE, timeout_msec: -1, NULL, |
341 | callback, user_data); |
342 | } |
343 | |
344 | static void |
345 | notification_sent (GObject *source_object, |
346 | GAsyncResult *result, |
347 | gpointer user_data) |
348 | { |
349 | FreedesktopNotification *n = user_data; |
350 | GVariant *val; |
351 | GError *error = NULL; |
352 | static gboolean warning_printed = FALSE; |
353 | |
354 | val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res: result, error: &error); |
355 | if (val) |
356 | { |
357 | GFdoNotificationBackend *backend = n->backend; |
358 | FreedesktopNotification *match; |
359 | |
360 | g_variant_get (value: val, format_string: "(u)" , &n->notify_id); |
361 | g_variant_unref (value: val); |
362 | |
363 | match = g_fdo_notification_backend_find_notification_by_notify_id (backend, id: n->notify_id); |
364 | if (match != NULL) |
365 | { |
366 | backend->notifications = g_slist_remove (list: backend->notifications, data: match); |
367 | freedesktop_notification_free (data: match); |
368 | } |
369 | backend->notifications = g_slist_prepend (list: backend->notifications, data: n); |
370 | } |
371 | else |
372 | { |
373 | if (!warning_printed) |
374 | { |
375 | g_warning ("unable to send notifications through org.freedesktop.Notifications: %s" , |
376 | error->message); |
377 | warning_printed = TRUE; |
378 | } |
379 | |
380 | freedesktop_notification_free (data: n); |
381 | g_error_free (error); |
382 | } |
383 | } |
384 | |
385 | static void |
386 | g_fdo_notification_backend_dispose (GObject *object) |
387 | { |
388 | GFdoNotificationBackend *backend = G_FDO_NOTIFICATION_BACKEND (object); |
389 | |
390 | if (backend->bus_name_id) |
391 | { |
392 | g_bus_unwatch_name (watcher_id: backend->bus_name_id); |
393 | backend->bus_name_id = 0; |
394 | } |
395 | |
396 | if (backend->notify_subscription) |
397 | { |
398 | GDBusConnection *session_bus; |
399 | |
400 | session_bus = G_NOTIFICATION_BACKEND (backend)->dbus_connection; |
401 | g_dbus_connection_signal_unsubscribe (connection: session_bus, subscription_id: backend->notify_subscription); |
402 | backend->notify_subscription = 0; |
403 | } |
404 | |
405 | if (backend->notifications) |
406 | { |
407 | g_slist_free_full (list: backend->notifications, free_func: freedesktop_notification_free); |
408 | backend->notifications = NULL; |
409 | } |
410 | |
411 | G_OBJECT_CLASS (g_fdo_notification_backend_parent_class)->dispose (object); |
412 | } |
413 | |
414 | static gboolean |
415 | g_fdo_notification_backend_is_supported (void) |
416 | { |
417 | /* This is the fallback backend with the lowest priority. To avoid an |
418 | * unnecessary synchronous dbus call to check for |
419 | * org.freedesktop.Notifications, this function always succeeds. A |
420 | * warning will be printed when sending the first notification fails. |
421 | */ |
422 | return TRUE; |
423 | } |
424 | |
425 | static void |
426 | g_fdo_notification_backend_send_notification (GNotificationBackend *backend, |
427 | const gchar *id, |
428 | GNotification *notification) |
429 | { |
430 | GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend); |
431 | FreedesktopNotification *n, *tmp; |
432 | |
433 | if (self->bus_name_id == 0) |
434 | { |
435 | self->bus_name_id = g_bus_watch_name_on_connection (connection: backend->dbus_connection, |
436 | name: "org.freedesktop.Notifications" , |
437 | flags: G_BUS_NAME_WATCHER_FLAGS_NONE, |
438 | NULL, |
439 | name_vanished_handler: name_vanished_handler_cb, |
440 | user_data: backend, |
441 | NULL); |
442 | } |
443 | |
444 | if (self->notify_subscription == 0) |
445 | { |
446 | self->notify_subscription = |
447 | g_dbus_connection_signal_subscribe (connection: backend->dbus_connection, |
448 | sender: "org.freedesktop.Notifications" , |
449 | interface_name: "org.freedesktop.Notifications" , NULL, |
450 | object_path: "/org/freedesktop/Notifications" , NULL, |
451 | flags: G_DBUS_SIGNAL_FLAGS_NONE, |
452 | callback: notify_signal, user_data: backend, NULL); |
453 | } |
454 | |
455 | n = freedesktop_notification_new (backend: self, id, notification); |
456 | |
457 | tmp = g_fdo_notification_backend_find_notification (backend: self, id); |
458 | if (tmp) |
459 | n->notify_id = tmp->notify_id; |
460 | |
461 | call_notify (con: backend->dbus_connection, app: backend->application, replace_id: n->notify_id, notification, callback: notification_sent, user_data: n); |
462 | } |
463 | |
464 | static void |
465 | g_fdo_notification_backend_withdraw_notification (GNotificationBackend *backend, |
466 | const gchar *id) |
467 | { |
468 | GFdoNotificationBackend *self = G_FDO_NOTIFICATION_BACKEND (backend); |
469 | FreedesktopNotification *n; |
470 | |
471 | n = g_fdo_notification_backend_find_notification (backend: self, id); |
472 | if (n) |
473 | { |
474 | if (n->notify_id > 0) |
475 | { |
476 | g_dbus_connection_call (connection: backend->dbus_connection, |
477 | bus_name: "org.freedesktop.Notifications" , |
478 | object_path: "/org/freedesktop/Notifications" , |
479 | interface_name: "org.freedesktop.Notifications" , method_name: "CloseNotification" , |
480 | parameters: g_variant_new (format_string: "(u)" , n->notify_id), NULL, |
481 | flags: G_DBUS_CALL_FLAGS_NONE, timeout_msec: -1, NULL, NULL, NULL); |
482 | } |
483 | |
484 | self->notifications = g_slist_remove (list: self->notifications, data: n); |
485 | freedesktop_notification_free (data: n); |
486 | } |
487 | } |
488 | |
489 | static void |
490 | g_fdo_notification_backend_init (GFdoNotificationBackend *backend) |
491 | { |
492 | } |
493 | |
494 | static void |
495 | g_fdo_notification_backend_class_init (GFdoNotificationBackendClass *class) |
496 | { |
497 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
498 | GNotificationBackendClass *backend_class = G_NOTIFICATION_BACKEND_CLASS (class); |
499 | |
500 | object_class->dispose = g_fdo_notification_backend_dispose; |
501 | |
502 | backend_class->is_supported = g_fdo_notification_backend_is_supported; |
503 | backend_class->send_notification = g_fdo_notification_backend_send_notification; |
504 | backend_class->withdraw_notification = g_fdo_notification_backend_withdraw_notification; |
505 | } |
506 | |