diff -rNaud pulseaudio-13.0/src/modules/bluetooth/bluez5-util.c pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.c --- pulseaudio-13.0/src/modules/bluetooth/bluez5-util.c 2019-09-13 15:20:03.000000000 +0200 +++ pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.c 2019-09-17 09:14:04.372778000 +0200 @@ -171,11 +171,13 @@ } static bool device_supports_profile(pa_bluetooth_device *device, pa_bluetooth_profile_t profile) { + const pa_a2dp_codec_capabilities *a2dp_codec_capabilities; + const pa_a2dp_codec *a2dp_codec; + bool is_a2dp_sink; + pa_hashmap *endpoints; + void *state; + switch (profile) { - case PA_BLUETOOTH_PROFILE_A2DP_SINK: - return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SINK); - case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: - return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SOURCE); case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS) || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS_ALT) @@ -184,10 +186,33 @@ return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_AG) || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HFP_AG); case PA_BLUETOOTH_PROFILE_OFF: - pa_assert_not_reached(); + return true; + default: + break; } - pa_assert_not_reached(); + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(profile); + is_a2dp_sink = pa_bluetooth_profile_is_a2dp_sink(profile); + + if (is_a2dp_sink && !pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SINK)) + return false; + else if (!is_a2dp_sink && !pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SOURCE)) + return false; + + if (is_a2dp_sink) + endpoints = pa_hashmap_get(device->a2dp_sink_endpoints, &a2dp_codec->id); + else + endpoints = pa_hashmap_get(device->a2dp_source_endpoints, &a2dp_codec->id); + + if (!endpoints) + return false; + + PA_HASHMAP_FOREACH(a2dp_codec_capabilities, endpoints, state) { + if (a2dp_codec->can_accept_capabilities(a2dp_codec_capabilities->buffer, a2dp_codec_capabilities->size, is_a2dp_sink)) + return true; + } + + return false; } static bool device_is_profile_connected(pa_bluetooth_device *device, pa_bluetooth_profile_t profile) { @@ -199,9 +224,11 @@ static unsigned device_count_disconnected_profiles(pa_bluetooth_device *device) { pa_bluetooth_profile_t profile; + unsigned bluetooth_profile_count; unsigned count = 0; - for (profile = 0; profile < PA_BLUETOOTH_PROFILE_COUNT; profile++) { + bluetooth_profile_count = pa_bluetooth_profile_count(); + for (profile = 0; profile < bluetooth_profile_count; profile++) { if (!device_supports_profile(device, profile)) continue; @@ -224,6 +251,7 @@ pa_bluetooth_device *device = userdata; pa_strbuf *buf; pa_bluetooth_profile_t profile; + unsigned bluetooth_profile_count; bool first = true; char *profiles_str; @@ -231,7 +259,8 @@ buf = pa_strbuf_new(); - for (profile = 0; profile < PA_BLUETOOTH_PROFILE_COUNT; profile++) { + bluetooth_profile_count = pa_bluetooth_profile_count(); + for (profile = 0; profile < bluetooth_profile_count; profile++) { if (device_is_profile_connected(device, profile)) continue; @@ -429,14 +458,15 @@ } bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d) { - unsigned i; + unsigned i, bluetooth_profile_count; pa_assert(d); if (!d->valid) return false; - for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) + bluetooth_profile_count = pa_bluetooth_profile_count(); + for (i = 0; i < bluetooth_profile_count; i++) if (d->transports[i] && d->transports[i]->state != PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) return true; @@ -510,6 +540,42 @@ return 0; } +static unsigned pa_a2dp_codec_id_hash_func(const void *_p) { + unsigned hash; + const pa_a2dp_codec_id *p = _p; + + hash = p->codec_id; + hash = 31 * hash + ((p->vendor_id >> 0) & 0xFF); + hash = 31 * hash + ((p->vendor_id >> 8) & 0xFF); + hash = 31 * hash + ((p->vendor_id >> 16) & 0xFF); + hash = 31 * hash + ((p->vendor_id >> 24) & 0xFF); + hash = 31 * hash + ((p->vendor_codec_id >> 0) & 0xFF); + hash = 31 * hash + ((p->vendor_codec_id >> 8) & 0xFF); + return hash; +} + +static int pa_a2dp_codec_id_compare_func(const void *_a, const void *_b) { + const pa_a2dp_codec_id *a = _a; + const pa_a2dp_codec_id *b = _b; + + if (a->codec_id < b->codec_id) + return -1; + if (a->codec_id > b->codec_id) + return 1; + + if (a->vendor_id < b->vendor_id) + return -1; + if (a->vendor_id > b->vendor_id) + return 1; + + if (a->vendor_codec_id < b->vendor_codec_id) + return -1; + if (a->vendor_codec_id > b->vendor_codec_id) + return 1; + + return 0; +} + static pa_bluetooth_device* device_create(pa_bluetooth_discovery *y, const char *path) { pa_bluetooth_device *d; @@ -520,6 +586,9 @@ d->discovery = y; d->path = pa_xstrdup(path); d->uuids = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, pa_xfree); + d->a2dp_sink_endpoints = pa_hashmap_new_full(pa_a2dp_codec_id_hash_func, pa_a2dp_codec_id_compare_func, pa_xfree, (pa_free_cb_t)pa_hashmap_free); + d->a2dp_source_endpoints = pa_hashmap_new_full(pa_a2dp_codec_id_hash_func, pa_a2dp_codec_id_compare_func, pa_xfree, (pa_free_cb_t)pa_hashmap_free); + d->transports = pa_xnew0(pa_bluetooth_transport *, pa_bluetooth_profile_count()); pa_hashmap_put(y->devices, d->path, d); @@ -555,8 +624,26 @@ return NULL; } +static void remote_endpoint_remove(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *device; + pa_hashmap *endpoints; + void *devices_state; + void *state; + + PA_HASHMAP_FOREACH(device, y->devices, devices_state) { + PA_HASHMAP_FOREACH(endpoints, device->a2dp_sink_endpoints, state) + pa_hashmap_remove_and_free(endpoints, path); + + PA_HASHMAP_FOREACH(endpoints, device->a2dp_source_endpoints, state) + pa_hashmap_remove_and_free(endpoints, path); + } + + pa_log_debug("Remote endpoint %s removed", path); +} + + static void device_free(pa_bluetooth_device *d) { - unsigned i; + unsigned i, bluetooth_profile_count; pa_assert(d); @@ -564,7 +651,8 @@ pa_hook_fire(&d->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_UNLINK], d); - for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) { + bluetooth_profile_count = pa_bluetooth_profile_count(); + for (i = 0; i < bluetooth_profile_count; i++) { pa_bluetooth_transport *t; if (!(t = d->transports[i])) @@ -573,9 +661,11 @@ pa_bluetooth_transport_free(t); } - if (d->uuids) - pa_hashmap_free(d->uuids); + pa_hashmap_free(d->uuids); + pa_hashmap_free(d->a2dp_sink_endpoints); + pa_hashmap_free(d->a2dp_source_endpoints); + pa_xfree(d->transports); pa_xfree(d->path); pa_xfree(d->alias); pa_xfree(d->address); @@ -814,6 +904,149 @@ } } +static void parse_remote_endpoint_properties(pa_bluetooth_discovery *y, const char *endpoint, DBusMessageIter *i) { + DBusMessageIter element_i; + pa_bluetooth_device *device; + pa_hashmap *codec_endpoints; + pa_hashmap *endpoints; + pa_a2dp_codec_id *a2dp_codec_id; + pa_a2dp_codec_capabilities *a2dp_codec_capabilities; + const char *uuid = NULL; + const char *device_path = NULL; + uint8_t codec_id = 0; + bool have_codec_id = false; + const uint8_t *capabilities = NULL; + int capabilities_size = 0; + + pa_log_debug("Parsing remote endpoint %s", endpoint); + + dbus_message_iter_recurse(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); + + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property for remote endpoint %s", endpoint); + return; + } + + dbus_message_iter_recurse(&dict_i, &variant_i); + + if (pa_streq(key, "UUID")) { + if (dbus_message_iter_get_arg_type(&variant_i) != DBUS_TYPE_STRING) { + pa_log_warn("Remote endpoint %s property 'UUID' is not string, ignoring", endpoint); + return; + } + + dbus_message_iter_get_basic(&variant_i, &uuid); + } else if (pa_streq(key, "Codec")) { + if (dbus_message_iter_get_arg_type(&variant_i) != DBUS_TYPE_BYTE) { + pa_log_warn("Remote endpoint %s property 'Codec' is not byte, ignoring", endpoint); + return; + } + + dbus_message_iter_get_basic(&variant_i, &codec_id); + have_codec_id = true; + } else if (pa_streq(key, "Capabilities")) { + DBusMessageIter array; + + if (dbus_message_iter_get_arg_type(&variant_i) != DBUS_TYPE_ARRAY) { + pa_log_warn("Remote endpoint %s property 'Capabilities' is not array, ignoring", endpoint); + return; + } + + dbus_message_iter_recurse(&variant_i, &array); + if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_BYTE) { + pa_log_warn("Remote endpoint %s property 'Capabilities' is not array of bytes, ignoring", endpoint); + return; + } + + dbus_message_iter_get_fixed_array(&array, &capabilities, &capabilities_size); + } else if (pa_streq(key, "Device")) { + if (dbus_message_iter_get_arg_type(&variant_i) != DBUS_TYPE_OBJECT_PATH) { + pa_log_warn("Remote endpoint %s property 'Device' is not path, ignoring", endpoint); + return; + } + + dbus_message_iter_get_basic(&variant_i, &device_path); + } + + dbus_message_iter_next(&element_i); + } + + if (!uuid) { + pa_log_warn("Remote endpoint %s does not have property 'UUID', ignoring", endpoint); + return; + } + + if (!have_codec_id) { + pa_log_warn("Remote endpoint %s does not have property 'Codec', ignoring", endpoint); + return; + } + + if (!capabilities || !capabilities_size) { + pa_log_warn("Remote endpoint %s does not have property 'Capabilities', ignoring", endpoint); + return; + } + + if (!device_path) { + pa_log_warn("Remote endpoint %s does not have property 'Device', ignoring", endpoint); + return; + } + + device = pa_hashmap_get(y->devices, device_path); + if (!device) { + pa_log_warn("Device for remote endpoint %s was not found", endpoint); + return; + } + + if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) { + codec_endpoints = device->a2dp_sink_endpoints; + } else if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) { + codec_endpoints = device->a2dp_source_endpoints; + } else { + pa_log_warn("Remote endpoint %s does not have valid property 'UUID', ignoring", endpoint); + return; + } + + if (capabilities_size < 0 || capabilities_size > MAX_A2DP_CAPS_SIZE) { + pa_log_warn("Remote endpoint %s does not have valid property 'Capabilities', ignoring", endpoint); + return; + } + + a2dp_codec_id = pa_xmalloc0(sizeof(*a2dp_codec_id)); + a2dp_codec_id->codec_id = codec_id; + if (codec_id == A2DP_CODEC_VENDOR) { + if ((size_t)capabilities_size < sizeof(a2dp_vendor_codec_t)) { + pa_log_warn("Remote endpoint %s does not have valid property 'Capabilities', ignoring", endpoint); + return; + } + a2dp_codec_id->vendor_id = A2DP_GET_VENDOR_ID(*(a2dp_vendor_codec_t *)capabilities); + a2dp_codec_id->vendor_codec_id = A2DP_GET_CODEC_ID(*(a2dp_vendor_codec_t *)capabilities); + } else { + a2dp_codec_id->vendor_id = 0; + a2dp_codec_id->vendor_codec_id = 0; + } + + a2dp_codec_capabilities = pa_xmalloc0(sizeof(*a2dp_codec_capabilities) + capabilities_size); + a2dp_codec_capabilities->size = capabilities_size; + memcpy(a2dp_codec_capabilities->buffer, capabilities, capabilities_size); + + endpoints = pa_hashmap_get(codec_endpoints, a2dp_codec_id); + if (!endpoints) { + endpoints = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, pa_xfree); + pa_hashmap_put(codec_endpoints, a2dp_codec_id, endpoints); + } + + if (pa_hashmap_remove_and_free(endpoints, endpoint) >= 0) + pa_log_debug("Replacing existing remote endpoint %s", endpoint); + pa_hashmap_put(endpoints, pa_xstrdup(endpoint), a2dp_codec_capabilities); +} + static void parse_adapter_properties(pa_bluetooth_adapter *a, DBusMessageIter *i, bool is_property_change) { DBusMessageIter element_i; @@ -989,7 +1222,9 @@ parse_device_properties(d, &iface_i); - } else + } else if (pa_streq(interface, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) + parse_remote_endpoint_properties(y, path, &iface_i); + else pa_log_debug("Unknown interface %s found, skipping", interface); dbus_message_iter_next(&element_i); @@ -1194,6 +1429,8 @@ if (pa_streq(iface, BLUEZ_DEVICE_INTERFACE)) device_remove(y, p); + else if (pa_streq(iface, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) + remote_endpoint_remove(y, p); else if (pa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) adapter_remove(y, p); @@ -1245,6 +1482,10 @@ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; parse_device_properties(d, &arg_i); + } else if (pa_streq(iface, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) { + pa_log_info("Properties changed in remote endpoint %s", dbus_message_get_path(m)); + + parse_remote_endpoint_properties(y, dbus_message_get_path(m), &arg_i); } else if (pa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) { pa_bluetooth_transport *t; @@ -1265,21 +1506,202 @@ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } +struct change_a2dp_profile_data { + char *pa_endpoint; + pa_bluetooth_device *device; + void (*cb)(bool, void *); + void *userdata; +}; + +static void change_a2dp_profile_reply(DBusPendingCall *pending, void *userdata) { + DBusMessage *r; + pa_dbus_pending *p; + pa_bluetooth_discovery *y; + struct change_a2dp_profile_data *data; + bool success; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(y = p->context_data); + pa_assert_se(data = p->call_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + success = (dbus_message_get_type(r) != DBUS_MESSAGE_TYPE_ERROR); + if (success) + pa_log_info("Changing a2dp endpoint successed"); + else + pa_log_error("Changing a2dp endpoint failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, y->pending, p); + pa_dbus_pending_free(p); + pa_xfree(data->pa_endpoint); + + data->device->change_a2dp_profile_in_progress = false; + + data->cb(success, data->userdata); + + pa_xfree(data); +} + +const char *pa_bluetooth_device_find_endpoint_for_codec(const pa_bluetooth_device *device, const pa_a2dp_codec *a2dp_codec, bool is_a2dp_sink) { + pa_hashmap *endpoints; + + endpoints = pa_hashmap_get(is_a2dp_sink ? device->a2dp_sink_endpoints : device->a2dp_source_endpoints, &a2dp_codec->id); + if (!endpoints) + return NULL; + + return a2dp_codec->choose_remote_endpoint(endpoints, &device->discovery->core->default_sample_spec, is_a2dp_sink); +} + +bool pa_bluetooth_device_change_a2dp_profile(pa_bluetooth_device *device, pa_bluetooth_profile_t profile, void (*cb)(bool, void *), void *userdata) { + const pa_a2dp_codec *a2dp_codec; + bool is_a2dp_sink; + const char *endpoint; + char *pa_endpoint; + struct change_a2dp_profile_data *data; + uint8_t config[MAX_A2DP_CAPS_SIZE]; + uint8_t config_size; + pa_hashmap *endpoints; + pa_a2dp_codec_capabilities *capabilities; + DBusMessage *m; + DBusMessageIter iter, dict; + + if (device->change_a2dp_profile_in_progress) { + pa_log_error("Changing a2dp profile for %s to %s failed: Operation already in progress", device->path, pa_bluetooth_profile_to_string(profile)); + return false; + } + + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(profile); + is_a2dp_sink = pa_bluetooth_profile_is_a2dp_sink(profile); + + endpoint = pa_bluetooth_device_find_endpoint_for_codec(device, a2dp_codec, is_a2dp_sink); + if (!endpoint) { + pa_log_error("Changing a2dp profile for %s to %s failed: Remote endpoint does not support codec %s", device->path, pa_bluetooth_profile_to_string(profile), a2dp_codec->name); + return false; + } + + pa_assert(endpoints = pa_hashmap_get(is_a2dp_sink ? device->a2dp_sink_endpoints : device->a2dp_source_endpoints, &a2dp_codec->id)); + pa_assert(capabilities = pa_hashmap_get(endpoints, endpoint)); + + config_size = a2dp_codec->fill_preferred_configuration(&device->discovery->core->default_sample_spec, capabilities->buffer, capabilities->size, config); + if (config_size == 0) + return false; + + pa_endpoint = pa_sprintf_malloc("%s/%s", is_a2dp_sink ? A2DP_SOURCE_ENDPOINT : A2DP_SINK_ENDPOINT, a2dp_codec->name); + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, endpoint, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration")); + + dbus_message_iter_init_append(m, &iter); + pa_assert_se(dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &pa_endpoint)); + dbus_message_iter_open_container(&iter, 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, &dict); + pa_dbus_append_basic_array_variant_dict_entry(&dict, "Capabilities", DBUS_TYPE_BYTE, &config, config_size); + dbus_message_iter_close_container(&iter, &dict); + + device->change_a2dp_profile_in_progress = true; + + data = pa_xnew0(struct change_a2dp_profile_data, 1); + data->pa_endpoint = pa_endpoint; + data->device = device; + data->cb = cb; + data->userdata = userdata; + send_and_add_to_pending(device->discovery, m, change_a2dp_profile_reply, data); + return true; +} + +unsigned pa_bluetooth_profile_count(void) { + return PA_BLUETOOTH_PROFILE_A2DP_START_INDEX + 2 * pa_bluetooth_a2dp_codec_count(); +} + +bool pa_bluetooth_profile_is_a2dp_source(pa_bluetooth_profile_t profile) { + unsigned source_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX; + unsigned sink_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX + pa_bluetooth_a2dp_codec_count(); + + pa_assert(profile < pa_bluetooth_profile_count()); + + return profile >= source_start_index && profile < sink_start_index; +} + +bool pa_bluetooth_profile_is_a2dp_sink(pa_bluetooth_profile_t profile) { + unsigned sink_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX + pa_bluetooth_a2dp_codec_count(); + + pa_assert(profile < pa_bluetooth_profile_count()); + + return profile >= sink_start_index; +} + +const pa_a2dp_codec *pa_bluetooth_profile_to_a2dp_codec(pa_bluetooth_profile_t profile) { + unsigned source_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX; + unsigned sink_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX + pa_bluetooth_a2dp_codec_count(); + + pa_assert(profile >= source_start_index && profile < pa_bluetooth_profile_count()); + + if (profile < sink_start_index) + return pa_bluetooth_a2dp_codec_iter(profile - source_start_index); + else + return pa_bluetooth_a2dp_codec_iter(profile - sink_start_index); +} + +pa_bluetooth_profile_t pa_bluetooth_profile_for_a2dp_codec(const char *codec_name, bool is_a2dp_sink) { + unsigned source_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX; + unsigned sink_start_index = PA_BLUETOOTH_PROFILE_A2DP_START_INDEX + pa_bluetooth_a2dp_codec_count(); + unsigned count = pa_bluetooth_a2dp_codec_count(); + const pa_a2dp_codec *a2dp_codec; + unsigned i; + + for (i = 0; i < count; i++) { + a2dp_codec = pa_bluetooth_a2dp_codec_iter(i); + if (pa_streq(a2dp_codec->name, codec_name)) + return i + (is_a2dp_sink ? sink_start_index : source_start_index); + } + + return PA_BLUETOOTH_PROFILE_OFF; +} + const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile) { + static __thread char profile_string[128]; + const pa_a2dp_codec *a2dp_codec; + bool is_a2dp_sink; + switch(profile) { - case PA_BLUETOOTH_PROFILE_A2DP_SINK: - return "a2dp_sink"; - case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: - return "a2dp_source"; case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: return "headset_head_unit"; case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: return "headset_audio_gateway"; case PA_BLUETOOTH_PROFILE_OFF: return "off"; + default: + break; } - return NULL; + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(profile); + is_a2dp_sink = pa_bluetooth_profile_is_a2dp_sink(profile); + + /* backward compatible profile names for SBC codec */ + if (pa_streq(a2dp_codec->name, "sbc")) + return is_a2dp_sink ? "a2dp_sink" : "a2dp_source"; + + pa_snprintf(profile_string, sizeof(profile_string), "a2dp_%s_%s", is_a2dp_sink ? "sink" : "source", a2dp_codec->name); + return profile_string; +} + +static pa_bluetooth_profile_t a2dp_endpoint_to_bluetooth_profile(const char *endpoint) { + const char *codec_name; + bool is_a2dp_sink; + + if (pa_startswith(endpoint, A2DP_SINK_ENDPOINT "/")) { + codec_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/"); + is_a2dp_sink = false; + } else if (pa_startswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) { + codec_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/"); + is_a2dp_sink = true; + } else { + return PA_BLUETOOTH_PROFILE_OFF; + } + + return pa_bluetooth_profile_for_a2dp_codec(codec_name, is_a2dp_sink); } static const pa_a2dp_codec *a2dp_endpoint_to_a2dp_codec(const char *endpoint) { @@ -1349,13 +1771,9 @@ dbus_message_iter_get_basic(&value, &uuid); - if (pa_startswith(endpoint_path, A2DP_SINK_ENDPOINT "/")) - p = PA_BLUETOOTH_PROFILE_A2DP_SOURCE; - else if (pa_startswith(endpoint_path, A2DP_SOURCE_ENDPOINT "/")) - p = PA_BLUETOOTH_PROFILE_A2DP_SINK; - - if ((pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE) && p != PA_BLUETOOTH_PROFILE_A2DP_SINK) || - (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK) && p != PA_BLUETOOTH_PROFILE_A2DP_SOURCE)) { + p = a2dp_endpoint_to_bluetooth_profile(endpoint_path); + if ((pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE) && !pa_bluetooth_profile_is_a2dp_sink(p)) || + (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK) && !pa_bluetooth_profile_is_a2dp_source(p))) { pa_log_error("UUID %s of transport %s incompatible with endpoint %s", uuid, path, endpoint_path); goto fail; } @@ -1419,7 +1837,6 @@ dbus_message_unref(r); t = pa_bluetooth_transport_new(d, sender, path, p, config, size); - t->a2dp_codec = a2dp_codec; t->acquire = bluez5_transport_acquire_cb; t->release = bluez5_transport_release_cb; pa_bluetooth_transport_put(t); @@ -1639,6 +2056,8 @@ "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" ",arg0='" BLUEZ_DEVICE_INTERFACE "'", "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_MEDIA_ENDPOINT_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" ",arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", NULL) < 0) { pa_log_error("Failed to add D-Bus matches: %s", err.message); @@ -1723,6 +2142,8 @@ "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," "member='PropertiesChanged',arg0='" BLUEZ_DEVICE_INTERFACE "'", "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_MEDIA_ENDPOINT_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," "member='PropertiesChanged',arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", NULL); diff -rNaud pulseaudio-13.0/src/modules/bluetooth/bluez5-util.c.orig pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.c.orig --- pulseaudio-13.0/src/modules/bluetooth/bluez5-util.c.orig 1970-01-01 01:00:00.000000000 +0100 +++ pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.c.orig 2019-09-13 15:20:03.000000000 +0200 @@ -0,0 +1,1750 @@ +/*** + This file is part of PulseAudio. + + Copyright 2008-2013 João Paulo Rechi Vita + Copyrigth 2018-2019 Pali Rohár + + 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 +#include + +#include "a2dp-codec-util.h" +#include "a2dp-codecs.h" + +#include "bluez5-util.h" + +#define WAIT_FOR_PROFILES_TIMEOUT_USEC (3 * PA_USEC_PER_SEC) + +#define BLUEZ_SERVICE "org.bluez" +#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1" +#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1" +#define BLUEZ_MEDIA_INTERFACE BLUEZ_SERVICE ".Media1" +#define BLUEZ_MEDIA_ENDPOINT_INTERFACE BLUEZ_SERVICE ".MediaEndpoint1" +#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1" + +#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported" + +#define A2DP_SOURCE_ENDPOINT "/MediaEndpoint/A2DPSource" +#define A2DP_SINK_ENDPOINT "/MediaEndpoint/A2DPSink" + +#define ENDPOINT_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + "" + +struct pa_bluetooth_discovery { + PA_REFCNT_DECLARE; + + pa_core *core; + pa_dbus_connection *connection; + bool filter_added; + bool matches_added; + bool objects_listed; + pa_hook hooks[PA_BLUETOOTH_HOOK_MAX]; + pa_hashmap *adapters; + pa_hashmap *devices; + pa_hashmap *transports; + + int headset_backend; + pa_bluetooth_backend *ofono_backend, *native_backend; + PA_LLIST_HEAD(pa_dbus_pending, pending); +}; + +static pa_dbus_pending* send_and_add_to_pending(pa_bluetooth_discovery *y, DBusMessage *m, + DBusPendingCallNotifyFunction func, void *call_data) { + pa_dbus_pending *p; + DBusPendingCall *call; + + pa_assert(y); + pa_assert(m); + + pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(y->connection), m, &call, -1)); + + p = pa_dbus_pending_new(pa_dbus_connection_get(y->connection), m, call, y, call_data); + PA_LLIST_PREPEND(pa_dbus_pending, y->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; +} + +pa_bluetooth_transport *pa_bluetooth_transport_new(pa_bluetooth_device *d, const char *owner, const char *path, + pa_bluetooth_profile_t p, const uint8_t *config, size_t size) { + pa_bluetooth_transport *t; + + t = pa_xnew0(pa_bluetooth_transport, 1); + t->device = d; + t->owner = pa_xstrdup(owner); + t->path = pa_xstrdup(path); + t->profile = p; + t->config_size = size; + + if (size > 0) { + t->config = pa_xnew(uint8_t, size); + memcpy(t->config, config, size); + } + + return t; +} + +static const char *transport_state_to_string(pa_bluetooth_transport_state_t state) { + switch(state) { + case PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED: + return "disconnected"; + case PA_BLUETOOTH_TRANSPORT_STATE_IDLE: + return "idle"; + case PA_BLUETOOTH_TRANSPORT_STATE_PLAYING: + return "playing"; + } + + return "invalid"; +} + +static bool device_supports_profile(pa_bluetooth_device *device, pa_bluetooth_profile_t profile) { + switch (profile) { + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SINK); + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SOURCE); + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS) + || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS_ALT) + || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HFP_HF); + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_AG) + || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HFP_AG); + case PA_BLUETOOTH_PROFILE_OFF: + pa_assert_not_reached(); + } + + pa_assert_not_reached(); +} + +static bool device_is_profile_connected(pa_bluetooth_device *device, pa_bluetooth_profile_t profile) { + if (device->transports[profile] && device->transports[profile]->state != PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) + return true; + else + return false; +} + +static unsigned device_count_disconnected_profiles(pa_bluetooth_device *device) { + pa_bluetooth_profile_t profile; + unsigned count = 0; + + for (profile = 0; profile < PA_BLUETOOTH_PROFILE_COUNT; profile++) { + if (!device_supports_profile(device, profile)) + continue; + + if (!device_is_profile_connected(device, profile)) + count++; + } + + return count; +} + +static void device_stop_waiting_for_profiles(pa_bluetooth_device *device) { + if (!device->wait_for_profiles_timer) + return; + + device->discovery->core->mainloop->time_free(device->wait_for_profiles_timer); + device->wait_for_profiles_timer = NULL; +} + +static void wait_for_profiles_cb(pa_mainloop_api *api, pa_time_event* event, const struct timeval *tv, void *userdata) { + pa_bluetooth_device *device = userdata; + pa_strbuf *buf; + pa_bluetooth_profile_t profile; + bool first = true; + char *profiles_str; + + device_stop_waiting_for_profiles(device); + + buf = pa_strbuf_new(); + + for (profile = 0; profile < PA_BLUETOOTH_PROFILE_COUNT; profile++) { + if (device_is_profile_connected(device, profile)) + continue; + + if (!device_supports_profile(device, profile)) + continue; + + if (first) + first = false; + else + pa_strbuf_puts(buf, ", "); + + pa_strbuf_puts(buf, pa_bluetooth_profile_to_string(profile)); + } + + profiles_str = pa_strbuf_to_string_free(buf); + pa_log_debug("Timeout expired, and device %s still has disconnected profiles: %s", + device->path, profiles_str); + pa_xfree(profiles_str); + pa_hook_fire(&device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], device); +} + +static void device_start_waiting_for_profiles(pa_bluetooth_device *device) { + pa_assert(!device->wait_for_profiles_timer); + device->wait_for_profiles_timer = pa_core_rttime_new(device->discovery->core, + pa_rtclock_now() + WAIT_FOR_PROFILES_TIMEOUT_USEC, + wait_for_profiles_cb, device); +} + +void pa_bluetooth_transport_set_state(pa_bluetooth_transport *t, pa_bluetooth_transport_state_t state) { + bool old_any_connected; + unsigned n_disconnected_profiles; + bool new_device_appeared; + bool device_disconnected; + + pa_assert(t); + + if (t->state == state) + return; + + old_any_connected = pa_bluetooth_device_any_transport_connected(t->device); + + pa_log_debug("Transport %s state: %s -> %s", + t->path, transport_state_to_string(t->state), transport_state_to_string(state)); + + t->state = state; + + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED], t); + + /* If there are profiles that are expected to get connected soon (based + * on the UUID list), we wait for a bit before announcing the new + * device, so that all profiles have time to get connected before the + * card object is created. If we didn't wait, the card would always + * have only one profile marked as available in the initial state, + * which would prevent module-card-restore from restoring the initial + * profile properly. */ + + n_disconnected_profiles = device_count_disconnected_profiles(t->device); + + new_device_appeared = !old_any_connected && pa_bluetooth_device_any_transport_connected(t->device); + device_disconnected = old_any_connected && !pa_bluetooth_device_any_transport_connected(t->device); + + if (new_device_appeared) { + if (n_disconnected_profiles > 0) + device_start_waiting_for_profiles(t->device); + else + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], t->device); + return; + } + + if (device_disconnected) { + if (t->device->wait_for_profiles_timer) { + /* If the timer is still running when the device disconnects, we + * never sent the notification of the device getting connected, so + * we don't need to send a notification about the disconnection + * either. Let's just stop the timer. */ + device_stop_waiting_for_profiles(t->device); + } else + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], t->device); + return; + } + + if (n_disconnected_profiles == 0 && t->device->wait_for_profiles_timer) { + /* All profiles are now connected, so we can stop the wait timer and + * send a notification of the new device. */ + device_stop_waiting_for_profiles(t->device); + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], t->device); + } +} + +void pa_bluetooth_transport_put(pa_bluetooth_transport *t) { + pa_assert(t); + + t->device->transports[t->profile] = t; + pa_assert_se(pa_hashmap_put(t->device->discovery->transports, t->path, t) >= 0); + pa_bluetooth_transport_set_state(t, PA_BLUETOOTH_TRANSPORT_STATE_IDLE); +} + +void pa_bluetooth_transport_unlink(pa_bluetooth_transport *t) { + pa_assert(t); + + pa_bluetooth_transport_set_state(t, PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED); + pa_hashmap_remove(t->device->discovery->transports, t->path); + t->device->transports[t->profile] = NULL; +} + +void pa_bluetooth_transport_free(pa_bluetooth_transport *t) { + pa_assert(t); + + if (t->destroy) + t->destroy(t); + pa_bluetooth_transport_unlink(t); + + pa_xfree(t->owner); + pa_xfree(t->path); + pa_xfree(t->config); + pa_xfree(t); +} + +static int bluez5_transport_acquire_cb(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) { + DBusMessage *m, *r; + DBusError err; + int ret; + uint16_t i, o; + const char *method = optional ? "TryAcquire" : "Acquire"; + + pa_assert(t); + pa_assert(t->device); + pa_assert(t->device->discovery); + + pa_assert_se(m = dbus_message_new_method_call(t->owner, t->path, BLUEZ_MEDIA_TRANSPORT_INTERFACE, method)); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(t->device->discovery->connection), m, -1, &err); + dbus_message_unref(m); + m = NULL; + if (!r) { + if (optional && pa_streq(err.name, "org.bluez.Error.NotAvailable")) + pa_log_info("Failed optional acquire of unavailable transport %s", t->path); + else + pa_log_error("Transport %s() failed for transport %s (%s)", method, t->path, err.message); + + dbus_error_free(&err); + return -1; + } + + if (!dbus_message_get_args(r, &err, DBUS_TYPE_UNIX_FD, &ret, DBUS_TYPE_UINT16, &i, DBUS_TYPE_UINT16, &o, + DBUS_TYPE_INVALID)) { + pa_log_error("Failed to parse %s() reply: %s", method, err.message); + dbus_error_free(&err); + ret = -1; + goto finish; + } + + if (imtu) + *imtu = i; + + if (omtu) + *omtu = o; + +finish: + dbus_message_unref(r); + return ret; +} + +static void bluez5_transport_release_cb(pa_bluetooth_transport *t) { + DBusMessage *m, *r; + DBusError err; + + pa_assert(t); + pa_assert(t->device); + pa_assert(t->device->discovery); + + dbus_error_init(&err); + + if (t->state <= PA_BLUETOOTH_TRANSPORT_STATE_IDLE) { + pa_log_info("Transport %s auto-released by BlueZ or already released", t->path); + return; + } + + pa_assert_se(m = dbus_message_new_method_call(t->owner, t->path, BLUEZ_MEDIA_TRANSPORT_INTERFACE, "Release")); + r = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(t->device->discovery->connection), m, -1, &err); + dbus_message_unref(m); + m = NULL; + if (r) { + dbus_message_unref(r); + r = NULL; + } + + if (dbus_error_is_set(&err)) { + pa_log_error("Failed to release transport %s: %s", t->path, err.message); + dbus_error_free(&err); + } else + pa_log_info("Transport %s released", t->path); +} + +bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d) { + unsigned i; + + pa_assert(d); + + if (!d->valid) + return false; + + for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) + if (d->transports[i] && d->transports[i]->state != PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) + return true; + + return false; +} + +static int transport_state_from_string(const char* value, pa_bluetooth_transport_state_t *state) { + pa_assert(value); + pa_assert(state); + + if (pa_streq(value, "idle")) + *state = PA_BLUETOOTH_TRANSPORT_STATE_IDLE; + else if (pa_streq(value, "pending") || pa_streq(value, "active")) + *state = PA_BLUETOOTH_TRANSPORT_STATE_PLAYING; + else + return -1; + + return 0; +} + +static void parse_transport_property(pa_bluetooth_transport *t, DBusMessageIter *i) { + const char *key; + DBusMessageIter variant_i; + + key = check_variant_property(i); + if (key == NULL) + return; + + dbus_message_iter_recurse(i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + + case DBUS_TYPE_STRING: { + + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "State")) { + pa_bluetooth_transport_state_t state; + + if (transport_state_from_string(value, &state) < 0) { + pa_log_error("Invalid state received: %s", value); + return; + } + + pa_bluetooth_transport_set_state(t, state); + } + + break; + } + } + + return; +} + +static int parse_transport_properties(pa_bluetooth_transport *t, DBusMessageIter *i) { + DBusMessageIter element_i; + + dbus_message_iter_recurse(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_transport_property(t, &dict_i); + + dbus_message_iter_next(&element_i); + } + + return 0; +} + +static pa_bluetooth_device* device_create(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *d; + + pa_assert(y); + pa_assert(path); + + d = pa_xnew0(pa_bluetooth_device, 1); + d->discovery = y; + d->path = pa_xstrdup(path); + d->uuids = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, pa_xfree); + + pa_hashmap_put(y->devices, d->path, d); + + return d; +} + +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_path(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *d; + + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + pa_assert(path); + + if ((d = pa_hashmap_get(y->devices, path)) && d->valid) + return d; + + return NULL; +} + +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_address(pa_bluetooth_discovery *y, const char *remote, const char *local) { + pa_bluetooth_device *d; + void *state = NULL; + + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + pa_assert(remote); + pa_assert(local); + + while ((d = pa_hashmap_iterate(y->devices, &state, NULL))) + if (d->valid && pa_streq(d->address, remote) && pa_streq(d->adapter->address, local)) + return d; + + return NULL; +} + +static void device_free(pa_bluetooth_device *d) { + unsigned i; + + pa_assert(d); + + device_stop_waiting_for_profiles(d); + + pa_hook_fire(&d->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_UNLINK], d); + + for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) { + pa_bluetooth_transport *t; + + if (!(t = d->transports[i])) + continue; + + pa_bluetooth_transport_free(t); + } + + if (d->uuids) + pa_hashmap_free(d->uuids); + + pa_xfree(d->path); + pa_xfree(d->alias); + pa_xfree(d->address); + pa_xfree(d->adapter_path); + pa_xfree(d); +} + +static void device_remove(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *d; + + if (!(d = pa_hashmap_remove(y->devices, path))) + pa_log_warn("Unknown device removed %s", path); + else { + pa_log_debug("Device %s removed", path); + device_free(d); + } +} + +static void device_set_valid(pa_bluetooth_device *device, bool valid) { + bool old_any_connected; + + pa_assert(device); + + if (valid == device->valid) + return; + + old_any_connected = pa_bluetooth_device_any_transport_connected(device); + device->valid = valid; + + if (pa_bluetooth_device_any_transport_connected(device) != old_any_connected) + pa_hook_fire(&device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], device); +} + +static void device_update_valid(pa_bluetooth_device *d) { + pa_assert(d); + + if (!d->properties_received) { + pa_assert(!d->valid); + return; + } + + /* Check if mandatory properties are set. */ + if (!d->address || !d->adapter_path || !d->alias) { + device_set_valid(d, false); + return; + } + + if (!d->adapter || !d->adapter->valid) { + device_set_valid(d, false); + return; + } + + device_set_valid(d, true); +} + +static void device_set_adapter(pa_bluetooth_device *device, pa_bluetooth_adapter *adapter) { + pa_assert(device); + + if (adapter == device->adapter) + return; + + device->adapter = adapter; + + device_update_valid(device); +} + +static pa_bluetooth_adapter* adapter_create(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_adapter *a; + + pa_assert(y); + pa_assert(path); + + a = pa_xnew0(pa_bluetooth_adapter, 1); + a->discovery = y; + a->path = pa_xstrdup(path); + + pa_hashmap_put(y->adapters, a->path, a); + + return a; +} + +static void adapter_free(pa_bluetooth_adapter *a) { + pa_bluetooth_device *d; + void *state; + + pa_assert(a); + pa_assert(a->discovery); + + PA_HASHMAP_FOREACH(d, a->discovery->devices, state) + if (d->adapter == a) + device_set_adapter(d, NULL); + + pa_xfree(a->path); + pa_xfree(a->address); + pa_xfree(a); +} + +static void adapter_remove(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_adapter *a; + + if (!(a = pa_hashmap_remove(y->adapters, path))) + pa_log_warn("Unknown adapter removed %s", path); + else { + pa_log_debug("Adapter %s removed", path); + adapter_free(a); + } +} + +static void parse_device_property(pa_bluetooth_device *d, DBusMessageIter *i) { + const char *key; + DBusMessageIter variant_i; + + pa_assert(d); + + key = check_variant_property(i); + if (key == NULL) { + pa_log_error("Received invalid property for device %s", d->path); + return; + } + + dbus_message_iter_recurse(i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + + case DBUS_TYPE_STRING: { + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "Alias")) { + pa_xfree(d->alias); + d->alias = pa_xstrdup(value); + pa_log_debug("%s: %s", key, value); + } else if (pa_streq(key, "Address")) { + if (d->properties_received) { + pa_log_warn("Device property 'Address' expected to be constant but changed for %s, ignoring", d->path); + return; + } + + if (d->address) { + pa_log_warn("Device %s: Received a duplicate 'Address' property, ignoring", d->path); + return; + } + + d->address = pa_xstrdup(value); + pa_log_debug("%s: %s", key, value); + } + + break; + } + + case DBUS_TYPE_OBJECT_PATH: { + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "Adapter")) { + + if (d->properties_received) { + pa_log_warn("Device property 'Adapter' expected to be constant but changed for %s, ignoring", d->path); + return; + } + + if (d->adapter_path) { + pa_log_warn("Device %s: Received a duplicate 'Adapter' property, ignoring", d->path); + return; + } + + d->adapter_path = pa_xstrdup(value); + pa_log_debug("%s: %s", key, value); + } + + break; + } + + case DBUS_TYPE_UINT32: { + uint32_t value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "Class")) { + d->class_of_device = value; + pa_log_debug("%s: %d", key, value); + } + + break; + } + + case DBUS_TYPE_ARRAY: { + DBusMessageIter ai; + dbus_message_iter_recurse(&variant_i, &ai); + + if (dbus_message_iter_get_arg_type(&ai) == DBUS_TYPE_STRING && pa_streq(key, "UUIDs")) { + /* bluetoothd never removes UUIDs from a device object so we + * don't need to check for disappeared UUIDs here. */ + while (dbus_message_iter_get_arg_type(&ai) != DBUS_TYPE_INVALID) { + const char *value; + char *uuid; + + dbus_message_iter_get_basic(&ai, &value); + + if (pa_hashmap_get(d->uuids, value)) { + dbus_message_iter_next(&ai); + continue; + } + + uuid = pa_xstrdup(value); + pa_hashmap_put(d->uuids, uuid, uuid); + + pa_log_debug("%s: %s", key, value); + dbus_message_iter_next(&ai); + } + } + + break; + } + } +} + +static void parse_device_properties(pa_bluetooth_device *d, DBusMessageIter *i) { + DBusMessageIter element_i; + + dbus_message_iter_recurse(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_device_property(d, &dict_i); + dbus_message_iter_next(&element_i); + } + + if (!d->properties_received) { + d->properties_received = true; + device_update_valid(d); + + if (!d->address || !d->adapter_path || !d->alias) + pa_log_error("Non-optional information missing for device %s", d->path); + } +} + +static void parse_adapter_properties(pa_bluetooth_adapter *a, DBusMessageIter *i, bool is_property_change) { + DBusMessageIter element_i; + + pa_assert(a); + + dbus_message_iter_recurse(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); + + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property for adapter %s", a->path); + return; + } + + dbus_message_iter_recurse(&dict_i, &variant_i); + + if (dbus_message_iter_get_arg_type(&variant_i) == DBUS_TYPE_STRING && pa_streq(key, "Address")) { + const char *value; + + if (is_property_change) { + pa_log_warn("Adapter property 'Address' expected to be constant but changed for %s, ignoring", a->path); + return; + } + + if (a->address) { + pa_log_warn("Adapter %s received a duplicate 'Address' property, ignoring", a->path); + return; + } + + dbus_message_iter_get_basic(&variant_i, &value); + a->address = pa_xstrdup(value); + a->valid = true; + } + + dbus_message_iter_next(&element_i); + } +} + +static void register_endpoint_reply(DBusPendingCall *pending, void *userdata) { + DBusMessage *r; + pa_dbus_pending *p; + pa_bluetooth_discovery *y; + char *endpoint; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(y = p->context_data); + pa_assert_se(endpoint = p->call_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + pa_log_info("Couldn't register endpoint %s because it is disabled in BlueZ", endpoint); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error(BLUEZ_MEDIA_INTERFACE ".RegisterEndpoint() failed: %s: %s", dbus_message_get_error_name(r), + pa_dbus_get_error_message(r)); + goto finish; + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, y->pending, p); + pa_dbus_pending_free(p); + + pa_xfree(endpoint); +} + +static void register_endpoint(pa_bluetooth_discovery *y, const pa_a2dp_codec *a2dp_codec, const char *path, const char *endpoint, const char *uuid) { + DBusMessage *m; + DBusMessageIter i, d; + uint8_t capabilities[MAX_A2DP_CAPS_SIZE]; + size_t capabilities_size; + uint8_t codec_id; + + pa_log_debug("Registering %s on adapter %s", endpoint, path); + + codec_id = a2dp_codec->id.codec_id; + capabilities_size = a2dp_codec->fill_capabilities(capabilities); + pa_assert(capabilities_size != 0); + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, path, BLUEZ_MEDIA_INTERFACE, "RegisterEndpoint")); + + dbus_message_iter_init_append(m, &i); + pa_assert_se(dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &endpoint)); + 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); + pa_dbus_append_basic_variant_dict_entry(&d, "UUID", DBUS_TYPE_STRING, &uuid); + pa_dbus_append_basic_variant_dict_entry(&d, "Codec", DBUS_TYPE_BYTE, &codec_id); + pa_dbus_append_basic_array_variant_dict_entry(&d, "Capabilities", DBUS_TYPE_BYTE, &capabilities, capabilities_size); + + dbus_message_iter_close_container(&i, &d); + + send_and_add_to_pending(y, m, register_endpoint_reply, pa_xstrdup(endpoint)); +} + +static void parse_interfaces_and_properties(pa_bluetooth_discovery *y, DBusMessageIter *dict_i) { + DBusMessageIter element_i; + const char *path; + void *state; + pa_bluetooth_device *d; + + 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 iface_i; + const char *interface; + + dbus_message_iter_recurse(&element_i, &iface_i); + + pa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_STRING); + dbus_message_iter_get_basic(&iface_i, &interface); + + pa_assert_se(dbus_message_iter_next(&iface_i)); + pa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + if (pa_streq(interface, BLUEZ_ADAPTER_INTERFACE)) { + pa_bluetooth_adapter *a; + unsigned a2dp_codec_i; + + if ((a = pa_hashmap_get(y->adapters, path))) { + pa_log_error("Found duplicated D-Bus path for adapter %s", path); + return; + } else + a = adapter_create(y, path); + + pa_log_debug("Adapter %s found", path); + + parse_adapter_properties(a, &iface_i, false); + + if (!a->valid) + return; + + /* Order is important. bluez prefers endpoints registered earlier. + * And codec with higher number has higher priority. So iterate in reverse order. */ + for (a2dp_codec_i = pa_bluetooth_a2dp_codec_count(); a2dp_codec_i > 0; a2dp_codec_i--) { + const pa_a2dp_codec *a2dp_codec = pa_bluetooth_a2dp_codec_iter(a2dp_codec_i-1); + char *endpoint; + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SINK_ENDPOINT, a2dp_codec->name); + register_endpoint(y, a2dp_codec, path, endpoint, PA_BLUETOOTH_UUID_A2DP_SINK); + pa_xfree(endpoint); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SOURCE_ENDPOINT, a2dp_codec->name); + register_endpoint(y, a2dp_codec, path, endpoint, PA_BLUETOOTH_UUID_A2DP_SOURCE); + pa_xfree(endpoint); + } + + } else if (pa_streq(interface, BLUEZ_DEVICE_INTERFACE)) { + + if ((d = pa_hashmap_get(y->devices, path))) { + if (d->properties_received) { + pa_log_error("Found duplicated D-Bus path for device %s", path); + return; + } + } else + d = device_create(y, path); + + pa_log_debug("Device %s found", d->path); + + parse_device_properties(d, &iface_i); + + } else + pa_log_debug("Unknown interface %s found, skipping", interface); + + dbus_message_iter_next(&element_i); + } + + PA_HASHMAP_FOREACH(d, y->devices, state) { + if (d->properties_received && !d->tried_to_link_with_adapter) { + if (d->adapter_path) { + device_set_adapter(d, pa_hashmap_get(d->discovery->adapters, d->adapter_path)); + + if (!d->adapter) + pa_log("Device %s points to a nonexistent adapter %s.", d->path, d->adapter_path); + else if (!d->adapter->valid) + pa_log("Device %s points to an invalid adapter %s.", d->path, d->adapter_path); + } + + d->tried_to_link_with_adapter = true; + } + } + + return; +} + +void pa_bluetooth_discovery_set_ofono_running(pa_bluetooth_discovery *y, bool is_running) { + pa_assert(y); + + pa_log_debug("oFono is running: %s", pa_yes_no(is_running)); + if (y->headset_backend != HEADSET_BACKEND_AUTO) + return; + + /* If ofono starts running, all devices that might be connected to the HS role + * need to be disconnected, so that the devices can be handled by ofono */ + if (is_running) { + void *state; + pa_bluetooth_device *d; + + PA_HASHMAP_FOREACH(d, y->devices, state) { + if (device_supports_profile(d, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY)) { + DBusMessage *m; + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, d->path, "org.bluez.Device1", "Disconnect")); + dbus_message_set_no_reply(m, true); + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(y->connection), m, NULL)); + dbus_message_unref(m); + } + } + } + + pa_bluetooth_native_backend_enable_hs_role(y->native_backend, !is_running); +} + +static void get_managed_objects_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_bluetooth_discovery *y; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + + pa_assert_se(p = userdata); + pa_assert_se(y = 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("BlueZ 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(y, &dict_i); + + dbus_message_iter_next(&element_i); + } + + y->objects_listed = true; + + if (!y->native_backend && y->headset_backend != HEADSET_BACKEND_OFONO) + y->native_backend = pa_bluetooth_native_backend_new(y->core, y, (y->headset_backend == HEADSET_BACKEND_NATIVE)); + if (!y->ofono_backend && y->headset_backend != HEADSET_BACKEND_NATIVE) + y->ofono_backend = pa_bluetooth_ofono_backend_new(y->core, y); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, y->pending, p); + pa_dbus_pending_free(p); +} + +static void get_managed_objects(pa_bluetooth_discovery *y) { + DBusMessage *m; + + pa_assert(y); + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, "/", "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects")); + send_and_add_to_pending(y, m, get_managed_objects_reply, NULL); +} + +pa_hook* pa_bluetooth_discovery_hook(pa_bluetooth_discovery *y, pa_bluetooth_hook_t hook) { + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + + return &y->hooks[hook]; +} + +static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y; + DBusError err; + + pa_assert(bus); + pa_assert(m); + pa_assert_se(y = userdata); + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "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 org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto fail; + } + + if (pa_streq(name, BLUEZ_SERVICE)) { + if (old_owner && *old_owner) { + pa_log_debug("Bluetooth daemon disappeared"); + pa_hashmap_remove_all(y->devices); + pa_hashmap_remove_all(y->adapters); + y->objects_listed = false; + if (y->ofono_backend) { + pa_bluetooth_ofono_backend_free(y->ofono_backend); + y->ofono_backend = NULL; + } + if (y->native_backend) { + pa_bluetooth_native_backend_free(y->native_backend); + y->native_backend = NULL; + } + } + + if (new_owner && *new_owner) { + pa_log_debug("Bluetooth daemon appeared"); + get_managed_objects(y); + } + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded")) { + DBusMessageIter arg_i; + + if (!y->objects_listed) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; /* No reply received yet from GetManagedObjects */ + + 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(y, &arg_i); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesRemoved")) { + const char *p; + DBusMessageIter arg_i; + DBusMessageIter element_i; + + if (!y->objects_listed) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; /* No reply received yet from GetManagedObjects */ + + 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); + + if (pa_streq(iface, BLUEZ_DEVICE_INTERFACE)) + device_remove(y, p); + else if (pa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) + adapter_remove(y, p); + + dbus_message_iter_next(&element_i); + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.Properties", "PropertiesChanged")) { + DBusMessageIter arg_i; + const char *iface; + + if (!y->objects_listed) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; /* No reply received yet from GetManagedObjects */ + + 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; + } + + dbus_message_iter_get_basic(&arg_i, &iface); + + pa_assert_se(dbus_message_iter_next(&arg_i)); + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + if (pa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) { + pa_bluetooth_adapter *a; + + pa_log_debug("Properties changed in adapter %s", dbus_message_get_path(m)); + + if (!(a = pa_hashmap_get(y->adapters, dbus_message_get_path(m)))) { + pa_log_warn("Properties changed in unknown adapter"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + parse_adapter_properties(a, &arg_i, true); + + } else if (pa_streq(iface, BLUEZ_DEVICE_INTERFACE)) { + pa_bluetooth_device *d; + + pa_log_debug("Properties changed in device %s", dbus_message_get_path(m)); + + if (!(d = pa_hashmap_get(y->devices, dbus_message_get_path(m)))) { + pa_log_warn("Properties changed in unknown device"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (!d->properties_received) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + parse_device_properties(d, &arg_i); + } else if (pa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) { + pa_bluetooth_transport *t; + + pa_log_debug("Properties changed in transport %s", dbus_message_get_path(m)); + + if (!(t = pa_hashmap_get(y->transports, dbus_message_get_path(m)))) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + parse_transport_properties(t, &arg_i); + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + +fail: + dbus_error_free(&err); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile) { + switch(profile) { + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + return "a2dp_sink"; + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + return "a2dp_source"; + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + return "headset_head_unit"; + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + return "headset_audio_gateway"; + case PA_BLUETOOTH_PROFILE_OFF: + return "off"; + } + + return NULL; +} + +static const pa_a2dp_codec *a2dp_endpoint_to_a2dp_codec(const char *endpoint) { + const char *codec_name; + + if (pa_startswith(endpoint, A2DP_SINK_ENDPOINT "/")) + codec_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/"); + else if (pa_startswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) + codec_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/"); + else + return NULL; + + return pa_bluetooth_get_a2dp_codec(codec_name); +} + +static DBusMessage *endpoint_set_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y = userdata; + pa_bluetooth_device *d; + pa_bluetooth_transport *t; + const pa_a2dp_codec *a2dp_codec = NULL; + const char *sender, *path, *endpoint_path, *dev_path = NULL, *uuid = NULL; + const uint8_t *config = NULL; + int size = 0; + pa_bluetooth_profile_t p = PA_BLUETOOTH_PROFILE_OFF; + DBusMessageIter args, props; + DBusMessage *r; + + if (!dbus_message_iter_init(m, &args) || !pa_streq(dbus_message_get_signature(m), "oa{sv}")) { + pa_log_error("Invalid signature for method SetConfiguration()"); + goto fail2; + } + + dbus_message_iter_get_basic(&args, &path); + + if (pa_hashmap_get(y->transports, path)) { + pa_log_error("Endpoint SetConfiguration(): Transport %s is already configured.", path); + goto fail2; + } + + pa_assert_se(dbus_message_iter_next(&args)); + + dbus_message_iter_recurse(&args, &props); + if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY) + goto fail; + + endpoint_path = dbus_message_get_path(m); + + /* Read transport properties */ + while (dbus_message_iter_get_arg_type(&props) == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter value, entry; + int var; + + dbus_message_iter_recurse(&props, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + var = dbus_message_iter_get_arg_type(&value); + + if (pa_streq(key, "UUID")) { + if (var != DBUS_TYPE_STRING) { + pa_log_error("Property %s of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_get_basic(&value, &uuid); + + if (pa_startswith(endpoint_path, A2DP_SINK_ENDPOINT "/")) + p = PA_BLUETOOTH_PROFILE_A2DP_SOURCE; + else if (pa_startswith(endpoint_path, A2DP_SOURCE_ENDPOINT "/")) + p = PA_BLUETOOTH_PROFILE_A2DP_SINK; + + if ((pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE) && p != PA_BLUETOOTH_PROFILE_A2DP_SINK) || + (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK) && p != PA_BLUETOOTH_PROFILE_A2DP_SOURCE)) { + pa_log_error("UUID %s of transport %s incompatible with endpoint %s", uuid, path, endpoint_path); + goto fail; + } + } else if (pa_streq(key, "Device")) { + if (var != DBUS_TYPE_OBJECT_PATH) { + pa_log_error("Property %s of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_get_basic(&value, &dev_path); + } else if (pa_streq(key, "Configuration")) { + DBusMessageIter array; + + if (var != DBUS_TYPE_ARRAY) { + pa_log_error("Property %s of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_recurse(&value, &array); + var = dbus_message_iter_get_arg_type(&array); + if (var != DBUS_TYPE_BYTE) { + pa_log_error("%s is an array of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_get_fixed_array(&array, &config, &size); + + a2dp_codec = a2dp_endpoint_to_a2dp_codec(endpoint_path); + pa_assert(a2dp_codec); + + if (!a2dp_codec->is_configuration_valid(config, size)) + goto fail; + } + + dbus_message_iter_next(&props); + } + + if (!a2dp_codec) + goto fail2; + + if ((d = pa_hashmap_get(y->devices, dev_path))) { + if (!d->valid) { + pa_log_error("Information about device %s is invalid", dev_path); + goto fail2; + } + } else { + /* InterfacesAdded signal is probably on its way, device_info_valid is kept as 0. */ + pa_log_warn("SetConfiguration() received for unknown device %s", dev_path); + d = device_create(y, dev_path); + } + + if (d->transports[p] != NULL) { + pa_log_error("Cannot configure transport %s because profile %s is already used", path, pa_bluetooth_profile_to_string(p)); + goto fail2; + } + + sender = dbus_message_get_sender(m); + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(y->connection), r, NULL)); + dbus_message_unref(r); + + t = pa_bluetooth_transport_new(d, sender, path, p, config, size); + t->a2dp_codec = a2dp_codec; + t->acquire = bluez5_transport_acquire_cb; + t->release = bluez5_transport_release_cb; + pa_bluetooth_transport_put(t); + + pa_log_debug("Transport %s available for profile %s", t->path, pa_bluetooth_profile_to_string(t->profile)); + + return NULL; + +fail: + pa_log_error("Endpoint SetConfiguration(): invalid arguments"); + +fail2: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to set configuration")); + return r; +} + +static DBusMessage *endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y = userdata; + const char *endpoint_path; + uint8_t *cap; + int size; + const pa_a2dp_codec *a2dp_codec; + uint8_t config[MAX_A2DP_CAPS_SIZE]; + uint8_t *config_ptr = config; + size_t config_size; + DBusMessage *r; + DBusError err; + + endpoint_path = dbus_message_get_path(m); + + dbus_error_init(&err); + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &cap, &size, DBUS_TYPE_INVALID)) { + pa_log_error("Endpoint SelectConfiguration(): %s", err.message); + dbus_error_free(&err); + goto fail; + } + + a2dp_codec = a2dp_endpoint_to_a2dp_codec(endpoint_path); + pa_assert(a2dp_codec); + + config_size = a2dp_codec->fill_preferred_configuration(&y->core->default_sample_spec, cap, size, config); + if (config_size == 0) + goto fail; + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &config_ptr, config_size, DBUS_TYPE_INVALID)); + + return r; + +fail: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to select configuration")); + return r; +} + +static DBusMessage *endpoint_clear_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y = userdata; + pa_bluetooth_transport *t; + DBusMessage *r; + DBusError err; + const char *path; + + dbus_error_init(&err); + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) { + pa_log_error("Endpoint ClearConfiguration(): %s", err.message); + dbus_error_free(&err); + goto fail; + } + + if ((t = pa_hashmap_get(y->transports, path))) { + pa_log_debug("Clearing transport %s profile %s", t->path, pa_bluetooth_profile_to_string(t->profile)); + pa_bluetooth_transport_free(t); + } + + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; + +fail: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to clear configuration")); + return r; +} + +static DBusMessage *endpoint_release(DBusConnection *conn, DBusMessage *m, void *userdata) { + DBusMessage *r = NULL; + + /* From doc/media-api.txt in bluez: + * + * This method gets called when the service daemon + * unregisters the endpoint. An endpoint can use it to do + * cleanup tasks. There is no need to unregister the + * endpoint, because when this method gets called it has + * already been unregistered. + * + * We don't have any cleanup to do. */ + + /* Reply only if requested. Generally bluetoothd doesn't request a reply + * to the Release() call. Sending replies when not requested on the system + * bus tends to cause errors in syslog from dbus-daemon, because it + * doesn't let unexpected replies through, so it's important to have this + * check here. */ + if (!dbus_message_get_no_reply(m)) + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; +} + +static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata) { + struct pa_bluetooth_discovery *y = userdata; + DBusMessage *r = NULL; + const char *path, *interface, *member; + + pa_assert(y); + + 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); + + if (!a2dp_endpoint_to_a2dp_codec(path)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = ENDPOINT_INTROSPECT_XML; + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)); + + } else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration")) + r = endpoint_set_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectConfiguration")) + r = endpoint_select_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration")) + r = endpoint_clear_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Release")) + r = endpoint_release(c, m, userdata); + else + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (r) { + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(y->connection), r, NULL)); + dbus_message_unref(r); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void endpoint_init(pa_bluetooth_discovery *y, const char *endpoint) { + static const DBusObjectPathVTable vtable_endpoint = { + .message_function = endpoint_handler, + }; + + pa_assert(y); + pa_assert(endpoint); + + pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(y->connection), endpoint, + &vtable_endpoint, y)); +} + +static void endpoint_done(pa_bluetooth_discovery *y, const char *endpoint) { + pa_assert(y); + pa_assert(endpoint); + + dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), endpoint); +} + +pa_bluetooth_discovery* pa_bluetooth_discovery_get(pa_core *c, int headset_backend) { + pa_bluetooth_discovery *y; + DBusError err; + DBusConnection *conn; + unsigned i, count; + const pa_a2dp_codec *a2dp_codec; + char *endpoint; + + y = pa_xnew0(pa_bluetooth_discovery, 1); + PA_REFCNT_INIT(y); + y->core = c; + y->headset_backend = headset_backend; + y->adapters = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) adapter_free); + y->devices = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) device_free); + y->transports = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + PA_LLIST_HEAD_INIT(pa_dbus_pending, y->pending); + + for (i = 0; i < PA_BLUETOOTH_HOOK_MAX; i++) + pa_hook_init(&y->hooks[i], y); + + pa_shared_set(c, "bluetooth-discovery", y); + + dbus_error_init(&err); + + if (!(y->connection = pa_dbus_bus_get(y->core, DBUS_BUS_SYSTEM, &err))) { + pa_log_error("Failed to get D-Bus connection: %s", err.message); + goto fail; + } + + conn = pa_dbus_connection_get(y->connection); + + /* dynamic detection of bluetooth audio devices */ + if (!dbus_connection_add_filter(conn, filter_cb, y, NULL)) { + pa_log_error("Failed to add filter function"); + goto fail; + } + y->filter_added = true; + + if (pa_dbus_add_matches(conn, &err, + "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'" + ",arg0='" BLUEZ_SERVICE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager'," + "member='InterfacesRemoved'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_ADAPTER_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_DEVICE_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", + NULL) < 0) { + pa_log_error("Failed to add D-Bus matches: %s", err.message); + goto fail; + } + y->matches_added = true; + + count = pa_bluetooth_a2dp_codec_count(); + for (i = 0; i < count; i++) { + a2dp_codec = pa_bluetooth_a2dp_codec_iter(i); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SINK_ENDPOINT, a2dp_codec->name); + endpoint_init(y, endpoint); + pa_xfree(endpoint); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SOURCE_ENDPOINT, a2dp_codec->name); + endpoint_init(y, endpoint); + pa_xfree(endpoint); + } + + get_managed_objects(y); + + return y; + +fail: + pa_bluetooth_discovery_unref(y); + dbus_error_free(&err); + + return NULL; +} + +pa_bluetooth_discovery* pa_bluetooth_discovery_ref(pa_bluetooth_discovery *y) { + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + + PA_REFCNT_INC(y); + + return y; +} + +void pa_bluetooth_discovery_unref(pa_bluetooth_discovery *y) { + unsigned i, count; + const pa_a2dp_codec *a2dp_codec; + char *endpoint; + + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + + if (PA_REFCNT_DEC(y) > 0) + return; + + pa_dbus_free_pending_list(&y->pending); + + if (y->ofono_backend) + pa_bluetooth_ofono_backend_free(y->ofono_backend); + if (y->native_backend) + pa_bluetooth_native_backend_free(y->native_backend); + + if (y->adapters) + pa_hashmap_free(y->adapters); + + if (y->devices) + pa_hashmap_free(y->devices); + + if (y->transports) { + pa_assert(pa_hashmap_isempty(y->transports)); + pa_hashmap_free(y->transports); + } + + if (y->connection) { + + if (y->matches_added) + pa_dbus_remove_matches(pa_dbus_connection_get(y->connection), + "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" BLUEZ_SERVICE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager'," + "member='InterfacesAdded'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager'," + "member='InterfacesRemoved'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_ADAPTER_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_DEVICE_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", + NULL); + + if (y->filter_added) + dbus_connection_remove_filter(pa_dbus_connection_get(y->connection), filter_cb, y); + + count = pa_bluetooth_a2dp_codec_count(); + for (i = 0; i < count; i++) { + a2dp_codec = pa_bluetooth_a2dp_codec_iter(i); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SINK_ENDPOINT, a2dp_codec->name); + endpoint_done(y, endpoint); + pa_xfree(endpoint); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SOURCE_ENDPOINT, a2dp_codec->name); + endpoint_done(y, endpoint); + pa_xfree(endpoint); + } + + pa_dbus_connection_unref(y->connection); + } + + pa_shared_remove(y->core, "bluetooth-discovery"); + pa_xfree(y); +} diff -rNaud pulseaudio-13.0/src/modules/bluetooth/bluez5-util.c.rej pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.c.rej --- pulseaudio-13.0/src/modules/bluetooth/bluez5-util.c.rej 1970-01-01 01:00:00.000000000 +0100 +++ pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.c.rej 2019-09-17 09:09:39.170806000 +0200 @@ -0,0 +1,37 @@ +--- src/modules/bluetooth/bluez5-util.c ++++ src/modules/bluetooth/bluez5-util.c +@@ -624,14 +693,32 @@ pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_address(pa_bluetooth_d + return NULL; + } + ++static void remote_endpoint_remove(pa_bluetooth_discovery *y, const char *path) { ++ pa_bluetooth_device *device; ++ pa_hashmap *endpoints; ++ void *devices_state; ++ void *state; ++ ++ PA_HASHMAP_FOREACH(device, y->devices, devices_state) { ++ PA_HASHMAP_FOREACH(endpoints, device->a2dp_sink_endpoints, state) ++ pa_hashmap_remove_and_free(endpoints, path); ++ ++ PA_HASHMAP_FOREACH(endpoints, device->a2dp_source_endpoints, state) ++ pa_hashmap_remove_and_free(endpoints, path); ++ } ++ ++ pa_log_debug("Remote endpoint %s removed", path); ++} ++ + static void device_free(pa_bluetooth_device *d) { +- unsigned i; ++ unsigned i, bluetooth_profile_count; + + pa_assert(d); + + device_stop_waiting_for_profiles(d); + +- for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) { ++ bluetooth_profile_count = pa_bluetooth_profile_count(); ++ for (i = 0; i < bluetooth_profile_count; i++) { + pa_bluetooth_transport *t; + + if (!(t = d->transports[i])) diff -rNaud pulseaudio-13.0/src/modules/bluetooth/bluez5-util.h pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.h --- pulseaudio-13.0/src/modules/bluetooth/bluez5-util.h 2019-09-13 15:20:03.000000000 +0200 +++ pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.h 2019-09-17 09:09:39.170806000 +0200 @@ -54,14 +54,14 @@ PA_BLUETOOTH_HOOK_MAX } pa_bluetooth_hook_t; -typedef enum profile { - PA_BLUETOOTH_PROFILE_A2DP_SINK, - PA_BLUETOOTH_PROFILE_A2DP_SOURCE, - PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT, - PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY, - PA_BLUETOOTH_PROFILE_OFF -} pa_bluetooth_profile_t; -#define PA_BLUETOOTH_PROFILE_COUNT PA_BLUETOOTH_PROFILE_OFF +/* Profile index is used also for card profile priority. Higher number has higher priority. + * All A2DP profiles have higher priority as all non-A2DP profiles. + * And all A2DP sink profiles have higher priority as all A2DP source profiles. */ +#define PA_BLUETOOTH_PROFILE_OFF 0 +#define PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY 1 +#define PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT 2 +#define PA_BLUETOOTH_PROFILE_A2DP_START_INDEX 3 +typedef unsigned pa_bluetooth_profile_t; typedef enum pa_bluetooth_transport_state { PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED, @@ -86,8 +86,6 @@ uint8_t *config; size_t config_size; - const pa_a2dp_codec *a2dp_codec; - uint16_t microphone_gain; uint16_t speaker_gain; @@ -109,6 +107,7 @@ bool tried_to_link_with_adapter; bool valid; bool autodetect_mtu; + bool change_a2dp_profile_in_progress; /* Device information */ char *path; @@ -117,8 +116,10 @@ char *address; uint32_t class_of_device; pa_hashmap *uuids; /* char* -> char* (hashmap-as-a-set) */ + pa_hashmap *a2dp_sink_endpoints; /* pa_a2dp_codec_id* -> pa_hashmap ( char* (remote endpoint) -> pa_a2dp_codec_capabilities* ) */ + pa_hashmap *a2dp_source_endpoints; /* pa_a2dp_codec_id* -> pa_hashmap ( char* (remote endpoint) -> pa_a2dp_codec_capabilities* ) */ - pa_bluetooth_transport *transports[PA_BLUETOOTH_PROFILE_COUNT]; + pa_bluetooth_transport **transports; pa_time_event *wait_for_profiles_timer; }; @@ -161,6 +162,9 @@ void pa_bluetooth_transport_unlink(pa_bluetooth_transport *t); void pa_bluetooth_transport_free(pa_bluetooth_transport *t); +const char *pa_bluetooth_device_find_endpoint_for_codec(const pa_bluetooth_device *device, const pa_a2dp_codec *a2dp_codec, bool is_a2dp_sink); +bool pa_bluetooth_device_change_a2dp_profile(pa_bluetooth_device *d, pa_bluetooth_profile_t profile, void (*cb)(bool, void *), void *userdata); + bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d); pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_path(pa_bluetooth_discovery *y, const char *path); @@ -168,8 +172,21 @@ pa_hook* pa_bluetooth_discovery_hook(pa_bluetooth_discovery *y, pa_bluetooth_hook_t hook); +unsigned pa_bluetooth_profile_count(void); +bool pa_bluetooth_profile_is_a2dp_sink(pa_bluetooth_profile_t profile); +bool pa_bluetooth_profile_is_a2dp_source(pa_bluetooth_profile_t profile); +const pa_a2dp_codec *pa_bluetooth_profile_to_a2dp_codec(pa_bluetooth_profile_t profile); +pa_bluetooth_profile_t pa_bluetooth_profile_for_a2dp_codec(const char *codec_name, bool is_a2dp_sink); const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile); +static inline bool pa_bluetooth_profile_is_a2dp(pa_bluetooth_profile_t profile) { + return pa_bluetooth_profile_is_a2dp_sink(profile) || pa_bluetooth_profile_is_a2dp_source(profile); +} + +static inline bool pa_bluetooth_profile_support_a2dp_backchannel(pa_bluetooth_profile_t profile) { + return pa_bluetooth_profile_to_a2dp_codec(profile)->support_backchannel; +} + static inline bool pa_bluetooth_uuid_is_hsp_hs(const char *uuid) { return pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_HS) || pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_HS_ALT); } diff -rNaud pulseaudio-13.0/src/modules/bluetooth/bluez5-util.h.orig pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.h.orig --- pulseaudio-13.0/src/modules/bluetooth/bluez5-util.h.orig 1970-01-01 01:00:00.000000000 +0100 +++ pulseaudio-13.0-v12/src/modules/bluetooth/bluez5-util.h.orig 2019-09-13 15:20:03.000000000 +0200 @@ -0,0 +1,185 @@ +#ifndef foobluez5utilhfoo +#define foobluez5utilhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2008-2013 João Paulo Rechi Vita + Copyrigth 2018-2019 Pali Rohár + + 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 + +#include "a2dp-codec-util.h" + +#define PA_BLUETOOTH_UUID_A2DP_SOURCE "0000110a-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_A2DP_SINK "0000110b-0000-1000-8000-00805f9b34fb" + +/* There are two HSP HS UUIDs. The first one (older?) is used both as the HSP + * profile identifier and as the HS role identifier, while the second one is + * only used to identify the role. As far as PulseAudio is concerned, the two + * UUIDs mean exactly the same thing. */ +#define PA_BLUETOOTH_UUID_HSP_HS "00001108-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_HSP_HS_ALT "00001131-0000-1000-8000-00805f9b34fb" + +#define PA_BLUETOOTH_UUID_HSP_AG "00001112-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_HFP_HF "0000111e-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_HFP_AG "0000111f-0000-1000-8000-00805f9b34fb" + +typedef struct pa_bluetooth_transport pa_bluetooth_transport; +typedef struct pa_bluetooth_device pa_bluetooth_device; +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 enum pa_bluetooth_hook { + PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED, /* Call data: pa_bluetooth_device */ + PA_BLUETOOTH_HOOK_DEVICE_UNLINK, /* Call data: pa_bluetooth_device */ + PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED, /* Call data: pa_bluetooth_transport */ + PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED, /* Call data: pa_bluetooth_transport */ + PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED, /* Call data: pa_bluetooth_transport */ + PA_BLUETOOTH_HOOK_MAX +} pa_bluetooth_hook_t; + +typedef enum profile { + PA_BLUETOOTH_PROFILE_A2DP_SINK, + PA_BLUETOOTH_PROFILE_A2DP_SOURCE, + PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT, + PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY, + PA_BLUETOOTH_PROFILE_OFF +} pa_bluetooth_profile_t; +#define PA_BLUETOOTH_PROFILE_COUNT PA_BLUETOOTH_PROFILE_OFF + +typedef enum pa_bluetooth_transport_state { + PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED, + PA_BLUETOOTH_TRANSPORT_STATE_IDLE, + PA_BLUETOOTH_TRANSPORT_STATE_PLAYING +} pa_bluetooth_transport_state_t; + +typedef int (*pa_bluetooth_transport_acquire_cb)(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu); +typedef void (*pa_bluetooth_transport_release_cb)(pa_bluetooth_transport *t); +typedef void (*pa_bluetooth_transport_destroy_cb)(pa_bluetooth_transport *t); +typedef void (*pa_bluetooth_transport_set_speaker_gain_cb)(pa_bluetooth_transport *t, uint16_t gain); +typedef void (*pa_bluetooth_transport_set_microphone_gain_cb)(pa_bluetooth_transport *t, uint16_t gain); + +struct pa_bluetooth_transport { + pa_bluetooth_device *device; + + char *owner; + char *path; + pa_bluetooth_profile_t profile; + + uint8_t codec; + uint8_t *config; + size_t config_size; + + const pa_a2dp_codec *a2dp_codec; + + uint16_t microphone_gain; + uint16_t speaker_gain; + + pa_bluetooth_transport_state_t state; + + pa_bluetooth_transport_acquire_cb acquire; + pa_bluetooth_transport_release_cb release; + pa_bluetooth_transport_destroy_cb destroy; + pa_bluetooth_transport_set_speaker_gain_cb set_speaker_gain; + pa_bluetooth_transport_set_microphone_gain_cb set_microphone_gain; + void *userdata; +}; + +struct pa_bluetooth_device { + pa_bluetooth_discovery *discovery; + pa_bluetooth_adapter *adapter; + + bool properties_received; + bool tried_to_link_with_adapter; + bool valid; + bool autodetect_mtu; + + /* Device information */ + char *path; + char *adapter_path; + char *alias; + char *address; + uint32_t class_of_device; + pa_hashmap *uuids; /* char* -> char* (hashmap-as-a-set) */ + + pa_bluetooth_transport *transports[PA_BLUETOOTH_PROFILE_COUNT]; + + pa_time_event *wait_for_profiles_timer; +}; + +struct pa_bluetooth_adapter { + pa_bluetooth_discovery *discovery; + char *path; + char *address; + + bool valid; +}; + +#ifdef HAVE_BLUEZ_5_OFONO_HEADSET +pa_bluetooth_backend *pa_bluetooth_ofono_backend_new(pa_core *c, pa_bluetooth_discovery *y); +void pa_bluetooth_ofono_backend_free(pa_bluetooth_backend *b); +#else +static inline pa_bluetooth_backend *pa_bluetooth_ofono_backend_new(pa_core *c, pa_bluetooth_discovery *y) { + return NULL; +} +static inline void pa_bluetooth_ofono_backend_free(pa_bluetooth_backend *b) {} +#endif + +#ifdef HAVE_BLUEZ_5_NATIVE_HEADSET +pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role); +void pa_bluetooth_native_backend_free(pa_bluetooth_backend *b); +void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *b, bool enable_hs_role); +#else +static inline pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role) { + return NULL; +} +static inline void pa_bluetooth_native_backend_free(pa_bluetooth_backend *b) {} +static inline void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *b, bool enable_hs_role) {} +#endif + +pa_bluetooth_transport *pa_bluetooth_transport_new(pa_bluetooth_device *d, const char *owner, const char *path, + pa_bluetooth_profile_t p, const uint8_t *config, size_t size); + +void pa_bluetooth_transport_set_state(pa_bluetooth_transport *t, pa_bluetooth_transport_state_t state); +void pa_bluetooth_transport_put(pa_bluetooth_transport *t); +void pa_bluetooth_transport_unlink(pa_bluetooth_transport *t); +void pa_bluetooth_transport_free(pa_bluetooth_transport *t); + +bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d); + +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_path(pa_bluetooth_discovery *y, const char *path); +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_address(pa_bluetooth_discovery *y, const char *remote, const char *local); + +pa_hook* pa_bluetooth_discovery_hook(pa_bluetooth_discovery *y, pa_bluetooth_hook_t hook); + +const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile); + +static inline bool pa_bluetooth_uuid_is_hsp_hs(const char *uuid) { + return pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_HS) || pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_HS_ALT); +} + +#define HEADSET_BACKEND_OFONO 0 +#define HEADSET_BACKEND_NATIVE 1 +#define HEADSET_BACKEND_AUTO 2 + +pa_bluetooth_discovery* pa_bluetooth_discovery_get(pa_core *core, int headset_backend); +pa_bluetooth_discovery* pa_bluetooth_discovery_ref(pa_bluetooth_discovery *y); +void pa_bluetooth_discovery_unref(pa_bluetooth_discovery *y); +void pa_bluetooth_discovery_set_ofono_running(pa_bluetooth_discovery *y, bool is_running); +#endif diff -rNaud pulseaudio-13.0/src/modules/bluetooth/module-bluez5-device.c pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-device.c --- pulseaudio-13.0/src/modules/bluetooth/module-bluez5-device.c 2019-09-17 09:22:09.800727350 +0200 +++ pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-device.c 2019-09-17 09:19:25.092744000 +0200 @@ -131,15 +131,16 @@ pa_usec_t started_at; pa_smoother *read_smoother; pa_memchunk write_memchunk; - - const pa_a2dp_codec *a2dp_codec; + bool support_a2dp_codec_switch; void *encoder_info; + void *encoder_backchannel_info; pa_sample_spec encoder_sample_spec; void *encoder_buffer; /* Codec transfer buffer */ size_t encoder_buffer_size; /* Size of the buffer */ void *decoder_info; + void *decoder_backchannel_info; pa_sample_spec decoder_sample_spec; void *decoder_buffer; /* Codec transfer buffer */ size_t decoder_buffer_size; /* Size of the buffer */ @@ -501,14 +502,15 @@ /* Run from IO thread */ static int a2dp_process_render(struct userdata *u) { + const pa_a2dp_codec *a2dp_codec; const uint8_t *ptr; size_t processed; size_t length; pa_assert(u); - pa_assert(u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK); pa_assert(u->sink); - pa_assert(u->a2dp_codec); + + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); /* First, render some data */ if (!u->write_memchunk.memblock) @@ -521,7 +523,7 @@ /* Try to create a packet of the full MTU */ ptr = (const uint8_t *) pa_memblock_acquire_chunk(&u->write_memchunk); - length = u->a2dp_codec->encode_buffer(u->encoder_info, u->write_index / pa_frame_size(&u->encoder_sample_spec), ptr, u->write_memchunk.length, u->encoder_buffer, u->encoder_buffer_size, &processed); + length = a2dp_codec->encode_buffer(pa_bluetooth_profile_is_a2dp_sink(u->profile) ? u->encoder_info : u->encoder_backchannel_info, u->write_index / pa_frame_size(&u->encoder_sample_spec), ptr, u->write_memchunk.length, u->encoder_buffer, u->encoder_buffer_size, &processed); pa_memblock_release(u->write_memchunk.memblock); @@ -535,14 +537,15 @@ /* Run from IO thread */ static int a2dp_process_push(struct userdata *u) { + const pa_a2dp_codec *a2dp_codec; int ret = 0; pa_memchunk memchunk; pa_assert(u); - pa_assert(u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE); pa_assert(u->source); pa_assert(u->read_smoother); - pa_assert(u->a2dp_codec); + + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); memchunk.memblock = pa_memblock_new(u->core->mempool, u->read_block_size); memchunk.index = memchunk.length = 0; @@ -613,7 +616,7 @@ memchunk.length = pa_memblock_get_length(memchunk.memblock); timestamp = 0; /* Decoder does not have to fill RTP timestamp */ - memchunk.length = u->a2dp_codec->decode_buffer(u->decoder_info, ×tamp, u->decoder_buffer, l, ptr, memchunk.length, &processed); + memchunk.length = a2dp_codec->decode_buffer(pa_bluetooth_profile_is_a2dp_source(u->profile) ? u->decoder_info : u->decoder_backchannel_info, ×tamp, u->decoder_buffer, l, ptr, memchunk.length, &processed); pa_memblock_release(memchunk.memblock); @@ -759,7 +762,7 @@ static void handle_sink_block_size_change(struct userdata *u) { pa_sink_set_max_request_within_thread(u->sink, u->write_block_size); pa_sink_set_fixed_latency_within_thread(u->sink, - (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK ? + (pa_bluetooth_profile_is_a2dp(u->profile) ? FIXED_LATENCY_PLAYBACK_A2DP : FIXED_LATENCY_PLAYBACK_SCO) + pa_bytes_to_usec(u->write_block_size, &u->encoder_sample_spec)); @@ -789,11 +792,15 @@ u->write_block_size = pa_frame_align(u->write_block_size, &u->sink->sample_spec); } } else { - pa_assert(u->a2dp_codec); - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { - u->write_block_size = u->a2dp_codec->get_write_block_size(u->encoder_info, u->write_link_mtu); + const pa_a2dp_codec *a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); + if (pa_bluetooth_profile_is_a2dp_sink(u->profile)) { + u->write_block_size = a2dp_codec->get_write_block_size(u->encoder_info, u->write_link_mtu); + if (u->source) + u->read_block_size = a2dp_codec->get_read_block_size(u->decoder_backchannel_info, u->read_link_mtu); } else { - u->read_block_size = u->a2dp_codec->get_read_block_size(u->decoder_info, u->read_link_mtu); + u->read_block_size = a2dp_codec->get_read_block_size(u->decoder_info, u->read_link_mtu); + if (u->sink) + u->write_block_size = a2dp_codec->get_write_block_size(u->encoder_backchannel_info, u->write_link_mtu); } } @@ -802,13 +809,14 @@ if (u->source) pa_source_set_fixed_latency_within_thread(u->source, - (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE ? + (pa_bluetooth_profile_is_a2dp(u->profile) ? FIXED_LATENCY_RECORD_A2DP : FIXED_LATENCY_RECORD_SCO) + pa_bytes_to_usec(u->read_block_size, &u->decoder_sample_spec)); } /* Run from I/O thread */ static int setup_stream(struct userdata *u) { + const pa_a2dp_codec *a2dp_codec; struct pollfd *pollfd; int one; @@ -820,14 +828,23 @@ pa_log_info("Transport %s resuming", u->transport->path); - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { - pa_assert(u->a2dp_codec); - if (u->a2dp_codec->reset(u->encoder_info) < 0) - return -1; - } else if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) { - pa_assert(u->a2dp_codec); - if (u->a2dp_codec->reset(u->decoder_info) < 0) - return -1; + if (pa_bluetooth_profile_is_a2dp(u->profile)) { + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); + if (pa_bluetooth_profile_is_a2dp_sink(u->profile)) { + if (a2dp_codec->reset(u->encoder_info) != 0) + return false; + if (u->source) { + if (a2dp_codec->reset(u->decoder_backchannel_info) != 0) + return false; + } + } else { + if (a2dp_codec->reset(u->decoder_info) != 0) + return false; + if (u->sink) { + if (a2dp_codec->reset(u->encoder_backchannel_info) != 0) + return false; + } + } } transport_config_mtu(u); @@ -1004,6 +1021,7 @@ /* Run from main thread */ static int add_source(struct userdata *u) { pa_source_new_data data; + pa_card_profile *cp; pa_assert(u->transport); @@ -1018,27 +1036,25 @@ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone"); + pa_assert_se(cp = pa_hashmap_get(u->card->profiles, pa_bluetooth_profile_to_string(u->profile))); + pa_proplist_setf(data.proplist, PA_PROP_DEVICE_DESCRIPTION, "%s - %s", pa_proplist_gets(u->card->proplist, PA_PROP_DEVICE_DESCRIPTION), cp->description); + connect_ports(u, &data, PA_DIRECTION_INPUT); - if (!u->transport_acquired) - switch (u->profile) { - case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: - case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + if (!u->transport_acquired) { + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY || pa_bluetooth_profile_is_a2dp(u->profile)) { + data.suspend_cause = PA_SUSPEND_USER; + } else if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) { + /* u->stream_fd contains the error returned by the last transport_acquire() + * EAGAIN means we are waiting for a NewConnection signal */ + if (u->stream_fd == -EAGAIN) data.suspend_cause = PA_SUSPEND_USER; - break; - case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: - /* u->stream_fd contains the error returned by the last transport_acquire() - * EAGAIN means we are waiting for a NewConnection signal */ - if (u->stream_fd == -EAGAIN) - data.suspend_cause = PA_SUSPEND_USER; - else - pa_assert_not_reached(); - break; - case PA_BLUETOOTH_PROFILE_A2DP_SINK: - case PA_BLUETOOTH_PROFILE_OFF: + else pa_assert_not_reached(); - break; + } else { + pa_assert_not_reached(); } + } u->source = pa_source_new(u->core, &data, PA_SOURCE_HARDWARE|PA_SOURCE_LATENCY); pa_source_new_data_done(&data); @@ -1188,6 +1204,7 @@ /* Run from main thread */ static int add_sink(struct userdata *u) { pa_sink_new_data data; + pa_card_profile *cp; pa_assert(u->transport); @@ -1202,6 +1219,9 @@ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone"); + pa_assert_se(cp = pa_hashmap_get(u->card->profiles, pa_bluetooth_profile_to_string(u->profile))); + pa_proplist_setf(data.proplist, PA_PROP_DEVICE_DESCRIPTION, "%s - %s", pa_proplist_gets(u->card->proplist, PA_PROP_DEVICE_DESCRIPTION), cp->description); + connect_ports(u, &data, PA_DIRECTION_OUTPUT); if (!u->transport_acquired) @@ -1217,10 +1237,8 @@ else pa_assert_not_reached(); break; - case PA_BLUETOOTH_PROFILE_A2DP_SINK: + default: /* Profile switch should have failed */ - case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: - case PA_BLUETOOTH_PROFILE_OFF: pa_assert_not_reached(); break; } @@ -1254,19 +1272,18 @@ u->decoder_sample_spec.rate = 8000; return 0; } else { - bool is_a2dp_sink = u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK; + const pa_a2dp_codec *a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); + bool is_a2dp_sink = pa_bluetooth_profile_is_a2dp_sink(u->profile); void *info; pa_assert(u->transport); - pa_assert(!u->a2dp_codec); pa_assert(!u->encoder_info); pa_assert(!u->decoder_info); + pa_assert(!u->encoder_backchannel_info); + pa_assert(!u->decoder_backchannel_info); - u->a2dp_codec = u->transport->a2dp_codec; - pa_assert(u->a2dp_codec); - - info = u->a2dp_codec->init(is_a2dp_sink, false, u->transport->config, u->transport->config_size, is_a2dp_sink ? &u->encoder_sample_spec : &u->decoder_sample_spec); + info = a2dp_codec->init(is_a2dp_sink, false, u->transport->config, u->transport->config_size, is_a2dp_sink ? &u->encoder_sample_spec : &u->decoder_sample_spec); if (is_a2dp_sink) u->encoder_info = info; else @@ -1275,10 +1292,54 @@ if (!info) return -1; + if (a2dp_codec->support_backchannel) { + info = a2dp_codec->init(!is_a2dp_sink, true, u->transport->config, u->transport->config_size, !is_a2dp_sink ? &u->encoder_sample_spec : &u->decoder_sample_spec); + if (is_a2dp_sink) + u->decoder_backchannel_info = info; + else + u->encoder_backchannel_info = info; + + if (!info) { + if (is_a2dp_sink) { + a2dp_codec->deinit(u->encoder_info); + u->encoder_info = NULL; + } else { + a2dp_codec->deinit(u->decoder_info); + u->decoder_info = NULL; + } + return -1; + } + } + return 0; } } +static int init_profile(struct userdata *u); +static int start_thread(struct userdata *u); +static void stop_thread(struct userdata *u); + +static void change_a2dp_profile_cb(bool success, void *userdata) { + struct userdata *u = (struct userdata *) userdata; + + if (!success) + goto off; + + if (u->profile != PA_BLUETOOTH_PROFILE_OFF) + if (init_profile(u) < 0) + goto off; + + if (u->sink || u->source) + if (start_thread(u) < 0) + goto off; + + return; + +off: + stop_thread(u); + pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0); +} + /* Run from main thread */ static int setup_transport(struct userdata *u) { pa_bluetooth_transport *t; @@ -1290,13 +1351,19 @@ /* check if profile has a transport */ t = u->device->transports[u->profile]; if (!t || t->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) { - pa_log_warn("Profile %s has no transport", pa_bluetooth_profile_to_string(u->profile)); - return -1; + if (!pa_bluetooth_profile_is_a2dp(u->profile) || !u->support_a2dp_codec_switch) { + pa_log_warn("Profile %s has no transport", pa_bluetooth_profile_to_string(u->profile)); + return -1; + } + if (!pa_bluetooth_device_change_a2dp_profile(u->device, u->profile, change_a2dp_profile_cb, u)) + return -1; + /* Changing A2DP endpoint is now in progress and callback will be called after operation finish */ + return -EINPROGRESS; } u->transport = t; - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + if (pa_bluetooth_profile_is_a2dp_source(u->profile) || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) transport_acquire(u, true); /* In case of error, the sink/sources will be created suspended */ else { int transport_error; @@ -1311,15 +1378,22 @@ /* Run from main thread */ static pa_direction_t get_profile_direction(pa_bluetooth_profile_t p) { - static const pa_direction_t profile_direction[] = { - [PA_BLUETOOTH_PROFILE_A2DP_SINK] = PA_DIRECTION_OUTPUT, - [PA_BLUETOOTH_PROFILE_A2DP_SOURCE] = PA_DIRECTION_INPUT, - [PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT] = PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT, - [PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY] = PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT, - [PA_BLUETOOTH_PROFILE_OFF] = 0 - }; - - return profile_direction[p]; + if (p == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || p == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + return PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT; + else if (p == PA_BLUETOOTH_PROFILE_OFF) + return 0; + else if (pa_bluetooth_profile_is_a2dp_sink(p)) { + if (pa_bluetooth_profile_support_a2dp_backchannel(p)) + return PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT; + else + return PA_DIRECTION_OUTPUT; + } else if (pa_bluetooth_profile_is_a2dp_source(p)) { + if (pa_bluetooth_profile_support_a2dp_backchannel(p)) + return PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT; + else + return PA_DIRECTION_INPUT; + } else + pa_assert_not_reached(); } /* Run from main thread */ @@ -1328,7 +1402,10 @@ pa_assert(u); pa_assert(u->profile != PA_BLUETOOTH_PROFILE_OFF); - if (setup_transport(u) < 0) + r = setup_transport(u); + if (r == -EINPROGRESS) + return 0; + else if (r < 0) return -1; pa_assert(u->transport); @@ -1350,7 +1427,7 @@ if (u->write_index <= 0) u->started_at = pa_rtclock_now(); - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { + if (pa_bluetooth_profile_is_a2dp(u->profile)) { if ((n_written = a2dp_process_render(u)) < 0) return -1; } else { @@ -1424,7 +1501,7 @@ if (pollfd->revents & POLLIN) { int n_read; - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) + if (pa_bluetooth_profile_is_a2dp(u->profile)) n_read = a2dp_process_push(u); else n_read = sco_process_push(u); @@ -1432,7 +1509,7 @@ if (n_read < 0) goto fail; - if (have_sink && n_read > 0) { + if (!pa_bluetooth_profile_is_a2dp(u->profile) && n_read > 0 && have_sink ) { /* We just read something, so we are supposed to write something, too */ bytes_to_write += n_read; blocks_to_write += bytes_to_write / u->write_block_size; @@ -1454,7 +1531,7 @@ /* If we have a source, we let the source determine the timing * for the sink */ - if (have_source) { + if (!pa_bluetooth_profile_is_a2dp(u->profile) && have_source) { if (writable && blocks_to_write > 0) { int result; @@ -1524,8 +1601,10 @@ skip_bytes -= bytes_to_render; } - if (u->write_index > 0 && u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { - size_t new_write_block_size = u->a2dp_codec->reduce_encoder_bitrate(u->encoder_info, u->write_link_mtu); + if (u->write_index > 0 && pa_bluetooth_profile_is_a2dp(u->profile)) { + bool is_a2dp_sink = pa_bluetooth_profile_is_a2dp_sink(u->profile); + const pa_a2dp_codec *a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); + size_t new_write_block_size = a2dp_codec->reduce_encoder_bitrate(is_a2dp_sink ? u->encoder_info : u->encoder_backchannel_info, u->write_link_mtu); if (new_write_block_size) { u->write_block_size = new_write_block_size; handle_sink_block_size_change(u); @@ -1645,7 +1724,7 @@ /* If we are in the headset role or the device is an a2dp source, * the source should not become default unless there is no other * sound device available. */ - if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY || pa_bluetooth_profile_is_a2dp_source(u->profile)) u->source->priority = 1500; pa_source_put(u->source); @@ -1659,6 +1738,8 @@ /* Run from main thread */ static void stop_thread(struct userdata *u) { + const pa_a2dp_codec *a2dp_codec; + pa_assert(u); if (u->sink) @@ -1704,30 +1785,41 @@ u->read_smoother = NULL; } - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) { + if (pa_bluetooth_profile_is_a2dp(u->profile)) { + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); + if (u->encoder_info) { - u->a2dp_codec->deinit(u->encoder_info); + a2dp_codec->deinit(u->encoder_info); u->encoder_info = NULL; } if (u->decoder_info) { - u->a2dp_codec->deinit(u->decoder_info); + a2dp_codec->deinit(u->decoder_info); u->decoder_info = NULL; } - u->a2dp_codec = NULL; + if (u->decoder_backchannel_info) { + a2dp_codec->deinit(u->decoder_backchannel_info); + u->decoder_backchannel_info = NULL; + } + + if (u->encoder_backchannel_info) { + a2dp_codec->deinit(u->encoder_backchannel_info); + u->encoder_backchannel_info = NULL; + } } } /* Run from main thread */ static pa_available_t get_port_availability(struct userdata *u, pa_direction_t direction) { pa_available_t result = PA_AVAILABLE_NO; - unsigned i; + unsigned i, count; pa_assert(u); pa_assert(u->device); - for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) { + count = pa_bluetooth_profile_count(); + for (i = 0; i < count; i++) { pa_bluetooth_transport *transport; if (!(get_profile_direction(i) & direction)) @@ -1861,8 +1953,12 @@ static pa_card_profile *create_card_profile(struct userdata *u, pa_bluetooth_profile_t profile, pa_hashmap *ports) { pa_device_port *input_port, *output_port; const char *name; + char *description; pa_card_profile *cp = NULL; pa_bluetooth_profile_t *p; + const pa_a2dp_codec *a2dp_codec; + bool is_a2dp_sink; + bool support_backchannel; pa_assert(u->input_port_name); pa_assert(u->output_port_name); @@ -1871,34 +1967,9 @@ name = pa_bluetooth_profile_to_string(profile); - switch (profile) { - case PA_BLUETOOTH_PROFILE_A2DP_SINK: - cp = pa_card_profile_new(name, _("High Fidelity Playback (A2DP Sink)"), sizeof(pa_bluetooth_profile_t)); - cp->priority = 40; - cp->n_sinks = 1; - cp->n_sources = 0; - cp->max_sink_channels = 2; - cp->max_source_channels = 0; - pa_hashmap_put(output_port->profiles, cp->name, cp); - - p = PA_CARD_PROFILE_DATA(cp); - break; - - case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: - cp = pa_card_profile_new(name, _("High Fidelity Capture (A2DP Source)"), sizeof(pa_bluetooth_profile_t)); - cp->priority = 20; - cp->n_sinks = 0; - cp->n_sources = 1; - cp->max_sink_channels = 0; - cp->max_source_channels = 2; - pa_hashmap_put(input_port->profiles, cp->name, cp); - - p = PA_CARD_PROFILE_DATA(cp); - break; - - case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + if (profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) { cp = pa_card_profile_new(name, _("Headset Head Unit (HSP/HFP)"), sizeof(pa_bluetooth_profile_t)); - cp->priority = 30; + cp->priority = profile; cp->n_sinks = 1; cp->n_sources = 1; cp->max_sink_channels = 1; @@ -1907,11 +1978,9 @@ pa_hashmap_put(output_port->profiles, cp->name, cp); p = PA_CARD_PROFILE_DATA(cp); - break; - - case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + } else if (profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) { cp = pa_card_profile_new(name, _("Headset Audio Gateway (HSP/HFP)"), sizeof(pa_bluetooth_profile_t)); - cp->priority = 10; + cp->priority = profile; cp->n_sinks = 1; cp->n_sources = 1; cp->max_sink_channels = 1; @@ -1920,9 +1989,41 @@ pa_hashmap_put(output_port->profiles, cp->name, cp); p = PA_CARD_PROFILE_DATA(cp); - break; + } else if (pa_bluetooth_profile_is_a2dp(profile)) { + a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(profile); + is_a2dp_sink = pa_bluetooth_profile_is_a2dp_sink(profile); + support_backchannel = pa_bluetooth_profile_support_a2dp_backchannel(profile); - case PA_BLUETOOTH_PROFILE_OFF: + if (is_a2dp_sink) + description = pa_sprintf_malloc(_("High Fidelity Playback (A2DP Sink) with codec %s"), a2dp_codec->description); + else + description = pa_sprintf_malloc(_("High Fidelity Capture (A2DP Source) with codec %s"), a2dp_codec->description); + + cp = pa_card_profile_new(name, description, sizeof(pa_bluetooth_profile_t)); + pa_xfree(description); + + cp->priority = profile; + + if (is_a2dp_sink) { + cp->n_sinks = 1; + cp->n_sources = support_backchannel ? 1 : 0; + cp->max_sink_channels = 2; + cp->max_source_channels = support_backchannel ? 1 : 0; + } else { + cp->n_sinks = support_backchannel ? 1 : 0; + cp->n_sources = 1; + cp->max_sink_channels = support_backchannel ? 1 : 0; + cp->max_source_channels = 2; + } + + if (is_a2dp_sink || support_backchannel) + pa_hashmap_put(output_port->profiles, cp->name, cp); + + if (!is_a2dp_sink || support_backchannel) + pa_hashmap_put(input_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + } else { pa_assert_not_reached(); } @@ -1933,6 +2034,9 @@ else cp->available = PA_AVAILABLE_NO; + if (cp->available == PA_AVAILABLE_NO && u->support_a2dp_codec_switch && pa_bluetooth_profile_is_a2dp(profile)) + cp->available = PA_AVAILABLE_UNKNOWN; + return cp; } @@ -1950,7 +2054,7 @@ if (*p != PA_BLUETOOTH_PROFILE_OFF) { const pa_bluetooth_device *d = u->device; - if (!d->transports[*p] || d->transports[*p]->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) { + if ((!d->transports[*p] || d->transports[*p]->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) && (!pa_bluetooth_profile_is_a2dp(*p) || !u->support_a2dp_codec_switch)) { pa_log_warn("Refused to switch profile to %s: Not connected", new_profile->name); return -PA_ERR_IO; } @@ -1978,21 +2082,6 @@ return -PA_ERR_IO; } -static int uuid_to_profile(const char *uuid, pa_bluetooth_profile_t *_r) { - if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) - *_r = PA_BLUETOOTH_PROFILE_A2DP_SINK; - else if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) - *_r = PA_BLUETOOTH_PROFILE_A2DP_SOURCE; - else if (pa_bluetooth_uuid_is_hsp_hs(uuid) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_HF)) - *_r = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT; - else if (pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_AG) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_AG)) - *_r = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY; - else - return -PA_ERR_INVALID; - - return 0; -} - static void choose_initial_profile(struct userdata *u) { struct pa_bluetooth_transport *transport; pa_card_profile *iter_profile; @@ -2065,6 +2154,8 @@ pa_bluetooth_form_factor_t ff; pa_card_profile *cp; pa_bluetooth_profile_t *p; + bool have_a2dp_sink; + bool have_a2dp_source; const char *uuid; void *state; @@ -2097,11 +2188,23 @@ create_card_ports(u, data.ports); + have_a2dp_sink = false; + have_a2dp_source = false; + PA_HASHMAP_FOREACH(uuid, d->uuids, state) { pa_bluetooth_profile_t profile; - if (uuid_to_profile(uuid, &profile) < 0) + if (pa_bluetooth_uuid_is_hsp_hs(uuid) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_HF)) + profile = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT; + else if (pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_AG) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_AG)) + profile = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY; + else { + if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) + have_a2dp_sink = true; + else if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) + have_a2dp_source = true; continue; + } if (pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(profile))) continue; @@ -2110,6 +2213,64 @@ pa_hashmap_put(data.profiles, cp->name, cp); } + if ((pa_hashmap_isempty(d->a2dp_sink_endpoints) && have_a2dp_sink) || (pa_hashmap_isempty(d->a2dp_source_endpoints) && have_a2dp_source)) { + /* + * We are running old version of bluez which does not announce supported codecs + * by remote device nor does not support codec switching. Create profile for + * every supported codec by pulseaudio and bluez or remote device will choose one. + * Therefore user will see all also unavilable profiles, but would not be able + * to switch codec. + */ + unsigned i, count; + + pa_log_warn("Detected old bluez version, changing A2DP codec is not possible"); + u->support_a2dp_codec_switch = false; + + count = pa_bluetooth_profile_count(); + for (i = 0; i < count; i++) { + if (!((have_a2dp_sink && pa_bluetooth_profile_is_a2dp_sink(i)) || (have_a2dp_source && pa_bluetooth_profile_is_a2dp_source(i)))) + continue; + + if (pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(i))) + continue; + + cp = create_card_profile(u, i, data.ports); + pa_hashmap_put(data.profiles, cp->name, cp); + } + } else { + const pa_a2dp_codec *a2dp_codec; + pa_bluetooth_profile_t profile; + const char *endpoint; + unsigned i, count; + + u->support_a2dp_codec_switch = true; + + count = pa_bluetooth_a2dp_codec_count(); + for (i = 0; i < count; i++) { + a2dp_codec = pa_bluetooth_a2dp_codec_iter(i); + + endpoint = pa_bluetooth_device_find_endpoint_for_codec(d, a2dp_codec, true); + if (endpoint) { + profile = pa_bluetooth_profile_for_a2dp_codec(a2dp_codec->name, true); + if (!pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(profile))) { + cp = create_card_profile(u, profile, data.ports); + pa_hashmap_put(data.profiles, cp->name, cp); + } + pa_log_info("Detected codec %s on sink endpoint %s", a2dp_codec->name, endpoint); + } + + endpoint = pa_bluetooth_device_find_endpoint_for_codec(d, a2dp_codec, false); + if (endpoint) { + profile = pa_bluetooth_profile_for_a2dp_codec(a2dp_codec->name, false); + if (!pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(profile))) { + cp = create_card_profile(u, profile, data.ports); + pa_hashmap_put(data.profiles, cp->name, cp); + } + pa_log_info("Detected codec %s on source endpoint %s", a2dp_codec->name, endpoint); + } + } + } + pa_assert(!pa_hashmap_isempty(data.profiles)); cp = pa_card_profile_new("off", _("Off"), sizeof(pa_bluetooth_profile_t)); @@ -2143,13 +2304,19 @@ pa_card_profile *cp; pa_device_port *port; pa_available_t oldavail; + pa_available_t newavail; pa_assert(u); pa_assert(t); pa_assert_se(cp = pa_hashmap_get(u->card->profiles, pa_bluetooth_profile_to_string(t->profile))); oldavail = cp->available; - pa_card_profile_set_available(cp, transport_state_to_availability(t->state)); + + newavail = transport_state_to_availability(t->state); + if (newavail == PA_AVAILABLE_NO && u->support_a2dp_codec_switch && pa_bluetooth_profile_is_a2dp(t->profile)) + newavail = PA_AVAILABLE_UNKNOWN; + + pa_card_profile_set_available(cp, newavail); /* Update port availability */ pa_assert_se(port = pa_hashmap_get(u->card->ports, u->output_port_name)); @@ -2219,7 +2386,7 @@ pa_assert(d); pa_assert(u); - if (d != u->device || pa_bluetooth_device_any_transport_connected(d)) + if (d != u->device || pa_bluetooth_device_any_transport_connected(d) || d->change_a2dp_profile_in_progress) return PA_HOOK_OK; pa_log_debug("Unloading module for device %s", d->path); diff -rNaud pulseaudio-13.0/src/modules/bluetooth/module-bluez5-device.c.orig pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-device.c.orig --- pulseaudio-13.0/src/modules/bluetooth/module-bluez5-device.c.orig 2019-09-17 09:22:09.799727350 +0200 +++ pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-device.c.orig 2019-09-17 09:09:39.166806000 +0200 @@ -1993,6 +1993,70 @@ return 0; } +static void choose_initial_profile(struct userdata *u) { + struct pa_bluetooth_transport *transport; + pa_card_profile *iter_profile; + pa_card_profile *profile; + void *state; + + pa_log_debug("Looking for A2DP profile which has active bluez transport for card %s", u->card->name); + + profile = NULL; + + /* First try to find the best A2DP profile with transport in playing state */ + PA_HASHMAP_FOREACH(iter_profile, u->card->profiles, state) { + transport = u->device->transports[*(pa_bluetooth_profile_t *)PA_CARD_PROFILE_DATA(iter_profile)]; + + /* Ignore profiles without active bluez transport in playing state */ + if (!transport || transport->state != PA_BLUETOOTH_TRANSPORT_STATE_PLAYING) + continue; + + /* Ignore non-A2DP profiles */ + if (!pa_startswith(iter_profile->name, "a2dp_")) + continue; + + pa_log_debug("%s has active bluez transport in playing state", iter_profile->name); + + if (!profile || iter_profile->priority > profile->priority) + profile = iter_profile; + } + + /* Second if there is no profile A2DP profile with transport in playing state, look at active transports */ + if (!profile) { + PA_HASHMAP_FOREACH(iter_profile, u->card->profiles, state) { + transport = u->device->transports[*(pa_bluetooth_profile_t *)PA_CARD_PROFILE_DATA(iter_profile)]; + + /* Ignore profiles without active bluez transport */ + if (!transport || transport->state == PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) + continue; + + /* Ignore non-A2DP profiles */ + if (!pa_startswith(iter_profile->name, "a2dp_")) + continue; + + pa_log_debug("%s has active bluez transport", iter_profile->name); + + if (!profile || iter_profile->priority > profile->priority) + profile = iter_profile; + } + } + + /* When there is no active A2DP bluez transport, fallback to core pulseaudio function for choosing initial profile */ + if (!profile) { + pa_log_debug("No A2DP profile with bluez active transport was found for card %s", u->card->name); + pa_card_choose_initial_profile(u->card); + return; + } + + /* Do same job as pa_card_choose_initial_profile() */ + pa_log_info("Setting initial A2DP profile '%s' for card %s", profile->name, u->card->name); + u->card->active_profile = profile; + u->card->save_profile = false; + + /* Let policy modules override the default. */ + pa_hook_fire(&u->card->core->hooks[PA_CORE_HOOK_CARD_CHOOSE_INITIAL_PROFILE], u->card); +} + /* Run from main thread */ static int add_card(struct userdata *u) { const pa_bluetooth_device *d; @@ -2063,7 +2127,7 @@ u->card->userdata = u; u->card->set_profile = set_profile_cb; - pa_card_choose_initial_profile(u->card); + choose_initial_profile(u); pa_card_put(u->card); p = PA_CARD_PROFILE_DATA(u->card->active_profile); diff -rNaud pulseaudio-13.0/src/modules/bluetooth/module-bluez5-device.c.rej pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-device.c.rej --- pulseaudio-13.0/src/modules/bluetooth/module-bluez5-device.c.rej 1970-01-01 01:00:00.000000000 +0100 +++ pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-device.c.rej 2019-09-17 09:09:39.171806000 +0200 @@ -0,0 +1,59 @@ +--- src/modules/bluetooth/module-bluez5-device.c ++++ src/modules/bluetooth/module-bluez5-device.c +@@ -802,13 +809,14 @@ static void transport_config_mtu(struct userdata *u) { + + if (u->source) + pa_source_set_fixed_latency_within_thread(u->source, +- (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE ? ++ (pa_bluetooth_profile_is_a2dp(u->profile) ? + FIXED_LATENCY_RECORD_A2DP : FIXED_LATENCY_RECORD_SCO) + + pa_bytes_to_usec(u->read_block_size, &u->decoder_sample_spec)); + } + + /* Run from I/O thread */ + static bool setup_stream(struct userdata *u) { ++ const pa_a2dp_codec *a2dp_codec; + struct pollfd *pollfd; + int one; + +@@ -818,14 +826,23 @@ static bool setup_stream(struct userdata *u) { + + pa_log_info("Transport %s resuming", u->transport->path); + +- if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { +- pa_assert(u->a2dp_codec); +- if (u->a2dp_codec->reset(u->encoder_info) != 0) +- return false; +- } else if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) { +- pa_assert(u->a2dp_codec); +- if (u->a2dp_codec->reset(u->decoder_info) != 0) +- return false; ++ if (pa_bluetooth_profile_is_a2dp(u->profile)) { ++ a2dp_codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); ++ if (pa_bluetooth_profile_is_a2dp_sink(u->profile)) { ++ if (a2dp_codec->reset(u->encoder_info) != 0) ++ return false; ++ if (u->source) { ++ if (a2dp_codec->reset(u->decoder_backchannel_info) != 0) ++ return false; ++ } ++ } else { ++ if (a2dp_codec->reset(u->decoder_info) != 0) ++ return false; ++ if (u->sink) { ++ if (a2dp_codec->reset(u->encoder_backchannel_info) != 0) ++ return false; ++ } ++ } + } + + transport_config_mtu(u); +@@ -1470,7 +1547,7 @@ static void thread_func(void *userdata) { + if (n_read < 0) + goto fail; + +- if (n_read > 0) { ++ if (!pa_bluetooth_profile_is_a2dp(u->profile) && n_read > 0) { + /* We just read something, so we are supposed to write something, too */ + bytes_to_write += n_read; + blocks_to_write += bytes_to_write / u->write_block_size; diff -rNaud pulseaudio-13.0/src/modules/bluetooth/module-bluez5-discover.c pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-discover.c --- pulseaudio-13.0/src/modules/bluetooth/module-bluez5-discover.c 2019-09-13 15:20:03.000000000 +0200 +++ pulseaudio-13.0-v12/src/modules/bluetooth/module-bluez5-discover.c 2019-09-17 09:09:39.171806000 +0200 @@ -62,7 +62,8 @@ module_loaded = pa_hashmap_get(u->loaded_device_paths, d->path) ? true : false; - if (module_loaded && !pa_bluetooth_device_any_transport_connected(d)) { + /* When changing A2DP profile there is no transport connected, ensure that no module is unloaded */ + if (module_loaded && !pa_bluetooth_device_any_transport_connected(d) && !d->change_a2dp_profile_in_progress) { /* disconnection, the module unloads itself */ pa_log_debug("Unregistering module for %s", d->path); pa_hashmap_remove(u->loaded_device_paths, d->path);