From 2726f8e8a28a802a880bdbf974f522ef02c27bea Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Thu, 24 Apr 2025 08:50:10 -0500 Subject: base-call: convert DTMF tone handling to a state machine Generalize the timeout/stop logic that QMI requires into the base call. We'll use this to implement the pause character (,) generically. Signed-off-by: Dan Williams --- src/mm-base-call.c | 270 +++++++++++++++++++++++++++++++++++++++++++++++++++-- src/mm-base-call.h | 24 +++-- src/mm-call-qmi.c | 169 ++++++++++++++++++--------------- 3 files changed, 371 insertions(+), 92 deletions(-) diff --git a/src/mm-base-call.c b/src/mm-base-call.c index 72982b1d..3f4e081a 100644 --- a/src/mm-base-call.c +++ b/src/mm-base-call.c @@ -89,6 +89,9 @@ struct _MMBaseCallPrivate { * 'terminated' is coming asynchronously (e.g. via in-call state * update notifications) */ GCancellable *start_cancellable; + + /* DTMF support */ + GQueue *dtmf_queue; }; /*****************************************************************************/ @@ -728,6 +731,239 @@ handle_hangup (MMBaseCall *self, /*****************************************************************************/ /* Send dtmf (DBus call handling) */ +typedef enum { + DTMF_STEP_FIRST, + DTMF_STEP_START, + DTMF_STEP_TIMEOUT, + DTMF_STEP_STOP, + DTMF_STEP_LAST, +} DtmfStep; + +typedef struct { + DtmfStep step; + GError *saved_error; + guint8 call_id; + gchar *dtmf; + guint timeout_id; +} SendDtmfContext; + +static void +send_dtmf_context_clear_timeout (SendDtmfContext *ctx) +{ + if (ctx->timeout_id) { + g_source_remove (ctx->timeout_id); + ctx->timeout_id = 0; + } +} + +static void +send_dtmf_context_free (SendDtmfContext *ctx) +{ + send_dtmf_context_clear_timeout (ctx); + g_free (ctx->dtmf); + g_assert (!ctx->saved_error); + g_slice_free (SendDtmfContext, ctx); +} + +static void send_dtmf_task_step_next (GTask *task); + +static void +stop_dtmf_ignore_ready (MMBaseCall *self, + GAsyncResult *res, + gpointer unused) +{ + /* Ignore the result and error */ + MM_BASE_CALL_GET_CLASS (self)->stop_dtmf_finish (self, res, NULL); +} + +static void +send_dtmf_task_cancel (GTask *task) +{ + MMBaseCall *self; + SendDtmfContext *ctx; + + ctx = g_task_get_task_data (task); + self = g_task_get_source_object (task); + + send_dtmf_context_clear_timeout (ctx); + if (ctx->step > DTMF_STEP_FIRST && ctx->step < DTMF_STEP_STOP) { + if (MM_BASE_CALL_GET_CLASS (self)->stop_dtmf) { + MM_BASE_CALL_GET_CLASS (self)->stop_dtmf (self, + (GAsyncReadyCallback)stop_dtmf_ignore_ready, + NULL); + } + } + g_assert (ctx->step != DTMF_STEP_LAST); + ctx->step = DTMF_STEP_LAST; + send_dtmf_task_step_next (task); +} + +static void +stop_dtmf_ready (MMBaseCall *self, + GAsyncResult *res, + GTask *task) +{ + SendDtmfContext *ctx; + GError *error = NULL; + gboolean success; + + ctx = g_task_get_task_data (task); + + success = MM_BASE_CALL_GET_CLASS (self)->stop_dtmf_finish (self, res, &error); + if (ctx->step == DTMF_STEP_STOP) { + if (!success) { + g_propagate_error (&ctx->saved_error, error); + ctx->step = DTMF_STEP_LAST; + } else + ctx->step++; + + send_dtmf_task_step_next (task); + } + + /* Balance stop_dtmf() */ + g_object_unref (task); +} + +static gboolean +dtmf_timeout (GTask *task) +{ + SendDtmfContext *ctx; + + ctx = g_task_get_task_data (task); + + send_dtmf_context_clear_timeout (ctx); + ctx->step++; + send_dtmf_task_step_next (task); + + return G_SOURCE_REMOVE; +} + +static void +send_dtmf_ready (MMBaseCall *self, + GAsyncResult *res, + GTask *task) +{ + SendDtmfContext *ctx; + GError *error = NULL; + gboolean success; + + ctx = g_task_get_task_data (task); + + success = MM_BASE_CALL_GET_CLASS (self)->send_dtmf_finish (self, res, &error); + if (ctx->step == DTMF_STEP_START) { + if (!success) { + g_propagate_error (&ctx->saved_error, error); + ctx->step = DTMF_STEP_LAST; + } else + ctx->step++; + + send_dtmf_task_step_next (task); + } + + /* Balance send_dtmf() */ + g_object_unref (task); +} + +static void +send_dtmf_task_step_next (GTask *task) +{ + SendDtmfContext *ctx; + gboolean need_stop; + MMBaseCall *self; + + self = g_task_get_source_object (task); + ctx = g_task_get_task_data (task); + + need_stop = MM_BASE_CALL_GET_CLASS (self)->stop_dtmf && + MM_BASE_CALL_GET_CLASS (self)->stop_dtmf_finish; + + switch (ctx->step) { + case DTMF_STEP_FIRST: + ctx->step++; + /* Fall through */ + case DTMF_STEP_START: + MM_BASE_CALL_GET_CLASS (self)->send_dtmf (self, + ctx->dtmf, + (GAsyncReadyCallback)send_dtmf_ready, + g_object_ref (task)); + break; + case DTMF_STEP_TIMEOUT: + if (need_stop) { + /* Disable DTMF press after DTMF tone duration elapses */ + ctx->timeout_id = g_timeout_add (mm_base_call_get_dtmf_tone_duration (self), + (GSourceFunc) dtmf_timeout, + task); + return; + } + /* Fall through */ + case DTMF_STEP_STOP: + if (need_stop) { + send_dtmf_context_clear_timeout (ctx); + MM_BASE_CALL_GET_CLASS (self)->stop_dtmf (self, + (GAsyncReadyCallback)stop_dtmf_ready, + g_object_ref (task)); + return; + } + /* Fall through */ + case DTMF_STEP_LAST: + send_dtmf_context_clear_timeout (ctx); + if (ctx->saved_error) + g_task_return_error (task, g_steal_pointer (&ctx->saved_error)); + else + g_task_return_boolean (task, TRUE); + g_object_unref (task); + + /* Start the next tone if any are queued */ + g_queue_remove (self->priv->dtmf_queue, task); + task = g_queue_peek_head (self->priv->dtmf_queue); + if (task) + send_dtmf_task_step_next (task); + break; + default: + g_assert_not_reached (); + } +} + +static gboolean +send_dtmf_task_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static GTask * +send_dtmf_task_new (MMBaseCall *self, + const gchar *dtmf, + GAsyncReadyCallback callback, + gpointer user_data, + GError **error) +{ + GTask *task; + SendDtmfContext *ctx; + guint8 call_id; + + call_id = mm_base_call_get_index (self); + if (call_id == 0) { + g_set_error (error, + MM_CORE_ERROR, + MM_CORE_ERROR_INVALID_ARGS, + "Invalid call index"); + return NULL; + } + + task = g_task_new (self, NULL, callback, user_data); + ctx = g_slice_new0 (SendDtmfContext); + ctx->call_id = call_id; + ctx->dtmf = g_strdup (dtmf); + g_task_set_task_data (task, ctx, (GDestroyNotify) send_dtmf_context_free); + + return task; +} + +/*****************************************************************************/ +/* Send DTMF D-Bus request handling */ + typedef struct { MMBaseCall *self; GDBusMethodInvocation *invocation; @@ -750,7 +986,7 @@ handle_send_dtmf_ready (MMBaseCall *self, { GError *error = NULL; - if (!MM_BASE_CALL_GET_CLASS (self)->send_dtmf_finish (self, res, &error)) { + if (!send_dtmf_task_finish (self, res, &error)) { mm_dbus_method_invocation_take_error (ctx->invocation, error); } else { mm_gdbus_call_complete_send_dtmf (MM_GDBUS_CALL (ctx->self), ctx->invocation); @@ -764,8 +1000,9 @@ handle_send_dtmf_auth_ready (MMAuthProvider *authp, GAsyncResult *res, HandleSendDtmfContext *ctx) { - MMCallState state; - GError *error = NULL; + MMCallState state; + GError *error = NULL; + GTask *task; if (!mm_auth_provider_authorize_finish (authp, res, &error)) { mm_dbus_method_invocation_take_error (ctx->invocation, error); @@ -775,7 +1012,7 @@ handle_send_dtmf_auth_ready (MMAuthProvider *authp, state = mm_gdbus_call_get_state (MM_GDBUS_CALL (ctx->self)); - /* Check if we do support doing it */ + /* Check if we do support doing DTMF at all */ if (!MM_BASE_CALL_GET_CLASS (ctx->self)->send_dtmf || !MM_BASE_CALL_GET_CLASS (ctx->self)->send_dtmf_finish) { mm_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED, @@ -793,9 +1030,20 @@ handle_send_dtmf_auth_ready (MMAuthProvider *authp, } mm_obj_info (ctx->self, "processing user request to send DTMF..."); - MM_BASE_CALL_GET_CLASS (ctx->self)->send_dtmf (ctx->self, ctx->dtmf, - (GAsyncReadyCallback)handle_send_dtmf_ready, - ctx); + task = send_dtmf_task_new (ctx->self, + ctx->dtmf, + (GAsyncReadyCallback)handle_send_dtmf_ready, + ctx, + &error); + if (!task) { + mm_dbus_method_invocation_take_error (ctx->invocation, error); + handle_send_dtmf_context_free (ctx); + return; + } + + g_queue_push_tail (ctx->self->priv->dtmf_queue, task); + if (g_queue_get_length (ctx->self->priv->dtmf_queue) == 1) + send_dtmf_task_step_next (task); } static gboolean @@ -1116,6 +1364,8 @@ mm_base_call_init (MMBaseCall *self) /* Setup authorization provider */ self->priv->authp = mm_auth_provider_get (); self->priv->authp_cancellable = g_cancellable_new (); + + self->priv->dtmf_queue = g_queue_new (); } static void @@ -1123,6 +1373,9 @@ finalize (GObject *object) { MMBaseCall *self = MM_BASE_CALL (object); + g_assert (g_queue_get_length (self->priv->dtmf_queue) == 0); + g_queue_free (g_steal_pointer (&self->priv->dtmf_queue)); + g_assert (!self->priv->start_cancellable); g_free (self->priv->path); @@ -1153,6 +1406,9 @@ dispose (GObject *object) g_cancellable_cancel (self->priv->authp_cancellable); g_clear_object (&self->priv->authp_cancellable); + g_queue_foreach (self->priv->dtmf_queue, (GFunc) send_dtmf_task_cancel, NULL); + g_queue_clear (self->priv->dtmf_queue); + G_OBJECT_CLASS (mm_base_call_parent_class)->dispose (object); } diff --git a/src/mm-base-call.h b/src/mm-base-call.h index fb738630..2e45ca5f 100644 --- a/src/mm-base-call.h +++ b/src/mm-base-call.h @@ -90,14 +90,22 @@ struct _MMBaseCallClass { GAsyncResult *res, GError **error); - /* Send a DTMF tone */ - void (* send_dtmf) (MMBaseCall *self, - const gchar *dtmf, - GAsyncReadyCallback callback, - gpointer user_data); - gboolean (* send_dtmf_finish) (MMBaseCall *self, - GAsyncResult *res, - GError **error); + /* DTMF tone handling */ + void (* send_dtmf) (MMBaseCall *self, + const gchar *dtmf, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* send_dtmf_finish) (MMBaseCall *self, + GAsyncResult *res, + GError **error); + + void (* stop_dtmf) (MMBaseCall *self, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* stop_dtmf_finish) (MMBaseCall *self, + GAsyncResult *res, + GError **error); + }; GType mm_base_call_get_type (void); diff --git a/src/mm-call-qmi.c b/src/mm-call-qmi.c index 1a9b2dd7..b1b6746d 100644 --- a/src/mm-call-qmi.c +++ b/src/mm-call-qmi.c @@ -331,22 +331,41 @@ call_hangup (MMBaseCall *self, } /*****************************************************************************/ -/* Send DTMF character */ +/* DTMF handling */ -typedef struct { - QmiClient *client; - guint8 call_id; -} SendDtmfContext; - -static void -send_dtmf_context_free (SendDtmfContext *ctx) +static gboolean +get_client_and_call_id (MMCallQmi *self, + GAsyncReadyCallback callback, + gpointer user_data, + QmiClient **client, + guint *call_id) { - g_clear_object (&ctx->client); - g_slice_free (SendDtmfContext, ctx); + g_return_val_if_fail (client, FALSE); + g_return_val_if_fail (call_id, FALSE); + + /* Ensure Voice client */ + if (!ensure_qmi_client (self, + QMI_SERVICE_VOICE, client, + callback, user_data)) + return FALSE; + + *call_id = mm_base_call_get_index (MM_BASE_CALL (self)); + if (*call_id == 0) { + g_task_report_new_error (self, + callback, + user_data, + (gpointer) __func__, + MM_CORE_ERROR, + MM_CORE_ERROR_INVALID_ARGS, + "Invalid call index"); + return FALSE; + } + + return TRUE; } static gboolean -call_send_dtmf_finish (MMBaseCall *self, +call_stop_dtmf_finish (MMBaseCall *call, GAsyncResult *res, GError **error) { @@ -366,35 +385,59 @@ voice_stop_continuous_dtmf_ready (QmiClientVoice *client, g_prefix_error (&error, "QMI operation failed: "); g_task_return_error (task, error); } else if (!qmi_message_voice_stop_continuous_dtmf_output_get_result (output, &error)) { - g_prefix_error (&error, "Couldn't send DTMF character: "); + g_prefix_error (&error, "Couldn't stop DTMF character: "); g_task_return_error (task, error); } else { g_task_return_boolean (task, TRUE); } - g_object_unref (task); } -static gboolean -voice_stop_continuous_dtmf (GTask *task) +static void +call_stop_dtmf (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) { - SendDtmfContext *ctx; - GError *error = NULL; - g_autoptr (QmiMessageVoiceStopContinuousDtmfInput) input = NULL; + MMCallQmi *self = MM_CALL_QMI (_self); + GTask *task; + QmiClient *client = NULL; + guint call_id = 0; + GError *error = NULL; + g_autoptr (QmiMessageVoiceStopContinuousDtmfInput) input = NULL; + + if (!get_client_and_call_id (self, + callback, + user_data, + &client, + &call_id)) + return; - ctx = g_task_get_task_data (task); + task = g_task_new (self, NULL, callback, user_data); input = qmi_message_voice_stop_continuous_dtmf_input_new (); - qmi_message_voice_stop_continuous_dtmf_input_set_data (input, ctx->call_id, &error); + if (!qmi_message_voice_stop_continuous_dtmf_input_set_data (input, + call_id, + &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } - qmi_client_voice_stop_continuous_dtmf (QMI_CLIENT_VOICE (ctx->client), + /* Stop sending DTMF tone */ + qmi_client_voice_stop_continuous_dtmf (QMI_CLIENT_VOICE (client), input, 5, NULL, (GAsyncReadyCallback) voice_stop_continuous_dtmf_ready, task); +} - return G_SOURCE_REMOVE; +static gboolean +call_send_dtmf_finish (MMBaseCall *call, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); } static void @@ -402,85 +445,55 @@ voice_start_continuous_dtmf_ready (QmiClientVoice *client, GAsyncResult *res, GTask *task) { - MMCallQmi *self; - g_autoptr(QmiMessageVoiceStartContinuousDtmfOutput) output = NULL; - GError *error = NULL; - - self = g_task_get_source_object (task); + g_autoptr (QmiMessageVoiceStartContinuousDtmfOutput) output = NULL; + GError *error = NULL; output = qmi_client_voice_start_continuous_dtmf_finish (client, res, &error); if (!output) { g_prefix_error (&error, "QMI operation failed: "); g_task_return_error (task, error); - g_object_unref (task); - return; - } - - if (!qmi_message_voice_start_continuous_dtmf_output_get_result (output, &error)) { + } else if (!qmi_message_voice_start_continuous_dtmf_output_get_result (output, &error)) { g_prefix_error (&error, "Couldn't send DTMF character: "); g_task_return_error (task, error); - g_object_unref (task); - return; + } else { + g_task_return_boolean (task, TRUE); } - - /* Disable DTMF press after DTMF tone duration elapses */ - g_timeout_add (mm_base_call_get_dtmf_tone_duration (MM_BASE_CALL (self)), - (GSourceFunc) voice_stop_continuous_dtmf, - task); + g_object_unref (task); } static void -call_send_dtmf (MMBaseCall *self, +call_send_dtmf (MMBaseCall *_self, const gchar *dtmf, GAsyncReadyCallback callback, gpointer user_data) { - GTask *task; - GError *error = NULL; - SendDtmfContext *ctx; - QmiClient *client = NULL; - guint8 call_id; - g_autoptr (QmiMessageVoiceStartContinuousDtmfInput) input = NULL; - - /* Ensure Voice client */ - if (!ensure_qmi_client (MM_CALL_QMI (self), - QMI_SERVICE_VOICE, &client, - callback, user_data)) + MMCallQmi *self = MM_CALL_QMI (_self); + GTask *task; + QmiClient *client = NULL; + guint call_id = 0; + GError *error = NULL; + g_autoptr (QmiMessageVoiceStartContinuousDtmfInput) input = NULL; + + if (!get_client_and_call_id (self, + callback, + user_data, + &client, + &call_id)) return; task = g_task_new (self, NULL, callback, user_data); - call_id = mm_base_call_get_index (self); - if (call_id == 0) { - g_task_return_new_error (task, - MM_CORE_ERROR, - MM_CORE_ERROR_INVALID_ARGS, - "Invalid call index"); - g_object_unref (task); - return; - } - - ctx = g_slice_new0 (SendDtmfContext); - ctx->client = g_object_ref (client); - ctx->call_id = call_id; - - g_task_set_task_data (task, ctx, (GDestroyNotify) send_dtmf_context_free); - - /* Send DTMF character as ASCII number */ input = qmi_message_voice_start_continuous_dtmf_input_new (); - qmi_message_voice_start_continuous_dtmf_input_set_data (input, - call_id, - (guint8) dtmf[0], - &error); - if (error) { - g_task_return_new_error (task, - MM_CORE_ERROR, - MM_CORE_ERROR_INVALID_ARGS, - "DTMF data build failed"); + if (!qmi_message_voice_start_continuous_dtmf_input_set_data (input, + call_id, + dtmf[0], + &error)) { + g_task_return_error (task, error); g_object_unref (task); return; } + /* Send DTMF character as ASCII number */ qmi_client_voice_start_continuous_dtmf (QMI_CLIENT_VOICE (client), input, 5, @@ -548,4 +561,6 @@ mm_call_qmi_class_init (MMCallQmiClass *klass) base_call_class->hangup_finish = call_hangup_finish; base_call_class->send_dtmf = call_send_dtmf; base_call_class->send_dtmf_finish = call_send_dtmf_finish; + base_call_class->stop_dtmf = call_stop_dtmf; + base_call_class->stop_dtmf_finish = call_stop_dtmf_finish; } -- cgit v1.2.3-70-g09d2