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