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