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-upload"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <gdk-pixbuf/gdk-pixbuf.h>
10 : : #include <json-glib/json-glib.h>
11 : : #include <valent.h>
12 : :
13 : : #ifdef HAVE_GTK4
14 : : #include <gtk/gtk.h>
15 : : #endif /* HAVE_GTK4 */
16 : :
17 : : #include "valent-notification-upload.h"
18 : :
19 : : #define DEFAULT_ICON_SIZE 512
20 : :
21 : :
22 : : /**
23 : : * ValentNotificationUpload:
24 : : *
25 : : * A class for notification icon uploads.
26 : : *
27 : : * `ValentNotificationUpload` is a class that abstracts uploading notifications
28 : : * with icon payloads for `ValentNotificationPlugin`.
29 : : */
30 : :
31 : : struct _ValentNotificationUpload
32 : : {
33 : : ValentTransfer parent_instance;
34 : :
35 : : ValentDevice *device;
36 : : JsonNode *packet;
37 : : GIcon *icon;
38 : : };
39 : :
40 [ + + + - ]: 14 : G_DEFINE_FINAL_TYPE (ValentNotificationUpload, valent_notification_upload, VALENT_TYPE_TRANSFER)
41 : :
42 : : typedef enum {
43 : : PROP_DEVICE = 1,
44 : : PROP_ICON,
45 : : PROP_PACKET,
46 : : } ValentNotificationUploadProperty;
47 : :
48 : : static GParamSpec *properties[PROP_PACKET + 1] = { NULL, };
49 : :
50 : :
51 : : #ifdef HAVE_GTK4
52 : : static GtkIconTheme *
53 : 2 : _gtk_icon_theme_get_default (void)
54 : : {
55 : 2 : static GtkIconTheme *icon_theme = NULL;
56 : 2 : size_t guard = 0;
57 : :
58 [ + - + - ]: 2 : if (g_once_init_enter (&guard))
59 : : {
60 [ - + ]: 2 : if (gtk_is_initialized ())
61 : : {
62 : 0 : GdkDisplay *display = NULL;
63 : :
64 [ # # ]: 0 : if ((display = gdk_display_get_default ()) != NULL)
65 : 0 : icon_theme = gtk_icon_theme_get_for_display (display);
66 : : }
67 : :
68 : 2 : g_once_init_leave (&guard, 1);
69 : : }
70 : :
71 : 2 : return icon_theme;
72 : : }
73 : :
74 : : static int
75 : 0 : _gtk_icon_theme_get_largest_icon (GtkIconTheme *theme,
76 : : const char *name)
77 : : {
78 : 0 : g_autofree int *sizes = NULL;
79 : 0 : int ret = 0;
80 : :
81 [ # # # # : 0 : g_assert (GTK_IS_ICON_THEME (theme));
# # # # ]
82 [ # # ]: 0 : g_assert (name != NULL);
83 : :
84 [ # # ]: 0 : if (!gtk_icon_theme_has_icon (theme, name))
85 : : return ret;
86 : :
87 : 0 : sizes = gtk_icon_theme_get_icon_sizes (theme, name);
88 : :
89 [ # # ]: 0 : for (unsigned int i = 0; sizes[i] != 0; i++)
90 : : {
91 [ # # ]: 0 : if (sizes[i] == -1)
92 : : return -1;
93 : :
94 : 0 : if (sizes[i] > ret)
95 : : ret = sizes[i];
96 : : }
97 : :
98 : : return ret;
99 : : }
100 : :
101 : : static GFile *
102 : 2 : get_largest_icon_file (GIcon *icon)
103 : : {
104 : 2 : GtkIconTheme *icon_theme = NULL;
105 : 2 : const char * const *names;
106 : 4 : g_autoptr (GtkIconPaintable) info = NULL;
107 : :
108 [ + - + - : 2 : g_assert (G_IS_THEMED_ICON (icon));
- + - - ]
109 : :
110 : 2 : icon_theme = _gtk_icon_theme_get_default ();
111 [ - + ]: 2 : if (icon_theme == NULL)
112 : : return NULL;
113 : :
114 : 0 : names = g_themed_icon_get_names (G_THEMED_ICON (icon));
115 [ # # ]: 0 : for (size_t i = 0; names[i]; i++)
116 : : {
117 : 0 : int size;
118 : :
119 : 0 : size = _gtk_icon_theme_get_largest_icon (icon_theme, names[i]);
120 [ # # ]: 0 : if (size == 0)
121 : 0 : continue;
122 : :
123 : 0 : info = gtk_icon_theme_lookup_icon (icon_theme,
124 : : names[i],
125 : : NULL,
126 : : size,
127 : : 1,
128 : : GTK_TEXT_DIR_NONE,
129 : : 0);
130 : :
131 [ # # ]: 0 : if (info != NULL)
132 : 0 : return gtk_icon_paintable_get_file (info);
133 : : }
134 : :
135 : : return NULL;
136 : : }
137 : : #endif /* HAVE_GTK4 */
138 : :
139 : : static void
140 : 2 : on_size_prepared (GdkPixbufLoader *loader,
141 : : int width,
142 : : int height,
143 : : gpointer user_data)
144 : : {
145 : 2 : GdkPixbufFormat *format = gdk_pixbuf_loader_get_format (loader);
146 : :
147 [ - + ]: 2 : if (!gdk_pixbuf_format_is_scalable (format))
148 : : return;
149 : :
150 [ # # ]: 0 : if (width >= DEFAULT_ICON_SIZE || height >= DEFAULT_ICON_SIZE)
151 : : return;
152 : :
153 : 0 : gdk_pixbuf_loader_set_size (loader, DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE);
154 : : }
155 : :
156 : : static GBytes *
157 : 4 : valent_notification_upload_get_icon_bytes (GIcon *icon,
158 : : GCancellable *cancellable,
159 : : GError **error)
160 : : {
161 : 8 : g_autoptr (GBytes) bytes = NULL;
162 [ + + ]: 4 : g_autoptr (GdkPixbufLoader) loader = NULL;
163 : 4 : GdkPixbuf *pixbuf = NULL;
164 [ + + ]: 4 : g_autoptr (GError) warn = NULL;
165 : 4 : char *data;
166 : 4 : size_t size;
167 : :
168 [ + - + - : 4 : g_assert (G_IS_ICON (icon));
+ - - + ]
169 [ + - + - : 4 : g_assert (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
- + - - ]
170 [ + - - + ]: 4 : g_assert (error == NULL || *error == NULL);
171 : :
172 : : /* First try to get the bytes of the GIcon */
173 [ + - + + : 4 : if (G_IS_THEMED_ICON (icon))
- + ]
174 : : {
175 : 6 : g_autoptr (GFile) file = NULL;
176 : :
177 : : #ifdef HAVE_GTK4
178 : 2 : file = get_largest_icon_file (icon);
179 [ - + ]: 2 : if (file == NULL)
180 : : {
181 : 2 : g_set_error (error,
182 : : G_IO_ERROR,
183 : : G_IO_ERROR_FAILED,
184 : : "Failed to load themed icon");
185 : : }
186 : : #else
187 : : g_set_error (error,
188 : : G_IO_ERROR,
189 : : G_IO_ERROR_NOT_SUPPORTED,
190 : : "GTK is not initialized");
191 : : #endif /* HAVE_GTK4 */
192 : :
193 : 2 : if (file != NULL)
194 : 0 : bytes = g_file_load_bytes (file, cancellable, NULL, error);
195 : : }
196 [ + - + + : 2 : else if (G_IS_FILE_ICON (icon))
- + ]
197 : : {
198 : 1 : GFile *file;
199 : :
200 : 1 : file = g_file_icon_get_file (G_FILE_ICON (icon));
201 : 1 : bytes = g_file_load_bytes (file, cancellable, NULL, error);
202 : : }
203 [ + - - + : 1 : else if (G_IS_BYTES_ICON (icon))
- - ]
204 : : {
205 : 1 : GBytes *buffer;
206 : :
207 : 1 : buffer = g_bytes_icon_get_bytes (G_BYTES_ICON (icon));
208 : :
209 [ - + ]: 1 : if (buffer == NULL)
210 : : {
211 : 0 : g_set_error (error,
212 : : G_IO_ERROR,
213 : : G_IO_ERROR_FAILED,
214 : : "Failed to load icon bytes");
215 : 0 : return NULL;
216 : : }
217 : :
218 : 1 : bytes = g_bytes_ref (buffer);
219 : : }
220 [ # # # # : 0 : else if (GDK_IS_PIXBUF (icon))
# # ]
221 : : {
222 : 0 : bytes = gdk_pixbuf_read_pixel_bytes (GDK_PIXBUF (icon));
223 : : }
224 : :
225 [ + + ]: 4 : if (bytes == NULL)
226 : 2 : return NULL;
227 : :
228 : : /* Now attempt to load the bytes as a pixbuf */
229 : 2 : loader = gdk_pixbuf_loader_new ();
230 : :
231 : 2 : g_signal_connect_object (loader,
232 : : "size-prepared",
233 : : G_CALLBACK (on_size_prepared),
234 : : NULL, 0);
235 : :
236 [ + - - + ]: 4 : if (!gdk_pixbuf_loader_write_bytes (loader, bytes, &warn) ||
237 : 2 : !gdk_pixbuf_loader_close (loader, &warn))
238 : : {
239 : 0 : g_debug ("%s(): %s", G_STRFUNC, warn->message);
240 : 0 : return g_steal_pointer (&bytes);
241 : : }
242 : :
243 [ - + ]: 2 : if ((pixbuf = gdk_pixbuf_loader_get_pixbuf (loader)) == NULL)
244 : : {
245 : 0 : g_debug ("%s(): Failed to create pixbuf from bytes", G_STRFUNC);
246 : 0 : return g_steal_pointer (&bytes);
247 : : }
248 : :
249 [ - + ]: 2 : if (!gdk_pixbuf_save_to_buffer (pixbuf, &data, &size, "png", &warn, NULL))
250 : : {
251 : 0 : g_debug ("%s(): %s", G_STRFUNC, warn->message);
252 : 0 : return g_steal_pointer (&bytes);
253 : : }
254 : :
255 : 2 : return g_bytes_new_take (data, size);
256 : : }
257 : :
258 : : /*
259 : : * ValentTransfer
260 : : */
261 : : static void
262 : 4 : valent_notification_upload_execute_task (GTask *task,
263 : : gpointer source_object,
264 : : gpointer task_data,
265 : : GCancellable *cancellable)
266 : : {
267 : 4 : ValentNotificationUpload *self = VALENT_NOTIFICATION_UPLOAD (source_object);
268 : 4 : g_autoptr (ValentChannel) channel = NULL;
269 [ + - ]: 4 : g_autoptr (GIcon) icon = NULL;
270 [ + - + - ]: 4 : g_autoptr (JsonNode) packet = NULL;
271 [ + - + - ]: 4 : g_autoptr (GBytes) bytes = NULL;
272 [ - + ]: 4 : g_autoptr (GIOStream) target = NULL;
273 [ - + ]: 4 : g_autofree char *payload_hash = NULL;
274 : 4 : const uint8_t *payload_data = NULL;
275 : 4 : size_t payload_size = 0;
276 : 4 : gboolean ret = FALSE;
277 : 4 : GError *error = NULL;
278 : :
279 [ + - ]: 4 : g_assert (VALENT_IS_NOTIFICATION_UPLOAD (self));
280 : :
281 [ + - ]: 4 : if (g_task_return_error_if_cancelled (task))
282 : : return;
283 : :
284 : 4 : valent_object_lock (VALENT_OBJECT (self));
285 : 4 : channel = valent_device_ref_channel (self->device);
286 : 4 : icon = g_object_ref (self->icon);
287 : 4 : packet = json_node_ref (self->packet);
288 : 4 : valent_object_unlock (VALENT_OBJECT (self));
289 : :
290 [ - + ]: 4 : if (channel == NULL)
291 : : {
292 : 0 : g_task_return_new_error (task,
293 : : G_IO_ERROR,
294 : : G_IO_ERROR_NOT_CONNECTED,
295 : : "Device is disconnected");
296 : 0 : return;
297 : : }
298 : :
299 : : /* Try to get the icon bytes */
300 : 4 : bytes = valent_notification_upload_get_icon_bytes (icon, cancellable, &error);
301 : :
302 [ + + ]: 4 : if (bytes == NULL)
303 : 2 : return g_task_return_error (task, error);
304 : :
305 : : /* A payload hash is included, allowing the remote device to ignore icon
306 : : * transfers that it already has cached. */
307 : 2 : payload_data = g_bytes_get_data (bytes, &payload_size);
308 : 2 : payload_hash = g_compute_checksum_for_data (G_CHECKSUM_MD5,
309 : : payload_data,
310 : : payload_size);
311 : 2 : json_object_set_string_member (valent_packet_get_body (packet),
312 : : "payloadHash",
313 : : payload_hash);
314 : 2 : valent_packet_set_payload_size (packet, payload_size);
315 : :
316 : 2 : target = valent_channel_upload (channel, packet, cancellable, &error);
317 : :
318 [ - + ]: 2 : if (target == NULL)
319 : 0 : return g_task_return_error (task, error);
320 : :
321 : : /* Upload the icon */
322 : 2 : ret = g_output_stream_write_all (g_io_stream_get_output_stream (target),
323 : : payload_data,
324 : : payload_size,
325 : : NULL,
326 : : cancellable,
327 : : &error);
328 : :
329 [ - + ]: 2 : if (!ret)
330 : 0 : return g_task_return_error (task, error);
331 : :
332 : 2 : g_task_return_boolean (task, TRUE);
333 : : }
334 : :
335 : : static void
336 : 4 : valent_notification_upload_execute (ValentTransfer *transfer,
337 : : GCancellable *cancellable,
338 : : GAsyncReadyCallback callback,
339 : : gpointer user_data)
340 : : {
341 : 8 : g_autoptr (GTask) task = NULL;
342 : :
343 [ + - ]: 4 : g_assert (VALENT_IS_NOTIFICATION_UPLOAD (transfer));
344 [ + - + - : 4 : g_assert (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
- + - - ]
345 : :
346 : 4 : task = g_task_new (transfer, cancellable, callback, user_data);
347 [ + - ]: 4 : g_task_set_source_tag (task, valent_notification_upload_execute);
348 [ + - ]: 4 : g_task_run_in_thread (task, valent_notification_upload_execute_task);
349 : 4 : }
350 : :
351 : : /*
352 : : * GObject
353 : : */
354 : : static void
355 : 4 : valent_notification_upload_finalize (GObject *object)
356 : : {
357 : 4 : ValentNotificationUpload *self = VALENT_NOTIFICATION_UPLOAD (object);
358 : :
359 : 4 : valent_object_lock (VALENT_OBJECT (self));
360 [ + - ]: 4 : g_clear_object (&self->device);
361 [ + - ]: 4 : g_clear_object (&self->icon);
362 [ + - ]: 4 : g_clear_pointer (&self->packet, json_node_unref);
363 : 4 : valent_object_unlock (VALENT_OBJECT (self));
364 : :
365 : 4 : G_OBJECT_CLASS (valent_notification_upload_parent_class)->finalize (object);
366 : 4 : }
367 : :
368 : : static void
369 : 4 : valent_notification_upload_get_property (GObject *object,
370 : : guint prop_id,
371 : : GValue *value,
372 : : GParamSpec *pspec)
373 : : {
374 : 4 : ValentNotificationUpload *self = VALENT_NOTIFICATION_UPLOAD (object);
375 : :
376 [ + - + - ]: 4 : switch ((ValentNotificationUploadProperty)prop_id)
377 : : {
378 : : case PROP_DEVICE:
379 : 2 : valent_object_lock (VALENT_OBJECT (self));
380 : 2 : g_value_set_object (value, self->device);
381 : 2 : valent_object_unlock (VALENT_OBJECT (self));
382 : 2 : break;
383 : :
384 : : case PROP_ICON:
385 : 0 : valent_object_lock (VALENT_OBJECT (self));
386 : 0 : g_value_set_object (value, self->icon);
387 : 0 : valent_object_unlock (VALENT_OBJECT (self));
388 : 0 : break;
389 : :
390 : : case PROP_PACKET:
391 : 2 : valent_object_lock (VALENT_OBJECT (self));
392 : 2 : g_value_set_boxed (value, self->packet);
393 : 2 : valent_object_unlock (VALENT_OBJECT (self));
394 : 2 : break;
395 : :
396 : 0 : default:
397 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
398 : : }
399 : 4 : }
400 : :
401 : : static void
402 : 12 : valent_notification_upload_set_property (GObject *object,
403 : : guint prop_id,
404 : : const GValue *value,
405 : : GParamSpec *pspec)
406 : : {
407 : 12 : ValentNotificationUpload *self = VALENT_NOTIFICATION_UPLOAD (object);
408 : :
409 [ + + + - ]: 12 : switch ((ValentNotificationUploadProperty)prop_id)
410 : : {
411 : : case PROP_DEVICE:
412 : 4 : valent_object_lock (VALENT_OBJECT (self));
413 : 4 : self->device = g_value_dup_object (value);
414 : 4 : valent_object_unlock (VALENT_OBJECT (self));
415 : 4 : break;
416 : :
417 : : case PROP_ICON:
418 : 4 : valent_object_lock (VALENT_OBJECT (self));
419 : 4 : self->icon = g_value_dup_object (value);
420 : 4 : valent_object_unlock (VALENT_OBJECT (self));
421 : 4 : break;
422 : :
423 : : case PROP_PACKET:
424 : 4 : valent_object_lock (VALENT_OBJECT (self));
425 : 4 : self->packet = g_value_dup_boxed (value);
426 : 4 : valent_object_unlock (VALENT_OBJECT (self));
427 : 4 : break;
428 : :
429 : 0 : default:
430 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
431 : : }
432 : 12 : }
433 : :
434 : : static void
435 : 1 : valent_notification_upload_class_init (ValentNotificationUploadClass *klass)
436 : : {
437 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
438 : 1 : ValentTransferClass *transfer_class = VALENT_TRANSFER_CLASS (klass);
439 : :
440 : 1 : object_class->finalize = valent_notification_upload_finalize;
441 : 1 : object_class->get_property = valent_notification_upload_get_property;
442 : 1 : object_class->set_property = valent_notification_upload_set_property;
443 : :
444 : 1 : transfer_class->execute = valent_notification_upload_execute;
445 : :
446 : : /**
447 : : * ValentNotificationUpload:device:
448 : : *
449 : : * The [class@Valent.Device] this transfer is for.
450 : : */
451 : 2 : properties [PROP_DEVICE] =
452 : 1 : g_param_spec_object ("device", NULL, NULL,
453 : : VALENT_TYPE_DEVICE,
454 : : (G_PARAM_READWRITE |
455 : : G_PARAM_CONSTRUCT_ONLY |
456 : : G_PARAM_EXPLICIT_NOTIFY |
457 : : G_PARAM_STATIC_STRINGS));
458 : :
459 : : /**
460 : : * ValentNotificationUpload:icon:
461 : : *
462 : : * The [iface@Gio.Icon] for the notification.
463 : : */
464 : 2 : properties [PROP_ICON] =
465 : 1 : g_param_spec_object ("icon", NULL, NULL,
466 : : G_TYPE_ICON,
467 : : (G_PARAM_READWRITE |
468 : : G_PARAM_CONSTRUCT_ONLY |
469 : : G_PARAM_EXPLICIT_NOTIFY |
470 : : G_PARAM_STATIC_STRINGS));
471 : :
472 : : /**
473 : : * ValentNotificationUpload:packet:
474 : : *
475 : : * The packet to send the payload with.
476 : : */
477 : 2 : properties [PROP_PACKET] =
478 : 1 : g_param_spec_boxed ("packet", NULL, NULL,
479 : : JSON_TYPE_NODE,
480 : : (G_PARAM_READWRITE |
481 : : G_PARAM_CONSTRUCT_ONLY |
482 : : G_PARAM_EXPLICIT_NOTIFY |
483 : : G_PARAM_STATIC_STRINGS));
484 : :
485 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
486 : 1 : }
487 : :
488 : : static void
489 : 4 : valent_notification_upload_init (ValentNotificationUpload *self)
490 : : {
491 : 4 : }
492 : :
493 : : /**
494 : : * valent_notification_upload_new:
495 : : * @device: a `ValentDevice`
496 : : * @packet: a KDE Connect packet
497 : : * @icon: a `GIcon`
498 : : *
499 : : * Create a new `ValentNotificationUpload` for @packet and @icon.
500 : : *
501 : : * Returns: (transfer full): a `ValentNotificationUpload`
502 : : */
503 : : ValentTransfer *
504 : 4 : valent_notification_upload_new (ValentDevice *device,
505 : : JsonNode *packet,
506 : : GIcon *icon)
507 : : {
508 [ + - ]: 4 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
509 [ - + ]: 4 : g_return_val_if_fail (VALENT_IS_PACKET (packet), NULL);
510 [ + - + - : 4 : g_return_val_if_fail (G_IS_ICON (icon), NULL);
+ - - + ]
511 : :
512 : 4 : return g_object_new (VALENT_TYPE_NOTIFICATION_UPLOAD,
513 : : "device", device,
514 : : "icon", icon,
515 : : "packet", packet,
516 : : NULL);
517 : : }
518 : :
|