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-notification-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gtk/gtk.h>
9 : : #include <libportal/portal.h>
10 : : #include <gio/gio.h>
11 : : #include <json-glib/json-glib.h>
12 : : #include <valent.h>
13 : :
14 : : #include "valent-notification-dialog.h"
15 : : #include "valent-notification-plugin.h"
16 : : #include "valent-notification-upload.h"
17 : :
18 : : #define DEFAULT_ICON_SIZE 512
19 : :
20 : :
21 : : struct _ValentNotificationPlugin
22 : : {
23 : : ValentDevicePlugin parent_instance;
24 : :
25 : : GCancellable *cancellable;
26 : : ValentNotifications *notifications;
27 : : ValentSession *session;
28 : :
29 : : GHashTable *cache;
30 : : GHashTable *dialogs;
31 : : unsigned int notifications_watch : 1;
32 : : };
33 : :
34 [ + + + - ]: 86 : G_DEFINE_FINAL_TYPE (ValentNotificationPlugin, valent_notification_plugin, VALENT_TYPE_DEVICE_PLUGIN)
35 : :
36 : : static void valent_notification_plugin_handle_notification (ValentNotificationPlugin *self,
37 : : JsonNode *packet);
38 : : static void valent_notification_plugin_handle_notification_action (ValentNotificationPlugin *self,
39 : : JsonNode *packet);
40 : : static void valent_notification_plugin_handle_notification_reply (ValentNotificationPlugin *self,
41 : : JsonNode *packet);
42 : : static void valent_notification_plugin_handle_notification_request (ValentNotificationPlugin *self,
43 : : JsonNode *packet);
44 : :
45 : : static void valent_notification_plugin_close_notification (ValentNotificationPlugin *self,
46 : : const char *id);
47 : : static void valent_notification_plugin_request_notifications (ValentNotificationPlugin *self);
48 : : static void valent_notification_plugin_send_notification (ValentNotificationPlugin *self,
49 : : const char *id,
50 : : const char *appName,
51 : : const char *title,
52 : : const char *body,
53 : : GIcon *icon);
54 : : static void valent_notification_plugin_show_notification (ValentNotificationPlugin *self,
55 : : JsonNode *packet,
56 : : GIcon *gicon);
57 : :
58 : :
59 : : /*
60 : : * ValentNotifications Callbacks
61 : : */
62 : : static void
63 : 5 : on_notification_added (ValentNotifications *listener,
64 : : ValentNotification *notification,
65 : : ValentNotificationPlugin *self)
66 : : {
67 : 5 : GSettings *settings;
68 : 5 : const char *application;
69 : 10 : g_auto (GStrv) deny = NULL;
70 : :
71 [ + - ]: 5 : g_assert (VALENT_IS_NOTIFICATIONS (listener));
72 [ - + ]: 5 : g_assert (VALENT_IS_NOTIFICATION (notification));
73 [ - + ]: 5 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
74 : :
75 : 5 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
76 : :
77 [ + - ]: 5 : if (!g_settings_get_boolean (settings, "forward-notifications"))
78 : : return;
79 : :
80 [ - + - - ]: 5 : if (!g_settings_get_boolean (settings, "forward-when-active") &&
81 : 0 : valent_session_get_active (self->session))
82 : : return;
83 : :
84 : 5 : application = valent_notification_get_application (notification);
85 : 5 : deny = g_settings_get_strv (settings, "forward-deny");
86 : :
87 [ + + - + ]: 5 : if (application && g_strv_contains ((const char * const *)deny, application))
88 [ # # ]: 0 : return;
89 : :
90 [ + - ]: 5 : valent_notification_plugin_send_notification (self,
91 : : valent_notification_get_id (notification),
92 : : valent_notification_get_application (notification),
93 : : valent_notification_get_title (notification),
94 : : valent_notification_get_body (notification),
95 : : valent_notification_get_icon (notification));
96 : : }
97 : :
98 : : static void
99 : 1 : on_notification_removed (ValentNotifications *notifications,
100 : : const char *id,
101 : : ValentNotificationPlugin *self)
102 : : {
103 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATIONS (notifications));
104 [ - + ]: 1 : g_assert (id != NULL);
105 : :
106 : 1 : valent_notification_plugin_close_notification (self, id);
107 : 1 : }
108 : :
109 : : static void
110 : 26 : valent_notification_plugin_watch_notifications (ValentNotificationPlugin *self,
111 : : gboolean watch)
112 : : {
113 : 26 : ValentNotifications *notifications = valent_notifications_get_default ();
114 : :
115 [ + - ]: 26 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
116 : :
117 [ + + ]: 26 : if (self->notifications_watch == watch)
118 : : return;
119 : :
120 [ + + ]: 10 : if (watch)
121 : : {
122 : 5 : g_signal_connect_object (notifications,
123 : : "notification-added",
124 : : G_CALLBACK (on_notification_added),
125 : : self, 0);
126 : 5 : g_signal_connect_object (notifications,
127 : : "notification-removed",
128 : : G_CALLBACK (on_notification_removed),
129 : : self, 0);
130 : 5 : self->notifications_watch = TRUE;
131 : : }
132 : : else
133 : : {
134 : 5 : g_signal_handlers_disconnect_by_data (notifications, self);
135 : 5 : self->notifications_watch = FALSE;
136 : : }
137 : : }
138 : :
139 : : /*
140 : : * Icon Transfers
141 : : */
142 : : typedef struct
143 : : {
144 : : GRecMutex mutex;
145 : : ValentDevice *device;
146 : : JsonNode *packet;
147 : : } IconTransferData;
148 : :
149 : : static void
150 : 1 : icon_transfer_data_free (gpointer data)
151 : : {
152 : 1 : IconTransferData *transfer = (IconTransferData *)data;
153 : :
154 : 1 : g_rec_mutex_lock (&transfer->mutex);
155 [ - + ]: 1 : g_clear_object (&transfer->device);
156 [ - + ]: 1 : g_clear_pointer (&transfer->packet, json_node_unref);
157 : 1 : g_rec_mutex_unlock (&transfer->mutex);
158 : 1 : g_rec_mutex_clear (&transfer->mutex);
159 : 1 : g_clear_pointer (&transfer, g_free);
160 : 1 : }
161 : :
162 : : static GFile *
163 : 1 : valent_notification_plugin_get_icon_file (ValentNotificationPlugin *self,
164 : : JsonNode *packet)
165 : : {
166 : 2 : g_autoptr (GFile) file = NULL;
167 : 1 : const char *payload_hash;
168 : :
169 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
170 [ - + ]: 1 : g_assert (VALENT_IS_PACKET (packet));
171 : :
172 [ + - ]: 1 : if (valent_packet_get_string (packet, "payloadHash", &payload_hash))
173 : : {
174 : 1 : ValentContext *context = NULL;
175 : :
176 : 1 : context = valent_extension_get_context (VALENT_EXTENSION (self));
177 : 1 : file = valent_context_get_cache_file (context, payload_hash);
178 : : }
179 : : else
180 : : {
181 : 0 : g_autoptr (GFileIOStream) stream = NULL;
182 : :
183 [ # # ]: 0 : file = g_file_new_tmp ("valent-notification-icon.XXXXXX", &stream, NULL);
184 : : }
185 : :
186 : 1 : return g_steal_pointer (&file);
187 : : }
188 : :
189 : : static void
190 : 1 : download_icon_task (GTask *task,
191 : : gpointer source_object,
192 : : gpointer task_data,
193 : : GCancellable *cancellable)
194 : : {
195 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (source_object);
196 : 1 : IconTransferData *transfer = (IconTransferData *)task_data;
197 : 1 : g_autoptr (ValentDevice) device = NULL;
198 [ + - - - ]: 1 : g_autoptr (JsonNode) packet = NULL;
199 [ + - - - ]: 1 : g_autoptr (GFile) file = NULL;
200 : 1 : GError *error = NULL;
201 : :
202 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
203 [ - + ]: 1 : g_assert (transfer != NULL);
204 : :
205 [ + - ]: 1 : if (g_task_return_error_if_cancelled (task))
206 : : return;
207 : :
208 : 1 : g_rec_mutex_lock (&transfer->mutex);
209 : 1 : device = g_steal_pointer (&transfer->device);
210 : 1 : packet = g_steal_pointer (&transfer->packet);
211 : 1 : g_rec_mutex_unlock (&transfer->mutex);
212 : :
213 : 1 : file = valent_notification_plugin_get_icon_file (self, packet);
214 : :
215 : : /* Check if we've already downloaded this icon */
216 [ + - ]: 1 : if (!g_file_query_exists (file, cancellable))
217 : : {
218 [ - - ]: 1 : g_autoptr (GIOStream) source = NULL;
219 [ - - ]: 1 : g_autoptr (GFileOutputStream) target = NULL;
220 [ - - ]: 1 : g_autoptr (GFile) cache_dir = NULL;
221 [ + - - - ]: 1 : g_autoptr (ValentChannel) channel = NULL;
222 : :
223 : : /* Ensure the cache directory exists */
224 : 1 : cache_dir = g_file_get_parent (file);
225 : :
226 [ - + ]: 1 : if (g_mkdir_with_parents (g_file_peek_path (cache_dir), 0700) != 0)
227 : : {
228 : 0 : return g_task_return_new_error (task,
229 : : G_IO_ERROR,
230 : : G_IO_ERROR_FAILED,
231 : : "Error: %s",
232 : 0 : g_strerror (errno));
233 : : }
234 : :
235 : : /* Get the device channel */
236 [ - + ]: 1 : if ((channel = valent_device_ref_channel (device)) == NULL)
237 : : {
238 : 0 : return g_task_return_new_error (task,
239 : : G_IO_ERROR,
240 : : G_IO_ERROR_NOT_CONNECTED,
241 : : "Device is disconnected");
242 : : }
243 : :
244 : 1 : source = valent_channel_download (channel, packet, cancellable, &error);
245 : :
246 [ - + ]: 1 : if (source == NULL)
247 : : {
248 : 0 : g_file_delete (file, NULL, NULL);
249 : 0 : return g_task_return_error (task, error);
250 : : }
251 : :
252 : : /* Get the output stream */
253 : 1 : target = g_file_replace (file,
254 : : NULL,
255 : : FALSE,
256 : : G_FILE_CREATE_REPLACE_DESTINATION,
257 : : cancellable,
258 : : &error);
259 : :
260 [ - + ]: 1 : if (target == NULL)
261 : : {
262 : 0 : g_file_delete (file, NULL, NULL);
263 : 0 : return g_task_return_error (task, error);
264 : : }
265 : :
266 : : /* Start download */
267 : 1 : g_output_stream_splice (G_OUTPUT_STREAM (target),
268 : : g_io_stream_get_input_stream (source),
269 : : (G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
270 : : G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET),
271 : : cancellable,
272 : : &error);
273 : :
274 [ - + ]: 1 : if (error != NULL)
275 : : {
276 : 0 : g_file_delete (file, NULL, NULL);
277 : 0 : return g_task_return_error (task, error);
278 : : }
279 : : }
280 : :
281 : : /* If we're in a sandbox, send the file as a GBytesIcon in case the file path
282 : : * is not valid for the host system. */
283 [ - + ]: 1 : if (xdp_portal_running_under_sandbox ())
284 : : {
285 : 0 : g_autoptr (GBytes) bytes = NULL;
286 : :
287 : 0 : bytes = g_file_load_bytes (file, cancellable, NULL, &error);
288 : :
289 [ # # ]: 0 : if (bytes == NULL)
290 : : {
291 : 0 : g_file_delete (file, NULL, NULL);
292 : 0 : return g_task_return_error (task, error);
293 : : }
294 : :
295 : 0 : g_task_return_pointer (task, g_bytes_icon_new (bytes), g_object_unref);
296 : : }
297 : : else
298 : : {
299 : 1 : g_task_return_pointer (task, g_file_icon_new (file), g_object_unref);
300 : : }
301 : : }
302 : :
303 : : static void
304 : 1 : valent_notification_plugin_download_icon (ValentNotificationPlugin *self,
305 : : JsonNode *packet,
306 : : GCancellable *cancellable,
307 : : GAsyncReadyCallback callback,
308 : : gpointer user_data)
309 : : {
310 : 1 : ValentExtension *extension = VALENT_EXTENSION (self);
311 : 2 : g_autoptr (GTask) task = NULL;
312 : 1 : IconTransferData *transfer = NULL;
313 : :
314 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
315 [ - + ]: 1 : g_assert (VALENT_IS_PACKET (packet));
316 [ + - + - : 1 : g_assert (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
- + - - ]
317 : :
318 : 1 : transfer = g_new0 (IconTransferData, 1);
319 : 1 : g_rec_mutex_init (&transfer->mutex);
320 : 1 : g_rec_mutex_lock (&transfer->mutex);
321 : 1 : transfer->device = g_object_ref (valent_extension_get_object (extension));
322 : 1 : transfer->packet = json_node_ref (packet);
323 : 1 : g_rec_mutex_unlock (&transfer->mutex);
324 : :
325 : 1 : task = g_task_new (self, cancellable, callback, user_data);
326 [ + - ]: 1 : g_task_set_source_tag (task, valent_notification_plugin_download_icon);
327 : 1 : g_task_set_task_data (task, transfer, icon_transfer_data_free);
328 [ + - ]: 1 : g_task_run_in_thread (task, download_icon_task);
329 : 1 : }
330 : :
331 : : static GIcon *
332 : 1 : valent_notification_plugin_download_icon_finish (ValentNotificationPlugin *self,
333 : : GAsyncResult *result,
334 : : GError **error)
335 : : {
336 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
337 [ - + ]: 1 : g_assert (g_task_is_valid (result, self));
338 [ + - - + ]: 1 : g_assert (error == NULL || *error == NULL);
339 : :
340 : 1 : return g_task_propagate_pointer (G_TASK (result), error);
341 : : }
342 : :
343 : : /*
344 : : * Remote Notifications
345 : : */
346 : : static void
347 : 3 : valent_notification_plugin_show_notification (ValentNotificationPlugin *self,
348 : : JsonNode *packet,
349 : : GIcon *gicon)
350 : : {
351 : 3 : ValentDevice *device;
352 : 3 : g_autoptr (GNotification) notification = NULL;
353 [ + - ]: 3 : g_autoptr (GIcon) icon = NULL;
354 : 3 : const char *id;
355 : 3 : const char *app_name = NULL;
356 : 3 : const char *title = NULL;
357 : 3 : const char *text = NULL;
358 : 3 : const char *reply_id;
359 : 3 : JsonArray *actions;
360 : :
361 : : /* Finish the icon task */
362 [ + + + - : 3 : if (G_IS_ICON (gicon))
+ - + - ]
363 : 1 : icon = g_object_ref (gicon);
364 : :
365 : : /* Ensure we have a notification id, appName and title */
366 [ - + ]: 3 : if (!valent_packet_get_string (packet, "id", &id))
367 : : {
368 : 0 : g_debug ("%s(): expected \"id\" field holding a string",
369 : : G_STRFUNC);
370 : 0 : return;
371 : : }
372 : :
373 [ - + ]: 3 : if (!valent_packet_get_string (packet, "appName", &app_name))
374 : : {
375 : 0 : g_debug ("%s(): expected \"appName\" field holding a string",
376 : : G_STRFUNC);
377 : 0 : return;
378 : : }
379 : :
380 [ - + ]: 3 : if (!valent_packet_get_string (packet, "title", &title))
381 : : {
382 : 0 : g_debug ("%s(): expected \"title\" field holding a string",
383 : : G_STRFUNC);
384 : 0 : return;
385 : : }
386 : :
387 [ - + ]: 3 : if (!valent_packet_get_string (packet, "text", &text))
388 : : {
389 [ # # ]: 0 : if (g_strcmp0 (app_name, title) != 0)
390 : : {
391 : 0 : text = title;
392 : 0 : title = app_name;
393 : : }
394 : : }
395 : :
396 : 3 : device = valent_extension_get_object (VALENT_EXTENSION (self));
397 : :
398 : 3 : notification = g_notification_new (title);
399 : 3 : g_notification_set_body (notification, text);
400 : :
401 [ + + ]: 3 : if (icon != NULL)
402 : 1 : g_notification_set_icon (notification, icon);
403 : :
404 : : /* Notification Actions */
405 [ + + ]: 3 : if (valent_packet_get_array (packet, "actions", &actions))
406 : : {
407 : 1 : unsigned int n_actions;
408 : :
409 : 1 : n_actions = json_array_get_length (actions);
410 : :
411 [ + + ]: 4 : for (unsigned int i = 0; i < n_actions; i++)
412 : : {
413 : 3 : JsonNode *element;
414 : 3 : const char *action;
415 : 3 : GVariant *target;
416 : :
417 [ + - - + ]: 6 : if ((element = json_array_get_element (actions, i)) == NULL ||
418 : 3 : json_node_get_value_type (element) != G_TYPE_STRING)
419 : 0 : continue;
420 : :
421 : 3 : action = json_node_get_string (element);
422 : 3 : target = g_variant_new ("(ss)", id, action);
423 : 3 : valent_notification_add_device_button (notification,
424 : : device,
425 : : action,
426 : : "notification.action",
427 : : target);
428 : : }
429 : : }
430 : :
431 : : /* Repliable Notification */
432 [ - + ]: 3 : if (valent_packet_get_string (packet, "requestReplyId", &reply_id))
433 : : {
434 : 0 : g_autoptr (ValentNotification) incoming = NULL;
435 : 0 : const char *time_str = NULL;
436 : 0 : int64_t time = 0;
437 : 0 : GVariant *target;
438 : :
439 [ # # ]: 0 : if (valent_packet_get_string (packet, "time", &time_str))
440 : 0 : time = g_ascii_strtoll (time_str, NULL, 10);
441 : :
442 : 0 : incoming = g_object_new (VALENT_TYPE_NOTIFICATION,
443 : : "id", id,
444 : : "application", app_name,
445 : : "icon", icon,
446 : : "title", title,
447 : : "body", text,
448 : : "time", time,
449 : : NULL);
450 : 0 : target = g_variant_new ("(ssv)",
451 : : reply_id,
452 : : "",
453 : : valent_notification_serialize (incoming));
454 : :
455 [ # # ]: 0 : valent_notification_set_device_action (notification,
456 : : device,
457 : : "notification.reply",
458 : : target);
459 : : }
460 : :
461 [ + + ]: 3 : valent_device_plugin_show_notification (VALENT_DEVICE_PLUGIN (self),
462 : : id,
463 : : notification);
464 : : }
465 : :
466 : : static void
467 : 1 : valent_notification_plugin_download_icon_cb (ValentNotificationPlugin *self,
468 : : GAsyncResult *result,
469 : : gpointer user_data)
470 : : {
471 : 1 : g_autoptr (JsonNode) packet = user_data;
472 [ - - + - ]: 1 : g_autoptr (GIcon) icon = NULL;
473 [ - - + - ]: 1 : g_autoptr (GError) error = NULL;
474 : :
475 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
476 [ - + ]: 1 : g_assert (g_task_is_valid (result, self));
477 : :
478 : 1 : icon = valent_notification_plugin_download_icon_finish (self, result, &error);
479 : :
480 [ - + ]: 1 : if (icon == NULL)
481 : : {
482 : : /* If the operation was cancelled, the plugin is being disposed */
483 [ # # ]: 0 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
484 [ # # ]: 0 : return;
485 : :
486 : 0 : g_warning ("Downloading icon: %s", error->message);
487 : : }
488 : :
489 [ - + ]: 1 : valent_notification_plugin_show_notification (self, packet, icon);
490 : : }
491 : :
492 : : static void
493 : 3 : valent_notification_plugin_handle_notification (ValentNotificationPlugin *self,
494 : : JsonNode *packet)
495 : : {
496 : 3 : const char *id;
497 : :
498 [ + - ]: 3 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
499 [ - + ]: 3 : g_assert (VALENT_IS_PACKET (packet));
500 : :
501 [ - + ]: 3 : if (!valent_packet_get_string (packet, "id", &id))
502 : : {
503 : 0 : g_debug ("%s(): expected \"id\" field holding a string",
504 : : G_STRFUNC);
505 : 0 : return;
506 : : }
507 : :
508 : : /* A report that a remote notification has been dismissed */
509 [ - + ]: 3 : if (valent_packet_check_field (packet, "isCancel"))
510 : : {
511 : 0 : g_hash_table_remove (self->cache, id);
512 : 0 : valent_device_plugin_hide_notification (VALENT_DEVICE_PLUGIN (self), id);
513 : 0 : return;
514 : : }
515 : :
516 : : /* A notification that should only be shown once, already existed on the
517 : : * device, and is already in the cache. This typically means the device just
518 : : * re-connected and is re-sending known notifications. */
519 [ - + - - ]: 3 : if (valent_packet_check_field (packet, "onlyOnce") &&
520 [ # # ]: 0 : valent_packet_check_field (packet, "silent") &&
521 : 0 : g_hash_table_contains (self->cache, id))
522 : : {
523 : : VALENT_NOTE ("skipping existing notification: %s", id);
524 : : return;
525 : : }
526 : :
527 [ - + ]: 3 : g_hash_table_replace (self->cache,
528 : 3 : g_strdup (id),
529 : 3 : json_node_ref (packet));
530 : :
531 [ + + ]: 3 : if (valent_packet_has_payload (packet))
532 : : {
533 : 1 : valent_notification_plugin_download_icon (self,
534 : : packet,
535 : : self->cancellable,
536 : : (GAsyncReadyCallback)valent_notification_plugin_download_icon_cb,
537 : 1 : json_node_ref (packet));
538 : : }
539 : : else
540 : : {
541 : 2 : valent_notification_plugin_show_notification (self, packet, NULL);
542 : : }
543 : : }
544 : :
545 : : static void
546 : 0 : valent_notification_plugin_handle_notification_action (ValentNotificationPlugin *self,
547 : : JsonNode *packet)
548 : : {
549 [ # # ]: 0 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
550 [ # # ]: 0 : g_assert (VALENT_IS_PACKET (packet));
551 : :
552 : 0 : VALENT_NOTE ("TODO: kdeconnect.notification.action");
553 : 0 : }
554 : :
555 : : static void
556 : 0 : valent_notification_plugin_handle_notification_reply (ValentNotificationPlugin *self,
557 : : JsonNode *packet)
558 : : {
559 [ # # ]: 0 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
560 [ # # ]: 0 : g_assert (VALENT_IS_PACKET (packet));
561 : :
562 : 0 : VALENT_NOTE ("TODO: kdeconnect.notification.reply");
563 : 0 : }
564 : :
565 : : static void
566 : 0 : valent_notification_plugin_handle_notification_request (ValentNotificationPlugin *self,
567 : : JsonNode *packet)
568 : : {
569 [ # # ]: 0 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
570 [ # # ]: 0 : g_assert (VALENT_IS_PACKET (packet));
571 : :
572 : 0 : VALENT_NOTE ("TODO: kdeconnect.notification.request");
573 : 0 : }
574 : :
575 : : static void
576 : 5 : valent_notification_plugin_request_notifications (ValentNotificationPlugin *self)
577 : : {
578 : 10 : g_autoptr (JsonBuilder) builder = NULL;
579 [ - + ]: 5 : g_autoptr (JsonNode) packet = NULL;
580 : :
581 [ + - ]: 5 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
582 : :
583 : 5 : valent_packet_init (&builder, "kdeconnect.notification.request");
584 : 5 : json_builder_set_member_name (builder, "request");
585 : 5 : json_builder_add_boolean_value (builder, TRUE);
586 : 5 : packet = valent_packet_end (&builder);
587 : :
588 [ + - ]: 5 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
589 : 5 : }
590 : :
591 : : static void
592 : 2 : valent_notification_plugin_close_notification (ValentNotificationPlugin *self,
593 : : const char *id)
594 : : {
595 : 4 : g_autoptr (JsonBuilder) builder = NULL;
596 [ - + ]: 2 : g_autoptr (JsonNode) packet = NULL;
597 : :
598 [ + - ]: 2 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
599 [ - + ]: 2 : g_assert (id != NULL);
600 : :
601 : 2 : valent_packet_init (&builder, "kdeconnect.notification.request");
602 : 2 : json_builder_set_member_name (builder, "cancel");
603 : 2 : json_builder_add_string_value (builder, id);
604 : 2 : packet = valent_packet_end (&builder);
605 : :
606 [ + - ]: 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
607 : 2 : }
608 : :
609 : : static void
610 : 4 : valent_notification_upload_execute_cb (GObject *object,
611 : : GAsyncResult *result,
612 : : gpointer user_data)
613 : : {
614 : 4 : ValentTransfer *transfer = VALENT_TRANSFER (object);
615 : 8 : g_autoptr (GError) error = NULL;
616 : :
617 [ - + - - ]: 4 : if (!valent_transfer_execute_finish (transfer, result, &error) &&
618 : 0 : !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
619 : : {
620 : 0 : g_autoptr (ValentDevice) device = NULL;
621 [ # # ]: 0 : g_autoptr (JsonNode) packet = NULL;
622 : :
623 : 0 : g_object_get (transfer,
624 : : "device", &device,
625 : : "packet", &packet,
626 : : NULL);
627 [ # # ]: 0 : valent_device_send_packet (device, packet, NULL, NULL, NULL);
628 : : }
629 : 4 : }
630 : :
631 : : static void
632 : 4 : valent_notification_plugin_send_notification_with_icon (ValentNotificationPlugin *self,
633 : : JsonNode *packet,
634 : : GIcon *icon)
635 : : {
636 [ + - ]: 4 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
637 [ - + ]: 4 : g_assert (VALENT_IS_PACKET (packet));
638 [ + - + - : 4 : g_assert (icon == NULL || G_IS_ICON (icon));
+ - - + ]
639 : :
640 [ + - + - : 4 : if (G_IS_ICON (icon))
+ - + - ]
641 : : {
642 : 4 : ValentDevice *device;
643 : 8 : g_autoptr (ValentTransfer) transfer = NULL;
644 : :
645 : 4 : device = valent_extension_get_object (VALENT_EXTENSION (self));
646 : 4 : transfer = valent_notification_upload_new (device, packet, icon);
647 : 4 : valent_transfer_execute (transfer,
648 : : NULL,
649 : : valent_notification_upload_execute_cb,
650 : : NULL);
651 [ + - ]: 4 : return;
652 : : }
653 : :
654 : 0 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
655 : : }
656 : :
657 : : /**
658 : : * valent_notification_plugin_send_notification:
659 : : * @self: a `ValentNotificationPlugin`
660 : : * @id: the notification id
661 : : * @appName: (nullable): the notifying application
662 : : * @title: (nullable): the notification title
663 : : * @body: (nullable): the notification body
664 : : * @icon: (nullable): a `GIcon`
665 : : *
666 : : * Send a notification to the remote device.
667 : : */
668 : : static void
669 : 6 : valent_notification_plugin_send_notification (ValentNotificationPlugin *self,
670 : : const char *id,
671 : : const char *appName,
672 : : const char *title,
673 : : const char *body,
674 : : GIcon *icon)
675 : : {
676 : 6 : g_autoptr (JsonBuilder) builder = NULL;
677 [ - + - - ]: 6 : g_autoptr (JsonNode) packet = NULL;
678 [ + - - - ]: 6 : g_autofree char *ticker = NULL;
679 : :
680 [ + - ]: 6 : g_return_if_fail (VALENT_IS_NOTIFICATION_PLUGIN (self));
681 [ - + ]: 6 : g_return_if_fail (id != NULL);
682 [ + + + - : 6 : g_return_if_fail (icon == NULL || G_IS_ICON (icon));
+ - - + ]
683 : :
684 : : /* Build packet */
685 : 6 : valent_packet_init (&builder, "kdeconnect.notification");
686 : 6 : json_builder_set_member_name (builder, "id");
687 : 6 : json_builder_add_string_value (builder, id);
688 : :
689 : : /* Application Name */
690 : 6 : json_builder_set_member_name (builder, "appName");
691 [ + + ]: 7 : json_builder_add_string_value (builder, appName ? appName : "Valent");
692 : :
693 : : /* Title & Body (aka Ticker) */
694 : 6 : json_builder_set_member_name (builder, "title");
695 [ + + ]: 7 : json_builder_add_string_value (builder, title ? title : "");
696 : 6 : json_builder_set_member_name (builder, "body");
697 [ + + ]: 7 : json_builder_add_string_value (builder, body ? body : "");
698 : :
699 : 6 : ticker = g_strdup_printf ("%s: %s", title, body);
700 : 6 : json_builder_set_member_name (builder, "ticker");
701 : 6 : json_builder_add_string_value (builder, ticker);
702 : :
703 : 6 : packet = valent_packet_end (&builder);
704 : :
705 [ + + ]: 6 : if (icon == NULL)
706 : 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
707 : : else
708 : 4 : valent_notification_plugin_send_notification_with_icon (self, packet, icon);
709 : : }
710 : :
711 : : /*
712 : : * GActions
713 : : */
714 : : static void
715 : 1 : notification_action_action (GSimpleAction *action,
716 : : GVariant *parameter,
717 : : gpointer user_data)
718 : : {
719 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
720 : 2 : g_autoptr (JsonBuilder) builder = NULL;
721 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
722 : 1 : char *id;
723 : 1 : char *name;
724 : :
725 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
726 : :
727 : 1 : g_variant_get (parameter, "(&s&s)", &id, &name);
728 : :
729 : 1 : valent_packet_init (&builder, "kdeconnect.notification.action");
730 : 1 : json_builder_set_member_name (builder, "key");
731 : 1 : json_builder_add_string_value (builder, id);
732 : 1 : json_builder_set_member_name (builder, "action");
733 : 1 : json_builder_add_string_value (builder, name);
734 : 1 : packet = valent_packet_end (&builder);
735 : :
736 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
737 : 1 : }
738 : :
739 : : static void
740 : 1 : notification_cancel_action (GSimpleAction *action,
741 : : GVariant *parameter,
742 : : gpointer user_data)
743 : : {
744 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
745 : 1 : const char *id;
746 : 2 : g_autoptr (JsonBuilder) builder = NULL;
747 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
748 : :
749 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
750 : :
751 : 1 : id = g_variant_get_string (parameter, NULL);
752 : :
753 : 1 : valent_packet_init (&builder, "kdeconnect.notification");
754 : 1 : json_builder_set_member_name (builder, "id");
755 : 1 : json_builder_add_string_value (builder, id);
756 : 1 : json_builder_set_member_name (builder, "isCancel");
757 : 1 : json_builder_add_boolean_value (builder, TRUE);
758 : 1 : packet = valent_packet_end (&builder);
759 : :
760 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
761 : 1 : }
762 : :
763 : : static void
764 : 1 : notification_close_action (GSimpleAction *action,
765 : : GVariant *parameter,
766 : : gpointer user_data)
767 : : {
768 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
769 : 1 : const char *id;
770 : :
771 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
772 : :
773 : 1 : id = g_variant_get_string (parameter, NULL);
774 : 1 : valent_notification_plugin_close_notification (self, id);
775 : 1 : }
776 : :
777 : : static void
778 : 0 : on_dialog_destroyed (ValentNotificationDialog *dialog,
779 : : GHashTable *dialogs)
780 : : {
781 : 0 : ValentNotification *notification = NULL;
782 : :
783 : 0 : notification = valent_notification_dialog_get_notification (dialog);
784 [ # # ]: 0 : if (notification != NULL)
785 : 0 : g_hash_table_remove (dialogs, notification);
786 : 0 : }
787 : :
788 : : static void
789 : 1 : notification_reply_action (GSimpleAction *action,
790 : : GVariant *parameter,
791 : : gpointer user_data)
792 : : {
793 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
794 : 1 : const char *reply_id;
795 : 1 : const char *message;
796 : 1 : g_autoptr (GVariant) notificationv = NULL;
797 : :
798 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
799 : :
800 : 1 : g_variant_get (parameter, "(&s&sv)", &reply_id, &message, ¬ificationv);
801 : :
802 : : /* If the reply ID is empty, we've received a broken request */
803 [ + - - + ]: 1 : if (reply_id == NULL || *reply_id == '\0')
804 : : {
805 : 0 : g_debug ("%s(): expected requestReplyId", G_STRFUNC);
806 : 0 : return;
807 : : }
808 : :
809 : : /* If the message is empty, we're being asked to show the dialog */
810 [ + - - + ]: 1 : if (message == NULL || *message == '\0')
811 : : {
812 [ + - ]: 1 : g_autoptr (ValentNotification) notification = NULL;
813 : 0 : ValentNotificationDialog *dialog = NULL;
814 : :
815 [ # # ]: 0 : if (!gtk_is_initialized ())
816 : : {
817 : 0 : g_warning ("%s: No display available", G_STRFUNC);
818 : 0 : return;
819 : : }
820 : :
821 : 0 : notification = valent_notification_deserialize (notificationv);
822 : :
823 [ # # ]: 0 : if ((dialog = g_hash_table_lookup (self->dialogs, notification)) == NULL)
824 : : {
825 : 0 : ValentDevice *device;
826 : :
827 : 0 : device = valent_extension_get_object (VALENT_EXTENSION (self));
828 : 0 : dialog = g_object_new (VALENT_TYPE_NOTIFICATION_DIALOG,
829 : : "device", device,
830 : : "notification", notification,
831 : : "reply-id", reply_id,
832 : : NULL);
833 : 0 : g_signal_connect_data (dialog,
834 : : "destroy",
835 : : G_CALLBACK (on_dialog_destroyed),
836 : 0 : g_hash_table_ref (self->dialogs),
837 : : (void *)g_hash_table_unref,
838 : : G_CONNECT_DEFAULT);
839 : 0 : g_hash_table_replace (self->dialogs,
840 : : g_object_ref (notification),
841 : : dialog);
842 : : }
843 : :
844 [ # # ]: 0 : gtk_window_present (GTK_WINDOW (dialog));
845 : : }
846 : : else
847 : : {
848 : 1 : g_autoptr (JsonBuilder) builder = NULL;
849 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
850 : :
851 : 1 : valent_packet_init (&builder, "kdeconnect.notification.reply");
852 : 1 : json_builder_set_member_name (builder, "requestReplyId");
853 : 1 : json_builder_add_string_value (builder, reply_id);
854 : 1 : json_builder_set_member_name (builder, "message");
855 : 1 : json_builder_add_string_value (builder, message);
856 : 1 : packet = valent_packet_end (&builder);
857 : :
858 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
859 : : }
860 : : }
861 : :
862 : : static void
863 : 1 : notification_send_action (GSimpleAction *action,
864 : : GVariant *parameter,
865 : : gpointer user_data)
866 : : {
867 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
868 : 1 : GVariantDict dict;
869 : 2 : g_autofree char *id = NULL;
870 : 1 : const char *app = NULL;
871 : 1 : const char *title = NULL;
872 : 1 : const char *body = NULL;
873 : 1 : g_autoptr (GVariant) iconv = NULL;
874 [ + - ]: 1 : g_autoptr (GIcon) icon = NULL;
875 : :
876 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
877 : :
878 : 1 : g_variant_dict_init (&dict, parameter);
879 : :
880 : : /* Use a random ID as a fallback */
881 [ - + ]: 1 : if (!g_variant_dict_lookup (&dict, "id", "s", &id))
882 : 0 : id = g_uuid_string_random ();
883 : :
884 : 1 : g_variant_dict_lookup (&dict, "application", "&s", &app);
885 : 1 : g_variant_dict_lookup (&dict, "title", "&s", &title);
886 : 1 : g_variant_dict_lookup (&dict, "body", "&s", &body);
887 : :
888 : : /* Check for a serialized GIcon */
889 : 1 : iconv = g_variant_dict_lookup_value (&dict, "icon", G_VARIANT_TYPE ("(sv)"));
890 : :
891 [ + - ]: 1 : if (iconv != NULL)
892 : 1 : icon = g_icon_deserialize (iconv);
893 : :
894 : 1 : valent_notification_plugin_send_notification (self, id, app, title, body, icon);
895 : :
896 [ + - ]: 1 : g_variant_dict_clear (&dict);
897 : 1 : }
898 : :
899 : : static const GActionEntry actions[] = {
900 : : {"action", notification_action_action, "(ss)", NULL, NULL},
901 : : {"cancel", notification_cancel_action, "s", NULL, NULL},
902 : : {"close", notification_close_action, "s", NULL, NULL},
903 : : {"reply", notification_reply_action, "(ssv)", NULL, NULL},
904 : : {"send", notification_send_action, "a{sv}", NULL, NULL},
905 : : };
906 : :
907 : : /*
908 : : * ValentDevicePlugin
909 : : */
910 : : static void
911 : 16 : valent_notification_plugin_update_state (ValentDevicePlugin *plugin,
912 : : ValentDeviceState state)
913 : : {
914 : 16 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (plugin);
915 : 16 : gboolean available;
916 : :
917 [ + - ]: 16 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
918 : :
919 : 16 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
920 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
921 : :
922 : 16 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
923 : 16 : valent_notification_plugin_watch_notifications (self, available);
924 : :
925 : : /* Request Notifications */
926 [ + + ]: 16 : if (available)
927 : : {
928 : 5 : valent_notification_plugin_request_notifications (self);
929 : 16 : VALENT_NOTE ("TODO: send active notifications");
930 : : }
931 : 16 : }
932 : :
933 : : static void
934 : 3 : valent_notification_plugin_handle_packet (ValentDevicePlugin *plugin,
935 : : const char *type,
936 : : JsonNode *packet)
937 : : {
938 : 3 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (plugin);
939 : :
940 [ + - ]: 3 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (plugin));
941 [ - + ]: 3 : g_assert (type != NULL);
942 [ - + ]: 3 : g_assert (VALENT_IS_PACKET (packet));
943 : :
944 [ + - ]: 3 : if (g_str_equal (type, "kdeconnect.notification"))
945 : 3 : valent_notification_plugin_handle_notification (self, packet);
946 : :
947 [ # # ]: 0 : else if (g_str_equal (type, "kdeconnect.notification.action"))
948 : 0 : valent_notification_plugin_handle_notification_action (self, packet);
949 : :
950 [ # # ]: 0 : else if (g_str_equal (type, "kdeconnect.notification.reply"))
951 : 0 : valent_notification_plugin_handle_notification_reply (self, packet);
952 : :
953 [ # # ]: 0 : else if (g_str_equal (type, "kdeconnect.notification.request"))
954 : 0 : valent_notification_plugin_handle_notification_request (self, packet);
955 : :
956 : : else
957 : 0 : g_assert_not_reached ();
958 : 3 : }
959 : :
960 : : /*
961 : : * ValentObject
962 : : */
963 : : static void
964 : 10 : valent_notification_plugin_destroy (ValentObject *object)
965 : : {
966 : 10 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (object);
967 : :
968 : 10 : valent_notification_plugin_watch_notifications (self, FALSE);
969 : :
970 : : /* Clear the cache and close any open reply dialogs */
971 [ + + ]: 10 : g_clear_pointer (&self->cache, g_hash_table_unref);
972 [ + + ]: 10 : if (self->dialogs != NULL)
973 : : {
974 : 5 : g_hash_table_remove_all (self->dialogs);
975 [ + - ]: 5 : g_clear_pointer (&self->dialogs, g_hash_table_unref);
976 : : }
977 : :
978 : : /* Cancel any pending operations */
979 : 10 : g_cancellable_cancel (self->cancellable);
980 [ + + ]: 10 : g_clear_object (&self->cancellable);
981 : :
982 : 10 : VALENT_OBJECT_CLASS (valent_notification_plugin_parent_class)->destroy (object);
983 : 10 : }
984 : :
985 : : /*
986 : : * GObject
987 : : */
988 : : static void
989 : 5 : valent_notification_plugin_constructed (GObject *object)
990 : : {
991 : 5 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (object);
992 : 5 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
993 : :
994 : 5 : self->cancellable = g_cancellable_new ();
995 : 5 : self->notifications = valent_notifications_get_default();
996 : 5 : self->session = valent_session_get_default ();
997 : :
998 : 5 : g_action_map_add_action_entries (G_ACTION_MAP (plugin),
999 : : actions,
1000 : : G_N_ELEMENTS (actions),
1001 : : plugin);
1002 : :
1003 : 5 : self->cache = g_hash_table_new_full (g_str_hash,
1004 : : g_str_equal,
1005 : : g_free,
1006 : : (GDestroyNotify)json_node_unref);
1007 : 5 : self->dialogs = g_hash_table_new_full (valent_notification_hash,
1008 : : valent_notification_equal,
1009 : : g_object_unref,
1010 : : (GDestroyNotify)gtk_window_destroy);
1011 : :
1012 : 5 : G_OBJECT_CLASS (valent_notification_plugin_parent_class)->constructed (object);
1013 : 5 : }
1014 : :
1015 : : static void
1016 : 2 : valent_notification_plugin_class_init (ValentNotificationPluginClass *klass)
1017 : : {
1018 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
1019 : 2 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
1020 : 2 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
1021 : :
1022 : 2 : object_class->constructed = valent_notification_plugin_constructed;
1023 : :
1024 : 2 : vobject_class->destroy = valent_notification_plugin_destroy;
1025 : :
1026 : 2 : plugin_class->handle_packet = valent_notification_plugin_handle_packet;
1027 : 2 : plugin_class->update_state = valent_notification_plugin_update_state;
1028 : : }
1029 : :
1030 : : static void
1031 : 5 : valent_notification_plugin_init (ValentNotificationPlugin *self)
1032 : : {
1033 : 5 : }
1034 : :
|