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-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <json-glib/json-glib.h>
10 : : #include <libpeas.h>
11 : : #include <libvalent-core.h>
12 : :
13 : : #include "valent-device.h"
14 : : #include "valent-device-plugin.h"
15 : : #include "valent-packet.h"
16 : :
17 : : /**
18 : : * ValentDevicePlugin:
19 : : *
20 : : * An abstract base class for device plugins.
21 : : *
22 : : * `ValentDevicePlugin` is a base class for plugins that operate in the scope of
23 : : * a single device. This usually means communicating with other devices, however
24 : : * plugins aren't required to be packet based.
25 : : *
26 : : * ## Plugin Requirements
27 : : *
28 : : * Device plugins essentially have two sets of dependent conditions for being
29 : : * enabled. Plugins become available (i.e. can be enabled) when any of the
30 : : * following are true:
31 : : *
32 : : * - any of the device's outgoing capabilities match any of the plugin's
33 : : * incoming capabilities
34 : : * - any of the device's incoming capabilities match any of the plugin's
35 : : * outgoing capabilities
36 : : * - the plugin doesn't list any capabilities (eg. a non-packet based plugin)
37 : : *
38 : : * When a plugin becomes available it may be enabled, disabled and configured.
39 : : *
40 : : * ## Plugin Actions
41 : : *
42 : : * `ValentDevicePlugin` implements the [iface@Gio.ActionGroup] and
43 : : * [iface@Gio.ActionMap] interfaces, providing a simple way for plugins to
44 : : * expose functions and states. Each [iface@Gio.Action] added to the action map
45 : : * will be included in the device action group with the plugin's module name as
46 : : * a prefix (eg. `share.uri`).
47 : : *
48 : : * If the [class@Valent.DeviceManager] is exported on D-Bus, the actions will be
49 : : * exported along with the [class@Valent.Device].
50 : : *
51 : : * ## Implementation Notes
52 : : *
53 : : * Implementations that define `X-DevicePluginIncoming` in the `.plugin` file
54 : : * must override [vfunc@Valent.DevicePlugin.handle_packet] to handle incoming
55 : : * packets. Implementations that depend on the device state, especially those
56 : : * that define `X-DevicePluginOutgoing` in the `.plugin` file, should override
57 : : * [vfunc@Valent.DevicePlugin.update_state].
58 : : *
59 : : * ## `.plugin` File
60 : : *
61 : : * Implementations may define the following extra fields in the `.plugin` file:
62 : : *
63 : : * - `X-DevicePluginIncoming`
64 : : *
65 : : * A list of packet types (eg. `kdeconnect.ping`) separated by semi-colons
66 : : * indicating the packets that the plugin can handle.
67 : : *
68 : : * - `X-DevicePluginOutgoing`
69 : : *
70 : : * A list of packet types (eg. `kdeconnect.share.request`) separated by
71 : : * semi-colons indicating the packets that the plugin may send.
72 : : *
73 : : * - `X-DevicePluginSettings`
74 : : *
75 : : * A [class@Gio.Settings] schema ID for the plugin's settings. See
76 : : * [method@Valent.Context.get_plugin_settings] for more information.
77 : : *
78 : : * Since: 1.0
79 : : */
80 : :
81 : : typedef struct
82 : : {
83 : : ValentDeviceState state;
84 : : GQueue packets;
85 : : } ValentDevicePluginPrivate;
86 : :
87 [ + + + - ]: 5786 : G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (ValentDevicePlugin, valent_device_plugin, VALENT_TYPE_EXTENSION)
88 : :
89 : : /* LCOV_EXCL_START */
90 : : static void
91 : : valent_device_plugin_real_handle_packet (ValentDevicePlugin *plugin,
92 : : const char *type,
93 : : JsonNode *packet)
94 : : {
95 : : g_assert (VALENT_IS_DEVICE_PLUGIN (plugin));
96 : : g_assert (type != NULL && *type != '\0');
97 : : g_assert (VALENT_IS_PACKET (packet));
98 : :
99 : : g_critical ("%s: expected handler for \"%s\" packet",
100 : : G_OBJECT_TYPE_NAME (plugin),
101 : : type);
102 : : }
103 : :
104 : : static void
105 : : valent_device_plugin_real_update_state (ValentDevicePlugin *plugin,
106 : : ValentDeviceState state)
107 : : {
108 : : g_assert (VALENT_IS_DEVICE_PLUGIN (plugin));
109 : : }
110 : : /* LCOV_EXCL_STOP */
111 : :
112 : : static void
113 : 99 : valent_device_send_packet_cb (ValentDevice *device,
114 : : GAsyncResult *result,
115 : : gpointer user_data)
116 : : {
117 : 198 : g_autoptr (GError) error = NULL;
118 : :
119 [ + + ]: 99 : if (!valent_device_send_packet_finish (device, result, &error))
120 : : {
121 [ - + ]: 4 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
122 : 0 : g_critical ("%s(): %s", G_STRFUNC, error->message);
123 [ - + ]: 4 : else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_CONNECTED))
124 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
125 [ + - ]: 4 : else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
126 : 4 : g_debug ("%s(): %s", G_STRFUNC, error->message);
127 : : }
128 : 99 : }
129 : :
130 : : /*
131 : : * GObject
132 : : */
133 : : static void
134 : 209 : valent_device_plugin_finalize (GObject *object)
135 : : {
136 : 209 : ValentDevicePlugin *self = VALENT_DEVICE_PLUGIN (object);
137 : 209 : ValentDevicePluginPrivate *priv = valent_device_plugin_get_instance_private (self);
138 : :
139 : 209 : g_queue_clear_full (&priv->packets, (GDestroyNotify)json_node_unref);
140 : :
141 : 209 : G_OBJECT_CLASS (valent_device_plugin_parent_class)->finalize (object);
142 : 209 : }
143 : :
144 : : static void
145 : 58 : valent_device_plugin_class_init (ValentDevicePluginClass *klass)
146 : : {
147 : 58 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
148 : :
149 : 58 : object_class->finalize = valent_device_plugin_finalize;
150 : :
151 : 58 : klass->handle_packet = valent_device_plugin_real_handle_packet;
152 : 58 : klass->update_state = valent_device_plugin_real_update_state;
153 : : }
154 : :
155 : : static void
156 : 216 : valent_device_plugin_init (ValentDevicePlugin *self)
157 : : {
158 : 216 : }
159 : :
160 : : /**
161 : : * valent_device_plugin_queue_packet:
162 : : * @plugin: a `ValentDevicePlugin`
163 : : * @packet: a KDE Connect packet
164 : : *
165 : : * Queue a KDE Connect packet to be sent to the device this plugin is bound to.
166 : : *
167 : : * For notification of success, you may call [method@Valent.Resource.get_source]
168 : : * and then [method@Valent.Device.send_packet], but note that there can be no
169 : : * guarantee the remote device has received the packet.
170 : : *
171 : : * Since: 1.0
172 : : */
173 : : void
174 : 99 : valent_device_plugin_queue_packet (ValentDevicePlugin *plugin,
175 : : JsonNode *packet)
176 : : {
177 : 99 : ValentDevice *device = NULL;
178 : 198 : g_autoptr (GCancellable) destroy = NULL;
179 : :
180 [ + - ]: 99 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
181 [ - + ]: 99 : g_return_if_fail (VALENT_IS_PACKET (packet));
182 : :
183 [ + - ]: 99 : if ((device = valent_resource_get_source (VALENT_RESOURCE (plugin))) == NULL)
184 : : return;
185 : :
186 : 99 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (plugin));
187 [ + - ]: 99 : valent_device_send_packet (device,
188 : : packet,
189 : : destroy,
190 : : (GAsyncReadyCallback)valent_device_send_packet_cb,
191 : : NULL);
192 : : }
193 : :
194 : : /**
195 : : * valent_device_plugin_show_notification:
196 : : * @plugin: a `ValentDevicePlugin`
197 : : * @id: an id for the notification
198 : : * @notification: a `GNotification`
199 : : *
200 : : * A convenience for showing a local notification.
201 : : *
202 : : * @id will be automatically prepended with the device ID and plugin module to
203 : : * prevent conflicting with other devices and plugins.
204 : : *
205 : : * Call [method@Valent.DevicePlugin.hide_notification] to make the same
206 : : * transformation on @id and withdraw the notification.
207 : : *
208 : : * Since: 1.0
209 : : */
210 : : void
211 : 40 : valent_device_plugin_show_notification (ValentDevicePlugin *plugin,
212 : : const char *id,
213 : : GNotification *notification)
214 : : {
215 : 40 : GApplication *application = g_application_get_default ();
216 : 40 : g_autoptr (ValentDevice) device = NULL;
217 [ - - - + ]: 40 : g_autoptr (PeasPluginInfo) plugin_info = NULL;
218 [ - - - + ]: 40 : g_autofree char *notification_id = NULL;
219 : :
220 [ + - ]: 40 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
221 [ - + ]: 40 : g_return_if_fail (id != NULL);
222 [ + - + - : 40 : g_return_if_fail (G_IS_NOTIFICATION (notification));
- + - - ]
223 : :
224 [ - + ]: 40 : if G_UNLIKELY (application == NULL)
225 : : return;
226 : :
227 : 0 : g_object_get (plugin,
228 : : "plugin-info", &plugin_info,
229 : : "source", &device,
230 : : NULL);
231 : 0 : notification_id = g_strdup_printf ("%s::%s::%s",
232 : : valent_device_get_id (device),
233 : : peas_plugin_info_get_module_name (plugin_info),
234 : : id);
235 : 0 : g_application_send_notification (application, notification_id, notification);
236 : : }
237 : :
238 : : /**
239 : : * valent_device_plugin_hide_notification:
240 : : * @plugin: a `ValentDevicePlugin`
241 : : * @id: an id for the notification
242 : : *
243 : : * A convenience for withdrawing a notification.
244 : : *
245 : : * This method will withdraw a notification shown with
246 : : * [method@Valent.DevicePlugin.show_notification].
247 : : *
248 : : * Since: 1.0
249 : : */
250 : : void
251 : 17 : valent_device_plugin_hide_notification (ValentDevicePlugin *plugin,
252 : : const char *id)
253 : : {
254 : 17 : GApplication *application = g_application_get_default ();
255 : 17 : g_autoptr (ValentDevice) device = NULL;
256 [ - - - + ]: 17 : g_autoptr (PeasPluginInfo) plugin_info = NULL;
257 [ - - - + ]: 17 : g_autofree char *notification_id = NULL;
258 : :
259 [ + - ]: 17 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
260 [ - + ]: 17 : g_return_if_fail (id != NULL);
261 : :
262 [ - + ]: 17 : if G_UNLIKELY (application == NULL)
263 : : return;
264 : :
265 : 0 : g_object_get (plugin,
266 : : "plugin-info", &plugin_info,
267 : : "source", &device,
268 : : NULL);
269 : 0 : notification_id = g_strdup_printf ("%s::%s::%s",
270 : : valent_device_get_id (device),
271 : : peas_plugin_info_get_module_name (plugin_info),
272 : : id);
273 : 0 : g_application_withdraw_notification (application, notification_id);
274 : : }
275 : :
276 : : /**
277 : : * valent_device_plugin_handle_packet: (virtual handle_packet)
278 : : * @plugin: a `ValentDevicePlugin`
279 : : * @type: a KDE Connect packet type
280 : : * @packet: a KDE Connect packet
281 : : *
282 : : * Handle a packet from the device the plugin is bound to.
283 : : *
284 : : * This is called when the device receives a packet type included in the
285 : : * `X-DevicePluginIncoming` field of the `.plugin` file.
286 : : *
287 : : * This is optional for implementations which do not register any incoming
288 : : * capabilities, such as plugins that do not provide packet-based functionality.
289 : : *
290 : : * Since: 1.0
291 : : */
292 : : void
293 : 119 : valent_device_plugin_handle_packet (ValentDevicePlugin *plugin,
294 : : const char *type,
295 : : JsonNode *packet)
296 : : {
297 : 119 : ValentDevicePluginPrivate *priv = valent_device_plugin_get_instance_private (plugin);
298 : :
299 : 119 : VALENT_ENTRY;
300 : :
301 [ + - ]: 119 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
302 [ + - - + ]: 119 : g_return_if_fail (type != NULL && *type != '\0');
303 [ - + ]: 119 : g_return_if_fail (VALENT_IS_PACKET (packet));
304 : :
305 [ + - ]: 119 : if G_UNLIKELY ((priv->state & VALENT_DEVICE_STATE_CONNECTED) == 0)
306 : : {
307 : 0 : VALENT_NOTE ("Disconnected; stashing \"%s\" packet", type);
308 : 0 : g_queue_push_tail (&priv->packets, json_node_ref (packet));
309 : 0 : return;
310 : : }
311 : :
312 : 119 : VALENT_DEVICE_PLUGIN_GET_CLASS (plugin)->handle_packet (plugin, type, packet);
313 : :
314 : 119 : VALENT_EXIT;
315 : : }
316 : :
317 : : /**
318 : : * valent_device_plugin_update_state: (virtual update_state)
319 : : * @plugin: a `ValentDevicePlugin`
320 : : * @state: a `ValentDeviceState`
321 : : *
322 : : * Update the plugin based on the new state of the device.
323 : : *
324 : : * This function is called when the connected or paired state of the device
325 : : * changes. This may be used to configure actions, event handlers that may
326 : : * trigger outgoing packets and exchange connect-time data with the device.
327 : : *
328 : : * This is optional for all implementations as plugins aren't required to be
329 : : * dependent on the device state.
330 : : *
331 : : * Since: 1.0
332 : : */
333 : : void
334 : 732 : valent_device_plugin_update_state (ValentDevicePlugin *plugin,
335 : : ValentDeviceState state)
336 : : {
337 : 732 : ValentDevicePluginPrivate *priv = valent_device_plugin_get_instance_private (plugin);
338 : :
339 : 732 : VALENT_ENTRY;
340 : :
341 [ + - ]: 732 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
342 : :
343 : 732 : priv->state = state;
344 : 732 : VALENT_DEVICE_PLUGIN_GET_CLASS (plugin)->update_state (plugin, state);
345 : :
346 : : // FIXME: this is a terrible, awful, no good hack
347 [ + + ]: 732 : if ((priv->state & VALENT_DEVICE_STATE_PAIRED) == 0)
348 : : {
349 : 258 : g_queue_clear_full (&priv->packets, (GDestroyNotify)json_node_unref);
350 : : }
351 [ + + ]: 474 : else if ((priv->state & VALENT_DEVICE_STATE_CONNECTED) != 0)
352 : : {
353 : 155 : ValentDevicePluginClass *klass = VALENT_DEVICE_PLUGIN_GET_CLASS (plugin);
354 : :
355 : 155 : while (priv->packets.head != NULL)
356 : : {
357 [ - + ]: 155 : g_autoptr (JsonNode) packet = g_queue_pop_head (&priv->packets);
358 [ # # ]: 0 : klass->handle_packet (plugin, valent_packet_get_type (packet), packet);
359 : : }
360 : : }
361 : :
362 : 732 : VALENT_EXIT;
363 : : }
364 : :
365 : : static int
366 : 163 : _g_menu_find_action (GMenuModel *menu,
367 : : const char *action)
368 : : {
369 : 163 : int i, n_items;
370 : :
371 [ + - + - : 163 : g_assert (G_IS_MENU_MODEL (menu));
+ - - + ]
372 [ - + ]: 163 : g_assert (action != NULL);
373 : :
374 : 163 : n_items = g_menu_model_get_n_items (menu);
375 : :
376 [ + + ]: 375 : for (i = 0; i < n_items; i++)
377 : : {
378 : 57 : g_autofree char *item_str = NULL;
379 : :
380 : 57 : g_menu_model_get_item_attribute (menu, i, "action", "s", &item_str);
381 : :
382 [ + + ]: 57 : if (g_strcmp0 (item_str, action) == 0)
383 : 8 : return i;
384 : : }
385 : :
386 : : return -1;
387 : : }
388 : :
389 : : /**
390 : : * valent_device_plugin_set_menu_action:
391 : : * @plugin: a `ValentDevicePlugin`
392 : : * @action: a `GAction` name
393 : : * @label: (nullable): a label for the action
394 : : * @icon_name: (nullable): an icon for the action
395 : : *
396 : : * Set or remove a device menu action by [iface@Gio.Action] name.
397 : : *
398 : : * If @label and @icon are %NULL, @action will be removed from the menu.
399 : : *
400 : : * Since: 1.0
401 : : */
402 : : void
403 : 146 : valent_device_plugin_set_menu_action (ValentDevicePlugin *plugin,
404 : : const char *action,
405 : : const char *label,
406 : : const char *icon_name)
407 : : {
408 : 292 : g_autoptr (GMenuItem) item = NULL;
409 : :
410 [ + - ]: 146 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
411 [ + - - + ]: 146 : g_return_if_fail (action != NULL && *action != '\0');
412 [ + - + - : 146 : g_return_if_fail ((label == NULL && icon_name == NULL) ||
- + ]
413 : : (label != NULL && *label != '\0'));
414 : :
415 [ + - ]: 146 : if (label != NULL)
416 : : {
417 : 292 : g_autoptr (GIcon) icon = NULL;
418 : :
419 [ + - ]: 146 : if (icon_name != NULL)
420 : 146 : icon = g_themed_icon_new (icon_name);
421 : :
422 : 146 : item = g_menu_item_new (label, action);
423 : 146 : g_menu_item_set_icon (item, icon);
424 [ + - ]: 146 : g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
425 : : }
426 : :
427 [ + - ]: 146 : valent_device_plugin_set_menu_item (plugin, action, item);
428 : : }
429 : :
430 : : /**
431 : : * valent_device_plugin_set_menu_item:
432 : : * @plugin: a `ValentDevicePlugin`
433 : : * @action: a `GAction` name
434 : : * @item: (nullable): a `GMenuItem`
435 : : *
436 : : * Set or remove a device [class@Gio.MenuItem] by [iface@Gio.Action] name.
437 : : *
438 : : * If @item is %NULL, @action will be removed from the menu.
439 : : *
440 : : * Since: 1.0
441 : : */
442 : : void
443 : 427 : valent_device_plugin_set_menu_item (ValentDevicePlugin *plugin,
444 : : const char *action,
445 : : GMenuItem *item)
446 : : {
447 : 427 : ValentDevice *device = NULL;
448 : 427 : GMenuModel *menu;
449 : 427 : int index_ = -1;
450 : :
451 [ + - ]: 427 : g_return_if_fail (VALENT_IS_DEVICE_PLUGIN (plugin));
452 [ + - - + ]: 427 : g_return_if_fail (action != NULL && *action != '\0');
453 [ + + + - : 427 : g_return_if_fail (item == NULL || G_IS_MENU_ITEM (item));
- + - - ]
454 : :
455 : : /* NOTE: this method may be called by plugins in their `dispose()` */
456 [ + + ]: 427 : if ((device = valent_resource_get_source (VALENT_RESOURCE (plugin))) == NULL)
457 : : return;
458 : :
459 : 163 : menu = valent_device_get_menu (device);
460 : 163 : index_ = _g_menu_find_action (menu, action);
461 : :
462 [ + + ]: 163 : if (index_ > -1)
463 : 8 : g_menu_remove (G_MENU (menu), index_);
464 : :
465 [ + + ]: 163 : if (item != NULL)
466 : : {
467 [ - + ]: 147 : if (index_ > -1)
468 : 0 : g_menu_insert_item (G_MENU (menu), index_, item);
469 : : else
470 : 147 : g_menu_append_item (G_MENU (menu), item);
471 : : }
472 : : }
473 : :
474 : : /**
475 : : * valent_notification_set_device_action:
476 : : * @notification: a `GNotification`
477 : : * @device: a `ValentDevice`
478 : : * @action: the device action name
479 : : * @target: (nullable): the action target
480 : : *
481 : : * Set the default action for @notification. @action is wrapped in the special
482 : : * `device` action for @device, which allows it to be activated from the `app`
483 : : * action scope.
484 : : *
485 : : * Since: 1.0
486 : : */
487 : : void
488 : 1 : valent_notification_set_device_action (GNotification *notification,
489 : : ValentDevice *device,
490 : : const char *action,
491 : : GVariant *target)
492 : : {
493 : 1 : GVariantBuilder builder;
494 : :
495 [ + - + - : 1 : g_return_if_fail (G_IS_NOTIFICATION (notification));
- + - - ]
496 [ - + ]: 1 : g_return_if_fail (VALENT_IS_DEVICE (device));
497 [ + - - + ]: 1 : g_return_if_fail (action != NULL && *action != '\0');
498 : :
499 : 1 : g_variant_builder_init (&builder, G_VARIANT_TYPE ("av"));
500 : :
501 [ + - ]: 1 : if (target != NULL)
502 : 1 : g_variant_builder_add (&builder, "v", target);
503 : :
504 : 1 : g_notification_set_default_action_and_target (notification,
505 : : "app.device",
506 : : "(ssav)",
507 : : valent_device_get_id (device),
508 : : action,
509 : : &builder);
510 : : }
511 : :
512 : : /**
513 : : * valent_notification_add_device_button:
514 : : * @notification: a `GNotification`
515 : : * @device: a `ValentDevice`
516 : : * @label: the button label
517 : : * @action: the device action name
518 : : * @target: (nullable): the action target
519 : : *
520 : : * Add an action button to @notification. @action is wrapped in the special
521 : : * `device` action for @device, which allows it to be activated from the `app`
522 : : * action scope.
523 : : *
524 : : * Since: 1.0
525 : : */
526 : : void
527 : 31 : valent_notification_add_device_button (GNotification *notification,
528 : : ValentDevice *device,
529 : : const char *label,
530 : : const char *action,
531 : : GVariant *target)
532 : : {
533 : 31 : GVariantBuilder builder;
534 : :
535 [ + - + - : 31 : g_return_if_fail (G_IS_NOTIFICATION (notification));
- + - - ]
536 [ - + ]: 31 : g_return_if_fail (VALENT_IS_DEVICE (device));
537 [ + - - + ]: 31 : g_return_if_fail (label != NULL && *label != '\0');
538 [ + - - + ]: 31 : g_return_if_fail (action != NULL && *action != '\0');
539 : :
540 : 31 : g_variant_builder_init (&builder, G_VARIANT_TYPE ("av"));
541 : :
542 [ + + ]: 31 : if (target != NULL)
543 : 26 : g_variant_builder_add (&builder, "v", target);
544 : :
545 : 31 : g_notification_add_button_with_target (notification,
546 : : label,
547 : : "app.device",
548 : : "(ssav)",
549 : : valent_device_get_id (device),
550 : : action,
551 : : &builder);
552 : : }
553 : :
|