diff options
Diffstat (limited to 'spa/plugins/bluez5/modemmanager.c')
-rw-r--r-- | spa/plugins/bluez5/modemmanager.c | 1249 |
1 files changed, 1249 insertions, 0 deletions
diff --git a/spa/plugins/bluez5/modemmanager.c b/spa/plugins/bluez5/modemmanager.c new file mode 100644 index 0000000..d9df95a --- /dev/null +++ b/spa/plugins/bluez5/modemmanager.c @@ -0,0 +1,1249 @@ +/* Spa Bluez5 ModemManager proxy + * + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <errno.h> +#include <spa/utils/string.h> + +#include <ModemManager.h> + +#include "modemmanager.h" + +#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager" + +struct modem { + char *path; + bool network_has_service; + unsigned int signal_strength; +}; + +struct impl { + struct spa_bt_monitor *monitor; + + struct spa_log *log; + DBusConnection *conn; + + char *allowed_modem_device; + bool filters_added; + DBusPendingCall *pending; + DBusPendingCall *voice_pending; + + const struct mm_ops *ops; + void *user_data; + + struct modem modem; + struct spa_list call_list; +}; + +struct dbus_cmd_data { + struct impl *this; + struct call *call; + void *user_data; +}; + +static bool mm_dbus_connection_send_with_reply(struct impl *this, DBusMessage *m, DBusPendingCall **pending_return, + DBusPendingCallNotifyFunction function, void *user_data) +{ + dbus_bool_t dbus_ret; + + spa_assert(*pending_return == NULL); + + dbus_ret = dbus_connection_send_with_reply(this->conn, m, pending_return, -1); + if (!dbus_ret || *pending_return == NULL) { + spa_log_debug(this->log, "dbus call failure"); + return false; + } + + dbus_ret = dbus_pending_call_set_notify(*pending_return, function, user_data, NULL); + if (!dbus_ret) { + spa_log_debug(this->log, "dbus set notify failure"); + dbus_pending_call_cancel(*pending_return); + dbus_pending_call_unref(*pending_return); + *pending_return = NULL; + return false; + } + + return true; +} + +static int mm_state_to_clcc(struct impl *this, MMCallState state) +{ + switch (state) { + case MM_CALL_STATE_DIALING: + return CLCC_DIALING; + case MM_CALL_STATE_RINGING_OUT: + return CLCC_ALERTING; + case MM_CALL_STATE_RINGING_IN: + return CLCC_INCOMING; + case MM_CALL_STATE_ACTIVE: + return CLCC_ACTIVE; + case MM_CALL_STATE_HELD: + return CLCC_HELD; + case MM_CALL_STATE_WAITING: + return CLCC_WAITING; + case MM_CALL_STATE_TERMINATED: + case MM_CALL_STATE_UNKNOWN: + default: + return -1; + } +} + +static void mm_call_state_changed(struct impl *this) +{ + struct call *call; + bool call_indicator = false; + enum call_setup call_setup_indicator = CIND_CALLSETUP_NONE; + + spa_list_for_each(call, &this->call_list, link) { + call_indicator |= (call->state == CLCC_ACTIVE); + + if (call->state == CLCC_INCOMING && call_setup_indicator < CIND_CALLSETUP_INCOMING) + call_setup_indicator = CIND_CALLSETUP_INCOMING; + else if (call->state == CLCC_DIALING && call_setup_indicator < CIND_CALLSETUP_DIALING) + call_setup_indicator = CIND_CALLSETUP_DIALING; + else if (call->state == CLCC_ALERTING && call_setup_indicator < CIND_CALLSETUP_ALERTING) + call_setup_indicator = CIND_CALLSETUP_ALERTING; + } + + if (this->ops->set_call_active) + this->ops->set_call_active(call_indicator, this->user_data); + + if (this->ops->set_call_setup) + this->ops->set_call_setup(call_setup_indicator, this->user_data); +} + +static void mm_get_call_properties_reply(DBusPendingCall *pending, void *user_data) +{ + struct call *call = user_data; + struct impl *this = call->this; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + MMCallDirection direction; + MMCallState state; + + spa_assert(call->pending == pending); + dbus_pending_call_unref(pending); + call->pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(this->log, "ModemManager D-Bus Call not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "GetAll() failed: %s", dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !spa_streq(dbus_message_get_signature(r), "a{sv}")) { + spa_log_error(this->log, "Invalid arguments in GetAll() reply"); + goto finish; + } + + spa_log_debug(this->log, "Call path: %s", call->path); + + dbus_message_iter_recurse(&arg_i, &element_i); + while (dbus_message_iter_get_arg_type(&element_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if (spa_streq(key, MM_CALL_PROPERTY_DIRECTION)) { + dbus_message_iter_get_basic(&value_i, &direction); + spa_log_debug(this->log, "Call direction: %u", direction); + call->direction = (direction == MM_CALL_DIRECTION_INCOMING) ? CALL_INCOMING : CALL_OUTGOING; + } else if (spa_streq(key, MM_CALL_PROPERTY_NUMBER)) { + char *number; + + dbus_message_iter_get_basic(&value_i, &number); + spa_log_debug(this->log, "Call number: %s", number); + if (call->number) + free(call->number); + call->number = strdup(number); + } else if (spa_streq(key, MM_CALL_PROPERTY_STATE)) { + int clcc_state; + + dbus_message_iter_get_basic(&value_i, &state); + spa_log_debug(this->log, "Call state: %u", state); + clcc_state = mm_state_to_clcc(this, state); + if (clcc_state < 0) { + spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state); + } else { + call->state = clcc_state; + mm_call_state_changed(this); + } + } + + dbus_message_iter_next(&element_i); + } + +finish: + dbus_message_unref(r); +} + +static DBusHandlerResult mm_parse_voice_properties(struct impl *this, DBusMessageIter *props_i) +{ + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i, element_i; + const char *key; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if (spa_streq(key, MM_MODEM_VOICE_PROPERTY_CALLS)) { + spa_log_debug(this->log, "Voice properties"); + dbus_message_iter_recurse(&value_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_OBJECT_PATH) { + const char *call_object; + + dbus_message_iter_get_basic(&element_i, &call_object); + spa_log_debug(this->log, " Call: %s", call_object); + + dbus_message_iter_next(&element_i); + } + } + + dbus_message_iter_next(props_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult mm_parse_modem3gpp_properties(struct impl *this, DBusMessageIter *props_i) +{ + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) { + char *operator_name; + + dbus_message_iter_get_basic(&value_i, &operator_name); + spa_log_debug(this->log, "Network operator code: %s", operator_name); + if (this->ops->set_modem_operator_name) + this->ops->set_modem_operator_name(operator_name, this->user_data); + } else if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) { + MMModem3gppRegistrationState state; + bool is_roaming; + + dbus_message_iter_get_basic(&value_i, &state); + spa_log_debug(this->log, "Registration state: %d", state); + + if (state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING || + state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED || + state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY) + is_roaming = true; + else + is_roaming = false; + + if (this->ops->set_modem_roaming) + this->ops->set_modem_roaming(is_roaming, this->user_data); + } + + dbus_message_iter_next(props_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult mm_parse_modem_properties(struct impl *this, DBusMessageIter *props_i) +{ + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if(spa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) { + char *imei; + + dbus_message_iter_get_basic(&value_i, &imei); + spa_log_debug(this->log, "Modem IMEI: %s", imei); + } else if(spa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) { + char *manufacturer; + + dbus_message_iter_get_basic(&value_i, &manufacturer); + spa_log_debug(this->log, "Modem manufacturer: %s", manufacturer); + } else if(spa_streq(key, MM_MODEM_PROPERTY_MODEL)) { + char *model; + + dbus_message_iter_get_basic(&value_i, &model); + spa_log_debug(this->log, "Modem model: %s", model); + } else if (spa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) { + char *number; + DBusMessageIter array_i; + + dbus_message_iter_recurse(&value_i, &array_i); + if (dbus_message_iter_get_arg_type(&array_i) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&array_i, &number); + spa_log_debug(this->log, "Modem own number: %s", number); + if (this->ops->set_modem_own_number) + this->ops->set_modem_own_number(number, this->user_data); + } + } else if(spa_streq(key, MM_MODEM_PROPERTY_REVISION)) { + char *revision; + + dbus_message_iter_get_basic(&value_i, &revision); + spa_log_debug(this->log, "Modem revision: %s", revision); + } else if(spa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) { + unsigned int percentage, signal_strength; + DBusMessageIter struct_i; + + dbus_message_iter_recurse(&value_i, &struct_i); + if (dbus_message_iter_get_arg_type(&struct_i) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&struct_i, &percentage); + signal_strength = (unsigned int) round(percentage / 20.0); + spa_log_debug(this->log, "Network signal strength: %d (%d)", percentage, signal_strength); + if(this->ops->set_modem_signal_strength) + this->ops->set_modem_signal_strength(signal_strength, this->user_data); + } + } else if(spa_streq(key, MM_MODEM_PROPERTY_STATE)) { + MMModemState state; + bool has_service; + + dbus_message_iter_get_basic(&value_i, &state); + spa_log_debug(this->log, "Network state: %d", state); + + has_service = (state >= MM_MODEM_STATE_REGISTERED); + if (this->ops->set_modem_service) + this->ops->set_modem_service(has_service, this->user_data); + } + + dbus_message_iter_next(props_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult mm_parse_interfaces(struct impl *this, DBusMessageIter *dict_i) +{ + DBusMessageIter element_i, props_i; + const char *path; + + spa_assert(this); + spa_assert(dict_i); + + dbus_message_iter_get_basic(dict_i, &path); + dbus_message_iter_next(dict_i); + dbus_message_iter_recurse(dict_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter iface_i; + const char *interface; + + dbus_message_iter_recurse(&element_i, &iface_i); + dbus_message_iter_get_basic(&iface_i, &interface); + dbus_message_iter_next(&iface_i); + spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&iface_i, &props_i); + + if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) { + spa_log_debug(this->log, "Found Modem interface %s, path %s", interface, path); + if (this->modem.path == NULL) { + if (this->allowed_modem_device) { + DBusMessageIter i; + + dbus_message_iter_recurse(&iface_i, &i); + while (dbus_message_iter_get_arg_type(&i) != DBUS_TYPE_INVALID) { + DBusMessageIter key_i, value_i; + const char *key; + + dbus_message_iter_recurse(&i, &key_i); + + dbus_message_iter_get_basic(&key_i, &key); + dbus_message_iter_next(&key_i); + dbus_message_iter_recurse(&key_i, &value_i); + + if (spa_streq(key, MM_MODEM_PROPERTY_DEVICE)) { + char *device; + + dbus_message_iter_get_basic(&value_i, &device); + if (!spa_streq(this->allowed_modem_device, device)) { + spa_log_debug(this->log, "Modem not allowed: %s", device); + goto next; + } + } + dbus_message_iter_next(&i); + } + } + this->modem.path = strdup(path); + } else if (!spa_streq(this->modem.path, path)) { + spa_log_debug(this->log, "A modem is already registered"); + goto next; + } + mm_parse_modem_properties(this, &props_i); + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + if (spa_streq(this->modem.path, path)) { + spa_log_debug(this->log, "Found Modem3GPP interface %s, path %s", interface, path); + mm_parse_modem3gpp_properties(this, &props_i); + } + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + if (spa_streq(this->modem.path, path)) { + spa_log_debug(this->log, "Found Voice interface %s, path %s", interface, path); + mm_parse_voice_properties(this, &props_i); + } + } + +next: + dbus_message_iter_next(&element_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void mm_get_managed_objects_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *this = user_data; + DBusMessage *r; + DBusMessageIter i, array_i; + + spa_assert(this->pending == pending); + dbus_pending_call_unref(pending); + this->pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "Failed to get a list of endpoints from ModemManager: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) { + spa_log_error(this->log, "Invalid arguments in GetManagedObjects() reply"); + goto finish; + } + + dbus_message_iter_recurse(&i, &array_i); + while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&array_i, &dict_i); + mm_parse_interfaces(this, &dict_i); + dbus_message_iter_next(&array_i); + } + +finish: + dbus_message_unref(r); +} + +static void call_free(struct call *call) { + spa_list_remove(&call->link); + + if (call->pending != NULL) { + dbus_pending_call_cancel(call->pending); + dbus_pending_call_unref(call->pending); + } + + if (call->number) + free(call->number); + if (call->path) + free(call->path); + free(call); +} + +static void mm_clean_voice(struct impl *this) +{ + struct call *call; + + spa_list_consume(call, &this->call_list, link) + call_free(call); + + if (this->voice_pending != NULL) { + dbus_pending_call_cancel(this->voice_pending); + dbus_pending_call_unref(this->voice_pending); + } + + if (this->ops->set_call_setup) + this->ops->set_call_setup(CIND_CALLSETUP_NONE, this->user_data); + if (this->ops->set_call_active) + this->ops->set_call_active(false, this->user_data); +} + +static void mm_clean_modem3gpp(struct impl *this) +{ + if (this->ops->set_modem_operator_name) + this->ops->set_modem_operator_name(NULL, this->user_data); + if (this->ops->set_modem_roaming) + this->ops->set_modem_roaming(false, this->user_data); +} + +static void mm_clean_modem(struct impl *this) +{ + if (this->modem.path) { + free(this->modem.path); + this->modem.path = NULL; + } + if(this->ops->set_modem_signal_strength) + this->ops->set_modem_signal_strength(0, this->user_data); + if (this->ops->set_modem_service) + this->ops->set_modem_service(false, this->user_data); + this->modem.network_has_service = false; +} + +static DBusHandlerResult mm_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct impl *this = user_data; + DBusError err; + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + + spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m)); + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto finish; + } + + if (spa_streq(name, MM_DBUS_SERVICE)) { + if (old_owner && *old_owner) { + spa_log_debug(this->log, "ModemManager daemon disappeared (%s)", old_owner); + mm_clean_voice(this); + mm_clean_modem3gpp(this); + mm_clean_modem(this); + } + + if (new_owner && *new_owner) + spa_log_debug(this->log, "ModemManager daemon appeared (%s)", new_owner); + } + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_ADDED)) { + DBusMessageIter arg_i; + + spa_log_warn(this->log, "sender: %s", dbus_message_get_sender(m)); + + if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) { + spa_log_error(this->log, "Invalid signature found in InterfacesAdded"); + goto finish; + } + + mm_parse_interfaces(this, &arg_i); + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_REMOVED)) { + const char *path; + DBusMessageIter arg_i, element_i; + + if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oas")) { + spa_log_error(this->log, "Invalid signature found in InterfacesRemoved"); + goto finish; + } + + dbus_message_iter_get_basic(&arg_i, &path); + if (!spa_streq(this->modem.path, path)) + goto finish; + + dbus_message_iter_next(&arg_i); + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) { + const char *iface; + + dbus_message_iter_get_basic(&element_i, &iface); + + spa_log_debug(this->log, "Interface removed %s", path); + if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM)) { + spa_log_debug(this->log, "Modem interface %s removed, path %s", iface, path); + mm_clean_modem(this); + } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + spa_log_debug(this->log, "Modem3GPP interface %s removed, path %s", iface, path); + mm_clean_modem3gpp(this); + } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + spa_log_debug(this->log, "Voice interface %s removed, path %s", iface, path); + mm_clean_voice(this); + } + + dbus_message_iter_next(&element_i); + } + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) { + const char *path; + DBusMessageIter iface_i, props_i; + const char *interface; + + path = dbus_message_get_path(m); + if (!spa_streq(this->modem.path, path)) + goto finish; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) { + spa_log_error(this->log, "Invalid signature found in PropertiesChanged"); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &interface); + dbus_message_iter_next(&iface_i); + spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&iface_i, &props_i); + + if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) { + spa_log_debug(this->log, "Properties changed on %s", path); + mm_parse_modem_properties(this, &props_i); + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + spa_log_debug(this->log, "Properties changed on %s", path); + mm_parse_modem3gpp_properties(this, &props_i); + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + spa_log_debug(this->log, "Properties changed on %s", path); + mm_parse_voice_properties(this, &props_i); + } + } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLADDED)) { + DBusMessageIter iface_i; + const char *path; + struct call *call_object; + const char *mm_call_interface = MM_DBUS_INTERFACE_CALL; + + if (!spa_streq(this->modem.path, dbus_message_get_path(m))) + goto finish; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) { + spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLADDED); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &path); + spa_log_debug(this->log, "New call: %s", path); + + call_object = calloc(1, sizeof(struct call)); + if (call_object == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + call_object->this = this; + call_object->path = strdup(path); + spa_list_append(&this->call_list, &call_object->link); + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "GetAll"); + if (m == NULL) + goto finish; + dbus_message_append_args(m, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID); + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_properties_reply, call_object)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + goto finish; + } + } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLDELETED)) { + const char *path; + DBusMessageIter iface_i; + struct call *call, *call_tmp; + + if (!spa_streq(this->modem.path, dbus_message_get_path(m))) + goto finish; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) { + spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLDELETED); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &path); + spa_log_debug(this->log, "Call ended: %s", path); + + spa_list_for_each_safe(call, call_tmp, &this->call_list, link) { + if (spa_streq(call->path, path)) + call_free(call); + } + mm_call_state_changed(this); + } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_CALL, MM_CALL_SIGNAL_STATECHANGED)) { + const char *path; + DBusMessageIter iface_i; + MMCallState old, new; + MMCallStateReason reason; + struct call *call = NULL, *call_tmp; + int clcc_state; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "iiu")) { + spa_log_error(this->log, "Invalid signature found in %s", MM_CALL_SIGNAL_STATECHANGED); + goto finish; + } + + path = dbus_message_get_path(m); + + dbus_message_iter_get_basic(&iface_i, &old); + dbus_message_iter_next(&iface_i); + dbus_message_iter_get_basic(&iface_i, &new); + dbus_message_iter_next(&iface_i); + dbus_message_iter_get_basic(&iface_i, &reason); + + spa_log_debug(this->log, "Call state %s changed to %d (old = %d, reason = %u)", path, new, old, reason); + + spa_list_for_each(call_tmp, &this->call_list, link) { + if (spa_streq(call_tmp->path, path)) { + call = call_tmp; + break; + } + } + + if (call == NULL) { + spa_log_warn(this->log, "No call reference for %s", path); + goto finish; + } + + clcc_state = mm_state_to_clcc(this, new); + if (clcc_state < 0) { + spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state); + } else { + call->state = clcc_state; + mm_call_state_changed(this); + } + } + +finish: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int add_filters(struct impl *this) +{ + DBusError err; + + if (this->filters_added) + return 0; + + dbus_error_init(&err); + + if (!dbus_connection_add_filter(this->conn, mm_filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + goto fail; + } + + dbus_bus_add_match(this->conn, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" MM_DBUS_SERVICE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_ADDED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_REMOVED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLADDED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLDELETED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" MM_DBUS_INTERFACE_CALL "',member='" MM_CALL_SIGNAL_STATECHANGED "'", &err); + + this->filters_added = true; + + return 0; + +fail: + dbus_error_free(&err); + return -EIO; +} + +static bool is_dbus_service_available(struct impl *this, const char *service) +{ + DBusMessage *m, *r; + DBusError err; + bool success = false; + + m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "NameHasOwner"); + if (m == NULL) + return false; + dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_info(this->log, "NameHasOwner failed for %s", service); + dbus_error_free(&err); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_get_args(r, &err, + DBUS_TYPE_BOOLEAN, &success, + DBUS_TYPE_INVALID)) { + spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message); + dbus_error_free(&err); + goto finish; + } + +finish: + if (r) + dbus_message_unref(r); + + return success; +} + +bool mm_is_available(void *modemmanager) +{ + struct impl *this = modemmanager; + + if (this == NULL) + return false; + + return this->modem.path != NULL; +} + +unsigned int mm_supported_features() +{ + return SPA_BT_HFP_AG_FEATURE_REJECT_CALL | SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS; +} + +static void mm_get_call_simple_reply(DBusPendingCall *pending, void *data) +{ + struct dbus_cmd_data *dbus_cmd_data = data; + struct impl *this = dbus_cmd_data->this; + struct call *call = dbus_cmd_data->call; + void *user_data = dbus_cmd_data->user_data; + DBusMessage *r; + + free(data); + + spa_assert(call->pending == pending); + dbus_pending_call_unref(pending); + call->pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(this->log, "ModemManager D-Bus method not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r)); + goto finish; + } + + this->ops->send_cmd_result(true, 0, user_data); + return; + +finish: + this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data); +} + +static void mm_get_call_create_reply(DBusPendingCall *pending, void *data) +{ + struct dbus_cmd_data *dbus_cmd_data = data; + struct impl *this = dbus_cmd_data->this; + void *user_data = dbus_cmd_data->user_data; + DBusMessage *r; + + free(data); + + spa_assert(this->voice_pending == pending); + dbus_pending_call_unref(pending); + this->voice_pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(this->log, "ModemManager D-Bus method not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r)); + goto finish; + } + + this->ops->send_cmd_result(true, 0, user_data); + return; + +finish: + this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data); +} + +bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + struct dbus_cmd_data *data; + DBusMessage *m; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_INCOMING) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_log_debug(this->log, "No ringing in call"); + if (error) + *error = CMEE_OPERATION_NOT_ALLOWED; + return false; + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->call = call_object; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_ACCEPT); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + struct dbus_cmd_data *data; + DBusMessage *m; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_ACTIVE) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_DIALING || + call_tmp->state == CLCC_ALERTING || + call_tmp->state == CLCC_INCOMING) { + call_object = call_tmp; + break; + } + } + } + if (!call_object) { + spa_log_debug(this->log, "No call to reject or hang up"); + if (error) + *error = CMEE_OPERATION_NOT_ALLOWED; + return false; + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->call = call_object; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_HANGUP); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) { + DBusMessageIter dict_entry_it, variant_it; + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it); + dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key); + + dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it); + dbus_message_iter_append_basic(&variant_it, variant_type_int, variant); + dbus_message_iter_close_container(&dict_entry_it, &variant_it); + dbus_message_iter_close_container(dict, &dict_entry_it); +} + +static inline bool is_valid_dial_string_char(char c) +{ + return ('0' <= c && c <= '9') + || ('A' <= c && c <= 'C') + || c == '*' + || c == '#' + || c == '+'; +} + +bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct dbus_cmd_data *data; + DBusMessage *m; + DBusMessageIter iter, dict; + + for (size_t i = 0; number[i]; i++) { + if (!is_valid_dial_string_char(number[i])) { + spa_log_warn(this->log, "Call creation canceled, invalid character found in dial string: %c", number[i]); + if (error) + *error = CMEE_INVALID_CHARACTERS_DIAL_STRING; + return false; + } + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, this->modem.path, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_METHOD_CREATECALL); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + dbus_message_iter_init_append(m, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + append_basic_variant_dict_entry(&dict, "number", DBUS_TYPE_STRING, "s", &number); + dbus_message_iter_close_container(&iter, &dict); + if (!mm_dbus_connection_send_with_reply(this, m, &this->voice_pending, mm_get_call_create_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + struct dbus_cmd_data *data; + DBusMessage *m; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_ACTIVE) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_log_debug(this->log, "No active call"); + if (error) + *error = CMEE_OPERATION_NOT_ALLOWED; + return false; + } + + /* Allowed dtmf characters: 0-9, *, #, A-D */ + if (!((dtmf[0] >= '0' && dtmf[0] <= '9') + || (dtmf[0] == '*') + || (dtmf[0] == '#') + || (dtmf[0] >= 'A' && dtmf[0] <= 'D'))) { + spa_log_debug(this->log, "Invalid DTMF character: %s", dtmf); + if (error) + *error = CMEE_INVALID_CHARACTERS_TEXT_STRING; + return false; + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->call = call_object; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_SENDDTMF); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + dbus_message_append_args(m, DBUS_TYPE_STRING, &dtmf, DBUS_TYPE_INVALID); + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +const char *mm_get_incoming_call_number(void *modemmanager) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_INCOMING) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_log_debug(this->log, "No ringing in call"); + return NULL; + } + + return call_object->number; +} + +struct spa_list *mm_get_calls(void *modemmanager) +{ + struct impl *this = modemmanager; + + return &this->call_list; +} + +void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info, + const struct mm_ops *ops, void *user_data) +{ + struct impl *this; + const char *modem_device_str = NULL; + bool modem_device_found = false; + + spa_assert(log); + spa_assert(dbus_connection); + + if (info) { + if ((modem_device_str = spa_dict_lookup(info, "bluez5.hfphsp-backend-native-modem")) != NULL) { + if (!spa_streq(modem_device_str, "none")) + modem_device_found = true; + } + } + if (!modem_device_found) { + spa_log_info(log, "No modem allowed, doesn't link to ModemManager"); + return NULL; + } + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) + return NULL; + + this->log = log; + this->conn = dbus_connection; + this->ops = ops; + this->user_data = user_data; + if (modem_device_str && !spa_streq(modem_device_str, "any")) + this->allowed_modem_device = strdup(modem_device_str); + spa_list_init(&this->call_list); + + if (add_filters(this) < 0) { + goto fail; + } + + if (is_dbus_service_available(this, MM_DBUS_SERVICE)) { + DBusMessage *m; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, "/org/freedesktop/ModemManager1", + DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects"); + if (m == NULL) + goto fail; + + if (!mm_dbus_connection_send_with_reply(this, m, &this->pending, mm_get_managed_objects_reply, this)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + goto fail; + } + } + + return this; + +fail: + free(this); + return NULL; +} + +void mm_unregister(void *data) +{ + struct impl *this = data; + + if (this->pending != NULL) { + dbus_pending_call_cancel(this->pending); + dbus_pending_call_unref(this->pending); + } + + mm_clean_voice(this); + mm_clean_modem3gpp(this); + mm_clean_modem(this); + + if (this->filters_added) { + dbus_connection_remove_filter(this->conn, mm_filter_cb, this); + this->filters_added = false; + } + + if (this->allowed_modem_device) + free(this->allowed_modem_device); + + free(this); +} |