aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Williams <dan@ioncontrol.co>2025-05-30 18:54:05 -0500
committerDan Williams <dan@ioncontrol.co>2025-05-30 18:54:05 -0500
commit37f17d4b5859d8a37d1a9350abc9fcb15917de07 (patch)
tree7af9420327a7deea70df1bbcaa004724fed1aec7
parent3ed7f378765b45a84ce6c0b4de6751769fefc221 (diff)
parentdad2d49b696c66ccf868bc89b35a6529f9e15777 (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
-rw-r--r--introspection/org.freedesktop.ModemManager1.Call.xml25
-rw-r--r--libmm-glib/mm-call-properties.c81
-rw-r--r--libmm-glib/mm-call-properties.h4
-rw-r--r--src/meson.build69
-rw-r--r--src/mm-base-call.c727
-rw-r--r--src/mm-base-call.h47
-rw-r--r--src/mm-broadband-modem-qmi.c6
-rw-r--r--src/mm-broadband-modem.c25
-rw-r--r--src/mm-call-at.c474
-rw-r--r--src/mm-call-at.h61
-rw-r--r--src/mm-call-list.c65
-rw-r--r--src/mm-call-list.h5
-rw-r--r--src/mm-call-qmi.c211
-rw-r--r--src/mm-call-qmi.h5
-rw-r--r--src/mm-iface-modem-voice.c12
-rw-r--r--src/mm-iface-modem-voice.h3
-rw-r--r--src/mm-modem-helpers.c50
-rw-r--r--src/mm-modem-helpers.h9
-rw-r--r--src/mm-test-utils.h52
-rw-r--r--src/plugins/cinterion/mm-shared-cinterion.c25
-rw-r--r--src/plugins/cinterion/mm-shared-cinterion.h3
-rw-r--r--src/plugins/huawei/mm-broadband-modem-huawei.c21
-rw-r--r--src/plugins/ublox/mm-broadband-modem-ublox.c21
-rw-r--r--src/tests/fake-call.c409
-rw-r--r--src/tests/fake-call.h77
-rw-r--r--src/tests/fake-modem.c463
-rw-r--r--src/tests/fake-modem.h57
-rw-r--r--src/tests/meson.build19
-rw-r--r--src/tests/test-base-call.c519
-rw-r--r--src/tests/test-modem-helpers.c45
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);