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