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