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