Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-fdo-notifications"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gdk-pixbuf/gdk-pixbuf.h>
9 : : #include <gio/gio.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-fdo-notifications.h"
13 : :
14 : :
15 : : struct _ValentFdoNotifications
16 : : {
17 : : ValentNotificationsAdapter parent_instance;
18 : :
19 : : GDBusInterfaceVTable vtable;
20 : : GDBusNodeInfo *node_info;
21 : : GDBusInterfaceInfo *iface_info;
22 : : GDBusConnection *monitor;
23 : : unsigned int monitor_id;
24 : : char *name_owner;
25 : : unsigned int name_owner_id;
26 : : GDBusConnection *session;
27 : : unsigned int closed_id;
28 : : };
29 : :
30 : : static void g_async_initable_iface_init (GAsyncInitableIface *iface);
31 : :
32 [ + + + - ]: 9 : G_DEFINE_FINAL_TYPE_WITH_CODE (ValentFdoNotifications, valent_fdo_notifications, VALENT_TYPE_NOTIFICATIONS_ADAPTER,
33 : : G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, g_async_initable_iface_init))
34 : :
35 : :
36 : : /*
37 : : * Map of notification-spec urgency to GNotificationPriority
38 : : *
39 : : * See: https://developer-old.gnome.org/notification-spec/#urgency-levels
40 : : */
41 : : static const unsigned int urgencies[] = {
42 : : G_NOTIFICATION_PRIORITY_LOW,
43 : : G_NOTIFICATION_PRIORITY_NORMAL,
44 : : G_NOTIFICATION_PRIORITY_URGENT
45 : : };
46 : :
47 : :
48 : : /*
49 : : * GDBusInterfaceSkeleton
50 : : */
51 : : static const char interface_xml[] =
52 : : "<node>"
53 : : " <interface name='org.freedesktop.Notifications'>"
54 : : " <method name='Notify'>"
55 : : " <arg name='appName' type='s' direction='in'/>"
56 : : " <arg name='replacesId' type='u' direction='in'/>"
57 : : " <arg name='iconName' type='s' direction='in'/>"
58 : : " <arg name='summary' type='s' direction='in'/>"
59 : : " <arg name='body' type='s' direction='in'/>"
60 : : " <arg name='actions' type='as' direction='in'/>"
61 : : " <arg name='hints' type='a{sv}' direction='in'/>"
62 : : " <arg name='timeout' type='i' direction='in'/>"
63 : : " </method>"
64 : : " </interface>"
65 : : "</node>";
66 : :
67 : : static const char *interface_matches[] = {
68 : : "interface='org.freedesktop.Notifications',member='Notify',type='method_call'",
69 : : NULL
70 : : };
71 : :
72 : :
73 : : static GIcon *
74 : 1 : _g_icon_new_for_variant (GVariant *image_data)
75 : : {
76 : 1 : GdkPixbuf *pixbuf;
77 : 1 : int32_t width, height, rowstride;
78 : 1 : gboolean has_alpha;
79 : 1 : int32_t bits_per_sample, n_channels;
80 : 2 : g_autoptr (GVariant) data_variant = NULL;
81 : 1 : unsigned char *data = NULL;
82 : 1 : size_t data_len = 0;
83 : 1 : size_t expected_len = 0;
84 : :
85 : 1 : g_variant_get (image_data, "(iiibii@ay)",
86 : : &width,
87 : : &height,
88 : : &rowstride,
89 : : &has_alpha,
90 : : &bits_per_sample,
91 : : &n_channels,
92 : : &data_variant);
93 : :
94 : 1 : data_len = g_variant_get_size (data_variant);
95 : 1 : expected_len = (height - 1) * rowstride + width
96 : 1 : * ((n_channels * bits_per_sample + 7) / 8);
97 : :
98 [ - + ]: 1 : if (expected_len != data_len)
99 : : {
100 : 0 : g_warning ("Expected image data to be of length %" G_GSIZE_FORMAT
101 : : " but got a length of %" G_GSIZE_FORMAT,
102 : : expected_len,
103 : : data_len);
104 : 0 : return NULL;
105 : : }
106 : :
107 : 1 : data = g_memdup2 (g_variant_get_data (data_variant), data_len);
108 : 1 : pixbuf = gdk_pixbuf_new_from_data (data,
109 : : GDK_COLORSPACE_RGB,
110 : : has_alpha,
111 : : bits_per_sample,
112 : : width,
113 : : height,
114 : : rowstride,
115 : : (GdkPixbufDestroyNotify)(GCallback)g_free,
116 : : NULL);
117 : :
118 : 1 : return (GIcon *)pixbuf;
119 : : }
120 : :
121 : : static void
122 : 1 : _notification_closed (ValentNotificationsAdapter *adapter,
123 : : GVariant *parameters)
124 : : {
125 : 1 : uint32_t id, reason;
126 : 2 : g_autofree char *id_str = NULL;
127 : :
128 : 1 : g_variant_get (parameters, "(uu)", &id, &reason);
129 : :
130 : 1 : id_str = g_strdup_printf ("%u", id);
131 : 1 : valent_notifications_adapter_notification_removed (adapter, id_str);
132 : 1 : }
133 : :
134 : : static void
135 : 2 : _notify (ValentNotificationsAdapter *adapter,
136 : : GVariant *parameters)
137 : : {
138 : 4 : g_autoptr (ValentNotification) notification = NULL;
139 [ + - ]: 2 : g_autoptr (GIcon) icon = NULL;
140 : :
141 : 2 : const char *app_name;
142 : 2 : uint32_t replaces_id;
143 : 2 : const char *app_icon;
144 : 2 : const char *summary;
145 : 2 : const char *body;
146 [ + - ]: 2 : g_autoptr (GVariant) actions = NULL;
147 [ + - ]: 2 : g_autoptr (GVariant) hints = NULL;
148 : 2 : int32_t expire_timeout;
149 : :
150 [ + - ]: 2 : g_autofree char *replaces_id_str = NULL;
151 : 2 : g_autoptr (GVariant) image_data = NULL;
152 : 2 : const char *image_path;
153 : 2 : unsigned char urgency;
154 : :
155 : : /* Extract what we need from the parameters */
156 : 2 : g_variant_get (parameters, "(&su&s&s&s@as@a{sv}i)",
157 : : &app_name,
158 : : &replaces_id,
159 : : &app_icon,
160 : : &summary,
161 : : &body,
162 : : &actions,
163 : : &hints,
164 : : &expire_timeout);
165 : :
166 : 2 : replaces_id_str = g_strdup_printf ("%u", replaces_id);
167 : :
168 : : /* Deserialize GNotification into ValentNotification */
169 : 2 : notification = valent_notification_new (NULL);
170 : 2 : valent_notification_set_id (notification, replaces_id_str);
171 : 2 : valent_notification_set_application (notification, app_name);
172 : 2 : valent_resource_set_title (VALENT_RESOURCE (notification), summary);
173 : 2 : valent_notification_set_body (notification, body);
174 : :
175 : : /* This bizarre ordering is required by the specification.
176 : : * See: https://developer-old.gnome.org/notification-spec/#icons-and-images
177 : : */
178 [ + + - + ]: 3 : if (g_variant_lookup (hints, "image-data", "@(iiibiiay)", &image_data) ||
179 : 1 : g_variant_lookup (hints, "image_data", "@(iiibiiay)", &image_data))
180 : : {
181 : 1 : icon = _g_icon_new_for_variant (image_data);
182 : 1 : valent_notification_set_icon (notification, icon);
183 : : }
184 [ + - - + ]: 2 : else if (g_variant_lookup (hints, "image-path", "&s", &image_path) ||
185 : 1 : g_variant_lookup (hints, "image_path", "&s", &image_path))
186 : : {
187 : 0 : icon = g_icon_new_for_string (image_path, NULL);
188 : 0 : valent_notification_set_icon (notification, icon);
189 : : }
190 [ + - ]: 1 : else if (*app_icon != '\0')
191 : : {
192 : 1 : icon = g_icon_new_for_string (app_icon, NULL);
193 : 1 : valent_notification_set_icon (notification, icon);
194 : : }
195 [ # # ]: 0 : else if (g_variant_lookup (hints, "icon_data", "@(iiibiiay)", &image_data))
196 : : {
197 : 0 : icon = _g_icon_new_for_variant (image_data);
198 : 0 : valent_notification_set_icon (notification, icon);
199 : : }
200 : :
201 : : /* Map libnotify urgency to GNotification priority */
202 [ + - + - ]: 2 : if (g_variant_lookup (hints, "urgency", "y", &urgency) && urgency < G_N_ELEMENTS (urgencies))
203 : 2 : valent_notification_set_priority (notification, urgencies[urgency]);
204 : : else
205 : 0 : valent_notification_set_priority (notification, G_NOTIFICATION_PRIORITY_NORMAL);
206 : :
207 : : /* Set a timestamp */
208 : 2 : valent_notification_set_time (notification, valent_timestamp_ms ());
209 : :
210 [ + + ]: 2 : valent_notifications_adapter_notification_added (adapter, notification);
211 : 2 : }
212 : :
213 : : static void
214 : 2 : valent_fdo_notifications_method_call (GDBusConnection *connection,
215 : : const char *sender,
216 : : const char *object_path,
217 : : const char *interface_name,
218 : : const char *method_name,
219 : : GVariant *parameters,
220 : : GDBusMethodInvocation *invocation,
221 : : gpointer user_data)
222 : : {
223 : 2 : ValentNotificationsAdapter *adapter = VALENT_NOTIFICATIONS_ADAPTER (user_data);
224 : 2 : ValentFdoNotifications *self = VALENT_FDO_NOTIFICATIONS (user_data);
225 : 2 : GDBusMessage *message;
226 : 2 : const char *destination;
227 : :
228 [ + - ]: 2 : g_assert (VALENT_IS_NOTIFICATIONS_ADAPTER (adapter));
229 [ - + ]: 2 : g_assert (VALENT_IS_FDO_NOTIFICATIONS (self));
230 : :
231 : 2 : message = g_dbus_method_invocation_get_message (invocation);
232 : 2 : destination = g_dbus_message_get_destination (message);
233 : :
234 : : // TODO: accepting notifications from the well-known name causes duplicates on
235 : : // GNOME Shell where a proxy daemon is run.
236 [ - + - - ]: 2 : if (g_strcmp0 ("org.freedesktop.Notifications", destination) != 0 &&
237 : 0 : g_strcmp0 (self->name_owner, destination) != 0)
238 : 0 : goto out;
239 : :
240 [ - + ]: 2 : if (g_strcmp0 (method_name, "Notify") == 0)
241 : 2 : _notify (adapter, parameters);
242 : :
243 : 0 : out:
244 : 2 : g_object_unref (invocation);
245 : 2 : }
246 : :
247 : : static void
248 : 1 : on_notification_closed (GDBusConnection *connection,
249 : : const char *sender_name,
250 : : const char *object_path,
251 : : const char *interface_name,
252 : : const char *signal_name,
253 : : GVariant *parameters,
254 : : gpointer user_data)
255 : : {
256 : 1 : ValentNotificationsAdapter *adapter = VALENT_NOTIFICATIONS_ADAPTER (user_data);
257 : :
258 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATIONS_ADAPTER (adapter));
259 [ - + ]: 1 : g_assert (g_strcmp0 (signal_name, "NotificationClosed") == 0);
260 : :
261 : 1 : _notification_closed (adapter, parameters);
262 : 1 : }
263 : :
264 : : /*
265 : : * Setup
266 : : */
267 : : static void
268 : 1 : on_name_appeared (GDBusConnection *connection,
269 : : const char *name,
270 : : const char *name_owner,
271 : : gpointer user_data)
272 : : {
273 : 1 : ValentFdoNotifications *self = VALENT_FDO_NOTIFICATIONS (user_data);
274 : :
275 [ - + ]: 1 : self->name_owner = g_strdup (name_owner);
276 : 1 : g_set_object (&self->session, connection);
277 : :
278 [ + - ]: 1 : if (self->closed_id == 0)
279 : : {
280 : 1 : self->closed_id =
281 : 1 : g_dbus_connection_signal_subscribe (self->session,
282 : : "org.freedesktop.Notifications",
283 : : "org.freedesktop.Notifications",
284 : : "NotificationClosed",
285 : : "/org/freedesktop/Notifications",
286 : : NULL,
287 : : G_DBUS_SIGNAL_FLAGS_NONE,
288 : : on_notification_closed,
289 : : self, NULL);
290 : : }
291 : :
292 : 1 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
293 : : VALENT_PLUGIN_STATE_ACTIVE,
294 : : NULL);
295 : 1 : }
296 : :
297 : : static void
298 : 0 : on_name_vanished (GDBusConnection *connection,
299 : : const char *name,
300 : : gpointer user_data)
301 : : {
302 : 0 : ValentFdoNotifications *self = VALENT_FDO_NOTIFICATIONS (user_data);
303 : :
304 [ # # ]: 0 : g_clear_pointer (&self->name_owner, g_free);
305 : :
306 [ # # ]: 0 : if (self->closed_id > 0)
307 : : {
308 : 0 : g_dbus_connection_signal_unsubscribe (self->session, self->closed_id);
309 : 0 : self->closed_id = 0;
310 : : }
311 : :
312 : 0 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
313 : : VALENT_PLUGIN_STATE_INACTIVE,
314 : : NULL);
315 : 0 : }
316 : :
317 : : static void
318 : 1 : become_monitor_cb (GDBusConnection *connection,
319 : : GAsyncResult *result,
320 : : gpointer user_data)
321 : : {
322 : 1 : g_autoptr (GTask) task = G_TASK (user_data);
323 : 1 : ValentFdoNotifications *self = g_task_get_source_object (task);
324 [ - - + - ]: 1 : g_autoptr (GVariant) reply = NULL;
325 [ - - ]: 1 : g_autoptr (GError) error = NULL;
326 : :
327 : 1 : reply = g_dbus_connection_call_finish (connection, result, &error);
328 : :
329 [ - + ]: 1 : if (reply == NULL)
330 : : {
331 : 0 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
332 : : VALENT_PLUGIN_STATE_ERROR,
333 : : error);
334 [ # # ]: 0 : g_clear_object (&self->monitor);
335 : 0 : g_dbus_error_strip_remote_error (error);
336 [ # # ]: 0 : return g_task_return_error (task, g_steal_pointer (&error));
337 : : }
338 : :
339 : : /* Watch the true name owner */
340 : 1 : self->name_owner_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
341 : : "org.freedesktop.Notifications",
342 : : G_BUS_NAME_WATCHER_FLAGS_NONE,
343 : : on_name_appeared,
344 : : on_name_vanished,
345 : : self, NULL);
346 : :
347 : :
348 : : /* Report the adapter as active */
349 : 1 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
350 : : VALENT_PLUGIN_STATE_ACTIVE,
351 : : NULL);
352 [ - + ]: 1 : g_task_return_boolean (task, TRUE);
353 : : }
354 : :
355 : : static void
356 : 1 : new_for_address_cb (GObject *object,
357 : : GAsyncResult *result,
358 : : gpointer user_data)
359 : : {
360 : 1 : g_autoptr (GTask) task = G_TASK (user_data);
361 : 1 : ValentFdoNotifications *self = g_task_get_source_object (task);
362 : 1 : GCancellable *cancellable = g_task_get_cancellable (task);
363 [ - - ]: 1 : g_autoptr (GError) error = NULL;
364 : :
365 : 1 : self->monitor = g_dbus_connection_new_for_address_finish (result, &error);
366 : :
367 [ - + ]: 1 : if (self->monitor == NULL)
368 : : {
369 : 0 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
370 : : VALENT_PLUGIN_STATE_ERROR,
371 : : error);
372 : 0 : g_dbus_error_strip_remote_error (error);
373 : 0 : return g_task_return_error (task, g_steal_pointer (&error));
374 : : }
375 : :
376 : : /* Export the monitor interface */
377 : 2 : self->monitor_id =
378 : 1 : g_dbus_connection_register_object (self->monitor,
379 : : "/org/freedesktop/Notifications",
380 : : self->iface_info,
381 : 1 : &self->vtable,
382 : : self, NULL,
383 : : &error);
384 : :
385 [ - + ]: 1 : if (self->monitor_id == 0)
386 : : {
387 : 0 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
388 : : VALENT_PLUGIN_STATE_ERROR,
389 : : error);
390 [ # # ]: 0 : g_clear_object (&self->monitor);
391 : 0 : g_dbus_error_strip_remote_error (error);
392 : 0 : return g_task_return_error (task, g_steal_pointer (&error));
393 : : }
394 : :
395 : : /* Become a monitor for notifications */
396 [ - + ]: 1 : g_dbus_connection_call (self->monitor,
397 : : "org.freedesktop.DBus",
398 : : "/org/freedesktop/DBus",
399 : : "org.freedesktop.DBus.Monitoring",
400 : : "BecomeMonitor",
401 : : g_variant_new ("(^asu)", interface_matches, 0),
402 : : NULL,
403 : : G_DBUS_CALL_FLAGS_NONE,
404 : : -1,
405 : : cancellable,
406 : : (GAsyncReadyCallback)become_monitor_cb,
407 : : g_steal_pointer (&task));
408 : : }
409 : :
410 : :
411 : : /*
412 : : * GAsyncInitable
413 : : */
414 : : static void
415 : 1 : valent_fdo_notifications_init_async (GAsyncInitable *initable,
416 : : int io_priority,
417 : : GCancellable *cancellable,
418 : : GAsyncReadyCallback callback,
419 : : gpointer user_data)
420 : : {
421 : 1 : g_autoptr (GTask) task = NULL;
422 [ - - ]: 1 : g_autoptr (GCancellable) destroy = NULL;
423 [ - - + - ]: 1 : g_autofree char *address = NULL;
424 : 1 : g_autoptr (GError) error = NULL;
425 : :
426 [ + - ]: 1 : g_assert (VALENT_IS_FDO_NOTIFICATIONS (initable));
427 : :
428 : : /* Cancel initialization if the object is destroyed */
429 : 1 : destroy = valent_object_chain_cancellable (VALENT_OBJECT (initable),
430 : : cancellable);
431 : :
432 : 1 : task = g_task_new (initable, destroy, callback, user_data);
433 : 1 : g_task_set_priority (task, io_priority);
434 [ + - ]: 1 : g_task_set_source_tag (task, valent_fdo_notifications_init_async);
435 : :
436 : : /* Get a bus address */
437 : 1 : address = g_dbus_address_get_for_bus_sync (G_BUS_TYPE_SESSION,
438 : : destroy,
439 : : &error);
440 : :
441 [ - + ]: 1 : if (address == NULL)
442 : : {
443 : 0 : valent_extension_plugin_state_changed (VALENT_EXTENSION (initable),
444 : : VALENT_PLUGIN_STATE_ERROR,
445 : : error);
446 [ # # ]: 0 : return g_task_return_error (task, g_steal_pointer (&error));
447 : : }
448 : :
449 : : /* Get a dedicated connection for monitoring */
450 [ - + ]: 1 : g_dbus_connection_new_for_address (address,
451 : : G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
452 : : G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
453 : : NULL,
454 : : destroy,
455 : : (GAsyncReadyCallback)new_for_address_cb,
456 : : g_steal_pointer (&task));
457 : : }
458 : :
459 : : static void
460 : 2 : g_async_initable_iface_init (GAsyncInitableIface *iface)
461 : : {
462 : 2 : iface->init_async = valent_fdo_notifications_init_async;
463 : 2 : }
464 : :
465 : : /*
466 : : * ValentObject
467 : : */
468 : : static void
469 : 2 : valent_fdo_notifications_destroy (ValentObject *object)
470 : : {
471 : 2 : ValentFdoNotifications *self = VALENT_FDO_NOTIFICATIONS (object);
472 : :
473 [ + + ]: 2 : if (self->closed_id > 0)
474 : : {
475 : 1 : g_dbus_connection_signal_unsubscribe (self->session, self->closed_id);
476 : 1 : self->closed_id = 0;
477 : : }
478 : :
479 [ + + ]: 2 : if (self->name_owner_id > 0)
480 : : {
481 : 1 : g_clear_handle_id (&self->name_owner_id, g_bus_unwatch_name);
482 [ + - ]: 1 : g_clear_pointer (&self->name_owner, g_free);
483 : : }
484 : :
485 [ + + ]: 2 : if (self->monitor_id != 0)
486 : : {
487 : 1 : g_dbus_connection_unregister_object (self->monitor, self->monitor_id);
488 : 1 : self->monitor_id = 0;
489 : : }
490 : :
491 [ + + ]: 2 : g_clear_object (&self->monitor);
492 [ + + ]: 2 : g_clear_object (&self->session);
493 : :
494 : 2 : VALENT_OBJECT_CLASS (valent_fdo_notifications_parent_class)->destroy (object);
495 : 2 : }
496 : :
497 : : /*
498 : : * GObject
499 : : */
500 : : static void
501 : 1 : valent_fdo_notifications_finalize (GObject *object)
502 : : {
503 : 1 : ValentFdoNotifications *self = VALENT_FDO_NOTIFICATIONS (object);
504 : :
505 [ + - ]: 1 : g_clear_pointer (&self->node_info, g_dbus_node_info_unref);
506 : :
507 : 1 : G_OBJECT_CLASS (valent_fdo_notifications_parent_class)->finalize (object);
508 : 1 : }
509 : :
510 : : static void
511 : 2 : valent_fdo_notifications_class_init (ValentFdoNotificationsClass *klass)
512 : : {
513 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
514 : 2 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
515 : :
516 : 2 : object_class->finalize = valent_fdo_notifications_finalize;
517 : :
518 : 2 : vobject_class->destroy = valent_fdo_notifications_destroy;
519 : : }
520 : :
521 : : static void
522 : 1 : valent_fdo_notifications_init (ValentFdoNotifications *self)
523 : : {
524 : 1 : self->node_info = g_dbus_node_info_new_for_xml (interface_xml, NULL);
525 : 1 : self->iface_info = self->node_info->interfaces[0];
526 : :
527 : 1 : self->vtable.method_call = valent_fdo_notifications_method_call;
528 : 1 : self->vtable.get_property = NULL;
529 : 1 : self->vtable.set_property = NULL;
530 : 1 : }
531 : :
|