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-systemvolume-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <json-glib/json-glib.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-systemvolume-device.h"
13 : :
14 : : #include "valent-systemvolume-plugin.h"
15 : :
16 : :
17 : : struct _ValentSystemvolumePlugin
18 : : {
19 : : ValentDevicePlugin parent_instance;
20 : :
21 : : ValentMixerAdapter *mixer;
22 : : unsigned int mixer_watch : 1;
23 : : GPtrArray *states;
24 : :
25 : : ValentMixerAdapter *adapter;
26 : : };
27 : :
28 : : static void valent_systemvolume_plugin_handle_request (ValentSystemvolumePlugin *self,
29 : : JsonNode *packet);
30 : : static void valent_systemvolume_plugin_handle_sink_change (ValentSystemvolumePlugin *self,
31 : : JsonNode *packet);
32 : : static void valent_systemvolume_plugin_send_sinklist (ValentSystemvolumePlugin *self);
33 : :
34 [ + + + - ]: 64 : G_DEFINE_FINAL_TYPE (ValentSystemvolumePlugin, valent_systemvolume_plugin, VALENT_TYPE_DEVICE_PLUGIN)
35 : :
36 : :
37 : : /*
38 : : * Local Mixer
39 : : */
40 : : typedef struct
41 : : {
42 : : ValentMixerStream *stream;
43 : : unsigned long notify_id;
44 : : char *name;
45 : : char *description;
46 : : unsigned int volume;
47 : : unsigned int muted : 1;
48 : : unsigned int enabled : 1;
49 : : } StreamState;
50 : :
51 : : static StreamState *
52 : 9 : stream_state_find (ValentSystemvolumePlugin *self,
53 : : const char *name)
54 : : {
55 [ + + ]: 11 : for (unsigned int i = 0; i < self->states->len; i++)
56 : : {
57 : 10 : StreamState *state = g_ptr_array_index (self->states, i);
58 : :
59 [ + + ]: 10 : if (g_strcmp0 (state->name, name) == 0)
60 : : return state;
61 : : }
62 : :
63 : : return NULL;
64 : : }
65 : :
66 : : static void
67 : 4 : on_stream_changed (ValentMixerStream *stream,
68 : : GParamSpec *pspec,
69 : : ValentSystemvolumePlugin *self)
70 : : {
71 : 4 : StreamState *state;
72 : 4 : const char *name;
73 : 4 : const char *description;
74 : 4 : gboolean enabled;
75 : 4 : gboolean muted;
76 : 4 : unsigned int volume;
77 : 4 : g_autoptr (JsonBuilder) builder = NULL;
78 [ - + - - ]: 4 : g_autoptr (JsonNode) packet = NULL;
79 : :
80 [ - + ]: 4 : g_assert (VALENT_IS_MIXER_STREAM (stream));
81 [ + - ]: 4 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
82 : :
83 : : /* If this is an unknown stream, send a new sink list */
84 : 4 : name = valent_mixer_stream_get_name (stream);
85 : :
86 [ - + ]: 4 : if ((state = stream_state_find (self, name)) == NULL)
87 : : {
88 : 0 : valent_systemvolume_plugin_send_sinklist (self);
89 : 0 : return;
90 : : }
91 : :
92 : : /* If the description changed it's probably because the port changed, so
93 : : * update the state and send the whole list */
94 : 4 : description = valent_mixer_stream_get_description (stream);
95 : :
96 [ - + ]: 4 : if (g_set_str (&state->description, description))
97 : : {
98 : 0 : valent_systemvolume_plugin_send_sinklist (self);
99 : 0 : return;
100 : : }
101 : :
102 : : /* If none of the other properties changed, there's nothing to update */
103 : 4 : enabled = valent_mixer_adapter_get_default_output (self->mixer) == stream;
104 : 4 : muted = valent_mixer_stream_get_muted (stream);
105 : 4 : volume = valent_mixer_stream_get_level (stream);
106 : :
107 [ + - ]: 4 : if (state->enabled == enabled &&
108 [ + + ]: 4 : state->muted == muted &&
109 [ + - ]: 2 : state->volume == volume)
110 : : return;
111 : :
112 : : /* Sink update */
113 : 4 : valent_packet_init (&builder, "kdeconnect.systemvolume");
114 : 4 : json_builder_set_member_name (builder, "name");
115 : 4 : json_builder_add_string_value (builder, state->name);
116 : :
117 [ + + ]: 4 : if (state->muted != muted)
118 : : {
119 : 2 : state->muted = muted;
120 : 2 : json_builder_set_member_name (builder, "muted");
121 : 2 : json_builder_add_boolean_value (builder, state->muted);
122 : : }
123 : :
124 [ + + ]: 4 : if (state->volume != volume)
125 : : {
126 : 2 : state->volume = volume;
127 : 2 : json_builder_set_member_name (builder, "volume");
128 : 2 : json_builder_add_int_value (builder, state->volume);
129 : : }
130 : :
131 [ - + ]: 4 : if (state->enabled != enabled)
132 : : {
133 : 0 : state->enabled = enabled;
134 : 0 : json_builder_set_member_name (builder, "enabled");
135 : 0 : json_builder_add_boolean_value (builder, state->enabled);
136 : : }
137 : :
138 : 4 : packet = valent_packet_end (&builder);
139 : :
140 [ + - ]: 4 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
141 : : }
142 : :
143 : : static StreamState *
144 : 8 : stream_state_new (ValentSystemvolumePlugin *self,
145 : : ValentMixerStream *stream)
146 : : {
147 : 8 : StreamState *state;
148 : :
149 [ - + ]: 8 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
150 [ + - ]: 8 : g_assert (VALENT_IS_MIXER_STREAM (stream));
151 : :
152 : 8 : state = g_new0 (StreamState, 1);
153 : 8 : state->stream = g_object_ref (stream);
154 : 8 : g_signal_connect_object (state->stream,
155 : : "notify",
156 : : G_CALLBACK (on_stream_changed),
157 : : self,
158 : : G_CONNECT_DEFAULT);
159 : :
160 [ - + ]: 8 : state->name = g_strdup (valent_mixer_stream_get_name (stream));
161 [ - + ]: 8 : state->description = g_strdup (valent_mixer_stream_get_description (stream));
162 : 8 : state->volume = valent_mixer_stream_get_level (stream);
163 : 8 : state->muted = valent_mixer_stream_get_muted (stream);
164 : 8 : state->enabled = valent_mixer_adapter_get_default_output (self->mixer) == stream;
165 : :
166 : 8 : return state;
167 : : }
168 : :
169 : : static void
170 : 8 : stream_state_free (gpointer data)
171 : : {
172 : 8 : StreamState *state = data;
173 : :
174 [ - + ]: 8 : g_clear_signal_handler (&state->notify_id, state->stream);
175 [ + - ]: 8 : g_clear_object (&state->stream);
176 [ + - ]: 8 : g_clear_pointer (&state->name, g_free);
177 [ + - ]: 8 : g_clear_pointer (&state->description, g_free);
178 : 8 : g_clear_pointer (&state, g_free);
179 : 8 : }
180 : :
181 : : static void
182 : 0 : on_default_output_changed (ValentMixerAdapter *adapter,
183 : : GParamSpec *pspec,
184 : : ValentSystemvolumePlugin *self)
185 : : {
186 : 0 : ValentMixerStream *default_output = NULL;
187 : :
188 [ # # ]: 0 : g_assert (VALENT_IS_MIXER_ADAPTER (adapter));
189 [ # # ]: 0 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
190 : :
191 : 0 : default_output = valent_mixer_adapter_get_default_output (adapter);
192 : :
193 [ # # ]: 0 : for (unsigned int i = 0; i < self->states->len; i++)
194 : : {
195 : 0 : StreamState *state = g_ptr_array_index (self->states, i);
196 : :
197 : 0 : state->enabled = state->stream == default_output;
198 : : }
199 : :
200 : : /* It's unclear whether the `enabled` field with a value of `false` is
201 : : * relevant in the protocol, we resend the whole list */
202 : 0 : valent_systemvolume_plugin_send_sinklist (self);
203 : 0 : }
204 : :
205 : : static void
206 : 6 : on_items_changed (GListModel *list,
207 : : unsigned int position,
208 : : unsigned int removed,
209 : : unsigned int added,
210 : : ValentSystemvolumePlugin *self)
211 : : {
212 : 6 : unsigned int n_streams = 0;
213 : :
214 [ - + ]: 6 : g_assert (G_IS_LIST_MODEL (list));
215 [ + - ]: 6 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
216 : :
217 : 6 : g_ptr_array_remove_range (self->states, 0, self->states->len);
218 : 6 : n_streams = g_list_model_get_n_items (list);
219 : :
220 [ + + ]: 14 : for (unsigned int i = 0; i < n_streams; i++)
221 : : {
222 : 8 : g_autoptr (ValentMixerStream) stream = NULL;
223 : :
224 : 8 : stream = g_list_model_get_item (list, i);
225 : :
226 [ - + ]: 8 : if (valent_mixer_stream_get_direction (stream) != VALENT_MIXER_OUTPUT)
227 [ # # ]: 0 : continue;
228 : :
229 [ + - ]: 8 : g_ptr_array_add (self->states, stream_state_new (self, stream));
230 : : }
231 : :
232 : 6 : valent_systemvolume_plugin_send_sinklist (self);
233 : 6 : }
234 : :
235 : : static void
236 : 4 : on_primary_adapter_changed (ValentMixer *mixer,
237 : : GParamSpec *pspec,
238 : : ValentSystemvolumePlugin *self)
239 : : {
240 [ + + ]: 4 : if (self->mixer != NULL)
241 : : {
242 : 2 : g_signal_handlers_disconnect_by_data (self->mixer, self);
243 : 2 : g_ptr_array_remove_range (self->states, 0, self->states->len);
244 [ + - ]: 2 : g_clear_object (&self->mixer);
245 : : }
246 : :
247 : 4 : g_object_get (mixer, "primary-adapter", &self->mixer, NULL);
248 [ + - ]: 4 : if (self->mixer != NULL)
249 : : {
250 : 4 : g_signal_connect_object (self->mixer,
251 : : "notify::default-output",
252 : : G_CALLBACK (on_default_output_changed),
253 : : self,
254 : : G_CONNECT_DEFAULT);
255 : 4 : g_signal_connect_object (self->mixer,
256 : : "items-changed",
257 : : G_CALLBACK (on_items_changed),
258 : : self,
259 : : G_CONNECT_DEFAULT);
260 : 4 : on_items_changed (G_LIST_MODEL (self->mixer),
261 : : 0,
262 : : 0,
263 : 4 : g_list_model_get_n_items (G_LIST_MODEL (self->mixer)),
264 : : self);
265 : : }
266 : 4 : }
267 : :
268 : : static void
269 : 8 : valent_systemvolume_plugin_watch_mixer (ValentSystemvolumePlugin *self,
270 : : gboolean watch)
271 : : {
272 : 8 : ValentMixer *mixer = valent_mixer_get_default ();
273 : :
274 [ - + ]: 8 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
275 : :
276 [ + + ]: 8 : if (self->mixer_watch == watch)
277 : : return;
278 : :
279 [ + + ]: 4 : if (watch)
280 : : {
281 : 2 : g_signal_connect_object (mixer,
282 : : "notify::default-output",
283 : : G_CALLBACK (on_primary_adapter_changed),
284 : : self,
285 : : G_CONNECT_DEFAULT);
286 : 2 : on_primary_adapter_changed (mixer, NULL, self);
287 : :
288 [ + - ]: 2 : if (self->adapter == NULL)
289 : : {
290 : 2 : ValentDevice *device = NULL;
291 : :
292 : 2 : device = valent_object_get_parent (VALENT_OBJECT (self));
293 : 2 : self->adapter = valent_systemvolume_device_new (device);
294 : 2 : valent_component_export_adapter (VALENT_COMPONENT (mixer),
295 : : VALENT_EXTENSION (self->adapter));
296 : : }
297 : : }
298 : : else
299 : : {
300 : 2 : g_signal_handlers_disconnect_by_func (mixer, self, on_primary_adapter_changed);
301 : :
302 [ + - ]: 2 : if (self->mixer != NULL)
303 : : {
304 : 2 : g_signal_handlers_disconnect_by_data (self->mixer, self);
305 : 2 : g_ptr_array_remove_range (self->states, 0, self->states->len);
306 [ + - ]: 2 : g_clear_object (&self->mixer);
307 : : }
308 : :
309 [ + - ]: 2 : if (self->adapter != NULL)
310 : : {
311 : 2 : valent_component_unexport_adapter (VALENT_COMPONENT (mixer),
312 : : VALENT_EXTENSION (self->adapter));
313 [ + - ]: 2 : g_clear_object (&self->adapter);
314 : : }
315 : : }
316 : :
317 : 4 : self->mixer_watch = watch;
318 : : }
319 : :
320 : : /*
321 : : * Packet Providers
322 : : */
323 : : static void
324 : 8 : valent_systemvolume_plugin_send_sinklist (ValentSystemvolumePlugin *self)
325 : : {
326 : 16 : g_autoptr (JsonBuilder) builder = NULL;
327 [ - + ]: 8 : g_autoptr (JsonNode) packet = NULL;
328 : 8 : unsigned int max_volume = 100;
329 : :
330 [ - + ]: 8 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
331 : :
332 : : /* Sink List */
333 : 8 : valent_packet_init (&builder, "kdeconnect.systemvolume");
334 : 8 : json_builder_set_member_name (builder, "sinkList");
335 : 8 : json_builder_begin_array (builder);
336 : :
337 [ + + ]: 18 : for (unsigned int i = 0; i < self->states->len; i++)
338 : : {
339 : 10 : StreamState *state;
340 : :
341 : 10 : state = g_ptr_array_index (self->states, i);
342 : :
343 : 10 : json_builder_begin_object (builder);
344 : 10 : json_builder_set_member_name (builder, "name");
345 : 10 : json_builder_add_string_value (builder, state->name);
346 : 10 : json_builder_set_member_name (builder, "description");
347 : 10 : json_builder_add_string_value (builder, state->description);
348 : 10 : json_builder_set_member_name (builder, "muted");
349 : 10 : json_builder_add_boolean_value (builder, state->muted);
350 : 10 : json_builder_set_member_name (builder, "volume");
351 : 10 : json_builder_add_int_value (builder, state->volume);
352 : 10 : json_builder_set_member_name (builder, "maxVolume");
353 : 10 : json_builder_add_int_value (builder, max_volume);
354 : 10 : json_builder_set_member_name (builder, "enabled");
355 : 10 : json_builder_add_boolean_value (builder, state->enabled);
356 : 10 : json_builder_end_object (builder);
357 : : }
358 : :
359 : 8 : json_builder_end_array (builder);
360 : 8 : packet = valent_packet_end (&builder);
361 : :
362 [ + - ]: 8 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
363 : 8 : }
364 : :
365 : : static void
366 : 5 : valent_systemvolume_plugin_handle_sink_change (ValentSystemvolumePlugin *self,
367 : : JsonNode *packet)
368 : : {
369 : 5 : StreamState *state;
370 : 5 : const char *name;
371 : 5 : int64_t volume;
372 : 5 : gboolean muted;
373 : 5 : gboolean enabled;
374 : :
375 [ - + ]: 5 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
376 [ + - ]: 5 : g_assert (VALENT_IS_PACKET (packet));
377 : :
378 [ - + ]: 5 : if (!valent_packet_get_string (packet, "name", &name))
379 : : {
380 : 0 : g_debug ("%s(): expected \"name\" field holding a string",
381 : : G_STRFUNC);
382 : 1 : return;
383 : : }
384 : :
385 [ + + ]: 5 : if ((state = stream_state_find (self, name)) == NULL)
386 : : {
387 : 1 : valent_systemvolume_plugin_send_sinklist (self);
388 : 1 : return;
389 : : }
390 : :
391 [ + + + - ]: 4 : if (valent_packet_get_int (packet, "volume", &volume) && volume >= 0)
392 : 1 : valent_mixer_stream_set_level (state->stream, volume);
393 : :
394 [ + + ]: 4 : if (valent_packet_get_boolean (packet, "muted", &muted))
395 : 1 : valent_mixer_stream_set_muted (state->stream, muted);
396 : :
397 [ + + + - ]: 4 : if (valent_packet_get_boolean (packet, "enabled", &enabled) && enabled)
398 : 2 : valent_mixer_adapter_set_default_output (self->mixer, state->stream);
399 : : }
400 : :
401 : : static void
402 : 6 : valent_systemvolume_plugin_handle_request (ValentSystemvolumePlugin *self,
403 : : JsonNode *packet)
404 : : {
405 [ - + ]: 6 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
406 : :
407 : : /* A request to change an audio output
408 : : */
409 [ + + ]: 6 : if (valent_packet_check_field (packet, "name"))
410 : 5 : valent_systemvolume_plugin_handle_sink_change (self, packet);
411 : :
412 : : /* A request for a list of audio outputs (deprecated)
413 : : */
414 [ + - ]: 1 : else if (valent_packet_check_field (packet, "requestSinks"))
415 : 1 : valent_systemvolume_plugin_send_sinklist (self);
416 : :
417 : : else
418 : 0 : g_warn_if_reached ();
419 : 6 : }
420 : :
421 : : /*
422 : : * ValentDevicePlugin
423 : : */
424 : : static void
425 : 4 : valent_systemvolume_plugin_update_state (ValentDevicePlugin *plugin,
426 : : ValentDeviceState state)
427 : : {
428 : 4 : ValentSystemvolumePlugin *self = VALENT_SYSTEMVOLUME_PLUGIN (plugin);
429 : 4 : gboolean available;
430 : :
431 [ - + ]: 4 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
432 : :
433 [ + + ]: 4 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
434 [ - + ]: 2 : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
435 : :
436 : : /* Watch stream changes */
437 : 4 : if (available)
438 : 2 : valent_systemvolume_plugin_watch_mixer (self, TRUE);
439 : : else
440 : 2 : valent_systemvolume_plugin_watch_mixer (self, FALSE);
441 : 4 : }
442 : :
443 : : static void
444 : 12 : valent_systemvolume_plugin_handle_packet (ValentDevicePlugin *plugin,
445 : : const char *type,
446 : : JsonNode *packet)
447 : : {
448 : 12 : ValentSystemvolumePlugin *self = VALENT_SYSTEMVOLUME_PLUGIN (plugin);
449 : :
450 [ - + ]: 12 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
451 [ + - ]: 12 : g_assert (type != NULL);
452 [ + - ]: 12 : g_assert (VALENT_IS_PACKET (packet));
453 : :
454 [ + + ]: 12 : if (g_str_equal (type, "kdeconnect.systemvolume"))
455 : 6 : valent_systemvolume_device_handle_packet (VALENT_SYSTEMVOLUME_DEVICE (self->adapter),
456 : : packet);
457 [ + - ]: 6 : else if (g_str_equal (type, "kdeconnect.systemvolume.request"))
458 : 6 : valent_systemvolume_plugin_handle_request (self, packet);
459 : : else
460 : 0 : g_assert_not_reached ();
461 : 12 : }
462 : :
463 : : /*
464 : : * ValentObject
465 : : */
466 : : static void
467 : 4 : valent_systemvolume_plugin_destroy (ValentObject *object)
468 : : {
469 : 4 : ValentSystemvolumePlugin *self = VALENT_SYSTEMVOLUME_PLUGIN (object);
470 : :
471 : 4 : valent_systemvolume_plugin_watch_mixer (self, FALSE);
472 [ + + ]: 4 : g_clear_pointer (&self->states, g_ptr_array_unref);
473 [ - + ]: 4 : g_clear_object (&self->mixer);
474 : :
475 : 4 : VALENT_OBJECT_CLASS (valent_systemvolume_plugin_parent_class)->destroy (object);
476 : 4 : }
477 : :
478 : : /*
479 : : * GObject
480 : : */
481 : : static void
482 : 1 : valent_systemvolume_plugin_class_init (ValentSystemvolumePluginClass *klass)
483 : : {
484 : 1 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
485 : 1 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
486 : :
487 : 1 : vobject_class->destroy = valent_systemvolume_plugin_destroy;
488 : :
489 : 1 : plugin_class->handle_packet = valent_systemvolume_plugin_handle_packet;
490 : 1 : plugin_class->update_state = valent_systemvolume_plugin_update_state;
491 : : }
492 : :
493 : : static void
494 : 2 : valent_systemvolume_plugin_init (ValentSystemvolumePlugin *self)
495 : : {
496 : 2 : self->states = g_ptr_array_new_with_free_func (stream_state_free);
497 : 2 : }
498 : :
|