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-device"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <inttypes.h>
9 : : #include <math.h>
10 : :
11 : : #include <glib/gi18n-lib.h>
12 : : #include <gio/gio.h>
13 : : #include <libvalent-core.h>
14 : :
15 : : #include "../core/valent-component-private.h"
16 : : #include "valent-certificate.h"
17 : : #include "valent-channel.h"
18 : : #include "valent-device-common.h"
19 : : #include "valent-device-enums.h"
20 : : #include "valent-device-plugin.h"
21 : : #include "valent-packet.h"
22 : :
23 : : #include "valent-device.h"
24 : : #include "valent-device-private.h"
25 : :
26 : : #define DEVICE_TYPE_DESKTOP "desktop"
27 : : #define DEVICE_TYPE_LAPTOP "laptop"
28 : : #define DEVICE_TYPE_PHONE "phone"
29 : : #define DEVICE_TYPE_TABLET "tablet"
30 : : #define DEVICE_TYPE_TV "tv"
31 : :
32 : : #define PAIR_REQUEST_ID "pair-request"
33 : : #define PAIR_REQUEST_TIMEOUT (30)
34 : : #define PAIR_CLOCK_SKEW_THRESHOLD (1800)
35 : :
36 : :
37 : : /**
38 : : * ValentDevice:
39 : : *
40 : : * A class representing a remote device, such as a smartphone or desktop.
41 : : *
42 : : * Device functionality is limited to pairing and sending packets, while other
43 : : * functionality is delegated to [class@Valent.DevicePlugin] extensions.
44 : : *
45 : : * `ValentDevice` implements the [iface@Gio.ActionGroup] interface, acting as an
46 : : * aggregate action group for plugins. Plugin actions are automatically included
47 : : * in the device action group with the plugin module name as a prefix
48 : : * (eg. `share.files`).
49 : : *
50 : : * Since: 1.0
51 : : */
52 : :
53 : : struct _ValentDevice
54 : : {
55 : : ValentResource parent_instance;
56 : :
57 : : ValentContext *context;
58 : :
59 : : /* Properties */
60 : : char *icon_name;
61 : : char *id;
62 : : char *name;
63 : : char *type;
64 : : char **incoming_capabilities;
65 : : char **outgoing_capabilities;
66 : : int64_t protocol_version;
67 : :
68 : : /* State */
69 : : GListModel *channels;
70 : : ValentChannel *channel;
71 : : gboolean paired;
72 : : unsigned int incoming_pair;
73 : : unsigned int outgoing_pair;
74 : : int64_t pair_timestamp;
75 : :
76 : : /* Plugins */
77 : : PeasEngine *engine;
78 : : GHashTable *plugins;
79 : : GHashTable *handlers;
80 : : GHashTable *actions;
81 : : GMenu *menu;
82 : : };
83 : :
84 : : static void g_action_group_iface_init (GActionGroupInterface *iface);
85 : :
86 [ + + + - ]: 4161 : G_DEFINE_FINAL_TYPE_WITH_CODE (ValentDevice, valent_device, VALENT_TYPE_RESOURCE,
87 : : G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, g_action_group_iface_init))
88 : :
89 : : typedef enum {
90 : : PROP_CONTEXT = 1,
91 : : PROP_ICON_NAME,
92 : : PROP_ID,
93 : : PROP_NAME,
94 : : PROP_STATE,
95 : : } ValentDeviceProperty;
96 : :
97 : : static GParamSpec *properties[PROP_STATE + 1] = { NULL, };
98 : :
99 : :
100 : : /*
101 : : * GActionGroup
102 : : */
103 : : static void
104 : 43 : valent_device_activate_action (GActionGroup *action_group,
105 : : const char *action_name,
106 : : GVariant *parameter)
107 : : {
108 : 43 : ValentDevice *self = VALENT_DEVICE (action_group);
109 : 43 : GAction *action;
110 : :
111 [ + - ]: 43 : if ((action = g_hash_table_lookup (self->actions, action_name)) != NULL)
112 : 43 : g_action_activate (action, parameter);
113 : 43 : }
114 : :
115 : : static void
116 : 3 : valent_device_change_action_state (GActionGroup *action_group,
117 : : const char *action_name,
118 : : GVariant *value)
119 : : {
120 : 3 : ValentDevice *self = VALENT_DEVICE (action_group);
121 : 3 : GAction *action;
122 : :
123 [ + - ]: 3 : if ((action = g_hash_table_lookup (self->actions, action_name)) != NULL)
124 : 3 : g_action_change_state (action, value);
125 : 3 : }
126 : :
127 : : static char **
128 : 9 : valent_device_list_actions (GActionGroup *action_group)
129 : : {
130 : 9 : ValentDevice *self = VALENT_DEVICE (action_group);
131 : 18 : g_auto (GStrv) actions = NULL;
132 : 9 : GHashTableIter iter;
133 : 9 : gpointer key;
134 : 9 : unsigned int i = 0;
135 : :
136 [ - + ]: 9 : actions = g_new0 (char *, g_hash_table_size (self->actions) + 1);
137 : :
138 : 9 : g_hash_table_iter_init (&iter, self->actions);
139 : :
140 [ + + ]: 39 : while (g_hash_table_iter_next (&iter, &key, NULL))
141 [ - + ]: 60 : actions[i++] = g_strdup (key);
142 : :
143 : 9 : return g_steal_pointer (&actions);
144 : : }
145 : :
146 : : static gboolean
147 : 186 : valent_device_query_action (GActionGroup *action_group,
148 : : const char *action_name,
149 : : gboolean *enabled,
150 : : const GVariantType **parameter_type,
151 : : const GVariantType **state_type,
152 : : GVariant **state_hint,
153 : : GVariant **state)
154 : : {
155 : 186 : ValentDevice *self = VALENT_DEVICE (action_group);
156 : 186 : GAction *action;
157 : :
158 [ + + ]: 186 : if ((action = g_hash_table_lookup (self->actions, action_name)) == NULL)
159 : : return FALSE;
160 : :
161 [ + + ]: 166 : if (enabled)
162 : 90 : *enabled = g_action_get_enabled (action);
163 : :
164 [ + + ]: 166 : if (parameter_type)
165 : 29 : *parameter_type = g_action_get_parameter_type (action);
166 : :
167 [ + + ]: 166 : if (state_type)
168 : 1 : *state_type = g_action_get_state_type (action);
169 : :
170 [ + + ]: 166 : if (state_hint)
171 : 1 : *state_hint = g_action_get_state_hint (action);
172 : :
173 [ + + ]: 166 : if (state)
174 : 48 : *state = g_action_get_state (action);
175 : :
176 : : return TRUE;
177 : : }
178 : :
179 : : static void
180 : 32 : g_action_group_iface_init (GActionGroupInterface *iface)
181 : : {
182 : 32 : iface->activate_action = valent_device_activate_action;
183 : 32 : iface->change_action_state = valent_device_change_action_state;
184 : 32 : iface->list_actions = valent_device_list_actions;
185 : 32 : iface->query_action = valent_device_query_action;
186 : 32 : }
187 : :
188 : :
189 : : /*
190 : : * Private plugin methods
191 : : */
192 : : static void
193 : 281 : on_plugin_action_added (GActionGroup *action_group,
194 : : const char *action_name,
195 : : ValentPlugin *plugin)
196 : : {
197 : 281 : ValentDevice *self = VALENT_DEVICE (plugin->parent);
198 : 562 : g_autofree char *full_name = NULL;
199 : 281 : GAction *action;
200 : :
201 : 281 : full_name = g_strdup_printf ("%s.%s",
202 : 281 : peas_plugin_info_get_module_name (plugin->info),
203 : : action_name);
204 : 281 : action = g_action_map_lookup_action (G_ACTION_MAP (action_group),
205 : : action_name);
206 : :
207 : 281 : g_hash_table_replace (self->actions,
208 [ - + ]: 562 : g_strdup (full_name),
209 : : g_object_ref (action));
210 : 281 : g_action_group_action_added (G_ACTION_GROUP (plugin->parent), full_name);
211 : 281 : }
212 : :
213 : : static void
214 : 468 : on_plugin_action_enabled_changed (GActionGroup *action_group,
215 : : const char *action_name,
216 : : gboolean enabled,
217 : : ValentPlugin *plugin)
218 : : {
219 : 936 : g_autofree char *full_name = NULL;
220 : :
221 : 468 : full_name = g_strdup_printf ("%s.%s",
222 : 468 : peas_plugin_info_get_module_name (plugin->info),
223 : : action_name);
224 : 468 : g_action_group_action_enabled_changed (G_ACTION_GROUP (plugin->parent),
225 : : full_name,
226 : : enabled);
227 : 468 : }
228 : :
229 : : static void
230 : 280 : on_plugin_action_removed (GActionGroup *action_group,
231 : : const char *action_name,
232 : : ValentPlugin *plugin)
233 : : {
234 : 280 : ValentDevice *self = VALENT_DEVICE (plugin->parent);
235 : 560 : g_autofree char *full_name = NULL;
236 : :
237 : 280 : full_name = g_strdup_printf ("%s.%s",
238 : 280 : peas_plugin_info_get_module_name (plugin->info),
239 : : action_name);
240 : :
241 : 280 : g_action_group_action_removed (G_ACTION_GROUP (plugin->parent), full_name);
242 : 280 : g_hash_table_remove (self->actions, full_name);
243 : 280 : }
244 : :
245 : : static void
246 : 19 : on_plugin_action_state_changed (GActionGroup *action_group,
247 : : const char *action_name,
248 : : GVariant *value,
249 : : ValentPlugin *plugin)
250 : : {
251 : 38 : g_autofree char *full_name = NULL;
252 : :
253 : 19 : full_name = g_strdup_printf ("%s.%s",
254 : 19 : peas_plugin_info_get_module_name (plugin->info),
255 : : action_name);
256 : 19 : g_action_group_action_state_changed (G_ACTION_GROUP (plugin->parent),
257 : : full_name,
258 : : value);
259 : 19 : }
260 : :
261 : : static void
262 : 115 : valent_device_enable_plugin (ValentDevice *device,
263 : : ValentPlugin *plugin)
264 : : {
265 : 230 : g_auto (GStrv) actions = NULL;
266 [ + - ]: 115 : g_autofree char *urn = NULL;
267 : 115 : const char *incoming = NULL;
268 : 115 : const char *title = NULL;
269 : 115 : const char *description = NULL;
270 : :
271 [ - + ]: 115 : g_assert (VALENT_IS_DEVICE (device));
272 [ + - ]: 115 : g_assert (plugin != NULL);
273 : :
274 : 115 : title = peas_plugin_info_get_name (plugin->info);
275 : 115 : description = peas_plugin_info_get_description (plugin->info);
276 : 115 : plugin->extension = peas_engine_create_extension (device->engine,
277 : : plugin->info,
278 : : VALENT_TYPE_DEVICE_PLUGIN,
279 : : "context", plugin->context,
280 : : "source", plugin->parent,
281 : : "title", title,
282 : : "description", description,
283 : : NULL);
284 [ + - ]: 115 : g_return_if_fail (G_IS_OBJECT (plugin->extension));
285 : :
286 : : /* Register packet handlers
287 : : */
288 : 115 : incoming = peas_plugin_info_get_external_data (plugin->info,
289 : : "DevicePluginIncoming");
290 [ + - ]: 115 : if (incoming != NULL)
291 : : {
292 : 230 : g_auto (GStrv) capabilities = NULL;
293 : :
294 : 115 : capabilities = g_strsplit (incoming, ";", -1);
295 : :
296 [ + + ]: 333 : for (unsigned int i = 0; capabilities[i] != NULL; i++)
297 : : {
298 : 218 : GPtrArray *handlers = NULL;
299 : 218 : const char *type = capabilities[i];
300 : :
301 [ + + ]: 218 : if ((handlers = g_hash_table_lookup (device->handlers, type)) == NULL)
302 : : {
303 : 208 : handlers = g_ptr_array_new ();
304 [ - + ]: 416 : g_hash_table_insert (device->handlers, g_strdup (type), handlers);
305 : : }
306 : :
307 : 218 : g_ptr_array_add (handlers, plugin->extension);
308 : : }
309 : : }
310 : :
311 : : /* Register plugin actions
312 : : */
313 : 115 : actions = g_action_group_list_actions (G_ACTION_GROUP (plugin->extension));
314 [ + + ]: 396 : for (unsigned int i = 0; actions[i] != NULL; i++)
315 : : {
316 : 281 : on_plugin_action_added (G_ACTION_GROUP (plugin->extension),
317 : : actions[i],
318 : : plugin);
319 : : }
320 : :
321 : 115 : g_signal_connect (plugin->extension,
322 : : "action-added",
323 : : G_CALLBACK (on_plugin_action_added),
324 : : plugin);
325 : 115 : g_signal_connect (plugin->extension,
326 : : "action-enabled-changed",
327 : : G_CALLBACK (on_plugin_action_enabled_changed),
328 : : plugin);
329 : 115 : g_signal_connect (plugin->extension,
330 : : "action-removed",
331 : : G_CALLBACK (on_plugin_action_removed),
332 : : plugin);
333 : 115 : g_signal_connect (plugin->extension,
334 : : "action-state-changed",
335 : : G_CALLBACK (on_plugin_action_state_changed),
336 : : plugin);
337 : :
338 : : /* Bootstrap the newly instantiated plugin
339 : : */
340 : 115 : valent_device_plugin_update_state (VALENT_DEVICE_PLUGIN (plugin->extension),
341 : : valent_device_get_state (device));
342 : : }
343 : :
344 : : static void
345 : 1 : valent_device_disable_plugin (ValentDevice *device,
346 : : ValentPlugin *plugin)
347 : : {
348 : 2 : g_auto (GStrv) actions = NULL;
349 : 1 : const char *incoming = NULL;
350 : :
351 [ - + ]: 1 : g_assert (VALENT_IS_DEVICE (device));
352 [ + - ]: 1 : g_assert (plugin != NULL);
353 [ + - ]: 1 : g_return_if_fail (G_IS_OBJECT (plugin->extension));
354 : :
355 : : /* Unregister actions
356 : : */
357 : 1 : g_signal_handlers_disconnect_by_data (plugin->extension, plugin);
358 : 1 : actions = g_action_group_list_actions (G_ACTION_GROUP (plugin->extension));
359 [ + + ]: 3 : for (size_t i = 0; actions[i]; i++)
360 : : {
361 : 2 : on_plugin_action_removed (G_ACTION_GROUP (plugin->extension),
362 : : actions[i],
363 : : plugin);
364 : : }
365 : :
366 : : /* Unregister packet handlers
367 : : */
368 : 1 : incoming = peas_plugin_info_get_external_data (plugin->info,
369 : : "DevicePluginIncoming");
370 [ + - ]: 1 : if (incoming != NULL)
371 : : {
372 : 2 : g_auto (GStrv) capabilities = NULL;
373 : :
374 : 1 : capabilities = g_strsplit (incoming, ";", -1);
375 [ + + ]: 3 : for (size_t i = 0; capabilities[i] != NULL; i++)
376 : : {
377 : 2 : GPtrArray *handlers = NULL;
378 : :
379 : 2 : handlers = g_hash_table_lookup (device->handlers, capabilities[i]);
380 [ - + ]: 2 : if (handlers == NULL)
381 : 0 : continue;
382 : :
383 : 2 : g_ptr_array_remove (handlers, plugin->extension);
384 [ - + ]: 2 : if (handlers->len == 0)
385 : 0 : g_hash_table_remove (device->handlers, capabilities[i]);
386 : : }
387 : : }
388 : :
389 : 1 : valent_object_destroy (VALENT_OBJECT (plugin->extension));
390 [ + - + - ]: 1 : g_clear_object (&plugin->extension);
391 : : }
392 : :
393 : : static void
394 : 2 : on_plugin_enabled_changed (ValentPlugin *plugin)
395 : : {
396 [ - + ]: 2 : g_assert (plugin != NULL);
397 [ + - ]: 2 : g_assert (VALENT_IS_DEVICE (plugin->parent));
398 : :
399 [ + + ]: 2 : if (valent_plugin_get_enabled (plugin))
400 : 1 : valent_device_enable_plugin (plugin->parent, plugin);
401 : : else
402 : 1 : valent_device_disable_plugin (plugin->parent, plugin);
403 : 2 : }
404 : :
405 : : static gboolean
406 : 924 : valent_device_supports_plugin (ValentDevice *device,
407 : : PeasPluginInfo *info)
408 : : {
409 : 924 : const char **device_incoming;
410 : 924 : const char **device_outgoing;
411 : 924 : const char *in_str, *out_str;
412 : :
413 [ - + ]: 924 : g_assert (VALENT_IS_DEVICE (device));
414 [ + - ]: 924 : g_assert (info != NULL);
415 : :
416 [ + + ]: 924 : if (!peas_engine_provides_extension (device->engine,
417 : : info,
418 : : VALENT_TYPE_DEVICE_PLUGIN))
419 : : return FALSE;
420 : :
421 : : /* Plugins that don't handle packets aren't dependent on capabilities
422 : : */
423 : 634 : in_str = peas_plugin_info_get_external_data (info, "DevicePluginIncoming");
424 : 634 : out_str = peas_plugin_info_get_external_data (info, "DevicePluginOutgoing");
425 [ + - ]: 634 : if (in_str == NULL && out_str == NULL)
426 : : return TRUE;
427 : :
428 : : /* If capabilities are ready, check if the plugin outgoing matches the
429 : : * incoming device or vice-versa.
430 : : */
431 : 634 : device_incoming = (const char **)device->incoming_capabilities;
432 : 634 : device_outgoing = (const char **)device->outgoing_capabilities;
433 [ + + ]: 634 : if (device_incoming == NULL || device_outgoing == NULL)
434 : : return FALSE;
435 : :
436 [ + - ]: 402 : if (out_str != NULL)
437 : : {
438 : 1526 : g_auto (GStrv) plugin_outgoing = NULL;
439 : :
440 : 402 : plugin_outgoing = g_strsplit (out_str, ";", -1);
441 [ + + ]: 799 : for (size_t i = 0; plugin_outgoing[i] != NULL; i++)
442 : : {
443 [ + + ]: 599 : if (g_strv_contains (device_incoming, plugin_outgoing[i]))
444 [ + - ]: 202 : return TRUE;
445 : : }
446 : : }
447 : :
448 [ - + ]: 200 : if (in_str != NULL)
449 : : {
450 : 1124 : g_auto (GStrv) plugin_incoming = NULL;
451 : :
452 : 200 : plugin_incoming = g_strsplit (in_str, ";", -1);
453 [ + + ]: 575 : for (size_t i = 0; plugin_incoming[i] != NULL; i++)
454 : : {
455 [ + + ]: 383 : if (g_strv_contains (device_outgoing, plugin_incoming[i]))
456 [ + - ]: 8 : return TRUE;
457 : : }
458 : : }
459 : :
460 : : return FALSE;
461 : : }
462 : :
463 : : static void
464 : 828 : on_load_plugin (PeasEngine *engine,
465 : : PeasPluginInfo *plugin_info,
466 : : ValentDevice *self)
467 : : {
468 : 828 : ValentPlugin *plugin;
469 : :
470 [ - + ]: 828 : g_assert (PEAS_IS_ENGINE (engine));
471 [ + - ]: 828 : g_assert (plugin_info != NULL);
472 [ + - ]: 828 : g_assert (VALENT_IS_DEVICE (self));
473 : :
474 [ + + ]: 828 : if (!valent_device_supports_plugin (self, plugin_info))
475 : : return;
476 : :
477 : 114 : VALENT_NOTE ("%s: %s",
478 : : self->name,
479 : : peas_plugin_info_get_module_name (plugin_info));
480 : :
481 : 114 : plugin = valent_plugin_new (self, self->context, plugin_info,
482 : : G_CALLBACK (on_plugin_enabled_changed));
483 : 114 : g_hash_table_insert (self->plugins, plugin_info, plugin);
484 : :
485 [ + - ]: 114 : if (valent_plugin_get_enabled (plugin))
486 : 114 : valent_device_enable_plugin (self, plugin);
487 : : }
488 : :
489 : : static void
490 : 5 : on_unload_plugin (PeasEngine *engine,
491 : : PeasPluginInfo *plugin_info,
492 : : ValentDevice *device)
493 : : {
494 [ - + ]: 5 : g_assert (PEAS_IS_ENGINE (engine));
495 [ + - ]: 5 : g_assert (plugin_info != NULL);
496 [ + - ]: 5 : g_assert (VALENT_IS_DEVICE (device));
497 : :
498 : 5 : if (g_hash_table_remove (device->plugins, plugin_info))
499 : : {
500 : 5 : VALENT_NOTE ("%s: %s",
501 : : device->name,
502 : : peas_plugin_info_get_module_name (plugin_info));
503 : : }
504 : 5 : }
505 : :
506 : : static void
507 : 176 : valent_device_reload_plugins (ValentDevice *self)
508 : : {
509 : 176 : unsigned int n_plugins = 0;
510 : :
511 [ - + ]: 176 : g_assert (VALENT_IS_DEVICE (self));
512 : :
513 : 176 : n_plugins = g_list_model_get_n_items (G_LIST_MODEL (self->engine));
514 [ + + ]: 760 : for (unsigned int i = 0; i < n_plugins; i++)
515 : : {
516 : 584 : g_autoptr (PeasPluginInfo) plugin_info = NULL;
517 : :
518 : 584 : plugin_info = g_list_model_get_item (G_LIST_MODEL (self->engine), i);
519 [ + + ]: 584 : if (!g_hash_table_contains (self->plugins, plugin_info))
520 : 488 : on_load_plugin (self->engine, plugin_info, self);
521 [ - + ]: 96 : else if (!valent_device_supports_plugin (self, plugin_info))
522 : 0 : on_unload_plugin (self->engine, plugin_info, self);
523 : : }
524 : 176 : }
525 : :
526 : : static void
527 : 91 : valent_device_update_plugins (ValentDevice *self)
528 : : {
529 : 91 : ValentDeviceState state = VALENT_DEVICE_STATE_NONE;
530 : 91 : GHashTableIter iter;
531 : 91 : ValentPlugin *plugin;
532 : :
533 [ - + ]: 91 : g_assert (VALENT_IS_DEVICE (self));
534 : :
535 : 91 : state = valent_device_get_state (self);
536 : :
537 : 91 : g_hash_table_iter_init (&iter, self->plugins);
538 [ + + ]: 198 : while (g_hash_table_iter_next (&iter, NULL, (void **)&plugin))
539 : : {
540 [ - + ]: 107 : if (plugin->extension == NULL)
541 : 0 : continue;
542 : :
543 : 107 : valent_device_plugin_update_state (VALENT_DEVICE_PLUGIN (plugin->extension),
544 : : state);
545 : : }
546 : 91 : }
547 : :
548 : : /*
549 : : * Private pairing methods
550 : : */
551 : : static void
552 : 114 : valent_device_reset_pair (gpointer object)
553 : : {
554 : 114 : ValentDevice *device = VALENT_DEVICE (object);
555 : 114 : GApplication *application = g_application_get_default ();
556 : :
557 [ - + ]: 114 : g_assert (VALENT_IS_DEVICE (device));
558 : :
559 [ - + ]: 114 : if (application != NULL)
560 : : {
561 : 0 : g_autofree char *notification_id = NULL;
562 : :
563 : 0 : notification_id = g_strdup_printf ("%s::%s", device->id, PAIR_REQUEST_ID);
564 : 0 : g_application_withdraw_notification (application, notification_id);
565 : : }
566 : :
567 [ + + ]: 114 : g_clear_handle_id (&device->incoming_pair, g_source_remove);
568 [ + + ]: 114 : g_clear_handle_id (&device->outgoing_pair, g_source_remove);
569 : 114 : device->pair_timestamp = 0;
570 : 114 : }
571 : :
572 : : static gboolean
573 : 2 : valent_device_cancel_pair (gpointer object)
574 : : {
575 : 2 : ValentDevice *self = VALENT_DEVICE (object);
576 : :
577 [ - + ]: 2 : g_assert (VALENT_IS_DEVICE (self));
578 : :
579 [ + + + - ]: 2 : if (self->incoming_pair != 0 || self->outgoing_pair != 0)
580 : : {
581 : 2 : valent_device_reset_pair (self);
582 : 2 : g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATE]);
583 : : }
584 : :
585 : 2 : return G_SOURCE_REMOVE;
586 : : }
587 : :
588 : : static void
589 : 13 : send_pair_cb (ValentChannel *channel,
590 : : GAsyncResult *result,
591 : : ValentDevice *device)
592 : : {
593 : 26 : g_autoptr (GError) error = NULL;
594 : :
595 [ - + ]: 13 : g_assert (VALENT_IS_CHANNEL (channel));
596 [ + - ]: 13 : g_assert (VALENT_IS_DEVICE (device));
597 : :
598 [ - + ]: 13 : if (!valent_channel_write_packet_finish (channel, result, &error))
599 : : {
600 : 0 : VALENT_NOTE ("%s: %s", device->name, error->message);
601 : :
602 : 0 : valent_device_reset_pair (device);
603 : 0 : valent_object_destroy (VALENT_OBJECT (channel));
604 : : }
605 : :
606 [ - + ]: 13 : g_object_unref (device);
607 : 13 : }
608 : :
609 : : static void
610 : 15 : valent_device_send_pair (ValentDevice *device,
611 : : gboolean pair)
612 : : {
613 : 15 : g_autoptr (JsonBuilder) builder = NULL;
614 [ - + - + ]: 15 : g_autoptr (JsonNode) packet = NULL;
615 [ - + + - ]: 15 : g_autoptr (GCancellable) cancellable = NULL;
616 : :
617 [ - + ]: 15 : g_assert (VALENT_IS_DEVICE (device));
618 : :
619 [ + + ]: 15 : if (device->channel == NULL)
620 [ - + ]: 2 : return;
621 : :
622 : 13 : valent_packet_init (&builder, "kdeconnect.pair");
623 : 13 : json_builder_set_member_name (builder, "pair");
624 : 13 : json_builder_add_boolean_value (builder, pair);
625 [ + - ]: 13 : if (device->protocol_version >= VALENT_NETWORK_PROTOCOL_V8 &&
626 [ + + + + ]: 13 : device->pair_timestamp == 0 && pair)
627 : : {
628 : 6 : device->pair_timestamp = (int64_t)floor (valent_timestamp_ms () / 1000);
629 : 6 : json_builder_set_member_name (builder, "timestamp");
630 : 6 : json_builder_add_int_value (builder, device->pair_timestamp);
631 : : }
632 : 13 : packet = valent_packet_end (&builder);
633 : :
634 : 13 : cancellable = valent_object_ref_cancellable (VALENT_OBJECT (device));
635 [ + - ]: 13 : valent_channel_write_packet (device->channel,
636 : : packet,
637 : : cancellable,
638 : : (GAsyncReadyCallback)send_pair_cb,
639 : : g_object_ref (device));
640 : : }
641 : :
642 : : static void
643 : 13 : valent_device_set_paired (ValentDevice *self,
644 : : gboolean paired)
645 : : {
646 : 13 : g_autoptr (GFile) certificate_file = NULL;
647 [ + - ]: 13 : g_autoptr (GError) error = NULL;
648 : :
649 [ - + ]: 13 : g_assert (VALENT_IS_DEVICE (self));
650 : :
651 [ + + ]: 13 : if (self->paired == paired)
652 : : {
653 : 2 : valent_device_cancel_pair (self);
654 [ - + ]: 2 : return;
655 : : }
656 : :
657 : : /* Store the certificate in the configuration directory if paired,
658 : : * otherwise delete the certificate and clear the data context.
659 : : *
660 : : * TODO: KDE Connect uses certificate pinning, but there is no shared resource
661 : : * mediating between a ValentDevice and a ValentChannelService.
662 : : */
663 : 11 : certificate_file = valent_context_get_config_file (self->context,
664 : : "certificate.pem");
665 [ + + ]: 11 : if (paired)
666 : : {
667 : 6 : g_autoptr (GTlsCertificate) certificate = NULL;
668 [ + - ]: 6 : g_autofree char *certificate_pem = NULL;
669 : :
670 [ + - ]: 6 : if (self->channel != NULL)
671 : : {
672 : 6 : g_object_get (self->channel,
673 : : "peer-certificate", &certificate,
674 : : NULL);
675 : 6 : g_object_get (certificate,
676 : : "certificate-pem", &certificate_pem,
677 : : NULL);
678 : 6 : g_file_replace_contents (certificate_file,
679 : : certificate_pem,
680 : 6 : strlen (certificate_pem),
681 : : NULL, /* etag */
682 : : FALSE, /* make_backup */
683 : : (G_FILE_CREATE_PRIVATE |
684 : : G_FILE_CREATE_REPLACE_DESTINATION),
685 : : NULL, /* etag (out) */
686 : : NULL,
687 : : &error);
688 [ - + ]: 6 : if (error != NULL)
689 : : {
690 : 0 : g_warning ("%s(): failed to write \"%s\": %s",
691 : : G_STRFUNC,
692 : : g_file_peek_path (certificate_file),
693 : : error->message);
694 : : }
695 : : }
696 : : }
697 : : else
698 : : {
699 [ - + - - ]: 5 : if (!g_file_delete (certificate_file, NULL, &error) &&
700 : 0 : !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
701 : : {
702 : 0 : g_warning ("%s(): failed to delete \"%s\": %s",
703 : : G_STRFUNC,
704 : : g_file_peek_path (certificate_file),
705 : : error->message);
706 : : }
707 : :
708 : 5 : valent_context_clear (self->context);
709 : : }
710 : :
711 : 11 : self->paired = paired;
712 : :
713 : 11 : valent_device_reset_pair (self);
714 : 11 : valent_device_update_plugins (self);
715 [ - + ]: 11 : g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATE]);
716 : : }
717 : :
718 : : static void
719 : 2 : valent_device_notify_pair_requested (ValentDevice *device)
720 : : {
721 : 2 : GApplication *application = NULL;
722 : 2 : g_autofree char *notification_id = NULL;
723 : 2 : g_autoptr (GNotification) notification = NULL;
724 [ - - ]: 2 : g_autoptr (GIcon) icon = NULL;
725 [ - - - + ]: 2 : g_autofree char *title = NULL;
726 : 2 : g_autofree char *verification_key = NULL;
727 : :
728 [ - + ]: 2 : g_assert (VALENT_IS_DEVICE (device));
729 : :
730 : 2 : application = g_application_get_default ();
731 [ - + ]: 2 : if (application == NULL)
732 : : return;
733 : :
734 : 0 : title = g_strdup_printf (_("Pairing request from “%s”"), device->name);
735 : 0 : verification_key = valent_device_get_verification_key (device);
736 : 0 : icon = g_themed_icon_new (APPLICATION_ID);
737 : :
738 [ # # ]: 0 : g_return_if_fail (verification_key != NULL);
739 : :
740 : 0 : notification = g_notification_new (title);
741 : 0 : g_notification_set_body (notification, verification_key);
742 : 0 : g_notification_set_icon (notification, icon);
743 : 0 : g_notification_set_priority (notification, G_NOTIFICATION_PRIORITY_URGENT);
744 : 0 : g_notification_add_button_with_target (notification, _("Reject"), "app.device",
745 : : "(ssav)",
746 : : device->id,
747 : : "unpair",
748 : : NULL);
749 : 0 : g_notification_add_button_with_target (notification, _("Accept"), "app.device",
750 : : "(ssav)",
751 : : device->id,
752 : : "pair",
753 : : NULL);
754 : :
755 : : /* Show the pairing notification and set a timeout for 30s
756 : : */
757 : 0 : notification_id = g_strdup_printf ("%s::%s", device->id, PAIR_REQUEST_ID);
758 : 0 : g_application_send_notification (application,
759 : : notification_id,
760 : : notification);
761 : : }
762 : :
763 : : static void
764 : 8 : valent_device_handle_pair (ValentDevice *device,
765 : : JsonNode *packet)
766 : : {
767 : 8 : gboolean pair;
768 : :
769 : 8 : VALENT_ENTRY;
770 : :
771 [ - + ]: 8 : g_assert (VALENT_IS_DEVICE (device));
772 [ + - ]: 8 : g_assert (VALENT_IS_PACKET (packet));
773 : :
774 [ - + ]: 8 : if (!valent_packet_get_boolean (packet, "pair", &pair))
775 : : {
776 : 0 : g_warning ("%s(): expected \"pair\" field holding a boolean from \"%s\"",
777 : : G_STRFUNC,
778 : : device->name);
779 : 0 : VALENT_EXIT;
780 : : }
781 : :
782 [ + + ]: 8 : if (pair)
783 : : {
784 [ + + ]: 7 : if (device->outgoing_pair > 0)
785 : : {
786 : 5 : g_debug ("Pairing accepted by \"%s\"", device->name);
787 : 5 : valent_device_set_paired (device, TRUE);
788 : : }
789 [ - + ]: 2 : else if (device->incoming_pair > 0)
790 : : {
791 : 0 : g_debug ("Ignoring concurrent pairing request from \"%s\"",
792 : : device->name);
793 : : }
794 : : else
795 : : {
796 : 2 : g_debug ("Pairing requested by \"%s\"", device->name);
797 : :
798 [ + - ]: 2 : if (device->protocol_version >= VALENT_NETWORK_PROTOCOL_V8)
799 : : {
800 : 2 : int64_t timestamp = 0;
801 : 2 : int64_t localtime = 0;
802 : :
803 [ - + ]: 2 : if (!valent_packet_get_int (packet, "timestamp", ×tamp))
804 : : {
805 : 0 : g_warning ("%s(): expected \"timestamp\" field holding an integer",
806 : : G_STRFUNC);
807 : 0 : VALENT_EXIT;
808 : : }
809 : :
810 : 2 : localtime = (int64_t)floor (valent_timestamp_ms () / 1000);
811 [ - + ]: 2 : if (ABS (localtime - timestamp) > PAIR_CLOCK_SKEW_THRESHOLD)
812 : : {
813 : 0 : g_warning ("%s(): device clocks are out of sync", G_STRFUNC);
814 : 0 : VALENT_EXIT;
815 : : }
816 : :
817 : 2 : device->pair_timestamp = timestamp;
818 : : }
819 : :
820 [ - + ]: 2 : if (device->paired)
821 : : {
822 : : /* In protocol v8, the timestamp field is mandatory for pair
823 : : * requests; unsolicited pair accept packets have been handled
824 : : * at this point, so the request can safely be auto-accepted.
825 : : *
826 : : * In protocol v7, pair request and accept packets are identical;
827 : : * if both devices interpret unsolicited accept packets as
828 : : * requests, they will get caught in an infinite loop.
829 : : */
830 [ # # ]: 0 : if (device->pair_timestamp > 0)
831 : : {
832 : 0 : g_debug ("Accepting pair request from paired device \"%s\"",
833 : : device->name);
834 : 0 : valent_device_send_pair (device, TRUE);
835 : 0 : valent_device_set_paired (device, TRUE);
836 : 0 : VALENT_EXIT;
837 : : }
838 : : else
839 : : {
840 : 0 : g_warning ("Pairing requested by paired device \"%s\"",
841 : : device->name);
842 : 0 : valent_device_set_paired (device, FALSE);
843 : : }
844 : : }
845 : :
846 : 2 : device->incoming_pair = g_timeout_add_seconds (PAIR_REQUEST_TIMEOUT,
847 : : valent_device_cancel_pair,
848 : : device);
849 : 2 : g_object_notify_by_pspec (G_OBJECT (device), properties [PROP_STATE]);
850 : 2 : valent_device_notify_pair_requested (device);
851 : : }
852 : : }
853 : : else
854 : : {
855 : 1 : g_debug ("Pairing rejected by \"%s\"", device->name);
856 : 1 : valent_device_set_paired (device, FALSE);
857 : : }
858 : :
859 : 8 : VALENT_EXIT;
860 : : }
861 : :
862 : : /*
863 : : * Private identity methods
864 : : */
865 : : static char *
866 : 176 : valent_device_sanitize_name (const char *name,
867 : : const char *fallback)
868 : : {
869 : 176 : static const uint8_t forbidden_chars[256] = {
870 : : ['"'] = 1, ['\''] = 1,
871 : : [','] = 1, ['.'] = 1,
872 : : [';'] = 1, [':'] = 1,
873 : : ['!'] = 1, ['?'] = 1,
874 : : ['('] = 1, [')'] = 1,
875 : : ['['] = 1, [']'] = 1,
876 : : ['<'] = 1, ['>'] = 1,
877 : : };
878 : 176 : g_autoptr (GString) ret = NULL;
879 : :
880 [ + - + - ]: 176 : g_assert (name != NULL && *name != '\0');
881 : :
882 : 176 : ret = g_string_new (NULL);
883 [ + + ]: 2112 : for (const char *p = name; *p != '\0'; p = g_utf8_next_char (p))
884 : : {
885 : 1936 : gunichar ch = g_utf8_get_char (p);
886 [ + - + - ]: 1936 : if (ch > 127 || !forbidden_chars[(uint8_t)ch])
887 : 1936 : g_string_append_unichar (ret, ch);
888 : :
889 [ + - ]: 1936 : if (ret->len == 32)
890 : : break;
891 : : }
892 : :
893 [ - + ]: 176 : if (ret->len == 0)
894 : : {
895 : 0 : g_warning ("%s(): device name \"%s\" could not be sanitized",
896 : : G_STRFUNC,
897 : : name);
898 [ # # ]: 0 : return g_strdup (fallback);
899 : : }
900 : :
901 : 176 : return g_string_free_and_steal (g_steal_pointer (&ret));
902 : : }
903 : :
904 : : static void
905 : 176 : valent_device_handle_identity (ValentDevice *device,
906 : : JsonNode *packet)
907 : : {
908 : 176 : const char *device_id;
909 : 176 : const char *device_name;
910 : 176 : const char *device_type;
911 : 352 : g_autofree char *sanitized_name = NULL;
912 : :
913 : 176 : VALENT_ENTRY;
914 : :
915 [ - + ]: 176 : g_assert (VALENT_IS_DEVICE (device));
916 [ + - ]: 176 : g_assert (VALENT_IS_PACKET (packet));
917 : :
918 : : /* The ID must match the construct-time value.
919 : : */
920 [ + - ]: 176 : if (!valent_packet_get_string (packet, "deviceId", &device_id) ||
921 [ - + ]: 176 : !g_str_equal (device->id, device_id))
922 : : {
923 : 0 : g_critical ("%s(): expected \"deviceId\" field holding \"%s\"",
924 : : G_STRFUNC,
925 : : device->id);
926 : 0 : VALENT_EXIT;
927 : : }
928 : :
929 : : /* If the device name is invalid, try removing the offended characters,
930 : : * falling back to the device ID.
931 : : */
932 [ - + ]: 176 : if (!valent_packet_get_string (packet, "deviceName", &device_name))
933 : : {
934 : 0 : g_critical ("%s(): expected \"deviceName\" field holding a string",
935 : : G_STRFUNC);
936 : 0 : VALENT_EXIT;
937 : : }
938 : :
939 : 176 : sanitized_name = valent_device_sanitize_name (device_name, device_id);
940 [ + + ]: 176 : if (g_set_str (&device->name, sanitized_name))
941 : 96 : g_object_notify_by_pspec (G_OBJECT (device), properties [PROP_NAME]);
942 : :
943 : : /* The device type is only used to generate an icon name.
944 : : */
945 [ - + ]: 176 : if (!valent_packet_get_string (packet, "deviceType", &device_type))
946 : : {
947 : 0 : g_warning ("%s(): expected \"deviceType\" field holding a string",
948 : : G_STRFUNC);
949 : 0 : device_type = DEVICE_TYPE_DESKTOP;
950 : : }
951 : :
952 [ + + ]: 176 : if (g_set_str (&device->type, device_type))
953 : : {
954 : 96 : const char *device_icon = "computer-symbolic";
955 : :
956 [ + + ]: 96 : if (g_str_equal (device_type, DEVICE_TYPE_DESKTOP))
957 : : device_icon = "computer-symbolic";
958 [ + - ]: 93 : else if (g_str_equal (device_type, DEVICE_TYPE_LAPTOP))
959 : : device_icon = "laptop-symbolic";
960 [ - + ]: 93 : else if (g_str_equal (device_type, DEVICE_TYPE_PHONE))
961 : : device_icon = "phone-symbolic";
962 [ # # ]: 0 : else if (g_str_equal (device_type, DEVICE_TYPE_TABLET))
963 : : device_icon = "tablet-symbolic";
964 [ # # ]: 0 : else if (g_str_equal (device_type, DEVICE_TYPE_TV))
965 : 96 : device_icon = "tv-symbolic";
966 : :
967 [ + - ]: 96 : if (g_set_str (&device->icon_name, device_icon))
968 : 96 : g_object_notify_by_pspec (G_OBJECT (device), properties [PROP_ICON_NAME]);
969 : : }
970 : :
971 : : /* In practice these are static, but in principle could change with the
972 : : * channel (e.g. TCP and Bluetooth).
973 : : */
974 [ + + ]: 176 : g_clear_pointer (&device->incoming_capabilities, g_strfreev);
975 : 352 : device->incoming_capabilities =
976 : 176 : valent_packet_dup_strv (packet, "incomingCapabilities");
977 : :
978 [ + + ]: 176 : g_clear_pointer (&device->outgoing_capabilities, g_strfreev);
979 : 352 : device->outgoing_capabilities =
980 : 176 : valent_packet_dup_strv (packet, "outgoingCapabilities");
981 : :
982 : : /* It's not clear if this is only a required field for TLS connections,
983 : : * or if it applies to Bluetooth as well.
984 : : */
985 [ - + ]: 176 : if (!valent_packet_get_int (packet, "protocolVersion", &device->protocol_version))
986 : 0 : device->protocol_version = VALENT_NETWORK_PROTOCOL_V8;
987 : :
988 : : /* Recheck plugins against current capabilities
989 : : */
990 : 176 : valent_device_reload_plugins (device);
991 : :
992 : 352 : VALENT_EXIT;
993 : : }
994 : :
995 : : static void
996 : 128 : valent_device_handle_packet (ValentDevice *self,
997 : : JsonNode *packet)
998 : : {
999 : 128 : GPtrArray *handlers = NULL;
1000 : 128 : const char *type;
1001 : :
1002 [ - + ]: 128 : g_assert (VALENT_IS_DEVICE (self));
1003 [ + - ]: 128 : g_assert (VALENT_IS_PACKET (packet));
1004 : :
1005 : 128 : VALENT_JSON (packet, self->name);
1006 : :
1007 : 128 : type = valent_packet_get_type (packet);
1008 [ + + ]: 128 : if G_UNLIKELY (g_str_equal (type, "kdeconnect.pair"))
1009 : : {
1010 : 8 : valent_device_handle_pair (self, packet);
1011 : 8 : return;
1012 : : }
1013 : :
1014 [ + + ]: 120 : if G_UNLIKELY (!self->paired)
1015 : : {
1016 : 1 : g_debug ("%s(): unexpected \"%s\" packet from unpaired device %s",
1017 : : G_STRFUNC, type, self->name);
1018 : 1 : valent_device_send_pair (self, FALSE);
1019 : 1 : return;
1020 : : }
1021 : :
1022 : 119 : handlers = g_hash_table_lookup (self->handlers, type);
1023 [ - + ]: 119 : if G_UNLIKELY (handlers == NULL)
1024 : : {
1025 : 0 : g_debug ("%s(): unsupported \"%s\" packet from %s",
1026 : : G_STRFUNC, type, self->name);
1027 : 0 : return;
1028 : : }
1029 : :
1030 [ + + ]: 238 : for (unsigned int i = 0, len = handlers->len; i < len; i++)
1031 : : {
1032 : 119 : ValentDevicePlugin *handler = g_ptr_array_index (handlers, i);
1033 : :
1034 : 119 : valent_device_plugin_handle_packet (handler, type, packet);
1035 : : }
1036 : : }
1037 : :
1038 : : /*
1039 : : * GActions
1040 : : */
1041 : : static void
1042 : 8 : pair_action (GSimpleAction *action,
1043 : : GVariant *parameter,
1044 : : gpointer user_data)
1045 : : {
1046 : 8 : ValentDevice *device = VALENT_DEVICE (user_data);
1047 : :
1048 [ + + ]: 8 : if (device->incoming_pair > 0)
1049 : : {
1050 : 1 : g_debug ("Accepting pair request from \"%s\"", device->name);
1051 : :
1052 : 1 : valent_device_send_pair (device, TRUE);
1053 : 1 : valent_device_set_paired (device, TRUE);
1054 : : }
1055 [ - + ]: 7 : else if (device->outgoing_pair > 0)
1056 : : {
1057 : 0 : g_warning ("Pairing request with \"%s\" already in progress",
1058 : : device->name);
1059 : : }
1060 [ - + ]: 7 : else if (device->paired)
1061 : : {
1062 : 0 : g_warning ("Ignoring attempt to pair with paired device \"%s\"",
1063 : : device->name);
1064 : : }
1065 : : else
1066 : : {
1067 : 7 : g_debug ("Sending pair request to \"%s\"", device->name);
1068 : :
1069 : 7 : valent_device_send_pair (device, TRUE);
1070 : 7 : device->outgoing_pair = g_timeout_add_seconds (PAIR_REQUEST_TIMEOUT,
1071 : : valent_device_cancel_pair,
1072 : : device);
1073 : 7 : g_object_notify_by_pspec (G_OBJECT (device), properties [PROP_STATE]);
1074 : : }
1075 : 8 : }
1076 : :
1077 : : static void
1078 : 6 : unpair_action (GSimpleAction *action,
1079 : : GVariant *parameter,
1080 : : gpointer user_data)
1081 : : {
1082 : 6 : ValentDevice *device = VALENT_DEVICE (user_data);
1083 : :
1084 : 6 : valent_device_send_pair (device, FALSE);
1085 : 6 : valent_device_set_paired (device, FALSE);
1086 : 6 : }
1087 : :
1088 : : /*
1089 : : * ValentObject
1090 : : */
1091 : : static void
1092 : 101 : valent_device_destroy (ValentObject *object)
1093 : : {
1094 : 101 : ValentDevice *self = VALENT_DEVICE (object);
1095 : 101 : unsigned int n_items = 0;
1096 : :
1097 : 101 : valent_device_reset_pair (self);
1098 : :
1099 : : /* Destroy all connections associated with this device,
1100 : : * ensuring a state change is propagated to plugins
1101 : : */
1102 : 101 : n_items = g_list_model_get_n_items (self->channels);
1103 : 101 : for (unsigned int i = n_items; i--;)
1104 : : {
1105 [ - + ]: 101 : g_autoptr (ValentObject) item = NULL;
1106 : :
1107 : 0 : item = g_list_model_get_item (self->channels, i);
1108 [ # # ]: 0 : valent_object_destroy (item);
1109 : : }
1110 : :
1111 : 101 : g_signal_handlers_disconnect_by_data (self->engine, self);
1112 : 101 : g_hash_table_remove_all (self->plugins);
1113 : 101 : g_hash_table_remove_all (self->actions);
1114 : 101 : g_hash_table_remove_all (self->handlers);
1115 : :
1116 : 101 : VALENT_OBJECT_CLASS (valent_device_parent_class)->destroy (object);
1117 : 101 : }
1118 : :
1119 : : /*
1120 : : * GObject
1121 : : */
1122 : : static void
1123 : 97 : valent_device_constructed (GObject *object)
1124 : : {
1125 : 97 : ValentDevice *self = VALENT_DEVICE (object);
1126 : 194 : g_autoptr (GFile) certificate_file = NULL;
1127 : 97 : unsigned int n_plugins = 0;
1128 : :
1129 : 97 : G_OBJECT_CLASS (valent_device_parent_class)->constructed (object);
1130 : :
1131 : : /* We must at least have a device ID */
1132 [ - + ]: 97 : g_assert (self->id != NULL);
1133 : :
1134 [ + + ]: 97 : if (self->context == NULL)
1135 : 91 : self->context = valent_context_new (NULL, "device", self->id);
1136 : :
1137 : 97 : certificate_file = valent_context_get_config_file (self->context,
1138 : : "certificate.pem");
1139 : 97 : self->paired = g_file_query_exists (certificate_file, NULL);
1140 : :
1141 : 97 : self->engine = valent_get_plugin_engine ();
1142 : 97 : g_signal_connect_object (self->engine,
1143 : : "load-plugin",
1144 : : G_CALLBACK (on_load_plugin),
1145 : : self,
1146 : : G_CONNECT_AFTER);
1147 : :
1148 : 97 : g_signal_connect_object (self->engine,
1149 : : "unload-plugin",
1150 : : G_CALLBACK (on_unload_plugin),
1151 : : self,
1152 : : G_CONNECT_DEFAULT);
1153 : :
1154 : 97 : n_plugins = g_list_model_get_n_items (G_LIST_MODEL (self->engine));
1155 [ + + ]: 434 : for (unsigned int i = 0; i < n_plugins; i++)
1156 : : {
1157 : 337 : g_autoptr (PeasPluginInfo) info = NULL;
1158 : :
1159 : 337 : info = g_list_model_get_item (G_LIST_MODEL (self->engine), i);
1160 [ + + ]: 337 : if (peas_plugin_info_is_loaded (info))
1161 : 336 : on_load_plugin (self->engine, info, self);
1162 : : }
1163 : 97 : }
1164 : :
1165 : : static void
1166 : 94 : valent_device_finalize (GObject *object)
1167 : : {
1168 : 94 : ValentDevice *self = VALENT_DEVICE (object);
1169 : :
1170 [ + - ]: 94 : g_clear_object (&self->context);
1171 : :
1172 : : /* Properties */
1173 [ + + ]: 94 : g_clear_pointer (&self->icon_name, g_free);
1174 [ + - ]: 94 : g_clear_pointer (&self->id, g_free);
1175 [ + + ]: 94 : g_clear_pointer (&self->name, g_free);
1176 [ + + ]: 94 : g_clear_pointer (&self->type, g_free);
1177 [ + + ]: 94 : g_clear_pointer (&self->incoming_capabilities, g_strfreev);
1178 [ + + ]: 94 : g_clear_pointer (&self->outgoing_capabilities, g_strfreev);
1179 : :
1180 : : /* State */
1181 [ + - ]: 94 : g_clear_object (&self->channels);
1182 [ - + ]: 94 : g_clear_object (&self->channel);
1183 : :
1184 : : /* Plugins */
1185 [ + - ]: 94 : g_clear_pointer (&self->plugins, g_hash_table_unref);
1186 [ + - ]: 94 : g_clear_pointer (&self->actions, g_hash_table_unref);
1187 [ + - ]: 94 : g_clear_pointer (&self->handlers, g_hash_table_unref);
1188 [ + - ]: 94 : g_clear_object (&self->menu);
1189 : :
1190 : 94 : G_OBJECT_CLASS (valent_device_parent_class)->finalize (object);
1191 : 94 : }
1192 : :
1193 : : static void
1194 : 31 : valent_device_get_property (GObject *object,
1195 : : guint prop_id,
1196 : : GValue *value,
1197 : : GParamSpec *pspec)
1198 : : {
1199 : 31 : ValentDevice *self = VALENT_DEVICE (object);
1200 : :
1201 [ + + + + : 31 : switch ((ValentDeviceProperty)prop_id)
+ - ]
1202 : : {
1203 : 10 : case PROP_CONTEXT:
1204 : 10 : g_value_set_object (value, self->context);
1205 : 10 : break;
1206 : :
1207 : 4 : case PROP_ICON_NAME:
1208 : 4 : g_value_set_string (value, self->icon_name);
1209 : 4 : break;
1210 : :
1211 : 5 : case PROP_ID:
1212 : 5 : g_value_set_string (value, self->id);
1213 : 5 : break;
1214 : :
1215 : 10 : case PROP_NAME:
1216 : 10 : g_value_set_string (value, self->name);
1217 : 10 : break;
1218 : :
1219 : 2 : case PROP_STATE:
1220 : 2 : g_value_set_flags (value, valent_device_get_state (self));
1221 : 2 : break;
1222 : :
1223 : 0 : default:
1224 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1225 : : }
1226 : 31 : }
1227 : :
1228 : : static void
1229 : 194 : valent_device_set_property (GObject *object,
1230 : : guint prop_id,
1231 : : const GValue *value,
1232 : : GParamSpec *pspec)
1233 : : {
1234 : 194 : ValentDevice *self = VALENT_DEVICE (object);
1235 : :
1236 [ + + - ]: 194 : switch ((ValentDeviceProperty)prop_id)
1237 : : {
1238 : 97 : case PROP_CONTEXT:
1239 : 97 : self->context = g_value_dup_object (value);
1240 : 97 : break;
1241 : :
1242 : 97 : case PROP_ID:
1243 : 97 : self->id = g_value_dup_string (value);
1244 : 97 : break;
1245 : :
1246 : 0 : case PROP_ICON_NAME:
1247 : : case PROP_NAME:
1248 : : case PROP_STATE:
1249 : : default:
1250 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1251 : : }
1252 : 194 : }
1253 : :
1254 : : static void
1255 : 97 : valent_device_init (ValentDevice *self)
1256 : : {
1257 : 97 : GSimpleAction *action = NULL;
1258 : :
1259 : 97 : self->channels = G_LIST_MODEL (g_list_store_new (VALENT_TYPE_CHANNEL));
1260 : 97 : self->plugins = g_hash_table_new_full (NULL, NULL, NULL, valent_plugin_free);
1261 : 97 : self->handlers = g_hash_table_new_full (g_str_hash,
1262 : : g_str_equal,
1263 : : g_free,
1264 : : (GDestroyNotify)g_ptr_array_unref);
1265 : 97 : self->actions = g_hash_table_new_full (g_str_hash,
1266 : : g_str_equal,
1267 : : g_free,
1268 : : g_object_unref);
1269 : 97 : self->menu = g_menu_new ();
1270 : :
1271 : : /* Stock Actions */
1272 : 97 : action = g_simple_action_new ("pair", NULL);
1273 : 97 : g_signal_connect_object (action,
1274 : : "activate",
1275 : : G_CALLBACK (pair_action),
1276 : : self,
1277 : : G_CONNECT_DEFAULT);
1278 : 97 : g_hash_table_replace (self->actions,
1279 : 97 : g_strdup ("pair"),
1280 : : g_steal_pointer (&action));
1281 : :
1282 : 97 : action = g_simple_action_new ("unpair", NULL);
1283 : 97 : g_signal_connect_object (action,
1284 : : "activate",
1285 : : G_CALLBACK (unpair_action),
1286 : : self,
1287 : : G_CONNECT_DEFAULT);
1288 : 97 : g_hash_table_replace (self->actions,
1289 : 97 : g_strdup ("unpair"),
1290 : : g_steal_pointer (&action));
1291 : 97 : }
1292 : :
1293 : : static void
1294 : 32 : valent_device_class_init (ValentDeviceClass *klass)
1295 : : {
1296 : 32 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
1297 : 32 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
1298 : :
1299 : 32 : object_class->constructed = valent_device_constructed;
1300 : 32 : object_class->finalize = valent_device_finalize;
1301 : 32 : object_class->get_property = valent_device_get_property;
1302 : 32 : object_class->set_property = valent_device_set_property;
1303 : :
1304 : 32 : vobject_class->destroy = valent_device_destroy;
1305 : :
1306 : : /**
1307 : : * ValentDevice:context: (getter get_context)
1308 : : *
1309 : : * The data context.
1310 : : *
1311 : : * Since: 1.0
1312 : : */
1313 : 64 : properties [PROP_CONTEXT] =
1314 : 32 : g_param_spec_object ("context", NULL, NULL,
1315 : : VALENT_TYPE_CONTEXT,
1316 : : (G_PARAM_READWRITE |
1317 : : G_PARAM_CONSTRUCT_ONLY |
1318 : : G_PARAM_EXPLICIT_NOTIFY |
1319 : : G_PARAM_STATIC_STRINGS));
1320 : :
1321 : : /**
1322 : : * ValentDevice:icon-name: (getter get_icon_name)
1323 : : *
1324 : : * A symbolic icon name for the device.
1325 : : *
1326 : : * Since: 1.0
1327 : : */
1328 : 64 : properties [PROP_ICON_NAME] =
1329 : 32 : g_param_spec_string ("icon-name", NULL, NULL,
1330 : : "computer-symbolic",
1331 : : (G_PARAM_READABLE |
1332 : : G_PARAM_EXPLICIT_NOTIFY |
1333 : : G_PARAM_STATIC_STRINGS));
1334 : :
1335 : : /**
1336 : : * ValentDevice:id: (getter get_id)
1337 : : *
1338 : : * A unique ID for the device.
1339 : : *
1340 : : * By convention, the single source of truth for a device ID in KDE Connect is
1341 : : * the common name of its TLS certificate. It is not well-defined how this ID
1342 : : * is generated, however.
1343 : : *
1344 : : * Since: 1.0
1345 : : */
1346 : 64 : properties [PROP_ID] =
1347 : 32 : g_param_spec_string ("id", NULL, NULL,
1348 : : NULL,
1349 : : (G_PARAM_READWRITE |
1350 : : G_PARAM_CONSTRUCT_ONLY |
1351 : : G_PARAM_EXPLICIT_NOTIFY |
1352 : : G_PARAM_STATIC_STRINGS));
1353 : :
1354 : : /**
1355 : : * ValentDevice:name: (getter get_name)
1356 : : *
1357 : : * A display name for the device.
1358 : : *
1359 : : * Since: 1.0
1360 : : */
1361 : 64 : properties [PROP_NAME] =
1362 : 32 : g_param_spec_string ("name", NULL, NULL,
1363 : : NULL,
1364 : : (G_PARAM_READABLE |
1365 : : G_PARAM_EXPLICIT_NOTIFY |
1366 : : G_PARAM_STATIC_STRINGS));
1367 : :
1368 : : /**
1369 : : * ValentDevice:state: (getter get_state)
1370 : : *
1371 : : * The state of the device.
1372 : : *
1373 : : * Since: 1.0
1374 : : */
1375 : 64 : properties [PROP_STATE] =
1376 : 32 : g_param_spec_flags ("state", NULL, NULL,
1377 : : VALENT_TYPE_DEVICE_STATE,
1378 : : VALENT_DEVICE_STATE_NONE,
1379 : : (G_PARAM_READABLE |
1380 : : G_PARAM_EXPLICIT_NOTIFY |
1381 : : G_PARAM_STATIC_STRINGS));
1382 : :
1383 : 32 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
1384 : 32 : }
1385 : :
1386 : : /**
1387 : : * valent_device_new:
1388 : : * @id: (not nullable): a device ID
1389 : : *
1390 : : * Create a new device for @id.
1391 : : *
1392 : : * Returns: (transfer full) (nullable): a new `ValentDevice`
1393 : : *
1394 : : * Since: 1.0
1395 : : */
1396 : : ValentDevice *
1397 : 1 : valent_device_new (const char *id)
1398 : : {
1399 [ - + ]: 1 : g_return_val_if_fail (valent_device_validate_id (id), NULL);
1400 : :
1401 : 1 : return g_object_new (VALENT_TYPE_DEVICE,
1402 : : "id", id,
1403 : : NULL);
1404 : : }
1405 : :
1406 : : /*< private >
1407 : : * valent_device_new_full:
1408 : : * @identity: a KDE Connect identity packet
1409 : : * @context: (nullable): a `ValentContext`
1410 : : *
1411 : : * Create a new device for @identity.
1412 : : *
1413 : : * Returns: (transfer full) (nullable): a new `ValentDevice`
1414 : : */
1415 : : ValentDevice *
1416 : 96 : valent_device_new_full (JsonNode *identity,
1417 : : ValentContext *context)
1418 : : {
1419 : 96 : ValentDevice *ret;
1420 : 96 : const char *id;
1421 : :
1422 [ - + ]: 96 : g_return_val_if_fail (VALENT_IS_PACKET (identity), NULL);
1423 : :
1424 [ - + ]: 96 : if (!valent_packet_get_string (identity, "deviceId", &id))
1425 : : {
1426 : 0 : g_critical ("%s(): missing \"deviceId\" field", G_STRFUNC);
1427 : 0 : return NULL;
1428 : : }
1429 : :
1430 [ - + ]: 96 : if (!valent_device_validate_id (id))
1431 : : {
1432 : 0 : g_critical ("%s(): invalid device ID \"%s\"", G_STRFUNC, id);
1433 : 0 : return NULL;
1434 : : }
1435 : :
1436 : 96 : ret = g_object_new (VALENT_TYPE_DEVICE,
1437 : : "id", id,
1438 : : "context", context,
1439 : : NULL);
1440 : 96 : valent_device_handle_identity (ret, identity);
1441 : :
1442 : 96 : return ret;
1443 : : }
1444 : :
1445 : : static void
1446 : 142 : valent_device_send_packet_cb (ValentChannel *channel,
1447 : : GAsyncResult *result,
1448 : : gpointer user_data)
1449 : : {
1450 : 284 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
1451 : 142 : ValentDevice *self = g_task_get_source_object (task);
1452 : 142 : GCancellable *cancellable = g_task_get_cancellable (task);
1453 : 142 : JsonNode *packet = g_task_get_task_data (task);
1454 [ + - + - ]: 142 : g_autoptr (GError) error = NULL;
1455 : :
1456 [ + + ]: 142 : if (valent_channel_write_packet_finish (channel, result, &error))
1457 : : {
1458 : 137 : VALENT_JSON (packet, self->name);
1459 : 137 : g_task_return_boolean (task, TRUE);
1460 : 137 : return;
1461 : : }
1462 : :
1463 : : /* Unless the operation was cancelled, destroy the defunct
1464 : : * channel and retry if possible
1465 : : */
1466 [ + + ]: 5 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1467 : : {
1468 : 3 : valent_object_destroy (VALENT_OBJECT (channel));
1469 [ - + ]: 3 : if (self->channel != NULL)
1470 : : {
1471 : 0 : valent_channel_write_packet (self->channel,
1472 : : packet,
1473 : : cancellable,
1474 : : (GAsyncReadyCallback)valent_device_send_packet_cb,
1475 : : g_object_ref (task));
1476 : 0 : return;
1477 : : }
1478 : : }
1479 : :
1480 [ - + ]: 5 : g_task_return_error (task, g_steal_pointer (&error));
1481 : : }
1482 : :
1483 : : /**
1484 : : * valent_device_send_packet:
1485 : : * @device: a `ValentDevice`
1486 : : * @packet: a KDE Connect packet
1487 : : * @cancellable: (nullable): a `GCancellable`
1488 : : * @callback: (scope async): a `GAsyncReadyCallback`
1489 : : * @user_data: user supplied data
1490 : : *
1491 : : * Send a KDE Connect packet to the device.
1492 : : *
1493 : : * Call [method@Valent.Device.send_packet_finish] to get the result.
1494 : : *
1495 : : * If @device is disconnected or unpaired when this method is called,
1496 : : * %G_IO_ERROR_NOT_CONNECTED or %G_IO_ERROR_PERMISSION_DENIED will be set on the
1497 : : * result, respectively.
1498 : : *
1499 : : * Since: 1.0
1500 : : */
1501 : : void
1502 : 144 : valent_device_send_packet (ValentDevice *device,
1503 : : JsonNode *packet,
1504 : : GCancellable *cancellable,
1505 : : GAsyncReadyCallback callback,
1506 : : gpointer user_data)
1507 : : {
1508 : 142 : g_autoptr (GTask) task = NULL;
1509 : :
1510 [ - + ]: 144 : g_return_if_fail (VALENT_IS_DEVICE (device));
1511 [ + - ]: 144 : g_return_if_fail (VALENT_IS_PACKET (packet));
1512 : :
1513 [ + + ]: 144 : if G_UNLIKELY (device->channel == NULL)
1514 : : {
1515 : 1 : g_task_report_new_error (device,
1516 : : callback,
1517 : : user_data,
1518 : : valent_device_send_packet,
1519 : : G_IO_ERROR,
1520 : : G_IO_ERROR_NOT_CONNECTED,
1521 : : "%s is disconnected", device->name);
1522 : 1 : return;
1523 : : }
1524 : :
1525 [ + + ]: 143 : if G_UNLIKELY (!device->paired)
1526 : : {
1527 : 1 : g_task_report_new_error (device,
1528 : : callback,
1529 : : user_data,
1530 : : valent_device_send_packet,
1531 : : G_IO_ERROR,
1532 : : G_IO_ERROR_PERMISSION_DENIED,
1533 : : "%s is unpaired", device->name);
1534 : 1 : return;
1535 : : }
1536 : :
1537 : 142 : task = g_task_new (device, cancellable, callback, user_data);
1538 [ + - ]: 142 : g_task_set_source_tag (task, valent_device_send_packet);
1539 : 142 : g_task_set_task_data (task,
1540 : 142 : json_node_ref (packet),
1541 : : (GDestroyNotify)json_node_unref);
1542 : 142 : valent_channel_write_packet (device->channel,
1543 : : packet,
1544 : : cancellable,
1545 : : (GAsyncReadyCallback)valent_device_send_packet_cb,
1546 : : g_steal_pointer (&task));
1547 : : }
1548 : :
1549 : : /**
1550 : : * valent_device_send_packet_finish:
1551 : : * @device: a `ValentDevice`
1552 : : * @result: a `GAsyncResult`
1553 : : * @error: (nullable): a `GError`
1554 : : *
1555 : : * Finish an operation started by [method@Valent.Device.send_packet].
1556 : : *
1557 : : * Returns: %TRUE if successful, or %FALSE with @error set
1558 : : *
1559 : : * Since: 1.0
1560 : : */
1561 : : gboolean
1562 : 126 : valent_device_send_packet_finish (ValentDevice *device,
1563 : : GAsyncResult *result,
1564 : : GError **error)
1565 : : {
1566 [ - + ]: 126 : g_return_val_if_fail (VALENT_IS_DEVICE (device), FALSE);
1567 [ + - ]: 126 : g_return_val_if_fail (g_task_is_valid (result, device), FALSE);
1568 [ + - + - ]: 126 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
1569 : :
1570 : 126 : return g_task_propagate_boolean (G_TASK (result), error);
1571 : : }
1572 : :
1573 : : static void
1574 : 208 : read_packet_cb (ValentChannel *channel,
1575 : : GAsyncResult *result,
1576 : : ValentDevice *device)
1577 : : {
1578 : 416 : g_autoptr (GError) error = NULL;
1579 [ + + ]: 208 : g_autoptr (JsonNode) packet = NULL;
1580 : :
1581 [ - + ]: 208 : g_assert (VALENT_IS_CHANNEL (channel));
1582 [ + - ]: 208 : g_assert (VALENT_IS_DEVICE (device));
1583 : :
1584 : : /* If successful, queue another read before handling the packet. Otherwise
1585 : : * drop our reference if it's still the active channel.
1586 : : */
1587 : 208 : packet = valent_channel_read_packet_finish (channel, result, &error);
1588 [ + + ]: 208 : if (packet != NULL)
1589 : : {
1590 : 128 : valent_channel_read_packet (channel,
1591 : : g_task_get_cancellable (G_TASK (result)),
1592 : : (GAsyncReadyCallback)read_packet_cb,
1593 : : g_object_ref (device));
1594 : 128 : valent_device_handle_packet (device, packet);
1595 : : }
1596 : : else
1597 : : {
1598 : 80 : VALENT_NOTE ("%s: %s", device->name, error->message);
1599 : 80 : valent_object_destroy (VALENT_OBJECT (channel));
1600 : : }
1601 : :
1602 [ + + ]: 208 : g_object_unref (device);
1603 : 208 : }
1604 : :
1605 : : /**
1606 : : * valent_device_get_channels: (get-property channels)
1607 : : * @device: a `ValentDevice`
1608 : : *
1609 : : * Get the active channels.
1610 : : *
1611 : : * Returns: (transfer none): a `GListModel`
1612 : : *
1613 : : * Since: 1.0
1614 : : */
1615 : : GListModel *
1616 : 40 : valent_device_get_channels (ValentDevice *device)
1617 : : {
1618 [ - + ]: 40 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
1619 : :
1620 : 40 : return device->channels;
1621 : : }
1622 : :
1623 : : static void
1624 : 80 : on_channel_destroy (ValentObject *object,
1625 : : ValentDevice *self)
1626 : : {
1627 : 80 : ValentChannel *channel = VALENT_CHANNEL (object);
1628 : 80 : unsigned int position = 0;
1629 : :
1630 [ + - ]: 80 : if (g_list_store_find (G_LIST_STORE (self->channels), object, &position))
1631 : : {
1632 : 80 : g_list_store_remove (G_LIST_STORE (self->channels), position);
1633 : :
1634 : : /* Drop the extra reference if this was the highest priority channel
1635 : : */
1636 [ + - ]: 80 : if (self->channel == channel)
1637 : : {
1638 [ - + ]: 80 : g_warn_if_fail (position == 0);
1639 [ + - ]: 80 : g_clear_object (&self->channel);
1640 : : }
1641 : :
1642 : : /* Notify of the state change if this was the last channel,
1643 : : * or ensure a reference to the channel with the highest priority.
1644 : : */
1645 [ + - ]: 80 : if (g_list_model_get_n_items (self->channels) == 0)
1646 : 80 : g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]);
1647 [ # # ]: 0 : else if (self->channel == NULL)
1648 : 0 : self->channel = g_list_model_get_item (self->channels, 0);
1649 : : }
1650 : 80 : }
1651 : :
1652 : : static inline int
1653 : 0 : channel_sort_func (gconstpointer a,
1654 : : gconstpointer b,
1655 : : gpointer user_data)
1656 : : {
1657 [ # # ]: 0 : if (g_str_equal (G_OBJECT_TYPE_NAME (a), G_OBJECT_TYPE_NAME (b)))
1658 : : return 0;
1659 : :
1660 [ # # ]: 0 : return g_str_equal (G_OBJECT_TYPE_NAME (a), "ValentLanChannel") ? -1 : 1;
1661 : : }
1662 : :
1663 : : /**
1664 : : * valent_device_add_channel:
1665 : : * @device: a `ValentDevice`
1666 : : * @channel: a `ValentChannel`
1667 : : *
1668 : : * Add @channel to the list of active channels.
1669 : : *
1670 : : * Since: 1.0
1671 : : */
1672 : : void
1673 : 80 : valent_device_add_channel (ValentDevice *device,
1674 : : ValentChannel *channel)
1675 : : {
1676 : 80 : JsonNode *peer_identity;
1677 : 160 : g_autoptr (GCancellable) cancellable = NULL;
1678 : 80 : unsigned int n_channels = 0;
1679 : 80 : unsigned int position = 0;
1680 : :
1681 [ - + ]: 80 : g_return_if_fail (VALENT_IS_DEVICE (device));
1682 [ + - ]: 80 : g_return_if_fail (VALENT_IS_CHANNEL (channel));
1683 : :
1684 : 80 : g_signal_connect_object (channel,
1685 : : "destroy",
1686 : : G_CALLBACK (on_channel_destroy),
1687 : : device,
1688 : : G_CONNECT_DEFAULT);
1689 : :
1690 : 80 : n_channels = g_list_model_get_n_items (device->channels);
1691 : 80 : position = g_list_store_insert_sorted (G_LIST_STORE (device->channels),
1692 : : channel,
1693 : : channel_sort_func,
1694 : : NULL);
1695 : :
1696 : : /* Handle the peer identity and start reading the first packet
1697 : : * before notifying of the state change
1698 : : */
1699 : 80 : peer_identity = valent_channel_get_peer_identity (channel);
1700 : 80 : valent_device_handle_identity (device, peer_identity);
1701 : :
1702 : 80 : cancellable = valent_object_ref_cancellable (VALENT_OBJECT (device));
1703 : 80 : valent_channel_read_packet (channel,
1704 : : cancellable,
1705 : : (GAsyncReadyCallback)read_packet_cb,
1706 : : g_object_ref (device));
1707 : :
1708 : : /* Hold a reference to the channel if it has the highest priority,
1709 : : * and notify of the state change if it's the first channel
1710 : : */
1711 [ + - ]: 80 : if (position == 0)
1712 : 80 : g_set_object (&device->channel, channel);
1713 : :
1714 [ + - ]: 80 : if (n_channels == 0)
1715 : : {
1716 : 80 : valent_device_update_plugins (device);
1717 : 80 : g_object_notify_by_pspec (G_OBJECT (device), properties[PROP_STATE]);
1718 : : }
1719 : : }
1720 : :
1721 : : /**
1722 : : * valent_device_get_context: (get-property context)
1723 : : * @device: a `ValentDevice`
1724 : : *
1725 : : * Get the data context.
1726 : : *
1727 : : * Returns: (transfer full): a `ValentContext`
1728 : : *
1729 : : * Since: 1.0
1730 : : */
1731 : : ValentContext *
1732 : 48 : valent_device_get_context (ValentDevice *device)
1733 : : {
1734 [ - + ]: 48 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
1735 : :
1736 : 48 : return device->context;
1737 : : }
1738 : :
1739 : : /**
1740 : : * valent_device_get_icon_name: (get-property icon-name)
1741 : : * @device: a `ValentDevice`
1742 : : *
1743 : : * Get the symbolic icon name.
1744 : : *
1745 : : * Returns: (transfer none): the icon name.
1746 : : *
1747 : : * Since: 1.0
1748 : : */
1749 : : const char *
1750 : 3 : valent_device_get_icon_name (ValentDevice *device)
1751 : : {
1752 [ - + ]: 3 : g_return_val_if_fail (VALENT_IS_DEVICE (device), "computer-symbolic");
1753 : :
1754 : 3 : return device->icon_name;
1755 : : }
1756 : :
1757 : : /**
1758 : : * valent_device_get_id: (get-property id)
1759 : : * @device: a `ValentDevice`
1760 : : *
1761 : : * Get the unique ID.
1762 : : *
1763 : : * Returns: (transfer none): the device id.
1764 : : *
1765 : : * Since: 1.0
1766 : : */
1767 : : const char *
1768 : 68 : valent_device_get_id (ValentDevice *device)
1769 : : {
1770 [ - + ]: 68 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
1771 [ - + ]: 68 : g_return_val_if_fail (device->id != NULL, NULL);
1772 : :
1773 : : return device->id;
1774 : : }
1775 : :
1776 : : /**
1777 : : * valent_device_get_menu:
1778 : : * @device: a `ValentDevice`
1779 : : *
1780 : : * Get the [class@Gio.MenuModel] of the device.
1781 : : *
1782 : : * Plugins may add items and submenus to this when they want to expose actions
1783 : : * with presentation details like a label or icon.
1784 : : *
1785 : : * Returns: (transfer none): a `GMenuModel`
1786 : : *
1787 : : * Since: 1.0
1788 : : */
1789 : : GMenuModel *
1790 : 65 : valent_device_get_menu (ValentDevice *device)
1791 : : {
1792 [ - + ]: 65 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
1793 : :
1794 : 65 : return G_MENU_MODEL (device->menu);
1795 : : }
1796 : :
1797 : : /**
1798 : : * valent_device_get_name:
1799 : : * @device: a `ValentDevice`
1800 : : *
1801 : : * Get the display name of the device.
1802 : : *
1803 : : * Returns: (transfer none) (nullable): a display name, or %NULL if unset
1804 : : *
1805 : : * Since: 1.0
1806 : : */
1807 : : const char *
1808 : 56 : valent_device_get_name (ValentDevice *device)
1809 : : {
1810 [ - + ]: 56 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
1811 : :
1812 : 56 : return device->name;
1813 : : }
1814 : :
1815 : : /**
1816 : : * valent_device_get_state: (get-property state)
1817 : : * @device: a `ValentDevice`
1818 : : *
1819 : : * Get the state of the device.
1820 : : *
1821 : : * Returns: `ValentDeviceState` flags describing the state of the device
1822 : : *
1823 : : * Since: 1.0
1824 : : */
1825 : : ValentDeviceState
1826 : 301 : valent_device_get_state (ValentDevice *device)
1827 : : {
1828 : 301 : ValentDeviceState state = VALENT_DEVICE_STATE_NONE;
1829 : :
1830 [ - + ]: 301 : g_return_val_if_fail (VALENT_IS_DEVICE (device), state);
1831 : :
1832 [ + + ]: 301 : if (device->channel != NULL)
1833 : 138 : state |= VALENT_DEVICE_STATE_CONNECTED;
1834 : :
1835 [ + + ]: 301 : if (device->paired)
1836 : 199 : state |= VALENT_DEVICE_STATE_PAIRED;
1837 : :
1838 [ + + ]: 301 : if (device->incoming_pair > 0)
1839 : 2 : state |= VALENT_DEVICE_STATE_PAIR_INCOMING;
1840 : :
1841 [ + + ]: 301 : if (device->outgoing_pair > 0)
1842 : 1 : state |= VALENT_DEVICE_STATE_PAIR_OUTGOING;
1843 : :
1844 : : return state;
1845 : : }
1846 : :
1847 : : /**
1848 : : * valent_device_get_verification_key:
1849 : : * @device: a `ValentDevice`
1850 : : *
1851 : : * Get a verification key for the device connection.
1852 : : *
1853 : : * Returns: (nullable) (transfer full): a verification key
1854 : : *
1855 : : * Since: 1.0
1856 : : */
1857 : : char *
1858 : 4 : valent_device_get_verification_key (ValentDevice *device)
1859 : : {
1860 : 4 : char *verification_key = NULL;
1861 : :
1862 : 4 : VALENT_ENTRY;
1863 : :
1864 [ - + ]: 4 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
1865 : :
1866 [ + + ]: 4 : if (device->channel != NULL)
1867 : : {
1868 : 6 : g_autoptr (GChecksum) checksum = NULL;
1869 [ + - ]: 2 : g_autoptr (GTlsCertificate) cert = NULL;
1870 [ + - - - ]: 2 : g_autoptr (GTlsCertificate) peer_cert = NULL;
1871 : 2 : GByteArray *pubkey;
1872 : 2 : GByteArray *peer_pubkey;
1873 : 2 : size_t cmplen;
1874 : :
1875 : 2 : cert = valent_channel_ref_certificate (device->channel);
1876 : 2 : peer_cert = valent_channel_ref_peer_certificate (device->channel);
1877 [ - + - - ]: 2 : g_return_val_if_fail (cert != NULL || peer_cert != NULL, NULL);
1878 : :
1879 : 2 : pubkey = valent_certificate_get_public_key (cert);
1880 : 2 : peer_pubkey = valent_certificate_get_public_key (peer_cert);
1881 [ + - ]: 2 : g_return_val_if_fail (pubkey != NULL || peer_pubkey != NULL, NULL);
1882 : :
1883 : 2 : checksum = g_checksum_new (G_CHECKSUM_SHA256);
1884 : 2 : cmplen = MIN (pubkey->len, peer_pubkey->len);
1885 [ + + ]: 2 : if (memcmp (pubkey->data, peer_pubkey->data, cmplen) > 0)
1886 : : {
1887 : 1 : g_checksum_update (checksum, pubkey->data, pubkey->len);
1888 : 1 : g_checksum_update (checksum, peer_pubkey->data, peer_pubkey->len);
1889 : : }
1890 : : else
1891 : : {
1892 : 1 : g_checksum_update (checksum, peer_pubkey->data, peer_pubkey->len);
1893 : 1 : g_checksum_update (checksum, pubkey->data, pubkey->len);
1894 : : }
1895 : :
1896 [ + - ]: 2 : if (device->protocol_version >= VALENT_NETWORK_PROTOCOL_V8)
1897 : : {
1898 : 2 : g_autofree char *timestamp_str = NULL;
1899 : :
1900 : 2 : timestamp_str = g_strdup_printf ("%"PRId64, device->pair_timestamp);
1901 : 2 : g_checksum_update (checksum, (const unsigned char *)timestamp_str, -1);
1902 : : }
1903 : :
1904 [ + - ]: 2 : verification_key = g_ascii_strup (g_checksum_get_string (checksum), 8);
1905 : : }
1906 : :
1907 : 4 : VALENT_RETURN (verification_key);
1908 : : }
1909 : :
1910 : : /**
1911 : : * valent_device_generate_id:
1912 : : *
1913 : : * Generate a new KDE Connect device ID.
1914 : : *
1915 : : * See [func@Valent.Device.validate_id] for a description of valid device ID.
1916 : : *
1917 : : * Returns: (transfer full): a new KDE Connect device ID
1918 : : *
1919 : : * Since: 1.0
1920 : : */
1921 : : char *
1922 : 206 : valent_device_generate_id (void)
1923 : : {
1924 : 206 : static const char hex_digits[16 + 1] = "0123456789abcdef";
1925 : 206 : char *id = g_new (char, 32 + 1);
1926 : :
1927 [ + + ]: 6798 : for (size_t i = 0; i < 32; i++)
1928 : 6592 : id[i] = hex_digits[g_random_int_range (0, 16)];
1929 : 206 : id[32] = '\0';
1930 : :
1931 : 206 : return g_steal_pointer (&id);
1932 : : }
1933 : :
1934 : : /**
1935 : : * valent_device_validate_id:
1936 : : * @id: (nullable): a KDE Connect device ID
1937 : : *
1938 : : * Validate a KDE Connect device ID.
1939 : : *
1940 : : * A compliant device ID matches the pattern `/^[a-zA-Z0-9]{32,38}$/`, being
1941 : : * alphanumeric with a length of 32-38 characters. However, for backwards
1942 : : * compatibility, implementations must accept device IDs that include hyphens
1943 : : * and underscores.
1944 : : *
1945 : : * This became a requirement in version 8 of the KDE Connect protocol.
1946 : : *
1947 : : * Returns: %TRUE if valid, or %FALSE
1948 : : *
1949 : : * Since: 1.0
1950 : : */
1951 : : gboolean
1952 : 152 : valent_device_validate_id (const char *id)
1953 : : {
1954 : 152 : size_t len = 0;
1955 : :
1956 [ + - + + ]: 152 : if G_UNLIKELY (id == NULL || *id == '\0')
1957 : : return FALSE;
1958 : :
1959 [ + + ]: 4980 : while (id[len] != '\0')
1960 : : {
1961 : 4832 : char c = id[len];
1962 : :
1963 [ + + + + ]: 4832 : if (!g_ascii_isalnum (c) && c != '_' && c != '-')
1964 : : return FALSE;
1965 : :
1966 [ + + ]: 4830 : if (++len > 38)
1967 : : return FALSE;
1968 : : }
1969 : :
1970 : 148 : return len >= 32;
1971 : : }
1972 : :
1973 : : /**
1974 : : * valent_device_validate_name:
1975 : : * @name: (nullable): a KDE Connect device name
1976 : : *
1977 : : * Validate a KDE Connect device name.
1978 : : *
1979 : : * A compliant device name matches the pattern `/^[^"',;:.!?()\[\]<>]{1,32}$/`,
1980 : : * containing none of `"',;:.!?()[]<>` with a length of 1-32 characters, with
1981 : : * at least one non-whitespace character.
1982 : : *
1983 : : * This became a requirement in version 8 or the KDE Connect protocol.
1984 : : *
1985 : : * Returns: %TRUE if valid, or %FALSE
1986 : : *
1987 : : * Since: 1.0
1988 : : */
1989 : : gboolean
1990 : 13 : valent_device_validate_name (const char *name)
1991 : : {
1992 : 13 : static const uint8_t forbidden_chars[256] = {
1993 : : ['"'] = 1, ['\''] = 1,
1994 : : [','] = 1, ['.'] = 1,
1995 : : [';'] = 1, [':'] = 1,
1996 : : ['!'] = 1, ['?'] = 1,
1997 : : ['('] = 1, [')'] = 1,
1998 : : ['['] = 1, [']'] = 1,
1999 : : ['<'] = 1, ['>'] = 1,
2000 : : };
2001 : 13 : size_t len = 0;
2002 : 13 : gboolean has_nonwhitespace = FALSE;
2003 : :
2004 [ + - + - ]: 13 : if G_UNLIKELY (name == NULL || *name == '\0')
2005 : : return FALSE;
2006 : :
2007 [ + + ]: 135 : for (const char *p = name; *p != '\0'; p = g_utf8_next_char (p))
2008 : : {
2009 : 123 : gunichar ch = g_utf8_get_char (p);
2010 [ + - + + ]: 123 : if (ch <= 127 && forbidden_chars[(uint8_t)ch])
2011 : : return FALSE;
2012 : :
2013 [ + + ]: 122 : if (!has_nonwhitespace)
2014 : 15 : has_nonwhitespace = !g_unichar_isspace (ch);
2015 : :
2016 [ + - ]: 122 : if (++len > 32)
2017 : : return FALSE;
2018 : : }
2019 : :
2020 : : return has_nonwhitespace;
2021 : : }
2022 : :
|