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-telephony-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <glib/gi18n.h>
9 : : #include <gio/gio.h>
10 : : #include <json-glib/json-glib.h>
11 : : #include <valent.h>
12 : :
13 : : #include "valent-telephony-plugin.h"
14 : :
15 : : /*
16 : : * MediaState Helpers
17 : : */
18 : : typedef struct
19 : : {
20 : : ValentMixerStream *stream;
21 : : unsigned int current_level;
22 : : unsigned int current_muted : 1;
23 : : unsigned int original_level;
24 : : unsigned int original_muted : 1;
25 : : } StreamState;
26 : :
27 : : static void
28 : 1 : on_stream_changed (StreamState *state)
29 : : {
30 : 1 : g_signal_handlers_disconnect_by_data (valent_mixer_get_default (), state);
31 [ + - ]: 1 : g_clear_object (&state->stream);
32 : 1 : }
33 : :
34 : : static StreamState *
35 : 8 : stream_state_new (ValentMixerStream *stream)
36 : : {
37 : 8 : StreamState *state;
38 : 8 : ValentMixerDirection direction;
39 : :
40 : 8 : state = g_new0 (StreamState, 1);
41 : 8 : state->stream = g_object_ref (stream);
42 : 8 : state->original_level = valent_mixer_stream_get_level (stream);
43 : 8 : state->original_muted = valent_mixer_stream_get_muted (stream);
44 : 8 : state->current_level = state->original_level;
45 : 8 : state->current_muted = state->original_muted;
46 : :
47 : 8 : direction = valent_mixer_stream_get_direction (stream);
48 [ + + ]: 13 : g_signal_connect_data (valent_mixer_get_default (),
49 : : direction == VALENT_MIXER_INPUT
50 : : ? "notify::default-input"
51 : : : "notify::default-output",
52 : : G_CALLBACK (on_stream_changed),
53 : : state, NULL,
54 : : G_CONNECT_SWAPPED);
55 : :
56 : 8 : return state;
57 : : }
58 : :
59 : : static void
60 : 8 : stream_state_free (gpointer data)
61 : : {
62 : 8 : StreamState *state = data;
63 : :
64 [ + + ]: 8 : if (state->stream != NULL)
65 : : {
66 : 7 : g_signal_handlers_disconnect_by_data (valent_mixer_get_default (), state);
67 [ + - ]: 7 : g_clear_object (&state->stream);
68 : : }
69 : 8 : g_free (state);
70 : 8 : }
71 : :
72 : : static inline void
73 : 8 : stream_state_restore (gpointer data)
74 : : {
75 : 8 : StreamState *state = data;
76 : :
77 [ + + ]: 8 : if (state->stream != NULL)
78 : : {
79 [ + - ]: 7 : if (valent_mixer_stream_get_level (state->stream) == state->current_level)
80 : 7 : valent_mixer_stream_set_level (state->stream, state->original_level);
81 : :
82 [ + - ]: 7 : if (valent_mixer_stream_get_muted (state->stream) == state->current_muted)
83 : 7 : valent_mixer_stream_set_muted (state->stream, state->original_muted);
84 : : }
85 : 8 : stream_state_free (state);
86 : 8 : }
87 : :
88 : : static void
89 : 11 : stream_state_update (StreamState *state,
90 : : int level)
91 : : {
92 [ + + ]: 11 : if (state->stream == NULL)
93 : : return;
94 : :
95 [ + + ]: 10 : if (level == 0)
96 : : {
97 : 5 : state->current_muted = TRUE;
98 : 5 : valent_mixer_stream_set_muted (state->stream, TRUE);
99 : : }
100 [ + - ]: 5 : else if (level > 0)
101 : : {
102 : 5 : state->current_level = level;
103 : 5 : valent_mixer_stream_set_level (state->stream, level);
104 : : }
105 : : }
106 : :
107 : : typedef struct
108 : : {
109 : : GPtrArray *players;
110 : : StreamState *speakers;
111 : : StreamState *microphone;
112 : : } MediaState;
113 : :
114 : : static void
115 : 3 : on_player_changed (ValentMediaPlayer *player,
116 : : GParamSpec *pspec,
117 : : GPtrArray *players)
118 : : {
119 : : /* The paused state may be deferred, but any other state stops tracking */
120 [ + - ]: 3 : if (valent_media_player_get_state (player) != VALENT_MEDIA_STATE_PAUSED)
121 : : {
122 : 3 : g_signal_handlers_disconnect_by_data (player, players);
123 : 3 : g_ptr_array_remove (players, player);
124 : : }
125 : 3 : }
126 : :
127 : : static MediaState *
128 : 5 : media_state_new (void)
129 : : {
130 : 5 : MediaState *state;
131 : :
132 : 5 : state = g_new0 (MediaState, 1);
133 : 5 : state->players = g_ptr_array_new ();
134 : :
135 : 5 : return state;
136 : : }
137 : :
138 : : static inline void
139 : 0 : media_state_free (gpointer data)
140 : : {
141 : 0 : MediaState *state = data;
142 : :
143 : 0 : g_signal_handlers_disconnect_by_data (valent_mixer_get_default (), state);
144 [ # # ]: 0 : g_clear_pointer (&state->players, g_ptr_array_unref);
145 [ # # ]: 0 : g_clear_pointer (&state->microphone, g_free);
146 [ # # ]: 0 : g_clear_pointer (&state->speakers, g_free);
147 : 0 : g_free (state);
148 : 0 : }
149 : :
150 : : static inline void
151 : 5 : media_state_restore (gpointer data)
152 : : {
153 : 5 : MediaState *state = data;
154 : :
155 : 5 : g_ptr_array_foreach (state->players, (void *)valent_media_player_play, NULL);
156 [ + - ]: 5 : g_clear_pointer (&state->players, g_ptr_array_unref);
157 [ + - ]: 5 : g_clear_pointer (&state->speakers, stream_state_restore);
158 [ + + ]: 5 : g_clear_pointer (&state->microphone, stream_state_restore);
159 : 5 : g_free (state);
160 : 5 : }
161 : :
162 : : static void
163 : 3 : media_state_pause_players (MediaState *state)
164 : : {
165 : 3 : ValentMedia *media = valent_media_get_default ();
166 : 3 : unsigned int n_adapters = 0;
167 : :
168 : 3 : n_adapters = g_list_model_get_n_items (G_LIST_MODEL (media));
169 [ + + ]: 6 : for (unsigned int i = 0; i < n_adapters; i++)
170 : : {
171 : 3 : g_autoptr (ValentMediaAdapter) adapter = NULL;
172 : 3 : unsigned int n_players = 0;
173 : :
174 : 3 : adapter = g_list_model_get_item (G_LIST_MODEL (media), i);
175 : 3 : n_players = g_list_model_get_n_items (G_LIST_MODEL (adapter));
176 [ + + ]: 9 : for (unsigned int j = 0; j < n_players; j++)
177 : : {
178 : 6 : g_autoptr (ValentMediaPlayer) player = NULL;
179 : :
180 : 6 : player = g_list_model_get_item (G_LIST_MODEL (adapter), j);
181 : :
182 : : /* Skip players already being tracked */
183 [ - + ]: 6 : if (g_ptr_array_find (state->players, player, NULL))
184 : 0 : continue;
185 : :
186 [ + + ]: 6 : if (valent_media_player_get_state (player) != VALENT_MEDIA_STATE_PLAYING)
187 : 3 : continue;
188 : :
189 : 3 : valent_media_player_pause (player);
190 : 3 : g_ptr_array_add (state->players, player);
191 : :
192 : : /* Stop tracking a player if its state changes or it's destroyed */
193 : 3 : g_signal_connect_data (player,
194 : : "notify::state",
195 : : G_CALLBACK (on_player_changed),
196 : 3 : g_ptr_array_ref (state->players),
197 : : (void *)g_ptr_array_unref,
198 : : G_CONNECT_DEFAULT);
199 [ + - ]: 3 : g_signal_connect_data (player,
200 : : "destroy",
201 : : G_CALLBACK (g_ptr_array_remove),
202 : 3 : g_ptr_array_ref (state->players),
203 : : (void *)g_ptr_array_unref,
204 : : G_CONNECT_SWAPPED);
205 : : }
206 : : }
207 : 3 : }
208 : :
209 : : static void
210 : 8 : media_state_update (MediaState *state,
211 : : int output_level,
212 : : int input_level,
213 : : gboolean pause)
214 : : {
215 : 8 : ValentMixer *mixer = valent_mixer_get_default ();
216 : 8 : ValentMixerStream *stream = NULL;
217 : :
218 : 8 : stream = valent_mixer_get_default_output (mixer);
219 [ + - ]: 8 : if (stream != NULL && output_level >= 0)
220 : : {
221 [ + + ]: 8 : if (state->speakers == NULL)
222 : 5 : state->speakers = stream_state_new (stream);
223 : 8 : stream_state_update (state->speakers, output_level);
224 : : }
225 : :
226 : 8 : stream = valent_mixer_get_default_input (mixer);
227 [ + + ]: 8 : if (stream != NULL && input_level >= 0)
228 : : {
229 [ + - ]: 3 : if (state->microphone == NULL)
230 : 3 : state->microphone = stream_state_new (stream);
231 : 3 : stream_state_update (state->microphone, input_level);
232 : : }
233 : :
234 [ + + ]: 8 : if (pause)
235 : 3 : media_state_pause_players (state);
236 : 8 : }
237 : :
238 : : /*
239 : : * Plugin
240 : : */
241 : : struct _ValentTelephonyPlugin
242 : : {
243 : : ValentDevicePlugin parent_instance;
244 : :
245 : : MediaState *media_state;
246 : : };
247 : :
248 [ + + + - ]: 81 : G_DEFINE_FINAL_TYPE (ValentTelephonyPlugin, valent_telephony_plugin, VALENT_TYPE_DEVICE_PLUGIN)
249 : :
250 : :
251 : : static void
252 : 8 : valent_telephony_plugin_update_media_state (ValentTelephonyPlugin *self,
253 : : const char *event)
254 : : {
255 : 8 : GSettings *settings = NULL;
256 : 8 : int output_level = -1;
257 : 8 : int input_level = -1;
258 : 8 : gboolean pause = FALSE;
259 : :
260 [ - + ]: 8 : g_assert (VALENT_IS_TELEPHONY_PLUGIN (self));
261 [ + - + - ]: 8 : g_assert (event != NULL && *event != '\0');
262 : :
263 : 8 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
264 : :
265 : : /* Retrieve the user preference for this event */
266 [ + + ]: 8 : if (g_str_equal (event, "ringing"))
267 : : {
268 : 5 : output_level = g_settings_get_int (settings, "ringing-volume");
269 : 5 : input_level = g_settings_get_int (settings, "ringing-microphone");
270 : 5 : pause = g_settings_get_boolean (settings, "ringing-pause");
271 : : }
272 [ + - ]: 3 : else if (g_str_equal (event, "talking"))
273 : : {
274 : 3 : output_level = g_settings_get_int (settings, "talking-volume");
275 : 3 : input_level = g_settings_get_int (settings, "talking-microphone");
276 : 3 : pause = g_settings_get_boolean (settings, "talking-pause");
277 : : }
278 : : else
279 : : {
280 : 0 : g_return_if_reached ();
281 : : }
282 : :
283 [ + + ]: 8 : if (self->media_state == NULL)
284 : 5 : self->media_state = media_state_new ();
285 : 8 : media_state_update (self->media_state, output_level, input_level, pause);
286 : : }
287 : :
288 : : static GIcon *
289 : 8 : valent_telephony_plugin_get_event_icon (JsonNode *packet,
290 : : const char *event)
291 : : {
292 : 8 : const char *phone_thumbnail = NULL;
293 : :
294 [ - + ]: 8 : g_assert (VALENT_IS_PACKET (packet));
295 : :
296 [ + - ]: 8 : if (valent_packet_get_string (packet, "phoneThumbnail", &phone_thumbnail))
297 : : {
298 : 8 : g_autofree unsigned char *data = NULL;
299 : 8 : size_t data_len = 0;
300 : :
301 : 8 : data = g_base64_decode (phone_thumbnail, &data_len);
302 [ + - ]: 8 : if (data_len > 0)
303 : : {
304 : 8 : g_autoptr (GBytes) bytes = NULL;
305 : :
306 : 8 : bytes = g_bytes_new_take (g_steal_pointer (&data), data_len);
307 : :
308 [ + - ]: 8 : return g_bytes_icon_new (bytes);
309 : : }
310 : : else
311 : : {
312 : 0 : g_warning ("%s(): Failed to decode thumbnail for \"%s\" event",
313 : : G_STRFUNC,
314 : : event);
315 : : }
316 : : }
317 : :
318 [ # # ]: 0 : if (g_str_equal (event, "ringing"))
319 : 0 : return g_themed_icon_new ("call-incoming-symbolic");
320 : :
321 [ # # ]: 0 : if (g_str_equal (event, "talking"))
322 : 0 : return g_themed_icon_new ("call-start-symbolic");
323 : :
324 [ # # ]: 0 : if (g_str_equal (event, "missedCall"))
325 : 0 : return g_themed_icon_new ("call-missed-symbolic");
326 : :
327 : : return NULL;
328 : : }
329 : :
330 : : static void
331 : 13 : valent_telephony_plugin_handle_telephony (ValentTelephonyPlugin *self,
332 : : JsonNode *packet)
333 : : {
334 : 13 : const char *event;
335 : 13 : const char *sender;
336 : 13 : g_autoptr (GNotification) notification = NULL;
337 [ + - ]: 13 : g_autoptr (GIcon) icon = NULL;
338 : :
339 [ - + ]: 13 : g_assert (VALENT_IS_TELEPHONY_PLUGIN (self));
340 [ + - ]: 13 : g_assert (VALENT_IS_PACKET (packet));
341 : :
342 [ - + ]: 13 : if (!valent_packet_get_string (packet, "event", &event))
343 : : {
344 : 0 : g_debug ("%s(): expected \"event\" field holding a string",
345 : : G_STRFUNC);
346 : 0 : return;
347 : : }
348 : :
349 : : /* Currently, only "ringing" and "talking" events are supported */
350 [ + + + - ]: 13 : if (!g_str_equal (event, "ringing") && !g_str_equal (event, "talking"))
351 : : {
352 : : VALENT_NOTE ("ignoring \"%s\" event", event);
353 : : return;
354 : : }
355 : :
356 : : /* Ensure there is a string representing the sender, so it can be used as the
357 : : * notification ID to handle interleaved events from multiple senders.
358 : : *
359 : : * Because we only support voice events (i.e. `ringing` and `talking`), we
360 : : * can be certain that subsequent events from the same sender supersede
361 : : * previous events, and replace the older notifications.
362 : : */
363 [ - + - - ]: 13 : if (!valent_packet_get_string (packet, "contactName", &sender) &&
364 : 0 : !valent_packet_get_string (packet, "phoneNumber", &sender))
365 : : {
366 : : /* TRANSLATORS: An unknown caller, with no name or phone number */
367 : 0 : sender = C_("contact identity", "Unknown");
368 : : }
369 : :
370 : : /* This is a cancelled event */
371 [ + + ]: 13 : if (valent_packet_check_field (packet, "isCancel"))
372 : : {
373 [ + - ]: 5 : g_clear_pointer (&self->media_state, media_state_restore);
374 : 5 : valent_device_plugin_hide_notification (VALENT_DEVICE_PLUGIN (self),
375 : : sender);
376 : 5 : return;
377 : : }
378 : :
379 : : /* Adjust volume/pause media */
380 : 8 : valent_telephony_plugin_update_media_state (self, event);
381 : :
382 : : /* The notification plugin handles SMS/MMS and missed call notifications,
383 : : * while the telephony plugin must handle incoming and ongoing calls.
384 : : */
385 : 8 : notification = g_notification_new (sender);
386 : 8 : icon = valent_telephony_plugin_get_event_icon (packet, event);
387 : 8 : g_notification_set_icon (notification, icon);
388 : :
389 [ + + ]: 8 : if (g_str_equal (event, "ringing"))
390 : : {
391 : 5 : ValentDevice *device = NULL;
392 : :
393 : : /* TRANSLATORS: The phone is ringing */
394 : 5 : g_notification_set_body (notification, _("Incoming call"));
395 : 5 : device = valent_resource_get_source (VALENT_RESOURCE (self));
396 : 5 : valent_notification_add_device_button (notification,
397 : : device,
398 : 5 : _("Mute"),
399 : : "telephony.mute-call",
400 : : NULL);
401 : 5 : g_notification_set_priority (notification,
402 : : G_NOTIFICATION_PRIORITY_URGENT);
403 : : }
404 [ + - ]: 3 : else if (g_str_equal (event, "talking"))
405 : : {
406 : : /* TRANSLATORS: The phone has been answered */
407 : 3 : g_notification_set_body (notification, _("Ongoing call"));
408 : : }
409 : :
410 [ + - ]: 8 : valent_device_plugin_show_notification (VALENT_DEVICE_PLUGIN (self),
411 : : sender,
412 : : notification);
413 : : }
414 : :
415 : : static void
416 : 1 : valent_telephony_plugin_mute_call (ValentTelephonyPlugin *self)
417 : : {
418 : 2 : g_autoptr (JsonNode) packet = NULL;
419 : :
420 [ - + ]: 1 : g_assert (VALENT_IS_TELEPHONY_PLUGIN (self));
421 : :
422 : 1 : packet = valent_packet_new ("kdeconnect.telephony.request_mute");
423 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
424 : 1 : }
425 : :
426 : : /*
427 : : * GActions
428 : : */
429 : : static void
430 : 1 : mute_call_action (GSimpleAction *action,
431 : : GVariant *parameter,
432 : : gpointer user_data)
433 : : {
434 : 1 : ValentTelephonyPlugin *self = VALENT_TELEPHONY_PLUGIN (user_data);
435 : :
436 [ - + ]: 1 : g_assert (VALENT_IS_TELEPHONY_PLUGIN (self));
437 : :
438 : 1 : valent_telephony_plugin_mute_call (self);
439 : 1 : }
440 : :
441 : : static const GActionEntry actions[] = {
442 : : {"mute-call", mute_call_action, NULL, NULL, NULL}
443 : : };
444 : :
445 : : /*
446 : : * ValentDevicePlugin
447 : : */
448 : : static void
449 : 12 : valent_telephony_plugin_update_state (ValentDevicePlugin *plugin,
450 : : ValentDeviceState state)
451 : : {
452 : 12 : ValentTelephonyPlugin *self = VALENT_TELEPHONY_PLUGIN (plugin);
453 : 12 : gboolean available;
454 : :
455 [ - + ]: 12 : g_assert (VALENT_IS_TELEPHONY_PLUGIN (plugin));
456 : :
457 [ + + ]: 12 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
458 [ - + ]: 6 : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
459 : :
460 : : /* Clear the media state, but don't restore it as there may still be an
461 : : * event in progress. */
462 : 6 : if (!available)
463 [ + - ]: 6 : g_clear_pointer (&self->media_state, media_state_free);
464 : :
465 : 12 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
466 : 12 : }
467 : :
468 : : static void
469 : 13 : valent_telephony_plugin_handle_packet (ValentDevicePlugin *plugin,
470 : : const char *type,
471 : : JsonNode *packet)
472 : : {
473 : 13 : ValentTelephonyPlugin *self = VALENT_TELEPHONY_PLUGIN (plugin);
474 : :
475 [ - + ]: 13 : g_assert (VALENT_IS_TELEPHONY_PLUGIN (plugin));
476 [ + - ]: 13 : g_assert (type != NULL);
477 [ + - ]: 13 : g_assert (VALENT_IS_PACKET (packet));
478 : :
479 [ + - ]: 13 : if (g_str_equal (type, "kdeconnect.telephony"))
480 : 13 : valent_telephony_plugin_handle_telephony (self, packet);
481 : : else
482 : 13 : g_assert_not_reached ();
483 : 13 : }
484 : :
485 : : /*
486 : : * ValentObject
487 : : */
488 : : static void
489 : 12 : valent_telephony_plugin_destroy (ValentObject *object)
490 : : {
491 : 12 : ValentTelephonyPlugin *self = VALENT_TELEPHONY_PLUGIN (object);
492 : :
493 [ - + ]: 12 : g_clear_pointer (&self->media_state, media_state_free);
494 : :
495 : 12 : VALENT_OBJECT_CLASS (valent_telephony_plugin_parent_class)->destroy (object);
496 : 12 : }
497 : :
498 : : /*
499 : : * GObject
500 : : */
501 : : static void
502 : 6 : valent_telephony_plugin_constructed (GObject *object)
503 : : {
504 : 6 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
505 : :
506 : 6 : G_OBJECT_CLASS (valent_telephony_plugin_parent_class)->constructed (object);
507 : :
508 : 6 : g_action_map_add_action_entries (G_ACTION_MAP (plugin),
509 : : actions,
510 : : G_N_ELEMENTS (actions),
511 : : plugin);
512 : 6 : }
513 : :
514 : : static void
515 : 11 : valent_telephony_plugin_class_init (ValentTelephonyPluginClass *klass)
516 : : {
517 : 11 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
518 : 11 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
519 : 11 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
520 : :
521 : 11 : object_class->constructed = valent_telephony_plugin_constructed;
522 : :
523 : 11 : vobject_class->destroy = valent_telephony_plugin_destroy;
524 : :
525 : 11 : plugin_class->handle_packet = valent_telephony_plugin_handle_packet;
526 : 11 : plugin_class->update_state = valent_telephony_plugin_update_state;
527 : : }
528 : :
529 : : static void
530 : 6 : valent_telephony_plugin_init (ValentTelephonyPlugin *self)
531 : : {
532 : 6 : }
533 : :
|