From 67fc83f344dc0a308aad5619621edff3307f54d6 Mon Sep 17 00:00:00 2001 From: Dylan Van Assche Date: Thu, 7 Apr 2022 20:30:24 +0200 Subject: [PATCH 05/26] bluetooth: add ModemManager backend ModemManager is an alternative to oFono for interacting with modems. Add a ModemManager backend to better support Bluetooth HFP 1.6 with cellular support such as making and rejecting calls, retrieving cellular status and in-band ringing. --- .gitlab-ci.yml | 3 +- meson.build | 2 + src/modules/bluetooth/bluez5-util.h | 7 + src/modules/bluetooth/meson.build | 4 +- src/modules/bluetooth/modemmanager.c | 1498 ++++++++++++++++++++++++++ src/modules/bluetooth/modemmanager.h | 98 ++ 6 files changed, 1610 insertions(+), 2 deletions(-) create mode 100644 src/modules/bluetooth/modemmanager.c create mode 100644 src/modules/bluetooth/modemmanager.h diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e6b36da87..f2e20dd64 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ variables: # CI runs, for example when adding new packages to FDO_DISTRIBUTION_PACKAGES. # The tag is an arbitrary string that identifies the exact container # contents. - FDO_DISTRIBUTION_TAG: '2021-11-03-00' + FDO_DISTRIBUTION_TAG: '2022-04-20-00' FDO_DISTRIBUTION_VERSION: '20.04' FDO_UPSTREAM_REPO: 'pulseaudio/pulseaudio' UBUNTU_IMAGE: "$CI_REGISTRY_IMAGE/ubuntu/$FDO_DISTRIBUTION_VERSION:$FDO_DISTRIBUTION_TAG" @@ -81,6 +81,7 @@ build-container: libxml2-utils libxtst-dev m4 + modemmanager-dev ninja-build pkg-config python3-setuptools diff --git a/meson.build b/meson.build index 3ae7ac69a..063390c7f 100644 --- a/meson.build +++ b/meson.build @@ -773,6 +773,8 @@ if get_option('daemon') endif endif + modemmanager_dep = dependency('ModemManager', version : '>= 1.10.0', required : get_option('bluez5-native-headset')) + jack_dep = dependency('jack', version : '>= 0.117.0', required : get_option('jack')) if jack_dep.found() cdata.set('HAVE_JACK', 1) diff --git a/src/modules/bluetooth/bluez5-util.h b/src/modules/bluetooth/bluez5-util.h index a24a0b823..f69176425 100644 --- a/src/modules/bluetooth/bluez5-util.h +++ b/src/modules/bluetooth/bluez5-util.h @@ -64,6 +64,7 @@ typedef struct pa_bluetooth_adapter pa_bluetooth_adapter; typedef struct pa_bluetooth_discovery pa_bluetooth_discovery; typedef struct pa_bluetooth_backend pa_bluetooth_backend; typedef struct pa_upower_backend pa_upower_backend; +typedef struct pa_modemmanager_backend pa_modemmanager_backend; typedef enum pa_bluetooth_hook { PA_BLUETOOTH_HOOK_ADAPTER_UUIDS_CHANGED, /* Call data: pa_bluetooth_adapter */ @@ -71,6 +72,12 @@ typedef enum pa_bluetooth_hook { PA_BLUETOOTH_HOOK_DEVICE_UNLINK, /* Call data: pa_bluetooth_device */ PA_BLUETOOTH_HOOK_DEVICE_BATTERY_LEVEL_CHANGED, /* Call data: pa_bluetooth_device */ PA_BLUETOOTH_HOOK_HOST_BATTERY_LEVEL_CHANGED, /* Call data: pa_upower_backend */ + PA_BLUETOOTH_HOOK_HOST_CALLS_CHANGED, /* Call data: pa_modemmanager_transport */ + PA_BLUETOOTH_HOOK_HOST_SIGNAL_STRENGTH_CHANGED, /* Call data: pa_modemmanager_transport */ + PA_BLUETOOTH_HOOK_HOST_HAS_SERVICE_CHANGED, /* Call data: pa_modemmanager_transport */ + PA_BLUETOOTH_HOOK_HOST_IS_ROAMING_CHANGED, /* Call data: pa_modemmanager_transport */ + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED, /* Call data: pa_modemmanager_transport */ + PA_BLUETOOTH_HOOK_HOST_OPERATION_SUCCEED, /* Call data: pa_modemmanager_transport */ PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED, /* Call data: pa_bluetooth_transport */ PA_BLUETOOTH_HOOK_TRANSPORT_SOURCE_VOLUME_CHANGED, /* Call data: pa_bluetooth_transport */ PA_BLUETOOTH_HOOK_TRANSPORT_SINK_VOLUME_CHANGED, /* Call data: pa_bluetooth_transport */ diff --git a/src/modules/bluetooth/meson.build b/src/modules/bluetooth/meson.build index 0703d5086..fdcc8a521 100644 --- a/src/modules/bluetooth/meson.build +++ b/src/modules/bluetooth/meson.build @@ -18,6 +18,8 @@ if get_option('bluez5-native-headset') libbluez5_util_sources += [ 'backend-native.c' ] libbluez5_util_sources += [ 'upower.c' ] libbluez5_util_headers += [ 'upower.h' ] + libbluez5_util_sources += [ 'modemmanager.c' ] + libbluez5_util_headers += [ 'modemmanager.h' ] endif if get_option('bluez5-ofono-headset') @@ -37,7 +39,7 @@ libbluez5_util = shared_library('bluez5-util', c_args : [pa_c_args, server_c_args], link_args : [nodelete_link_args], include_directories : [configinc, topinc], - dependencies : [libpulse_dep, libpulsecommon_dep, libpulsecore_dep, bluez_dep, dbus_dep, sbc_dep, libintl_dep, bluez5_gst_dep, bluez5_gstapp_dep, libm_dep], + dependencies : [libpulse_dep, libpulsecommon_dep, libpulsecore_dep, bluez_dep, dbus_dep, sbc_dep, libintl_dep, bluez5_gst_dep, bluez5_gstapp_dep, libm_dep, modemmanager_dep], install : true, install_rpath : privlibdir, install_dir : modlibexecdir, diff --git a/src/modules/bluetooth/modemmanager.c b/src/modules/bluetooth/modemmanager.c new file mode 100644 index 000000000..60c3fabec --- /dev/null +++ b/src/modules/bluetooth/modemmanager.c @@ -0,0 +1,1498 @@ +/*** + This file is part of PulseAudio. + + Copyright 2022 Dylan Van Assche + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + PulseAudio 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 Lesser General Public + License along with PulseAudio; if not, see . +***/ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "modemmanager.h" + +static pa_dbus_pending* send_and_add_to_pending(pa_modemmanager_backend *backend, DBusMessage *m, + DBusPendingCallNotifyFunction func, void *call_data) { + + pa_dbus_pending *p; + DBusPendingCall *call; + + pa_assert(backend); + pa_assert(m); + + pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(backend->connection), m, &call, -1)); + + p = pa_dbus_pending_new(pa_dbus_connection_get(backend->connection), m, call, backend, call_data); + PA_LLIST_PREPEND(pa_dbus_pending, backend->pending, p); + dbus_pending_call_set_notify(call, func, p, NULL); + + return p; +} + +static const char *check_variant_property(DBusMessageIter *i) { + const char *key; + + pa_assert(i); + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_STRING) { + pa_log_error("Property name not a string."); + return NULL; + } + + dbus_message_iter_get_basic(i, &key); + + if (!dbus_message_iter_next(i)) { + pa_log_error("Property value missing"); + return NULL; + } + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_VARIANT) { + pa_log_error("Property value not a variant."); + return NULL; + } + + return key; +} + +static void create_call_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Voice not available"); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("CreateCall() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_SUCCEED), b); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void call_hangup_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Call not available"); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("Hangup() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_SUCCEED), b); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void call_accept_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Call not available"); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("Accept() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_SUCCEED), b); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void send_dtmf_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Call not available"); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("SendDtmf() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), b); + goto finish; + } + + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_SUCCEED), b); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void parse_state(pa_modemmanager_backend *b, DBusMessageIter *i) { + MMModemState state; + bool has_service; + + pa_assert(i); + + /* + * ModemManager emits the State property as UINT32 or INT32. + * Cast to unsigned int if needed. + */ + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_UINT32 + || dbus_message_iter_get_arg_type(i) == DBUS_TYPE_INT32); + + if (dbus_message_iter_get_arg_type(i) == DBUS_TYPE_INT32) { + int s; + + dbus_message_iter_get_basic(i, &s); + state = (unsigned int) s; + } else + dbus_message_iter_get_basic(i, &state); + + /* All states from REGISTERED and higher indicate that the modem has service */ + if (state >= MM_MODEM_STATE_REGISTERED) + has_service = true; + else + has_service = false; + + pa_log_debug("Network has service: %s", pa_yes_no(has_service)); + + if (b->modem->network_has_service != has_service) { + b->modem->network_has_service = has_service; + pa_log_debug("AG service status updated: %s", pa_yes_no(b->modem->network_has_service)); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_HAS_SERVICE_CHANGED), b); + } +} + +static void parse_registration(pa_modemmanager_backend *b, DBusMessageIter *i) { + MMModem3gppRegistrationState registration; + bool is_roaming; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_UINT32); + + dbus_message_iter_get_basic(i, ®istration); + + /* All states containing ROAMING indicate that the modem is roaming */ + if (registration == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING + || registration == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED + || registration == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY) { + is_roaming = true; + } else + is_roaming = false; + + pa_log_debug("Network is roaming: %s", pa_yes_no(is_roaming)); + + if (b->modem->network_is_roaming != is_roaming) { + b->modem->network_is_roaming = is_roaming; + pa_log_debug("AG roaming status updated: %s", pa_yes_no(b->modem->network_is_roaming)); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_IS_ROAMING_CHANGED), b); + } +} + +static void parse_signal_quality(pa_modemmanager_backend *b, DBusMessageIter *i) { + unsigned int percentage, signal_strength; + DBusMessageIter structIter; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRUCT); + + dbus_message_iter_recurse(i, &structIter); + + if (dbus_message_iter_get_arg_type(&structIter) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&structIter, &percentage); + signal_strength = (unsigned int) round(percentage / 20.0); + + pa_log_debug("Network signal strength: %d/100", percentage); + + if (b->modem->network_signal_strength != signal_strength) { + b->modem->network_signal_strength = signal_strength; + pa_log_debug("AG signal strength updated (%d/5)", b->modem->network_signal_strength); + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_SIGNAL_STRENGTH_CHANGED), b); + } + } +} + +static void parse_operator_name(pa_modemmanager_backend *b, DBusMessageIter *i) { + char *name; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &name); + if (b->modem->network_operator_name) + pa_xfree(b->modem->network_operator_name); + b->modem->network_operator_name = pa_xstrdup(name); + + pa_log_debug("Network operator name: %s", b->modem->network_operator_name); +} + +static void parse_operator_code(pa_modemmanager_backend *b, DBusMessageIter *i) { + char *code; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &code); + if (b->modem->network_operator_code) + pa_xfree(b->modem->network_operator_code); + b->modem->network_operator_code = pa_xstrdup(code); + + pa_log_debug("Network operator code: %s", b->modem->network_operator_code); +} + +static void parse_own_number(pa_modemmanager_backend *b, DBusMessageIter *i) { + DBusMessageIter element_i; + char *number; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(i, &element_i); + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&element_i, &number); + if (b->modem->modem_own_number) + pa_xfree(b->modem->modem_own_number); + b->modem->modem_own_number = pa_xstrdup(number); + pa_log_debug("Modem own number: %s", b->modem->modem_own_number); + + /* Bluetooth HFP cannot deal with multiple SIMs and numbers */ + break; + + dbus_message_iter_next(&element_i); + } +} + +static void parse_call_number(pa_modemmanager_backend *b, DBusMessageIter *i, const char *call_object) { + char *number; + call_status_t *call_state; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &number); + + call_state = pa_hashmap_get(b->calls, call_object); + if (!call_state) { + return; + } + + if (call_state->number) + pa_xfree(call_state->number); + call_state->number = pa_xstrdup(number); + + pa_log_debug("Call number %s", call_state->number); +} + +static void parse_call_state(pa_modemmanager_backend *b, DBusMessageIter *i, const char *call_object) { + MMCallState state; + call_status_t *call_state; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_INT32); + + dbus_message_iter_get_basic(i, &state); + + call_state = pa_hashmap_get(b->calls, call_object); + if (!call_state) { + return; + } + + switch(state) { + case MM_CALL_STATE_UNKNOWN: + pa_log_warn("Call state unknown"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_INVALID; + break; + case MM_CALL_STATE_ACTIVE: + pa_log_debug("Call active"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_ACTIVE; + break; + /* FIXME: implement three-way calling */ + case MM_CALL_STATE_HELD: + pa_log_warn("Three-way calling is not implemented!"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_INVALID; + break; + case MM_CALL_STATE_DIALING: + pa_log_debug("Call dailing"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_DIALING; + break; + case MM_CALL_STATE_RINGING_OUT: + case MM_CALL_STATE_RINGING_IN: + pa_log_debug("Call ringing"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_RINGING; + break; + /* FIXME: implement three-way calling */ + case MM_CALL_STATE_WAITING: + pa_log_warn("Three-way calling is not implemented!"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_INVALID; + return; + case MM_CALL_STATE_TERMINATED: + pa_log_debug("Call terminated"); + call_state->status = PA_MODEMMANAGER_CALL_STATE_TERMINATED; + break; + default: + pa_assert_not_reached(); + } +} + +static void parse_call_direction(pa_modemmanager_backend *b, DBusMessageIter *i, const char *call_object) { + MMCallDirection direction; + call_status_t *call_state; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_INT32); + + dbus_message_iter_get_basic(i, &direction); + + call_state = pa_hashmap_get(b->calls, call_object); + if (!call_state) { + return; + } + + switch(direction) { + case MM_CALL_DIRECTION_UNKNOWN: + pa_log_warn("Unknown call direction"); + return; + case MM_CALL_DIRECTION_INCOMING: + call_state->is_incoming = true; + break; + case MM_CALL_DIRECTION_OUTGOING: + call_state->is_incoming = false; + break; + default: + pa_assert_not_reached(); + } + + pa_log_debug("Call direction updated. Is incoming? %s", pa_yes_no(call_state->is_incoming)); +} + +static void parse_manufacturer(pa_modemmanager_backend *b, DBusMessageIter *i) { + char *manufacturer; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &manufacturer); + if (b->modem->modem_manufacturer) + pa_xfree(b->modem->modem_manufacturer); + b->modem->modem_manufacturer = pa_xstrdup(manufacturer); + + pa_log_debug("Modem manufacturer: %s", b->modem->modem_manufacturer); +} + +static void parse_model(pa_modemmanager_backend *b, DBusMessageIter *i) { + char *model; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &model); + if (b->modem->modem_model) + pa_xfree(b->modem->modem_model); + b->modem->modem_model = pa_xstrdup(model); + + pa_log_debug("Modem model: %s", b->modem->modem_model); +} + +static void parse_revision(pa_modemmanager_backend *b, DBusMessageIter *i) { + char *revision; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &revision); + if (b->modem->modem_revision) + pa_xfree(b->modem->modem_revision); + b->modem->modem_revision = pa_xstrdup(revision); + + pa_log_debug("Modem revision: %s", b->modem->modem_revision); +} + +static void parse_imei(pa_modemmanager_backend *b, DBusMessageIter *i) { + char *imei; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_STRING); + + dbus_message_iter_get_basic(i, &imei); + if (b->modem->modem_imei) + pa_xfree(b->modem->modem_imei); + b->modem->modem_imei = pa_xstrdup(imei); + + pa_log_debug("Modem IMEI: %s", b->modem->modem_imei); +} + +static void get_modem_properties_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Modem not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("GetAll() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !pa_streq(dbus_message_get_signature(r), "a{sv}")) { + pa_log_error("Invalid reply signature for GetAll()"); + goto finish; + } + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + /* Retrieve property name */ + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property!"); + break; + } + + /* Retrieve and parse property value */ + dbus_message_iter_recurse(&dict_i, &variant_i); + + if(pa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) + parse_own_number(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) + parse_imei(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) + parse_manufacturer(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_MODEL)) + parse_model(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_REVISION)) + parse_revision(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) + parse_signal_quality(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_STATE)) + parse_state(b, &variant_i); + + dbus_message_iter_next(&element_i); + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void get_modem_3gpp_properties_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Modem 3GPP not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("GetAll() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !pa_streq(dbus_message_get_signature(r), "a{sv}")) { + pa_log_error("Invalid reply signature for GetAll()"); + goto finish; + } + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + /* Retrieve property name */ + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property!"); + break; + } + + /* Retrieve and parse property value */ + dbus_message_iter_recurse(&dict_i, &variant_i); + + if(pa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) + parse_registration(b, &variant_i); + else if(pa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORCODE)) + parse_operator_code(b, &variant_i); + else if(pa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) + parse_operator_name(b, &variant_i); + + dbus_message_iter_next(&element_i); + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void get_call_properties_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + char *call_object; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(call_object = p->call_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Call not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("GetAll() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !pa_streq(dbus_message_get_signature(r), "a{sv}")) { + pa_log_error("Invalid reply signature for GetAll()"); + goto finish; + } + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + /* Retrieve property name */ + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property!"); + break; + } + + /* Retrieve and parse property value */ + dbus_message_iter_recurse(&dict_i, &variant_i); + + if(pa_streq(key, MM_CALL_PROPERTY_NUMBER)) + parse_call_number(b, &variant_i, call_object); + else if(pa_streq(key, MM_CALL_PROPERTY_STATE)) + parse_call_state(b, &variant_i, call_object); + else if(pa_streq(key, MM_CALL_PROPERTY_DIRECTION)) + parse_call_direction(b, &variant_i, call_object); + + dbus_message_iter_next(&element_i); + } + + /* Notify change for call */ + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_CALLS_CHANGED), b); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); + + pa_xfree(call_object); +} + +static void parse_calls(pa_modemmanager_backend *b, DBusMessageIter *i) { + const char *mm_call_interface = MM_DBUS_INTERFACE_CALL; + const char *call_object; + DBusMessageIter element_i; + DBusMessage *m; + + pa_assert(i); + pa_assert(dbus_message_iter_get_arg_type(i) == DBUS_TYPE_ARRAY); + + /* Iterate over array of dictonaries */ + dbus_message_iter_recurse(i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_OBJECT_PATH) { + dbus_message_iter_get_basic(&element_i, &call_object); + + if (pa_hashmap_put(b->calls, pa_xstrdup(call_object), pa_xnew0(call_status_t, 1)) != 0) + pa_log_error("Call %s already exist", call_object); + + /* Retrieve call properties */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object, DBUS_INTERFACE_PROPERTIES, DBUS_INTERFACE_PROPERTIES_METHOD_GETALL)); + pa_assert_se(dbus_message_append_args(m, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID)); + send_and_add_to_pending(b, m, get_call_properties_reply, pa_xstrdup(call_object)); + + // FIXME: Implement three-way calling. Only the first call is used. + return; + + dbus_message_iter_next(&element_i); + } +} + +static void get_modem_voice_properties_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus Modem Voice not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("GetAll() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !pa_streq(dbus_message_get_signature(r), "a{sv}")) { + pa_log_error("Invalid reply signature for GetAll()"); + goto finish; + } + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + /* Retrieve property name */ + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property!"); + break; + } + + /* Retrieve and parse property value */ + dbus_message_iter_recurse(&dict_i, &variant_i); + + if(pa_streq(key, MM_MODEM_VOICE_PROPERTY_CALLS)) + parse_calls(b, &variant_i); + + dbus_message_iter_next(&element_i); + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static void parse_interfaces_and_properties(pa_modemmanager_backend *b, DBusMessageIter *dict_i) { + DBusMessage *m; + DBusMessageIter element_i; + const char *path; + const char *mm_modem_interface = MM_DBUS_INTERFACE_MODEM; + const char *mm_modem_3gpp_interface = MM_DBUS_INTERFACE_MODEM_MODEM3GPP; + const char *mm_modem_voice_interface = MM_DBUS_INTERFACE_MODEM_VOICE; + + pa_assert(dbus_message_iter_get_arg_type(dict_i) == DBUS_TYPE_OBJECT_PATH); + dbus_message_iter_get_basic(dict_i, &path); + + pa_assert_se(dbus_message_iter_next(dict_i)); + pa_assert(dbus_message_iter_get_arg_type(dict_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(dict_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + const char *interface; + + dbus_message_iter_recurse(&element_i, &dict_i); + + pa_assert(dbus_message_iter_get_arg_type(&dict_i) == DBUS_TYPE_STRING); + dbus_message_iter_get_basic(&dict_i, &interface); + + pa_assert_se(dbus_message_iter_next(&dict_i)); + pa_assert(dbus_message_iter_get_arg_type(&dict_i) == DBUS_TYPE_ARRAY); + + /* ModemManager Modem inteface */ + if (pa_streq(interface, MM_DBUS_INTERFACE_MODEM)) { + pa_log_debug("Modem Interface %s found", path); + b->mm_modem = pa_xstrdup(path); + + /* Retrieve all modem properties */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, b->mm_modem, DBUS_INTERFACE_PROPERTIES, DBUS_INTERFACE_PROPERTIES_METHOD_GETALL)); + pa_assert_se(dbus_message_append_args(m, + DBUS_TYPE_STRING, &mm_modem_interface, + DBUS_TYPE_INVALID)); + send_and_add_to_pending(b, m, get_modem_properties_reply, NULL); + /* ModemManager Modem 3GPP interface */ + } else if (pa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + pa_log_debug("Modem 3GPP Interface %s found", path); + b->mm_modem_3gpp = pa_xstrdup(path); + + /* Retrieve all network properties */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, b->mm_modem_3gpp, DBUS_INTERFACE_PROPERTIES, DBUS_INTERFACE_PROPERTIES_METHOD_GETALL)); + pa_assert_se(dbus_message_append_args(m, + DBUS_TYPE_STRING, &mm_modem_3gpp_interface, + DBUS_TYPE_INVALID)); + send_and_add_to_pending(b, m, get_modem_3gpp_properties_reply, NULL); + /* ModemManager Modem Voice interface */ + } else if (pa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + pa_log_debug("Modem Voice Interface %s found", path); + b->mm_modem_voice = pa_xstrdup(path); + + /* Retrieve current call list and properties */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, b->mm_modem_voice, DBUS_INTERFACE_PROPERTIES, DBUS_INTERFACE_PROPERTIES_METHOD_GETALL)); + pa_assert_se(dbus_message_append_args(m, + DBUS_TYPE_STRING, &mm_modem_voice_interface, + DBUS_TYPE_INVALID)); + send_and_add_to_pending(b, m, get_modem_voice_properties_reply, NULL); + } else + pa_log_debug("Unknown interface %s found, skipping", interface); + + dbus_message_iter_next(&element_i); + } + + return; +} + +static void invalidate_modem_properties(pa_modemmanager_backend *b) { + if (b->modem) { + if (b->modem->modem_manufacturer) { + pa_xfree(b->modem->modem_manufacturer); + b->modem->modem_manufacturer = NULL; + } + if (b->modem->modem_model) { + pa_xfree(b->modem->modem_model); + b->modem->modem_model = NULL; + } + if (b->modem->modem_revision) { + pa_xfree(b->modem->modem_revision); + b->modem->modem_revision = NULL; + } + if (b->modem->modem_imei) { + pa_xfree(b->modem->modem_imei); + b->modem->modem_imei = NULL; + } + if (b->modem->modem_own_number) { + pa_xfree(b->modem->modem_own_number); + b->modem->modem_own_number = NULL; + } + } +} + +static void invalidate_modem_3gpp_properties(pa_modemmanager_backend *b) { + if (b->modem) { + if (b->modem->network_operator_name) { + pa_xfree(b->modem->network_operator_name); + b->modem->network_operator_name = NULL; + } + if (b->modem->network_operator_code) { + pa_xfree(b->modem->network_operator_code); + b->modem->network_operator_code = NULL; + } + b->modem->network_has_service = false; + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_HAS_SERVICE_CHANGED), b); + b->modem->network_is_roaming = false; + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_IS_ROAMING_CHANGED), b); + b->modem->network_signal_strength = 0; + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_SIGNAL_STRENGTH_CHANGED), b); + } +} + +static void invalidate_modem_voice_properties(pa_modemmanager_backend *b) { + call_status_t *c = NULL; + void *state = NULL; + + /* Remove all call objects */ + if (b->calls) { + while ((c = pa_hashmap_iterate(b->calls, &state, NULL))) { + if (!c) + continue; + + if (c->number) + pa_xfree(c->number); + pa_hashmap_remove_and_free(b->calls, c); + } + + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_CALLS_CHANGED), b); + } +} + +static void get_managed_objects_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_modemmanager_backend *b; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("ModemManager D-Bus ObjectManager not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("GetManagedObjects() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !pa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) { + pa_log_error("Invalid reply signature for GetManagedObjects()"); + goto finish; + } + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&element_i, &dict_i); + parse_interfaces_and_properties(b, &dict_i); + dbus_message_iter_next(&element_i); + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); +} + +static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *data) { + DBusError err; + DBusMessage *m2; + const char *mm_call_interface = MM_DBUS_INTERFACE_CALL; + pa_modemmanager_backend *b = data; + const char *path, *interface, *member; + + pa_assert(bus); + pa_assert(m); + pa_assert(b); + + dbus_error_init(&err); + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member); + + /* ModemManager D-Bus status change */ + if (dbus_message_is_signal(m, DBUS_INTERFACE_DBUS, DBUS_INTERFACE_DBUS_SIGNAL_NAMEOWNERCHANGED)) { + const char *name, *old_owner, *new_owner; + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + pa_log_error("Failed to parse " DBUS_INTERFACE_DBUS ".NameOwnerChanged: %s", err.message); + goto fail; + } + + if (pa_streq(name, MM_DBUS_SERVICE)) { + + /* ModemManager disappeared from D-Bus */ + if (old_owner && *old_owner) { + pa_log_debug("ModemManager disappeared from D-Bus"); + if (b->mm_bus_id) + pa_xfree(b->mm_bus_id); + b->mm_bus_id = NULL; + if (b->mm_modem) + pa_xfree(b->mm_modem); + b->mm_modem = NULL; + if (b->mm_modem_3gpp) + pa_xfree(b->mm_modem_3gpp); + b->mm_modem_3gpp = NULL; + if (b->mm_modem_voice) + pa_xfree(b->mm_modem_voice); + b->mm_modem_voice = NULL; + + /* Invalidate all properties since ModemManager is unavailable */ + invalidate_modem_properties(b); + invalidate_modem_3gpp_properties(b); + invalidate_modem_voice_properties(b); + } + + /* ModemManager appeared on D-Bus, properties are refreshed through the InterfacesAdded signal callback */ + if (new_owner && *new_owner) { + pa_log_debug("ModemManager appeared on D-Bus"); + b->mm_bus_id = pa_xstrdup(dbus_message_get_sender(m)); + if (b->modem) + pa_xfree(b->modem); + b->modem = pa_xnew0(modem_status_t, 1); + } + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + /* Modem added, refresh managed objects */ + else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECT_MANAGER, DBUS_INTERFACE_OBJECT_MANAGER_SIGNAL_INTERFACESADDED)) { + DBusMessageIter arg_i; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) { + pa_log_error("Invalid signature found in InterfacesAdded"); + goto fail; + } + + parse_interfaces_and_properties(b, &arg_i); + } + /* Modem removed, refresh managed objects */ + else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECT_MANAGER, DBUS_INTERFACE_OBJECT_MANAGER_SIGNAL_INTERFACESREMOVED)) { + const char *p; + DBusMessageIter arg_i; + DBusMessageIter element_i; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oas")) { + pa_log_error("Invalid signature found in InterfacesRemoved"); + goto fail; + } + + dbus_message_iter_get_basic(&arg_i, &p); + + pa_assert_se(dbus_message_iter_next(&arg_i)); + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + 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); + + /* ModemManager Modem interface removed */ + if (pa_streq(iface, MM_DBUS_INTERFACE_MODEM)) { + if (b->mm_modem) { + pa_xfree(b->mm_modem); + b->mm_modem = NULL; + } + invalidate_modem_properties(b); + pa_log_debug("Modem Interface removed"); + } + + /* ModemManager Modem 3GPP interface removed */ + if (pa_streq(iface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + if (b->mm_modem_3gpp) { + pa_xfree(b->mm_modem_3gpp); + b->mm_modem_3gpp = NULL; + } + invalidate_modem_3gpp_properties(b); + pa_log_debug("Modem 3GPP Interface removed"); + } + + /* ModemManager Modem Voice interface removed */ + if (pa_streq(iface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + if (b->mm_modem_voice) { + pa_xfree(b->mm_modem_voice); + b->mm_modem_voice = NULL; + } + invalidate_modem_voice_properties(b); + pa_log_debug("Modem Voice Interface removed"); + } + + dbus_message_iter_next(&element_i); + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + /* Update properties if updated by ModemManager */ + else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_INTERFACE_PROPERTIES_SIGNAL_PROPERTIESCHANGED)) { + DBusMessageIter arg_i, element_i; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "sa{sv}as")) { + pa_log_error("Invalid signature found in PropertiesChanged"); + goto fail; + } + + /* Skip interface name */ + pa_assert_se(dbus_message_iter_next(&arg_i)); + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + /* Retrieve property name and parse value */ + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property!"); + break; + } + dbus_message_iter_recurse(&dict_i, &variant_i); + + /* + * ModemManager Modem interface + * See https://www.freedesktop.org/software/ModemManager/api/latest/gdbus-org.freedesktop.ModemManager1.Modem.html + */ + if(b->mm_modem && pa_streq(path, b->mm_modem)) { + pa_log_debug("ModemManager Modem property updated: %s", key); + + if(pa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) + parse_own_number(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) + parse_imei(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) + parse_manufacturer(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_MODEL)) + parse_model(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_REVISION)) + parse_revision(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) + parse_signal_quality(b, &variant_i); + else if(pa_streq(key, MM_MODEM_PROPERTY_STATE)) + parse_state(b, &variant_i); + } + + /* + * ModemManager Modem 3GPP interface + * See https://www.freedesktop.org/software/ModemManager/api/latest/gdbus-org.freedesktop.ModemManager1.Modem.Modem3gpp.html + */ + if(b->mm_modem_3gpp && pa_streq(path, b->mm_modem_3gpp)) { + pa_log_debug("ModemManager Modem 3GPP property updated: %s", key); + + if(pa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) + parse_registration(b, &variant_i); + else if(pa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORCODE)) + parse_operator_code(b, &variant_i); + else if(pa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) + parse_operator_name(b, &variant_i); + } + + dbus_message_iter_next(&element_i); + } + } + /* Call started */ + else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLADDED)) { + DBusMessageIter arg_i; + char *call_object; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "o")) { + pa_log_error("Invalid signature found in CallAdded"); + goto fail; + } + + /* Create call state */ + dbus_message_iter_get_basic(&arg_i, &call_object); + pa_log_debug("Call added: %s", call_object); + + if (pa_hashmap_put(b->calls, pa_xstrdup(call_object), pa_xnew0(call_status_t, 1)) != 0) + pa_log_error("Call %s already exist", call_object); + + /* Retrieve call properties */ + pa_assert_se(m2 = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object, DBUS_INTERFACE_PROPERTIES, DBUS_INTERFACE_PROPERTIES_METHOD_GETALL)); + pa_assert_se(dbus_message_append_args(m2, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID)); + send_and_add_to_pending(b, m2, get_call_properties_reply, pa_xstrdup(call_object)); + } + /* Call rejected or ended */ + else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLDELETED)) { + DBusMessageIter arg_i; + char *call_object; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "o")) { + pa_log_error("Invalid signature found in CallDeleted"); + goto fail; + } + + /* Remove call state */ + dbus_message_iter_get_basic(&arg_i, &call_object); + pa_log_debug("Call removed: %s", call_object); + pa_hashmap_remove_and_free(b->calls, call_object); + + /* Notify about call deletion */ + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_CALLS_CHANGED), b); + } + /* Call state updated */ + else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_CALL, MM_CALL_SIGNAL_STATECHANGED)) { + DBusMessageIter arg_i; + + pa_log_debug("Call %s: state updated", path); + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "iiu")) { + pa_log_error("Invalid signature found in StateChanged"); + goto fail; + } + + /* Skip old call state */ + pa_assert_se(dbus_message_iter_next(&arg_i)); + + /* Parse new call state */ + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_INT32); + parse_call_state(b, &arg_i, path); + + /* Skip call state change reason */ + pa_assert_se(dbus_message_iter_next(&arg_i)); + + /* Notify change for call */ + pa_hook_fire(pa_bluetooth_discovery_hook(b->discovery, + PA_BLUETOOTH_HOOK_HOST_CALLS_CHANGED), b); + } + +fail: + dbus_error_free(&err); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +/* Accept an incoming call */ +void pa_modemmanager_accept_call(pa_modemmanager_backend *backend, char *call) { + DBusMessage *m; + call_status_t *call_state; + + /* Check if call is incoming ringing */ + call_state = pa_hashmap_get(backend->calls, call); + if (!call_state || call_state->status != PA_MODEMMANAGER_CALL_STATE_RINGING || !call_state->is_incoming) { + pa_log_error("Call is not ringing and/or incoming, unable to accept call"); + pa_hook_fire(pa_bluetooth_discovery_hook(backend->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), backend); + return; + } + + /* Accept call */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, call, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_ACCEPT)); + send_and_add_to_pending(backend, m, call_accept_reply, NULL); +} + +/* End a call */ +void pa_modemmanager_end_call(pa_modemmanager_backend *backend, char *call) { + DBusMessage *m; + call_status_t *call_state; + + /* Check if call is active or incoming ringing */ + call_state = pa_hashmap_get(backend->calls, call); + if (!call_state || (call_state->status != PA_MODEMMANAGER_CALL_STATE_ACTIVE + && (call_state->status != PA_MODEMMANAGER_CALL_STATE_RINGING && call_state->is_incoming))) { + pa_log_error("Call is not active or incoming ringing, unable to hangup call"); + pa_hook_fire(pa_bluetooth_discovery_hook(backend->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), backend); + return; + } + + /* End call */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, call, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_HANGUP)); + send_and_add_to_pending(backend, m, call_hangup_reply, NULL); +} + +/* Create a new call by dialing the provided number */ +void pa_modemmanager_start_call(pa_modemmanager_backend *backend, char *number) { + DBusMessage *m; + DBusMessageIter i, d; + char *call_number; + + /* Create call for filtered number */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, backend->mm_modem, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_METHOD_CREATECALL)); + dbus_message_iter_init_append(m, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING + DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, + &d); + call_number = pa_xstrdup(number); + pa_dbus_append_basic_variant_dict_entry(&d, "number", DBUS_TYPE_STRING, &call_number); + dbus_message_iter_close_container(&i, &d); + send_and_add_to_pending(backend, m, create_call_reply, NULL); +} + +/* Send a DTMF tone to a given call */ +void pa_modemmanager_send_dtmf(pa_modemmanager_backend *backend, char *call, char *character) { + DBusMessage *m; + char *dtmf_char = pa_xstrdup(character); + call_status_t *call_state; + + /* Check if call is active */ + call_state = pa_hashmap_get(backend->calls, call); + if (call_state && (call_state->status != PA_MODEMMANAGER_CALL_STATE_ACTIVE)) { + pa_log_error("Call is not active, unable to send DTMF tone"); + pa_hook_fire(pa_bluetooth_discovery_hook(backend->discovery, + PA_BLUETOOTH_HOOK_HOST_OPERATION_FAILED), backend); + return; + } + + /* Let ModemManager generate DTMF tone */ + pa_log_debug("Generating DTMF tone: '%s'", dtmf_char); + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_SENDDTMF); + pa_assert_se(dbus_message_append_args(m, + DBUS_TYPE_STRING, &dtmf_char, + DBUS_TYPE_INVALID)); + send_and_add_to_pending(backend, m, send_dtmf_reply, NULL); +} + +/* Get network operator name */ +char *pa_modemmanager_get_operator_name(const pa_modemmanager_backend *backend) { + return backend->modem->network_operator_name; +} + +/* Get network operator code */ +char *pa_modemmanager_get_operator_code(const pa_modemmanager_backend *backend) { + return backend->modem->network_operator_code; +} + +/* Get service status */ +bool pa_modemmanager_has_service(const pa_modemmanager_backend *backend) { + return backend->modem->network_has_service; +} + +/* Get roaming status */ +bool pa_modemmanager_is_roaming(const pa_modemmanager_backend *backend) { + return backend->modem->network_is_roaming; +} + +/* Get signal strength percentage */ +unsigned int pa_modemmanager_get_signal_strength(const pa_modemmanager_backend *backend) { + return backend->modem->network_signal_strength; +} + +/* Get all calls */ +pa_hashmap *pa_modemmanager_get_calls(const pa_modemmanager_backend *backend) { + return backend->calls; +} + +/* Get active call */ +char *pa_modemmanager_get_active_call_key(const pa_modemmanager_backend *backend) { + pa_hashmap *calls; + call_status_t *c; + const void *key; + void *state = NULL; + + calls = pa_modemmanager_get_calls(backend); + while ((c = pa_hashmap_iterate(calls, &state, &key))) { + if (key == NULL) + break; + /* FIXME: support three-way calling */ + return (char *) key; + } + + /* No active calls */ + return NULL; +} + +/* Get modem model */ +char *pa_modemmanager_get_modem_model(const pa_modemmanager_backend *backend) { + return backend->modem->modem_model; +} + +/* Get modem manufacturer */ +char *pa_modemmanager_get_modem_manufacturer(const pa_modemmanager_backend *backend) { + return backend->modem->modem_manufacturer; +} + +/* Get modem revision */ +char *pa_modemmanager_get_modem_revision(const pa_modemmanager_backend *backend) { + return backend->modem->modem_revision; +} + +/* Get modem IMEI */ +char *pa_modemmanager_get_modem_imei(const pa_modemmanager_backend *backend) { + return backend->modem->modem_imei; +} + +/* Get modem's own number */ +char *pa_modemmanager_get_own_number(const pa_modemmanager_backend *backend) { + return backend->modem->modem_own_number; +} + +/* Check if modem is present */ +bool pa_modemmanager_has_modem(const pa_modemmanager_backend *backend) { + if (!backend->mm_modem) + return false; + + if (!backend->mm_modem_3gpp) + return false; + + if (!backend->mm_modem_voice) + return false; + + if(!backend->modem) + return false; + + return true; +} + +pa_modemmanager_backend *pa_modemmanager_backend_new(pa_core *c, pa_bluetooth_discovery *d) { + pa_modemmanager_backend *backend; + DBusError err; + DBusMessage *m; + + pa_log_debug("Native backend enabled ModemManager modem & call status reporting"); + + backend = pa_xnew0(pa_modemmanager_backend, 1); + backend->core = c; + backend->discovery = d; + backend->calls = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + backend->modem = pa_xnew0(modem_status_t, 1); + + /* Get DBus connection */ + dbus_error_init(&err); + if (!(backend->connection = pa_dbus_bus_get(c, DBUS_BUS_SYSTEM, &err))) { + pa_log("Failed to get D-Bus connection: %s", err.message); + dbus_error_free(&err); + pa_xfree(backend); + return NULL; + } + + /* Add filter callback for DBus connection */ + if (!dbus_connection_add_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend, NULL)) { + pa_log_error("Failed to add filter function"); + pa_dbus_connection_unref(backend->connection); + pa_xfree(backend); + return NULL; + } + + /* Register for call & modem changes from ModemManager */ + if (pa_dbus_add_matches(pa_dbus_connection_get(backend->connection), &err, + "type='signal',sender='" DBUS_SERVICE_DBUS "',interface='" DBUS_INTERFACE_DBUS "',member='" DBUS_INTERFACE_DBUS_SIGNAL_NAMEOWNERCHANGED "'," + "arg0='" MM_DBUS_SERVICE "'", + "type='signal',sender='" MM_DBUS_SERVICE "',interface='" DBUS_INTERFACE_OBJECT_MANAGER "',member='" DBUS_INTERFACE_OBJECT_MANAGER_SIGNAL_INTERFACESADDED "'", + "type='signal',sender='" MM_DBUS_SERVICE "',interface='" DBUS_INTERFACE_OBJECT_MANAGER "',member='" DBUS_INTERFACE_OBJECT_MANAGER_SIGNAL_INTERFACESREMOVED "'", + "type='signal',sender='" MM_DBUS_SERVICE "',interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_INTERFACE_PROPERTIES_SIGNAL_PROPERTIESCHANGED "'", + "type='signal',sender='" MM_DBUS_SERVICE "',interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLADDED "'", + "type='signal',sender='" MM_DBUS_SERVICE "',interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLDELETED "'", + "type='signal',sender='" MM_DBUS_SERVICE "',interface='" MM_DBUS_INTERFACE_CALL "',member='" MM_CALL_SIGNAL_STATECHANGED "'", + NULL) < 0) { + pa_log("Failed to add ModemManager D-Bus matches: %s", err.message); + dbus_connection_remove_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend); + pa_dbus_connection_unref(backend->connection); + pa_xfree(backend); + return NULL; + } + + /* Initialize list of modems by requesting it from ModemManager */ + pa_assert_se(m = dbus_message_new_method_call(MM_DBUS_SERVICE, MM_DBUS_PATH, DBUS_INTERFACE_OBJECT_MANAGER, DBUS_INTERFACE_OBJECT_MANAGER_METHOD_GETMANAGEDOBJECTS)); + send_and_add_to_pending(backend, m, get_managed_objects_reply, NULL); + + return backend; +} + +void pa_modemmanager_backend_free(pa_modemmanager_backend *backend) { + pa_assert(backend); + + pa_dbus_free_pending_list(&backend->pending); + + pa_dbus_connection_unref(backend->connection); + + invalidate_modem_properties(backend); + invalidate_modem_3gpp_properties(backend); + invalidate_modem_voice_properties(backend); + + pa_xfree(backend); +} + diff --git a/src/modules/bluetooth/modemmanager.h b/src/modules/bluetooth/modemmanager.h new file mode 100644 index 000000000..9a1d82219 --- /dev/null +++ b/src/modules/bluetooth/modemmanager.h @@ -0,0 +1,98 @@ +#pragma once + +/*** + This file is part of PulseAudio. + + Copyright 2022 Dylan Van Assche + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + PulseAudio 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 Lesser General Public + License along with PulseAudio; if not, see . +***/ + +#include "bluez5-util.h" +#include "ModemManager.h" + +#define DBUS_INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager" +#define DBUS_INTERFACE_OBJECT_MANAGER_METHOD_GETMANAGEDOBJECTS "GetManagedObjects" +#define DBUS_INTERFACE_OBJECT_MANAGER_SIGNAL_INTERFACESADDED "InterfacesAdded" +#define DBUS_INTERFACE_OBJECT_MANAGER_SIGNAL_INTERFACESREMOVED "InterfacesRemoved" +#define DBUS_INTERFACE_DBUS_SIGNAL_NAMEOWNERCHANGED "NameOwnerChanged" +#define DBUS_INTERFACE_PROPERTIES_METHOD_GETALL "GetAll" +#define DBUS_INTERFACE_PROPERTIES_SIGNAL_PROPERTIESCHANGED "PropertiesChanged" + +#define MAX_NUMBER_LENGTH 30 + +#define STR_VALUE(x) STR(x) +#define STR(x) #x + +typedef enum pa_modemmanager_call_state { + PA_MODEMMANAGER_CALL_STATE_INVALID = 0, + PA_MODEMMANAGER_CALL_STATE_ACTIVE = 1, + PA_MODEMMANAGER_CALL_STATE_RINGING = 2, + PA_MODEMMANAGER_CALL_STATE_DIALING = 3, + PA_MODEMMANAGER_CALL_STATE_TERMINATED = 4 + /* FIXME: implement three-way calling */ +} pa_modemmanager_call_state_t; + +typedef struct call_status { + bool is_incoming; + pa_modemmanager_call_state_t status; + char *number; +} call_status_t; + +typedef struct modem_status { + char *network_operator_name; + char *network_operator_code; + unsigned int network_signal_strength; + bool network_has_service; + bool network_is_roaming; + char *modem_own_number; + char *modem_manufacturer; + char *modem_model; + char *modem_revision; + char *modem_imei; +} modem_status_t; + +struct pa_modemmanager_backend { + pa_core *core; + pa_dbus_connection *connection; + pa_bluetooth_discovery *discovery; + char *mm_bus_id; + char *mm_modem; + char *mm_modem_3gpp; + char *mm_modem_voice; + pa_hashmap *calls; + modem_status_t *modem; + + PA_LLIST_HEAD(pa_dbus_pending, pending); +}; + +pa_modemmanager_backend *pa_modemmanager_backend_new(pa_core *c, pa_bluetooth_discovery *d); +void pa_modemmanager_backend_free(pa_modemmanager_backend *backend); +bool pa_modemmanager_has_modem(const pa_modemmanager_backend *backend); +void pa_modemmanager_accept_call(pa_modemmanager_backend *backend, char *call); +void pa_modemmanager_end_call(pa_modemmanager_backend *backend, char *call); +void pa_modemmanager_start_call(pa_modemmanager_backend *backend, char *number); +void pa_modemmanager_send_dtmf(pa_modemmanager_backend *backend, char *call, char *character); +pa_hashmap *pa_modemmanager_get_calls(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_active_call_key(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_operator_name(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_operator_code(const pa_modemmanager_backend *backend); +bool pa_modemmanager_has_service(const pa_modemmanager_backend *backend); +bool pa_modemmanager_is_roaming(const pa_modemmanager_backend *backend); +unsigned int pa_modemmanager_get_signal_strength(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_modem_model(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_modem_manufacturer(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_modem_revision(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_modem_imei(const pa_modemmanager_backend *backend); +char *pa_modemmanager_get_own_number(const pa_modemmanager_backend *backend); -- 2.35.1