diff --git a/README.md b/README.md
index 54698b8..b650995 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Calls is licensed under the GPLv3+.
## Dependencies
- sudo apt-get install libgtk-3-dev libhandy-0.0-dev
+ sudo apt-get install libgtk-3-dev libhandy-0.0-dev modemmanager-dev libmm-glib-dev
## Building
diff --git a/src/calls-mm-call.c b/src/calls-mm-call.c
new file mode 100644
index 0000000..67dfa88
--- /dev/null
+++ b/src/calls-mm-call.c
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see .
+ *
+ * Author: Bob Ham
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "calls-mm-call.h"
+#include "calls-call.h"
+#include "calls-message-source.h"
+#include "util.h"
+
+#include
+#include
+
+
+struct _CallsMMCall
+{
+ GObject parent_instance;
+ MMCall *mm_call;
+ GString *number;
+ CallsCallState state;
+ gchar *disconnect_reason;
+};
+
+static void calls_mm_call_message_source_interface_init (CallsCallInterface *iface);
+static void calls_mm_call_call_interface_init (CallsCallInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CallsMMCall, calls_mm_call, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (CALLS_TYPE_MESSAGE_SOURCE,
+ calls_mm_call_message_source_interface_init)
+ G_IMPLEMENT_INTERFACE (CALLS_TYPE_CALL,
+ calls_mm_call_call_interface_init))
+
+enum {
+ PROP_0,
+ PROP_MM_CALL,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+
+static const gchar *
+get_name (CallsCall *call)
+{
+ return NULL;
+}
+
+
+static const gchar *
+get_number (CallsCall *call)
+{
+ CallsMMCall *self = CALLS_MM_CALL (call);
+ return self->number->str;
+}
+
+
+static CallsCallState
+get_state (CallsCall *call)
+{
+ CallsMMCall *self = CALLS_MM_CALL (call);
+ return self->state;
+}
+
+
+static void
+change_state (CallsMMCall *self,
+ MMCallState state)
+{
+ self->state = state;
+ g_signal_emit_by_name (CALLS_CALL (self),
+ "state-changed", state);
+}
+
+
+static void
+notify_number_cb (CallsMMCall *self,
+ const gchar *number)
+{
+ g_string_assign (self->number, number);
+}
+
+
+struct CallsMMCallStateReasonMap
+{
+ MMCallStateReason value;
+ const gchar *desc;
+};
+
+static const struct CallsMMCallStateReasonMap STATE_REASON_MAP[] = {
+
+#define row(ENUMVALUE,DESCRIPTION) \
+ { MM_CALL_STATE_REASON_##ENUMVALUE, DESCRIPTION } \
+
+ row (UNKNOWN, "Outgoing call created"),
+ row (OUTGOING_STARTED, "Outgoing call started"),
+ row (INCOMING_NEW, "Incoming call"),
+ row (ACCEPTED, "Call accepted"),
+ row (TERMINATED, "Call terminated"),
+ row (REFUSED_OR_BUSY, "Busy or call refused"),
+ row (ERROR, "Wrong number or network problem"),
+
+#undef row
+
+ { -1, NULL }
+};
+
+static void
+set_disconnect_reason (CallsMMCall *self,
+ MMCallStateReason reason)
+{
+ const struct CallsMMCallStateReasonMap *map_row;
+ GString *reason_str;
+
+ for (map_row = STATE_REASON_MAP; map_row->desc; ++map_row)
+ {
+ if (map_row->value == reason)
+ {
+ self->disconnect_reason = g_strdup(map_row->desc);
+ return;
+ }
+ }
+
+ reason_str = g_string_new ("Unknown disconnect reason ");
+ g_string_append_printf (reason_str, "(%i)", (int)reason);
+
+ g_warning ("%s", reason_str->str);
+ CALLS_SET_PTR_PROPERTY (self->disconnect_reason,
+ reason_str->str);
+
+ g_string_free (reason_str, FALSE);
+}
+
+
+struct CallsMMCallStateMap
+{
+ MMCallState mm;
+ CallsCallState calls;
+};
+
+static const struct CallsMMCallStateMap STATE_MAP[] = {
+
+#define row(MMENUM,CALLSENUM) \
+ { MM_CALL_STATE_##MMENUM, CALLS_CALL_STATE_##CALLSENUM } \
+
+ row (DIALING, DIALING),
+ row (RINGING_OUT, INCOMING),
+ row (RINGING_IN, ALERTING),
+ row (ACTIVE, ACTIVE),
+ row (HELD, HELD),
+ row (WAITING, INCOMING),
+ row (TERMINATED, DISCONNECTED),
+
+#undef row
+
+ { MM_CALL_STATE_UNKNOWN, (CallsCallState)0 },
+ { -1, -1 }
+};
+
+static void
+state_changed_cb (CallsMMCall *self,
+ MMCallState old,
+ MMCallState mm_new,
+ MMCallStateReason reason)
+{
+ const struct CallsMMCallStateMap *map_row;
+
+ if (mm_new == MM_CALL_STATE_TERMINATED)
+ {
+ set_disconnect_reason (self, reason);
+ }
+
+ for (map_row = STATE_MAP; map_row->mm != -1; ++map_row)
+ {
+ if (map_row->mm == mm_new)
+ {
+ change_state (self, map_row->calls);
+ return;
+ }
+ }
+}
+
+
+struct CallsMMOperationData
+{
+ const gchar *desc;
+ CallsMMCall *self;
+ gboolean (*finish_func) (MMCall *, GAsyncResult *, GError **);
+};
+
+static void
+operation_cb (MMCall *mm_call,
+ GAsyncResult *res,
+ struct CallsMMOperationData *data)
+{
+ gboolean ok;
+ GError *error = NULL;
+
+ ok = data->finish_func (mm_call, res, &error);
+ if (!ok)
+ {
+ g_warning ("Error %s ModemManager call to `%s': %s",
+ data->desc, data->self->number->str, error->message);
+ CALLS_ERROR (data->self, error);
+ }
+
+ g_free (data);
+}
+
+#define DEFINE_OPERATION(op,name,desc_str) \
+ static void \
+ name (CallsCall *call) \
+ { \
+ CallsMMCall *self = CALLS_MM_CALL (call); \
+ struct CallsMMOperationData *data; \
+ \
+ data = g_new0 (struct CallsMMOperationData, 1); \
+ data->desc = desc_str; \
+ data->self = self; \
+ data->finish_func = mm_call_##op##_finish; \
+ \
+ mm_call_##op \
+ (self->mm_call, \
+ NULL, \
+ (GAsyncReadyCallback) operation_cb, \
+ data); \
+ }
+
+DEFINE_OPERATION(accept, answer, "accepting");
+DEFINE_OPERATION(hangup, hang_up, "hanging up");
+DEFINE_OPERATION(start, start_call, "starting outgoing call");
+
+
+static void
+tone_start (CallsCall *call, gchar key)
+{
+ CallsMMCall *self = CALLS_MM_CALL (call);
+ struct CallsMMOperationData *data;
+ char key_str[2] = { key, '\0' };
+
+ data = g_new0 (struct CallsMMOperationData, 1);
+ data->desc = "sending DTMF";
+ data->self = self;
+ data->finish_func = mm_call_send_dtmf_finish;
+
+ mm_call_send_dtmf
+ (self->mm_call,
+ key_str,
+ NULL,
+ (GAsyncReadyCallback) operation_cb,
+ data);
+}
+
+
+static void
+set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CallsMMCall *self = CALLS_MM_CALL (object);
+
+ switch (property_id) {
+ case PROP_MM_CALL:
+ g_set_object (&self->mm_call,
+ MM_CALL (g_value_get_object (value)));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+constructed (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMCall *self = CALLS_MM_CALL (object);
+ MmGdbusCall *gdbus_call = MM_GDBUS_CALL (self->mm_call);
+
+ g_signal_connect_swapped (gdbus_call, "notify::number",
+ G_CALLBACK (notify_number_cb), self);
+ g_signal_connect_swapped (gdbus_call, "state-changed",
+ G_CALLBACK (state_changed_cb), self);
+
+ notify_number_cb (self, mm_call_get_number (self->mm_call));
+
+ /* Start outgoing call */
+ if (mm_call_get_state (self->mm_call) == MM_CALL_STATE_UNKNOWN
+ && mm_call_get_direction (self->mm_call) == MM_CALL_DIRECTION_OUTGOING)
+ {
+ start_call (CALLS_CALL (self));
+ }
+
+ parent_class->constructed (object);
+}
+
+
+static void
+dispose (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMCall *self = CALLS_MM_CALL (object);
+
+ g_clear_object (&self->mm_call);
+
+ parent_class->dispose (object);
+}
+
+
+static void
+finalize (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMCall *self = CALLS_MM_CALL (object);
+
+ g_free (self->disconnect_reason);
+ g_string_free (self->number, TRUE);
+
+ parent_class->finalize (object);
+}
+
+
+static void
+calls_mm_call_class_init (CallsMMCallClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = set_property;
+ object_class->constructed = constructed;
+ object_class->dispose = dispose;
+ object_class->finalize = finalize;
+
+ props[PROP_MM_CALL] =
+ g_param_spec_object ("mm-call",
+ _("MM call"),
+ _("A libmm-glib proxy object for the underlying call object"),
+ MM_TYPE_CALL,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+}
+
+
+static void
+calls_mm_call_message_source_interface_init (CallsCallInterface *iface)
+{
+}
+
+
+static void
+calls_mm_call_call_interface_init (CallsCallInterface *iface)
+{
+ iface->get_number = get_number;
+ iface->get_name = get_name;
+ iface->get_state = get_state;
+ iface->answer = answer;
+ iface->hang_up = hang_up;
+ iface->tone_start = tone_start;
+}
+
+
+static void
+calls_mm_call_init (CallsMMCall *self)
+{
+ self->number = g_string_new (NULL);
+}
+
+
+CallsMMCall *
+calls_mm_call_new (MMCall *mm_call)
+{
+ g_return_val_if_fail (MM_IS_CALL (mm_call), NULL);
+
+ return g_object_new (CALLS_TYPE_MM_CALL,
+ "mm-call", mm_call,
+ NULL);
+}
+
+
+const gchar *
+calls_mm_call_get_object_path (CallsMMCall *call)
+{
+ return mm_call_get_path (call->mm_call);
+}
+
+
+const gchar *
+calls_mm_call_get_disconnect_reason (CallsMMCall *call)
+{
+ return call->disconnect_reason;
+}
diff --git a/src/calls-mm-call.h b/src/calls-mm-call.h
new file mode 100644
index 0000000..ccb3d9d
--- /dev/null
+++ b/src/calls-mm-call.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see .
+ *
+ * Author: Bob Ham
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#ifndef CALLS_MM_CALL_H__
+#define CALLS_MM_CALL_H__
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CALLS_TYPE_MM_CALL (calls_mm_call_get_type ())
+
+G_DECLARE_FINAL_TYPE (CallsMMCall, calls_mm_call, CALLS, MM_CALL, GObject);
+
+CallsMMCall *calls_mm_call_new (MMCall *mm_call);
+const gchar *calls_mm_call_get_object_path (CallsMMCall *call);
+const gchar *calls_mm_call_get_disconnect_reason (CallsMMCall *call);
+
+G_END_DECLS
+
+#endif /* CALLS_MM_CALL_H__ */
diff --git a/src/calls-mm-origin.c b/src/calls-mm-origin.c
new file mode 100644
index 0000000..e3636b3
--- /dev/null
+++ b/src/calls-mm-origin.c
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see .
+ *
+ * Author: Bob Ham
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "calls-mm-origin.h"
+#include "calls-origin.h"
+#include "calls-mm-call.h"
+#include "calls-message-source.h"
+
+#include
+
+
+struct _CallsMMOrigin
+{
+ GObject parent_instance;
+ MMObject *mm_obj;
+ MMModemVoice *voice;
+ gchar *name;
+ GHashTable *calls;
+};
+
+static void calls_mm_origin_message_source_interface_init (CallsOriginInterface *iface);
+static void calls_mm_origin_origin_interface_init (CallsOriginInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CallsMMOrigin, calls_mm_origin, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (CALLS_TYPE_MESSAGE_SOURCE,
+ calls_mm_origin_message_source_interface_init)
+ G_IMPLEMENT_INTERFACE (CALLS_TYPE_ORIGIN,
+ calls_mm_origin_origin_interface_init))
+
+enum {
+ PROP_0,
+ PROP_MODEM,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+
+static const gchar *
+get_name (CallsOrigin *origin)
+{
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (origin);
+ return self->name;
+}
+
+
+static GList *
+get_calls (CallsOrigin * origin)
+{
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (origin);
+ return g_hash_table_get_values (self->calls);
+}
+
+
+static void
+dial_cb (MMModemVoice *voice,
+ GAsyncResult *res,
+ CallsMMOrigin *self)
+{
+ MMCall *call;
+ GError *error = NULL;
+
+ call = mm_modem_voice_create_call_finish (voice, res, &error);
+ if (!call)
+ {
+ g_warning ("Error dialing number on ModemManager modem `%s': %s",
+ self->name, error->message);
+ CALLS_ERROR (self, error);
+ }
+}
+
+
+static void
+dial (CallsOrigin *origin, const gchar *number)
+{
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (origin);
+ MMCallProperties *props;
+
+ g_assert (self->voice != NULL);
+
+ props = mm_call_properties_new();
+ mm_call_properties_set_number (props, number);
+
+ mm_modem_voice_create_call
+ (self->voice,
+ props,
+ NULL,
+ (GAsyncReadyCallback) dial_cb,
+ self);
+
+ g_object_unref (props);
+}
+
+
+static void
+remove_call (CallsMMOrigin *self,
+ CallsMMCall *call,
+ const gchar *path,
+ const gchar *reason)
+{
+ g_signal_emit_by_name (CALLS_ORIGIN(self), "call-removed",
+ CALLS_CALL(call), reason);
+ g_hash_table_remove (self->calls, path);
+}
+
+
+struct CallsMMRemoveCallsData
+{
+ CallsOrigin *origin;
+ const gchar *reason;
+};
+
+
+static gboolean
+remove_calls_cb (const gchar *path,
+ CallsMMCall *call,
+ struct CallsMMRemoveCallsData *data)
+{
+ g_signal_emit_by_name (data->origin, "call-removed",
+ CALLS_CALL(call), data->reason);
+ return TRUE;
+}
+
+
+static void
+remove_calls (CallsMMOrigin *self, const gchar *reason)
+{
+ struct CallsMMRemoveCallsData data = { CALLS_ORIGIN (self), reason };
+
+ g_hash_table_foreach_remove (self->calls,
+ (GHRFunc) remove_calls_cb,
+ &data);
+}
+
+
+struct CallsMMOriginDeleteCallData
+{
+ CallsMMOrigin *self;
+ gchar *path;
+};
+
+
+static void
+delete_call_cb (MMModemVoice *voice,
+ GAsyncResult *res,
+ struct CallsMMOriginDeleteCallData *data)
+{
+ gboolean ok;
+ GError *error = NULL;
+
+ ok = mm_modem_voice_delete_call_finish (voice, res, &error);
+ if (!ok)
+ {
+ g_warning ("Error deleting call `%s' on MMModemVoice `%s': %s",
+ data->path, data->self->name, error->message);
+ CALLS_ERROR (data->self, error);
+ }
+
+ g_free (data->path);
+ g_free (data);
+}
+
+
+static void
+call_state_changed_cb (CallsMMOrigin *self,
+ CallsCallState state,
+ CallsCall *call)
+{
+ const gchar *path;
+ struct CallsMMOriginDeleteCallData *data;
+
+ if (state != CALLS_CALL_STATE_DISCONNECTED)
+ {
+ return;
+ }
+
+ path = calls_mm_call_get_object_path (CALLS_MM_CALL (call));
+
+ data = g_new0 (struct CallsMMOriginDeleteCallData, 1);
+ data->self = self;
+ data->path = g_strdup (path);
+
+ mm_modem_voice_delete_call
+ (self->voice,
+ path,
+ NULL,
+ (GAsyncReadyCallback) delete_call_cb,
+ data);
+}
+
+
+static void
+add_call (CallsMMOrigin *self,
+ MMCall *mm_call)
+{
+ CallsMMCall *call;
+ gchar *path;
+
+ call = calls_mm_call_new (mm_call);
+
+ g_signal_connect_swapped (call, "state-changed",
+ G_CALLBACK (call_state_changed_cb),
+ self);
+
+ path = mm_call_dup_path (mm_call);
+ g_hash_table_insert (self->calls, path, call);
+
+ g_signal_emit_by_name (CALLS_ORIGIN(self), "call-added",
+ CALLS_CALL(call));
+
+ g_debug ("Call `%s' added", path);
+}
+
+
+struct CallsMMOriginCallAddedData
+{
+ CallsMMOrigin *self;
+ gchar *path;
+};
+
+
+static void
+call_added_list_calls_cb (MMModemVoice *voice,
+ GAsyncResult *res,
+ struct CallsMMOriginCallAddedData *data)
+{
+ GList *calls;
+ GError *error = NULL;
+
+ calls = mm_modem_voice_list_calls_finish (voice, res, &error);
+ if (!calls)
+ {
+ if (error)
+ {
+ g_warning ("Error listing calls on MMModemVoice `%s'"
+ " after call-added signal: %s",
+ data->self->name, error->message);
+ CALLS_ERROR (data->self, error);
+ }
+ else
+ {
+ g_warning ("No calls on MMModemVoice `%s'"
+ " after call-added signal",
+ data->self->name);
+ }
+ }
+ else
+ {
+ GList *node;
+ MMCall *call;
+ gboolean found = FALSE;
+
+ for (node = calls; node; node = node->next)
+ {
+ call = MM_CALL (node->data);
+
+ if (g_strcmp0 (mm_call_get_path (call), data->path) == 0)
+ {
+ add_call (data->self, call);
+ found = TRUE;
+ }
+ }
+
+ if (!found)
+ {
+ g_warning ("Could not find new call `%s' in call list"
+ " on MMModemVoice `%s' after call-added signal",
+ data->path, data->self->name);
+ }
+
+ g_list_free_full (calls, g_object_unref);
+ }
+
+ g_free (data->path);
+ g_free (data);
+}
+
+
+static void
+call_added_cb (MMModemVoice *voice,
+ gchar *path,
+ CallsMMOrigin *self)
+{
+ struct CallsMMOriginCallAddedData *data;
+
+ if (g_hash_table_contains (self->calls, path))
+ {
+ g_warning ("Received call-added signal for"
+ " existing call object path `%s'", path);
+ return;
+ }
+
+ data = g_new0 (struct CallsMMOriginCallAddedData, 1);
+ data->self = self;
+ data->path = g_strdup (path);
+
+ mm_modem_voice_list_calls
+ (voice,
+ NULL,
+ (GAsyncReadyCallback) call_added_list_calls_cb,
+ data);
+}
+
+
+static void
+call_deleted_cb (MMModemVoice *voice,
+ const gchar *path,
+ CallsMMOrigin *self)
+{
+ CallsMMCall *call;
+ GString *reason;
+ const gchar *mm_reason;
+
+ g_debug ("Removing call `%s'", path);
+
+ call = g_hash_table_lookup (self->calls, path);
+ if (!call)
+ {
+ g_warning ("Could not find removed call `%s'", path);
+ return;
+ }
+
+ reason = g_string_new ("Call removed");
+
+ mm_reason = calls_mm_call_get_disconnect_reason (call);
+ if (mm_reason)
+ {
+ g_string_assign (reason, mm_reason);
+ }
+
+ remove_call (self, call, path, reason->str);
+
+ g_string_free (reason, TRUE);
+
+ g_debug ("Removed call `%s'", path);
+}
+
+
+static void
+list_calls_cb (MMModemVoice *voice,
+ GAsyncResult *res,
+ CallsMMOrigin *self)
+{
+ GList *calls, *node;
+ GError *error = NULL;
+
+ calls = mm_modem_voice_list_calls_finish (voice, res, &error);
+ if (!calls)
+ {
+ if (error)
+ {
+ g_warning ("Error listing calls on MMModemVoice `%s': %s",
+ self->name, error->message);
+ CALLS_ERROR (self, error);
+ }
+ return;
+ }
+
+ for (node = calls; node; node = node->next)
+ {
+ add_call (self, MM_CALL (node->data));
+ }
+
+ g_list_free_full (calls, g_object_unref);
+}
+
+
+static void
+set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (object);
+
+ switch (property_id) {
+ case PROP_MODEM:
+ g_set_object (&self->mm_obj, g_value_get_object(value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static gchar *
+modem_get_name (MMModem *modem)
+{
+ gchar *name = NULL;
+
+#define try(prop) \
+ name = mm_modem_dup_##prop (modem); \
+ if (name) { \
+ return name; \
+ }
+
+ try (model);
+ try (manufacturer);
+ try (device);
+ try (primary_port);
+ try (device_identifier);
+ try (plugin);
+
+#undef try
+
+ return NULL;
+}
+
+
+static void
+constructed (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (object);
+ MmGdbusModemVoice *gdbus_voice;
+
+ self->name = modem_get_name (mm_object_get_modem (self->mm_obj));
+
+ self->voice = mm_object_get_modem_voice (self->mm_obj);
+ g_assert (self->voice != NULL);
+
+ gdbus_voice = MM_GDBUS_MODEM_VOICE (self->voice);
+ g_signal_connect (gdbus_voice, "call-added",
+ G_CALLBACK (call_added_cb), self);
+ g_signal_connect (gdbus_voice, "call-deleted",
+ G_CALLBACK (call_deleted_cb), self);
+
+ mm_modem_voice_list_calls
+ (self->voice,
+ NULL,
+ (GAsyncReadyCallback) list_calls_cb,
+ self);
+ parent_class->constructed (object);
+}
+
+
+static void
+dispose (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (object);
+
+ remove_calls (self, NULL);
+ g_clear_object (&self->mm_obj);
+
+ parent_class->dispose (object);
+}
+
+
+static void
+finalize (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMOrigin *self = CALLS_MM_ORIGIN (object);
+
+ g_hash_table_unref (self->calls);
+ g_free (self->name);
+
+ parent_class->finalize (object);
+}
+
+
+static void
+calls_mm_origin_class_init (CallsMMOriginClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = set_property;
+ object_class->constructed = constructed;
+ object_class->dispose = dispose;
+ object_class->finalize = finalize;
+
+ props[PROP_MODEM] =
+ g_param_spec_object ("mm-object",
+ _("Modem Object"),
+ _("A libmm-glib proxy object for the modem"),
+ MM_TYPE_OBJECT,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+}
+
+
+static void
+calls_mm_origin_message_source_interface_init (CallsOriginInterface *iface)
+{
+}
+
+
+static void
+calls_mm_origin_origin_interface_init (CallsOriginInterface *iface)
+{
+ iface->get_name = get_name;
+ iface->get_calls = get_calls;
+ iface->dial = dial;
+}
+
+
+static void
+calls_mm_origin_init (CallsMMOrigin *self)
+{
+ self->calls = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, g_object_unref);
+}
+
+CallsMMOrigin *
+calls_mm_origin_new (MMObject *mm_obj)
+{
+ return g_object_new (CALLS_TYPE_MM_ORIGIN,
+ "mm-object", mm_obj,
+ NULL);
+}
diff --git a/src/calls-mm-origin.h b/src/calls-mm-origin.h
new file mode 100644
index 0000000..daee945
--- /dev/null
+++ b/src/calls-mm-origin.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see .
+ *
+ * Author: Bob Ham
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#ifndef CALLS_MM_ORIGIN_H__
+#define CALLS_MM_ORIGIN_H__
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CALLS_TYPE_MM_ORIGIN (calls_mm_origin_get_type ())
+
+G_DECLARE_FINAL_TYPE (CallsMMOrigin, calls_mm_origin, CALLS, MM_ORIGIN, GObject);
+
+CallsMMOrigin *calls_mm_origin_new (MMObject *modem);
+
+G_END_DECLS
+
+#endif /* CALLS_MM_ORIGIN_H__ */
diff --git a/src/calls-mm-provider.c b/src/calls-mm-provider.c
new file mode 100644
index 0000000..85471b6
--- /dev/null
+++ b/src/calls-mm-provider.c
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see .
+ *
+ * Author: Bob Ham
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "calls-mm-provider.h"
+#include "calls-provider.h"
+#include "calls-mm-origin.h"
+#include "calls-message-source.h"
+#include "calls-origin.h"
+
+#include
+#include
+
+struct _CallsMMProvider
+{
+ GObject parent_instance;
+
+ /** D-Bus connection */
+ GDBusConnection *connection;
+ /** ModemManager object proxy */
+ MMManager *mm;
+ /** Map of D-Bus object paths to origins */
+ GHashTable *origins;
+};
+
+static void calls_mm_provider_message_source_interface_init (CallsProviderInterface *iface);
+static void calls_mm_provider_provider_interface_init (CallsProviderInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CallsMMProvider, calls_mm_provider, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (CALLS_TYPE_MESSAGE_SOURCE,
+ calls_mm_provider_message_source_interface_init)
+ G_IMPLEMENT_INTERFACE (CALLS_TYPE_PROVIDER,
+ calls_mm_provider_provider_interface_init))
+
+
+enum {
+ PROP_0,
+ PROP_CONNECTION,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+
+static const gchar *
+get_name (CallsProvider *iface)
+{
+ return "ModemManager";
+}
+
+
+static GList *
+get_origins (CallsProvider *iface)
+{
+ CallsMMProvider *self = CALLS_MM_PROVIDER (iface);
+ return g_hash_table_get_values (self->origins);
+}
+
+
+static void
+add_origin (CallsMMProvider *self,
+ GDBusObject *object)
+{
+ MMObject *mm_obj;
+ CallsMMOrigin *origin;
+
+ g_debug ("Adding new voice-cable modem `%s'",
+ g_dbus_object_get_object_path (object));
+
+ g_assert (MM_IS_OBJECT (object));
+ mm_obj = MM_OBJECT (object);
+
+ origin = calls_mm_origin_new (mm_obj);
+
+ g_hash_table_insert (self->origins,
+ mm_object_dup_path (mm_obj),
+ origin);
+
+ g_signal_emit_by_name (CALLS_PROVIDER (self),
+ "origin-added", origin);
+}
+
+
+static void
+interface_added_cb (CallsMMProvider *self,
+ GDBusObject *object,
+ GDBusInterface *interface)
+{
+ GDBusInterfaceInfo *info;
+
+ info = g_dbus_interface_get_info (interface);
+
+ if (g_strcmp0 (info->name,
+ "org.freedesktop.ModemManager1.Modem.Voice") == 0)
+ {
+ add_origin (self, object);
+ }
+}
+
+
+static void
+remove_origin (CallsMMProvider *self,
+ GDBusObject *object)
+{
+ const gchar *path;
+ gpointer *origin;
+
+ path = g_dbus_object_get_object_path (object);
+
+ origin = g_hash_table_lookup (self->origins, path);
+ g_assert (origin != NULL && CALLS_IS_ORIGIN (origin));
+
+ g_signal_emit_by_name (CALLS_PROVIDER (self),
+ "origin-removed", CALLS_ORIGIN (origin));
+
+ g_hash_table_remove (self->origins, path);
+}
+
+
+static void
+interface_removed_cb (CallsMMProvider *self,
+ GDBusObject *object,
+ GDBusInterface *interface)
+{
+ GDBusInterfaceInfo *info;
+
+ info = g_dbus_interface_get_info (interface);
+
+ if (g_strcmp0 (info->name,
+ "org.freedesktop.ModemManager1.Modem.Voice") != 0)
+ {
+ remove_origin (self, object);
+ }
+}
+
+
+static void
+set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CallsMMProvider *self = CALLS_MM_PROVIDER (object);
+
+ switch (property_id) {
+ case PROP_CONNECTION:
+ g_set_object (&self->connection,
+ g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+add_mm_object (CallsMMProvider *self, GDBusObject *object)
+{
+ GList *ifaces, *node;
+
+ ifaces = g_dbus_object_get_interfaces (object);
+ for (node = ifaces; node; node = node->next)
+ {
+ interface_added_cb (self, object,
+ G_DBUS_INTERFACE (node->data));
+ }
+
+ g_list_free_full (ifaces, g_object_unref);
+}
+
+
+static void
+add_mm_objects (CallsMMProvider *self)
+{
+ GList *objects, *node;
+
+ objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self->mm));
+ for (node = objects; node; node = node->next)
+ {
+ add_mm_object (self, G_DBUS_OBJECT (node->data));
+ }
+
+ g_list_free_full (objects, g_object_unref);
+}
+
+
+static void
+mm_manager_new_cb (GDBusConnection *connection,
+ GAsyncResult *res,
+ CallsMMProvider *self)
+{
+ GError *error = NULL;
+
+ self->mm = mm_manager_new_finish (res, &error);
+ if (!self->mm)
+ {
+ g_error ("Error creating ModemManager Manager: %s",
+ error->message);
+ g_assert_not_reached();
+ }
+
+
+ g_signal_connect_swapped (self->mm, "interface-added",
+ G_CALLBACK (interface_added_cb), self);
+ g_signal_connect_swapped (self->mm, "interface-removed",
+ G_CALLBACK (interface_removed_cb), self);
+
+ add_mm_objects (self);
+ if (g_hash_table_size (self->origins) == 0)
+ {
+ g_warning ("No modems available");
+ CALLS_EMIT_MESSAGE (self, "No modems available",
+ GTK_MESSAGE_WARNING);
+ }
+}
+
+
+static void
+constructed (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMProvider *self = CALLS_MM_PROVIDER (object);
+
+ mm_manager_new (self->connection,
+ G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
+ NULL,
+ (GAsyncReadyCallback) mm_manager_new_cb,
+ self);
+
+ parent_class->constructed (object);
+}
+
+
+static void
+dispose (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMProvider *self = CALLS_MM_PROVIDER (object);
+
+ g_hash_table_remove_all (self->origins);
+ g_clear_object (&self->mm);
+ g_clear_object (&self->connection);
+
+ parent_class->dispose (object);
+}
+
+
+static void
+finalize (GObject *object)
+{
+ GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT);
+ CallsMMProvider *self = CALLS_MM_PROVIDER (object);
+
+ g_hash_table_unref (self->origins);
+
+ parent_class->finalize (object);
+}
+
+
+static void
+calls_mm_provider_class_init (CallsMMProviderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = set_property;
+ object_class->constructed = constructed;
+ object_class->dispose = dispose;
+ object_class->finalize = finalize;
+
+ props[PROP_CONNECTION] =
+ g_param_spec_object ("connection",
+ _("Connection"),
+ _("The D-Bus connection to use for communication with ModemManager"),
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+}
+
+
+static void
+calls_mm_provider_message_source_interface_init (CallsProviderInterface *iface)
+{
+}
+
+
+static void
+calls_mm_provider_provider_interface_init (CallsProviderInterface *iface)
+{
+ iface->get_name = get_name;
+ iface->get_origins = get_origins;
+}
+
+
+static void
+calls_mm_provider_init (CallsMMProvider *self)
+{
+ self->origins = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, g_object_unref);
+}
+
+
+CallsMMProvider *
+calls_mm_provider_new (GDBusConnection *connection)
+{
+ g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
+
+ return g_object_new (CALLS_TYPE_MM_PROVIDER,
+ "connection", connection,
+ NULL);
+}
diff --git a/src/calls-mm-provider.h b/src/calls-mm-provider.h
new file mode 100644
index 0000000..d8f5c40
--- /dev/null
+++ b/src/calls-mm-provider.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see .
+ *
+ * Author: Bob Ham
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#ifndef CALLS_MM_PROVIDER_H__
+#define CALLS_MM_PROVIDER_H__
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define CALLS_TYPE_MM_PROVIDER (calls_mm_provider_get_type ())
+
+G_DECLARE_FINAL_TYPE (CallsMMProvider, calls_mm_provider, CALLS, MM_PROVIDER, GObject);
+
+CallsMMProvider *calls_mm_provider_new (GDBusConnection *connection);
+
+G_END_DECLS
+
+#endif /* CALLS_MM_PROVIDER_H__ */
diff --git a/src/main.c b/src/main.c
index f240cd9..591ea68 100644
--- a/src/main.c
+++ b/src/main.c
@@ -29,7 +29,7 @@
#include "calls-encryption-indicator.h"
#include "calls-main-window.h"
-#include "calls-ofono-provider.h"
+#include "calls-mm-provider.h"
static void
show_window (GtkApplication *app)
@@ -50,7 +50,8 @@ show_window (GtkApplication *app)
return;
}
- provider = CALLS_PROVIDER (calls_ofono_provider_new (connection));
+ provider = CALLS_PROVIDER (calls_mm_provider_new (connection));
+ g_assert (provider != NULL);
main_window = calls_main_window_new (app, provider);
diff --git a/src/meson.build b/src/meson.build
index 7d987ef..21d1066 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -26,6 +26,8 @@ gnome = import('gnome')
calls_deps = [ dependency('gobject-2.0'),
dependency('gtk+-3.0'),
dependency('libhandy-0.0'),
+ dependency('ModemManager'),
+ dependency('mm-glib'),
]
calls_sources = ['calls-message-source.c', 'calls-message-source.h',
@@ -35,6 +37,9 @@ calls_sources = ['calls-message-source.c', 'calls-message-source.h',
'calls-ofono-call.c', 'calls-ofono-call.h',
'calls-ofono-origin.c', 'calls-ofono-origin.h',
'calls-ofono-provider.c', 'calls-ofono-provider.h',
+ 'calls-mm-call.c', 'calls-mm-call.h',
+ 'calls-mm-origin.c', 'calls-mm-origin.h',
+ 'calls-mm-provider.c', 'calls-mm-provider.h',
'calls-party.c', 'calls-party.h',
'calls-call-data.c', 'calls-call-data.h',
'calls-call-holder.c', 'calls-call-holder.h',