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