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