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-clipboard-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <json-glib/json-glib.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-clipboard-plugin.h"
13 : :
14 : :
15 : : struct _ValentClipboardPlugin
16 : : {
17 : : ValentDevicePlugin parent_instance;
18 : :
19 : : ValentClipboard *clipboard;
20 : : unsigned long changed_id;
21 : :
22 : : char *remote_text;
23 : : int64_t remote_timestamp;
24 : : int64_t local_timestamp;
25 : : unsigned int auto_pull : 1;
26 : : unsigned int auto_push : 1;
27 : : };
28 : :
29 [ + + + - ]: 49 : G_DEFINE_FINAL_TYPE (ValentClipboardPlugin, valent_clipboard_plugin, VALENT_TYPE_DEVICE_PLUGIN)
30 : :
31 : :
32 : : /*
33 : : * Local Clipboard
34 : : */
35 : : static void
36 : 2 : valent_clipboard_plugin_clipboard (ValentClipboardPlugin *self,
37 : : const char *content)
38 : : {
39 : 2 : g_autoptr (JsonBuilder) builder = NULL;
40 [ - + - - ]: 2 : g_autoptr (JsonNode) packet = NULL;
41 : :
42 [ + - ]: 2 : g_return_if_fail (VALENT_IS_CLIPBOARD_PLUGIN (self));
43 : :
44 [ + - ]: 2 : if (content == NULL)
45 : : return;
46 : :
47 : 2 : valent_packet_init (&builder, "kdeconnect.clipboard");
48 : 2 : json_builder_set_member_name (builder, "content");
49 : 2 : json_builder_add_string_value (builder, content);
50 : 2 : packet = valent_packet_end (&builder);
51 : :
52 [ + - ]: 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
53 : : }
54 : :
55 : : static void
56 : 3 : valent_clipboard_plugin_clipboard_connect (ValentClipboardPlugin *self,
57 : : const char *content,
58 : : int64_t timestamp)
59 : : {
60 : 3 : g_autoptr (JsonBuilder) builder = NULL;
61 [ - + - - ]: 3 : g_autoptr (JsonNode) packet = NULL;
62 : :
63 [ + - ]: 3 : g_return_if_fail (VALENT_IS_CLIPBOARD_PLUGIN (self));
64 : :
65 [ + - ]: 3 : if (content == NULL)
66 : : return;
67 : :
68 : 3 : valent_packet_init (&builder, "kdeconnect.clipboard.connect");
69 : 3 : json_builder_set_member_name (builder, "content");
70 : 3 : json_builder_add_string_value (builder, content);
71 : 3 : json_builder_set_member_name (builder, "timestamp");
72 : 3 : json_builder_add_int_value (builder, timestamp);
73 : 3 : packet = valent_packet_end (&builder);
74 : :
75 [ + - ]: 3 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
76 : : }
77 : :
78 : : static void
79 : 4 : valent_clipboard_read_text_cb (ValentClipboard *clipboard,
80 : : GAsyncResult *result,
81 : : ValentClipboardPlugin *self)
82 : : {
83 : 4 : g_autoptr (GError) error = NULL;
84 [ - + - + ]: 4 : g_autofree char *text = NULL;
85 : :
86 [ + - ]: 4 : g_assert (VALENT_IS_CLIPBOARD (clipboard));
87 [ - + ]: 4 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
88 : :
89 : 4 : text = valent_clipboard_read_text_finish (clipboard, result, &error);
90 : :
91 [ - + ]: 4 : if (error != NULL)
92 : : {
93 [ # # ]: 0 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
94 : 0 : g_debug ("%s(): %s", G_STRFUNC, error->message);
95 : :
96 : 0 : return;
97 : : }
98 : :
99 : : /* Skip if the local clipboard is empty, or already synced with the device */
100 [ + - + + ]: 4 : if (text == NULL || g_strcmp0 (self->remote_text, text) == 0)
101 : 2 : return;
102 : :
103 : 2 : valent_clipboard_plugin_clipboard (self, text);
104 : : }
105 : :
106 : : static void
107 : 3 : valent_clipboard_read_text_connect_cb (ValentClipboard *clipboard,
108 : : GAsyncResult *result,
109 : : ValentClipboardPlugin *self)
110 : : {
111 : 3 : g_autofree char *text = NULL;
112 : 3 : g_autoptr (GError) error = NULL;
113 : :
114 [ + - ]: 3 : g_assert (VALENT_IS_CLIPBOARD (clipboard));
115 [ - + ]: 3 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
116 : :
117 : 3 : text = valent_clipboard_read_text_finish (clipboard, result, &error);
118 : :
119 [ - + ]: 3 : if (error != NULL)
120 : : {
121 [ # # ]: 0 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
122 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
123 : :
124 : 0 : return;
125 : : }
126 : :
127 [ + - ]: 3 : if (text == NULL)
128 : : return;
129 : :
130 [ - + ]: 3 : valent_clipboard_plugin_clipboard_connect (self, text, self->local_timestamp);
131 : : }
132 : :
133 : : static void
134 : 3 : valent_clipboard_write_text_cb (ValentClipboard *clipboard,
135 : : GAsyncResult *result,
136 : : gpointer user_data)
137 : : {
138 : 6 : g_autoptr (GError) error = NULL;
139 : :
140 [ + - ]: 3 : g_assert (VALENT_IS_CLIPBOARD (clipboard));
141 : :
142 [ - + - - ]: 3 : if (!valent_clipboard_write_text_finish (clipboard, result, &error) &&
143 : 0 : !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
144 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
145 : 3 : }
146 : :
147 : : static void
148 : 2 : on_auto_pull_changed (GSettings *settings,
149 : : const char *key,
150 : : ValentClipboardPlugin *self)
151 : : {
152 : 2 : ValentDevice *device;
153 : 2 : ValentDeviceState state;
154 : 4 : g_autoptr (GCancellable) destroy = NULL;
155 : :
156 [ + - + - : 2 : g_assert (G_IS_SETTINGS (settings));
- + - - ]
157 [ - + ]: 2 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
158 : :
159 : 2 : self->auto_pull = g_settings_get_boolean (settings, "auto-pull");
160 : :
161 [ + + ]: 2 : if (!self->auto_pull)
162 : : return;
163 : :
164 : 1 : device = valent_extension_get_object (VALENT_EXTENSION (self));
165 : 1 : state = valent_device_get_state (device);
166 : :
167 [ - + ]: 1 : if ((state & VALENT_DEVICE_STATE_CONNECTED) != 0 ||
168 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0)
169 : : return;
170 : :
171 : 0 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
172 [ # # ]: 0 : valent_clipboard_write_text (self->clipboard,
173 : 0 : self->remote_text,
174 : : destroy,
175 : : (GAsyncReadyCallback)valent_clipboard_write_text_cb,
176 : : NULL);
177 : : }
178 : :
179 : : static void
180 : 2 : on_auto_push_changed (GSettings *settings,
181 : : const char *key,
182 : : ValentClipboardPlugin *self)
183 : : {
184 : 2 : ValentDevice *device;
185 : 2 : ValentDeviceState state;
186 : 4 : g_autoptr (GCancellable) destroy = NULL;
187 : :
188 [ + - + - : 2 : g_assert (G_IS_SETTINGS (settings));
- + - - ]
189 [ - + ]: 2 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
190 : :
191 : 2 : self->auto_push = g_settings_get_boolean (settings, "auto-push");
192 : :
193 [ + + ]: 2 : if (!self->auto_push)
194 : : return;
195 : :
196 : 1 : device = valent_extension_get_object (VALENT_EXTENSION (self));
197 : 1 : state = valent_device_get_state (device);
198 : :
199 [ - + ]: 1 : if ((state & VALENT_DEVICE_STATE_CONNECTED) != 0 ||
200 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0)
201 : : return;
202 : :
203 : 0 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
204 [ # # ]: 0 : valent_clipboard_read_text (valent_clipboard_get_default (),
205 : : destroy,
206 : : (GAsyncReadyCallback)valent_clipboard_read_text_cb,
207 : : self);
208 : :
209 : : }
210 : :
211 : : static void
212 : 5 : on_clipboard_changed (ValentClipboard *clipboard,
213 : : ValentClipboardPlugin *self)
214 : : {
215 : 10 : g_autoptr (GCancellable) destroy = NULL;
216 : :
217 [ + - ]: 5 : g_assert (VALENT_IS_CLIPBOARD (clipboard));
218 [ - + ]: 5 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
219 : :
220 : 5 : self->local_timestamp = valent_clipboard_get_timestamp (clipboard);
221 : :
222 [ + + ]: 5 : if (!self->auto_push)
223 : : return;
224 : :
225 : 3 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
226 [ + - ]: 3 : valent_clipboard_read_text (clipboard,
227 : : destroy,
228 : : (GAsyncReadyCallback)valent_clipboard_read_text_cb,
229 : : self);
230 : : }
231 : :
232 : : /*
233 : : * Remote Clipboard
234 : : */
235 : : static void
236 : 2 : valent_clipboard_plugin_handle_clipboard (ValentClipboardPlugin *self,
237 : : JsonNode *packet)
238 : : {
239 : 2 : const char *content;
240 : 2 : g_autoptr (GCancellable) destroy = NULL;
241 : :
242 [ + - ]: 2 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
243 [ - + ]: 2 : g_assert (VALENT_IS_PACKET (packet));
244 : :
245 [ - + ]: 2 : if (!valent_packet_get_string (packet, "content", &content))
246 : : {
247 : 0 : g_debug ("%s(): expected \"content\" field holding a string",
248 : : G_STRFUNC);
249 : 0 : return;
250 : : }
251 : :
252 : : /* The remote clipboard content is cached, for manual control over syncing,
253 : : * because there is no packet type for requesting it on-demand. */
254 [ + + ]: 2 : g_clear_pointer (&self->remote_text, g_free);
255 [ - + ]: 2 : self->remote_text = g_strdup (content);
256 : 2 : self->remote_timestamp = valent_timestamp_ms ();
257 : :
258 [ + + ]: 2 : if (!self->auto_pull)
259 : : return;
260 : :
261 : 1 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
262 [ + - ]: 1 : valent_clipboard_write_text (self->clipboard,
263 : 1 : self->remote_text,
264 : : destroy,
265 : : (GAsyncReadyCallback)valent_clipboard_write_text_cb,
266 : : NULL);
267 : : }
268 : :
269 : : static void
270 : 2 : valent_clipboard_plugin_handle_clipboard_connect (ValentClipboardPlugin *self,
271 : : JsonNode *packet)
272 : : {
273 : 2 : int64_t timestamp;
274 : 2 : const char *content;
275 : 2 : g_autoptr (GCancellable) destroy = NULL;
276 : :
277 [ + - ]: 2 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
278 [ - + ]: 2 : g_assert (VALENT_IS_PACKET (packet));
279 : :
280 [ - + ]: 2 : if (!valent_packet_get_int (packet, "timestamp", ×tamp))
281 : : {
282 : 0 : g_debug ("%s(): expected \"timestamp\" field holding an integer",
283 : : G_STRFUNC);
284 : 0 : return;
285 : : }
286 : :
287 [ - + ]: 2 : if (!valent_packet_get_string (packet, "content", &content))
288 : : {
289 : 0 : g_debug ("%s(): expected \"content\" field holding a string",
290 : : G_STRFUNC);
291 : 0 : return;
292 : : }
293 : :
294 : : /* The remote clipboard content is cached, for manual control over syncing,
295 : : * because there is no packet type for requesting it on-demand. */
296 [ + + ]: 2 : g_clear_pointer (&self->remote_text, g_free);
297 [ - + ]: 2 : self->remote_text = g_strdup (content);
298 : 2 : self->remote_timestamp = timestamp;
299 : :
300 [ + + ]: 2 : if (self->remote_timestamp <= self->local_timestamp)
301 : : return;
302 : :
303 [ + - ]: 1 : if (!self->auto_pull)
304 : : return;
305 : :
306 : 1 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
307 [ + - ]: 1 : valent_clipboard_write_text (self->clipboard,
308 : 1 : self->remote_text,
309 : : destroy,
310 : : (GAsyncReadyCallback)valent_clipboard_write_text_cb,
311 : : NULL);
312 : : }
313 : :
314 : : /*
315 : : * GActions
316 : : */
317 : : static void
318 : 1 : clipboard_pull_action (GSimpleAction *action,
319 : : GVariant *parameter,
320 : : gpointer user_data)
321 : : {
322 : 1 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (user_data);
323 : 0 : g_autoptr (GCancellable) destroy = NULL;
324 : :
325 [ + - ]: 1 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
326 : :
327 [ + - - + ]: 1 : if (self->remote_text == NULL || *self->remote_text == '\0')
328 : : {
329 : 0 : g_debug ("%s(): remote clipboard empty", G_STRFUNC);
330 : 0 : return;
331 : : }
332 : :
333 : 1 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
334 [ + - ]: 1 : valent_clipboard_write_text (self->clipboard,
335 : 1 : self->remote_text,
336 : : destroy,
337 : : (GAsyncReadyCallback)valent_clipboard_write_text_cb,
338 : : NULL);
339 : : }
340 : :
341 : : static void
342 : 1 : clipboard_push_action (GSimpleAction *action,
343 : : GVariant *parameter,
344 : : gpointer user_data)
345 : : {
346 : 1 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (user_data);
347 : 2 : g_autoptr (GCancellable) destroy = NULL;
348 : :
349 [ + - ]: 1 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
350 : :
351 : 1 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
352 [ + - ]: 1 : valent_clipboard_read_text (valent_clipboard_get_default (),
353 : : destroy,
354 : : (GAsyncReadyCallback)valent_clipboard_read_text_cb,
355 : : self);
356 : 1 : }
357 : :
358 : : static const GActionEntry actions[] = {
359 : : {"pull", clipboard_pull_action, NULL, NULL, NULL},
360 : : {"push", clipboard_push_action, NULL, NULL, NULL}
361 : : };
362 : :
363 : : /*
364 : : * ValentDevicePlugin
365 : : */
366 : : static void
367 : 16 : valent_clipboard_plugin_update_state (ValentDevicePlugin *plugin,
368 : : ValentDeviceState state)
369 : : {
370 : 16 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (plugin);
371 : 16 : gboolean available;
372 : :
373 [ + - ]: 16 : g_assert (VALENT_IS_CLIPBOARD_PLUGIN (self));
374 : :
375 : 16 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
376 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
377 : :
378 : 16 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
379 : :
380 [ + + ]: 16 : if (available)
381 : : {
382 [ + - ]: 5 : if (self->changed_id == 0)
383 : 5 : self->changed_id = g_signal_connect (self->clipboard,
384 : : "changed",
385 : : G_CALLBACK (on_clipboard_changed),
386 : : self);
387 : :
388 [ + + ]: 5 : if (self->auto_push)
389 : : {
390 : 19 : g_autoptr (GCancellable) destroy = NULL;
391 : :
392 : 3 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
393 [ + - ]: 3 : valent_clipboard_read_text (self->clipboard,
394 : : destroy,
395 : : (GAsyncReadyCallback)valent_clipboard_read_text_connect_cb,
396 : : self);
397 : : }
398 : : }
399 : : else
400 : : {
401 [ + + ]: 11 : g_clear_signal_handler (&self->changed_id, self->clipboard);
402 : : }
403 : 16 : }
404 : :
405 : : static void
406 : 4 : valent_clipboard_plugin_handle_packet (ValentDevicePlugin *plugin,
407 : : const char *type,
408 : : JsonNode *packet)
409 : : {
410 : 4 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (plugin);
411 : :
412 [ + - ]: 4 : g_assert (VALENT_IS_DEVICE_PLUGIN (plugin));
413 [ - + ]: 4 : g_assert (type != NULL);
414 [ - + ]: 4 : g_assert (VALENT_IS_PACKET (packet));
415 : :
416 : : /* The remote clipboard content changed */
417 [ + + ]: 4 : if (g_str_equal (type, "kdeconnect.clipboard"))
418 : 2 : valent_clipboard_plugin_handle_clipboard (self, packet);
419 : :
420 [ + - ]: 2 : else if (g_str_equal (type, "kdeconnect.clipboard.connect"))
421 : 2 : valent_clipboard_plugin_handle_clipboard_connect (self, packet);
422 : :
423 : : else
424 : 0 : g_assert_not_reached ();
425 : 4 : }
426 : :
427 : : /*
428 : : * GObject
429 : : */
430 : : static void
431 : 5 : valent_clipboard_plugin_constructed (GObject *object)
432 : : {
433 : 5 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (object);
434 : 5 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
435 : 5 : GSettings *settings = NULL;
436 : :
437 : 5 : g_action_map_add_action_entries (G_ACTION_MAP (plugin),
438 : : actions,
439 : : G_N_ELEMENTS (actions),
440 : : plugin);
441 : :
442 : 5 : settings = valent_extension_get_settings (VALENT_EXTENSION (plugin));
443 : 5 : self->auto_pull = g_settings_get_boolean (settings, "auto-pull");
444 : 5 : g_signal_connect_object (settings,
445 : : "changed::auto-pull",
446 : : G_CALLBACK (on_auto_pull_changed),
447 : : self, 0);
448 : :
449 : 5 : self->auto_push = g_settings_get_boolean (settings, "auto-push");
450 : 5 : g_signal_connect_object (settings,
451 : : "changed::auto-push",
452 : : G_CALLBACK (on_auto_push_changed),
453 : : self, 0);
454 : :
455 : 5 : self->clipboard = valent_clipboard_get_default ();
456 : 5 : self->local_timestamp = valent_clipboard_get_timestamp (self->clipboard);
457 : :
458 : 5 : G_OBJECT_CLASS (valent_clipboard_plugin_parent_class)->constructed (object);
459 : 5 : }
460 : :
461 : : static void
462 : 10 : valent_clipboard_plugin_dispose (GObject *object)
463 : : {
464 : 10 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (object);
465 : :
466 [ - + ]: 10 : g_clear_signal_handler (&self->changed_id, self->clipboard);
467 : :
468 : 10 : G_OBJECT_CLASS (valent_clipboard_plugin_parent_class)->dispose (object);
469 : 10 : }
470 : :
471 : : static void
472 : 5 : valent_clipboard_plugin_finalize (GObject *object)
473 : : {
474 : 5 : ValentClipboardPlugin *self = VALENT_CLIPBOARD_PLUGIN (object);
475 : :
476 [ + + ]: 5 : g_clear_pointer (&self->remote_text, g_free);
477 : :
478 : 5 : G_OBJECT_CLASS (valent_clipboard_plugin_parent_class)->finalize (object);
479 : 5 : }
480 : :
481 : : static void
482 : 2 : valent_clipboard_plugin_class_init (ValentClipboardPluginClass *klass)
483 : : {
484 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
485 : 2 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
486 : :
487 : 2 : object_class->constructed = valent_clipboard_plugin_constructed;
488 : 2 : object_class->dispose = valent_clipboard_plugin_dispose;
489 : 2 : object_class->finalize = valent_clipboard_plugin_finalize;
490 : :
491 : 2 : plugin_class->handle_packet = valent_clipboard_plugin_handle_packet;
492 : 2 : plugin_class->update_state = valent_clipboard_plugin_update_state;
493 : : }
494 : :
495 : : static void
496 : 5 : valent_clipboard_plugin_init (ValentClipboardPlugin *self)
497 : : {
498 : 5 : }
499 : :
|