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 <pali.rohar@gmail.com>
+
+  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 <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pulse/rtclock.h>
+#include <pulse/timeval.h>
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/dbus-shared.h>
+#include <pulsecore/log.h>
+#include <pulsecore/macro.h>
+#include <pulsecore/refcnt.h>
+#include <pulsecore/shared.h>
+
+#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                           \
+    "<node>"                                                            \
+    " <interface name=\"" BLUEZ_MEDIA_ENDPOINT_INTERFACE "\">"          \
+    "  <method name=\"SetConfiguration\">"                              \
+    "   <arg name=\"transport\" direction=\"in\" type=\"o\"/>"          \
+    "   <arg name=\"properties\" direction=\"in\" type=\"ay\"/>"        \
+    "  </method>"                                                       \
+    "  <method name=\"SelectConfiguration\">"                           \
+    "   <arg name=\"capabilities\" direction=\"in\" type=\"ay\"/>"      \
+    "   <arg name=\"configuration\" direction=\"out\" type=\"ay\"/>"    \
+    "  </method>"                                                       \
+    "  <method name=\"ClearConfiguration\">"                            \
+    "   <arg name=\"transport\" direction=\"in\" type=\"o\"/>"          \
+    "  </method>"                                                       \
+    "  <method name=\"Release\">"                                       \
+    "  </method>"                                                       \
+    " </interface>"                                                     \
+    " <interface name=\"org.freedesktop.DBus.Introspectable\">"         \
+    "  <method name=\"Introspect\">"                                    \
+    "   <arg name=\"data\" type=\"s\" direction=\"out\"/>"              \
+    "  </method>"                                                       \
+    " </interface>"                                                     \
+    "</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 <pali.rohar@gmail.com>
+
+  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 <http://www.gnu.org/licenses/>.
+***/
+
+#include <pulsecore/core.h>
+
+#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, &timestamp, 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, &timestamp, 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);
