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