diff options
-rw-r--r-- | src/mm-iface-modem-voice.c | 577 | ||||
-rw-r--r-- | src/mm-iface-modem-voice.h | 34 |
2 files changed, 609 insertions, 2 deletions
diff --git a/src/mm-iface-modem-voice.c b/src/mm-iface-modem-voice.c index abaa64a5..7e91fbd8 100644 --- a/src/mm-iface-modem-voice.c +++ b/src/mm-iface-modem-voice.c @@ -25,10 +25,12 @@ #define SUPPORT_CHECKED_TAG "voice-support-checked-tag" #define SUPPORTED_TAG "voice-supported-tag" #define CALL_LIST_POLLING_CONTEXT_TAG "voice-call-list-polling-context-tag" +#define IN_CALL_EVENT_CONTEXT_TAG "voice-in-call-event-context-tag" static GQuark support_checked_quark; static GQuark supported_quark; static GQuark call_list_polling_context_quark; +static GQuark in_call_event_context_quark; /*****************************************************************************/ @@ -40,13 +42,21 @@ mm_iface_modem_voice_bind_simple_status (MMIfaceModemVoice *self, /*****************************************************************************/ +/* new calls will inherit audio settings if the modem is already in-call state */ +static void update_audio_settings_in_call (MMIfaceModemVoice *self, + MMBaseCall *call); + static MMBaseCall * create_incoming_call (MMIfaceModemVoice *self, const gchar *number) { + MMBaseCall *call; + g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call != NULL); - return MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number); + call = MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number); + update_audio_settings_in_call (self, call); + return call; } static MMBaseCall * @@ -54,6 +64,7 @@ create_outgoing_call_from_properties (MMIfaceModemVoice *self, MMCallProperties *properties, GError **error) { + MMBaseCall *call; const gchar *number; /* Don't create CALL from properties if either number is missing */ @@ -68,7 +79,9 @@ create_outgoing_call_from_properties (MMIfaceModemVoice *self, /* Create a call object as defined by the interface */ g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call != NULL); - return MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_OUTGOING, number); + call = MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_OUTGOING, number); + update_audio_settings_in_call (self, call); + return call; } /*****************************************************************************/ @@ -1113,6 +1126,560 @@ handle_transfer (MmGdbusModemVoice *skeleton, } /*****************************************************************************/ +/* In-call setup operation + * + * It will setup URC handlers for all in-call URCs, and also setup the audio + * channel if the plugin requires to do so. + */ + +typedef enum { + IN_CALL_SETUP_STEP_FIRST, + IN_CALL_SETUP_STEP_UNSOLICITED_EVENTS, + IN_CALL_SETUP_STEP_AUDIO_CHANNEL, + IN_CALL_SETUP_STEP_LAST, +} InCallSetupStep; + +typedef struct { + InCallSetupStep step; + MMPort *audio_port; + MMCallAudioFormat *audio_format; +} InCallSetupContext; + +static void +in_call_setup_context_free (InCallSetupContext *ctx) +{ + g_clear_object (&ctx->audio_port); + g_clear_object (&ctx->audio_format); + g_slice_free (InCallSetupContext, ctx); +} + +static gboolean +in_call_setup_finish (MMIfaceModemVoice *self, + GAsyncResult *res, + MMPort **audio_port, /* optional */ + MMCallAudioFormat **audio_format, /* optional */ + GError **error) +{ + InCallSetupContext *ctx; + + if (!g_task_propagate_boolean (G_TASK (res), error)) + return FALSE; + + ctx = g_task_get_task_data (G_TASK (res)); + if (audio_port) { + *audio_port = ctx->audio_port; + ctx->audio_port = NULL; + } + if (audio_format) { + *audio_format = ctx->audio_format; + ctx->audio_format = NULL; + } + + return TRUE; +} + +static void in_call_setup_context_step (GTask *task); + +static void +setup_in_call_audio_channel_ready (MMIfaceModemVoice *self, + GAsyncResult *res, + GTask *task) +{ + InCallSetupContext *ctx; + GError *error = NULL; + + ctx = g_task_get_task_data (task); + + if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel_finish (self, + res, + &ctx->audio_port, + &ctx->audio_format, + &error)) { + mm_warn ("Couldn't setup in-call audio channel: %s", error->message); + g_clear_error (&error); + } + + ctx->step++; + in_call_setup_context_step (task); +} + +static void +setup_in_call_unsolicited_events_ready (MMIfaceModemVoice *self, + GAsyncResult *res, + GTask *task) +{ + InCallSetupContext *ctx; + GError *error = NULL; + + ctx = g_task_get_task_data (task); + + if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events_finish (self, res, &error)) { + mm_warn ("Couldn't setup in-call unsolicited events: %s", error->message); + g_clear_error (&error); + } + + ctx->step++; + in_call_setup_context_step (task); +} + +static void +in_call_setup_context_step (GTask *task) +{ + MMIfaceModemVoice *self; + InCallSetupContext *ctx; + + if (g_task_return_error_if_cancelled (task)) + return; + + self = g_task_get_source_object (task); + ctx = g_task_get_task_data (task); + + switch (ctx->step) { + case IN_CALL_SETUP_STEP_FIRST: + ctx->step++; + /* fall-through */ + case IN_CALL_SETUP_STEP_UNSOLICITED_EVENTS: + if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events && + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events_finish) { + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events ( + self, + (GAsyncReadyCallback) setup_in_call_unsolicited_events_ready, + task); + break; + } + ctx->step++; + /* fall-through */ + case IN_CALL_SETUP_STEP_AUDIO_CHANNEL: + if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel && + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel_finish) { + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel ( + self, + (GAsyncReadyCallback) setup_in_call_audio_channel_ready, + task); + break; + } + ctx->step++; + /* fall-through */ + case IN_CALL_SETUP_STEP_LAST: + g_task_return_boolean (task, TRUE); + g_object_unref (task); + return; + } +} + +static void +in_call_setup (MMIfaceModemVoice *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + InCallSetupContext *ctx; + + ctx = g_slice_new0 (InCallSetupContext); + ctx->step = IN_CALL_SETUP_STEP_FIRST; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, ctx, (GDestroyNotify) in_call_setup_context_free); + + in_call_setup_context_step (task); +} + +/*****************************************************************************/ +/* In-call cleanup operation + * + * It will cleanup audio channel settings and remove all in-call URC handlers. + */ + +typedef enum { + IN_CALL_CLEANUP_STEP_FIRST, + IN_CALL_CLEANUP_STEP_AUDIO_CHANNEL, + IN_CALL_CLEANUP_STEP_UNSOLICITED_EVENTS, + IN_CALL_CLEANUP_STEP_LAST, +} InCallCleanupStep; + +typedef struct { + InCallCleanupStep step; +} InCallCleanupContext; + +static gboolean +in_call_cleanup_finish (MMIfaceModemVoice *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static void in_call_cleanup_context_step (GTask *task); + +static void +cleanup_in_call_unsolicited_events_ready (MMIfaceModemVoice *self, + GAsyncResult *res, + GTask *task) +{ + InCallCleanupContext *ctx; + GError *error = NULL; + + ctx = g_task_get_task_data (task); + + if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events_finish (self, res, &error)) { + mm_warn ("Couldn't cleanup in-call unsolicited events: %s", error->message); + g_clear_error (&error); + } + + ctx->step++; + in_call_cleanup_context_step (task); +} + +static void +cleanup_in_call_audio_channel_ready (MMIfaceModemVoice *self, + GAsyncResult *res, + GTask *task) +{ + InCallCleanupContext *ctx; + GError *error = NULL; + + ctx = g_task_get_task_data (task); + + if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel_finish (self, res, &error)) { + mm_warn ("Couldn't cleanup in-call audio channel: %s", error->message); + g_clear_error (&error); + } + + ctx->step++; + in_call_cleanup_context_step (task); +} + +static void +in_call_cleanup_context_step (GTask *task) +{ + MMIfaceModemVoice *self; + InCallCleanupContext *ctx; + + if (g_task_return_error_if_cancelled (task)) + return; + + self = g_task_get_source_object (task); + ctx = g_task_get_task_data (task); + + switch (ctx->step) { + case IN_CALL_CLEANUP_STEP_FIRST: + ctx->step++; + /* fall-through */ + case IN_CALL_CLEANUP_STEP_AUDIO_CHANNEL: + if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel && + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel_finish) { + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel ( + self, + (GAsyncReadyCallback) cleanup_in_call_audio_channel_ready, + task); + break; + } + ctx->step++; + /* fall-through */ + case IN_CALL_CLEANUP_STEP_UNSOLICITED_EVENTS: + if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events && + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events_finish) { + MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events ( + self, + (GAsyncReadyCallback) cleanup_in_call_unsolicited_events_ready, + task); + break; + } + ctx->step++; + /* fall-through */ + case IN_CALL_CLEANUP_STEP_LAST: + g_task_return_boolean (task, TRUE); + g_object_unref (task); + return; + } +} + +static void +in_call_cleanup (MMIfaceModemVoice *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + InCallCleanupContext *ctx; + + ctx = g_new0 (InCallCleanupContext, 1); + ctx->step = IN_CALL_CLEANUP_STEP_FIRST; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, ctx, g_free); + + in_call_cleanup_context_step (task); +} + +/*****************************************************************************/ +/* In-call event handling logic + * + * This procedure will run a in-call setup async function whenever we detect + * that there is at least one call that is ongoing. This setup function will + * try to setup in-call unsolicited events as well as any audio channel + * requirements. + * + * The procedure will run a in-call cleanup async function whenever we detect + * that there are no longer any ongoing calls. The cleanup function will + * cleanup the audio channel and remove the in-call unsolicited event handlers. + */ + +typedef struct { + guint check_id; + GCancellable *setup_cancellable; + GCancellable *cleanup_cancellable; + gboolean in_call_state; + MMPort *audio_port; + MMCallAudioFormat *audio_format; +} InCallEventContext; + +static void +in_call_event_context_free (InCallEventContext *ctx) +{ + if (ctx->check_id) + g_source_remove (ctx->check_id); + if (ctx->cleanup_cancellable) { + g_cancellable_cancel (ctx->cleanup_cancellable); + g_clear_object (&ctx->cleanup_cancellable); + } + if (ctx->setup_cancellable) { + g_cancellable_cancel (ctx->setup_cancellable); + g_clear_object (&ctx->setup_cancellable); + } + g_clear_object (&ctx->audio_port); + g_clear_object (&ctx->audio_format); + g_slice_free (InCallEventContext, ctx); +} + +static InCallEventContext * +get_in_call_event_context (MMIfaceModemVoice *self) +{ + InCallEventContext *ctx; + + if (G_UNLIKELY (!in_call_event_context_quark)) + in_call_event_context_quark = g_quark_from_static_string (IN_CALL_EVENT_CONTEXT_TAG); + + ctx = g_object_get_qdata (G_OBJECT (self), in_call_event_context_quark); + if (!ctx) { + /* Create context and keep it as object data */ + ctx = g_slice_new0 (InCallEventContext); + g_object_set_qdata_full ( + G_OBJECT (self), + in_call_event_context_quark, + ctx, + (GDestroyNotify)in_call_event_context_free); + } + + return ctx; +} + +static void +call_list_foreach_audio_settings (MMBaseCall *call, + InCallEventContext *ctx) +{ + if (mm_base_call_get_state (call) != MM_CALL_STATE_TERMINATED) + return; + mm_base_call_change_audio_settings (call, ctx->audio_port, ctx->audio_format); +} + +static void +update_audio_settings_in_ongoing_calls (MMIfaceModemVoice *self) +{ + MMCallList *list = NULL; + InCallEventContext *ctx; + + ctx = get_in_call_event_context (self); + + g_object_get (MM_BASE_MODEM (self), + MM_IFACE_MODEM_VOICE_CALL_LIST, &list, + NULL); + if (!list) { + mm_warn ("Cannot update audio settings in active calls: missing internal call list"); + return; + } + + mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_audio_settings, ctx); + g_clear_object (&list); +} + +static void +update_audio_settings_in_call (MMIfaceModemVoice *self, + MMBaseCall *call) +{ + InCallEventContext *ctx; + + ctx = get_in_call_event_context (self); + mm_base_call_change_audio_settings (call, ctx->audio_port, ctx->audio_format); +} + +static void +call_list_foreach_count_in_call (MMBaseCall *call, + gpointer user_data) +{ + guint *n_calls_in_call = (guint *)user_data; + + switch (mm_base_call_get_state (call)) { + case MM_CALL_STATE_DIALING: + case MM_CALL_STATE_RINGING_OUT: + case MM_CALL_STATE_HELD: + case MM_CALL_STATE_ACTIVE: + *n_calls_in_call = *n_calls_in_call + 1; + break; + case MM_CALL_STATE_RINGING_IN: + case MM_CALL_STATE_WAITING: + /* NOTE: ringing-in and waiting calls are NOT yet in-call, e.g. there must + * be no audio settings enabled and we must not enable in-call URC handling + * yet. */ + default: + break; + } +} + +static void +in_call_cleanup_ready (MMIfaceModemVoice *self, + GAsyncResult *res) +{ + GError *error = NULL; + InCallEventContext *ctx; + + ctx = get_in_call_event_context (self); + + if (!in_call_cleanup_finish (self, res, &error)) { + /* ignore cancelled operations */ + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && !g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED)) + mm_warn ("Cannot cleanup in-call modem state: %s", error->message); + g_clear_error (&error); + } else { + mm_dbg ("modem is no longer in-call state"); + ctx->in_call_state = FALSE; + g_clear_object (&ctx->audio_port); + g_clear_object (&ctx->audio_format); + } + + g_clear_object (&ctx->cleanup_cancellable); +} + +static void +in_call_setup_ready (MMIfaceModemVoice *self, + GAsyncResult *res) +{ + GError *error = NULL; + InCallEventContext *ctx; + + ctx = get_in_call_event_context (self); + + if (!in_call_setup_finish (self, res, &ctx->audio_port, &ctx->audio_format, &error)) { + /* ignore cancelled operations */ + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && !g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_CANCELLED)) + mm_warn ("Cannot setup in-call modem state: %s", error->message); + g_clear_error (&error); + } else { + mm_dbg ("modem is now in-call state"); + ctx->in_call_state = TRUE; + update_audio_settings_in_ongoing_calls (self); + } + + g_clear_object (&ctx->setup_cancellable); +} + +static gboolean +call_list_check_in_call_events (MMIfaceModemVoice *self) +{ + InCallEventContext *ctx; + MMCallList *list = NULL; + guint n_calls_in_call = 0; + + ctx = get_in_call_event_context (self); + ctx->check_id = 0; + + g_object_get (MM_BASE_MODEM (self), + MM_IFACE_MODEM_VOICE_CALL_LIST, &list, + NULL); + if (!list) { + mm_warn ("Cannot update in-call state: missing internal call list"); + goto out; + } + + mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_count_in_call, &n_calls_in_call); + + /* Need to setup in-call events? */ + if (n_calls_in_call > 0 && !ctx->in_call_state) { + /* if setup already ongoing, do nothing */ + if (ctx->setup_cancellable) + goto out; + + /* cancel ongoing cleanup if any */ + if (ctx->cleanup_cancellable) { + g_cancellable_cancel (ctx->cleanup_cancellable); + g_clear_object (&ctx->cleanup_cancellable); + } + + /* run setup */ + mm_dbg ("Setting up in-call state..."); + ctx->setup_cancellable = g_cancellable_new (); + in_call_setup (self, ctx->setup_cancellable, (GAsyncReadyCallback) in_call_setup_ready, NULL); + goto out; + } + + /* Need to cleanup in-call events? */ + if (n_calls_in_call == 0 && ctx->in_call_state) { + /* if cleanup already ongoing, do nothing */ + if (ctx->cleanup_cancellable) + goto out; + + /* cancel ongoing setup if any */ + if (ctx->setup_cancellable) { + g_cancellable_cancel (ctx->setup_cancellable); + g_clear_object (&ctx->setup_cancellable); + } + + /* run cleanup */ + mm_dbg ("Cleaning up in-call state..."); + ctx->cleanup_cancellable = g_cancellable_new (); + in_call_cleanup (self, ctx->cleanup_cancellable, (GAsyncReadyCallback) in_call_cleanup_ready, NULL); + goto out; + } + + out: + g_clear_object (&list); + return G_SOURCE_REMOVE; +} + +static void +call_state_changed (MMIfaceModemVoice *self) +{ + InCallEventContext *ctx; + + ctx = get_in_call_event_context (self); + if (ctx->check_id) + return; + + /* Process check for in-call events in an idle, so that we can combine + * together in the same check multiple call state updates happening + * at the same time for different calls (e.g. when swapping active/held + * calls). */ + ctx->check_id = g_idle_add ((GSourceFunc)call_list_check_in_call_events, self); +} + +static void +setup_in_call_event_handling (MMCallList *call_list, + const gchar *call_path_added, + MMIfaceModemVoice *self) +{ + MMBaseCall *call; + + call = mm_call_list_get_call (call_list, call_path_added); + g_assert (call); + + g_signal_connect_swapped (call, + "state-changed", + G_CALLBACK (call_state_changed), + self); +} + +/*****************************************************************************/ /* Call list polling logic * * The call list polling is exclusively used to detect detailed call state @@ -1568,6 +2135,12 @@ interface_enabling_step (GTask *task) G_CALLBACK (call_deleted), ctx->skeleton); + /* Setup monitoring for in-call event handling */ + g_signal_connect (list, + MM_CALL_ADDED, + G_CALLBACK (setup_in_call_event_handling), + self); + /* Unless we're told not to, setup call list polling logic */ if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list && MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish) { diff --git a/src/mm-iface-modem-voice.h b/src/mm-iface-modem-voice.h index f3d4ce76..7cc82601 100644 --- a/src/mm-iface-modem-voice.h +++ b/src/mm-iface-modem-voice.h @@ -78,6 +78,40 @@ struct _MMIfaceModemVoice { GAsyncResult *res, GError **error); + /* Asynchronous setup of in-call unsolicited events */ + void (* setup_in_call_unsolicited_events) (MMIfaceModemVoice *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* setup_in_call_unsolicited_events_finish) (MMIfaceModemVoice *self, + GAsyncResult *res, + GError **error); + + /* Asynchronous cleanup of in-call unsolicited events */ + void (* cleanup_in_call_unsolicited_events) (MMIfaceModemVoice *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* cleanup_in_call_unsolicited_events_finish) (MMIfaceModemVoice *self, + GAsyncResult *res, + GError **error); + + /* Asynchronous setup of in-call audio channel */ + void (* setup_in_call_audio_channel) (MMIfaceModemVoice *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* setup_in_call_audio_channel_finish) (MMIfaceModemVoice *self, + GAsyncResult *res, + MMPort **audio_port, /* optional */ + MMCallAudioFormat **audio_format, /* optional */ + GError **error); + + /* Asynchronous cleanup of in-call audio channel */ + void (* cleanup_in_call_audio_channel) (MMIfaceModemVoice *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* cleanup_in_call_audio_channel_finish) (MMIfaceModemVoice *self, + GAsyncResult *res, + GError **error); + /* Load full list of calls (MMCallInfo list) */ void (* load_call_list) (MMIfaceModemVoice *self, GAsyncReadyCallback callback, |