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-mpris-adapter"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <libportal/portal.h>
9 : : #include <valent.h>
10 : :
11 : : #include "valent-mpris-adapter.h"
12 : : #include "valent-mpris-impl.h"
13 : : #include "valent-mpris-player.h"
14 : : #include "valent-mpris-utils.h"
15 : :
16 : :
17 : : struct _ValentMPRISAdapter
18 : : {
19 : : ValentMediaAdapter parent_instance;
20 : :
21 : : GDBusConnection *connection;
22 : : unsigned int name_owner_changed_id;
23 : : GHashTable *players;
24 : :
25 : : /* Exports */
26 : : ValentMPRISImpl *active_export;
27 : : GHashTable *exports;
28 : : };
29 : :
30 : : static void g_async_initable_iface_init (GAsyncInitableIface *iface);
31 : :
32 [ + + + - ]: 17 : G_DEFINE_FINAL_TYPE_WITH_CODE (ValentMPRISAdapter, valent_mpris_adapter, VALENT_TYPE_MEDIA_ADAPTER,
33 : : G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, g_async_initable_iface_init))
34 : :
35 : :
36 : : static unsigned int n_exports = 0;
37 : :
38 : : static void
39 : 0 : on_player_state_changed (ValentMPRISAdapter *self,
40 : : GParamSpec *pspec,
41 : : ValentMediaPlayer *player)
42 : : {
43 : 0 : ValentMPRISImpl *export = NULL;
44 : :
45 [ # # ]: 0 : g_assert (VALENT_IS_MPRIS_ADAPTER (self));
46 [ # # # # ]: 0 : g_assert (player == NULL || VALENT_IS_MEDIA_PLAYER (player));
47 : :
48 [ # # ]: 0 : if (g_hash_table_size (self->exports) == 0)
49 : 0 : return;
50 : :
51 : : /* @player may be %NULL if the active export was just removed */
52 [ # # ]: 0 : if (player == NULL)
53 : : {
54 : 0 : GHashTableIter iter;
55 : :
56 : 0 : g_hash_table_iter_init (&iter, self->exports);
57 : 0 : g_hash_table_iter_next (&iter, (void **)&player, NULL);
58 : : }
59 : :
60 [ # # ]: 0 : g_assert (VALENT_IS_MEDIA_PLAYER (player));
61 : :
62 : : /* The policy is to favour the player that most recently started playing.
63 : : *
64 : : * If a player just started playing it will replace any active export, under
65 : : * the assumption that it is now the focus of attention.
66 : : *
67 : : * If it was stopped or paused, another that is currently playing may become
68 : : * the active export, but may not replace an existing player.
69 : : */
70 [ # # ]: 0 : if (valent_media_player_get_state (player) == VALENT_MEDIA_STATE_PLAYING)
71 : : {
72 : 0 : export = g_hash_table_lookup (self->exports, player);
73 : : }
74 [ # # ]: 0 : else if (self->active_export != NULL)
75 : : {
76 : 0 : export = self->active_export;
77 : : }
78 : : else
79 : : {
80 : 0 : GHashTableIter iter;
81 : :
82 : 0 : g_hash_table_iter_init (&iter, self->exports);
83 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (void **)&player, (void **)&export))
84 : : {
85 [ # # ]: 0 : if (valent_media_player_get_state (player) == VALENT_MEDIA_STATE_PLAYING)
86 : : break;
87 : : }
88 : : }
89 : :
90 [ # # ]: 0 : g_assert (VALENT_IS_MPRIS_IMPL (export));
91 : :
92 [ # # ]: 0 : if (self->active_export != export)
93 : : {
94 : 0 : g_autoptr (GError) error = NULL;
95 : :
96 [ # # ]: 0 : g_clear_pointer (&self->active_export, valent_mpris_impl_unexport);
97 : :
98 [ # # ]: 0 : if (!valent_mpris_impl_export (export, self->connection, &error))
99 : : {
100 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
101 [ # # ]: 0 : return;
102 : : }
103 : :
104 : 0 : self->active_export = export;
105 : 0 : g_object_freeze_notify (G_OBJECT (player));
106 : 0 : g_object_notify (G_OBJECT (player), "flags");
107 : 0 : g_object_notify (G_OBJECT (player), "metadata");
108 : 0 : g_object_notify (G_OBJECT (player), "name");
109 : 0 : g_object_notify (G_OBJECT (player), "repeat");
110 : 0 : g_object_notify (G_OBJECT (player), "shuffle");
111 : 0 : g_object_notify (G_OBJECT (player), "state");
112 : 0 : g_object_notify (G_OBJECT (player), "volume");
113 [ # # ]: 0 : g_object_thaw_notify (G_OBJECT (player));
114 : : }
115 : : }
116 : :
117 : : static void
118 : 4 : g_async_initable_new_async_cb (GObject *object,
119 : : GAsyncResult *result,
120 : : gpointer user_data)
121 : : {
122 : 4 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (user_data);
123 : 4 : GAsyncInitable *initable = G_ASYNC_INITABLE (object);
124 : 4 : g_autoptr (GObject) player = NULL;
125 [ - - ]: 4 : g_autofree char *name = NULL;
126 : 4 : g_autoptr (GError) error = NULL;
127 : :
128 [ + - ]: 4 : g_assert (VALENT_IS_MPRIS_ADAPTER (self));
129 [ + - + - : 4 : g_assert (G_IS_ASYNC_INITABLE (initable));
+ - - + ]
130 : :
131 [ - + ]: 4 : if ((player = g_async_initable_new_finish (initable, result, &error)) == NULL)
132 : : {
133 [ # # ]: 0 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
134 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
135 : :
136 : 0 : return;
137 : : }
138 : :
139 : 4 : g_object_get (player, "bus-name", &name, NULL);
140 : :
141 [ + - ]: 4 : if (g_hash_table_contains (self->players, name))
142 : : return;
143 : :
144 : 4 : g_hash_table_replace (self->players,
145 : : g_steal_pointer (&name),
146 : : g_object_ref (player));
147 : :
148 [ - + ]: 4 : valent_media_adapter_player_added (VALENT_MEDIA_ADAPTER (self),
149 : : VALENT_MEDIA_PLAYER (player));
150 : : }
151 : :
152 : : static void
153 : 10 : on_name_owner_changed (GDBusConnection *connection,
154 : : const char *sender_name,
155 : : const char *object_path,
156 : : const char *interface_name,
157 : : const char *signal_name,
158 : : GVariant *parameters,
159 : : gpointer user_data)
160 : : {
161 : 10 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (user_data);
162 : 10 : const char *name;
163 : 10 : const char *old_owner;
164 : 10 : const char *new_owner;
165 : 10 : gboolean known;
166 : :
167 : 10 : g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner);
168 : :
169 : : /* This is the D-Bus name we export on */
170 [ - + + + : 10 : if G_UNLIKELY (g_str_has_prefix (name, VALENT_MPRIS_DBUS_NAME))
- + ]
171 : 2 : return;
172 : :
173 : 8 : known = g_hash_table_contains (self->players, name);
174 : :
175 [ + + + - ]: 8 : if (*new_owner != '\0' && !known)
176 : : {
177 : 8 : g_autoptr (GCancellable) destroy = NULL;
178 : :
179 : : /* Cancel initialization if the adapter is destroyed */
180 : 4 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
181 [ + - ]: 4 : g_async_initable_new_async (VALENT_TYPE_MPRIS_PLAYER,
182 : : G_PRIORITY_DEFAULT,
183 : : destroy,
184 : : g_async_initable_new_async_cb,
185 : : self,
186 : : "bus-name", name,
187 : : NULL);
188 : : }
189 [ + - + - ]: 4 : else if (*old_owner != '\0' && known)
190 : : {
191 : 4 : ValentMediaAdapter *adapter = VALENT_MEDIA_ADAPTER (self);
192 : 4 : gpointer key, value;
193 : :
194 [ + - ]: 4 : if (g_hash_table_steal_extended (self->players, name, &key, &value))
195 : : {
196 : 4 : valent_media_adapter_player_removed (adapter, value);
197 : 4 : g_free (key);
198 : 4 : g_object_unref (value);
199 : : }
200 : : }
201 : : }
202 : :
203 : : /*
204 : : * GAsyncInitable
205 : : */
206 : : static void
207 : 6 : list_names_cb (GDBusConnection *connection,
208 : : GAsyncResult *result,
209 : : gpointer user_data)
210 : : {
211 : 6 : ValentMPRISAdapter *self;
212 : 12 : g_autoptr (GTask) task = G_TASK (user_data);
213 [ - - + - ]: 6 : g_autoptr (GVariant) reply = NULL;
214 : :
215 : 6 : self = g_task_get_source_object (task);
216 : :
217 : : /* If successful, add any currently exported players */
218 : 6 : reply = g_dbus_connection_call_finish (connection, result, NULL);
219 : :
220 [ + - ]: 6 : if (reply != NULL)
221 : : {
222 : 6 : g_autoptr (GCancellable) destroy = NULL;
223 [ + - ]: 6 : g_autoptr (GVariant) names = NULL;
224 : 6 : GVariantIter iter;
225 : 6 : const char *name;
226 : :
227 : 6 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (self));
228 : 6 : names = g_variant_get_child_value (reply, 0);
229 : 6 : g_variant_iter_init (&iter, names);
230 : :
231 [ + + ]: 18 : while (g_variant_iter_next (&iter, "&s", &name))
232 : : {
233 [ - + - + : 12 : if G_LIKELY (!g_str_has_prefix (name, "org.mpris.MediaPlayer2"))
- - ]
234 : 12 : continue;
235 : :
236 : : /* This is the D-Bus name we export on */
237 [ # # # # : 0 : if G_UNLIKELY (g_str_has_prefix (name, VALENT_MPRIS_DBUS_NAME))
# # ]
238 : 0 : continue;
239 : :
240 : 0 : g_async_initable_new_async (VALENT_TYPE_MPRIS_PLAYER,
241 : : G_PRIORITY_DEFAULT,
242 : : destroy,
243 : : g_async_initable_new_async_cb,
244 : : self,
245 : : "bus-name", name,
246 : : NULL);
247 : : }
248 : : }
249 : :
250 [ - + ]: 6 : if (g_task_return_error_if_cancelled (task))
251 [ # # ]: 0 : return;
252 : :
253 : : /* Regardless of the result of `ListNames()`, the connection is valid */
254 : 12 : self->name_owner_changed_id =
255 : 6 : g_dbus_connection_signal_subscribe (connection,
256 : : "org.freedesktop.DBus",
257 : : "org.freedesktop.DBus",
258 : : "NameOwnerChanged",
259 : : "/org/freedesktop/DBus",
260 : : "org.mpris.MediaPlayer2",
261 : : G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE,
262 : : on_name_owner_changed,
263 : : self, NULL);
264 : :
265 : :
266 : : /* Report the adapter as active */
267 : 6 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
268 : : VALENT_PLUGIN_STATE_ACTIVE,
269 : : NULL);
270 [ + - ]: 6 : g_task_return_boolean (task, TRUE);
271 : : }
272 : :
273 : : static void
274 : 6 : valent_mpris_adapter_init_async (GAsyncInitable *initable,
275 : : int io_priority,
276 : : GCancellable *cancellable,
277 : : GAsyncReadyCallback callback,
278 : : gpointer user_data)
279 : : {
280 : 6 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (initable);
281 : 6 : g_autoptr (GTask) task = NULL;
282 [ - - ]: 6 : g_autoptr (GCancellable) destroy = NULL;
283 [ - - + - ]: 6 : g_autoptr (GError) error = NULL;
284 : :
285 [ + - ]: 6 : g_assert (VALENT_IS_MPRIS_ADAPTER (self));
286 : :
287 : : /* Cede the primary position until complete */
288 : 6 : valent_extension_plugin_state_changed (VALENT_EXTENSION (initable),
289 : : VALENT_PLUGIN_STATE_INACTIVE,
290 : : NULL);
291 : :
292 : : /* Cancel initialization if the object is destroyed */
293 : 6 : destroy = valent_object_chain_cancellable (VALENT_OBJECT (initable),
294 : : cancellable);
295 : :
296 : 6 : task = g_task_new (initable, destroy, callback, user_data);
297 : 6 : g_task_set_priority (task, io_priority);
298 [ + - ]: 6 : g_task_set_source_tag (task, valent_mpris_adapter_init_async);
299 : :
300 : 6 : self->connection = g_bus_get_sync (G_BUS_TYPE_SESSION,
301 : : destroy,
302 : : &error);
303 : :
304 [ - + ]: 6 : if (self->connection == NULL)
305 : : {
306 : 0 : valent_extension_plugin_state_changed (VALENT_EXTENSION (self),
307 : : VALENT_PLUGIN_STATE_ERROR,
308 : : error);
309 [ # # ]: 0 : return g_task_return_error (task, g_steal_pointer (&error));
310 : : }
311 : :
312 [ - + ]: 6 : g_dbus_connection_call (self->connection,
313 : : "org.freedesktop.DBus",
314 : : "/org/freedesktop/DBus",
315 : : "org.freedesktop.DBus",
316 : : "ListNames",
317 : : NULL,
318 : : NULL,
319 : : G_DBUS_CALL_FLAGS_NONE,
320 : : -1,
321 : : destroy,
322 : : (GAsyncReadyCallback)list_names_cb,
323 : : g_steal_pointer (&task));
324 : : }
325 : :
326 : : static void
327 : 2 : g_async_initable_iface_init (GAsyncInitableIface *iface)
328 : : {
329 : 2 : iface->init_async = valent_mpris_adapter_init_async;
330 : 2 : }
331 : :
332 : : /*
333 : : * ValentMediaAdapter
334 : : */
335 : : static void
336 : 1 : valent_mpris_impl_export_full_cb (ValentMPRISImpl *impl,
337 : : GAsyncResult *result,
338 : : ValentMPRISAdapter *self)
339 : : {
340 : 2 : g_autoptr (GError) error = NULL;
341 : :
342 [ - + - - ]: 1 : if (!valent_mpris_impl_export_finish (impl, result, &error) &&
343 : 0 : !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
344 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
345 : 1 : }
346 : :
347 : : static void
348 : 1 : valent_mpris_adapter_export (ValentMediaAdapter *adapter,
349 : : ValentMediaPlayer *player)
350 : : {
351 : 1 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (adapter);
352 : 2 : g_autoptr (ValentMPRISImpl) impl = NULL;
353 : :
354 [ + - ]: 1 : if (g_hash_table_contains (self->exports, player))
355 : : return;
356 : :
357 : 1 : impl = valent_mpris_impl_new (player);
358 : 1 : g_hash_table_replace (self->exports, player, g_object_ref (impl));
359 : :
360 : : /* If running in a sandbox, assume only one D-Bus name may be owned and watch
361 : : * the player to see if it should be prioritized for export */
362 [ - + ]: 1 : if (xdp_portal_running_under_sandbox ())
363 : : {
364 : 0 : g_signal_connect_object (player,
365 : : "notify::state",
366 : : G_CALLBACK (on_player_state_changed),
367 : : self,
368 : : G_CONNECT_SWAPPED);
369 : 0 : on_player_state_changed (self, NULL, player);
370 : : }
371 : : else
372 : : {
373 [ + - ]: 1 : g_autoptr (GCancellable) destroy = NULL;
374 [ + - ]: 1 : g_autofree char *bus_name = NULL;
375 : :
376 : : /* Cancel export if the object is destroyed */
377 : 1 : destroy = valent_object_ref_cancellable (VALENT_OBJECT (adapter));
378 : :
379 : 1 : bus_name = g_strdup_printf ("%s.Player%u",
380 : : VALENT_MPRIS_DBUS_NAME,
381 : : n_exports++);
382 : 1 : valent_mpris_impl_export_full (impl,
383 : : bus_name,
384 : : destroy,
385 : : (GAsyncReadyCallback)valent_mpris_impl_export_full_cb,
386 : : self);
387 : : }
388 : : }
389 : :
390 : : static void
391 : 1 : valent_mpris_adapter_unexport (ValentMediaAdapter *adapter,
392 : : ValentMediaPlayer *player)
393 : : {
394 : 1 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (adapter);
395 : 1 : g_autoptr (ValentMPRISImpl) impl = NULL;
396 : :
397 [ + - ]: 1 : g_assert (VALENT_IS_MPRIS_ADAPTER (self));
398 [ - + ]: 1 : g_assert (VALENT_IS_MEDIA_PLAYER (player));
399 : :
400 [ - + ]: 1 : if (!g_hash_table_steal_extended (self->exports, player, NULL, (void **)&impl))
401 [ # # ]: 0 : return;
402 : :
403 : : /* If running in a sandbox, stop watching the player and ensure export
404 : : * priority is relinquished */
405 [ - + ]: 1 : if (xdp_portal_running_under_sandbox ())
406 : : {
407 : 0 : g_signal_handlers_disconnect_by_data (impl, self);
408 : :
409 [ # # ]: 0 : if (self->active_export == impl)
410 : : {
411 [ # # ]: 0 : g_clear_pointer (&self->active_export, valent_mpris_impl_unexport);
412 : 0 : on_player_state_changed (self, NULL, NULL);
413 : : }
414 : : }
415 : : else
416 : : {
417 : 1 : g_signal_handlers_disconnect_by_data (impl, self);
418 : 1 : valent_mpris_impl_unexport (impl);
419 : : }
420 : : }
421 : :
422 : : /*
423 : : * ValentObject
424 : : */
425 : : static void
426 : 5 : valent_mpris_adapter_destroy (ValentObject *object)
427 : : {
428 : 5 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (object);
429 : 5 : GHashTableIter iter;
430 : 5 : ValentMPRISImpl *impl;
431 : :
432 [ + - ]: 5 : if (self->name_owner_changed_id > 0)
433 : : {
434 : 5 : g_dbus_connection_signal_unsubscribe (self->connection,
435 : : self->name_owner_changed_id);
436 : 5 : self->name_owner_changed_id = 0;
437 : : }
438 : :
439 : 5 : g_hash_table_iter_init (&iter, self->exports);
440 : :
441 [ - + ]: 5 : while (g_hash_table_iter_next (&iter, NULL, (void **)&impl))
442 : : {
443 : 0 : g_signal_handlers_disconnect_by_data (impl, self);
444 : 0 : valent_mpris_impl_unexport (impl);
445 : 0 : g_hash_table_iter_remove (&iter);
446 : : }
447 : :
448 : 5 : VALENT_OBJECT_CLASS (valent_mpris_adapter_parent_class)->destroy (object);
449 : 5 : }
450 : :
451 : : /*
452 : : * GObject
453 : : */
454 : : static void
455 : 5 : valent_mpris_adapter_finalize (GObject *object)
456 : : {
457 : 5 : ValentMPRISAdapter *self = VALENT_MPRIS_ADAPTER (object);
458 : :
459 [ + - ]: 5 : g_clear_object (&self->connection);
460 [ + - ]: 5 : g_clear_pointer (&self->players, g_hash_table_unref);
461 [ + - ]: 5 : g_clear_pointer (&self->exports, g_hash_table_unref);
462 : :
463 : 5 : G_OBJECT_CLASS (valent_mpris_adapter_parent_class)->finalize (object);
464 : 5 : }
465 : :
466 : : static void
467 : 2 : valent_mpris_adapter_class_init (ValentMPRISAdapterClass *klass)
468 : : {
469 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
470 : 2 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
471 : 2 : ValentMediaAdapterClass *adapter_class = VALENT_MEDIA_ADAPTER_CLASS (klass);
472 : :
473 : 2 : object_class->finalize = valent_mpris_adapter_finalize;
474 : :
475 : 2 : vobject_class->destroy = valent_mpris_adapter_destroy;
476 : :
477 : 2 : adapter_class->export_player = valent_mpris_adapter_export;
478 : 2 : adapter_class->unexport_player = valent_mpris_adapter_unexport;
479 : : }
480 : :
481 : : static void
482 : 6 : valent_mpris_adapter_init (ValentMPRISAdapter *self)
483 : : {
484 : 6 : self->exports = g_hash_table_new_full (NULL, NULL,
485 : : NULL, g_object_unref);
486 : 6 : self->players = g_hash_table_new_full (g_str_hash, g_str_equal,
487 : : g_free, g_object_unref);
488 : 6 : }
489 : :
|