diff options
Diffstat (limited to '')
-rw-r--r-- | spa/plugins/bluez5/backend-native.c | 2838 |
1 files changed, 2838 insertions, 0 deletions
diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c new file mode 100644 index 0000000..5eec82c --- /dev/null +++ b/spa/plugins/bluez5/backend-native.c @@ -0,0 +1,2838 @@ +/* Spa HSP/HFP native backend + * + * Copyright © 2018 Wim Taymans + * Copyright © 2021 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 <unistd.h> +#include <stdarg.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <poll.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sco.h> + +#include <dbus/dbus.h> + +#include <spa/debug/mem.h> +#include <spa/debug/log.h> +#include <spa/support/log.h> +#include <spa/support/loop.h> +#include <spa/support/dbus.h> +#include <spa/support/plugin.h> +#include <spa/utils/string.h> +#include <spa/utils/type.h> +#include <spa/utils/json.h> +#include <spa/param/audio/raw.h> + +#include "defs.h" + +#ifdef HAVE_LIBUSB +#include <libusb.h> +#endif + +#include "modemmanager.h" +#include "upower.h" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.native"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles" + +#define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000 +#define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000 + +#define INTERNATIONAL_NUMBER 145 +#define NATIONAL_NUMBER 129 + +#define MAX_HF_INDICATORS 16 + +enum { + HFP_AG_INITIAL_CODEC_SETUP_NONE = 0, + HFP_AG_INITIAL_CODEC_SETUP_SEND, + HFP_AG_INITIAL_CODEC_SETUP_WAIT +}; + +#define CIND_INDICATORS "(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5)),(\"roam\",(0-1)),(\"battchg\",(0-5))" +enum { + CIND_SERVICE = 1, + CIND_CALL, + CIND_CALLSETUP, + CIND_CALLHELD, + CIND_SIGNAL, + CIND_ROAM, + CIND_BATTERY_LEVEL, + CIND_MAX +}; + +struct modem { + bool network_has_service; + unsigned int signal_strength; + bool network_is_roaming; + char *operator_name; + char *own_number; + bool active_call; + unsigned int call_setup; +}; + +struct impl { + struct spa_bt_backend this; + + struct spa_bt_monitor *monitor; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_system *main_system; + struct spa_loop_utils *loop_utils; + struct spa_dbus *dbus; + DBusConnection *conn; + +#define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG) + enum spa_bt_profile enabled_profiles; + + struct spa_source sco; + + const struct spa_bt_quirks *quirks; + + struct spa_list rfcomm_list; + unsigned int defer_setup_enabled:1; + + struct modem modem; + unsigned int battery_level; + + void *modemmanager; + struct spa_source *ring_timer; + void *upower; +}; + +struct transport_data { + struct rfcomm *rfcomm; + struct spa_source sco; +}; + +enum hfp_hf_state { + hfp_hf_brsf, + hfp_hf_bac, + hfp_hf_cind1, + hfp_hf_cind2, + hfp_hf_cmer, + hfp_hf_slc1, + hfp_hf_slc2, + hfp_hf_vgs, + hfp_hf_vgm, + hfp_hf_bcs +}; + +enum hsp_hs_state { + hsp_hs_init1, + hsp_hs_init2, + hsp_hs_vgs, + hsp_hs_vgm, +}; + +struct rfcomm_volume { + bool active; + int hw_volume; +}; + +struct rfcomm { + struct spa_list link; + struct spa_source source; + struct impl *backend; + struct spa_bt_device *device; + struct spa_hook device_listener; + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + enum spa_bt_profile profile; + struct spa_source timer; + struct spa_source *volume_sync_timer; + char* path; + bool has_volume; + struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM]; + unsigned int broken_mic_hw_volume:1; +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + unsigned int slc_configured:1; + unsigned int codec_negotiation_supported:1; + unsigned int msbc_supported_by_hfp:1; + unsigned int hfp_ag_switching_codec:1; + unsigned int hfp_ag_initial_codec_setup:2; + unsigned int cind_call_active:1; + unsigned int cind_call_notify:1; + unsigned int extended_error_reporting:1; + unsigned int clip_notify:1; + enum hfp_hf_state hf_state; + enum hsp_hs_state hs_state; + unsigned int codec; + uint32_t cind_enabled_indicators; + char *hf_indicators[MAX_HF_INDICATORS]; +#endif +}; + +static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + DBusMessage *r; + + r = dbus_message_new_error(m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", + "Method not implemented"); + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void transport_destroy(void *data) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + + spa_log_debug(backend->log, "transport %p destroy", rfcomm->transport); + rfcomm->transport = NULL; +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = transport_destroy, +}; + +static const struct spa_bt_transport_implementation sco_transport_impl; + +static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct spa_bt_transport *t = NULL; + struct transport_data *td; + char* pathfd; + + if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL) + return NULL; + + t = spa_bt_transport_create(backend->monitor, pathfd, sizeof(struct transport_data)); + if (t == NULL) + goto finish; + spa_bt_transport_set_implementation(t, &sco_transport_impl, t); + + t->device = rfcomm->device; + spa_list_append(&t->device->transport_list, &t->device_link); + t->profile = rfcomm->profile; + t->backend = &backend->this; + t->n_channels = 1; + t->channels[0] = SPA_AUDIO_CHANNEL_MONO; + + td = t->user_data; + td->rfcomm = rfcomm; + + if (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) { + t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME; + t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME; + } else { + t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME; + t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME; + } + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) { + t->volumes[i].active = rfcomm->volumes[i].active; + t->volumes[i].hw_volume_max = SPA_BT_VOLUME_HS_MAX; + if (rfcomm->volumes[i].active && rfcomm->volumes[i].hw_volume != SPA_BT_VOLUME_INVALID) + t->volumes[i].volume = + spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t->volumes[i].hw_volume_max); + } + + spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm); + +finish: + return t; +} + +static int codec_switch_stop_timer(struct rfcomm *rfcomm); + +static void volume_sync_stop_timer(struct rfcomm *rfcomm); + +static void rfcomm_free(struct rfcomm *rfcomm) +{ + codec_switch_stop_timer(rfcomm); + for (int i = 0; i < MAX_HF_INDICATORS; i++) { + if (rfcomm->hf_indicators[i]) { + free(rfcomm->hf_indicators[i]); + } + } + spa_list_remove(&rfcomm->link); + if (rfcomm->path) + free(rfcomm->path); + if (rfcomm->transport) { + spa_hook_remove(&rfcomm->transport_listener); + spa_bt_transport_free(rfcomm->transport); + } + if (rfcomm->device) { + spa_bt_device_report_battery_level(rfcomm->device, SPA_BT_NO_BATTERY); + spa_hook_remove(&rfcomm->device_listener); + rfcomm->device = NULL; + } + if (rfcomm->source.fd >= 0) { + if (rfcomm->source.loop) + spa_loop_remove_source(rfcomm->source.loop, &rfcomm->source); + shutdown(rfcomm->source.fd, SHUT_RDWR); + close (rfcomm->source.fd); + rfcomm->source.fd = -1; + } + if (rfcomm->volume_sync_timer) + spa_loop_utils_destroy_source(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer); + free(rfcomm); +} + +#define RFCOMM_MESSAGE_MAX_LENGTH 256 + +/* from HF/HS to AG */ +SPA_PRINTF_FUNC(2, 3) +static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format, ...) +{ + struct impl *backend = rfcomm->backend; + char message[RFCOMM_MESSAGE_MAX_LENGTH + 1]; + ssize_t len; + va_list args; + + va_start(args, format); + len = vsnprintf(message, RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args); + va_end(args); + + if (len < 0) + return -EINVAL; + + if (len > RFCOMM_MESSAGE_MAX_LENGTH) + return -E2BIG; + + spa_log_debug(backend->log, "RFCOMM >> %s", message); + + /* + * The format of an AT command from the HF to the AG shall be: <AT command><cr> + * - HFP 1.8, 4.34.1 + * + * The format for a command from the HS to the AG is thus: AT<cmd>=<value><cr> + * - HSP 1.2, 4.8.1 + */ + message[len] = '\r'; + /* `message` is no longer null-terminated */ + + len = write(rfcomm->source.fd, message, len + 1); + /* we ignore any errors, it's not critical and real errors should + * be caught with the HANGUP and ERROR events handled above */ + if (len < 0) { + len = -errno; + spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno)); + } + + return len; +} + +/* from AG to HF/HS */ +SPA_PRINTF_FUNC(2, 3) +static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format, ...) +{ + struct impl *backend = rfcomm->backend; + char message[RFCOMM_MESSAGE_MAX_LENGTH + 4]; + ssize_t len; + va_list args; + + va_start(args, format); + len = vsnprintf(&message[2], RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args); + va_end(args); + + if (len < 0) + return -EINVAL; + + if (len > RFCOMM_MESSAGE_MAX_LENGTH) + return -E2BIG; + + spa_log_debug(backend->log, "RFCOMM >> %s", &message[2]); + + /* + * The format of the OK code from the AG to the HF shall be: <cr><lf>OK<cr><lf> + * The format of the generic ERROR code from the AG to the HF shall be: <cr><lf>ERROR<cr><lf> + * The format of an unsolicited result code from the AG to the HF shall be: <cr><lf><result code><cr><lf> + * - HFP 1.8, 4.34.1 + * + * If the command is processed successfully, the resulting response from the AG to the HS is: <cr><lf>OK<cr><lf> + * If the command is not processed successfully, or is not recognized, + * the resulting response from the AG to the HS is: <cr><lf>ERROR<cr><lf> + * The format for an unsolicited result code (such as RING) from the AG to the HS is: <cr><lf><result code><cr><lf> + * - HSP 1.2, 4.8.1 + */ + message[0] = '\r'; + message[1] = '\n'; + message[len + 2] = '\r'; + message[len + 3] = '\n'; + /* `message` is no longer null-terminated */ + + len = write(rfcomm->source.fd, message, len + 4); + /* we ignore any errors, it's not critical and real errors should + * be caught with the HANGUP and ERROR events handled above */ + if (len < 0) { + len = -errno; + spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno)); + } + + return len; +} + +static void rfcomm_send_error(const struct rfcomm *rfcomm, enum cmee_error error) +{ + if (rfcomm->extended_error_reporting) + rfcomm_send_reply(rfcomm, "+CME ERROR: %d", error); + else + rfcomm_send_reply(rfcomm, "ERROR"); +} + +static bool rfcomm_volume_enabled(struct rfcomm *rfcomm) +{ + return rfcomm->device != NULL + && (rfcomm->device->hw_volume_profiles & rfcomm->profile); +} + +static void rfcomm_emit_volume_changed(struct rfcomm *rfcomm, int id, int hw_volume) +{ + struct spa_bt_transport_volume *t_volume; + + if (!rfcomm_volume_enabled(rfcomm)) + return; + + if ((id == SPA_BT_VOLUME_ID_RX || id == SPA_BT_VOLUME_ID_TX) && hw_volume >= 0) { + rfcomm->volumes[id].active = true; + rfcomm->volumes[id].hw_volume = hw_volume; + } + + spa_log_debug(rfcomm->backend->log, "volume changed %d", hw_volume); + + if (rfcomm->transport == NULL || !rfcomm->has_volume) + return; + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) { + t_volume = &rfcomm->transport->volumes[i]; + t_volume->active = rfcomm->volumes[i].active; + t_volume->volume = + spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t_volume->hw_volume_max); + } + + spa_bt_transport_emit_volume_changed(rfcomm->transport); +} + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE +static bool rfcomm_hsp_ag(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int gain, dummy; + + /* There are only three HSP AT commands: + * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain. + * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain. + * AT+CKPD=200: Sent by HS when headset button is pressed. */ + if (sscanf(buf, "AT+VGS=%d", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf); + rfcomm_send_reply(rfcomm, "ERROR"); + } + } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + if (!rfcomm->broken_mic_hw_volume) + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + rfcomm_send_reply(rfcomm, "ERROR"); + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); + } + } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) { + rfcomm_send_reply(rfcomm, "OK"); + } else { + return false; + } + + return true; +} + +static bool rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int id) +{ + struct spa_bt_transport_volume *t_volume; + const char *format; + int hw_volume; + + if (!rfcomm_volume_enabled(rfcomm)) + return false; + + t_volume = rfcomm->transport ? &rfcomm->transport->volumes[id] : NULL; + + if (!(t_volume && t_volume->active)) + return false; + + hw_volume = spa_bt_volume_linear_to_hw(t_volume->volume, t_volume->hw_volume_max); + rfcomm->volumes[id].hw_volume = hw_volume; + + if (id == SPA_BT_VOLUME_ID_TX) + format = "AT+VGM"; + else if (id == SPA_BT_VOLUME_ID_RX) + format = "AT+VGS"; + else + spa_assert_not_reached(); + + rfcomm_send_cmd(rfcomm, "%s=%d", format, hw_volume); + + return true; +} + +static bool rfcomm_hsp_hs(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int gain; + + /* There are only three HSP AT result codes: + * +VGS=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGS command + * or when the gain is changed on the AG side. + * +VGM=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGM command + * or when the gain is changed on the AG side. + * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because + * it does not expect a reply. */ + if (sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf); + } + } else if (sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); + } + } else if (spa_strstartswith(buf, "\r\nOK\r\n")) { + if (rfcomm->hs_state == hsp_hs_init2) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hs_state = hsp_hs_vgs; + else + rfcomm->hs_state = hsp_hs_init1; + } else if (rfcomm->hs_state == hsp_hs_vgs) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) + rfcomm->hs_state = hsp_hs_vgm; + else + rfcomm->hs_state = hsp_hs_init1; + } + } + + return true; +} +#endif + +#ifdef HAVE_LIBUSB +static bool check_usb_altsetting_6(struct impl *backend, uint16_t vendor_id, uint16_t product_id) +{ + libusb_context *ctx = NULL; + struct libusb_config_descriptor *cfg = NULL; + libusb_device **devices = NULL; + + ssize_t ndev, idev; + int res; + bool ok = false; + + if ((res = libusb_init(&ctx)) < 0) { + ctx = NULL; + goto fail; + } + + if ((ndev = libusb_get_device_list(ctx, &devices)) < 0) { + res = ndev; + devices = NULL; + goto fail; + } + + for (idev = 0; idev < ndev; ++idev) { + libusb_device *dev = devices[idev]; + struct libusb_device_descriptor desc; + int icfg; + + libusb_get_device_descriptor(dev, &desc); + if (vendor_id != desc.idVendor || product_id != desc.idProduct) + continue; + + /* Check the device has Bluetooth isoch. altsetting 6 interface */ + + for (icfg = 0; icfg < desc.bNumConfigurations; ++icfg) { + int iiface; + + if ((res = libusb_get_config_descriptor(dev, icfg, &cfg)) != 0) { + cfg = NULL; + goto fail; + } + + for (iiface = 0; iiface < cfg->bNumInterfaces; ++iiface) { + const struct libusb_interface *iface = &cfg->interface[iiface]; + int ialt; + + for (ialt = 0; ialt < iface->num_altsetting; ++ialt) { + const struct libusb_interface_descriptor *idesc = &iface->altsetting[ialt]; + int iep; + + if (idesc->bInterfaceClass != LIBUSB_CLASS_WIRELESS || + idesc->bInterfaceSubClass != 1 /* RF */ || + idesc->bInterfaceProtocol != 1 /* Bluetooth */ || + idesc->bAlternateSetting != 6) + continue; + + for (iep = 0; iep < idesc->bNumEndpoints; ++iep) { + const struct libusb_endpoint_descriptor *ep = &idesc->endpoint[iep]; + if ((ep->bmAttributes & 0x3) == 0x1 /* isochronous */) { + ok = true; + goto done; + } + } + } + } + + libusb_free_config_descriptor(cfg); + cfg = NULL; + } + } + +done: + if (cfg) + libusb_free_config_descriptor(cfg); + if (devices) + libusb_free_device_list(devices, 0); + if (ctx) + libusb_exit(ctx); + return ok; + +fail: + spa_log_info(backend->log, "failed to acquire USB device info: %d (%s)", + res, libusb_strerror(res)); + ok = false; + goto done; +} +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + +static bool device_supports_required_mSBC_transport_modes( + struct impl *backend, struct spa_bt_device *device) +{ + int res; + bool msbc_ok, msbc_alt1_ok; + uint32_t bt_features; + + if (device->adapter == NULL) + return false; + + if (backend->quirks && spa_bt_quirks_get_features(backend->quirks, device->adapter, device, &bt_features) == 0) { + msbc_ok = bt_features & SPA_BT_FEATURE_MSBC; + msbc_alt1_ok = bt_features & (SPA_BT_FEATURE_MSBC_ALT1 | SPA_BT_FEATURE_MSBC_ALT1_RTL); + } else { + msbc_ok = true; + msbc_alt1_ok = true; + } + + spa_log_info(backend->log, + "bluez-monitor/hardware.conf: msbc:%d msbc-alt1:%d", (int)msbc_ok, (int)msbc_alt1_ok); + + if (!msbc_ok && !msbc_alt1_ok) + return false; + + res = spa_bt_adapter_has_msbc(device->adapter); + if (res < 0) { + spa_log_warn(backend->log, + "adapter %s: failed to determine msbc/esco capability (%d)", + device->adapter->path, res); + } else if (res == 0) { + spa_log_info(backend->log, + "adapter %s: no msbc/esco transport", + device->adapter->path); + return false; + } else { + spa_log_debug(backend->log, + "adapter %s: has msbc/esco transport", + device->adapter->path); + } + + /* Check if USB ALT6 is really available on the device */ + if (device->adapter->bus_type == BUS_TYPE_USB && !msbc_alt1_ok && msbc_ok) { +#ifdef HAVE_LIBUSB + if (device->adapter->source_id == SOURCE_ID_USB) { + msbc_ok = check_usb_altsetting_6(backend, device->adapter->vendor_id, + device->adapter->product_id); + } else { + msbc_ok = false; + } + if (!msbc_ok) + spa_log_info(backend->log, "bluetooth host adapter does not support USB ALT6"); +#else + spa_log_info(backend->log, + "compiled without libusb; can't check if bluetooth adapter has USB ALT6"); + msbc_ok = false; +#endif + } + if (device->adapter->bus_type != BUS_TYPE_USB) + msbc_alt1_ok = false; + + return msbc_ok || msbc_alt1_ok; +} + +static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec); + +static void process_iphoneaccev_indicator(struct rfcomm *rfcomm, unsigned int key, unsigned int value) +{ + struct impl *backend = rfcomm->backend; + + spa_log_debug(backend->log, "key:%u value:%u", key, value); + + switch (key) { + case SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL: { + // Battery level is reported in range of 0-9, convert to 10-100% + uint8_t level = (SPA_CLAMP(value, 0u, 9u) + 1) * 10; + spa_log_debug(backend->log, "battery level: %d%%", (int) level); + + // TODO: report without Battery Provider (using props) + spa_bt_device_report_battery_level(rfcomm->device, level); + break; + } + case SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE: + break; + default: + spa_log_warn(backend->log, "unknown AT+IPHONEACCEV key:%u value:%u", key, value); + break; + } +} + +static void process_hfp_hf_indicator(struct rfcomm *rfcomm, unsigned int indicator, unsigned int value) +{ + struct impl *backend = rfcomm->backend; + + spa_log_debug(backend->log, "indicator:%u value:%u", indicator, value); + + switch (indicator) { + case SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY: + break; + case SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL: + // Battery level is reported in range 0-100 + spa_log_debug(backend->log, "battery level: %u%%", value); + + if (value <= 100) { + // TODO: report without Battery Provider (using props) + spa_bt_device_report_battery_level(rfcomm->device, value); + } else { + spa_log_warn(backend->log, "battery HF indicator %u outside of range [0, 100]: %u", indicator, value); + } + break; + default: + spa_log_warn(backend->log, "unknown HF indicator:%u value:%u", indicator, value); + break; + } +} + +static void rfcomm_hfp_ag_set_cind(struct rfcomm *rfcomm, bool call_active) +{ + if (rfcomm->profile != SPA_BT_PROFILE_HFP_HF) + return; + + if (call_active == rfcomm->cind_call_active) + return; + + rfcomm->cind_call_active = call_active; + + if (!rfcomm->cind_call_notify) + return; + + rfcomm_send_reply(rfcomm, "+CIEV: 2,%d", rfcomm->cind_call_active); +} + +static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int features; + unsigned int gain; + unsigned int count, r; + unsigned int selected_codec; + unsigned int indicator; + unsigned int indicator_value; + unsigned int value; + int xapl_vendor; + int xapl_product; + int xapl_features; + + spa_debug_log_mem(backend->log, SPA_LOG_LEVEL_DEBUG, 2, buf, strlen(buf)); + + /* Some devices send initial \n: be permissive */ + while (*buf == '\n') + ++buf; + + if (sscanf(buf, "AT+BRSF=%u", &features) == 1) { + unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE; + + /* + * Determine device volume control. Some headsets only support control of + * TX volume, but not RX, even if they have a microphone. Determine this + * separately based on whether we also get AT+VGS/AT+VGM, and quirks. + */ + rfcomm->has_volume = (features & SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL); + + /* Decide if we want to signal that the computer supports mSBC negotiation + This should be done when the computers bluetooth adapter supports the necessary transport mode */ + if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) { + + /* set the feature bit that indicates AG (=computer) supports codec negotiation */ + ag_features |= SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION; + + /* let's see if the headset supports codec negotiation */ + if ((features & (SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION)) != 0) { + spa_log_debug(backend->log, + "RFCOMM features = %i, codec negotiation supported by headset", + features); + /* Prepare reply: Audio Gateway (=computer) supports codec negotiation */ + rfcomm->codec_negotiation_supported = true; + rfcomm->msbc_supported_by_hfp = false; + } else { + /* Codec negotiation not supported */ + spa_log_debug(backend->log, + "RFCOMM features = %i, codec negotiation NOT supported by headset", + features); + + rfcomm->codec_negotiation_supported = false; + rfcomm->msbc_supported_by_hfp = false; + } + } + + /* send reply to HF with the features supported by Audio Gateway (=computer) */ + ag_features |= mm_supported_features(); + ag_features |= SPA_BT_HFP_AG_FEATURE_HF_INDICATORS; + rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BAC=")) { + /* retrieve supported codecs */ + /* response has the form AT+BAC=<codecID1>,<codecID2>,<codecIDx> + strategy: split the string into tokens */ + + char* token; + int cntr = 0; + + while ((token = strsep(&buf, "=,"))) { + unsigned int codec_id; + + /* skip token 0 i.e. the "AT+BAC=" part */ + if (cntr > 0 && sscanf(token, "%u", &codec_id) == 1) { + spa_log_debug(backend->log, "RFCOMM AT+BAC found codec %u", codec_id); + if (codec_id == HFP_AUDIO_CODEC_MSBC) { + rfcomm->msbc_supported_by_hfp = true; + spa_log_debug(backend->log, "RFCOMM headset supports mSBC codec"); + } + } + cntr++; + } + + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CIND=?")) { + rfcomm_send_reply(rfcomm, "+CIND:%s", CIND_INDICATORS); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CIND?")) { + rfcomm_send_reply(rfcomm, "+CIND: %d,%d,%d,0,%d,%d,%d", backend->modem.network_has_service, + backend->modem.active_call, backend->modem.call_setup, backend->modem.signal_strength, + backend->modem.network_is_roaming, backend->battery_level); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CMER")) { + int mode, keyp, disp, ind; + + rfcomm->slc_configured = true; + rfcomm_send_reply(rfcomm, "OK"); + + rfcomm->cind_call_active = false; + if (sscanf(buf, "AT+CMER= %d , %d , %d , %d", &mode, &keyp, &disp, &ind) == 4) + rfcomm->cind_call_notify = ind ? true : false; + else + rfcomm->cind_call_notify = false; + + /* switch codec to mSBC by sending unsolicited +BCS message */ + if (rfcomm->codec_negotiation_supported && rfcomm->msbc_supported_by_hfp) { + spa_log_debug(backend->log, "RFCOMM initial codec setup"); + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_SEND; + rfcomm_send_reply(rfcomm, "+BCS: 2"); + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC); + } else { + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + } else { + rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID); + } + } + } else if (spa_streq(buf, "\r")) { + /* No commands, reply OK (ITU-T Rec. V.250 Sec. 5.2.1 & 5.6) */ + rfcomm_send_reply(rfcomm, "OK"); + } else if (!rfcomm->slc_configured) { + spa_log_warn(backend->log, "RFCOMM receive command before SLC completed: %s", buf); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + + /* ***** + * Following commands requires a Service Level Connection + * ***** */ + + } else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) { + /* parse BCS(=Bluetooth Codec Selection) reply */ + bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL); + rfcomm->hfp_ag_switching_codec = false; + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; + codec_switch_stop_timer(rfcomm); + volume_sync_stop_timer(rfcomm); + + if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) { + spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -EIO); + return true; + } + + rfcomm->codec = selected_codec; + + spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec); + + /* Recreate transport, since previous connection may now be invalid */ + if (rfcomm->transport) + spa_bt_transport_free(rfcomm->transport); + + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM); + return true; + } + rfcomm->transport->codec = selected_codec; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID); + + rfcomm_send_reply(rfcomm, "OK"); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, 0); + } else if (spa_strstartswith(buf, "AT+BIA=")) { + /* retrieve indicators activation + * form: AT+BIA=[indrep1],[indrep2],[indrepx] */ + char *str = buf + 7; + unsigned int ind = 1; + + while (*str && ind < CIND_MAX && *str != '\r' && *str != '\n') { + if (*str == ',') { + ind++; + goto next_indicator; + } + + /* Ignore updates to mandantory indicators which are always ON */ + if (ind == CIND_CALL || ind == CIND_CALLSETUP || ind == CIND_CALLHELD) + goto next_indicator; + + switch (*str) { + case '0': + rfcomm->cind_enabled_indicators &= ~(1 << ind); + break; + case '1': + rfcomm->cind_enabled_indicators |= (1 << ind); + break; + default: + spa_log_warn(backend->log, "Unsupported entry in %s: %c", buf, *str); + } +next_indicator: + str++; + } + + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CLCC")) { + struct spa_list *calls; + struct call *call; + unsigned int type; + + if (backend->modemmanager) { + calls = mm_get_calls(backend->modemmanager); + spa_list_for_each(call, calls, link) { + if (!call->number) { + rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u", call->index, call->direction, call->state, call->multiparty); + } else { + if (spa_strstartswith(call->number, "+")) + type = INTERNATIONAL_NUMBER; + else + type = NATIONAL_NUMBER; + rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u,\"%s\",%d", call->index, call->direction, call->state, + call->multiparty, call->number, type); + } + } + } + + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+CLIP=%u", &value) == 1) { + if (value > 1) { + spa_log_debug(backend->log, "Unsupported AT+CLIP value: %u", value); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + rfcomm->clip_notify = value; + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+CMEE=%u", &value) == 1) { + if (value > 1) { + spa_log_debug(backend->log, "Unsupported AT+CMEE value: %u", value); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + rfcomm->extended_error_reporting = value; + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CNUM")) { + if (backend->modem.own_number) { + unsigned int type; + if (spa_strstartswith(backend->modem.own_number, "+")) + type = INTERNATIONAL_NUMBER; + else + type = NATIONAL_NUMBER; + rfcomm_send_reply(rfcomm, "+CNUM: ,\"%s\",%u", backend->modem.own_number, type); + } + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+COPS=")) { + unsigned int mode, val; + + if (sscanf(buf, "AT+COPS=%u,%u", &mode, &val) != 2 || + mode != 3 || val != 0) { + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + } else { + rfcomm_send_reply(rfcomm, "OK"); + } + } else if (spa_strstartswith(buf, "AT+COPS?")) { + if (!backend->modem.network_has_service) { + rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE); + } else { + if (backend->modem.operator_name) + rfcomm_send_reply(rfcomm, "+COPS: 0,0,\"%s\"", backend->modem.operator_name); + else + rfcomm_send_reply(rfcomm, "+COPS: 0,,"); + rfcomm_send_reply(rfcomm, "OK"); + } + } else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + if (!rfcomm->broken_mic_hw_volume) + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED); + } + } else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf); + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED); + } + } else if (spa_strstartswith(buf, "AT+BIND=?")) { + rfcomm_send_reply(rfcomm, "+BIND: (2)"); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BIND?")) { + rfcomm_send_reply(rfcomm, "+BIND: 2,1"); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BIND=")) { + // BIND=... should return a comma separated list of indicators and + // 2 should be among the other numbers telling that battery charge + // is supported + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+BIEV=%u,%u", &indicator, &indicator_value) == 2) { + process_hfp_hf_indicator(rfcomm, indicator, indicator_value); + } else if (sscanf(buf, "AT+XAPL=%04x-%04x-%*[^,],%u", &xapl_vendor, &xapl_product, &xapl_features) == 3) { + if (xapl_features & SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING) { + /* claim, that we support battery status reports */ + rfcomm_send_reply(rfcomm, "+XAPL=iPhone,%u", SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING); + } + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+IPHONEACCEV=%u%n", &count, &r) == 1) { + if (count < 1 || count > 100) + return false; + + buf += r; + + for (unsigned int i = 0; i < count; i++) { + unsigned int key, value; + + if (sscanf(buf, " , %u , %u%n", &key, &value, &r) != 2) + return false; + + process_iphoneaccev_indicator(rfcomm, key, value); + buf += r; + } + } else if (spa_strstartswith(buf, "AT+APLSIRI?")) { + // This command is sent when we activate Apple extensions + rfcomm_send_reply(rfcomm, "OK"); + } else if (!mm_is_available(backend->modemmanager)) { + spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf); + rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE); + return true; + + /* ***** + * Following commands requires a Service Level Connection + * and acces to a modem + * ***** */ + + } else if (!backend->modem.network_has_service) { + spa_log_warn(backend->log, "RFCOMM receive command but network not available: %s", buf); + rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE); + return true; + + /* ***** + * Following commands requires a Service Level Connection, + * acces to a modem and to the network + * ***** */ + + } else if (spa_strstartswith(buf, "ATA")) { + enum cmee_error error; + + if (!mm_answer_call(backend->modemmanager, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else if (spa_strstartswith(buf, "ATD")) { + char number[31], sep; + enum cmee_error error; + + if (sscanf(buf, "ATD%30[^;]%c", number, &sep) != 2 || sep != ';') { + spa_log_debug(backend->log, "Failed to parse ATD: \"%s\"", buf); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + if (!mm_do_call(backend->modemmanager, number, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else if (spa_strstartswith(buf, "AT+CHUP")) { + enum cmee_error error; + + if (!mm_hangup_call(backend->modemmanager, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else if (spa_strstartswith(buf, "AT+VTS=")) { + char *dtmf; + enum cmee_error error; + + dtmf = calloc(1, 2); + if (sscanf(buf, "AT+VTS=%1s", dtmf) != 1) { + spa_log_debug(backend->log, "Failed to parse AT+VTS: \"%s\"", buf); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + if (!mm_send_dtmf(backend->modemmanager, dtmf, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else { + return false; + } + + return true; +} + +static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int features, gain, selected_codec, indicator, value; + char* token; + + while ((token = strsep(&buf, "\r\n"))) { + if (sscanf(token, "+BRSF:%u", &features) == 1) { + if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) && + rfcomm->msbc_supported_by_hfp) + rfcomm->codec_negotiation_supported = true; + } else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) { + if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) { + spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec); + } else { + spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec); + + /* send codec selection to AG */ + rfcomm_send_cmd(rfcomm, "AT+BCS=%u", selected_codec); + + rfcomm->hf_state = hfp_hf_bcs; + + if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) { + if (rfcomm->transport) + spa_bt_transport_free(rfcomm->transport); + + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + } else { + rfcomm->transport->codec = selected_codec; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + } + } else if (sscanf(token, "+VGM%*1[:=]%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", token); + } + } else if (sscanf(token, "+VGS%*1[:=]%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", token); + } + } else if (spa_strstartswith(token, "+CIND: (")) { + uint8_t i = 1; + while (strstr(token, "\"")) { + token += strcspn(token, "\"") + 1; + token[strcspn(token, "\"")] = 0; + rfcomm->hf_indicators[i] = strdup(token); + token += strcspn(token, "\"") + 1; + i++; + if (i == MAX_HF_INDICATORS) { + break; + } + } + } else if (spa_strstartswith(token, "+CIND: ")) { + token[strcspn(token, "\r")] = 0; + token[strcspn(token, "\n")] = 0; + token += strlen("+CIND: "); + uint8_t i = 1; + while (strlen(token)) { + if (i >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[i]) { + break; + } + token[strcspn(token, ",")] = 0; + spa_log_info(backend->log, "AG indicator state: %s = %i", rfcomm->hf_indicators[i], atoi(token)); + + if (spa_streq(rfcomm->hf_indicators[i], "battchg")) { + spa_bt_device_report_battery_level(rfcomm->device, atoi(token) * 100 / 5); + } + + token += strcspn(token, "\0") + 1; + i++; + } + } else if (sscanf(token, "+CIEV: %u,%u", &indicator, &value) == 2) { + if (indicator >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[indicator]) { + spa_log_warn(backend->log, "indicator %u has not been registered, ignoring", indicator); + } else { + spa_log_info(backend->log, "AG indicator update: %s = %u", rfcomm->hf_indicators[indicator], value); + + if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) { + spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5); + } + } + } else if (spa_strstartswith(token, "OK")) { + switch(rfcomm->hf_state) { + case hfp_hf_brsf: + if (rfcomm->codec_negotiation_supported) { + rfcomm_send_cmd(rfcomm, "AT+BAC=1,2"); + rfcomm->hf_state = hfp_hf_bac; + } else { + rfcomm_send_cmd(rfcomm, "AT+CIND=?"); + rfcomm->hf_state = hfp_hf_cind1; + } + break; + case hfp_hf_bac: + rfcomm_send_cmd(rfcomm, "AT+CIND=?"); + rfcomm->hf_state = hfp_hf_cind1; + break; + case hfp_hf_cind1: + rfcomm_send_cmd(rfcomm, "AT+CIND?"); + rfcomm->hf_state = hfp_hf_cind2; + break; + case hfp_hf_cind2: + rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1"); + rfcomm->hf_state = hfp_hf_cmer; + break; + case hfp_hf_cmer: + rfcomm->hf_state = hfp_hf_slc1; + rfcomm->slc_configured = true; + if (!rfcomm->codec_negotiation_supported) { + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + } else { + rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + /* Report volume on SLC establishment */ + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + break; + case hfp_hf_slc2: + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + break; + case hfp_hf_vgs: + rfcomm->hf_state = hfp_hf_slc1; + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) + rfcomm->hf_state = hfp_hf_vgm; + break; + default: + break; + } + } + } + + return true; +} + +#endif + +static void rfcomm_event(struct spa_source *source) +{ + struct rfcomm *rfcomm = source->data; + struct impl *backend = rfcomm->backend; + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_info(backend->log, "lost RFCOMM connection."); + rfcomm_free(rfcomm); + return; + } + + if (source->rmask & SPA_IO_IN) { + char buf[512]; + ssize_t len; + bool res = false; + + len = read(source->fd, buf, 511); + if (len < 0) { + spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno)); + return; + } + buf[len] = 0; + spa_log_debug(backend->log, "RFCOMM << %s", buf); + + switch (rfcomm->profile) { +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + case SPA_BT_PROFILE_HSP_HS: + res = rfcomm_hsp_ag(rfcomm, buf); + break; + case SPA_BT_PROFILE_HSP_AG: + res = rfcomm_hsp_hs(rfcomm, buf); + break; +#endif +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + case SPA_BT_PROFILE_HFP_HF: + res = rfcomm_hfp_ag(rfcomm, buf); + break; + case SPA_BT_PROFILE_HFP_AG: + res = rfcomm_hfp_hf(rfcomm, buf); + break; +#endif + default: + break; + } + + if (!res) { + spa_log_debug(backend->log, "RFCOMM received unsupported command: %s", buf); + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_SUPPORTED); + } + } +} + +static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapter, bool msbc) +{ + struct sockaddr_sco addr; + socklen_t len; + bdaddr_t src; + int sock = -1; + + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO); + if (sock < 0) { + spa_log_error(backend->log, "socket(SEQPACKET, SCO) %s", strerror(errno)); + goto fail; + } + + str2ba(adapter->address, &src); + + len = sizeof(addr); + memset(&addr, 0, len); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &src); + + if (bind(sock, (struct sockaddr *) &addr, len) < 0) { + spa_log_error(backend->log, "bind(): %s", strerror(errno)); + goto fail; + } + + spa_log_debug(backend->log, "msbc=%d", (int)msbc); + if (msbc) { + /* set correct socket options for mSBC */ + struct bt_voice voice_config; + memset(&voice_config, 0, sizeof(voice_config)); + voice_config.setting = BT_VOICE_TRANSPARENT; + if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) { + spa_log_error(backend->log, "setsockopt(): %s", strerror(errno)); + goto fail; + } + } + + return sock; + +fail: + if (sock >= 0) + close(sock); + return -1; +} + +static int sco_do_connect(struct spa_bt_transport *t) +{ + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + struct spa_bt_device *d = t->device; + struct transport_data *td = t->user_data; + struct sockaddr_sco addr; + socklen_t len; + int err; + int sock; + bdaddr_t dst; + int retry = 2; + + spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%u", + t, t->codec); + + if (d->adapter == NULL) + return -EIO; + + str2ba(d->address, &dst); + +again: + sock = sco_create_socket(backend, d->adapter, (t->codec == HFP_AUDIO_CODEC_MSBC)); + if (sock < 0) + return -1; + + len = sizeof(addr); + memset(&addr, 0, len); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &dst); + + spa_log_debug(backend->log, "transport %p: doing connect", t); + err = connect(sock, (struct sockaddr *) &addr, len); + if (err < 0 && errno == ECONNABORTED && retry-- > 0) { + spa_log_warn(backend->log, "connect(): %s. Remaining retry:%d", + strerror(errno), retry); + close(sock); + goto again; + } else if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) { + spa_log_error(backend->log, "connect(): %s", strerror(errno)); +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (errno == EOPNOTSUPP && t->codec == HFP_AUDIO_CODEC_MSBC && + td->rfcomm->msbc_supported_by_hfp) { + /* Adapter doesn't support msbc. Renegotiate. */ + d->adapter->msbc_probed = true; + d->adapter->has_msbc = false; + td->rfcomm->msbc_supported_by_hfp = false; + if (t->profile == SPA_BT_PROFILE_HFP_HF) { + td->rfcomm->hfp_ag_switching_codec = true; + rfcomm_send_reply(td->rfcomm, "+BCS: 1"); + } else if (t->profile == SPA_BT_PROFILE_HFP_AG) { + rfcomm_send_cmd(td->rfcomm, "AT+BAC=1"); + } + } +#endif + goto fail_close; + } + + return sock; + +fail_close: + close(sock); + return -1; +} + +static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later); + +static void wait_for_socket(int fd) +{ + struct pollfd fds[1]; + const int timeout_ms = 500; + + fds[0].fd = fd; + fds[0].events = POLLIN | POLLERR | POLLHUP; + poll(fds, 1, timeout_ms); +} + +static int sco_acquire_cb(void *data, bool optional) +{ + struct spa_bt_transport *t = data; + struct transport_data *td = t->user_data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + int sock; + socklen_t len; + + spa_log_debug(backend->log, "transport %p: enter sco_acquire_cb", t); + + if (optional || t->fd > 0) + sock = t->fd; + else + sock = sco_do_connect(t); + + if (sock < 0) + goto fail; + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + rfcomm_hfp_ag_set_cind(td->rfcomm, true); +#endif + + /* + * Send RFCOMM volume after connection is ready, and also after + * a timeout. + * + * Some headsets adjust their HFP volume when in A2DP mode + * without reporting via RFCOMM to us, so the volume level can + * be out of sync, and we can't know what it is. Moreover, they may + * take the first +VGS command after connection only partially + * into account, and need a long enough timeout. + * + * E.g. with Sennheiser HD-250BT, the first +VGS changes the + * actual volume, but does not update the level in the hardware + * volume buttons, which is updated by an +VGS event only after + * sufficient time is elapsed from the connection. + */ + wait_for_socket(sock); + rfcomm_ag_sync_volume(td->rfcomm, false); + rfcomm_ag_sync_volume(td->rfcomm, true); + + t->fd = sock; + + /* Fallback value */ + t->read_mtu = 48; + t->write_mtu = 48; + + if (true) { + struct sco_options sco_opt; + + len = sizeof(sco_opt); + memset(&sco_opt, 0, len); + + if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0) + spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults"); + else { + spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu); + t->read_mtu = sco_opt.mtu; + t->write_mtu = sco_opt.mtu; + } + } + spa_log_debug(backend->log, "transport %p: read_mtu=%u, write_mtu=%u", t, t->read_mtu, t->write_mtu); + + return 0; + +fail: + return -1; +} + +static int sco_release_cb(void *data) +{ + struct spa_bt_transport *t = data; + struct transport_data *td = t->user_data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + + spa_log_info(backend->log, "Transport %s released", t->path); + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + rfcomm_hfp_ag_set_cind(td->rfcomm, false); +#endif + + if (t->sco_io) { + spa_bt_sco_io_destroy(t->sco_io); + t->sco_io = NULL; + } + + if (t->fd > 0) { + /* Shutdown and close the socket */ + shutdown(t->fd, SHUT_RDWR); + close(t->fd); + t->fd = -1; + } + + return 0; +} + +static void sco_event(struct spa_source *source) +{ + struct spa_bt_transport *t = source->data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno)); + if (t->fd >= 0) { + if (source->loop) + spa_loop_remove_source(source->loop, source); + shutdown(t->fd, SHUT_RDWR); + close (t->fd); + t->fd = -1; + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE); + } + } +} + +static void sco_listen_event(struct spa_source *source) +{ + struct impl *backend = source->data; + struct sockaddr_sco addr; + socklen_t addrlen; + int sock = -1; + char local_address[18], remote_address[18]; + struct rfcomm *rfcomm; + struct spa_bt_transport *t = NULL; + struct transport_data *td; + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_error(backend->log, "error listening SCO connection: %s", strerror(errno)); + goto fail; + } + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + spa_log_debug(backend->log, "doing accept"); + sock = accept(source->fd, (struct sockaddr *) &addr, &addrlen); + if (sock < 0) { + if (errno != EAGAIN) + spa_log_error(backend->log, "SCO accept(): %s", strerror(errno)); + goto fail; + } + + ba2str(&addr.sco_bdaddr, remote_address); + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + if (getsockname(sock, (struct sockaddr *) &addr, &addrlen) < 0) { + spa_log_error(backend->log, "SCO getsockname(): %s", strerror(errno)); + goto fail; + } + + ba2str(&addr.sco_bdaddr, local_address); + + /* Find transport for local and remote address */ + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->transport && spa_streq(rfcomm->transport->device->address, remote_address) && + spa_streq(rfcomm->transport->device->adapter->address, local_address)) { + t = rfcomm->transport; + break; + } + } + if (!t) { + spa_log_debug(backend->log, "No transport for adapter %s and remote %s", + local_address, remote_address); + goto fail; + } + + /* The Synchronous Connection shall always be established by the AG, i.e. the remote profile + should be a HSP AG or HFP AG profile */ + if ((t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) == 0) { + spa_log_debug(backend->log, "transport %p: Rejecting incoming audio connection to an AG profile", t); + goto fail; + } + + if (t->fd >= 0) { + spa_log_debug(backend->log, "transport %p: Rejecting, audio already connected", t); + goto fail; + } + + spa_log_debug(backend->log, "transport %p: codec=%u", t, t->codec); + if (backend->defer_setup_enabled) { + /* In BT_DEFER_SETUP mode, when a connection is accepted, the listening socket is unblocked but + * the effective connection setup happens only on first receive, allowing to configure the + * accepted socket. */ + char buff; + + if (t->codec == HFP_AUDIO_CODEC_MSBC) { + /* set correct socket options for mSBC */ + struct bt_voice voice_config; + memset(&voice_config, 0, sizeof(voice_config)); + voice_config.setting = BT_VOICE_TRANSPARENT; + if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) { + spa_log_error(backend->log, "transport %p: setsockopt(): %s", t, strerror(errno)); + goto fail; + } + } + + /* First read from the accepted socket is non-blocking and returns a zero length buffer. */ + if (read(sock, &buff, 1) == -1) { + spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(errno)); + goto fail; + } + } + + t->fd = sock; + + td = t->user_data; + td->sco.func = sco_event; + td->sco.data = t; + td->sco.fd = sock; + td->sco.mask = SPA_IO_HUP | SPA_IO_ERR; + td->sco.rmask = 0; + spa_loop_add_source(backend->main_loop, &td->sco); + + spa_log_debug(backend->log, "transport %p: audio connected", t); + + /* Report initial volume to remote */ + if (t->profile == SPA_BT_PROFILE_HSP_AG) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hs_state = hsp_hs_vgs; + else + rfcomm->hs_state = hsp_hs_init1; + } else if (t->profile == SPA_BT_PROFILE_HFP_AG) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + else + rfcomm->hf_state = hfp_hf_slc1; + } + + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING); + return; + +fail: + if (sock >= 0) + close(sock); + return; +} + +static int sco_listen(struct impl *backend) +{ + struct sockaddr_sco addr; + int sock; + uint32_t defer = 1; + + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, BTPROTO_SCO); + if (sock < 0) { + spa_log_error(backend->log, "socket(SEQPACKET, SCO) %m"); + return -errno; + } + + /* Bind to local address */ + memset(&addr, 0, sizeof(addr)); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, BDADDR_ANY); + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + spa_log_error(backend->log, "bind(): %m"); + goto fail_close; + } + + if (setsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, &defer, sizeof(defer)) < 0) { + spa_log_warn(backend->log, "Can't enable deferred setup: %s", strerror(errno)); + backend->defer_setup_enabled = 0; + } else { + backend->defer_setup_enabled = 1; + } + + spa_log_debug(backend->log, "doing listen"); + if (listen(sock, 1) < 0) { + spa_log_error(backend->log, "listen(): %m"); + goto fail_close; + } + + backend->sco.func = sco_listen_event; + backend->sco.data = backend; + backend->sco.fd = sock; + backend->sco.mask = SPA_IO_IN; + backend->sco.rmask = 0; + spa_loop_add_source(backend->main_loop, &backend->sco); + + return sock; + +fail_close: + close(sock); + return -1; +} + +static int rfcomm_ag_set_volume(struct spa_bt_transport *t, int id) +{ + struct transport_data *td = t->user_data; + struct rfcomm *rfcomm = td->rfcomm; + const char *format; + int value; + + if (!rfcomm_volume_enabled(rfcomm) + || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + || !(rfcomm->has_volume && rfcomm->volumes[id].active)) + return -ENOTSUP; + + value = rfcomm->volumes[id].hw_volume; + + if (id == SPA_BT_VOLUME_ID_RX) + if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF) + format = "+VGM: %d"; + else + format = "+VGM=%d"; + else if (id == SPA_BT_VOLUME_ID_TX) + if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF) + format = "+VGS: %d"; + else + format = "+VGS=%d"; + else + spa_assert_not_reached(); + + if (rfcomm->transport) + rfcomm_send_reply(rfcomm, format, value); + + return 0; +} + +static int sco_set_volume_cb(void *data, int id, float volume) +{ + struct spa_bt_transport *t = data; + struct spa_bt_transport_volume *t_volume = &t->volumes[id]; + struct transport_data *td = t->user_data; + struct rfcomm *rfcomm = td->rfcomm; + int value; + + if (!rfcomm_volume_enabled(rfcomm) + || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + || !(rfcomm->has_volume && rfcomm->volumes[id].active)) + return -ENOTSUP; + + value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max); + t_volume->volume = volume; + + if (rfcomm->volumes[id].hw_volume == value) + return 0; + rfcomm->volumes[id].hw_volume = value; + + return rfcomm_ag_set_volume(t, id); +} + +static const struct spa_bt_transport_implementation sco_transport_impl = { + SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, + .acquire = sco_acquire_cb, + .release = sco_release_cb, + .set_volume = sco_set_volume_cb, +}; + +static struct rfcomm *device_find_rfcomm(struct impl *backend, struct spa_bt_device *device) +{ + struct rfcomm *rfcomm; + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->device == device) + return rfcomm; + } + return NULL; +} + +static int backend_native_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct impl *backend = data; + struct rfcomm *rfcomm; + + rfcomm = device_find_rfcomm(backend, device); + if (rfcomm == NULL || rfcomm->profile != SPA_BT_PROFILE_HFP_HF) + return -ENOTSUP; + + if (codec == HFP_AUDIO_CODEC_CVSD) + return 1; + + return (codec == HFP_AUDIO_CODEC_MSBC && + (rfcomm->profile == SPA_BT_PROFILE_HFP_AG || + rfcomm->profile == SPA_BT_PROFILE_HFP_HF) && + rfcomm->msbc_supported_by_hfp && + rfcomm->codec_negotiation_supported) ? 1 : 0; +#else + return -ENOTSUP; +#endif +} + +static int codec_switch_stop_timer(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct itimerspec ts; + + if (rfcomm->timer.data == NULL) + return 0; + + spa_loop_remove_source(backend->main_loop, &rfcomm->timer); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL); + spa_system_close(backend->main_system, rfcomm->timer.fd); + rfcomm->timer.data = NULL; + return 0; +} + +static void volume_sync_stop_timer(struct rfcomm *rfcomm) +{ + if (rfcomm->volume_sync_timer) + spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer, + NULL, NULL, false); +} + +static void volume_sync_timer_event(void *data, uint64_t expirations) +{ + struct rfcomm *rfcomm = data; + + volume_sync_stop_timer(rfcomm); + + if (rfcomm->transport) { + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX); + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX); + } +} + +static int volume_sync_start_timer(struct rfcomm *rfcomm) +{ + struct timespec ts; + const uint64_t timeout = 1500 * SPA_NSEC_PER_MSEC; + + if (rfcomm->volume_sync_timer == NULL) + rfcomm->volume_sync_timer = spa_loop_utils_add_timer(rfcomm->backend->loop_utils, + volume_sync_timer_event, rfcomm); + + if (rfcomm->volume_sync_timer == NULL) + return -EIO; + + ts.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.tv_nsec = timeout % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer, + &ts, NULL, false); + + return 0; +} + +static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later) +{ + if (rfcomm->transport == NULL) + return -ENOENT; + + if (!later) { + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX); + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX); + } else { + volume_sync_start_timer(rfcomm); + } + + return 0; +} + +static void codec_switch_timer_event(struct spa_source *source) +{ + struct rfcomm *rfcomm = source->data; + struct impl *backend = rfcomm->backend; + uint64_t exp; + + if (spa_system_timerfd_read(backend->main_system, source->fd, &exp) < 0) + spa_log_warn(backend->log, "error reading timerfd: %s", strerror(errno)); + + codec_switch_stop_timer(rfcomm); + + spa_log_debug(backend->log, "rfcomm %p: codec switch timeout", rfcomm); + + switch (rfcomm->hfp_ag_initial_codec_setup) { + case HFP_AG_INITIAL_CODEC_SETUP_SEND: + /* Retry codec selection */ + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_WAIT; + rfcomm_send_reply(rfcomm, "+BCS: 2"); + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + return; + case HFP_AG_INITIAL_CODEC_SETUP_WAIT: + /* Failure, try falling back to CVSD. */ + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; + if (rfcomm->transport == NULL) { + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + } else { + rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + rfcomm_send_reply(rfcomm, "+BCS: 1"); + return; + default: + break; + } + + if (rfcomm->hfp_ag_switching_codec) { + rfcomm->hfp_ag_switching_codec = false; + if (rfcomm->device) + spa_bt_device_emit_codec_switched(rfcomm->device, -EIO); + } +} + +static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec) +{ + struct impl *backend = rfcomm->backend; + struct itimerspec ts; + + spa_log_debug(backend->log, "rfcomm %p: start timer", rfcomm); + if (rfcomm->timer.data == NULL) { + rfcomm->timer.data = rfcomm; + rfcomm->timer.func = codec_switch_timer_event; + rfcomm->timer.fd = spa_system_timerfd_create(backend->main_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + rfcomm->timer.mask = SPA_IO_IN; + rfcomm->timer.rmask = 0; + spa_loop_add_source(backend->main_loop, &rfcomm->timer); + } + ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC; + ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL); + return 0; +} + +static int backend_native_ensure_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct impl *backend = data; + struct rfcomm *rfcomm; + int res; + + res = backend_native_supports_codec(data, device, codec); + if (res <= 0) + return -EINVAL; + + rfcomm = device_find_rfcomm(backend, device); + if (rfcomm == NULL) + return -ENOTSUP; + + if (!rfcomm->codec_negotiation_supported) + return -ENOTSUP; + + if (rfcomm->codec == codec) { + spa_bt_device_emit_codec_switched(device, 0); + return 0; + } + + if ((res = rfcomm_send_reply(rfcomm, "+BCS: %u", codec)) < 0) + return res; + + rfcomm->hfp_ag_switching_codec = true; + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + + return 0; +#else + return -ENOTSUP; +#endif +} + +static void device_destroy(void *data) +{ + struct rfcomm *rfcomm = data; + rfcomm_free(rfcomm); +} + +static const struct spa_bt_device_events device_events = { + SPA_VERSION_BT_DEVICE_EVENTS, + .destroy = device_destroy, +}; + +static enum spa_bt_profile path_to_profile(const char *path) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + if (spa_streq(path, PROFILE_HSP_AG)) + return SPA_BT_PROFILE_HSP_HS; + + if (spa_streq(path, PROFILE_HSP_HS)) + return SPA_BT_PROFILE_HSP_AG; +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (spa_streq(path, PROFILE_HFP_AG)) + return SPA_BT_PROFILE_HFP_HF; + + if (spa_streq(path, PROFILE_HFP_HF)) + return SPA_BT_PROFILE_HFP_AG; +#endif + + return SPA_BT_PROFILE_NULL; +} + +static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + DBusMessage *r; + DBusMessageIter it[5]; + const char *handler, *path; + enum spa_bt_profile profile; + struct rfcomm *rfcomm; + struct spa_bt_device *d; + struct spa_bt_transport *t = NULL; + int fd; + + if (!dbus_message_has_signature(m, "oha{sv}")) { + spa_log_warn(backend->log, "invalid NewConnection() signature"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + handler = dbus_message_get_path(m); + profile = path_to_profile(handler); + if (profile == SPA_BT_PROFILE_NULL) { + spa_log_warn(backend->log, "invalid handler %s", handler); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_init(m, &it[0]); + dbus_message_iter_get_basic(&it[0], &path); + + d = spa_bt_device_find(backend->monitor, path); + if (d == NULL || d->adapter == NULL) { + spa_log_warn(backend->log, "unknown device for path %s", path); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + spa_bt_device_add_profile(d, profile); + + dbus_message_iter_next(&it[0]); + dbus_message_iter_get_basic(&it[0], &fd); + + spa_log_debug(backend->log, "NewConnection path=%s, fd=%d, profile %s", path, fd, handler); + + rfcomm = calloc(1, sizeof(struct rfcomm)); + if (rfcomm == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + rfcomm->backend = backend; + rfcomm->profile = profile; + rfcomm->device = d; + rfcomm->path = strdup(path); + rfcomm->source.func = rfcomm_event; + rfcomm->source.data = rfcomm; + rfcomm->source.fd = fd; + rfcomm->source.mask = SPA_IO_IN; + rfcomm->source.rmask = 0; + /* By default all indicators are enabled */ + rfcomm->cind_enabled_indicators = 0xFFFFFFFF; + memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators); + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) { + if (rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) + rfcomm->volumes[i].active = true; + rfcomm->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID; + } + + spa_bt_device_add_listener(d, &rfcomm->device_listener, &device_events, rfcomm); + spa_loop_add_source(backend->main_loop, &rfcomm->source); + spa_list_append(&backend->rfcomm_list, &rfcomm->link); + + if (profile == SPA_BT_PROFILE_HSP_HS || profile == SPA_BT_PROFILE_HSP_AG) { + t = _transport_create(rfcomm); + if (t == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + goto fail_need_memory; + } + rfcomm->transport = t; + rfcomm->has_volume = rfcomm_volume_enabled(rfcomm); + + if (profile == SPA_BT_PROFILE_HSP_AG) { + rfcomm->hs_state = hsp_hs_init1; + } + + spa_bt_device_connect_profile(t->device, profile); + + spa_log_debug(backend->log, "Transport %s available for profile %s", t->path, handler); + } else if (profile == SPA_BT_PROFILE_HFP_AG) { + /* Start SLC connection */ + unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_NONE; + + /* Decide if we want to signal that the HF supports mSBC negotiation + This should be done when the bluetooth adapter supports the necessary transport mode */ + if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) { + /* set the feature bit that indicates HF supports codec negotiation */ + hf_features |= SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION; + rfcomm->msbc_supported_by_hfp = true; + rfcomm->codec_negotiation_supported = false; + } else { + rfcomm->msbc_supported_by_hfp = false; + rfcomm->codec_negotiation_supported = false; + } + + if (rfcomm_volume_enabled(rfcomm)) { + rfcomm->has_volume = true; + hf_features |= SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL; + } + + /* send command to AG with the features supported by Hands-Free */ + rfcomm_send_cmd(rfcomm, "AT+BRSF=%u", hf_features); + + rfcomm->hf_state = hfp_hf_brsf; + } + + if (rfcomm_volume_enabled(rfcomm) && (profile == SPA_BT_PROFILE_HFP_HF || profile == SPA_BT_PROFILE_HSP_HS)) { + uint32_t device_features; + if (spa_bt_quirks_get_features(backend->quirks, d->adapter, d, &device_features) == 0) { + rfcomm->broken_mic_hw_volume = !(device_features & SPA_BT_FEATURE_HW_VOLUME_MIC); + if (rfcomm->broken_mic_hw_volume) + spa_log_debug(backend->log, "microphone HW volume disabled by quirk"); + } + } + + if ((r = dbus_message_new_method_return(m)) == NULL) + goto fail_need_memory; + if (!dbus_connection_send(conn, r, NULL)) + goto fail_need_memory; + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; + +fail_need_memory: + if (rfcomm) + rfcomm_free(rfcomm); + return DBUS_HANDLER_RESULT_NEED_MEMORY; +} + +static DBusHandlerResult profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + DBusMessage *r; + const char *handler, *path; + struct spa_bt_device *d; + enum spa_bt_profile profile = SPA_BT_PROFILE_NULL; + DBusMessageIter it[5]; + struct rfcomm *rfcomm, *rfcomm_tmp; + + if (!dbus_message_has_signature(m, "o")) { + spa_log_warn(backend->log, "invalid RequestDisconnection() signature"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + handler = dbus_message_get_path(m); + profile = path_to_profile(handler); + if (profile == SPA_BT_PROFILE_NULL) { + spa_log_warn(backend->log, "invalid handler %s", handler); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_init(m, &it[0]); + dbus_message_iter_get_basic(&it[0], &path); + + d = spa_bt_device_find(backend->monitor, path); + if (d == NULL || d->adapter == NULL) { + spa_log_warn(backend->log, "unknown device for path %s", path); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + spa_list_for_each_safe(rfcomm, rfcomm_tmp, &backend->rfcomm_list, link) { + if (rfcomm->device == d && rfcomm->profile == profile) { + rfcomm_free(rfcomm); + } + } + spa_bt_device_check_profiles(d, false); + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + DBusMessage *r; + DBusHandlerResult res; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = PROFILE_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + res = DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Release")) + res = profile_release(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection")) + res = profile_request_disconnection(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection")) + res = profile_new_connection(c, m, userdata); + else + res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + return res; +} + +static void register_profile_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *backend = user_data; + DBusMessage *r; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + spa_log_warn(backend->log, "Register profile not supported"); + goto finish; + } + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(backend->log, "Error registering profile"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "RegisterProfile() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + finish: + dbus_message_unref(r); + dbus_pending_call_unref(pending); +} + +static int register_profile(struct impl *backend, const char *profile, const char *uuid) +{ + DBusMessage *m; + DBusMessageIter it[4]; + dbus_bool_t autoconnect; + dbus_uint16_t version, chan, features; + char *str; + DBusPendingCall *call; + + if (!(backend->enabled_profiles & spa_bt_profile_from_uuid(uuid))) + return -ECANCELED; + + spa_log_debug(backend->log, "Registering Profile %s %s", profile, uuid); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", + BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile"); + if (m == NULL) + return -ENOMEM; + + dbus_message_iter_init_append(m, &it[0]); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_OBJECT_PATH, &profile); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &uuid); + dbus_message_iter_open_container(&it[0], DBUS_TYPE_ARRAY, "{sv}", &it[1]); + + if (spa_streq(uuid, SPA_BT_UUID_HSP_HS) || + spa_streq(uuid, SPA_BT_UUID_HSP_HS_ALT)) { + + /* In the headset role, the connection will only be initiated from the remote side */ + str = "AutoConnect"; + autoconnect = 0; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "b", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_BOOLEAN, &autoconnect); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + str = "Channel"; + chan = HSP_HS_DEFAULT_CHANNEL; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &chan); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + /* HSP version 1.2 */ + str = "Version"; + version = 0x0102; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + } else if (spa_streq(uuid, SPA_BT_UUID_HFP_AG)) { + str = "Features"; + + /* We announce wideband speech support anyway */ + features = SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + /* HFP version 1.7 */ + str = "Version"; + version = 0x0107; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + } else if (spa_streq(uuid, SPA_BT_UUID_HFP_HF)) { + str = "Features"; + + /* We announce wideband speech support anyway */ + features = SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + /* HFP version 1.7 */ + str = "Version"; + version = 0x0107; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + } + dbus_message_iter_close_container(&it[0], &it[1]); + + dbus_connection_send_with_reply(backend->conn, m, &call, -1); + dbus_pending_call_set_notify(call, register_profile_reply, backend, NULL); + dbus_message_unref(m); + return 0; +} + +static void unregister_profile(struct impl *backend, const char *profile) +{ + DBusMessage *m, *r; + DBusError err; + + spa_log_debug(backend->log, "Unregistering Profile %s", profile); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", + BLUEZ_PROFILE_MANAGER_INTERFACE, "UnregisterProfile"); + if (m == NULL) + return; + + dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &profile, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_info(backend->log, "Unregistering Profile %s failed", profile); + dbus_error_free(&err); + return; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "UnregisterProfile() returned error: %s", dbus_message_get_error_name(r)); + return; + } + + dbus_message_unref(r); +} + +static int backend_native_register_profiles(void *data) +{ + struct impl *backend = data; + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + register_profile(backend, PROFILE_HSP_AG, SPA_BT_UUID_HSP_AG); + register_profile(backend, PROFILE_HSP_HS, SPA_BT_UUID_HSP_HS); +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + register_profile(backend, PROFILE_HFP_AG, SPA_BT_UUID_HFP_AG); + register_profile(backend, PROFILE_HFP_HF, SPA_BT_UUID_HFP_HF); +#endif + + if (backend->enabled_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + sco_listen(backend); + + return 0; +} + +static void sco_close(struct impl *backend) +{ + if (backend->sco.fd >= 0) { + if (backend->sco.loop) + spa_loop_remove_source(backend->sco.loop, &backend->sco); + shutdown(backend->sco.fd, SHUT_RDWR); + close (backend->sco.fd); + backend->sco.fd = -1; + } +} + +static int backend_native_unregister_profiles(void *data) +{ + struct impl *backend = data; + + sco_close(backend); + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_AG) + unregister_profile(backend, PROFILE_HSP_AG); + if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_HS) + unregister_profile(backend, PROFILE_HSP_HS); +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_AG) + unregister_profile(backend, PROFILE_HFP_AG); + if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_HF) + unregister_profile(backend, PROFILE_HFP_HF); +#endif + + return 0; +} + +static void send_ciev_for_each_rfcomm(struct impl *backend, int indicator, int value) +{ + struct rfcomm *rfcomm; + + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->slc_configured && + ((indicator == CIND_CALL || indicator == CIND_CALLSETUP || indicator == CIND_CALLHELD) || + (rfcomm->cind_call_notify && (rfcomm->cind_enabled_indicators & (1 << indicator))))) + rfcomm_send_reply(rfcomm, "+CIEV: %d,%d", indicator, value); + } +} + +static void ring_timer_event(void *data, uint64_t expirations) +{ + struct impl *backend = data; + const char *number; + unsigned int type; + struct timespec ts; + const uint64_t timeout = 1 * SPA_NSEC_PER_SEC; + struct rfcomm *rfcomm; + + number = mm_get_incoming_call_number(backend->modemmanager); + if (number) { + if (spa_strstartswith(number, "+")) + type = INTERNATIONAL_NUMBER; + else + type = NATIONAL_NUMBER; + } + + ts.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.tv_nsec = timeout % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, &ts, NULL, false); + + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->slc_configured) { + rfcomm_send_reply(rfcomm, "RING"); + if (rfcomm->clip_notify && number) + rfcomm_send_reply(rfcomm, "+CLIP: \"%s\",%u", number, type); + } + } +} + +static void set_call_active(bool active, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.active_call != active) { + backend->modem.active_call = active; + send_ciev_for_each_rfcomm(backend, CIND_CALL, active); + } +} + +static void set_call_setup(enum call_setup value, void *user_data) +{ + struct impl *backend = user_data; + enum call_setup old = backend->modem.call_setup; + + if (backend->modem.call_setup != value) { + backend->modem.call_setup = value; + send_ciev_for_each_rfcomm(backend, CIND_CALLSETUP, value); + } + + if (value == CIND_CALLSETUP_INCOMING) { + if (backend->ring_timer == NULL) + backend->ring_timer = spa_loop_utils_add_timer(backend->loop_utils, ring_timer_event, backend); + + if (backend->ring_timer == NULL) { + spa_log_warn(backend->log, "Failed to create ring timer"); + return; + } + + ring_timer_event(backend, 0); + } else if (old == CIND_CALLSETUP_INCOMING) { + spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, NULL, NULL, false); + } +} + +void set_battery_level(unsigned int level, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->battery_level != level) { + backend->battery_level = level; + send_ciev_for_each_rfcomm(backend, CIND_BATTERY_LEVEL, level); + } +} + +static void set_modem_operator_name(const char *name, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.operator_name) { + free(backend->modem.operator_name); + backend->modem.operator_name = NULL; + } + + if (name) + backend->modem.operator_name = strdup(name); +} + +static void set_modem_roaming(bool is_roaming, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.network_is_roaming != is_roaming) { + backend->modem.network_is_roaming = is_roaming; + send_ciev_for_each_rfcomm(backend, CIND_ROAM, is_roaming); + } +} + +static void set_modem_own_number(const char *number, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.own_number) { + free(backend->modem.own_number); + backend->modem.own_number = NULL; + } + + if (number) + backend->modem.own_number = strdup(number); +} + +static void set_modem_service(bool available, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.network_has_service != available) { + backend->modem.network_has_service = available; + send_ciev_for_each_rfcomm(backend, CIND_SERVICE, available); + } +} + +static void set_modem_signal_strength(unsigned int strength, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.signal_strength != strength) { + backend->modem.signal_strength = strength; + send_ciev_for_each_rfcomm(backend, CIND_SIGNAL, strength); + } +} + +static void send_cmd_result(bool success, enum cmee_error error, void *user_data) +{ + struct rfcomm *rfcomm = user_data; + + if (success) { + rfcomm_send_reply(rfcomm, "OK"); + return; + } + + rfcomm_send_error(rfcomm, error); +} + +static int backend_native_free(void *data) +{ + struct impl *backend = data; + + struct rfcomm *rfcomm; + + sco_close(backend); + + if (backend->modemmanager) { + mm_unregister(backend); + backend->modemmanager = NULL; + } + + if (backend->upower) { + upower_unregister(backend->upower); + backend->upower = NULL; + } + + if (backend->ring_timer) + spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer); + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG); + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS); +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG); + dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_HF); +#endif + + spa_list_consume(rfcomm, &backend->rfcomm_list, link) + rfcomm_free(rfcomm); + + if (backend->modem.operator_name) + free(backend->modem.operator_name); + free(backend); + + return 0; +} + +static int parse_headset_roles(struct impl *backend, const struct spa_dict *info) +{ + const char *str; + int profiles = SPA_BT_PROFILE_NULL; + + if (info == NULL || + (str = spa_dict_lookup(info, PROP_KEY_HEADSET_ROLES)) == NULL) + goto fallback; + + profiles = spa_bt_profiles_from_json_array(str); + if (profiles < 0) + goto fallback; + + backend->enabled_profiles = profiles & SPA_BT_PROFILE_HEADSET_AUDIO; + return 0; +fallback: + backend->enabled_profiles = DEFAULT_ENABLED_PROFILES; + return 0; +} + +static const struct spa_bt_backend_implementation backend_impl = { + SPA_VERSION_BT_BACKEND_IMPLEMENTATION, + .free = backend_native_free, + .register_profiles = backend_native_register_profiles, + .unregister_profiles = backend_native_unregister_profiles, + .ensure_codec = backend_native_ensure_codec, + .supports_codec = backend_native_supports_codec, +}; + +static const struct mm_ops mm_ops = { + .send_cmd_result = send_cmd_result, + .set_modem_service = set_modem_service, + .set_modem_signal_strength = set_modem_signal_strength, + .set_modem_operator_name = set_modem_operator_name, + .set_modem_own_number = set_modem_own_number, + .set_modem_roaming = set_modem_roaming, + .set_call_active = set_call_active, + .set_call_setup = set_call_setup, +}; + +struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *backend; + + static const DBusObjectPathVTable vtable_profile = { + .message_function = profile_handler, + }; + + backend = calloc(1, sizeof(struct impl)); + if (backend == NULL) + return NULL; + + spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); + + backend->this.name = "native"; + backend->monitor = monitor; + backend->quirks = quirks; + backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); + backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + backend->conn = dbus_connection; + backend->sco.fd = -1; + + spa_log_topic_init(backend->log, &log_topic); + + spa_list_init(&backend->rfcomm_list); + + if (parse_headset_roles(backend, info) < 0) + goto fail; + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HSP_AG, + &vtable_profile, backend)) { + goto fail; + } + + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HSP_HS, + &vtable_profile, backend)) { + goto fail1; + } +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HFP_AG, + &vtable_profile, backend)) { + goto fail2; + } + + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HFP_HF, + &vtable_profile, backend)) { + goto fail3; + } +#endif + + backend->modemmanager = mm_register(backend->log, backend->conn, info, &mm_ops, backend); + backend->upower = upower_register(backend->log, backend->conn, set_battery_level, backend); + + return &backend->this; + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE +fail3: + dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG); +fail2: +#endif +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS); +fail1: + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG); +#endif +fail: + free(backend); + return NULL; +} |