diff options
author | Dan Williams <dan@ioncontrol.co> | 2025-05-30 18:54:05 -0500 |
---|---|---|
committer | Dan Williams <dan@ioncontrol.co> | 2025-05-30 18:54:05 -0500 |
commit | 37f17d4b5859d8a37d1a9350abc9fcb15917de07 (patch) | |
tree | 7af9420327a7deea70df1bbcaa004724fed1aec7 | |
parent | 3ed7f378765b45a84ce6c0b4de6751769fefc221 (diff) | |
parent | dad2d49b696c66ccf868bc89b35a6529f9e15777 (diff) |
Merge request !1336 from 'dtmf-serialize'
Serialize DTMF requests and allow setting DTMF duration at call creation time
https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/merge_requests/1336
Closes #970
30 files changed, 2983 insertions, 607 deletions
diff --git a/introspection/org.freedesktop.ModemManager1.Call.xml b/introspection/org.freedesktop.ModemManager1.Call.xml index 3aa6a643..6f1fdf44 100644 --- a/introspection/org.freedesktop.ModemManager1.Call.xml +++ b/introspection/org.freedesktop.ModemManager1.Call.xml @@ -103,9 +103,15 @@ <!-- SendDtmf: - @dtmf: DTMF tone identifier [0-9A-D*#]. + @dtmf: A string of DTMF tone identifiers [0-9A-D*#] and/or pause characters [,]. + + Send one or more DTMF tones (Dual Tone Multi-Frequency) (only on supported modems). + Before 1.26 only the first character in @dtmf was sent to the modem; + all others were discarded. - Send a DTMF tone (Dual Tone Multi-Frequency) (only on supported modem). + Since 1.26 up to 50 tone identifiers are accepted and each will be + sent to the modem in the order given. The comma [,] character pauses + DTMF tones for two-seconds then continues with the remaining characters. Applicable only if state is <link linkend="MM-CALL-STATE-ACTIVE:CAPS"><constant>MM_CALL_STATE_ACTIVE</constant></link>. @@ -119,7 +125,7 @@ DtmfReceived: @dtmf: DTMF tone identifier [0-9A-D*#]. - Emitted when a DTMF tone is received (only on supported modem) + Emitted when a DTMF tone is received (only on supported modems) Since: 1.6 --> @@ -232,5 +238,18 @@ Since: 1.10 --> <property name="AudioFormat" type="a{sv}" access="read" /> + + <!-- + DtmfToneDuration: + + The length of DTMF tones, in milliseconds. Valid range is 100ms to + 1000ms and is rounded up to the next 100ms if not evenly divisble + by 100. Set the duration by passing this property to + <link linkend="gdbus-method-org-freedesktop-ModemManager1-Voice.CreateCall">CreateCall()</link> + at call creation time. + + Since: 1.26 + --> + <property name="DtmfToneDuration" type="u" access="read" /> </interface> </node> diff --git a/libmm-glib/mm-call-properties.c b/libmm-glib/mm-call-properties.c index c5da4920..ff486aa5 100644 --- a/libmm-glib/mm-call-properties.c +++ b/libmm-glib/mm-call-properties.c @@ -44,10 +44,12 @@ G_DEFINE_TYPE (MMCallProperties, mm_call_properties, G_TYPE_OBJECT) -#define PROPERTY_NUMBER "number" +#define PROPERTY_NUMBER "number" +#define PROPERTY_DTMF_TONE_DURATION "dtmf-tone-duration" struct _MMCallPropertiesPrivate { gchar *number; + guint dtmf_tone_duration; }; /*****************************************************************************/ @@ -92,6 +94,43 @@ mm_call_properties_get_number (MMCallProperties *self) /*****************************************************************************/ +/** + * mm_call_properties_set_dtmf_tone_duration: + * @self: A #MMCallProperties. + * @duration_ms: The desired duration of DTMF tones in milliseconds. + * + * Sets the DTMF tone duration if supported by the modem. + * + * Since: 1.26 + */ +void +mm_call_properties_set_dtmf_tone_duration (MMCallProperties *self, + const guint duration_ms) +{ + g_return_if_fail (MM_IS_CALL_PROPERTIES (self)); + + self->priv->dtmf_tone_duration = duration_ms; +} + +/** + * mm_call_properties_get_dtmf_tone_duration: + * @self: A #MMCallProperties. + * + * Gets the desired DTMF tone duration in milliseconds. + * + * Returns: the DTMF tone duration in milliseconds. + * + * Since: 1.26 + */ +guint +mm_call_properties_get_dtmf_tone_duration (MMCallProperties *self) +{ + g_return_val_if_fail (MM_IS_CALL_PROPERTIES (self), 0); + + return self->priv->dtmf_tone_duration; +} + +/*****************************************************************************/ /* * mm_call_properties_get_dictionary: (skip) */ @@ -114,6 +153,12 @@ mm_call_properties_get_dictionary (MMCallProperties *self) PROPERTY_NUMBER, g_variant_new_string (self->priv->number)); + if (self->priv->dtmf_tone_duration) + g_variant_builder_add (&builder, + "{sv}", + PROPERTY_DTMF_TONE_DURATION, + g_variant_new_uint32 (self->priv->dtmf_tone_duration)); + return g_variant_ref_sink (g_variant_builder_end (&builder)); } @@ -126,6 +171,18 @@ consume_string (MMCallProperties *self, { if (g_str_equal (key, PROPERTY_NUMBER)) { mm_call_properties_set_number (self, value); + } else if (g_str_equal (key, PROPERTY_DTMF_TONE_DURATION)) { + guint num; + + if (!mm_get_uint_from_str (value, &num)) { + g_set_error (error, + MM_CORE_ERROR, + MM_CORE_ERROR_INVALID_ARGS, + "Failed to parse DTMF tone duration from value '%s'", + value); + return FALSE; + } + mm_call_properties_set_dtmf_tone_duration (self, num); } else { g_set_error (error, MM_CORE_ERROR, @@ -189,11 +246,29 @@ consume_variant (MMCallProperties *properties, GVariant *value, GError **error) { - if (g_str_equal (key, PROPERTY_NUMBER)) + if (g_str_equal (key, PROPERTY_NUMBER)) { + if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) { + g_set_error_literal (error, + MM_CORE_ERROR, + MM_CORE_ERROR_INVALID_ARGS, + "Invalid properties dictionary; number not a string"); + return FALSE; + } mm_call_properties_set_number ( properties, g_variant_get_string (value, NULL)); - else { + } else if (g_str_equal (key, PROPERTY_DTMF_TONE_DURATION)) { + if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32)) { + g_set_error_literal (error, + MM_CORE_ERROR, + MM_CORE_ERROR_INVALID_ARGS, + "Invalid properties dictionary; dtmf-tone-duration not a uint32"); + return FALSE; + } + mm_call_properties_set_dtmf_tone_duration ( + properties, + g_variant_get_uint32 (value)); + } else { /* Set error */ g_set_error (error, MM_CORE_ERROR, diff --git a/libmm-glib/mm-call-properties.h b/libmm-glib/mm-call-properties.h index 12b4e330..1329ee3b 100644 --- a/libmm-glib/mm-call-properties.h +++ b/libmm-glib/mm-call-properties.h @@ -68,6 +68,10 @@ void mm_call_properties_set_number (MMCallProperties *self, const gchar *text); const gchar *mm_call_properties_get_number (MMCallProperties *self); +void mm_call_properties_set_dtmf_tone_duration (MMCallProperties *self, + const guint duration_ms); +guint mm_call_properties_get_dtmf_tone_duration (MMCallProperties *self); + /*****************************************************************************/ /* ModemManager/libmm-glib/mmcli specific methods */ diff --git a/src/meson.build b/src/meson.build index 10838cf1..04d199d1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -315,17 +315,11 @@ daemon_enums_types_dep = declare_dependency( include_directories: '.', ) -# Additional vendor plugins -subdir('plugins') - -# ModemManager daemon -sources = files( - 'main.c', - 'mm-auth-provider.c', +base_sources = files( 'mm-base-bearer.c', 'mm-base-call.c', + 'mm-call-at.c', 'mm-base-cbm.c', - 'mm-base-manager.c', 'mm-base-modem-at.c', 'mm-base-modem.c', 'mm-base-sim.c', @@ -340,7 +334,6 @@ sources = files( 'mm-dispatcher-connection.c', 'mm-dispatcher-fcc-unlock.c', 'mm-dispatcher-modem-setup.c', - 'mm-filter.c', 'mm-iface-modem-3gpp.c', 'mm-iface-modem-3gpp-profile-manager.c', 'mm-iface-modem-3gpp-ussd.c', @@ -358,15 +351,68 @@ sources = files( 'mm-iface-modem-voice.c', 'mm-iface-op-lock.c', 'mm-log-helpers.c', + 'mm-private-boxed-types.c', + 'mm-sleep-context.c', +) + +# MM base library (used by MM and tests) +incs = [ + top_inc, + kerneldevice_inc, +] + +deps = [ + libmm_glib_dep, + libhelpers_dep, + libauth_dep, + libport_dep, + libqcdm_dep, + daemon_enums_types_dep, +] + +private_deps = [] + +c_args = [ + '-DMM_COMPILATION', + '-DPLUGINDIR="@0@"'.format(mm_prefix / mm_pkglibdir), + '-DMODEMSETUPDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'modem-setup.d'), + '-DMODEMSETUPDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'modem-setup.d'), + '-DFCCUNLOCKDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'fcc-unlock.d'), + '-DFCCUNLOCKDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'fcc-unlock.d'), + '-DCONNECTIONDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'connection.d'), + '-DCONNECTIONDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'connection.d'), +] + +libmmbase = static_library( + 'mmbase', + sources: base_sources + daemon_enums_sources, + include_directories: incs, + dependencies: deps, + c_args: c_args, +) + +libmmbase_dep = declare_dependency( + include_directories: ['.', kerneldevice_inc], + dependencies: deps, + link_with: libmmbase, +) + +# Additional vendor plugins +subdir('plugins') + +# ModemManager daemon +sources = files( + 'main.c', + 'mm-base-manager.c', + 'mm-filter.c', 'mm-plugin.c', 'mm-plugin-manager.c', 'mm-port-probe.c', 'mm-port-probe-at.c', - 'mm-private-boxed-types.c', - 'mm-sleep-context.c', ) sources += daemon_enums_sources +sources += base_sources deps = [ gmodule_dep, @@ -382,7 +428,6 @@ endif c_args = [ '-DMM_COMPILATION', - '-DPLUGINDIR="@0@"'.format(mm_prefix / mm_pkglibdir), '-DMODEMSETUPDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'modem-setup.d'), '-DMODEMSETUPDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'modem-setup.d'), '-DFCCUNLOCKDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'fcc-unlock.d'), diff --git a/src/mm-base-call.c b/src/mm-base-call.c index 6308fd18..e011c448 100644 --- a/src/mm-base-call.c +++ b/src/mm-base-call.c @@ -28,10 +28,8 @@ #include "mm-base-call.h" #include "mm-broadband-modem.h" -#include "mm-iface-modem.h" +#include "mm-auth-provider.h" #include "mm-iface-modem-voice.h" -#include "mm-base-modem-at.h" -#include "mm-base-modem.h" #include "mm-log-object.h" #include "mm-modem-helpers.h" #include "mm-error-helpers.h" @@ -49,7 +47,7 @@ enum { PROP_PATH, PROP_CONNECTION, PROP_BIND_TO, - PROP_MODEM, + PROP_IFACE_MODEM_VOICE, PROP_SKIP_INCOMING_TIMEOUT, PROP_SUPPORTS_DIALING_TO_RINGING, PROP_SUPPORTS_RINGING_TO_ACTIVE, @@ -70,8 +68,8 @@ struct _MMBaseCallPrivate { /* The object this Call is bound to */ GObject *bind_to; - /* The modem which owns this call */ - MMBaseModem *modem; + /* The voice interface which owns this call */ + MMIfaceModemVoice *iface; /* The path where the call object is exported */ gchar *path; /* Features */ @@ -91,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; }; /*****************************************************************************/ @@ -239,7 +240,7 @@ handle_start_auth_ready (MMAuthProvider *authp, mm_obj_info (ctx->self, "processing user request to start voice call..."); /* Disallow non-emergency calls when in emergency-only state */ - if (!mm_iface_modem_voice_authorize_outgoing_call (MM_IFACE_MODEM_VOICE (ctx->self->priv->modem), ctx->self, &error)) { + if (!mm_iface_modem_voice_authorize_outgoing_call (ctx->self->priv->iface, ctx->self, &error)) { mm_base_call_change_state (ctx->self, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_UNKNOWN); mm_dbus_method_invocation_take_error (ctx->invocation, error); handle_start_context_free (ctx); @@ -534,7 +535,7 @@ handle_join_multiparty_auth_ready (MMAuthProvider *authp, /* This action is provided in the Call API, but implemented in the Modem.Voice interface * logic, because the action affects not only one call object, but all call objects that * are part of the multiparty call. */ - mm_iface_modem_voice_join_multiparty (MM_IFACE_MODEM_VOICE (ctx->self->priv->modem), + mm_iface_modem_voice_join_multiparty (ctx->self->priv->iface, ctx->self, (GAsyncReadyCallback)modem_voice_join_multiparty_ready, ctx); @@ -608,7 +609,7 @@ handle_leave_multiparty_auth_ready (MMAuthProvider *authp, /* This action is provided in the Call API, but implemented in the Modem.Voice interface * logic, because the action affects not only one call object, but all call objects that * are part of the multiparty call. */ - mm_iface_modem_voice_leave_multiparty (MM_IFACE_MODEM_VOICE (ctx->self->priv->modem), + mm_iface_modem_voice_leave_multiparty (ctx->self->priv->iface, ctx->self, (GAsyncReadyCallback)modem_voice_leave_multiparty_ready, ctx); @@ -730,6 +731,283 @@ 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_NEXT, + DTMF_STEP_LAST, +} DtmfStep; + +typedef struct { + DtmfStep step; + GError *saved_error; + guint8 call_id; + /* Array of DTMF runs; split by pauses */ + GPtrArray *dtmfs; + /* index into dtmfs */ + guint cur_dtmf; + /* index into cur dtmf run string */ + gchar *cur_tone; + 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_ptr_array_foreach (ctx->dtmfs, (GFunc) g_free, NULL); + g_ptr_array_free (ctx->dtmfs, TRUE); + 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); + + /* If this was a pause character; move past it */ + if (ctx->cur_tone[0] == MM_CALL_DTMF_PAUSE_CHAR) + ctx->cur_tone++; + + 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; + gssize num_sent; + + ctx = g_task_get_task_data (task); + + num_sent = MM_BASE_CALL_GET_CLASS (self)->send_dtmf_finish (self, res, &error); + if (ctx->step == DTMF_STEP_START) { + if (num_sent < 0) { + g_propagate_error (&ctx->saved_error, error); + ctx->step = DTMF_STEP_LAST; + } else { + g_assert (num_sent > 0); + g_assert ((guint) num_sent <= strlen (ctx->cur_tone)); + ctx->cur_tone += num_sent; + 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; + gboolean is_pause; + + self = g_task_get_source_object (task); + ctx = g_task_get_task_data (task); + + is_pause = (ctx->cur_tone[0] == MM_CALL_DTMF_PAUSE_CHAR); + 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: + if (!is_pause) { + MM_BASE_CALL_GET_CLASS (self)->send_dtmf (self, + ctx->cur_tone, + (GAsyncReadyCallback)send_dtmf_ready, + g_object_ref (task)); + return; + } + /* Fall through */ + case DTMF_STEP_TIMEOUT: + if (need_stop || is_pause) { + guint duration; + + duration = is_pause ? 2000 : mm_base_call_get_dtmf_tone_duration (self); + + /* Disable DTMF press after DTMF tone duration elapses */ + ctx->timeout_id = g_timeout_add (duration, + (GSourceFunc) dtmf_timeout, + task); + return; + } + /* Fall through */ + case DTMF_STEP_STOP: + send_dtmf_context_clear_timeout (ctx); + if (need_stop && !is_pause) { + MM_BASE_CALL_GET_CLASS (self)->stop_dtmf (self, + (GAsyncReadyCallback)stop_dtmf_ready, + g_object_ref (task)); + return; + } + /* Fall through */ + case DTMF_STEP_NEXT: + /* Advance to next DTMF run? */ + if (ctx->cur_tone[0] == '\0') { + ctx->cur_dtmf++; + if (ctx->cur_dtmf < ctx->dtmfs->len) + ctx->cur_tone = g_ptr_array_index (ctx->dtmfs, ctx->cur_dtmf); + } + + /* More to send? */ + if (ctx->cur_tone[0]) { + ctx->step = DTMF_STEP_START; + send_dtmf_task_step_next (task); + return; + } + /* no more DTMF characters to send */ + /* 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; + /* Split DTMF into runs of DTMF characters interrupted by pauses */ + ctx->dtmfs = mm_dtmf_split (dtmf); + g_assert (ctx->dtmfs->len > 0); + ctx->cur_tone = g_ptr_array_index (ctx->dtmfs, ctx->cur_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; @@ -752,7 +1030,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); @@ -766,8 +1044,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); @@ -777,7 +1056,23 @@ 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 */ + /* Ensure there are DTMF characters to send */ + if (!ctx->dtmf || !ctx->dtmf[0]) { + mm_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS, + "No DTMF characters given"); + handle_send_dtmf_context_free (ctx); + return; + } + + /* And that there aren't too many */ + if (strlen (ctx->dtmf) > 50) { + mm_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS, + "Too many DTMF characters"); + handle_send_dtmf_context_free (ctx); + return; + } + + /* 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, @@ -787,7 +1082,7 @@ handle_send_dtmf_auth_ready (MMAuthProvider *authp, } /* We can only send_dtmf when call is in ACTIVE state */ - if (state != MM_CALL_STATE_ACTIVE ){ + if (state != MM_CALL_STATE_ACTIVE) { mm_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "This call was not active, cannot send dtmf"); handle_send_dtmf_context_free (ctx); @@ -795,9 +1090,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 @@ -924,6 +1230,20 @@ mm_base_call_set_multiparty (MMBaseCall *self, return mm_gdbus_call_set_multiparty (MM_GDBUS_CALL (self), multiparty); } +guint +mm_base_call_get_dtmf_tone_duration (MMBaseCall *self) +{ + return mm_dtmf_duration_normalize (mm_gdbus_call_get_dtmf_tone_duration (MM_GDBUS_CALL (self))); +} + +void +mm_base_call_set_dtmf_tone_duration (MMBaseCall *self, + guint duration_ms) +{ + return mm_gdbus_call_set_dtmf_tone_duration (MM_GDBUS_CALL (self), + mm_dtmf_duration_normalize (duration_ms)); +} + /*****************************************************************************/ /* Current call index, only applicable while the call is ongoing * See 3GPP TS 22.030 [27], subclause 6.5.5.1. @@ -989,314 +1309,6 @@ mm_base_call_received_dtmf (MMBaseCall *self, } /*****************************************************************************/ -/* Start the CALL */ - -static gboolean -call_start_finish (MMBaseCall *self, - GAsyncResult *res, - GError **error) -{ - return g_task_propagate_boolean (G_TASK (res), error); -} - -static void -call_start_ready (MMBaseModem *modem, - GAsyncResult *res, - GTask *task) -{ - GError *error = NULL; - const gchar *response = NULL; - - response = mm_base_modem_at_command_finish (modem, res, &error); - - /* check response for error */ - if (response && response[0]) - error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, - "Couldn't start the call: Unhandled response '%s'", response); - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); - g_object_unref (task); -} - -static void -call_start (MMBaseCall *self, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GError *error = NULL; - GTask *task; - gchar *cmd; - MMIfacePortAt *port; - - task = g_task_new (self, NULL, callback, user_data); - - port = mm_base_modem_peek_best_at_port (MM_BASE_MODEM (self->priv->modem), &error); - if (!port) { - g_task_return_error (task, error); - g_object_unref (task); - return; - } - - cmd = g_strdup_printf ("ATD%s;", mm_gdbus_call_get_number (MM_GDBUS_CALL (self))); - mm_base_modem_at_command_full (self->priv->modem, - port, - cmd, - 90, - FALSE, /* no cached */ - FALSE, /* no raw */ - cancellable, - (GAsyncReadyCallback)call_start_ready, - task); - g_free (cmd); -} - -/*****************************************************************************/ -/* Accept the call */ - -static gboolean -call_accept_finish (MMBaseCall *self, - GAsyncResult *res, - GError **error) -{ - return g_task_propagate_boolean (G_TASK (res), error); -} - -static void -call_accept_ready (MMBaseModem *modem, - GAsyncResult *res, - GTask *task) -{ - GError *error = NULL; - const gchar *response; - - response = mm_base_modem_at_command_finish (modem, res, &error); - - /* check response for error */ - if (response && response[0]) - g_set_error (&error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, - "Couldn't accept the call: Unhandled response '%s'", response); - - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); - g_object_unref (task); -} - -static void -call_accept (MMBaseCall *self, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - task = g_task_new (self, NULL, callback, user_data); - mm_base_modem_at_command (self->priv->modem, - "ATA", - 2, - FALSE, - (GAsyncReadyCallback)call_accept_ready, - task); -} - -/*****************************************************************************/ -/* Deflect the call */ - -static gboolean -call_deflect_finish (MMBaseCall *self, - GAsyncResult *res, - GError **error) -{ - return g_task_propagate_boolean (G_TASK (res), error); -} - -static void -call_deflect_ready (MMBaseModem *modem, - GAsyncResult *res, - GTask *task) -{ - GError *error = NULL; - - mm_base_modem_at_command_finish (modem, res, &error); - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); - g_object_unref (task); -} - -static void -call_deflect (MMBaseCall *self, - const gchar *number, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - gchar *cmd; - - task = g_task_new (self, NULL, callback, user_data); - - cmd = g_strdup_printf ("+CTFR=%s", number); - mm_base_modem_at_command (self->priv->modem, - cmd, - 20, - FALSE, - (GAsyncReadyCallback)call_deflect_ready, - task); - g_free (cmd); -} - -/*****************************************************************************/ -/* Hangup the call */ - -static gboolean -call_hangup_finish (MMBaseCall *self, - GAsyncResult *res, - GError **error) -{ - return g_task_propagate_boolean (G_TASK (res), error); -} - -static void -chup_ready (MMBaseModem *modem, - GAsyncResult *res, - GTask *task) -{ - GError *error = NULL; - - mm_base_modem_at_command_finish (modem, res, &error); - if (error) - g_task_return_error (task, error); - else - g_task_return_boolean (task, TRUE); - g_object_unref (task); -} - -static void -chup_fallback (GTask *task) -{ - MMBaseCall *self; - - self = g_task_get_source_object (task); - mm_base_modem_at_command (self->priv->modem, - "+CHUP", - 2, - FALSE, - (GAsyncReadyCallback)chup_ready, - task); -} - -static void -chld_hangup_ready (MMBaseModem *modem, - GAsyncResult *res, - GTask *task) -{ - MMBaseCall *self; - GError *error = NULL; - - self = g_task_get_source_object (task); - - mm_base_modem_at_command_finish (modem, res, &error); - if (error) { - mm_obj_warn (self, "couldn't hangup single call with call id '%u': %s", - self->priv->index, error->message); - g_error_free (error); - chup_fallback (task); - return; - } - - g_task_return_boolean (task, TRUE); - g_object_unref (task); -} - -static void -call_hangup (MMBaseCall *self, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - - task = g_task_new (self, NULL, callback, user_data); - - /* Try to hangup the single call id */ - if (self->priv->index) { - gchar *cmd; - - cmd = g_strdup_printf ("+CHLD=1%u", self->priv->index); - mm_base_modem_at_command (self->priv->modem, - cmd, - 2, - FALSE, - (GAsyncReadyCallback)chld_hangup_ready, - task); - g_free (cmd); - return; - } - - /* otherwise terminate all */ - chup_fallback (task); -} - -/*****************************************************************************/ -/* Send DTMF tone to call */ - -static gboolean -call_send_dtmf_finish (MMBaseCall *self, - GAsyncResult *res, - GError **error) -{ - return g_task_propagate_boolean (G_TASK (res), error); -} - -static void -call_send_dtmf_ready (MMBaseModem *modem, - GAsyncResult *res, - GTask *task) -{ - MMBaseCall *self; - GError *error = NULL; - - self = g_task_get_source_object (task); - - mm_base_modem_at_command_finish (modem, res, &error); - if (error) { - mm_obj_dbg (self, "couldn't send dtmf: %s", error->message); - g_task_return_error (task, error); - g_object_unref (task); - return; - } - - 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) -{ - GTask *task; - gchar *cmd; - - task = g_task_new (self, NULL, callback, user_data); - - cmd = g_strdup_printf ("AT+VTS=%c", dtmf[0]); - mm_base_modem_at_command (self->priv->modem, - cmd, - 3, - FALSE, - (GAsyncReadyCallback)call_send_dtmf_ready, - task); - - g_free (cmd); -} - -/*****************************************************************************/ static gchar * log_object_build_id (MMLogObject *_self) @@ -1309,28 +1321,6 @@ log_object_build_id (MMLogObject *_self) /*****************************************************************************/ -MMBaseCall * -mm_base_call_new (MMBaseModem *modem, - GObject *bind_to, - MMCallDirection direction, - const gchar *number, - gboolean skip_incoming_timeout, - gboolean supports_dialing_to_ringing, - gboolean supports_ringing_to_active) -{ - return MM_BASE_CALL (g_object_new (MM_TYPE_BASE_CALL, - MM_BASE_CALL_MODEM, modem, - MM_BIND_TO, bind_to, - "direction", direction, - "number", number, - MM_BASE_CALL_SKIP_INCOMING_TIMEOUT, skip_incoming_timeout, - MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, supports_dialing_to_ringing, - MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE, supports_ringing_to_active, - NULL)); -} - -/*****************************************************************************/ - static void set_property (GObject *object, guint prop_id, @@ -1365,9 +1355,9 @@ set_property (GObject *object, self->priv->bind_to = g_value_dup_object (value); mm_bind_to (MM_BIND (self), MM_BASE_CALL_CONNECTION, self->priv->bind_to); break; - case PROP_MODEM: - g_clear_object (&self->priv->modem); - self->priv->modem = g_value_dup_object (value); + case PROP_IFACE_MODEM_VOICE: + g_clear_object (&self->priv->iface); + self->priv->iface = g_value_dup_object (value); break; case PROP_SKIP_INCOMING_TIMEOUT: self->priv->skip_incoming_timeout = g_value_get_boolean (value); @@ -1402,8 +1392,8 @@ get_property (GObject *object, case PROP_BIND_TO: g_value_set_object (value, self->priv->bind_to); break; - case PROP_MODEM: - g_value_set_object (value, self->priv->modem); + case PROP_IFACE_MODEM_VOICE: + g_value_set_object (value, self->priv->iface); break; case PROP_SKIP_INCOMING_TIMEOUT: g_value_set_boolean (value, self->priv->skip_incoming_timeout); @@ -1434,6 +1424,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 @@ -1441,6 +1433,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); @@ -1466,11 +1461,14 @@ dispose (GObject *object) g_clear_object (&self->priv->connection); } - g_clear_object (&self->priv->modem); + g_clear_object (&self->priv->iface); g_clear_object (&self->priv->bind_to); 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); } @@ -1498,17 +1496,6 @@ mm_base_call_class_init (MMBaseCallClass *klass) object_class->finalize = finalize; object_class->dispose = dispose; - klass->start = call_start; - klass->start_finish = call_start_finish; - klass->accept = call_accept; - klass->accept_finish = call_accept_finish; - klass->deflect = call_deflect; - klass->deflect_finish = call_deflect_finish; - klass->hangup = call_hangup; - klass->hangup_finish = call_hangup_finish; - klass->send_dtmf = call_send_dtmf; - klass->send_dtmf_finish = call_send_dtmf_finish; - properties[PROP_CONNECTION] = g_param_spec_object (MM_BASE_CALL_CONNECTION, "Connection", @@ -1527,13 +1514,13 @@ mm_base_call_class_init (MMBaseCallClass *klass) g_object_class_override_property (object_class, PROP_BIND_TO, MM_BIND_TO); - properties[PROP_MODEM] = - g_param_spec_object (MM_BASE_CALL_MODEM, - "Modem", - "The Modem which owns this call", - MM_TYPE_BASE_MODEM, + properties[PROP_IFACE_MODEM_VOICE] = + g_param_spec_object (MM_BASE_CALL_IFACE_MODEM_VOICE, + "Modem Voice Interface", + "The Modem voice interface which owns this call", + MM_TYPE_IFACE_MODEM_VOICE, G_PARAM_READWRITE); - g_object_class_install_property (object_class, PROP_MODEM, properties[PROP_MODEM]); + g_object_class_install_property (object_class, PROP_IFACE_MODEM_VOICE, properties[PROP_IFACE_MODEM_VOICE]); properties[PROP_SKIP_INCOMING_TIMEOUT] = g_param_spec_boolean (MM_BASE_CALL_SKIP_INCOMING_TIMEOUT, diff --git a/src/mm-base-call.h b/src/mm-base-call.h index fc69bf2f..a9a6efc3 100644 --- a/src/mm-base-call.h +++ b/src/mm-base-call.h @@ -23,8 +23,8 @@ #define _LIBMM_INSIDE_MM #include <libmm-glib.h> -#include "mm-base-modem.h" #include "mm-call-audio-format.h" +#include "mm-port.h" #define MM_TYPE_BASE_CALL (mm_base_call_get_type ()) #define MM_BASE_CALL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_BASE_CALL, MMBaseCall)) @@ -37,9 +37,13 @@ typedef struct _MMBaseCall MMBaseCall; typedef struct _MMBaseCallClass MMBaseCallClass; typedef struct _MMBaseCallPrivate MMBaseCallPrivate; +#define MM_CALL_DIRECTION "direction" +#define MM_CALL_NUMBER "number" +#define MM_CALL_DTMF_TONE_DURATION "dtmf-tone-duration" + #define MM_BASE_CALL_PATH "call-path" #define MM_BASE_CALL_CONNECTION "call-connection" -#define MM_BASE_CALL_MODEM "call-modem" +#define MM_BASE_CALL_IFACE_MODEM_VOICE "call-iface-modem-voice" #define MM_BASE_CALL_SKIP_INCOMING_TIMEOUT "call-skip-incoming-timeout" #define MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING "call-supports-dialing-to-ringing" #define MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE "call-supports-ringing-to-active" @@ -86,28 +90,28 @@ 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); + /* Returns the number of DTMF characters sent, or < 1 on error */ + gssize (* 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); G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMBaseCall, g_object_unref) -/* This one can be overridden by plugins */ -MMBaseCall *mm_base_call_new (MMBaseModem *modem, - GObject *bind_to, - MMCallDirection direction, - const gchar *number, - gboolean skip_incoming_timeout, - gboolean supports_dialing_to_ringing, - gboolean supports_ringing_to_active); - void mm_base_call_export (MMBaseCall *self); void mm_base_call_unexport (MMBaseCall *self); @@ -138,4 +142,9 @@ void mm_base_call_received_dtmf (MMBaseCall *self, void mm_base_call_incoming_refresh (MMBaseCall *self); +guint mm_base_call_get_dtmf_tone_duration (MMBaseCall *self); + +void mm_base_call_set_dtmf_tone_duration (MMBaseCall *self, + guint duration_ms); + #endif /* MM_BASE_CALL_H */ diff --git a/src/mm-broadband-modem-qmi.c b/src/mm-broadband-modem-qmi.c index 1b9cf04f..80cf157d 100644 --- a/src/mm-broadband-modem-qmi.c +++ b/src/mm-broadband-modem-qmi.c @@ -12190,11 +12190,13 @@ modem_voice_load_call_list (MMIfaceModemVoice *self, static MMBaseCall * modem_voice_create_call (MMIfaceModemVoice *self, MMCallDirection direction, - const gchar *number) + const gchar *number, + const guint dtmf_tone_duration) { return mm_call_qmi_new (MM_BASE_MODEM (self), direction, - number); + number, + dtmf_tone_duration); } /*****************************************************************************/ diff --git a/src/mm-broadband-modem.c b/src/mm-broadband-modem.c index 3d02553d..27ac8f56 100644 --- a/src/mm-broadband-modem.c +++ b/src/mm-broadband-modem.c @@ -50,6 +50,7 @@ #include "mm-bearer-list.h" #include "mm-cbm-list.h" #include "mm-cbm-part.h" +#include "mm-call-at.h" #include "mm-sms-list.h" #include "mm-sms-part-3gpp.h" #include "mm-sms-at.h" @@ -8898,20 +8899,22 @@ modem_voice_disable_unsolicited_events (MMIfaceModemVoice *self, static MMBaseCall * modem_voice_create_call (MMIfaceModemVoice *_self, MMCallDirection direction, - const gchar *number) + const gchar *number, + const guint dtmf_tone_duration) { MMBroadbandModem *self = MM_BROADBAND_MODEM (_self); - return mm_base_call_new (MM_BASE_MODEM (self), - G_OBJECT (self), - direction, - number, - /* If +CLCC is supported, we want no incoming timeout. - * Also, we're able to support detailed call state updates without - * additional vendor-specific commands. */ - self->priv->clcc_supported, /* skip incoming timeout */ - self->priv->clcc_supported, /* dialing->ringing supported */ - self->priv->clcc_supported); /* ringing->active supported */ + return mm_call_at_new (MM_BASE_MODEM (self), + G_OBJECT (self), + direction, + number, + dtmf_tone_duration, + /* If +CLCC is supported, we want no incoming timeout. + * Also, we're able to support detailed call state updates without + * additional vendor-specific commands. */ + self->priv->clcc_supported, /* skip incoming timeout */ + self->priv->clcc_supported, /* dialing->ringing supported */ + self->priv->clcc_supported); /* ringing->active supported */ } /*****************************************************************************/ diff --git a/src/mm-call-at.c b/src/mm-call-at.c new file mode 100644 index 00000000..3a1c3263 --- /dev/null +++ b/src/mm-call-at.c @@ -0,0 +1,474 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it> + * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es> + * Copyright (C) 2019 Purism SPC + */ + +#include <config.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <ctype.h> + +#include <ModemManager.h> +#define _LIBMM_INSIDE_MM +#include <libmm-glib.h> + +#include "mm-call-at.h" +#include "mm-base-modem-at.h" +#include "mm-base-modem.h" +#include "mm-modem-helpers.h" +#include "mm-error-helpers.h" +#include "mm-log-object.h" +#include "mm-bind.h" + +G_DEFINE_TYPE (MMCallAt, mm_call_at, MM_TYPE_BASE_CALL) + +typedef enum { + FEATURE_SUPPORT_UNKNOWN, + FEATURE_NOT_SUPPORTED, + FEATURE_SUPPORTED, +} FeatureSupport; + +struct _MMCallAtPrivate { + /* The modem which owns this call */ + MMBaseModem *modem; + + /* DTMF support */ + FeatureSupport vtd_supported; +}; + +/*****************************************************************************/ +/* Start the CALL */ + +static gboolean +call_start_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static void +call_start_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + GError *error = NULL; + const gchar *response = NULL; + + response = mm_base_modem_at_command_finish (modem, res, &error); + + /* check response for error */ + if (response && response[0]) + error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Couldn't start the call: Unhandled response '%s'", response); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +call_start (MMBaseCall *_self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMCallAt *self = MM_CALL_AT (_self); + GError *error = NULL; + GTask *task; + gchar *cmd; + MMIfacePortAt *port; + + task = g_task_new (self, NULL, callback, user_data); + + port = mm_base_modem_peek_best_at_port (self->priv->modem, &error); + if (!port) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + cmd = g_strdup_printf ("ATD%s;", mm_gdbus_call_get_number (MM_GDBUS_CALL (self))); + mm_base_modem_at_command_full (self->priv->modem, + port, + cmd, + 90, + FALSE, /* no cached */ + FALSE, /* no raw */ + cancellable, + (GAsyncReadyCallback)call_start_ready, + task); + g_free (cmd); +} + +/*****************************************************************************/ +/* Accept the call */ + +static gboolean +call_accept_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static void +call_accept_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + GError *error = NULL; + const gchar *response; + + response = mm_base_modem_at_command_finish (modem, res, &error); + + /* check response for error */ + if (response && response[0]) + g_set_error (&error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Couldn't accept the call: Unhandled response '%s'", response); + + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +call_accept (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMCallAt *self = MM_CALL_AT (_self); + GTask *task; + + task = g_task_new (self, NULL, callback, user_data); + mm_base_modem_at_command (self->priv->modem, + "ATA", + 2, + FALSE, + (GAsyncReadyCallback)call_accept_ready, + task); +} + +/*****************************************************************************/ +/* Deflect the call */ + +static gboolean +call_deflect_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static void +call_deflect_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + GError *error = NULL; + + mm_base_modem_at_command_finish (modem, res, &error); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +call_deflect (MMBaseCall *_self, + const gchar *number, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMCallAt *self = MM_CALL_AT (_self); + GTask *task; + gchar *cmd; + + task = g_task_new (self, NULL, callback, user_data); + + cmd = g_strdup_printf ("+CTFR=%s", number); + mm_base_modem_at_command (self->priv->modem, + cmd, + 20, + FALSE, + (GAsyncReadyCallback)call_deflect_ready, + task); + g_free (cmd); +} + +/*****************************************************************************/ +/* Hangup the call */ + +static gboolean +call_hangup_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static void +chup_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + GError *error = NULL; + + mm_base_modem_at_command_finish (modem, res, &error); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +chup_fallback (GTask *task) +{ + MMCallAt *self; + + self = MM_CALL_AT (g_task_get_source_object (task)); + mm_base_modem_at_command (self->priv->modem, + "+CHUP", + 2, + FALSE, + (GAsyncReadyCallback)chup_ready, + task); +} + +static void +chld_hangup_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + MMBaseCall *self; + GError *error = NULL; + + self = g_task_get_source_object (task); + + mm_base_modem_at_command_finish (modem, res, &error); + if (error) { + mm_obj_warn (self, "couldn't hangup single call with call id '%u': %s", + mm_base_call_get_index (MM_BASE_CALL (self)), error->message); + g_error_free (error); + chup_fallback (task); + return; + } + + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +call_hangup (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMCallAt *self = MM_CALL_AT (_self); + GTask *task; + guint index; + + task = g_task_new (self, NULL, callback, user_data); + + /* Try to hangup the single call id */ + index = mm_base_call_get_index (MM_BASE_CALL (self)); + if (index) { + gchar *cmd; + + cmd = g_strdup_printf ("+CHLD=1%u", index); + mm_base_modem_at_command (self->priv->modem, + cmd, + 2, + FALSE, + (GAsyncReadyCallback)chld_hangup_ready, + task); + g_free (cmd); + return; + } + + /* otherwise terminate all */ + chup_fallback (task); +} + +/*****************************************************************************/ +/* Send DTMF tone to call */ + +static gssize +call_send_dtmf_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +static void +call_send_dtmf_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + MMBaseCall *self; + GError *error = NULL; + + self = g_task_get_source_object (task); + + mm_base_modem_at_command_finish (modem, res, &error); + if (error) { + mm_obj_dbg (self, "couldn't send dtmf: %s", error->message); + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + /* We sent one character */ + g_task_return_int (task, 1); + g_object_unref (task); +} + +static void +send_dtmf_digit (MMCallAt *self, + GTask *task, + const gchar dtmf_digit) +{ + g_autofree gchar *cmd = NULL; + + cmd = g_strdup_printf ("AT+VTS=%c", dtmf_digit); + mm_base_modem_at_command (self->priv->modem, + cmd, + 3, + FALSE, + (GAsyncReadyCallback)call_send_dtmf_ready, + task); +} + +static void +call_dtmf_vtd_ready (MMBaseModem *modem, + GAsyncResult *res, + GTask *task) +{ + MMCallAt *self; + g_autoptr(GError) error = NULL; + gchar dtmf_digit; + + self = g_task_get_source_object (task); + + mm_base_modem_at_command_finish (modem, res, &error); + self->priv->vtd_supported = error ? FEATURE_NOT_SUPPORTED : FEATURE_SUPPORTED; + + dtmf_digit = (gchar) GPOINTER_TO_UINT (g_task_get_task_data (task)); + send_dtmf_digit (self, task, dtmf_digit); +} + +static void +call_send_dtmf (MMBaseCall *_self, + const gchar *dtmf, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMCallAt *self = MM_CALL_AT (_self); + GTask *task; + g_autofree gchar *cmd = NULL; + + task = g_task_new (self, NULL, callback, user_data); + + if (self->priv->vtd_supported == FEATURE_NOT_SUPPORTED) { + send_dtmf_digit (self, task, dtmf[0]); + return; + } + + g_task_set_task_data (task, GUINT_TO_POINTER (dtmf[0]), NULL); + + /* Otherwise try to set duration */ + cmd = g_strdup_printf ("AT+VTD=%u", mm_base_call_get_dtmf_tone_duration (_self)); + mm_base_modem_at_command (self->priv->modem, + cmd, + 3, + FALSE, + (GAsyncReadyCallback)call_dtmf_vtd_ready, + task); +} + +/*****************************************************************************/ + +MMBaseCall * +mm_call_at_new (MMBaseModem *modem, + GObject *bind_to, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration, + gboolean skip_incoming_timeout, + gboolean supports_dialing_to_ringing, + gboolean supports_ringing_to_active) +{ + MMBaseCall *call; + + call = MM_BASE_CALL (g_object_new (MM_TYPE_CALL_AT, + MM_BASE_CALL_IFACE_MODEM_VOICE, modem, + MM_BIND_TO, bind_to, + MM_CALL_DIRECTION, direction, + MM_CALL_NUMBER, number, + MM_CALL_DTMF_TONE_DURATION, dtmf_tone_duration, + MM_BASE_CALL_SKIP_INCOMING_TIMEOUT, skip_incoming_timeout, + MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, supports_dialing_to_ringing, + MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE, supports_ringing_to_active, + NULL)); + MM_CALL_AT (call)->priv->modem = g_object_ref (modem); + return call; +} + +static void +mm_call_at_init (MMCallAt *self) +{ + /* Initialize private data */ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_CALL_AT, MMCallAtPrivate); +} + +static void +dispose (GObject *object) +{ + MMCallAt *self = MM_CALL_AT (object); + + g_clear_object (&self->priv->modem); + + G_OBJECT_CLASS (mm_call_at_parent_class)->dispose (object); +} + +static void +mm_call_at_class_init (MMCallAtClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + MMBaseCallClass *base_call_class = MM_BASE_CALL_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMCallAtPrivate)); + + object_class->dispose = dispose; + + base_call_class->start = call_start; + base_call_class->start_finish = call_start_finish; + base_call_class->accept = call_accept; + base_call_class->accept_finish = call_accept_finish; + base_call_class->deflect = call_deflect; + base_call_class->deflect_finish = call_deflect_finish; + base_call_class->hangup = call_hangup; + 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; +} diff --git a/src/mm-call-at.h b/src/mm-call-at.h new file mode 100644 index 00000000..64aed048 --- /dev/null +++ b/src/mm-call-at.h @@ -0,0 +1,61 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2015 Riccardo Vangelisti <riccardo.vangelisti@sadel.it> + * Copyright (C) 2019 Purism SPC + */ + +#ifndef MM_CALL_AT_H +#define MM_CALL_AT_H + +#include <glib.h> +#include <glib-object.h> + +#define _LIBMM_INSIDE_MM +#include <libmm-glib.h> + +#include "mm-base-call.h" +#include "mm-base-modem.h" + +#define MM_TYPE_CALL_AT (mm_call_at_get_type ()) +#define MM_CALL_AT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_CALL_AT, MMCallAt)) +#define MM_CALL_AT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_CALL_AT, MMCallAtClass)) +#define MM_IS_CALL_AT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_CALL_AT)) +#define MM_IS_CALL_AT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_CALL_AT)) +#define MM_CALL_AT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_CALL_AT, MMCallAtClass)) + +typedef struct _MMCallAt MMCallAt; +typedef struct _MMCallAtClass MMCallAtClass; +typedef struct _MMCallAtPrivate MMCallAtPrivate; + +struct _MMCallAt { + MMBaseCall parent; + MMCallAtPrivate *priv; +}; + +struct _MMCallAtClass { + MMBaseCallClass parent; +}; + +GType mm_call_at_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMCallAt, g_object_unref) + +MMBaseCall *mm_call_at_new (MMBaseModem *modem, + GObject *bind_to, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration, + gboolean skip_incoming_timeout, + gboolean supports_dialing_to_ringing, + gboolean supports_ringing_to_active); + +#endif /* MM_CALL_AT_H */ diff --git a/src/mm-call-list.c b/src/mm-call-list.c index 66e8382c..5e303390 100644 --- a/src/mm-call-list.c +++ b/src/mm-call-list.c @@ -24,7 +24,6 @@ #define _LIBMM_INSIDE_MM #include <libmm-glib.h> -#include "mm-iface-modem-messaging.h" #include "mm-call-list.h" #include "mm-base-call.h" #include "mm-log.h" @@ -32,13 +31,6 @@ G_DEFINE_TYPE (MMCallList, mm_call_list, G_TYPE_OBJECT) enum { - PROP_0, - PROP_MODEM, - PROP_LAST -}; -static GParamSpec *properties[PROP_LAST]; - -enum { SIGNAL_CALL_ADDED, SIGNAL_CALL_DELETED, SIGNAL_LAST @@ -46,8 +38,6 @@ enum { static guint signals[SIGNAL_LAST]; struct _MMCallListPrivate { - /* The owner modem */ - MMBaseModem *modem; /* List of call objects */ GList *list; }; @@ -213,49 +203,10 @@ mm_call_list_add_call (MMCallList *self, /*****************************************************************************/ MMCallList * -mm_call_list_new (MMBaseModem *modem) +mm_call_list_new (void) { /* Create the object */ - return g_object_new (MM_TYPE_CALL_LIST, - MM_CALL_LIST_MODEM, modem, - NULL); -} - -static void -set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) -{ - MMCallList *self = MM_CALL_LIST (object); - - switch (prop_id) { - case PROP_MODEM: - g_clear_object (&self->priv->modem); - self->priv->modem = g_value_dup_object (value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) -{ - MMCallList *self = MM_CALL_LIST (object); - - switch (prop_id) { - case PROP_MODEM: - g_value_set_object (value, self->priv->modem); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } + return g_object_new (MM_TYPE_CALL_LIST, NULL); } static void @@ -272,7 +223,6 @@ dispose (GObject *object) { MMCallList *self = MM_CALL_LIST (object); - g_clear_object (&self->priv->modem); g_list_free_full (self->priv->list, g_object_unref); self->priv->list = NULL; @@ -287,19 +237,8 @@ mm_call_list_class_init (MMCallListClass *klass) g_type_class_add_private (object_class, sizeof (MMCallListPrivate)); /* Virtual methods */ - object_class->get_property = get_property; - object_class->set_property = set_property; object_class->dispose = dispose; - /* Properties */ - properties[PROP_MODEM] = - g_param_spec_object (MM_CALL_LIST_MODEM, - "Modem", - "The Modem which owns this CALL list", - MM_TYPE_BASE_MODEM, - G_PARAM_READWRITE); - g_object_class_install_property (object_class, PROP_MODEM, properties[PROP_MODEM]); - /* Signals */ signals[SIGNAL_CALL_ADDED] = g_signal_new (MM_CALL_ADDED, diff --git a/src/mm-call-list.h b/src/mm-call-list.h index f77a5b37..c1e86c0a 100644 --- a/src/mm-call-list.h +++ b/src/mm-call-list.h @@ -19,7 +19,6 @@ #include <glib.h> #include <glib-object.h> -#include "mm-base-modem.h" #include "mm-base-call.h" #define MM_TYPE_CALL_LIST (mm_call_list_get_type ()) @@ -33,8 +32,6 @@ typedef struct _MMCallList MMCallList; typedef struct _MMCallListClass MMCallListClass; typedef struct _MMCallListPrivate MMCallListPrivate; -#define MM_CALL_LIST_MODEM "call-list-modem" - #define MM_CALL_ADDED "call-added" #define MM_CALL_DELETED "call-deleted" @@ -57,7 +54,7 @@ struct _MMCallListClass { GType mm_call_list_get_type (void); G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMCallList, g_object_unref) -MMCallList *mm_call_list_new (MMBaseModem *modem); +MMCallList *mm_call_list_new (void); GStrv mm_call_list_get_paths (MMCallList *self); guint mm_call_list_get_count (MMCallList *self); diff --git a/src/mm-call-qmi.c b/src/mm-call-qmi.c index 441be147..44ab1f37 100644 --- a/src/mm-call-qmi.c +++ b/src/mm-call-qmi.c @@ -35,6 +35,11 @@ G_DEFINE_TYPE (MMCallQmi, mm_call_qmi, MM_TYPE_BASE_CALL) +struct _MMCallQmiPrivate { + /* The modem which owns this call */ + MMBaseModem *modem; +}; + /*****************************************************************************/ static gboolean @@ -48,12 +53,7 @@ ensure_qmi_client (MMCallQmi *self, QmiClient *client; MMPortQmi *port; - g_object_get (self, - MM_BASE_CALL_MODEM, &modem, - NULL); - g_assert (MM_IS_BASE_MODEM (modem)); - - port = mm_broadband_modem_qmi_peek_port_qmi (MM_BROADBAND_MODEM_QMI (modem)); + port = mm_broadband_modem_qmi_peek_port_qmi (MM_BROADBAND_MODEM_QMI (self->priv->modem)); g_object_unref (modem); if (!port) { @@ -331,22 +331,41 @@ call_hangup (MMBaseCall *self, } /*****************************************************************************/ -/* Send DTMF character */ - -typedef struct { - QmiClient *client; - guint8 call_id; -} SendDtmfContext; +/* DTMF handling */ -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 gssize +call_send_dtmf_finish (MMBaseCall *call, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); } static void @@ -402,80 +445,55 @@ voice_start_continuous_dtmf_ready (QmiClientVoice *client, GAsyncResult *res, GTask *task) { - g_autoptr(QmiMessageVoiceStartContinuousDtmfOutput) output = NULL; - GError *error = NULL; + 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_int (task, 1); } - - /* Disable DTMF press after 500 ms */ - g_timeout_add (500, (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, @@ -489,29 +507,52 @@ call_send_dtmf (MMBaseCall *self, MMBaseCall * mm_call_qmi_new (MMBaseModem *modem, MMCallDirection direction, - const gchar *number) + const gchar *number, + const guint dtmf_tone_duration) { - return MM_BASE_CALL (g_object_new (MM_TYPE_CALL_QMI, - MM_BASE_CALL_MODEM, modem, - MM_BIND_TO, G_OBJECT (modem), - "direction", direction, - "number", number, + MMBaseCall *call; + + call = MM_BASE_CALL (g_object_new (MM_TYPE_CALL_QMI, + MM_BASE_CALL_IFACE_MODEM_VOICE, modem, + MM_BIND_TO, modem, + MM_CALL_DIRECTION, direction, + MM_CALL_NUMBER, number, + MM_CALL_DTMF_TONE_DURATION, dtmf_tone_duration, MM_BASE_CALL_SKIP_INCOMING_TIMEOUT, TRUE, MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, TRUE, MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE, TRUE, NULL)); + MM_CALL_QMI (call)->priv->modem = g_object_ref (modem); + return call; } static void mm_call_qmi_init (MMCallQmi *self) { + /* Initialize private data */ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_CALL_QMI, MMCallQmiPrivate); +} + +static void +dispose (GObject *object) +{ + MMCallQmi *self = MM_CALL_QMI (object); + + g_clear_object (&self->priv->modem); + + G_OBJECT_CLASS (mm_call_qmi_parent_class)->dispose (object); } static void mm_call_qmi_class_init (MMCallQmiClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS (klass); MMBaseCallClass *base_call_class = MM_BASE_CALL_CLASS (klass); + g_type_class_add_private (object_class, sizeof (MMCallQmiPrivate)); + + object_class->dispose = dispose; + base_call_class->start = call_start; base_call_class->start_finish = call_start_finish; base_call_class->accept = call_accept; @@ -520,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; } diff --git a/src/mm-call-qmi.h b/src/mm-call-qmi.h index 78402539..f99e7e10 100644 --- a/src/mm-call-qmi.h +++ b/src/mm-call-qmi.h @@ -33,9 +33,11 @@ typedef struct _MMCallQmi MMCallQmi; typedef struct _MMCallQmiClass MMCallQmiClass; +typedef struct _MMCallQmiPrivate MMCallQmiPrivate; struct _MMCallQmi { MMBaseCall parent; + MMCallQmiPrivate *priv; }; struct _MMCallQmiClass { @@ -47,6 +49,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMCallQmi, g_object_unref) MMBaseCall *mm_call_qmi_new (MMBaseModem *modem, MMCallDirection direction, - const gchar *number); + const gchar *number, + const guint dtmf_tone_duration); #endif /* MM_CALL_QMI_H */ diff --git a/src/mm-iface-modem-voice.c b/src/mm-iface-modem-voice.c index bca51c83..0bba2a34 100644 --- a/src/mm-iface-modem-voice.c +++ b/src/mm-iface-modem-voice.c @@ -161,7 +161,7 @@ create_incoming_call (MMIfaceModemVoice *self, g_assert (MM_IFACE_MODEM_VOICE_GET_IFACE (self)->create_call != NULL); - call = MM_IFACE_MODEM_VOICE_GET_IFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number); + call = MM_IFACE_MODEM_VOICE_GET_IFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number, 0); update_audio_settings_in_call (self, call); return call; } @@ -173,6 +173,7 @@ create_outgoing_call_from_properties (MMIfaceModemVoice *self, { MMBaseCall *call; const gchar *number; + guint dtmf_tone_duration; /* Don't create CALL from properties if either number is missing */ number = mm_call_properties_get_number (properties) ; @@ -184,9 +185,14 @@ create_outgoing_call_from_properties (MMIfaceModemVoice *self, return NULL; } + dtmf_tone_duration = mm_call_properties_get_dtmf_tone_duration (properties) ; + /* Create a call object as defined by the interface */ g_assert (MM_IFACE_MODEM_VOICE_GET_IFACE (self)->create_call != NULL); - call = MM_IFACE_MODEM_VOICE_GET_IFACE (self)->create_call (self, MM_CALL_DIRECTION_OUTGOING, number); + call = MM_IFACE_MODEM_VOICE_GET_IFACE (self)->create_call (self, + MM_CALL_DIRECTION_OUTGOING, + number, + dtmf_tone_duration); update_audio_settings_in_call (self, call); return call; } @@ -3012,7 +3018,7 @@ interface_initialization_step (GTask *task) /* Create a new call list if not already available (this initialization * may be called multiple times) */ if (!list) { - list = mm_call_list_new (MM_BASE_MODEM (self)); + list = mm_call_list_new (); g_object_set (self, MM_IFACE_MODEM_VOICE_CALL_LIST, list, NULL); diff --git a/src/mm-iface-modem-voice.h b/src/mm-iface-modem-voice.h index 2d8b5160..716988d5 100644 --- a/src/mm-iface-modem-voice.h +++ b/src/mm-iface-modem-voice.h @@ -123,7 +123,8 @@ struct _MMIfaceModemVoiceInterface { /* Create call objects */ MMBaseCall * (* create_call) (MMIfaceModemVoice *self, MMCallDirection direction, - const gchar *number); + const gchar *number, + const guint dtmf_tone_duration); /* Hold and accept */ void (* hold_and_accept) (MMIfaceModemVoice *self, diff --git a/src/mm-modem-helpers.c b/src/mm-modem-helpers.c index a79ca956..5cbf0836 100644 --- a/src/mm-modem-helpers.c +++ b/src/mm-modem-helpers.c @@ -5871,3 +5871,53 @@ mm_parse_cpin_response (const gchar *response, return MM_MODEM_LOCK_UNKNOWN; } + +/*****************************************************************************/ + +guint +mm_dtmf_duration_normalize (guint duration_ms) +{ + /* Default to 500ms */ + if (duration_ms == 0) + return 500; + + /* round to next highest 100ms */ + if (duration_ms % 100) + duration_ms = ((duration_ms + 100) / 100) * 100; + + return CLAMP (duration_ms, 100, 1000); +} + +GPtrArray * +mm_dtmf_split (const gchar *dtmf) +{ + GPtrArray *array; + const gchar *p = dtmf; + GString *cur = NULL; + + array = g_ptr_array_new (); + + while (*p) { + if (*p == MM_CALL_DTMF_PAUSE_CHAR) { + if (cur) { + g_ptr_array_add (array, g_string_free (cur, FALSE)); + cur = NULL; + } + g_ptr_array_add (array, g_strdup (",")); + } else { + if (!cur) + cur = g_string_new (NULL); + g_string_append_c (cur, *p); + } + p++; + } + if (cur) + g_ptr_array_add (array, g_string_free (cur, FALSE)); + + if (array->len == 0) { + g_ptr_array_free (array, TRUE); + return NULL; + } + + return array; +} diff --git a/src/mm-modem-helpers.h b/src/mm-modem-helpers.h index eb62a551..1e3dbb12 100644 --- a/src/mm-modem-helpers.h +++ b/src/mm-modem-helpers.h @@ -643,4 +643,13 @@ guint mm_string_uint_map_lookup (const MMStringUintMap *map, MMModemLock mm_parse_cpin_response (const gchar *response, gboolean expect_cpin_prefix); +/*****************************************************************************/ + +/* Helper to clamp duration and round to next 100ms */ +guint mm_dtmf_duration_normalize (guint duration_ms); + +#define MM_CALL_DTMF_PAUSE_CHAR ',' + +GPtrArray *mm_dtmf_split (const gchar *dtmf); + #endif /* MM_MODEM_HELPERS_H */ diff --git a/src/mm-test-utils.h b/src/mm-test-utils.h new file mode 100644 index 00000000..f484ff1e --- /dev/null +++ b/src/mm-test-utils.h @@ -0,0 +1,52 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#ifndef MM_TEST_UTILS_H +#define MM_TEST_UTILS_H + +#include <glib.h> +#include <glib-object.h> + +/*****************************************************************************/ + +/* cmp = TRUE (s1 contains s2) or FALSE (s1 does not contain s2) */ +#define mm_assert_strstr(s1, cmp, s2) \ + G_STMT_START { \ + const char *__s1 = (s1), *__s2 = (s2); \ + if (strstr (__s1, __s2) == cmp) ; else \ + g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + #s1 " %s " #s2, \ + __s1, \ + cmp ? "contains" : "does not contain", \ + __s2); \ + } G_STMT_END + +/* Asserts that err's message contains s1 */ +#define mm_assert_error_str(err, s1) \ + G_STMT_START { \ + if (!err || !err->message || !strstr (err->message, s1)) { \ + GString *gstring; \ + gstring = g_string_new ("assertion failed "); \ + if (err) \ + g_string_append_printf (gstring, "%s (%s, %d) did not contain '%s'", \ + err->message, g_quark_to_string (err->domain), err->code, s1); \ + else \ + g_string_append_printf (gstring, "%s is NULL", #err); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, gstring->str); \ + g_string_free (gstring, TRUE); \ + } \ + } G_STMT_END + +#endif /* MM_TEST_UTILS_H */ diff --git a/src/plugins/cinterion/mm-shared-cinterion.c b/src/plugins/cinterion/mm-shared-cinterion.c index 4ae1f503..aa9b6208 100644 --- a/src/plugins/cinterion/mm-shared-cinterion.c +++ b/src/plugins/cinterion/mm-shared-cinterion.c @@ -31,6 +31,7 @@ #include "mm-base-modem-at.h" #include "mm-shared-cinterion.h" #include "mm-modem-helpers-cinterion.h" +#include "mm-call-at.h" G_DEFINE_INTERFACE (MMSharedCinterion, mm_shared_cinterion, MM_TYPE_IFACE_MODEM) @@ -997,7 +998,8 @@ mm_shared_cinterion_enable_location_gathering (MMIfaceModemLocation *self, MMBaseCall * mm_shared_cinterion_create_call (MMIfaceModemVoice *self, MMCallDirection direction, - const gchar *number) + const gchar *number, + const guint dtmf_tone_duration) { Private *priv; @@ -1005,21 +1007,22 @@ mm_shared_cinterion_create_call (MMIfaceModemVoice *self, priv = get_private (MM_SHARED_CINTERION (self)); if (priv->slcc_support == FEATURE_SUPPORTED) { mm_obj_dbg (self, "created new call with ^SLCC support"); - return mm_base_call_new (MM_BASE_MODEM (self), - G_OBJECT (self), - direction, - number, - /* When SLCC is supported we have support for detailed - * call list events via call list report URCs */ - TRUE, /* incoming timeout not required */ - TRUE, /* dialing->ringing supported */ - TRUE); /* ringing->active supported */ + return mm_call_at_new (MM_BASE_MODEM (self), + G_OBJECT (self), + direction, + number, + dtmf_tone_duration, + /* When SLCC is supported we have support for detailed + * call list events via call list report URCs */ + TRUE, /* incoming timeout not required */ + TRUE, /* dialing->ringing supported */ + TRUE); /* ringing->active supported */ } /* otherwise, run parent's generic base call logic */ g_assert (priv->iface_modem_voice_parent); g_assert (priv->iface_modem_voice_parent->create_call); - return priv->iface_modem_voice_parent->create_call (self, direction, number); + return priv->iface_modem_voice_parent->create_call (self, direction, number, dtmf_tone_duration); } /*****************************************************************************/ diff --git a/src/plugins/cinterion/mm-shared-cinterion.h b/src/plugins/cinterion/mm-shared-cinterion.h index f330e328..146ae96a 100644 --- a/src/plugins/cinterion/mm-shared-cinterion.h +++ b/src/plugins/cinterion/mm-shared-cinterion.h @@ -105,7 +105,8 @@ gboolean mm_shared_cinterion_disable_location_gathering_finish (MMI MMBaseCall *mm_shared_cinterion_create_call (MMIfaceModemVoice *self, MMCallDirection direction, - const gchar *number); + const gchar *number, + const guint dtmf_tone_duration); void mm_shared_cinterion_voice_check_support (MMIfaceModemVoice *self, GAsyncReadyCallback callback, diff --git a/src/plugins/huawei/mm-broadband-modem-huawei.c b/src/plugins/huawei/mm-broadband-modem-huawei.c index 5a7f2afc..5de6c4d5 100644 --- a/src/plugins/huawei/mm-broadband-modem-huawei.c +++ b/src/plugins/huawei/mm-broadband-modem-huawei.c @@ -49,6 +49,7 @@ #include "mm-broadband-bearer.h" #include "mm-bearer-list.h" #include "mm-sim-huawei.h" +#include "mm-call-at.h" static void iface_modem_init (MMIfaceModemInterface *iface); static void iface_modem_3gpp_init (MMIfaceModem3gppInterface *iface); @@ -3878,15 +3879,17 @@ modem_voice_disable_unsolicited_events (MMIfaceModemVoice *self, static MMBaseCall * create_call (MMIfaceModemVoice *self, MMCallDirection direction, - const gchar *number) -{ - return mm_base_call_new (MM_BASE_MODEM (self), - G_OBJECT (self), - direction, - number, - TRUE, /* skip_incoming_timeout */ - TRUE, /* supports_dialing_to_ringing */ - TRUE); /* supports_ringing_to_active) */ + const gchar *number, + const guint dtmf_tone_duration) +{ + return mm_call_at_new (MM_BASE_MODEM (self), + G_OBJECT (self), + direction, + number, + dtmf_tone_duration, + TRUE, /* skip_incoming_timeout */ + TRUE, /* supports_dialing_to_ringing */ + TRUE); /* supports_ringing_to_active) */ } /*****************************************************************************/ diff --git a/src/plugins/ublox/mm-broadband-modem-ublox.c b/src/plugins/ublox/mm-broadband-modem-ublox.c index 3c58adda..ab049024 100644 --- a/src/plugins/ublox/mm-broadband-modem-ublox.c +++ b/src/plugins/ublox/mm-broadband-modem-ublox.c @@ -33,6 +33,7 @@ #include "mm-sim-ublox.h" #include "mm-modem-helpers-ublox.h" #include "mm-ublox-enums-types.h" +#include "mm-call-at.h" static void iface_modem_init (MMIfaceModemInterface *iface); static void iface_modem_voice_init (MMIfaceModemVoiceInterface *iface); @@ -1503,15 +1504,17 @@ modem_voice_setup_unsolicited_events (MMIfaceModemVoice *self, static MMBaseCall * create_call (MMIfaceModemVoice *self, MMCallDirection direction, - const gchar *number) -{ - return mm_base_call_new (MM_BASE_MODEM (self), - G_OBJECT (self), - direction, - number, - TRUE, /* skip_incoming_timeout */ - TRUE, /* supports_dialing_to_ringing */ - TRUE); /* supports_ringing_to_active */ + const gchar *number, + const guint dtmf_tone_duration) +{ + return mm_call_at_new (MM_BASE_MODEM (self), + G_OBJECT (self), + direction, + number, + dtmf_tone_duration, + TRUE, /* skip_incoming_timeout */ + TRUE, /* supports_dialing_to_ringing */ + TRUE); /* supports_ringing_to_active */ } /*****************************************************************************/ diff --git a/src/tests/fake-call.c b/src/tests/fake-call.c new file mode 100644 index 00000000..dd77e094 --- /dev/null +++ b/src/tests/fake-call.c @@ -0,0 +1,409 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#include <glib.h> +#include <glib-object.h> +#include <string.h> +#include <stdio.h> +#include <locale.h> + +#define _LIBMM_INSIDE_MM +#include <libmm-glib.h> + +#include "fake-call.h" + +G_DEFINE_TYPE (MMFakeCall, mm_fake_call, MM_TYPE_BASE_CALL) + +/*****************************************************************************/ +/* Start the CALL */ + +static gboolean +call_start_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_start_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->start_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->start_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_start (MMBaseCall *_self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_start_ready, task); +} + +/*****************************************************************************/ +/* Accept the call */ + +static gboolean +call_accept_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_accept_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->accept_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->accept_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_accept (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_accept_ready, task); +} + +/*****************************************************************************/ +/* Deflect the call */ + +static gboolean +call_deflect_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_deflect_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->deflect_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->deflect_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_deflect (MMBaseCall *_self, + const gchar *number, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_deflect_ready, task); +} + +/*****************************************************************************/ +/* Hangup the call */ + +static gboolean +call_hangup_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_hangup_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->hangup_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->hangup_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_hangup (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_hangup_ready, task); +} + +/*****************************************************************************/ +/* Send DTMF tone to call */ + +static gssize +call_send_dtmf_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +static gboolean +call_send_dtmf_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->dtmf_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->dtmf_error_msg); + } else + g_task_return_int (task, self->priv->dtmf_num_accepted); + + self->priv->dtmf_in_send = FALSE; + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_send_dtmf (MMBaseCall *_self, + const gchar *dtmf, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + + if (!self->priv->dtmf_sent) + self->priv->dtmf_sent = g_string_new (""); + self->priv->dtmf_num_accepted = MIN (self->priv->dtmf_accept_len, strlen (dtmf)); + g_string_append_len (self->priv->dtmf_sent, dtmf, self->priv->dtmf_num_accepted); + + self->priv->idle_id = g_idle_add ((GSourceFunc) call_send_dtmf_ready, task); + self->priv->dtmf_in_send = TRUE; +} + +/*****************************************************************************/ +/* Stop DTMF tone */ + +static gboolean +call_stop_dtmf_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_stop_dtmf_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + self->priv->dtmf_stop_called = TRUE; + + if (self->priv->dtmf_stop_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->dtmf_stop_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_stop_dtmf (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + g_assert_false (self->priv->dtmf_in_send); + + task = g_task_new (self, NULL, callback, user_data); + + self->priv->idle_id = g_idle_add ((GSourceFunc) call_stop_dtmf_ready, task); +} + +void +mm_fake_call_enable_dtmf_stop (MMFakeCall *self, + gboolean enable) +{ + MMBaseCallClass *base_call_class = MM_BASE_CALL_GET_CLASS (self); + + base_call_class->stop_dtmf = enable ? call_stop_dtmf : NULL; + base_call_class->stop_dtmf_finish = enable ? call_stop_dtmf_finish : NULL; +} + +/*****************************************************************************/ + +MMFakeCall * +mm_fake_call_new (GDBusConnection *connection, + MMIfaceModemVoice *voice, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration) +{ + MMFakeCall *call; + + call = MM_FAKE_CALL (g_object_new (MM_TYPE_FAKE_CALL, + MM_BASE_CALL_CONNECTION, connection, + MM_BASE_CALL_IFACE_MODEM_VOICE, voice, + MM_CALL_DIRECTION, direction, + MM_CALL_NUMBER, number, + MM_CALL_DTMF_TONE_DURATION, dtmf_tone_duration, + MM_BASE_CALL_SKIP_INCOMING_TIMEOUT, TRUE, + MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, TRUE, + MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE, TRUE, + NULL)); + return call; +} + +static void +mm_fake_call_init (MMFakeCall *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_FAKE_CALL, MMFakeCallPrivate); +} + +static void +dispose (GObject *object) +{ + MMFakeCall *self = MM_FAKE_CALL (object); + + if (self->priv->idle_id) + g_source_remove (self->priv->idle_id); + self->priv->idle_id = 0; + + if (self->priv->dtmf_sent) { + g_string_free (self->priv->dtmf_sent, TRUE); + self->priv->dtmf_sent = NULL; + } + + G_OBJECT_CLASS (mm_fake_call_parent_class)->dispose (object); +} + +static void +finalize (GObject *object) +{ + G_OBJECT_CLASS (mm_fake_call_parent_class)->finalize (object); +} + +static void +mm_fake_call_class_init (MMFakeCallClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + MMBaseCallClass *base_call_class = MM_BASE_CALL_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMFakeCallPrivate)); + + object_class->dispose = dispose; + object_class->finalize = finalize; + + base_call_class->start = call_start; + base_call_class->start_finish = call_start_finish; + base_call_class->accept = call_accept; + base_call_class->accept_finish = call_accept_finish; + base_call_class->deflect = call_deflect; + base_call_class->deflect_finish = call_deflect_finish; + base_call_class->hangup = call_hangup; + 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; +} diff --git a/src/tests/fake-call.h b/src/tests/fake-call.h new file mode 100644 index 00000000..1b035333 --- /dev/null +++ b/src/tests/fake-call.h @@ -0,0 +1,77 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#ifndef MM_FAKE_CALL_H +#define MM_FAKE_CALL_H + +#include <glib.h> +#include <glib-object.h> + +#include "mm-base-call.h" +#include "mm-iface-modem-voice.h" + +#define MM_TYPE_FAKE_CALL (mm_fake_call_get_type ()) +#define MM_FAKE_CALL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_FAKE_CALL, MMFakeCall)) +#define MM_FAKE_CALL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_FAKE_CALL, MMFakeCallClass)) +#define MM_IS_FAKE_CALL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_FAKE_CALL)) +#define MM_IS_FAKE_CALL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_FAKE_CALL)) +#define MM_FAKE_CALL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_FAKE_CALL, MMFakeCallClass)) + +typedef struct _MMFakeCall MMFakeCall; +typedef struct _MMFakeCallClass MMFakeCallClass; +typedef struct _MMFakeCallPrivate MMFakeCallPrivate; + +struct _MMFakeCallPrivate { + const gchar *start_error_msg; + const gchar *accept_error_msg; + const gchar *deflect_error_msg; + const gchar *hangup_error_msg; + + /* DTMF */ + const gchar *dtmf_error_msg; + const gchar *dtmf_stop_error_msg; + /* How many DTMF characters we can accept at a time */ + guint dtmf_accept_len; + /* How many DTMF characters were actually accepted */ + guint dtmf_num_accepted; + GString *dtmf_sent; + gboolean dtmf_in_send; + gboolean dtmf_stop_called; + + guint idle_id; +}; + +struct _MMFakeCall { + MMBaseCall parent; + MMFakeCallPrivate *priv; +}; + +struct _MMFakeCallClass { + MMBaseCallClass parent; +}; + +GType mm_fake_call_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMFakeCall, g_object_unref) + +MMFakeCall *mm_fake_call_new (GDBusConnection *connection, + MMIfaceModemVoice *voice, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration); + +void mm_fake_call_enable_dtmf_stop (MMFakeCall *self, + gboolean enable); + +#endif /* MM_FAKE_CALL_H */ diff --git a/src/tests/fake-modem.c b/src/tests/fake-modem.c new file mode 100644 index 00000000..0d1100bc --- /dev/null +++ b/src/tests/fake-modem.c @@ -0,0 +1,463 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#include <glib.h> +#include <glib-object.h> +#include <string.h> +#include <stdio.h> +#include <locale.h> + +#define _LIBMM_INSIDE_MM +#include <libmm-glib.h> + +#include "fake-modem.h" +#include "mm-iface-modem.h" +#include "mm-iface-modem-voice.h" +#include "mm-call-list.h" +#include "fake-call.h" +#include "mm-bind.h" + +#define MM_FAKE_MODEM_PATH "fake-modem-path" + +static void iface_modem_init (MMIfaceModemInterface *iface); +static void iface_modem_voice_init (MMIfaceModemVoiceInterface *iface); + +G_DEFINE_TYPE_EXTENDED (MMFakeModem, mm_fake_modem, MM_TYPE_BASE_MODEM, 0, + G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM, iface_modem_init) + G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_VOICE, iface_modem_voice_init)) + +enum { + PROP_0, + PROP_CONNECTION, + PROP_PATH, + PROP_MODEM_DBUS_SKELETON, + PROP_MODEM_STATE, + PROP_MODEM_SIM, + PROP_MODEM_SIM_SLOTS, + PROP_MODEM_SIM_HOT_SWAP_SUPPORTED, + PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED, + PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED, + PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED, + PROP_MODEM_CARRIER_CONFIG_MAPPING, + PROP_MODEM_BEARER_LIST, + PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED, + PROP_MODEM_VOICE_DBUS_SKELETON, + PROP_MODEM_VOICE_CALL_LIST, + PROP_LAST +}; + +static GParamSpec *properties[PROP_LAST]; + +struct _MMFakeModemPrivate { + GDBusConnection *connection; + guint dbus_id; + gchar *path; + + GObject *modem_dbus_skeleton; + MMModemState modem_state; + GObject *modem_sim; + GPtrArray *modem_sim_slots; + gboolean sim_hot_swap_supported; + gboolean periodic_signal_check_disabled; + gboolean periodic_access_tech_check_disabled; + gboolean periodic_call_list_check_disabled; + gchar *carrier_config_mapping; + MMBearerList *modem_bearer_list; + gboolean indication_call_list_reload_enabled; + GObject *modem_voice_dbus_skeleton; + MMCallList *modem_voice_call_list; +}; + +/*****************************************************************************/ + +const gchar * +mm_fake_modem_get_path (MMFakeModem *self) +{ + return self->priv->path; +} + +MMCallList * +mm_fake_modem_get_call_list (MMFakeModem *self) +{ + return self->priv->modem_voice_call_list; +} + +gboolean +mm_fake_modem_export_interfaces (MMFakeModem *self, GError **error) +{ + g_assert (self->priv->path); + g_assert (self->priv->connection); + + if (self->priv->modem_dbus_skeleton) { + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->priv->modem_dbus_skeleton), + self->priv->connection, + self->priv->path, + error)) + return FALSE; + } + + if (self->priv->modem_voice_dbus_skeleton) { + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->priv->modem_voice_dbus_skeleton), + self->priv->connection, + self->priv->path, + error)) + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +modem_voice_check_support_finish (MMIfaceModemVoice *self, + GAsyncResult *res, + GError **error) +{ + gboolean foobar; + + foobar = g_task_propagate_boolean (G_TASK (res), error); + return foobar; +} + +static void +modem_voice_check_support (MMIfaceModemVoice *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + task = g_task_new (self, NULL, callback, user_data); + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +/*****************************************************************************/ + +static MMBaseCall * +modem_voice_create_call (MMIfaceModemVoice *_self, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration) +{ + MMFakeModem *self = MM_FAKE_MODEM (_self); + + return MM_BASE_CALL (mm_fake_call_new (self->priv->connection, + _self, + direction, + number, + dtmf_tone_duration)); +} + +/*****************************************************************************/ + +MMFakeModem * +mm_fake_modem_new (GDBusConnection *connection) +{ + return MM_FAKE_MODEM (g_object_new (MM_TYPE_FAKE_MODEM, + MM_BINDABLE_CONNECTION, connection, + NULL)); +} + +static void +set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + switch (prop_id) { + case PROP_PATH: + g_free (self->priv->path); + self->priv->path = g_value_dup_string (value); + break; + case PROP_CONNECTION: + g_clear_object (&self->priv->connection); + self->priv->connection = g_value_dup_object (value); + break; + case PROP_MODEM_DBUS_SKELETON: + g_clear_object (&self->priv->modem_dbus_skeleton); + self->priv->modem_dbus_skeleton = g_value_dup_object (value); + break; + case PROP_MODEM_STATE: + self->priv->modem_state = g_value_get_enum (value); + break; + case PROP_MODEM_SIM: + g_clear_object (&self->priv->modem_sim); + self->priv->modem_sim = g_value_dup_object (value); + break; + case PROP_MODEM_SIM_SLOTS: + g_clear_pointer (&self->priv->modem_sim_slots, g_ptr_array_unref); + self->priv->modem_sim_slots = g_value_dup_boxed (value); + break; + case PROP_MODEM_SIM_HOT_SWAP_SUPPORTED: + self->priv->sim_hot_swap_supported = g_value_get_boolean (value); + break; + case PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED: + self->priv->periodic_signal_check_disabled = g_value_get_boolean (value); + break; + case PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED: + self->priv->periodic_access_tech_check_disabled = g_value_get_boolean (value); + break; + case PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED: + self->priv->periodic_call_list_check_disabled = g_value_get_boolean (value); + break; + case PROP_MODEM_CARRIER_CONFIG_MAPPING: + self->priv->carrier_config_mapping = g_value_dup_string (value); + break; + case PROP_MODEM_BEARER_LIST: + g_clear_object (&self->priv->modem_bearer_list); + self->priv->modem_bearer_list = g_value_dup_object (value); + break; + case PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED: + self->priv->indication_call_list_reload_enabled = g_value_get_boolean (value); + break; + case PROP_MODEM_VOICE_DBUS_SKELETON: + g_clear_object (&self->priv->modem_voice_dbus_skeleton); + self->priv->modem_voice_dbus_skeleton = g_value_dup_object (value); + break; + case PROP_MODEM_VOICE_CALL_LIST: + g_clear_object (&self->priv->modem_voice_call_list); + self->priv->modem_voice_call_list = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + switch (prop_id) { + case PROP_PATH: + g_value_set_string (value, self->priv->path); + break; + case PROP_CONNECTION: + g_value_set_object (value, self->priv->connection); + break; + case PROP_MODEM_DBUS_SKELETON: + g_value_set_object (value, self->priv->modem_dbus_skeleton); + break; + case PROP_MODEM_STATE: + g_value_set_enum (value, self->priv->modem_state); + break; + case PROP_MODEM_SIM: + g_value_set_object (value, self->priv->modem_sim); + break; + case PROP_MODEM_SIM_SLOTS: + g_value_set_boxed (value, self->priv->modem_sim_slots); + break; + case PROP_MODEM_SIM_HOT_SWAP_SUPPORTED: + g_value_set_boolean (value, self->priv->sim_hot_swap_supported); + break; + case PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED: + g_value_set_boolean (value, self->priv->periodic_signal_check_disabled); + break; + case PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED: + g_value_set_boolean (value, self->priv->periodic_access_tech_check_disabled); + break; + case PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED: + g_value_set_boolean (value, self->priv->periodic_call_list_check_disabled); + break; + case PROP_MODEM_CARRIER_CONFIG_MAPPING: + g_value_set_string (value, self->priv->carrier_config_mapping); + break; + case PROP_MODEM_BEARER_LIST: + g_value_set_object (value, self->priv->modem_bearer_list); + break; + case PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED: + g_value_set_boolean (value, self->priv->indication_call_list_reload_enabled); + break; + case PROP_MODEM_VOICE_DBUS_SKELETON: + g_value_set_object (value, self->priv->modem_voice_dbus_skeleton); + break; + case PROP_MODEM_VOICE_CALL_LIST: + g_value_set_object (value, self->priv->modem_voice_call_list); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +mm_fake_modem_init (MMFakeModem *self) +{ + static guint id = 0; + + /* Initialize private data */ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + MM_TYPE_FAKE_MODEM, + MMFakeModemPrivate); + + /* Each modem is given a unique id to build its own DBus path */ + self->priv->dbus_id = id++; + self->priv->path = g_strdup_printf (MM_DBUS_MODEM_PREFIX "/%d", self->priv->dbus_id); +} + +static void +finalize (GObject *object) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + g_free (self->priv->path); + g_free (self->priv->carrier_config_mapping); + + G_OBJECT_CLASS (mm_fake_modem_parent_class)->finalize (object); +} + +static void +dispose (GObject *object) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + if (self->priv->modem_dbus_skeleton) { + mm_iface_modem_shutdown (MM_IFACE_MODEM (object)); + g_clear_object (&self->priv->modem_dbus_skeleton); + } + if (self->priv->modem_voice_dbus_skeleton) { + mm_iface_modem_voice_shutdown (MM_IFACE_MODEM_VOICE (object)); + g_clear_object (&self->priv->modem_voice_dbus_skeleton); + } + g_clear_object (&self->priv->modem_sim); + g_clear_pointer (&self->priv->modem_sim_slots, g_ptr_array_unref); + g_clear_object (&self->priv->modem_bearer_list); + g_clear_object (&self->priv->modem_voice_call_list); + g_clear_object (&self->priv->connection); + + G_OBJECT_CLASS (mm_fake_modem_parent_class)->dispose (object); +} + +static void +iface_modem_init (MMIfaceModemInterface *iface) +{ +} + +static void +iface_modem_voice_init (MMIfaceModemVoiceInterface *iface) +{ + iface->check_support = modem_voice_check_support; + iface->check_support_finish = modem_voice_check_support_finish; + iface->create_call = modem_voice_create_call; + +#if 0 + iface->load_call_list = modem_voice_load_call_list; + iface->load_call_list_finish = modem_voice_load_call_list_finish; + iface->hold_and_accept = modem_voice_hold_and_accept; + iface->hold_and_accept_finish = modem_voice_hold_and_accept_finish; + iface->hangup_and_accept = modem_voice_hangup_and_accept; + iface->hangup_and_accept_finish = modem_voice_hangup_and_accept_finish; + iface->hangup_all = modem_voice_hangup_all; + iface->hangup_all_finish = modem_voice_hangup_all_finish; + iface->join_multiparty = modem_voice_join_multiparty; + iface->join_multiparty_finish = modem_voice_join_multiparty_finish; + iface->leave_multiparty = modem_voice_leave_multiparty; + iface->leave_multiparty_finish = modem_voice_leave_multiparty_finish; + iface->transfer = modem_voice_transfer; + iface->transfer_finish = modem_voice_transfer_finish; + iface->call_waiting_setup = modem_voice_call_waiting_setup; + iface->call_waiting_setup_finish = modem_voice_call_waiting_setup_finish; + iface->call_waiting_query = modem_voice_call_waiting_query; + iface->call_waiting_query_finish = modem_voice_call_waiting_query_finish; +#endif +} + +static void +mm_fake_modem_class_init (MMFakeModemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMFakeModemPrivate)); + + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->finalize = finalize; + object_class->dispose = dispose; + + properties[PROP_CONNECTION] = + g_param_spec_object (MM_BINDABLE_CONNECTION, + "Connection", + "GDBus connection to the system bus.", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_CONNECTION, properties[PROP_CONNECTION]); + + properties[PROP_PATH] = + g_param_spec_string (MM_FAKE_MODEM_PATH, + "Path", + "DBus path of the call", + NULL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PATH, properties[PROP_PATH]); + + g_object_class_override_property (object_class, + PROP_MODEM_DBUS_SKELETON, + MM_IFACE_MODEM_DBUS_SKELETON); + + g_object_class_override_property (object_class, + PROP_MODEM_STATE, + MM_IFACE_MODEM_STATE); + + g_object_class_override_property (object_class, + PROP_MODEM_SIM, + MM_IFACE_MODEM_SIM); + + g_object_class_override_property (object_class, + PROP_MODEM_SIM_SLOTS, + MM_IFACE_MODEM_SIM_SLOTS); + + g_object_class_override_property (object_class, + PROP_MODEM_SIM_HOT_SWAP_SUPPORTED, + MM_IFACE_MODEM_SIM_HOT_SWAP_SUPPORTED); + + g_object_class_override_property (object_class, + PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED, + MM_IFACE_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED, + MM_IFACE_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED, + MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_CARRIER_CONFIG_MAPPING, + MM_IFACE_MODEM_CARRIER_CONFIG_MAPPING); + + g_object_class_override_property (object_class, + PROP_MODEM_BEARER_LIST, + MM_IFACE_MODEM_BEARER_LIST); + + g_object_class_override_property (object_class, + PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED, + MM_IFACE_MODEM_VOICE_INDICATION_CALL_LIST_RELOAD_ENABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_VOICE_DBUS_SKELETON, + MM_IFACE_MODEM_VOICE_DBUS_SKELETON); + + g_object_class_override_property (object_class, + PROP_MODEM_VOICE_CALL_LIST, + MM_IFACE_MODEM_VOICE_CALL_LIST); +} diff --git a/src/tests/fake-modem.h b/src/tests/fake-modem.h new file mode 100644 index 00000000..df1378c3 --- /dev/null +++ b/src/tests/fake-modem.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#ifndef MM_FAKE_MODEM_H +#define MM_FAKE_MODEM_H + +#include <glib.h> +#include <glib-object.h> + +#include "mm-base-modem.h" +#include "mm-call-list.h" + +#define MM_TYPE_FAKE_MODEM (mm_fake_modem_get_type ()) +#define MM_FAKE_MODEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_FAKE_MODEM, MMFakeModem)) +#define MM_FAKE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_FAKE_MODEM, MMFakeModemClass)) +#define MM_IS_FAKE_MODEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_FAKE_MODEM)) +#define MM_IS_FAKE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_FAKE_MODEM)) +#define MM_FAKE_MODEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_FAKE_MODEM, MMFakeModemClass)) + +typedef struct _MMFakeModem MMFakeModem; +typedef struct _MMFakeModemClass MMFakeModemClass; +typedef struct _MMFakeModemPrivate MMFakeModemPrivate; + +struct _MMFakeModem { + MMBaseModem parent; + MMFakeModemPrivate *priv; +}; + +struct _MMFakeModemClass { + MMBaseModemClass parent; +}; + +GType mm_fake_modem_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMFakeModem, g_object_unref) + +MMFakeModem *mm_fake_modem_new (GDBusConnection *connection); + +const gchar *mm_fake_modem_get_path (MMFakeModem *self); + +gboolean mm_fake_modem_export_interfaces (MMFakeModem *self, + GError **error); + +MMCallList *mm_fake_modem_get_call_list (MMFakeModem *self); + +#endif /* MM_FAKE_MODEM_H */ diff --git a/src/tests/meson.build b/src/tests/meson.build index 5c6764b9..a9216c24 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -31,6 +31,11 @@ if enable_mbim test_units += {'modem-helpers-mbim': libkerneldevice_dep} endif +c_args = [ + '-DTEST_SERVICES="@0@"'.format(build_root / 'data/tests'), + '-DTESTUDEVRULESDIR="@0@"'.format(src_dir), +] + foreach test_unit, test_deps: test_units test_name = 'test-' + test_unit @@ -39,12 +44,24 @@ foreach test_unit, test_deps: test_units sources: test_name + '.c', include_directories: top_inc, dependencies: test_deps, - c_args: '-DTESTUDEVRULESDIR="@0@"'.format(src_dir) + c_args: c_args, ) test(test_name, exe) endforeach +# base call test +exe = executable( + 'test-base-call', + sources: [ 'test-base-call.c', 'fake-modem.c', 'fake-call.c' ], + include_directories: top_inc, + dependencies: libmmbase_dep, + c_args: c_args, +) + +test('test-base-call', exe) + + if get_option('fuzzer') fuzzer_tests = ['test-sms-part-3gpp-fuzzer', 'test-sms-part-3gpp-tr-fuzzer', diff --git a/src/tests/test-base-call.c b/src/tests/test-base-call.c new file mode 100644 index 00000000..c7987d61 --- /dev/null +++ b/src/tests/test-base-call.c @@ -0,0 +1,519 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#include <glib.h> +#include <glib-object.h> +#include <string.h> +#include <stdio.h> +#include <locale.h> + +#define _LIBMM_INSIDE_MM +#include <libmm-glib.h> + +#include "mm-base-call.h" +#include "mm-context.h" +#include "mm-call-list.h" +#include "mm-iface-modem-voice.h" +#include "fake-modem.h" +#include "fake-call.h" +#include "mm-log.h" +#include "mm-test-utils.h" + +/****************************************************************/ +/* Make the linker happy */ + +#if defined WITH_QMI + +typedef struct MMBroadbandModemQmi MMBroadbandModemQmi; +GType mm_broadband_modem_qmi_get_type (void); +MMPortQmi *mm_broadband_modem_qmi_peek_port_qmi (MMBroadbandModemQmi *self); + +GType +mm_broadband_modem_qmi_get_type (void) +{ + return G_TYPE_INVALID; +} + +MMPortQmi * +mm_broadband_modem_qmi_peek_port_qmi (MMBroadbandModemQmi *self) +{ + return NULL; +} + +#endif /* WITH_QMI */ + +#if defined WITH_MBIM + +typedef struct MMBroadbandModemMbim MMBroadbandModemMbim; +GType mm_broadband_modem_mbim_get_type (void); +MMPortMbim *mm_broadband_modem_mbim_peek_port_mbim (MMBroadbandModemMbim *self); + +GType +mm_broadband_modem_mbim_get_type (void) +{ + return G_TYPE_INVALID; +} + +MMPortMbim * +mm_broadband_modem_mbim_peek_port_mbim (MMBroadbandModemMbim *self) +{ + return NULL; +} + +#endif /* WITH_MBIM */ + +/****************************************************************/ + +typedef struct { + const gchar *desc; + + const gchar *start_error_msg; + const gchar *accept_error_msg; + const gchar *deflect_error_msg; + const gchar *hangup_error_msg; + + const gchar *number; + + /* DTMF */ + const gchar *dtmf_error_msg; + const gchar *dtmf_stop_error_msg; + const guint dtmf_accept_len; /* how many chars modem can accept at a time */ + const guint dtmf_tone_duration; + const gchar *dtmf; + const guint dtmf_min_duration; +} Testcase; + +typedef struct { + GTestDBus *dbus; + GDBusConnection *connection; + GMainLoop *loop; + guint name_id; + + MmGdbusModemVoice *voice_proxy; + MMFakeModem *modem; + + MmGdbusCall *call_proxy; + MMFakeCall *call; + + GError *error; + + const Testcase *tc; +} TestFixture; + +/****************************************************************/ + +static MMFakeCall * +get_call (TestFixture *tf) +{ + MMCallList *list; + MMFakeCall *call; + const gchar *call_path; + + list = mm_fake_modem_get_call_list (tf->modem); + g_assert (list); + + g_assert (tf->call_proxy); + call_path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (tf->call_proxy)); + call = (MMFakeCall *) mm_call_list_get_call (list, call_path); + g_assert (call); + return call; +} + +/****************************************************************/ + +static void +dtmf_send_ready (MmGdbusCall *call, + GAsyncResult *res, + TestFixture *tf) +{ + if (!mm_gdbus_call_call_send_dtmf_finish (call, res, &tf->error)) + g_assert_true (tf->error); + g_main_loop_quit (tf->loop); +} + +static void +dtmf_call_start_ready (MmGdbusCall *call, + GAsyncResult *res, + TestFixture *tf) +{ + gboolean success; + g_autoptr(GError) error = NULL; + const MMCallInfo cinfo = { + .index = 1, + .number = (gchar *) tf->tc->number, + .direction = MM_CALL_DIRECTION_OUTGOING, + .state = MM_CALL_STATE_ACTIVE, + }; + + success = mm_gdbus_call_call_start_finish (call, res, &error); + g_assert_no_error (error); + g_assert_true (success); + + /* Set the call active */ + mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (tf->modem), &cinfo); + g_main_loop_quit (tf->loop); +} + +static void +dtmf_proxy_ready (gpointer unused, + GAsyncResult *res, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + g_autoptr(MMCallList) list = NULL; + + tf->call_proxy = mm_gdbus_call_proxy_new_for_bus_finish (res, &error); + g_assert_no_error (error); + g_assert (tf->call_proxy); + g_main_loop_quit (tf->loop); +} + +static void +dtmf_create_call_ready (MMIfaceModemVoice *self, + GAsyncResult *res, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + g_autofree gchar *call_path = NULL; + gboolean success; + + success = mm_gdbus_modem_voice_call_create_call_finish (MM_GDBUS_MODEM_VOICE (self), + &call_path, + res, + &error); + g_assert_true (success); + g_assert_no_error (error); + + /* Create our call proxy */ + mm_gdbus_call_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.ModemManager1", + call_path, + NULL, + (GAsyncReadyCallback) dtmf_proxy_ready, + tf); +} + +static void +test_dtmf (TestFixture *tf, const Testcase *tc, gboolean test_stop) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) dictionary = NULL; + g_autoptr(MMCallProperties) call_props = NULL; + MMFakeCall *call; + guint dtmf_len_no_pause = 0; + const gchar *p; + gint64 call_start_time; + + tf->tc = tc; + + call_props = mm_call_properties_new (); + mm_call_properties_set_number (call_props, tf->tc->number); + mm_call_properties_set_dtmf_tone_duration (call_props, tf->tc->dtmf_tone_duration); + dictionary = mm_call_properties_get_dictionary (call_props); + + /* Create the voice call we'll use for DTMF */ + mm_gdbus_modem_voice_call_create_call (tf->voice_proxy, + dictionary, + NULL, + (GAsyncReadyCallback)dtmf_create_call_ready, + tf); + g_main_loop_run (tf->loop); + + /* start the voice call */ + mm_gdbus_call_call_start (tf->call_proxy, + NULL, + (GAsyncReadyCallback) dtmf_call_start_ready, + tf); + g_main_loop_run (tf->loop); + + /* Get the call and copy expectations into it */ + call = get_call (tf); + call->priv->dtmf_accept_len = tf->tc->dtmf_accept_len; + call->priv->start_error_msg = tf->tc->start_error_msg; + call->priv->accept_error_msg = tf->tc->accept_error_msg; + call->priv->deflect_error_msg = tf->tc->deflect_error_msg; + call->priv->hangup_error_msg = tf->tc->hangup_error_msg; + call->priv->dtmf_error_msg = tf->tc->dtmf_error_msg; + call->priv->dtmf_stop_error_msg = tf->tc->dtmf_stop_error_msg; + + mm_fake_call_enable_dtmf_stop (call, test_stop); + +g_message ("####### about to run %s", tf->tc->desc); + /* Run the test */ + call_start_time = g_get_real_time (); + mm_gdbus_call_call_send_dtmf (tf->call_proxy, + tf->tc->dtmf, + NULL, + (GAsyncReadyCallback) dtmf_send_ready, + tf); + g_main_loop_run (tf->loop); + + /* Validate results */ + if (tf->tc->start_error_msg) + mm_assert_error_str (tf->error, tf->tc->start_error_msg); + else if (tf->tc->accept_error_msg) + mm_assert_error_str (tf->error, tf->tc->accept_error_msg); + else if (tf->tc->deflect_error_msg) + mm_assert_error_str (tf->error, tf->tc->deflect_error_msg); + else if (tf->tc->hangup_error_msg) + mm_assert_error_str (tf->error, tf->tc->hangup_error_msg); + else if (tf->tc->dtmf_error_msg) + mm_assert_error_str (tf->error, tf->tc->dtmf_error_msg); + else if (test_stop && tf->tc->dtmf_stop_error_msg) + mm_assert_error_str (tf->error, tf->tc->dtmf_stop_error_msg); + else { + p = tf->tc->dtmf; + while (*p++) { + if (*p != MM_CALL_DTMF_PAUSE_CHAR) + dtmf_len_no_pause++; + } + g_assert_cmpint (strlen (call->priv->dtmf_sent->str), ==, dtmf_len_no_pause); + + if (tf->tc->dtmf_min_duration) { + g_assert_cmpint (call_start_time + (tf->tc->dtmf_min_duration * G_USEC_PER_SEC), <=, g_get_real_time ()); + } + } + if (test_stop && !tf->tc->dtmf_error_msg) + g_assert_true (call->priv->dtmf_stop_called); +} + +static void +test_dtmf_nostop (TestFixture *tf, gconstpointer user_data) +{ + test_dtmf (tf, (const Testcase *) user_data, FALSE); +} + +static void +test_dtmf_need_stop (TestFixture *tf, gconstpointer user_data) +{ + test_dtmf (tf, (const Testcase *) user_data, TRUE); +} + +/************************************************************/ + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + TestFixture *tf) +{ + tf->connection = connection; +} + +static void +name_acquired_cb (GDBusConnection *connection, + const gchar *name, + TestFixture *tf) +{ + g_main_loop_quit (tf->loop); +} + +static void +voice_init_ready (MMIfaceModemVoice *_self, + GAsyncResult *result, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + gboolean success; + + success = mm_iface_modem_voice_initialize_finish (_self, result, &error); + g_assert_no_error (error); + g_assert (success); + g_main_loop_quit (tf->loop); +} + +static void +voice_proxy_ready (gpointer unused, + GAsyncResult *res, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + + tf->voice_proxy = mm_gdbus_modem_voice_proxy_new_for_bus_finish (res, &error); + g_assert_no_error (error); + g_assert (tf->voice_proxy); + g_main_loop_quit (tf->loop); +} + +static void +test_fixture_setup (TestFixture *tf, gconstpointer unused) +{ + g_autoptr(GError) error = NULL; + gboolean success; + + success = mm_log_setup (mm_context_get_log_level (), + mm_context_get_log_file (), + mm_context_get_log_journal (), + mm_context_get_log_timestamps (), + mm_context_get_log_relative_timestamps (), + mm_context_get_log_personal_info (), + &error); + g_assert_no_error (error); + g_assert (success); + + tf->loop = g_main_loop_new (NULL, FALSE); + + /* Create the global dbus-daemon for this test suite */ + tf->dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + g_assert (tf->dbus); + + /* Add the private directory with our in-tree service files, + * TEST_SERVICES is defined by the build system to point + * to the right directory. */ + g_test_dbus_add_service_dir (tf->dbus, TEST_SERVICES); + + /* Start the private DBus daemon */ + g_test_dbus_up (tf->dbus); + + /* Acquire name, don't allow replacement */ + tf->name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + MM_DBUS_SERVICE, + G_BUS_NAME_OWNER_FLAGS_NONE, + (GBusAcquiredCallback) on_bus_acquired, + (GBusNameAcquiredCallback) name_acquired_cb, + NULL, + tf, + NULL); + /* Wait for name acquired */ + g_main_loop_run (tf->loop); + + /* Create and export the server-side modem voice interface */ + g_assert (tf->connection); + tf->modem = mm_fake_modem_new (tf->connection); + mm_iface_modem_voice_initialize (MM_IFACE_MODEM_VOICE (tf->modem), + NULL, + (GAsyncReadyCallback) voice_init_ready, + tf); + g_main_loop_run (tf->loop); + + if (!mm_fake_modem_export_interfaces (tf->modem, &error)) + g_assert_no_error (error); + + /* Create client-side modem proxy */ + mm_gdbus_modem_voice_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.ModemManager1", + mm_fake_modem_get_path (tf->modem), + NULL, + (GAsyncReadyCallback) voice_proxy_ready, + tf); + g_main_loop_run (tf->loop); +} + +static void +test_fixture_cleanup (TestFixture *tf, gconstpointer unused) +{ + g_bus_unown_name (tf->name_id); + mm_iface_modem_voice_shutdown (MM_IFACE_MODEM_VOICE (tf->modem)); + + g_clear_error (&tf->error); + + g_clear_object (&tf->call_proxy); + g_clear_object (&tf->call); + g_clear_object (&tf->voice_proxy); + /* Run dispose to break a ref cycle in case there's still a FakeCall hanging around */ + g_object_run_dispose (G_OBJECT (tf->modem)); + g_clear_object (&tf->modem); + g_dbus_connection_close_sync (tf->connection, NULL, NULL); + g_test_dbus_stop (tf->dbus); + g_test_dbus_down (tf->dbus); + g_clear_object (&tf->dbus); + g_main_loop_unref (tf->loop); + mm_log_shutdown (); +} + +/****************************************************************/ + +static const Testcase tests[] = { + { + .desc = "/MM/Call/DTMF/send-one-accept-len", + .number = "911", + .dtmf_accept_len = 1, + .dtmf_tone_duration = 300, + .dtmf = "987654321", + }, + { + .desc = "/MM/Call/DTMF/send-single-tone", + .number = "911", + .dtmf_accept_len = 3, + .dtmf_tone_duration = 300, + .dtmf = "9", + }, + { + .desc = "/MM/Call/DTMF/send-multi-tone", + .number = "911", + .dtmf_accept_len = 3, + .dtmf_tone_duration = 300, + .dtmf = "123", + }, + { + .desc = "/MM/Call/DTMF/send-pause", + .number = "911", + .dtmf_accept_len = 10, + .dtmf_tone_duration = 300, + .dtmf = "123,,4", + .dtmf_min_duration = 4, + }, + /* Error testing */ + { + .desc = "/MM/Call/DTMF/send-error", + .number = "911", + .dtmf_accept_len = 1, + .dtmf = "123", + .dtmf_error_msg = "send failure", + }, + { + .desc = "/MM/Call/DTMF/stop-error", + .number = "911", + .dtmf_accept_len = 1, + .dtmf = "123", + .dtmf_stop_error_msg = "stop failure", + }, +}; + + + +#define TCASE(n, d, f) \ + g_test_add (n, \ + TestFixture, \ + d, \ + test_fixture_setup, \ + f, \ + test_fixture_cleanup); \ + +#define TCASE_DTMF_STOP(n, d, f) \ + { \ + g_autofree gchar *desc = g_strdup_printf ("%s-stop", n); \ + TCASE(desc, d, f); \ + } + +int main (int argc, char **argv) +{ + const gchar *test_args[] = { argv[0], "--test-session" }; + guint i; + + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + mm_context_init (G_N_ELEMENTS (test_args), (gchar **) test_args); + + for (i = 0; i < G_N_ELEMENTS (tests); i++) { + TCASE(tests[i].desc, &tests[i], test_dtmf_nostop); + /* Test everything again for paired start/stop (eg QMI) */ + TCASE_DTMF_STOP(tests[i].desc, &tests[i], test_dtmf_need_stop); + } + + return g_test_run (); +} diff --git a/src/tests/test-modem-helpers.c b/src/tests/test-modem-helpers.c index 3421921b..51aedef2 100644 --- a/src/tests/test-modem-helpers.c +++ b/src/tests/test-modem-helpers.c @@ -5120,6 +5120,44 @@ test_cpin_response (void) /*****************************************************************************/ +typedef struct { + const char *desc; + const char *dtmf; + const char *expected[5]; +} DtmfTestData; + +static const DtmfTestData test_dtmf_data[] = { + { "/MM/ModemHelpers/DTMF/empty", "", { NULL } }, + { "/MM/ModemHelpers/DTMF/one", "1", { "1", NULL } }, + { "/MM/ModemHelpers/DTMF/no-pause", "1234", { "1234", NULL } }, + { "/MM/ModemHelpers/DTMF/mid-pause", "123,,456", { "123", ",", ",", "456", NULL } }, + { "/MM/ModemHelpers/DTMF/end-pause", "123,,", { "123", ",", ",", NULL } }, +}; + +static void +test_dtmf_split (gpointer user_data) +{ + DtmfTestData *td = user_data; + GPtrArray *split; + guint expected_len; + guint i; + + split = mm_dtmf_split (td->dtmf); + + expected_len = g_strv_length ((gchar **) td->expected); + if (expected_len == 0) + g_assert_true (split == NULL); + else { + g_assert_true (split != NULL); + g_assert_cmpint (split->len, ==, expected_len); + } + + for (i = 0; i < expected_len; i++) + g_assert_cmpstr (td->expected[i], ==, g_ptr_array_index (split, i)); +} + +/*****************************************************************************/ + #define TESTCASE(t, d) g_test_create_case (#t, 0, d, NULL, (GTestFixtureFunc) t, NULL) int main (int argc, char **argv) @@ -5128,6 +5166,7 @@ int main (int argc, char **argv) RegTestData *reg_data; gint result; DevidItem *item = &devids[0]; + guint i; g_test_init (&argc, &argv, NULL); @@ -5384,6 +5423,12 @@ int main (int argc, char **argv) g_test_suite_add (suite, TESTCASE (test_cpin_response, NULL)); + for (i = 0; i < G_N_ELEMENTS (test_dtmf_data); i++) { + g_test_add_data_func (test_dtmf_data[i].desc, + &test_dtmf_data[i], + (GTestDataFunc) test_dtmf_split); + } + result = g_test_run (); reg_test_data_free (reg_data); |