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 <dan@ioncontrol.co>
This commit is contained in:
Dan Williams
2025-05-30 07:59:59 -05:00
parent 5448a85a2b
commit 2726f8e8a2
3 changed files with 383 additions and 104 deletions
+263 -7
View File
@@ -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);
}
+16 -8
View File
@@ -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);
+104 -89
View File
@@ -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;
ctx = g_task_get_task_data (task);
if (!get_client_and_call_id (self,
callback,
user_data,
&client,
&call_id))
return;
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);
} else {
g_task_return_boolean (task, TRUE);
}
g_object_unref (task);
}
static void
call_send_dtmf (MMBaseCall *_self,
const gchar *dtmf,
GAsyncReadyCallback callback,
gpointer 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);
input = qmi_message_voice_start_continuous_dtmf_input_new ();
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;
}
/* 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);
}
static void
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))
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");
g_object_unref (task);
return;
}
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;
}