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 [ + + + - ]: 88 : 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 : 4 : valent_notification_plugin_show_notification (ValentNotificationPlugin *self,
348 : : JsonNode *packet,
349 : : GIcon *gicon)
350 : : {
351 : 4 : ValentDevice *device;
352 : 4 : g_autoptr (GNotification) notification = NULL;
353 [ + - ]: 4 : g_autoptr (GIcon) icon = NULL;
354 [ + + - - ]: 4 : g_auto (GStrv) ticker_strv = NULL;
355 : 4 : const char *id;
356 : 4 : const char *app_name = NULL;
357 : 4 : const char *title = NULL;
358 : 4 : const char *text = NULL;
359 : 4 : const char *ticker;
360 : 4 : const char *reply_id;
361 : 4 : JsonArray *actions;
362 : :
363 : : /* Finish the icon task */
364 [ + + + - : 4 : if (G_IS_ICON (gicon))
+ - + - ]
365 : 1 : icon = g_object_ref (gicon);
366 : :
367 : : /* Ensure we have a notification id */
368 [ - + ]: 4 : if (!valent_packet_get_string (packet, "id", &id))
369 : : {
370 : 0 : g_debug ("%s(): expected \"id\" field holding a string",
371 : : G_STRFUNC);
372 : 0 : return;
373 : : }
374 : :
375 : : /* This should never be absent, but we check anyways */
376 [ - + ]: 4 : if (!valent_packet_get_string (packet, "appName", &app_name))
377 : : {
378 : 0 : g_debug ("%s(): expected \"appName\" field holding a string",
379 : : G_STRFUNC);
380 : 0 : return;
381 : : }
382 : :
383 : : /* If `title` or `text` are missing, try to compose them from `ticker` */
384 [ + - + - ]: 8 : if ((!valent_packet_get_string (packet, "title", &title) ||
385 [ - - ]: 4 : !valent_packet_get_string (packet, "text", &text)) &&
386 : 0 : valent_packet_get_string (packet, "ticker", &ticker))
387 : : {
388 : 0 : ticker_strv = g_strsplit (ticker, ": ", 2);
389 : 0 : title = ticker_strv[0];
390 : 0 : text = ticker_strv[1];
391 : : }
392 : :
393 [ + - - + ]: 4 : if (title == NULL || text == NULL)
394 : : {
395 : 0 : g_debug ("%s(): expected either title and text, or ticker",
396 : : G_STRFUNC);
397 [ # # ]: 0 : return;
398 : : }
399 : :
400 : 4 : device = valent_extension_get_object (VALENT_EXTENSION (self));
401 : :
402 : : /* Start building the GNotification */
403 : 4 : notification = g_notification_new (title);
404 : :
405 : : /* Repliable Notification */
406 [ + + ]: 4 : if (valent_packet_get_string (packet, "requestReplyId", &reply_id))
407 : : {
408 : 1 : g_autoptr (ValentNotification) incoming = NULL;
409 : 1 : const char *time_str = NULL;
410 : 1 : int64_t time = 0;
411 : 1 : GVariant *target;
412 : :
413 [ + - ]: 1 : if (valent_packet_get_string (packet, "time", &time_str))
414 : 1 : time = g_ascii_strtoll (time_str, NULL, 10);
415 : :
416 : 1 : incoming = g_object_new (VALENT_TYPE_NOTIFICATION,
417 : : "id", id,
418 : : "application", app_name,
419 : : "icon", icon,
420 : : "title", title,
421 : : "body", text,
422 : : "time", time,
423 : : NULL);
424 : 1 : target = g_variant_new ("(ssv)",
425 : : reply_id,
426 : : "",
427 : : valent_notification_serialize (incoming));
428 : :
429 [ + - ]: 1 : valent_notification_set_device_action (notification,
430 : : device,
431 : : "notification.reply",
432 : : target);
433 : : }
434 : :
435 : : /* Notification Actions */
436 [ + + ]: 4 : if (valent_packet_get_array (packet, "actions", &actions))
437 : : {
438 : 1 : unsigned int n_actions;
439 : :
440 : 1 : n_actions = json_array_get_length (actions);
441 : :
442 [ + + ]: 4 : for (unsigned int i = 0; i < n_actions; i++)
443 : : {
444 : 3 : JsonNode *element;
445 : 3 : const char *action;
446 : 3 : GVariant *target;
447 : :
448 [ + - - + ]: 6 : if ((element = json_array_get_element (actions, i)) == NULL ||
449 : 3 : json_node_get_value_type (element) != G_TYPE_STRING)
450 : 0 : continue;
451 : :
452 : 3 : action = json_node_get_string (element);
453 : 3 : target = g_variant_new ("(ss)", id, action);
454 : 3 : valent_notification_add_device_button (notification,
455 : : device,
456 : : action,
457 : : "notification.action",
458 : : target);
459 : : }
460 : : }
461 : :
462 : : /* Special cases.
463 : : *
464 : : * In some cases, usually on Android, a notification "type" can be inferred
465 : : * from the ID string or the appName is duplicated as the title.
466 : : */
467 [ - + ]: 4 : if (g_strrstr (id, "MissedCall") != NULL)
468 : : {
469 : 0 : g_notification_set_title (notification, title);
470 : 0 : g_notification_set_body (notification, text);
471 : :
472 [ # # ]: 0 : if (icon == NULL)
473 : 0 : icon = g_themed_icon_new ("call-missed-symbolic");
474 : : }
475 [ - + ]: 4 : else if (g_strrstr (id, "sms") != NULL)
476 : : {
477 : 0 : g_notification_set_title (notification, title);
478 : 0 : g_notification_set_body (notification, text);
479 : :
480 [ # # ]: 0 : if (icon == NULL)
481 : 0 : icon = g_themed_icon_new ("sms-symbolic");
482 : : }
483 [ - + ]: 4 : else if (g_strcmp0 (app_name, title) == 0)
484 : : {
485 : 0 : g_notification_set_title (notification, title);
486 : 0 : g_notification_set_body (notification, text);
487 : : }
488 : : else
489 : : {
490 : 4 : g_autofree char *ticker_body = NULL;
491 : :
492 : 4 : ticker_body = g_strdup_printf ("%s: %s", title, text);
493 : 4 : g_notification_set_title (notification, app_name);
494 : 4 : g_notification_set_body (notification, ticker_body);
495 : : }
496 : :
497 [ + + ]: 4 : if (icon != NULL)
498 : 1 : g_notification_set_icon (notification, icon);
499 : :
500 [ - + ]: 4 : valent_device_plugin_show_notification (VALENT_DEVICE_PLUGIN (self),
501 : : id,
502 : : notification);
503 : : }
504 : :
505 : : static void
506 : 1 : valent_notification_plugin_download_icon_cb (ValentNotificationPlugin *self,
507 : : GAsyncResult *result,
508 : : gpointer user_data)
509 : : {
510 : 1 : g_autoptr (JsonNode) packet = user_data;
511 [ - - + - ]: 1 : g_autoptr (GIcon) icon = NULL;
512 [ - - + - ]: 1 : g_autoptr (GError) error = NULL;
513 : :
514 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
515 [ - + ]: 1 : g_assert (g_task_is_valid (result, self));
516 : :
517 : 1 : icon = valent_notification_plugin_download_icon_finish (self, result, &error);
518 : :
519 [ - + ]: 1 : if (icon == NULL)
520 : : {
521 : : /* If the operation was cancelled, the plugin is being disposed */
522 [ # # ]: 0 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
523 [ # # ]: 0 : return;
524 : :
525 : 0 : g_warning ("Downloading icon: %s", error->message);
526 : : }
527 : :
528 [ - + ]: 1 : valent_notification_plugin_show_notification (self, packet, icon);
529 : : }
530 : :
531 : : static void
532 : 4 : valent_notification_plugin_handle_notification (ValentNotificationPlugin *self,
533 : : JsonNode *packet)
534 : : {
535 : 4 : const char *id;
536 : :
537 [ + - ]: 4 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
538 [ - + ]: 4 : g_assert (VALENT_IS_PACKET (packet));
539 : :
540 [ - + ]: 4 : if (!valent_packet_get_string (packet, "id", &id))
541 : : {
542 : 0 : g_debug ("%s(): expected \"id\" field holding a string",
543 : : G_STRFUNC);
544 : 0 : return;
545 : : }
546 : :
547 : : /* A report that a remote notification has been dismissed */
548 [ - + ]: 4 : if (valent_packet_check_field (packet, "isCancel"))
549 : : {
550 : 0 : g_hash_table_remove (self->cache, id);
551 : 0 : valent_device_plugin_hide_notification (VALENT_DEVICE_PLUGIN (self), id);
552 : 0 : return;
553 : : }
554 : :
555 : : /* A notification that should only be shown once, already existed on the
556 : : * device, and is already in the cache. This typically means the device just
557 : : * re-connected and is re-sending known notifications. */
558 [ - + - - ]: 4 : if (valent_packet_check_field (packet, "onlyOnce") &&
559 [ # # ]: 0 : valent_packet_check_field (packet, "silent") &&
560 : 0 : g_hash_table_contains (self->cache, id))
561 : : {
562 : : VALENT_NOTE ("skipping existing notification: %s", id);
563 : : return;
564 : : }
565 : :
566 [ - + ]: 4 : g_hash_table_replace (self->cache,
567 : 4 : g_strdup (id),
568 : 4 : json_node_ref (packet));
569 : :
570 [ + + ]: 4 : if (valent_packet_has_payload (packet))
571 : : {
572 : 1 : valent_notification_plugin_download_icon (self,
573 : : packet,
574 : : self->cancellable,
575 : : (GAsyncReadyCallback)valent_notification_plugin_download_icon_cb,
576 : 1 : json_node_ref (packet));
577 : : }
578 : : else
579 : : {
580 : 3 : valent_notification_plugin_show_notification (self, packet, NULL);
581 : : }
582 : : }
583 : :
584 : : static void
585 : 0 : valent_notification_plugin_handle_notification_action (ValentNotificationPlugin *self,
586 : : JsonNode *packet)
587 : : {
588 [ # # ]: 0 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
589 [ # # ]: 0 : g_assert (VALENT_IS_PACKET (packet));
590 : :
591 : 0 : VALENT_NOTE ("TODO: kdeconnect.notification.action");
592 : 0 : }
593 : :
594 : : static void
595 : 0 : valent_notification_plugin_handle_notification_reply (ValentNotificationPlugin *self,
596 : : JsonNode *packet)
597 : : {
598 [ # # ]: 0 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
599 [ # # ]: 0 : g_assert (VALENT_IS_PACKET (packet));
600 : :
601 : 0 : VALENT_NOTE ("TODO: kdeconnect.notification.reply");
602 : 0 : }
603 : :
604 : : static void
605 : 0 : valent_notification_plugin_handle_notification_request (ValentNotificationPlugin *self,
606 : : JsonNode *packet)
607 : : {
608 [ # # ]: 0 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
609 [ # # ]: 0 : g_assert (VALENT_IS_PACKET (packet));
610 : :
611 : 0 : VALENT_NOTE ("TODO: kdeconnect.notification.request");
612 : 0 : }
613 : :
614 : : static void
615 : 5 : valent_notification_plugin_request_notifications (ValentNotificationPlugin *self)
616 : : {
617 : 10 : g_autoptr (JsonBuilder) builder = NULL;
618 [ - + ]: 5 : g_autoptr (JsonNode) packet = NULL;
619 : :
620 [ + - ]: 5 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
621 : :
622 : 5 : valent_packet_init (&builder, "kdeconnect.notification.request");
623 : 5 : json_builder_set_member_name (builder, "request");
624 : 5 : json_builder_add_boolean_value (builder, TRUE);
625 : 5 : packet = valent_packet_end (&builder);
626 : :
627 [ + - ]: 5 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
628 : 5 : }
629 : :
630 : : static void
631 : 2 : valent_notification_plugin_close_notification (ValentNotificationPlugin *self,
632 : : const char *id)
633 : : {
634 : 4 : g_autoptr (JsonBuilder) builder = NULL;
635 [ - + ]: 2 : g_autoptr (JsonNode) packet = NULL;
636 : :
637 [ + - ]: 2 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
638 [ - + ]: 2 : g_assert (id != NULL);
639 : :
640 : 2 : valent_packet_init (&builder, "kdeconnect.notification.request");
641 : 2 : json_builder_set_member_name (builder, "cancel");
642 : 2 : json_builder_add_string_value (builder, id);
643 : 2 : packet = valent_packet_end (&builder);
644 : :
645 [ + - ]: 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
646 : 2 : }
647 : :
648 : : static void
649 : 4 : valent_notification_upload_execute_cb (GObject *object,
650 : : GAsyncResult *result,
651 : : gpointer user_data)
652 : : {
653 : 4 : ValentTransfer *transfer = VALENT_TRANSFER (object);
654 : 8 : g_autoptr (GError) error = NULL;
655 : :
656 [ - + - - ]: 4 : if (!valent_transfer_execute_finish (transfer, result, &error) &&
657 : 0 : !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
658 : : {
659 : 0 : g_autoptr (ValentDevice) device = NULL;
660 [ # # ]: 0 : g_autoptr (JsonNode) packet = NULL;
661 : :
662 : 0 : g_object_get (transfer,
663 : : "device", &device,
664 : : "packet", &packet,
665 : : NULL);
666 [ # # ]: 0 : valent_device_send_packet (device, packet, NULL, NULL, NULL);
667 : : }
668 : 4 : }
669 : :
670 : : static void
671 : 4 : valent_notification_plugin_send_notification_with_icon (ValentNotificationPlugin *self,
672 : : JsonNode *packet,
673 : : GIcon *icon)
674 : : {
675 [ + - ]: 4 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
676 [ - + ]: 4 : g_assert (VALENT_IS_PACKET (packet));
677 [ + - + - : 4 : g_assert (icon == NULL || G_IS_ICON (icon));
+ - - + ]
678 : :
679 [ + - + - : 4 : if (G_IS_ICON (icon))
+ - + - ]
680 : : {
681 : 4 : ValentDevice *device;
682 : 8 : g_autoptr (ValentTransfer) transfer = NULL;
683 : :
684 : 4 : device = valent_extension_get_object (VALENT_EXTENSION (self));
685 : 4 : transfer = valent_notification_upload_new (device, packet, icon);
686 : 4 : valent_transfer_execute (transfer,
687 : : NULL,
688 : : valent_notification_upload_execute_cb,
689 : : NULL);
690 [ + - ]: 4 : return;
691 : : }
692 : :
693 : 0 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
694 : : }
695 : :
696 : : /**
697 : : * valent_notification_plugin_send_notification:
698 : : * @self: a `ValentNotificationPlugin`
699 : : * @id: the notification id
700 : : * @appName: (nullable): the notifying application
701 : : * @title: (nullable): the notification title
702 : : * @body: (nullable): the notification body
703 : : * @icon: (nullable): a `GIcon`
704 : : *
705 : : * Send a notification to the remote device.
706 : : */
707 : : static void
708 : 6 : valent_notification_plugin_send_notification (ValentNotificationPlugin *self,
709 : : const char *id,
710 : : const char *appName,
711 : : const char *title,
712 : : const char *body,
713 : : GIcon *icon)
714 : : {
715 : 6 : g_autoptr (JsonBuilder) builder = NULL;
716 [ - + - - ]: 6 : g_autoptr (JsonNode) packet = NULL;
717 [ + - - - ]: 6 : g_autofree char *ticker = NULL;
718 : :
719 [ + - ]: 6 : g_return_if_fail (VALENT_IS_NOTIFICATION_PLUGIN (self));
720 [ - + ]: 6 : g_return_if_fail (id != NULL);
721 [ + + + - : 6 : g_return_if_fail (icon == NULL || G_IS_ICON (icon));
+ - - + ]
722 : :
723 : : /* Build packet */
724 : 6 : valent_packet_init (&builder, "kdeconnect.notification");
725 : 6 : json_builder_set_member_name (builder, "id");
726 : 6 : json_builder_add_string_value (builder, id);
727 : :
728 : : /* Application Name */
729 : 6 : json_builder_set_member_name (builder, "appName");
730 [ + + ]: 7 : json_builder_add_string_value (builder, appName ? appName : "Valent");
731 : :
732 : : /* Title & Body (aka Ticker) */
733 : 6 : json_builder_set_member_name (builder, "title");
734 [ + + ]: 7 : json_builder_add_string_value (builder, title ? title : "");
735 : 6 : json_builder_set_member_name (builder, "body");
736 [ + + ]: 7 : json_builder_add_string_value (builder, body ? body : "");
737 : :
738 : 6 : ticker = g_strdup_printf ("%s: %s", title, body);
739 : 6 : json_builder_set_member_name (builder, "ticker");
740 : 6 : json_builder_add_string_value (builder, ticker);
741 : :
742 : 6 : packet = valent_packet_end (&builder);
743 : :
744 [ + + ]: 6 : if (icon == NULL)
745 : 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
746 : : else
747 : 4 : valent_notification_plugin_send_notification_with_icon (self, packet, icon);
748 : : }
749 : :
750 : : /*
751 : : * GActions
752 : : */
753 : : static void
754 : 1 : notification_action_action (GSimpleAction *action,
755 : : GVariant *parameter,
756 : : gpointer user_data)
757 : : {
758 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
759 : 2 : g_autoptr (JsonBuilder) builder = NULL;
760 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
761 : 1 : char *id;
762 : 1 : char *name;
763 : :
764 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
765 : :
766 : 1 : g_variant_get (parameter, "(&s&s)", &id, &name);
767 : :
768 : 1 : valent_packet_init (&builder, "kdeconnect.notification.action");
769 : 1 : json_builder_set_member_name (builder, "key");
770 : 1 : json_builder_add_string_value (builder, id);
771 : 1 : json_builder_set_member_name (builder, "action");
772 : 1 : json_builder_add_string_value (builder, name);
773 : 1 : packet = valent_packet_end (&builder);
774 : :
775 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
776 : 1 : }
777 : :
778 : : static void
779 : 1 : notification_cancel_action (GSimpleAction *action,
780 : : GVariant *parameter,
781 : : gpointer user_data)
782 : : {
783 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
784 : 1 : const char *id;
785 : 2 : g_autoptr (JsonBuilder) builder = NULL;
786 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
787 : :
788 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
789 : :
790 : 1 : id = g_variant_get_string (parameter, NULL);
791 : :
792 : 1 : valent_packet_init (&builder, "kdeconnect.notification");
793 : 1 : json_builder_set_member_name (builder, "id");
794 : 1 : json_builder_add_string_value (builder, id);
795 : 1 : json_builder_set_member_name (builder, "isCancel");
796 : 1 : json_builder_add_boolean_value (builder, TRUE);
797 : 1 : packet = valent_packet_end (&builder);
798 : :
799 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
800 : 1 : }
801 : :
802 : : static void
803 : 1 : notification_close_action (GSimpleAction *action,
804 : : GVariant *parameter,
805 : : gpointer user_data)
806 : : {
807 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
808 : 1 : const char *id;
809 : :
810 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
811 : :
812 : 1 : id = g_variant_get_string (parameter, NULL);
813 : 1 : valent_notification_plugin_close_notification (self, id);
814 : 1 : }
815 : :
816 : : static void
817 : 1 : notification_reply_action (GSimpleAction *action,
818 : : GVariant *parameter,
819 : : gpointer user_data)
820 : : {
821 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
822 : 1 : const char *reply_id;
823 : 1 : const char *message;
824 : 1 : g_autoptr (GVariant) notificationv = NULL;
825 : :
826 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
827 : :
828 : 1 : g_variant_get (parameter, "(&s&sv)", &reply_id, &message, ¬ificationv);
829 : :
830 : : /* If the reply ID is empty, we've received a broken request */
831 [ + - - + ]: 1 : if (reply_id == NULL || *reply_id == '\0')
832 : : {
833 : 0 : g_debug ("%s(): expected requestReplyId", G_STRFUNC);
834 : 0 : return;
835 : : }
836 : :
837 : : /* If the message is empty, we're being asked to show the dialog */
838 [ + - - + ]: 1 : if (message == NULL || *message == '\0')
839 : : {
840 [ + - ]: 1 : g_autoptr (ValentNotificationDialog) dialog = NULL;
841 [ # # ]: 0 : g_autoptr (ValentNotification) notification = NULL;
842 : :
843 [ # # ]: 0 : if (!gtk_is_initialized ())
844 : : {
845 : 0 : g_warning ("%s: No display available", G_STRFUNC);
846 : 0 : return;
847 : : }
848 : :
849 : 0 : notification = valent_notification_deserialize (notificationv);
850 : :
851 [ # # ]: 0 : if ((dialog = g_hash_table_lookup (self->dialogs, notification)) == NULL)
852 : : {
853 : 0 : ValentDevice *device;
854 : :
855 : 0 : device = valent_extension_get_object (VALENT_EXTENSION (self));
856 : 0 : dialog = g_object_new (VALENT_TYPE_NOTIFICATION_DIALOG,
857 : : "device", device,
858 : : "notification", notification,
859 : : "reply-id", reply_id,
860 : : NULL);
861 : 0 : g_hash_table_insert (self->dialogs,
862 : : g_object_ref (notification),
863 : : g_object_ref_sink (dialog));
864 : : }
865 : :
866 [ # # ]: 0 : gtk_window_present (GTK_WINDOW (dialog));
867 : : }
868 : : else
869 : : {
870 : 1 : g_autoptr (JsonBuilder) builder = NULL;
871 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
872 : :
873 : 1 : valent_packet_init (&builder, "kdeconnect.notification.reply");
874 : 1 : json_builder_set_member_name (builder, "requestReplyId");
875 : 1 : json_builder_add_string_value (builder, reply_id);
876 : 1 : json_builder_set_member_name (builder, "message");
877 : 1 : json_builder_add_string_value (builder, message);
878 : 1 : packet = valent_packet_end (&builder);
879 : :
880 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
881 : : }
882 : : }
883 : :
884 : : static void
885 : 1 : notification_send_action (GSimpleAction *action,
886 : : GVariant *parameter,
887 : : gpointer user_data)
888 : : {
889 : 1 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (user_data);
890 : 1 : GVariantDict dict;
891 : 2 : g_autofree char *id = NULL;
892 : 1 : const char *app = NULL;
893 : 1 : const char *title = NULL;
894 : 1 : const char *body = NULL;
895 : 1 : g_autoptr (GVariant) iconv = NULL;
896 [ + - ]: 1 : g_autoptr (GIcon) icon = NULL;
897 : :
898 [ + - ]: 1 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
899 : :
900 : 1 : g_variant_dict_init (&dict, parameter);
901 : :
902 : : /* Use a random ID as a fallback */
903 [ - + ]: 1 : if (!g_variant_dict_lookup (&dict, "id", "s", &id))
904 : 0 : id = g_uuid_string_random ();
905 : :
906 : 1 : g_variant_dict_lookup (&dict, "application", "&s", &app);
907 : 1 : g_variant_dict_lookup (&dict, "title", "&s", &title);
908 : 1 : g_variant_dict_lookup (&dict, "body", "&s", &body);
909 : :
910 : : /* Check for a serialized GIcon */
911 : 1 : iconv = g_variant_dict_lookup_value (&dict, "icon", G_VARIANT_TYPE ("(sv)"));
912 : :
913 [ + - ]: 1 : if (iconv != NULL)
914 : 1 : icon = g_icon_deserialize (iconv);
915 : :
916 : 1 : valent_notification_plugin_send_notification (self, id, app, title, body, icon);
917 : :
918 [ + - ]: 1 : g_variant_dict_clear (&dict);
919 : 1 : }
920 : :
921 : : static const GActionEntry actions[] = {
922 : : {"action", notification_action_action, "(ss)", NULL, NULL},
923 : : {"cancel", notification_cancel_action, "s", NULL, NULL},
924 : : {"close", notification_close_action, "s", NULL, NULL},
925 : : {"reply", notification_reply_action, "(ssv)", NULL, NULL},
926 : : {"send", notification_send_action, "a{sv}", NULL, NULL},
927 : : };
928 : :
929 : : /*
930 : : * ValentDevicePlugin
931 : : */
932 : : static void
933 : 16 : valent_notification_plugin_update_state (ValentDevicePlugin *plugin,
934 : : ValentDeviceState state)
935 : : {
936 : 16 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (plugin);
937 : 16 : gboolean available;
938 : :
939 [ + - ]: 16 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (self));
940 : :
941 : 16 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
942 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
943 : :
944 : 16 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
945 : 16 : valent_notification_plugin_watch_notifications (self, available);
946 : :
947 : : /* Request Notifications */
948 [ + + ]: 16 : if (available)
949 : : {
950 : 5 : valent_notification_plugin_request_notifications (self);
951 : 16 : VALENT_NOTE ("TODO: send active notifications");
952 : : }
953 : 16 : }
954 : :
955 : : static void
956 : 4 : valent_notification_plugin_handle_packet (ValentDevicePlugin *plugin,
957 : : const char *type,
958 : : JsonNode *packet)
959 : : {
960 : 4 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (plugin);
961 : :
962 [ + - ]: 4 : g_assert (VALENT_IS_NOTIFICATION_PLUGIN (plugin));
963 [ - + ]: 4 : g_assert (type != NULL);
964 [ - + ]: 4 : g_assert (VALENT_IS_PACKET (packet));
965 : :
966 [ + - ]: 4 : if (g_str_equal (type, "kdeconnect.notification"))
967 : 4 : valent_notification_plugin_handle_notification (self, packet);
968 : :
969 [ # # ]: 0 : else if (g_str_equal (type, "kdeconnect.notification.action"))
970 : 0 : valent_notification_plugin_handle_notification_action (self, packet);
971 : :
972 [ # # ]: 0 : else if (g_str_equal (type, "kdeconnect.notification.reply"))
973 : 0 : valent_notification_plugin_handle_notification_reply (self, packet);
974 : :
975 [ # # ]: 0 : else if (g_str_equal (type, "kdeconnect.notification.request"))
976 : 0 : valent_notification_plugin_handle_notification_request (self, packet);
977 : :
978 : : else
979 : 0 : g_assert_not_reached ();
980 : 4 : }
981 : :
982 : : /*
983 : : * ValentObject
984 : : */
985 : : static void
986 : 10 : valent_notification_plugin_destroy (ValentObject *object)
987 : : {
988 : 10 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (object);
989 : :
990 : 10 : valent_notification_plugin_watch_notifications (self, FALSE);
991 : :
992 : : /* Close any open reply dialogs */
993 [ + + ]: 10 : g_clear_pointer (&self->cache, g_hash_table_unref);
994 [ + + ]: 10 : g_clear_pointer (&self->dialogs, g_hash_table_unref);
995 : :
996 : : /* Cancel any pending operations */
997 : 10 : g_cancellable_cancel (self->cancellable);
998 [ + + ]: 10 : g_clear_object (&self->cancellable);
999 : :
1000 : 10 : VALENT_OBJECT_CLASS (valent_notification_plugin_parent_class)->destroy (object);
1001 : 10 : }
1002 : :
1003 : : /*
1004 : : * GObject
1005 : : */
1006 : : static void
1007 : 5 : valent_notification_plugin_constructed (GObject *object)
1008 : : {
1009 : 5 : ValentNotificationPlugin *self = VALENT_NOTIFICATION_PLUGIN (object);
1010 : 5 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
1011 : :
1012 : 5 : self->cancellable = g_cancellable_new ();
1013 : 5 : self->notifications = valent_notifications_get_default();
1014 : 5 : self->session = valent_session_get_default ();
1015 : :
1016 : 5 : g_action_map_add_action_entries (G_ACTION_MAP (plugin),
1017 : : actions,
1018 : : G_N_ELEMENTS (actions),
1019 : : plugin);
1020 : :
1021 : 5 : self->cache = g_hash_table_new_full (g_str_hash,
1022 : : g_str_equal,
1023 : : g_free,
1024 : : (GDestroyNotify)json_node_unref);
1025 : 5 : self->dialogs = g_hash_table_new_full (valent_notification_hash,
1026 : : valent_notification_equal,
1027 : : g_object_unref,
1028 : : (GDestroyNotify)gtk_window_destroy);
1029 : :
1030 : 5 : G_OBJECT_CLASS (valent_notification_plugin_parent_class)->constructed (object);
1031 : 5 : }
1032 : :
1033 : : static void
1034 : 2 : valent_notification_plugin_class_init (ValentNotificationPluginClass *klass)
1035 : : {
1036 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
1037 : 2 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
1038 : 2 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
1039 : :
1040 : 2 : object_class->constructed = valent_notification_plugin_constructed;
1041 : :
1042 : 2 : vobject_class->destroy = valent_notification_plugin_destroy;
1043 : :
1044 : 2 : plugin_class->handle_packet = valent_notification_plugin_handle_packet;
1045 : 2 : plugin_class->update_state = valent_notification_plugin_update_state;
1046 : : }
1047 : :
1048 : : static void
1049 : 5 : valent_notification_plugin_init (ValentNotificationPlugin *self)
1050 : : {
1051 : 5 : }
1052 : :
|