aboutsummaryrefslogtreecommitdiff
path: root/src/mm-iface-modem-voice.c
diff options
context:
space:
mode:
authorAleksander Morgado <aleksander@aleksander.es>2019-07-01 20:52:56 +0200
committerAleksander Morgado <aleksander@aleksander.es>2019-07-11 23:21:00 +0200
commit86aeb84f950c3b3e3be63c8920e63877a9c622f2 (patch)
tree099daec13a5b6adf45682cd3668f395b5c081803 /src/mm-iface-modem-voice.c
parentde2c24ec440c0a9f618f6c83b34a8bdf67d10fc2 (diff)
iface-modem-voice: handle in-call state at modem level
We cannot handle in-call state at call level because the state may apply to multiple active calls at the same time: * the modem is in-call if there is at least one ongoing call (>=1). * the modem is not in-call if there are no ongoing calls (==0). The new logic implements call state monitoring for all available call objects, and whenever the state of one or more calls change, we'll go over all of them to see how many of the calls can be considered to be ongoing (i.e. not terminated, not unknown). If we have at least one call ongoing, we'll setup the in-call state and otherwise, we'll clean it up. Setting up and cleaning up the in-call state involves unsolicited message and audio channel settings management operations.
Diffstat (limited to 'src/mm-iface-modem-voice.c')
-rw-r--r--src/mm-iface-modem-voice.c577
1 files changed, 575 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) {