summaryrefslogtreecommitdiffstats
path: root/src/modules/bluetooth
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:03:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:03:18 +0000
commit2dd5bc6a074165ddfbd57c4bd52c2d2dac8f47a1 (patch)
tree465b29cb405d3af0b0ad50c78e1dccc636594fec /src/modules/bluetooth
parentInitial commit. (diff)
downloadpulseaudio-2dd5bc6a074165ddfbd57c4bd52c2d2dac8f47a1.tar.xz
pulseaudio-2dd5bc6a074165ddfbd57c4bd52c2d2dac8f47a1.zip
Adding upstream version 14.2.upstream/14.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/modules/bluetooth')
-rw-r--r--src/modules/bluetooth/a2dp-codec-api.h98
-rw-r--r--src/modules/bluetooth/a2dp-codec-sbc.c682
-rw-r--r--src/modules/bluetooth/a2dp-codec-util.c56
-rw-r--r--src/modules/bluetooth/a2dp-codec-util.h34
-rw-r--r--src/modules/bluetooth/a2dp-codecs.h435
-rw-r--r--src/modules/bluetooth/backend-native.c714
-rw-r--r--src/modules/bluetooth/backend-ofono.c764
-rw-r--r--src/modules/bluetooth/bluez5-util.c1744
-rw-r--r--src/modules/bluetooth/bluez5-util.h185
-rw-r--r--src/modules/bluetooth/meson.build33
-rw-r--r--src/modules/bluetooth/module-bluetooth-discover.c76
-rw-r--r--src/modules/bluetooth/module-bluetooth-policy.c517
-rw-r--r--src/modules/bluetooth/module-bluez5-device.c2417
-rw-r--r--src/modules/bluetooth/module-bluez5-discover.c173
-rw-r--r--src/modules/bluetooth/rtp.h80
15 files changed, 8008 insertions, 0 deletions
diff --git a/src/modules/bluetooth/a2dp-codec-api.h b/src/modules/bluetooth/a2dp-codec-api.h
new file mode 100644
index 0000000..a3123f4
--- /dev/null
+++ b/src/modules/bluetooth/a2dp-codec-api.h
@@ -0,0 +1,98 @@
+#ifndef fooa2dpcodechfoo
+#define fooa2dpcodechfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 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>
+
+#define MAX_A2DP_CAPS_SIZE 254
+
+typedef struct pa_a2dp_codec_capabilities {
+ uint8_t size;
+ uint8_t buffer[]; /* max size is 254 bytes */
+} pa_a2dp_codec_capabilities;
+
+typedef struct pa_a2dp_codec_id {
+ uint8_t codec_id;
+ uint32_t vendor_id;
+ uint16_t vendor_codec_id;
+} pa_a2dp_codec_id;
+
+typedef struct pa_a2dp_codec {
+ /* Unique name of the codec, lowercase and without whitespaces, used for
+ * constructing identifier, D-Bus paths, ... */
+ const char *name;
+ /* Human readable codec description */
+ const char *description;
+
+ /* A2DP codec id */
+ pa_a2dp_codec_id id;
+
+ /* True if codec is bi-directional and supports backchannel */
+ bool support_backchannel;
+
+ /* Returns true if codec accepts capabilities, for_encoding is true when
+ * capabilities are used for encoding */
+ bool (*can_accept_capabilities)(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding);
+ /* Choose remote endpoint based on capabilities from hash map
+ * (const char *endpoint -> const pa_a2dp_codec_capabilities *capability)
+ * and returns corresponding endpoint key (or NULL when there is no valid),
+ * for_encoder is true when capabilities hash map is used for encoding */
+ const char *(*choose_remote_endpoint)(const pa_hashmap *capabilities_hashmap, const pa_sample_spec *default_sample_spec, bool for_encoding);
+ /* Fill codec capabilities, returns size of filled buffer */
+ uint8_t (*fill_capabilities)(uint8_t capabilities_buffer[MAX_A2DP_CAPS_SIZE]);
+ /* Validate codec configuration, returns true on success */
+ bool (*is_configuration_valid)(const uint8_t *config_buffer, uint8_t config_size);
+ /* Fill preferred codec configuration, returns size of filled buffer or 0 on failure */
+ uint8_t (*fill_preferred_configuration)(const pa_sample_spec *default_sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[MAX_A2DP_CAPS_SIZE]);
+
+ /* Initialize codec, returns codec info data and set sample_spec,
+ * for_encoding is true when codec_info is used for encoding,
+ * for_backchannel is true when codec_info is used for backchannel */
+ void *(*init)(bool for_encoding, bool for_backchannel, const uint8_t *config_buffer, uint8_t config_size, pa_sample_spec *sample_spec);
+ /* Deinitialize and release codec info data in codec_info */
+ void (*deinit)(void *codec_info);
+ /* Reset internal state of codec info data in codec_info, returns
+ * a negative value on failure */
+ int (*reset)(void *codec_info);
+
+ /* Get read block size for codec, it is minimal size of buffer
+ * needed to decode read_link_mtu bytes of encoded data */
+ size_t (*get_read_block_size)(void *codec_info, size_t read_link_mtu);
+ /* Get write block size for codec, it is maximal size of buffer
+ * which can produce at most write_link_mtu bytes of encoded data */
+ size_t (*get_write_block_size)(void *codec_info, size_t write_link_mtu);
+
+ /* Reduce encoder bitrate for codec, returns new write block size or zero
+ * if not changed, called when socket is not accepting encoded data fast
+ * enough */
+ size_t (*reduce_encoder_bitrate)(void *codec_info, size_t write_link_mtu);
+
+ /* Encode input_buffer of input_size to output_buffer of output_size,
+ * returns size of filled ouput_buffer and set processed to size of
+ * processed input_buffer */
+ size_t (*encode_buffer)(void *codec_info, uint32_t timestamp, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed);
+ /* Decode input_buffer of input_size to output_buffer of output_size,
+ * returns size of filled ouput_buffer and set processed to size of
+ * processed input_buffer */
+ size_t (*decode_buffer)(void *codec_info, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed);
+} pa_a2dp_codec;
+
+#endif
diff --git a/src/modules/bluetooth/a2dp-codec-sbc.c b/src/modules/bluetooth/a2dp-codec-sbc.c
new file mode 100644
index 0000000..89c647f
--- /dev/null
+++ b/src/modules/bluetooth/a2dp-codec-sbc.c
@@ -0,0 +1,682 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 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 <pulsecore/core-util.h>
+#include <pulsecore/log.h>
+#include <pulsecore/macro.h>
+#include <pulsecore/once.h>
+#include <pulse/sample.h>
+#include <pulse/xmalloc.h>
+
+#include <arpa/inet.h>
+
+#include <sbc/sbc.h>
+
+#include "a2dp-codecs.h"
+#include "a2dp-codec-api.h"
+#include "rtp.h"
+
+#define SBC_BITPOOL_DEC_LIMIT 32
+#define SBC_BITPOOL_DEC_STEP 5
+
+struct sbc_info {
+ sbc_t sbc; /* Codec data */
+ size_t codesize, frame_length; /* SBC Codesize, frame_length. We simply cache those values here */
+ uint16_t seq_num; /* Cumulative packet sequence */
+ uint8_t frequency;
+ uint8_t blocks;
+ uint8_t subbands;
+ uint8_t mode;
+ uint8_t allocation;
+ uint8_t initial_bitpool;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+};
+
+static bool can_accept_capabilities(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding) {
+ const a2dp_sbc_t *capabilities = (const a2dp_sbc_t *) capabilities_buffer;
+
+ if (capabilities_size != sizeof(*capabilities))
+ return false;
+
+ if (!(capabilities->frequency & (SBC_SAMPLING_FREQ_16000 | SBC_SAMPLING_FREQ_32000 | SBC_SAMPLING_FREQ_44100 | SBC_SAMPLING_FREQ_48000)))
+ return false;
+
+ if (!(capabilities->channel_mode & (SBC_CHANNEL_MODE_MONO | SBC_CHANNEL_MODE_DUAL_CHANNEL | SBC_CHANNEL_MODE_STEREO | SBC_CHANNEL_MODE_JOINT_STEREO)))
+ return false;
+
+ if (!(capabilities->allocation_method & (SBC_ALLOCATION_SNR | SBC_ALLOCATION_LOUDNESS)))
+ return false;
+
+ if (!(capabilities->subbands & (SBC_SUBBANDS_4 | SBC_SUBBANDS_8)))
+ return false;
+
+ if (!(capabilities->block_length & (SBC_BLOCK_LENGTH_4 | SBC_BLOCK_LENGTH_8 | SBC_BLOCK_LENGTH_12 | SBC_BLOCK_LENGTH_16)))
+ return false;
+
+ return true;
+}
+
+static const char *choose_remote_endpoint(const pa_hashmap *capabilities_hashmap, const pa_sample_spec *default_sample_spec, bool for_encoding) {
+ const pa_a2dp_codec_capabilities *a2dp_capabilities;
+ const char *key;
+ void *state;
+
+ /* There is no preference, just choose random valid entry */
+ PA_HASHMAP_FOREACH_KV(key, a2dp_capabilities, capabilities_hashmap, state) {
+ if (can_accept_capabilities(a2dp_capabilities->buffer, a2dp_capabilities->size, for_encoding))
+ return key;
+ }
+
+ return NULL;
+}
+
+static uint8_t fill_capabilities(uint8_t capabilities_buffer[MAX_A2DP_CAPS_SIZE]) {
+ a2dp_sbc_t *capabilities = (a2dp_sbc_t *) capabilities_buffer;
+
+ pa_zero(*capabilities);
+
+ capabilities->channel_mode = SBC_CHANNEL_MODE_MONO | SBC_CHANNEL_MODE_DUAL_CHANNEL | SBC_CHANNEL_MODE_STEREO |
+ SBC_CHANNEL_MODE_JOINT_STEREO;
+ capabilities->frequency = SBC_SAMPLING_FREQ_16000 | SBC_SAMPLING_FREQ_32000 | SBC_SAMPLING_FREQ_44100 |
+ SBC_SAMPLING_FREQ_48000;
+ capabilities->allocation_method = SBC_ALLOCATION_SNR | SBC_ALLOCATION_LOUDNESS;
+ capabilities->subbands = SBC_SUBBANDS_4 | SBC_SUBBANDS_8;
+ capabilities->block_length = SBC_BLOCK_LENGTH_4 | SBC_BLOCK_LENGTH_8 | SBC_BLOCK_LENGTH_12 | SBC_BLOCK_LENGTH_16;
+ capabilities->min_bitpool = SBC_MIN_BITPOOL;
+ capabilities->max_bitpool = SBC_BITPOOL_HQ_JOINT_STEREO_44100;
+
+ return sizeof(*capabilities);
+}
+
+static bool is_configuration_valid(const uint8_t *config_buffer, uint8_t config_size) {
+ const a2dp_sbc_t *config = (const a2dp_sbc_t *) config_buffer;
+
+ if (config_size != sizeof(*config)) {
+ pa_log_error("Invalid size of config buffer");
+ return false;
+ }
+
+ if (config->frequency != SBC_SAMPLING_FREQ_16000 && config->frequency != SBC_SAMPLING_FREQ_32000 &&
+ config->frequency != SBC_SAMPLING_FREQ_44100 && config->frequency != SBC_SAMPLING_FREQ_48000) {
+ pa_log_error("Invalid sampling frequency in configuration");
+ return false;
+ }
+
+ if (config->channel_mode != SBC_CHANNEL_MODE_MONO && config->channel_mode != SBC_CHANNEL_MODE_DUAL_CHANNEL &&
+ config->channel_mode != SBC_CHANNEL_MODE_STEREO && config->channel_mode != SBC_CHANNEL_MODE_JOINT_STEREO) {
+ pa_log_error("Invalid channel mode in configuration");
+ return false;
+ }
+
+ if (config->allocation_method != SBC_ALLOCATION_SNR && config->allocation_method != SBC_ALLOCATION_LOUDNESS) {
+ pa_log_error("Invalid allocation method in configuration");
+ return false;
+ }
+
+ if (config->subbands != SBC_SUBBANDS_4 && config->subbands != SBC_SUBBANDS_8) {
+ pa_log_error("Invalid SBC subbands in configuration");
+ return false;
+ }
+
+ if (config->block_length != SBC_BLOCK_LENGTH_4 && config->block_length != SBC_BLOCK_LENGTH_8 &&
+ config->block_length != SBC_BLOCK_LENGTH_12 && config->block_length != SBC_BLOCK_LENGTH_16) {
+ pa_log_error("Invalid block length in configuration");
+ return false;
+ }
+
+ if (config->min_bitpool > config->max_bitpool) {
+ pa_log_error("Invalid bitpool in configuration");
+ return false;
+ }
+
+ return true;
+}
+
+static uint8_t default_bitpool(uint8_t freq, uint8_t mode) {
+ /* These bitpool values were chosen based on the A2DP spec recommendation */
+ switch (freq) {
+ case SBC_SAMPLING_FREQ_16000:
+ case SBC_SAMPLING_FREQ_32000:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return SBC_BITPOOL_HQ_JOINT_STEREO_44100;
+ }
+ break;
+
+ case SBC_SAMPLING_FREQ_44100:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ return SBC_BITPOOL_HQ_MONO_44100;
+
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return SBC_BITPOOL_HQ_JOINT_STEREO_44100;
+ }
+ break;
+
+ case SBC_SAMPLING_FREQ_48000:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ return SBC_BITPOOL_HQ_MONO_48000;
+
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return SBC_BITPOOL_HQ_JOINT_STEREO_48000;
+ }
+ break;
+ }
+
+ pa_assert_not_reached();
+}
+
+static uint8_t fill_preferred_configuration(const pa_sample_spec *default_sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[MAX_A2DP_CAPS_SIZE]) {
+ a2dp_sbc_t *config = (a2dp_sbc_t *) config_buffer;
+ const a2dp_sbc_t *capabilities = (const a2dp_sbc_t *) capabilities_buffer;
+ int i;
+
+ static const struct {
+ uint32_t rate;
+ uint8_t cap;
+ } freq_table[] = {
+ { 16000U, SBC_SAMPLING_FREQ_16000 },
+ { 32000U, SBC_SAMPLING_FREQ_32000 },
+ { 44100U, SBC_SAMPLING_FREQ_44100 },
+ { 48000U, SBC_SAMPLING_FREQ_48000 }
+ };
+
+ if (capabilities_size != sizeof(*capabilities)) {
+ pa_log_error("Invalid size of capabilities buffer");
+ return 0;
+ }
+
+ pa_zero(*config);
+
+ /* Find the lowest freq that is at least as high as the requested sampling rate */
+ for (i = 0; (unsigned) i < PA_ELEMENTSOF(freq_table); i++)
+ if (freq_table[i].rate >= default_sample_spec->rate && (capabilities->frequency & freq_table[i].cap)) {
+ config->frequency = freq_table[i].cap;
+ break;
+ }
+
+ if ((unsigned) i == PA_ELEMENTSOF(freq_table)) {
+ for (--i; i >= 0; i--) {
+ if (capabilities->frequency & freq_table[i].cap) {
+ config->frequency = freq_table[i].cap;
+ break;
+ }
+ }
+
+ if (i < 0) {
+ pa_log_error("Not suitable sample rate");
+ return 0;
+ }
+ }
+
+ pa_assert((unsigned) i < PA_ELEMENTSOF(freq_table));
+
+ if (default_sample_spec->channels <= 1) {
+ if (capabilities->channel_mode & SBC_CHANNEL_MODE_MONO)
+ config->channel_mode = SBC_CHANNEL_MODE_MONO;
+ else if (capabilities->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO)
+ config->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO;
+ else if (capabilities->channel_mode & SBC_CHANNEL_MODE_STEREO)
+ config->channel_mode = SBC_CHANNEL_MODE_STEREO;
+ else if (capabilities->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL)
+ config->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL;
+ else {
+ pa_log_error("No supported channel modes");
+ return 0;
+ }
+ } else {
+ if (capabilities->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO)
+ config->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO;
+ else if (capabilities->channel_mode & SBC_CHANNEL_MODE_STEREO)
+ config->channel_mode = SBC_CHANNEL_MODE_STEREO;
+ else if (capabilities->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL)
+ config->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL;
+ else if (capabilities->channel_mode & SBC_CHANNEL_MODE_MONO)
+ config->channel_mode = SBC_CHANNEL_MODE_MONO;
+ else {
+ pa_log_error("No supported channel modes");
+ return 0;
+ }
+ }
+
+ if (capabilities->block_length & SBC_BLOCK_LENGTH_16)
+ config->block_length = SBC_BLOCK_LENGTH_16;
+ else if (capabilities->block_length & SBC_BLOCK_LENGTH_12)
+ config->block_length = SBC_BLOCK_LENGTH_12;
+ else if (capabilities->block_length & SBC_BLOCK_LENGTH_8)
+ config->block_length = SBC_BLOCK_LENGTH_8;
+ else if (capabilities->block_length & SBC_BLOCK_LENGTH_4)
+ config->block_length = SBC_BLOCK_LENGTH_4;
+ else {
+ pa_log_error("No supported block lengths");
+ return 0;
+ }
+
+ if (capabilities->subbands & SBC_SUBBANDS_8)
+ config->subbands = SBC_SUBBANDS_8;
+ else if (capabilities->subbands & SBC_SUBBANDS_4)
+ config->subbands = SBC_SUBBANDS_4;
+ else {
+ pa_log_error("No supported subbands");
+ return 0;
+ }
+
+ if (capabilities->allocation_method & SBC_ALLOCATION_LOUDNESS)
+ config->allocation_method = SBC_ALLOCATION_LOUDNESS;
+ else if (capabilities->allocation_method & SBC_ALLOCATION_SNR)
+ config->allocation_method = SBC_ALLOCATION_SNR;
+ else {
+ pa_log_error("No supported allocation method");
+ return 0;
+ }
+
+ config->min_bitpool = (uint8_t) PA_MAX(SBC_MIN_BITPOOL, capabilities->min_bitpool);
+ config->max_bitpool = (uint8_t) PA_MIN(default_bitpool(config->frequency, config->channel_mode), capabilities->max_bitpool);
+
+ if (config->min_bitpool > config->max_bitpool) {
+ pa_log_error("No supported bitpool");
+ return 0;
+ }
+
+ return sizeof(*config);
+}
+
+static void set_params(struct sbc_info *sbc_info) {
+ sbc_info->sbc.frequency = sbc_info->frequency;
+ sbc_info->sbc.blocks = sbc_info->blocks;
+ sbc_info->sbc.subbands = sbc_info->subbands;
+ sbc_info->sbc.mode = sbc_info->mode;
+ sbc_info->sbc.allocation = sbc_info->allocation;
+ sbc_info->sbc.bitpool = sbc_info->initial_bitpool;
+ sbc_info->sbc.endian = SBC_LE;
+
+ sbc_info->codesize = sbc_get_codesize(&sbc_info->sbc);
+ sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc);
+}
+
+static void *init(bool for_encoding, bool for_backchannel, const uint8_t *config_buffer, uint8_t config_size, pa_sample_spec *sample_spec) {
+ struct sbc_info *sbc_info;
+ const a2dp_sbc_t *config = (const a2dp_sbc_t *) config_buffer;
+ int ret;
+
+ pa_assert(config_size == sizeof(*config));
+ pa_assert(!for_backchannel);
+
+ sbc_info = pa_xnew0(struct sbc_info, 1);
+
+ ret = sbc_init(&sbc_info->sbc, 0);
+ if (ret != 0) {
+ pa_xfree(sbc_info);
+ pa_log_error("SBC initialization failed: %d", ret);
+ return NULL;
+ }
+
+ sample_spec->format = PA_SAMPLE_S16LE;
+
+ switch (config->frequency) {
+ case SBC_SAMPLING_FREQ_16000:
+ sbc_info->frequency = SBC_FREQ_16000;
+ sample_spec->rate = 16000U;
+ break;
+ case SBC_SAMPLING_FREQ_32000:
+ sbc_info->frequency = SBC_FREQ_32000;
+ sample_spec->rate = 32000U;
+ break;
+ case SBC_SAMPLING_FREQ_44100:
+ sbc_info->frequency = SBC_FREQ_44100;
+ sample_spec->rate = 44100U;
+ break;
+ case SBC_SAMPLING_FREQ_48000:
+ sbc_info->frequency = SBC_FREQ_48000;
+ sample_spec->rate = 48000U;
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+
+ switch (config->channel_mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ sbc_info->mode = SBC_MODE_MONO;
+ sample_spec->channels = 1;
+ break;
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ sbc_info->mode = SBC_MODE_DUAL_CHANNEL;
+ sample_spec->channels = 2;
+ break;
+ case SBC_CHANNEL_MODE_STEREO:
+ sbc_info->mode = SBC_MODE_STEREO;
+ sample_spec->channels = 2;
+ break;
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ sbc_info->mode = SBC_MODE_JOINT_STEREO;
+ sample_spec->channels = 2;
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+
+ switch (config->allocation_method) {
+ case SBC_ALLOCATION_SNR:
+ sbc_info->allocation = SBC_AM_SNR;
+ break;
+ case SBC_ALLOCATION_LOUDNESS:
+ sbc_info->allocation = SBC_AM_LOUDNESS;
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+
+ switch (config->subbands) {
+ case SBC_SUBBANDS_4:
+ sbc_info->subbands = SBC_SB_4;
+ break;
+ case SBC_SUBBANDS_8:
+ sbc_info->subbands = SBC_SB_8;
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+
+ switch (config->block_length) {
+ case SBC_BLOCK_LENGTH_4:
+ sbc_info->blocks = SBC_BLK_4;
+ break;
+ case SBC_BLOCK_LENGTH_8:
+ sbc_info->blocks = SBC_BLK_8;
+ break;
+ case SBC_BLOCK_LENGTH_12:
+ sbc_info->blocks = SBC_BLK_12;
+ break;
+ case SBC_BLOCK_LENGTH_16:
+ sbc_info->blocks = SBC_BLK_16;
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+
+ sbc_info->min_bitpool = config->min_bitpool;
+ sbc_info->max_bitpool = config->max_bitpool;
+
+ /* Set minimum bitpool for source to get the maximum possible block_size
+ * in get_block_size() function. This block_size is length of buffer used
+ * for decoded audio data and so is inversely proportional to frame length
+ * which depends on bitpool value. Bitpool is controlled by other side from
+ * range [min_bitpool, max_bitpool]. */
+ sbc_info->initial_bitpool = for_encoding ? sbc_info->max_bitpool : sbc_info->min_bitpool;
+
+ set_params(sbc_info);
+
+ pa_log_info("SBC parameters: allocation=%s, subbands=%u, blocks=%u, mode=%s bitpool=%u codesize=%u frame_length=%u",
+ sbc_info->sbc.allocation ? "SNR" : "Loudness", sbc_info->sbc.subbands ? 8 : 4,
+ (sbc_info->sbc.blocks+1)*4, sbc_info->sbc.mode == SBC_MODE_MONO ? "Mono" :
+ sbc_info->sbc.mode == SBC_MODE_DUAL_CHANNEL ? "DualChannel" :
+ sbc_info->sbc.mode == SBC_MODE_STEREO ? "Stereo" : "JointStereo",
+ sbc_info->sbc.bitpool, (unsigned)sbc_info->codesize, (unsigned)sbc_info->frame_length);
+
+ return sbc_info;
+}
+
+static void deinit(void *codec_info) {
+ struct sbc_info *sbc_info = (struct sbc_info *) codec_info;
+
+ sbc_finish(&sbc_info->sbc);
+ pa_xfree(sbc_info);
+}
+
+static void set_bitpool(struct sbc_info *sbc_info, uint8_t bitpool) {
+ if (bitpool > sbc_info->max_bitpool)
+ bitpool = sbc_info->max_bitpool;
+ else if (bitpool < sbc_info->min_bitpool)
+ bitpool = sbc_info->min_bitpool;
+
+ sbc_info->sbc.bitpool = bitpool;
+
+ sbc_info->codesize = sbc_get_codesize(&sbc_info->sbc);
+ sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc);
+
+ pa_log_debug("Bitpool has changed to %u", sbc_info->sbc.bitpool);
+}
+
+static int reset(void *codec_info) {
+ struct sbc_info *sbc_info = (struct sbc_info *) codec_info;
+ int ret;
+
+ ret = sbc_reinit(&sbc_info->sbc, 0);
+ if (ret != 0) {
+ pa_log_error("SBC reinitialization failed: %d", ret);
+ return -1;
+ }
+
+ /* sbc_reinit() sets also default parameters, so reset them back */
+ set_params(sbc_info);
+
+ sbc_info->seq_num = 0;
+ return 0;
+}
+
+static size_t get_block_size(void *codec_info, size_t link_mtu) {
+ struct sbc_info *sbc_info = (struct sbc_info *) codec_info;
+ size_t rtp_size = sizeof(struct rtp_header) + sizeof(struct rtp_sbc_payload);
+ size_t frame_count = (link_mtu - rtp_size) / sbc_info->frame_length;
+
+ /* frame_count is only 4 bit number */
+ if (frame_count > 15)
+ frame_count = 15;
+
+ return frame_count * sbc_info->codesize;
+}
+
+static size_t reduce_encoder_bitrate(void *codec_info, size_t write_link_mtu) {
+ struct sbc_info *sbc_info = (struct sbc_info *) codec_info;
+ uint8_t bitpool;
+
+ /* Check if bitpool is already at its limit */
+ if (sbc_info->sbc.bitpool <= SBC_BITPOOL_DEC_LIMIT)
+ return 0;
+
+ bitpool = sbc_info->sbc.bitpool - SBC_BITPOOL_DEC_STEP;
+
+ if (bitpool < SBC_BITPOOL_DEC_LIMIT)
+ bitpool = SBC_BITPOOL_DEC_LIMIT;
+
+ if (sbc_info->sbc.bitpool == bitpool)
+ return 0;
+
+ set_bitpool(sbc_info, bitpool);
+ return get_block_size(codec_info, write_link_mtu);
+}
+
+static size_t encode_buffer(void *codec_info, uint32_t timestamp, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed) {
+ struct sbc_info *sbc_info = (struct sbc_info *) codec_info;
+ struct rtp_header *header;
+ struct rtp_sbc_payload *payload;
+ uint8_t *d;
+ const uint8_t *p;
+ size_t to_write, to_encode;
+ uint8_t frame_count;
+
+ header = (struct rtp_header*) output_buffer;
+ payload = (struct rtp_sbc_payload*) (output_buffer + sizeof(*header));
+
+ frame_count = 0;
+
+ p = input_buffer;
+ to_encode = input_size;
+
+ d = output_buffer + sizeof(*header) + sizeof(*payload);
+ to_write = output_size - sizeof(*header) - sizeof(*payload);
+
+ /* frame_count is only 4 bit number */
+ while (PA_LIKELY(to_encode > 0 && to_write > 0 && frame_count < 15)) {
+ ssize_t written;
+ ssize_t encoded;
+
+ encoded = sbc_encode(&sbc_info->sbc,
+ p, to_encode,
+ d, to_write,
+ &written);
+
+ if (PA_UNLIKELY(encoded <= 0)) {
+ pa_log_error("SBC encoding error (%li)", (long) encoded);
+ break;
+ }
+
+ if (PA_UNLIKELY(written < 0)) {
+ pa_log_error("SBC encoding error (%li)", (long) written);
+ break;
+ }
+
+ pa_assert_fp((size_t) encoded <= to_encode);
+ pa_assert_fp((size_t) encoded == sbc_info->codesize);
+
+ pa_assert_fp((size_t) written <= to_write);
+ pa_assert_fp((size_t) written == sbc_info->frame_length);
+
+ p += encoded;
+ to_encode -= encoded;
+
+ d += written;
+ to_write -= written;
+
+ frame_count++;
+ }
+
+ PA_ONCE_BEGIN {
+ pa_log_debug("Using SBC codec implementation: %s", pa_strnull(sbc_get_implementation_info(&sbc_info->sbc)));
+ } PA_ONCE_END;
+
+ if (PA_UNLIKELY(frame_count == 0)) {
+ *processed = 0;
+ return 0;
+ }
+
+ /* write it to the fifo */
+ pa_memzero(output_buffer, sizeof(*header) + sizeof(*payload));
+ header->v = 2;
+
+ /* A2DP spec: "A payload type in the RTP dynamic range shall be chosen".
+ * RFC3551 defines the dynamic range to span from 96 to 127, and 96 appears
+ * to be the most common choice in A2DP implementations. */
+ header->pt = 96;
+
+ header->sequence_number = htons(sbc_info->seq_num++);
+ header->timestamp = htonl(timestamp);
+ header->ssrc = htonl(1);
+ payload->frame_count = frame_count;
+
+ *processed = p - input_buffer;
+ return d - output_buffer;
+}
+
+static size_t decode_buffer(void *codec_info, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed) {
+ struct sbc_info *sbc_info = (struct sbc_info *) codec_info;
+
+ struct rtp_header *header;
+ struct rtp_sbc_payload *payload;
+ const uint8_t *p;
+ uint8_t *d;
+ size_t to_write, to_decode;
+ uint8_t frame_count;
+
+ header = (struct rtp_header *) input_buffer;
+ payload = (struct rtp_sbc_payload*) (input_buffer + sizeof(*header));
+
+ frame_count = payload->frame_count;
+
+ /* TODO: Add support for decoding fragmented SBC frames */
+ if (payload->is_fragmented) {
+ pa_log_error("Unsupported fragmented SBC frame");
+ *processed = 0;
+ return 0;
+ }
+
+ p = input_buffer + sizeof(*header) + sizeof(*payload);
+ to_decode = input_size - sizeof(*header) - sizeof(*payload);
+
+ d = output_buffer;
+ to_write = output_size;
+
+ while (PA_LIKELY(to_decode > 0 && to_write > 0 && frame_count > 0)) {
+ size_t written;
+ ssize_t decoded;
+
+ decoded = sbc_decode(&sbc_info->sbc,
+ p, to_decode,
+ d, to_write,
+ &written);
+
+ if (PA_UNLIKELY(decoded <= 0)) {
+ pa_log_error("SBC decoding error (%li)", (long) decoded);
+ break;
+ }
+
+ /* Reset frame length, it can be changed due to bitpool change */
+ sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc);
+
+ pa_assert_fp((size_t) decoded <= to_decode);
+ pa_assert_fp((size_t) decoded == sbc_info->frame_length);
+
+ pa_assert_fp((size_t) written <= to_write);
+ pa_assert_fp((size_t) written == sbc_info->codesize);
+
+ p += decoded;
+ to_decode -= decoded;
+
+ d += written;
+ to_write -= written;
+
+ frame_count--;
+ }
+
+ *processed = p - input_buffer;
+ return d - output_buffer;
+}
+
+const pa_a2dp_codec pa_a2dp_codec_sbc = {
+ .name = "sbc",
+ .description = "SBC",
+ .id = { A2DP_CODEC_SBC, 0, 0 },
+ .support_backchannel = false,
+ .can_accept_capabilities = can_accept_capabilities,
+ .choose_remote_endpoint = choose_remote_endpoint,
+ .fill_capabilities = fill_capabilities,
+ .is_configuration_valid = is_configuration_valid,
+ .fill_preferred_configuration = fill_preferred_configuration,
+ .init = init,
+ .deinit = deinit,
+ .reset = reset,
+ .get_read_block_size = get_block_size,
+ .get_write_block_size = get_block_size,
+ .reduce_encoder_bitrate = reduce_encoder_bitrate,
+ .encode_buffer = encode_buffer,
+ .decode_buffer = decode_buffer,
+};
diff --git a/src/modules/bluetooth/a2dp-codec-util.c b/src/modules/bluetooth/a2dp-codec-util.c
new file mode 100644
index 0000000..94d01e7
--- /dev/null
+++ b/src/modules/bluetooth/a2dp-codec-util.c
@@ -0,0 +1,56 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 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 <pulsecore/core.h>
+#include <pulsecore/core-util.h>
+
+#include "a2dp-codec-util.h"
+
+extern const pa_a2dp_codec pa_a2dp_codec_sbc;
+
+/* This is list of supported codecs. Their order is important.
+ * Codec with higher index has higher priority. */
+const pa_a2dp_codec *pa_a2dp_codecs[] = {
+ &pa_a2dp_codec_sbc,
+};
+
+unsigned int pa_bluetooth_a2dp_codec_count(void) {
+ return PA_ELEMENTSOF(pa_a2dp_codecs);
+}
+
+const pa_a2dp_codec *pa_bluetooth_a2dp_codec_iter(unsigned int i) {
+ pa_assert(i < pa_bluetooth_a2dp_codec_count());
+ return pa_a2dp_codecs[i];
+}
+
+const pa_a2dp_codec *pa_bluetooth_get_a2dp_codec(const char *name) {
+ unsigned int i;
+ unsigned int count = pa_bluetooth_a2dp_codec_count();
+
+ for (i = 0; i < count; i++) {
+ if (pa_streq(pa_a2dp_codecs[i]->name, name))
+ return pa_a2dp_codecs[i];
+ }
+
+ return NULL;
+}
diff --git a/src/modules/bluetooth/a2dp-codec-util.h b/src/modules/bluetooth/a2dp-codec-util.h
new file mode 100644
index 0000000..86f233a
--- /dev/null
+++ b/src/modules/bluetooth/a2dp-codec-util.h
@@ -0,0 +1,34 @@
+#ifndef fooa2dpcodecutilhfoo
+#define fooa2dpcodecutilhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 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 "a2dp-codec-api.h"
+
+/* Get number of supported A2DP codecs */
+unsigned int pa_bluetooth_a2dp_codec_count(void);
+
+/* Get i-th codec. Codec with higher number has higher priority */
+const pa_a2dp_codec *pa_bluetooth_a2dp_codec_iter(unsigned int i);
+
+/* Get codec by name */
+const pa_a2dp_codec *pa_bluetooth_get_a2dp_codec(const char *name);
+
+#endif
diff --git a/src/modules/bluetooth/a2dp-codecs.h b/src/modules/bluetooth/a2dp-codecs.h
new file mode 100644
index 0000000..93e9d35
--- /dev/null
+++ b/src/modules/bluetooth/a2dp-codecs.h
@@ -0,0 +1,435 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2006-2010 Nokia Corporation
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ * Copyright (C) 2018 Pali Rohár <pali.rohar@gmail.com>
+ *
+ *
+ * This library 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.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include <endian.h>
+#include <stdint.h>
+
+#define A2DP_CODEC_SBC 0x00
+#define A2DP_CODEC_MPEG12 0x01
+#define A2DP_CODEC_MPEG24 0x02
+#define A2DP_CODEC_ATRAC 0x04
+#define A2DP_CODEC_VENDOR 0xFF
+
+#define SBC_SAMPLING_FREQ_16000 (1 << 3)
+#define SBC_SAMPLING_FREQ_32000 (1 << 2)
+#define SBC_SAMPLING_FREQ_44100 (1 << 1)
+#define SBC_SAMPLING_FREQ_48000 1
+
+#define SBC_CHANNEL_MODE_MONO (1 << 3)
+#define SBC_CHANNEL_MODE_DUAL_CHANNEL (1 << 2)
+#define SBC_CHANNEL_MODE_STEREO (1 << 1)
+#define SBC_CHANNEL_MODE_JOINT_STEREO 1
+
+#define SBC_BLOCK_LENGTH_4 (1 << 3)
+#define SBC_BLOCK_LENGTH_8 (1 << 2)
+#define SBC_BLOCK_LENGTH_12 (1 << 1)
+#define SBC_BLOCK_LENGTH_16 1
+
+#define SBC_SUBBANDS_4 (1 << 1)
+#define SBC_SUBBANDS_8 1
+
+#define SBC_ALLOCATION_SNR (1 << 1)
+#define SBC_ALLOCATION_LOUDNESS 1
+
+#define SBC_MIN_BITPOOL 2
+#define SBC_MAX_BITPOOL 250
+
+/* Other settings:
+ * Block length = 16
+ * Allocation method = Loudness
+ * Subbands = 8
+ */
+#define SBC_BITPOOL_MQ_MONO_44100 19
+#define SBC_BITPOOL_MQ_MONO_48000 18
+#define SBC_BITPOOL_MQ_JOINT_STEREO_44100 35
+#define SBC_BITPOOL_MQ_JOINT_STEREO_48000 33
+#define SBC_BITPOOL_HQ_MONO_44100 31
+#define SBC_BITPOOL_HQ_MONO_48000 29
+#define SBC_BITPOOL_HQ_JOINT_STEREO_44100 53
+#define SBC_BITPOOL_HQ_JOINT_STEREO_48000 51
+
+#define MPEG_CHANNEL_MODE_MONO (1 << 3)
+#define MPEG_CHANNEL_MODE_DUAL_CHANNEL (1 << 2)
+#define MPEG_CHANNEL_MODE_STEREO (1 << 1)
+#define MPEG_CHANNEL_MODE_JOINT_STEREO 1
+
+#define MPEG_LAYER_MP1 (1 << 2)
+#define MPEG_LAYER_MP2 (1 << 1)
+#define MPEG_LAYER_MP3 1
+
+#define MPEG_SAMPLING_FREQ_16000 (1 << 5)
+#define MPEG_SAMPLING_FREQ_22050 (1 << 4)
+#define MPEG_SAMPLING_FREQ_24000 (1 << 3)
+#define MPEG_SAMPLING_FREQ_32000 (1 << 2)
+#define MPEG_SAMPLING_FREQ_44100 (1 << 1)
+#define MPEG_SAMPLING_FREQ_48000 1
+
+#define MPEG_BIT_RATE_INDEX_0 (1 << 0)
+#define MPEG_BIT_RATE_INDEX_1 (1 << 1)
+#define MPEG_BIT_RATE_INDEX_2 (1 << 2)
+#define MPEG_BIT_RATE_INDEX_3 (1 << 3)
+#define MPEG_BIT_RATE_INDEX_4 (1 << 4)
+#define MPEG_BIT_RATE_INDEX_5 (1 << 5)
+#define MPEG_BIT_RATE_INDEX_6 (1 << 6)
+#define MPEG_BIT_RATE_INDEX_7 (1 << 7)
+#define MPEG_BIT_RATE_INDEX_8 (1 << 8)
+#define MPEG_BIT_RATE_INDEX_9 (1 << 9)
+#define MPEG_BIT_RATE_INDEX_10 (1 << 10)
+#define MPEG_BIT_RATE_INDEX_11 (1 << 11)
+#define MPEG_BIT_RATE_INDEX_12 (1 << 12)
+#define MPEG_BIT_RATE_INDEX_13 (1 << 13)
+#define MPEG_BIT_RATE_INDEX_14 (1 << 14)
+
+#define MPEG_MP1_BIT_RATE_32000 MPEG_BIT_RATE_INDEX_1
+#define MPEG_MP1_BIT_RATE_64000 MPEG_BIT_RATE_INDEX_2
+#define MPEG_MP1_BIT_RATE_96000 MPEG_BIT_RATE_INDEX_3
+#define MPEG_MP1_BIT_RATE_128000 MPEG_BIT_RATE_INDEX_4
+#define MPEG_MP1_BIT_RATE_160000 MPEG_BIT_RATE_INDEX_5
+#define MPEG_MP1_BIT_RATE_192000 MPEG_BIT_RATE_INDEX_6
+#define MPEG_MP1_BIT_RATE_224000 MPEG_BIT_RATE_INDEX_7
+#define MPEG_MP1_BIT_RATE_256000 MPEG_BIT_RATE_INDEX_8
+#define MPEG_MP1_BIT_RATE_288000 MPEG_BIT_RATE_INDEX_9
+#define MPEG_MP1_BIT_RATE_320000 MPEG_BIT_RATE_INDEX_10
+#define MPEG_MP1_BIT_RATE_352000 MPEG_BIT_RATE_INDEX_11
+#define MPEG_MP1_BIT_RATE_384000 MPEG_BIT_RATE_INDEX_12
+#define MPEG_MP1_BIT_RATE_416000 MPEG_BIT_RATE_INDEX_13
+#define MPEG_MP1_BIT_RATE_448000 MPEG_BIT_RATE_INDEX_14
+
+#define MPEG_MP2_BIT_RATE_32000 MPEG_BIT_RATE_INDEX_1
+#define MPEG_MP2_BIT_RATE_48000 MPEG_BIT_RATE_INDEX_2
+#define MPEG_MP2_BIT_RATE_56000 MPEG_BIT_RATE_INDEX_3
+#define MPEG_MP2_BIT_RATE_64000 MPEG_BIT_RATE_INDEX_4
+#define MPEG_MP2_BIT_RATE_80000 MPEG_BIT_RATE_INDEX_5
+#define MPEG_MP2_BIT_RATE_96000 MPEG_BIT_RATE_INDEX_6
+#define MPEG_MP2_BIT_RATE_112000 MPEG_BIT_RATE_INDEX_7
+#define MPEG_MP2_BIT_RATE_128000 MPEG_BIT_RATE_INDEX_8
+#define MPEG_MP2_BIT_RATE_160000 MPEG_BIT_RATE_INDEX_9
+#define MPEG_MP2_BIT_RATE_192000 MPEG_BIT_RATE_INDEX_10
+#define MPEG_MP2_BIT_RATE_224000 MPEG_BIT_RATE_INDEX_11
+#define MPEG_MP2_BIT_RATE_256000 MPEG_BIT_RATE_INDEX_12
+#define MPEG_MP2_BIT_RATE_320000 MPEG_BIT_RATE_INDEX_13
+#define MPEG_MP2_BIT_RATE_384000 MPEG_BIT_RATE_INDEX_14
+
+#define MPEG_MP3_BIT_RATE_32000 MPEG_BIT_RATE_INDEX_1
+#define MPEG_MP3_BIT_RATE_40000 MPEG_BIT_RATE_INDEX_2
+#define MPEG_MP3_BIT_RATE_48000 MPEG_BIT_RATE_INDEX_3
+#define MPEG_MP3_BIT_RATE_56000 MPEG_BIT_RATE_INDEX_4
+#define MPEG_MP3_BIT_RATE_64000 MPEG_BIT_RATE_INDEX_5
+#define MPEG_MP3_BIT_RATE_80000 MPEG_BIT_RATE_INDEX_6
+#define MPEG_MP3_BIT_RATE_96000 MPEG_BIT_RATE_INDEX_7
+#define MPEG_MP3_BIT_RATE_112000 MPEG_BIT_RATE_INDEX_8
+#define MPEG_MP3_BIT_RATE_128000 MPEG_BIT_RATE_INDEX_9
+#define MPEG_MP3_BIT_RATE_160000 MPEG_BIT_RATE_INDEX_10
+#define MPEG_MP3_BIT_RATE_192000 MPEG_BIT_RATE_INDEX_11
+#define MPEG_MP3_BIT_RATE_224000 MPEG_BIT_RATE_INDEX_12
+#define MPEG_MP3_BIT_RATE_256000 MPEG_BIT_RATE_INDEX_13
+#define MPEG_MP3_BIT_RATE_320000 MPEG_BIT_RATE_INDEX_14
+
+#define MPEG_BIT_RATE_FREE MPEG_BIT_RATE_INDEX_0
+
+#define MPEG_GET_BITRATE(a) ((uint16_t)(a).bitrate1 << 8 | (a).bitrate2)
+#define MPEG_SET_BITRATE(a, b) \
+ do { \
+ (a).bitrate1 = ((b) >> 8) & 0x7f; \
+ (a).bitrate2 = (b) & 0xff; \
+ } while (0)
+
+#define AAC_OBJECT_TYPE_MPEG2_AAC_LC 0x80
+#define AAC_OBJECT_TYPE_MPEG4_AAC_LC 0x40
+#define AAC_OBJECT_TYPE_MPEG4_AAC_LTP 0x20
+#define AAC_OBJECT_TYPE_MPEG4_AAC_SCA 0x10
+
+#define AAC_SAMPLING_FREQ_8000 0x0800
+#define AAC_SAMPLING_FREQ_11025 0x0400
+#define AAC_SAMPLING_FREQ_12000 0x0200
+#define AAC_SAMPLING_FREQ_16000 0x0100
+#define AAC_SAMPLING_FREQ_22050 0x0080
+#define AAC_SAMPLING_FREQ_24000 0x0040
+#define AAC_SAMPLING_FREQ_32000 0x0020
+#define AAC_SAMPLING_FREQ_44100 0x0010
+#define AAC_SAMPLING_FREQ_48000 0x0008
+#define AAC_SAMPLING_FREQ_64000 0x0004
+#define AAC_SAMPLING_FREQ_88200 0x0002
+#define AAC_SAMPLING_FREQ_96000 0x0001
+
+#define AAC_CHANNELS_1 0x02
+#define AAC_CHANNELS_2 0x01
+
+#define AAC_GET_BITRATE(a) ((a).bitrate1 << 16 | \
+ (a).bitrate2 << 8 | (a).bitrate3)
+#define AAC_GET_FREQUENCY(a) ((a).frequency1 << 4 | (a).frequency2)
+
+#define AAC_SET_BITRATE(a, b) \
+ do { \
+ (a).bitrate1 = (b >> 16) & 0x7f; \
+ (a).bitrate2 = (b >> 8) & 0xff; \
+ (a).bitrate3 = b & 0xff; \
+ } while (0)
+#define AAC_SET_FREQUENCY(a, f) \
+ do { \
+ (a).frequency1 = (f >> 4) & 0xff; \
+ (a).frequency2 = f & 0x0f; \
+ } while (0)
+
+#define AAC_INIT_BITRATE(b) \
+ .bitrate1 = (b >> 16) & 0x7f, \
+ .bitrate2 = (b >> 8) & 0xff, \
+ .bitrate3 = b & 0xff,
+#define AAC_INIT_FREQUENCY(f) \
+ .frequency1 = (f >> 4) & 0xff, \
+ .frequency2 = f & 0x0f,
+
+#define APTX_VENDOR_ID 0x0000004f
+#define APTX_CODEC_ID 0x0001
+
+#define APTX_CHANNEL_MODE_MONO 0x01
+#define APTX_CHANNEL_MODE_STEREO 0x02
+
+#define APTX_SAMPLING_FREQ_16000 0x08
+#define APTX_SAMPLING_FREQ_32000 0x04
+#define APTX_SAMPLING_FREQ_44100 0x02
+#define APTX_SAMPLING_FREQ_48000 0x01
+
+#define FASTSTREAM_VENDOR_ID 0x0000000a
+#define FASTSTREAM_CODEC_ID 0x0001
+
+#define FASTSTREAM_DIRECTION_SINK 0x1
+#define FASTSTREAM_DIRECTION_SOURCE 0x2
+
+#define FASTSTREAM_SINK_SAMPLING_FREQ_44100 0x2
+#define FASTSTREAM_SINK_SAMPLING_FREQ_48000 0x1
+
+#define FASTSTREAM_SOURCE_SAMPLING_FREQ_16000 0x2
+
+#define APTX_LL_VENDOR_ID 0x0000000a
+#define APTX_LL_CODEC_ID 0x0002
+
+/* Default parameters for aptX Low Latency encoder */
+
+/* Target codec buffer level = 180 */
+#define APTX_LL_TARGET_LEVEL2 0xb4
+#define APTX_LL_TARGET_LEVEL1 0x00
+
+/* Initial codec buffer level = 360 */
+#define APTX_LL_INITIAL_LEVEL2 0x68
+#define APTX_LL_INITIAL_LEVEL1 0x01
+
+/* SRA max rate 0.005 * 10000 = 50 */
+#define APTX_LL_SRA_MAX_RATE 0x32
+
+/* SRA averaging time = 1s */
+#define APTX_LL_SRA_AVG_TIME 0x01
+
+/* Good working codec buffer level = 180 */
+#define APTX_LL_GOOD_WORKING_LEVEL2 0xB4
+#define APTX_LL_GOOD_WORKING_LEVEL1 0x00
+
+#define APTX_HD_VENDOR_ID 0x000000D7
+#define APTX_HD_CODEC_ID 0x0024
+
+#define LDAC_VENDOR_ID 0x0000012d
+#define LDAC_CODEC_ID 0x00aa
+
+#define LDAC_SAMPLING_FREQ_44100 0x20
+#define LDAC_SAMPLING_FREQ_48000 0x10
+#define LDAC_SAMPLING_FREQ_88200 0x08
+#define LDAC_SAMPLING_FREQ_96000 0x04
+#define LDAC_SAMPLING_FREQ_176400 0x02
+#define LDAC_SAMPLING_FREQ_192000 0x01
+
+#define LDAC_CHANNEL_MODE_MONO 0x04
+#define LDAC_CHANNEL_MODE_DUAL 0x02
+#define LDAC_CHANNEL_MODE_STEREO 0x01
+
+typedef struct {
+ uint8_t vendor_id4;
+ uint8_t vendor_id3;
+ uint8_t vendor_id2;
+ uint8_t vendor_id1;
+ uint8_t codec_id2;
+ uint8_t codec_id1;
+} __attribute__ ((packed)) a2dp_vendor_codec_t;
+
+#define A2DP_GET_VENDOR_ID(a) ( \
+ (((uint32_t)(a).vendor_id4) << 0) | \
+ (((uint32_t)(a).vendor_id3) << 8) | \
+ (((uint32_t)(a).vendor_id2) << 16) | \
+ (((uint32_t)(a).vendor_id1) << 24) \
+ )
+#define A2DP_GET_CODEC_ID(a) ((a).codec_id2 | (((uint16_t)(a).codec_id1) << 8))
+#define A2DP_SET_VENDOR_ID_CODEC_ID(v, c) ((a2dp_vendor_codec_t){ \
+ .vendor_id4 = (((v) >> 0) & 0xff), \
+ .vendor_id3 = (((v) >> 8) & 0xff), \
+ .vendor_id2 = (((v) >> 16) & 0xff), \
+ .vendor_id1 = (((v) >> 24) & 0xff), \
+ .codec_id2 = (((c) >> 0) & 0xff), \
+ .codec_id1 = (((c) >> 8) & 0xff), \
+ })
+
+typedef struct {
+ uint8_t reserved;
+ uint8_t target_level2;
+ uint8_t target_level1;
+ uint8_t initial_level2;
+ uint8_t initial_level1;
+ uint8_t sra_max_rate;
+ uint8_t sra_avg_time;
+ uint8_t good_working_level2;
+ uint8_t good_working_level1;
+} __attribute__ ((packed)) a2dp_aptx_ll_new_caps_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency;
+ uint8_t channel_mode;
+} __attribute__ ((packed)) a2dp_ldac_t;
+
+#if defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && \
+ __BYTE_ORDER == __LITTLE_ENDIAN
+
+typedef struct {
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+ uint8_t allocation_method:2;
+ uint8_t subbands:2;
+ uint8_t block_length:4;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+} __attribute__ ((packed)) a2dp_sbc_t;
+
+typedef struct {
+ uint8_t channel_mode:4;
+ uint8_t crc:1;
+ uint8_t layer:3;
+ uint8_t frequency:6;
+ uint8_t mpf:1;
+ uint8_t rfa:1;
+ uint8_t bitrate1:7;
+ uint8_t vbr:1;
+ uint8_t bitrate2;
+} __attribute__ ((packed)) a2dp_mpeg_t;
+
+typedef struct {
+ uint8_t object_type;
+ uint8_t frequency1;
+ uint8_t rfa:2;
+ uint8_t channels:2;
+ uint8_t frequency2:4;
+ uint8_t bitrate1:7;
+ uint8_t vbr:1;
+ uint8_t bitrate2;
+ uint8_t bitrate3;
+} __attribute__ ((packed)) a2dp_aac_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+} __attribute__ ((packed)) a2dp_aptx_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t direction;
+ uint8_t sink_frequency:4;
+ uint8_t source_frequency:4;
+} __attribute__ ((packed)) a2dp_faststream_t;
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t bidirect_link:1;
+ uint8_t has_new_caps:1;
+ uint8_t reserved:6;
+ a2dp_aptx_ll_new_caps_t new_caps[0];
+} __attribute__ ((packed)) a2dp_aptx_ll_t;
+
+#elif defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && \
+ __BYTE_ORDER == __BIG_ENDIAN
+
+typedef struct {
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+ uint8_t block_length:4;
+ uint8_t subbands:2;
+ uint8_t allocation_method:2;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+} __attribute__ ((packed)) a2dp_sbc_t;
+
+typedef struct {
+ uint8_t layer:3;
+ uint8_t crc:1;
+ uint8_t channel_mode:4;
+ uint8_t rfa:1;
+ uint8_t mpf:1;
+ uint8_t frequency:6;
+ uint8_t vbr:1;
+ uint8_t bitrate1:7;
+ uint8_t bitrate2;
+} __attribute__ ((packed)) a2dp_mpeg_t;
+
+typedef struct {
+ uint8_t object_type;
+ uint8_t frequency1;
+ uint8_t frequency2:4;
+ uint8_t channels:2;
+ uint8_t rfa:2;
+ uint8_t vbr:1;
+ uint8_t bitrate1:7;
+ uint8_t bitrate2;
+ uint8_t bitrate3;
+} __attribute__ ((packed)) a2dp_aac_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+} __attribute__ ((packed)) a2dp_aptx_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t direction;
+ uint8_t source_frequency:4;
+ uint8_t sink_frequency:4;
+} __attribute__ ((packed)) a2dp_faststream_t;
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t reserved:6;
+ uint8_t has_new_caps:1;
+ uint8_t bidirect_link:1;
+ a2dp_aptx_ll_new_caps_t new_caps[0];
+} __attribute__ ((packed)) a2dp_aptx_ll_t;
+
+#else
+#error "Unknown byte order"
+#endif
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t reserved0;
+ uint8_t reserved1;
+ uint8_t reserved2;
+ uint8_t reserved3;
+} __attribute__ ((packed)) a2dp_aptx_hd_t;
diff --git a/src/modules/bluetooth/backend-native.c b/src/modules/bluetooth/backend-native.c
new file mode 100644
index 0000000..5ba7439
--- /dev/null
+++ b/src/modules/bluetooth/backend-native.c
@@ -0,0 +1,714 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2014 Wim Taymans <wim.taymans at 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 <pulsecore/shared.h>
+#include <pulsecore/core-error.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/dbus-shared.h>
+#include <pulsecore/log.h>
+
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sco.h>
+
+#include "bluez5-util.h"
+
+struct pa_bluetooth_backend {
+ pa_core *core;
+ pa_dbus_connection *connection;
+ pa_bluetooth_discovery *discovery;
+ bool enable_hs_role;
+
+ PA_LLIST_HEAD(pa_dbus_pending, pending);
+};
+
+struct transport_data {
+ int rfcomm_fd;
+ pa_io_event *rfcomm_io;
+ int sco_fd;
+ pa_io_event *sco_io;
+ pa_mainloop_api *mainloop;
+};
+
+#define BLUEZ_SERVICE "org.bluez"
+#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1"
+
+#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported"
+
+#define BLUEZ_PROFILE_MANAGER_INTERFACE BLUEZ_SERVICE ".ProfileManager1"
+#define BLUEZ_PROFILE_INTERFACE BLUEZ_SERVICE ".Profile1"
+
+#define HSP_AG_PROFILE "/Profile/HSPAGProfile"
+#define HSP_HS_PROFILE "/Profile/HSPHSProfile"
+
+/* RFCOMM channel for HSP headset role
+ * The choice seems to be a bit arbitrary -- it looks like at least channels 2, 4 and 5 also work*/
+#define HSP_HS_DEFAULT_CHANNEL 3
+
+#define PROFILE_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" BLUEZ_PROFILE_INTERFACE "\">" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " <method name=\"RequestDisconnection\">" \
+ " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \
+ " </method>" \
+ " <method name=\"NewConnection\">" \
+ " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \
+ " <arg name=\"opts\" direction=\"in\" type=\"a{sv}\"/>" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+static pa_dbus_pending* send_and_add_to_pending(pa_bluetooth_backend *backend, DBusMessage *m,
+ DBusPendingCallNotifyFunction func, void *call_data) {
+
+ pa_dbus_pending *p;
+ DBusPendingCall *call;
+
+ pa_assert(backend);
+ pa_assert(m);
+
+ pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(backend->connection), m, &call, -1));
+
+ p = pa_dbus_pending_new(pa_dbus_connection_get(backend->connection), m, call, backend, call_data);
+ PA_LLIST_PREPEND(pa_dbus_pending, backend->pending, p);
+ dbus_pending_call_set_notify(call, func, p, NULL);
+
+ return p;
+}
+
+static int sco_do_connect(pa_bluetooth_transport *t) {
+ pa_bluetooth_device *d = t->device;
+ struct sockaddr_sco addr;
+ socklen_t len;
+ int err, i;
+ int sock;
+ bdaddr_t src;
+ bdaddr_t dst;
+ const char *src_addr, *dst_addr;
+
+ src_addr = d->adapter->address;
+ dst_addr = d->address;
+
+ /* don't use ba2str to avoid -lbluetooth */
+ for (i = 5; i >= 0; i--, src_addr += 3)
+ src.b[i] = strtol(src_addr, NULL, 16);
+ for (i = 5; i >= 0; i--, dst_addr += 3)
+ dst.b[i] = strtol(dst_addr, NULL, 16);
+
+ sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);
+ if (sock < 0) {
+ pa_log_error("socket(SEQPACKET, SCO) %s", pa_cstrerror(errno));
+ return -1;
+ }
+
+ len = sizeof(addr);
+ memset(&addr, 0, len);
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &src);
+
+ if (bind(sock, (struct sockaddr *) &addr, len) < 0) {
+ pa_log_error("bind(): %s", pa_cstrerror(errno));
+ goto fail_close;
+ }
+
+ memset(&addr, 0, len);
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &dst);
+
+ pa_log_info("doing connect");
+ err = connect(sock, (struct sockaddr *) &addr, len);
+ if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) {
+ pa_log_error("connect(): %s", pa_cstrerror(errno));
+ goto fail_close;
+ }
+ return sock;
+
+fail_close:
+ close(sock);
+ return -1;
+}
+
+static int sco_do_accept(pa_bluetooth_transport *t) {
+ struct transport_data *trd = t->userdata;
+ struct sockaddr_sco addr;
+ socklen_t optlen;
+ int sock;
+
+ memset(&addr, 0, sizeof(addr));
+ optlen = sizeof(addr);
+
+ pa_log_info ("doing accept");
+ sock = accept(trd->sco_fd, (struct sockaddr *) &addr, &optlen);
+ if (sock < 0) {
+ if (errno != EAGAIN)
+ pa_log_error("accept(): %s", pa_cstrerror(errno));
+ goto fail;
+ }
+ return sock;
+
+fail:
+ return -1;
+}
+
+static int sco_acquire_cb(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) {
+ int sock;
+ socklen_t len;
+
+ if (optional)
+ sock = sco_do_accept(t);
+ else
+ sock = sco_do_connect(t);
+
+ if (sock < 0)
+ goto fail;
+
+ if (imtu) *imtu = 48;
+ if (omtu) *omtu = 48;
+
+ if (t->device->autodetect_mtu) {
+ struct sco_options sco_opt;
+
+ len = sizeof(sco_opt);
+ memset(&sco_opt, 0, len);
+
+ if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0)
+ pa_log_warn("getsockopt(SCO_OPTIONS) failed, loading defaults");
+ else {
+ pa_log_debug("autodetected imtu = omtu = %u", sco_opt.mtu);
+ if (imtu) *imtu = sco_opt.mtu;
+ if (omtu) *omtu = sco_opt.mtu;
+ }
+ }
+
+ return sock;
+
+fail:
+ return -1;
+}
+
+static void sco_release_cb(pa_bluetooth_transport *t) {
+ pa_log_info("Transport %s released", t->path);
+ /* device will close the SCO socket for us */
+}
+
+static void sco_io_callback(pa_mainloop_api *io, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) {
+ pa_bluetooth_transport *t = userdata;
+
+ pa_assert(io);
+ pa_assert(t);
+
+ if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) {
+ pa_log_error("error listening SCO connection: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+
+ if (t->state != PA_BLUETOOTH_TRANSPORT_STATE_PLAYING) {
+ pa_log_info("SCO incoming connection: changing state to PLAYING");
+ pa_bluetooth_transport_set_state (t, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING);
+ }
+
+fail:
+ return;
+}
+
+static int sco_listen(pa_bluetooth_transport *t) {
+ struct transport_data *trd = t->userdata;
+ struct sockaddr_sco addr;
+ int sock, i;
+ bdaddr_t src;
+ const char *src_addr;
+
+ sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, BTPROTO_SCO);
+ if (sock < 0) {
+ pa_log_error("socket(SEQPACKET, SCO) %s", pa_cstrerror(errno));
+ return -1;
+ }
+
+ src_addr = t->device->adapter->address;
+
+ /* don't use ba2str to avoid -lbluetooth */
+ for (i = 5; i >= 0; i--, src_addr += 3)
+ src.b[i] = strtol(src_addr, NULL, 16);
+
+ /* Bind to local address */
+ memset(&addr, 0, sizeof(addr));
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &src);
+
+ if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+ pa_log_error("bind(): %s", pa_cstrerror(errno));
+ goto fail_close;
+ }
+
+ pa_log_info ("doing listen");
+ if (listen(sock, 1) < 0) {
+ pa_log_error("listen(): %s", pa_cstrerror(errno));
+ goto fail_close;
+ }
+
+ trd->sco_fd = sock;
+ trd->sco_io = trd->mainloop->io_new(trd->mainloop, sock, PA_IO_EVENT_INPUT,
+ sco_io_callback, t);
+
+ return sock;
+
+fail_close:
+ close(sock);
+ return -1;
+}
+
+static void register_profile_reply(DBusPendingCall *pending, void *userdata) {
+ DBusMessage *r;
+ pa_dbus_pending *p;
+ pa_bluetooth_backend *b;
+ char *profile;
+
+ pa_assert(pending);
+ pa_assert_se(p = userdata);
+ pa_assert_se(b = p->context_data);
+ pa_assert_se(profile = 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 profile %s because it is disabled in BlueZ", profile);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ pa_log_error(BLUEZ_PROFILE_MANAGER_INTERFACE ".RegisterProfile() 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, b->pending, p);
+ pa_dbus_pending_free(p);
+
+ pa_xfree(profile);
+}
+
+static void register_profile(pa_bluetooth_backend *b, const char *profile, const char *uuid) {
+ DBusMessage *m;
+ DBusMessageIter i, d;
+ dbus_bool_t autoconnect;
+ dbus_uint16_t version, chan;
+
+ pa_log_debug("Registering Profile %s %s", profile, uuid);
+
+ pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile"));
+
+ dbus_message_iter_init_append(m, &i);
+ pa_assert_se(dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &profile));
+ pa_assert_se(dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &uuid));
+ 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);
+ if (pa_bluetooth_uuid_is_hsp_hs(uuid)) {
+ /* In the headset role, the connection will only be initiated from the remote side */
+ autoconnect = 0;
+ pa_dbus_append_basic_variant_dict_entry(&d, "AutoConnect", DBUS_TYPE_BOOLEAN, &autoconnect);
+ chan = HSP_HS_DEFAULT_CHANNEL;
+ pa_dbus_append_basic_variant_dict_entry(&d, "Channel", DBUS_TYPE_UINT16, &chan);
+ /* HSP version 1.2 */
+ version = 0x0102;
+ pa_dbus_append_basic_variant_dict_entry(&d, "Version", DBUS_TYPE_UINT16, &version);
+ }
+ dbus_message_iter_close_container(&i, &d);
+
+ send_and_add_to_pending(b, m, register_profile_reply, pa_xstrdup(profile));
+}
+
+static void rfcomm_io_callback(pa_mainloop_api *io, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) {
+ pa_bluetooth_transport *t = userdata;
+
+ pa_assert(io);
+ pa_assert(t);
+
+ if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) {
+ pa_log_info("Lost RFCOMM connection.");
+ goto fail;
+ }
+
+ if (events & PA_IO_EVENT_INPUT) {
+ char buf[512];
+ ssize_t len;
+ int gain, dummy;
+ bool do_reply = false;
+
+ len = pa_read(fd, buf, 511, NULL);
+ if (len < 0) {
+ pa_log_error("RFCOMM read error: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+ buf[len] = 0;
+ pa_log_debug("RFCOMM << %s", buf);
+
+ /* There are only four HSP AT commands:
+ * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain.
+ * +VGS=value is sent by AG to HS as a response to an AT+VGS command or when the gain
+ * is changed on the AG side.
+ * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain.
+ * +VGM=value is sent by AG to HS as a response to an AT+VGM command or when the gain
+ * is changed on the AG side.
+ * AT+CKPD=200: Sent by HS when headset button is pressed.
+ * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because
+ * it does not expect a reply. */
+ if (sscanf(buf, "AT+VGS=%d", &gain) == 1 || sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) {
+ t->speaker_gain = gain;
+ pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED), t);
+ do_reply = true;
+
+ } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1 || sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) {
+ t->microphone_gain = gain;
+ pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED), t);
+ do_reply = true;
+ } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) {
+ do_reply = true;
+ } else {
+ do_reply = false;
+ }
+
+ if (do_reply) {
+ pa_log_debug("RFCOMM >> OK");
+
+ len = write(fd, "\r\nOK\r\n", 6);
+
+ /* we ignore any errors, it's not critical and real errors should
+ * be caught with the HANGUP and ERROR events handled above */
+ if (len < 0)
+ pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno));
+ }
+ }
+
+ return;
+
+fail:
+ pa_bluetooth_transport_unlink(t);
+ pa_bluetooth_transport_free(t);
+}
+
+static void transport_destroy(pa_bluetooth_transport *t) {
+ struct transport_data *trd = t->userdata;
+
+ if (trd->sco_io) {
+ trd->mainloop->io_free(trd->sco_io);
+ shutdown(trd->sco_fd, SHUT_RDWR);
+ close (trd->sco_fd);
+ }
+
+ trd->mainloop->io_free(trd->rfcomm_io);
+ shutdown(trd->rfcomm_fd, SHUT_RDWR);
+ close (trd->rfcomm_fd);
+
+ pa_xfree(trd);
+}
+
+static void set_speaker_gain(pa_bluetooth_transport *t, uint16_t gain) {
+ struct transport_data *trd = t->userdata;
+ char buf[512];
+ ssize_t len, written;
+
+ if (t->speaker_gain == gain)
+ return;
+
+ t->speaker_gain = gain;
+
+ /* If we are in the AG role, we send a command to the head set to change
+ * the speaker gain. In the HS role, source and sink are swapped, so
+ * in this case we notify the AG that the microphone gain has changed */
+ if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) {
+ len = sprintf(buf, "\r\n+VGS=%d\r\n", gain);
+ pa_log_debug("RFCOMM >> +VGS=%d", gain);
+ } else {
+ len = sprintf(buf, "\r\nAT+VGM=%d\r\n", gain);
+ pa_log_debug("RFCOMM >> AT+VGM=%d", gain);
+ }
+
+ written = write(trd->rfcomm_fd, buf, len);
+
+ if (written != len)
+ pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno));
+}
+
+static void set_microphone_gain(pa_bluetooth_transport *t, uint16_t gain) {
+ struct transport_data *trd = t->userdata;
+ char buf[512];
+ ssize_t len, written;
+
+ if (t->microphone_gain == gain)
+ return;
+
+ t->microphone_gain = gain;
+
+ /* If we are in the AG role, we send a command to the head set to change
+ * the microphone gain. In the HS role, source and sink are swapped, so
+ * in this case we notify the AG that the speaker gain has changed */
+ if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) {
+ len = sprintf(buf, "\r\n+VGM=%d\r\n", gain);
+ pa_log_debug("RFCOMM >> +VGM=%d", gain);
+ } else {
+ len = sprintf(buf, "\r\nAT+VGS=%d\r\n", gain);
+ pa_log_debug("RFCOMM >> AT+VGS=%d", gain);
+ }
+
+ written = write (trd->rfcomm_fd, buf, len);
+
+ if (written != len)
+ pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno));
+}
+
+static DBusMessage *profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata) {
+ pa_bluetooth_backend *b = userdata;
+ pa_bluetooth_device *d;
+ pa_bluetooth_transport *t;
+ pa_bluetooth_profile_t p;
+ DBusMessage *r;
+ int fd;
+ const char *sender, *path, PA_UNUSED *handler;
+ DBusMessageIter arg_i;
+ char *pathfd;
+ struct transport_data *trd;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oha{sv}")) {
+ pa_log_error("Invalid signature found in NewConnection");
+ goto fail;
+ }
+
+ handler = dbus_message_get_path(m);
+ if (pa_streq(handler, HSP_AG_PROFILE)) {
+ p = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT;
+ } else if (pa_streq(handler, HSP_HS_PROFILE)) {
+ p = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY;
+ } else {
+ pa_log_error("Invalid handler");
+ goto fail;
+ }
+
+ pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_OBJECT_PATH);
+ dbus_message_iter_get_basic(&arg_i, &path);
+
+ d = pa_bluetooth_discovery_get_device_by_path(b->discovery, path);
+ if (d == NULL) {
+ pa_log_error("Device doesnt exist for %s", path);
+ goto fail;
+ }
+
+ pa_assert_se(dbus_message_iter_next(&arg_i));
+
+ pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_UNIX_FD);
+ dbus_message_iter_get_basic(&arg_i, &fd);
+
+ pa_log_debug("dbus: NewConnection path=%s, fd=%d, profile %s", path, fd,
+ pa_bluetooth_profile_to_string(p));
+
+ sender = dbus_message_get_sender(m);
+
+ pathfd = pa_sprintf_malloc ("%s/fd%d", path, fd);
+ t = pa_bluetooth_transport_new(d, sender, pathfd, p, NULL, 0);
+ pa_xfree(pathfd);
+
+ t->acquire = sco_acquire_cb;
+ t->release = sco_release_cb;
+ t->destroy = transport_destroy;
+ t->set_speaker_gain = set_speaker_gain;
+ t->set_microphone_gain = set_microphone_gain;
+
+ trd = pa_xnew0(struct transport_data, 1);
+ trd->rfcomm_fd = fd;
+ trd->mainloop = b->core->mainloop;
+ trd->rfcomm_io = trd->mainloop->io_new(b->core->mainloop, fd, PA_IO_EVENT_INPUT,
+ rfcomm_io_callback, t);
+ t->userdata = trd;
+
+ sco_listen(t);
+
+ pa_bluetooth_transport_put(t);
+
+ pa_log_debug("Transport %s available for profile %s", t->path, pa_bluetooth_profile_to_string(t->profile));
+
+ 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 handle new connection"));
+ return r;
+}
+
+static DBusMessage *profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata) {
+ DBusMessage *r;
+
+ pa_assert_se(r = dbus_message_new_method_return(m));
+
+ return r;
+}
+
+static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata) {
+ pa_bluetooth_backend *b = userdata;
+ DBusMessage *r = NULL;
+ const char *path, *interface, *member;
+
+ pa_assert(b);
+
+ 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 (!pa_streq(path, HSP_AG_PROFILE) && !pa_streq(path, HSP_HS_PROFILE))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = PROFILE_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_PROFILE_INTERFACE, "Release")) {
+ pa_log_debug("Release not handled");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection")) {
+ r = profile_request_disconnection(c, m, userdata);
+ } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection"))
+ r = profile_new_connection(c, m, userdata);
+ else
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (r) {
+ pa_assert_se(dbus_connection_send(pa_dbus_connection_get(b->connection), r, NULL));
+ dbus_message_unref(r);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void profile_init(pa_bluetooth_backend *b, pa_bluetooth_profile_t profile) {
+ static const DBusObjectPathVTable vtable_profile = {
+ .message_function = profile_handler,
+ };
+ const char *object_name;
+ const char *uuid;
+
+ pa_assert(b);
+
+ switch (profile) {
+ case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT:
+ object_name = HSP_AG_PROFILE;
+ uuid = PA_BLUETOOTH_UUID_HSP_AG;
+ break;
+ case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY:
+ object_name = HSP_HS_PROFILE;
+ uuid = PA_BLUETOOTH_UUID_HSP_HS;
+ break;
+ default:
+ pa_assert_not_reached();
+ break;
+ }
+
+ pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(b->connection), object_name, &vtable_profile, b));
+ register_profile(b, object_name, uuid);
+}
+
+static void profile_done(pa_bluetooth_backend *b, pa_bluetooth_profile_t profile) {
+ pa_assert(b);
+
+ switch (profile) {
+ case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT:
+ dbus_connection_unregister_object_path(pa_dbus_connection_get(b->connection), HSP_AG_PROFILE);
+ break;
+ case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY:
+ dbus_connection_unregister_object_path(pa_dbus_connection_get(b->connection), HSP_HS_PROFILE);
+ break;
+ default:
+ pa_assert_not_reached();
+ break;
+ }
+}
+
+void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *native_backend, bool enable_hs_role) {
+
+ if (enable_hs_role == native_backend->enable_hs_role)
+ return;
+
+ if (enable_hs_role)
+ profile_init(native_backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY);
+ else
+ profile_done(native_backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY);
+
+ native_backend->enable_hs_role = enable_hs_role;
+}
+
+pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role) {
+ pa_bluetooth_backend *backend;
+ DBusError err;
+
+ pa_log_debug("Bluetooth Headset Backend API support using the native backend");
+
+ backend = pa_xnew0(pa_bluetooth_backend, 1);
+ backend->core = c;
+
+ dbus_error_init(&err);
+ if (!(backend->connection = pa_dbus_bus_get(c, DBUS_BUS_SYSTEM, &err))) {
+ pa_log("Failed to get D-Bus connection: %s", err.message);
+ dbus_error_free(&err);
+ pa_xfree(backend);
+ return NULL;
+ }
+
+ backend->discovery = y;
+ backend->enable_hs_role = enable_hs_role;
+
+ if (enable_hs_role)
+ profile_init(backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY);
+ profile_init(backend, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT);
+
+ return backend;
+}
+
+void pa_bluetooth_native_backend_free(pa_bluetooth_backend *backend) {
+ pa_assert(backend);
+
+ pa_dbus_free_pending_list(&backend->pending);
+
+ if (backend->enable_hs_role)
+ profile_done(backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY);
+ profile_done(backend, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT);
+
+ pa_dbus_connection_unref(backend->connection);
+
+ pa_xfree(backend);
+}
diff --git a/src/modules/bluetooth/backend-ofono.c b/src/modules/bluetooth/backend-ofono.c
new file mode 100644
index 0000000..d7a13ef
--- /dev/null
+++ b/src/modules/bluetooth/backend-ofono.c
@@ -0,0 +1,764 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 João Paulo Rechi Vita
+
+ 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 <errno.h>
+#include <poll.h>
+
+#include <pulsecore/core-util.h>
+#include <pulsecore/dbus-shared.h>
+#include <pulsecore/shared.h>
+#include <pulsecore/core-error.h>
+
+#include "bluez5-util.h"
+
+#define HFP_AUDIO_CODEC_CVSD 0x01
+#define HFP_AUDIO_CODEC_MSBC 0x02
+
+#define OFONO_SERVICE "org.ofono"
+#define HF_AUDIO_AGENT_INTERFACE OFONO_SERVICE ".HandsfreeAudioAgent"
+#define HF_AUDIO_MANAGER_INTERFACE OFONO_SERVICE ".HandsfreeAudioManager"
+
+#define HF_AUDIO_AGENT_PATH "/HandsfreeAudioAgent"
+
+#define HF_AUDIO_AGENT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg direction=\"out\" type=\"s\" />" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.ofono.HandsfreeAudioAgent\">" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " <method name=\"NewConnection\">" \
+ " <arg direction=\"in\" type=\"o\" name=\"card_path\" />" \
+ " <arg direction=\"in\" type=\"h\" name=\"sco_fd\" />" \
+ " <arg direction=\"in\" type=\"y\" name=\"codec\" />" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+struct hf_audio_card {
+ pa_bluetooth_backend *backend;
+ char *path;
+ char *remote_address;
+ char *local_address;
+
+ bool connecting;
+ int fd;
+ int (*acquire)(struct hf_audio_card *card);
+
+ pa_bluetooth_transport *transport;
+ pa_hook_slot *device_unlink_slot;
+};
+
+struct pa_bluetooth_backend {
+ pa_core *core;
+ pa_bluetooth_discovery *discovery;
+ pa_dbus_connection *connection;
+ pa_hashmap *cards;
+ char *ofono_bus_id;
+
+ PA_LLIST_HEAD(pa_dbus_pending, pending);
+};
+
+static pa_dbus_pending* hf_dbus_send_and_add_to_pending(pa_bluetooth_backend *backend, DBusMessage *m,
+ DBusPendingCallNotifyFunction func, void *call_data) {
+ pa_dbus_pending *p;
+ DBusPendingCall *call;
+
+ pa_assert(backend);
+ pa_assert(m);
+
+ pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(backend->connection), m, &call, -1));
+
+ p = pa_dbus_pending_new(pa_dbus_connection_get(backend->connection), m, call, backend, call_data);
+ PA_LLIST_PREPEND(pa_dbus_pending, backend->pending, p);
+ dbus_pending_call_set_notify(call, func, p, NULL);
+
+ return p;
+}
+
+static DBusMessage *card_send(struct hf_audio_card *card, const char *method, DBusError *err)
+{
+ pa_bluetooth_transport *t = card->transport;
+ DBusMessage *m, *r;
+
+ pa_assert_se(m = dbus_message_new_method_call(t->owner, t->path, "org.ofono.HandsfreeAudioCard", method));
+ r = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(card->backend->connection), m, -1, err);
+ dbus_message_unref(m);
+
+ return r;
+}
+
+static int card_connect(struct hf_audio_card *card) {
+ DBusMessage *r;
+ DBusError err;
+
+ if (card->connecting)
+ return -EAGAIN;
+
+ card->connecting = true;
+
+ dbus_error_init(&err);
+ r = card_send(card, "Connect", &err);
+
+ if (!r) {
+ pa_log_error("Failed to connect %s: %s", err.name, err.message);
+ card->connecting = false;
+ dbus_error_free(&err);
+ return -1;
+ }
+
+ dbus_message_unref(r);
+
+ if (card->connecting)
+ return -EAGAIN;
+
+ return 0;
+}
+
+static int card_acquire(struct hf_audio_card *card) {
+ int fd;
+ uint8_t codec;
+ DBusMessage *r;
+ DBusError err;
+
+ /* Try acquiring the stream first which was introduced in 1.21 */
+ dbus_error_init(&err);
+ r = card_send(card, "Acquire", &err);
+
+ if (!r) {
+ if (!pa_streq(err.name, DBUS_ERROR_UNKNOWN_METHOD)) {
+ pa_log_error("Failed to acquire %s: %s", err.name, err.message);
+ dbus_error_free(&err);
+ return -1;
+ }
+ dbus_error_free(&err);
+ /* Fallback to Connect as this might be an old version of ofono */
+ card->acquire = card_connect;
+ return card_connect(card);
+ }
+
+ if ((dbus_message_get_args(r, NULL, DBUS_TYPE_UNIX_FD, &fd,
+ DBUS_TYPE_BYTE, &codec,
+ DBUS_TYPE_INVALID) == true)) {
+ dbus_message_unref(r);
+ if (codec != HFP_AUDIO_CODEC_CVSD) {
+ pa_log_error("Invalid codec: %u", codec);
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ return -1;
+ }
+ card->transport->codec = codec;
+ card->fd = fd;
+ return 0;
+ }
+
+ pa_log_error("Unable to acquire");
+ dbus_message_unref(r);
+ return -1;
+}
+
+static void hf_audio_agent_card_removed(pa_bluetooth_backend *backend, const char *path);
+
+static pa_hook_result_t device_unlink_cb(pa_bluetooth_discovery *y, const pa_bluetooth_device *d, struct hf_audio_card *card) {
+ pa_assert(d);
+ pa_assert(card);
+
+ hf_audio_agent_card_removed(card->backend, card->path);
+
+ return PA_HOOK_OK;
+}
+
+static struct hf_audio_card *hf_audio_card_new(pa_bluetooth_backend *backend, const char *path) {
+ struct hf_audio_card *card = pa_xnew0(struct hf_audio_card, 1);
+
+ card->path = pa_xstrdup(path);
+ card->backend = backend;
+ card->fd = -1;
+ card->acquire = card_acquire;
+
+ card->device_unlink_slot = pa_hook_connect(pa_bluetooth_discovery_hook(backend->discovery, PA_BLUETOOTH_HOOK_DEVICE_UNLINK),
+ PA_HOOK_NORMAL, (pa_hook_cb_t) device_unlink_cb, card);
+
+ return card;
+}
+
+static void hf_audio_card_free(struct hf_audio_card *card) {
+ pa_assert(card);
+
+ if (card->device_unlink_slot)
+ pa_hook_slot_free(card->device_unlink_slot);
+
+ if (card->transport)
+ pa_bluetooth_transport_free(card->transport);
+
+ pa_xfree(card->path);
+ pa_xfree(card->remote_address);
+ pa_xfree(card->local_address);
+ pa_xfree(card);
+}
+
+static int socket_accept(int sock)
+{
+ char c;
+ struct pollfd pfd;
+
+ if (sock < 0)
+ return -ENOTCONN;
+
+ memset(&pfd, 0, sizeof(pfd));
+ pfd.fd = sock;
+ pfd.events = POLLOUT;
+
+ if (poll(&pfd, 1, 0) < 0)
+ return -errno;
+
+ /*
+ * If socket already writable then it is not in defer setup state,
+ * otherwise it needs to be read to authorize the connection.
+ */
+ if ((pfd.revents & POLLOUT))
+ return 0;
+
+ /* Enable socket by reading 1 byte */
+ if (read(sock, &c, 1) < 0)
+ return -errno;
+
+ return 0;
+}
+
+static int hf_audio_agent_transport_acquire(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) {
+ struct hf_audio_card *card = t->userdata;
+ int err;
+
+ pa_assert(card);
+
+ if (!optional && card->fd < 0) {
+ err = card->acquire(card);
+ if (err < 0)
+ return err;
+ }
+
+ /* The correct block size should take into account the SCO MTU from
+ * the Bluetooth adapter and (for adapters in the USB bus) the MxPS
+ * value from the Isoc USB endpoint in use by btusb and should be
+ * made available to userspace by the Bluetooth kernel subsystem.
+ * Meanwhile the empiric value 48 will be used. */
+ if (imtu)
+ *imtu = 48;
+ if (omtu)
+ *omtu = 48;
+
+ err = socket_accept(card->fd);
+ if (err < 0) {
+ pa_log_error("Deferred setup failed on fd %d: %s", card->fd, pa_cstrerror(-err));
+ return -1;
+ }
+
+ return card->fd;
+}
+
+static void hf_audio_agent_transport_release(pa_bluetooth_transport *t) {
+ struct hf_audio_card *card = t->userdata;
+
+ pa_assert(card);
+
+ if (card->fd < 0) {
+ pa_log_info("Transport %s already released", t->path);
+ return;
+ }
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(card->fd, SHUT_RDWR);
+ close(card->fd);
+ card->fd = -1;
+}
+
+static void hf_audio_agent_card_found(pa_bluetooth_backend *backend, const char *path, DBusMessageIter *props_i) {
+ DBusMessageIter i, value_i;
+ const char *key, *value;
+ struct hf_audio_card *card;
+ pa_bluetooth_device *d;
+ pa_bluetooth_profile_t p = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ pa_assert(backend);
+ pa_assert(path);
+ pa_assert(props_i);
+
+ pa_log_debug("New HF card found: %s", path);
+
+ card = hf_audio_card_new(backend, path);
+
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ char c;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if ((c = dbus_message_iter_get_arg_type(&value_i)) != DBUS_TYPE_STRING) {
+ pa_log_error("Invalid properties for %s: expected 's', received '%c'", path, c);
+ goto fail;
+ }
+
+ dbus_message_iter_get_basic(&value_i, &value);
+
+ if (pa_streq(key, "RemoteAddress")) {
+ pa_xfree(card->remote_address);
+ card->remote_address = pa_xstrdup(value);
+ } else if (pa_streq(key, "LocalAddress")) {
+ pa_xfree(card->local_address);
+ card->local_address = pa_xstrdup(value);
+ } else if (pa_streq(key, "Type")) {
+ if (pa_streq(value, "gateway"))
+ p = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT;
+ }
+
+ pa_log_debug("%s: %s", key, value);
+
+ dbus_message_iter_next(props_i);
+ }
+
+ d = pa_bluetooth_discovery_get_device_by_address(backend->discovery, card->remote_address, card->local_address);
+ if (!d) {
+ pa_log_error("Device doesnt exist for %s", path);
+ goto fail;
+ }
+
+ card->transport = pa_bluetooth_transport_new(d, backend->ofono_bus_id, path, p, NULL, 0);
+ card->transport->acquire = hf_audio_agent_transport_acquire;
+ card->transport->release = hf_audio_agent_transport_release;
+ card->transport->userdata = card;
+
+ pa_bluetooth_transport_put(card->transport);
+ pa_hashmap_put(backend->cards, card->path, card);
+
+ return;
+
+fail:
+ hf_audio_card_free(card);
+}
+
+static void hf_audio_agent_card_removed(pa_bluetooth_backend *backend, const char *path) {
+ struct hf_audio_card *card;
+
+ pa_assert(backend);
+ pa_assert(path);
+
+ pa_log_debug("HF card removed: %s", path);
+
+ card = pa_hashmap_remove(backend->cards, path);
+ if (!card)
+ return;
+
+ hf_audio_card_free(card);
+}
+
+static void hf_audio_agent_get_cards_reply(DBusPendingCall *pending, void *userdata) {
+ DBusMessage *r;
+ pa_dbus_pending *p;
+ pa_bluetooth_backend *backend;
+ DBusMessageIter i, array_i, struct_i, props_i;
+
+ pa_assert_se(p = userdata);
+ pa_assert_se(backend = p->context_data);
+ pa_assert_se(r = dbus_pending_call_steal_reply(pending));
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ pa_log_error("Failed to get a list of handsfree audio cards from ofono: %s: %s",
+ dbus_message_get_error_name(r), pa_dbus_get_error_message(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !pa_streq(dbus_message_get_signature(r), "a(oa{sv})")) {
+ pa_log_error("Invalid arguments in GetCards() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ const char *path;
+
+ dbus_message_iter_recurse(&array_i, &struct_i);
+ dbus_message_iter_get_basic(&struct_i, &path);
+ dbus_message_iter_next(&struct_i);
+
+ dbus_message_iter_recurse(&struct_i, &props_i);
+
+ hf_audio_agent_card_found(backend, path, &props_i);
+
+ dbus_message_iter_next(&array_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+
+ PA_LLIST_REMOVE(pa_dbus_pending, backend->pending, p);
+ pa_dbus_pending_free(p);
+}
+
+static void hf_audio_agent_get_cards(pa_bluetooth_backend *hf) {
+ DBusMessage *m;
+
+ pa_assert(hf);
+
+ pa_assert_se(m = dbus_message_new_method_call(OFONO_SERVICE, "/", HF_AUDIO_MANAGER_INTERFACE, "GetCards"));
+ hf_dbus_send_and_add_to_pending(hf, m, hf_audio_agent_get_cards_reply, NULL);
+}
+
+static void ofono_bus_id_destroy(pa_bluetooth_backend *backend) {
+ pa_hashmap_remove_all(backend->cards);
+
+ if (backend->ofono_bus_id) {
+ pa_xfree(backend->ofono_bus_id);
+ backend->ofono_bus_id = NULL;
+ pa_bluetooth_discovery_set_ofono_running(backend->discovery, false);
+ }
+}
+
+static void hf_audio_agent_register_reply(DBusPendingCall *pending, void *userdata) {
+ DBusMessage *r;
+ pa_dbus_pending *p;
+ pa_bluetooth_backend *backend;
+
+ pa_assert_se(p = userdata);
+ pa_assert_se(backend = p->context_data);
+ pa_assert_se(r = dbus_pending_call_steal_reply(pending));
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ pa_log_info("Failed to register as a handsfree audio agent with ofono: %s: %s",
+ dbus_message_get_error_name(r), pa_dbus_get_error_message(r));
+ goto finish;
+ }
+
+ backend->ofono_bus_id = pa_xstrdup(dbus_message_get_sender(r));
+
+ hf_audio_agent_get_cards(backend);
+
+finish:
+ dbus_message_unref(r);
+
+ PA_LLIST_REMOVE(pa_dbus_pending, backend->pending, p);
+ pa_dbus_pending_free(p);
+
+ pa_bluetooth_discovery_set_ofono_running(backend->discovery, backend->ofono_bus_id != NULL);
+}
+
+static void hf_audio_agent_register(pa_bluetooth_backend *hf) {
+ DBusMessage *m;
+ uint8_t codecs[2];
+ const uint8_t *pcodecs = codecs;
+ int ncodecs = 0;
+ const char *path = HF_AUDIO_AGENT_PATH;
+
+ pa_assert(hf);
+
+ pa_assert_se(m = dbus_message_new_method_call(OFONO_SERVICE, "/", HF_AUDIO_MANAGER_INTERFACE, "Register"));
+
+ codecs[ncodecs++] = HFP_AUDIO_CODEC_CVSD;
+
+ pa_assert_se(dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &pcodecs, ncodecs,
+ DBUS_TYPE_INVALID));
+
+ hf_dbus_send_and_add_to_pending(hf, m, hf_audio_agent_register_reply, NULL);
+}
+
+static void hf_audio_agent_unregister(pa_bluetooth_backend *backend) {
+ DBusMessage *m;
+ const char *path = HF_AUDIO_AGENT_PATH;
+
+ pa_assert(backend);
+ pa_assert(backend->connection);
+
+ if (backend->ofono_bus_id) {
+ pa_assert_se(m = dbus_message_new_method_call(backend->ofono_bus_id, "/", HF_AUDIO_MANAGER_INTERFACE, "Unregister"));
+ pa_assert_se(dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID));
+ pa_assert_se(dbus_connection_send(pa_dbus_connection_get(backend->connection), m, NULL));
+
+ ofono_bus_id_destroy(backend);
+ }
+}
+
+static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *data) {
+ const char *sender;
+ DBusError err;
+ pa_bluetooth_backend *backend = data;
+
+ pa_assert(bus);
+ pa_assert(m);
+ pa_assert(backend);
+
+ sender = dbus_message_get_sender(m);
+ if (!pa_safe_streq(backend->ofono_bus_id, sender) && !pa_streq("org.freedesktop.DBus", sender))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ 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, OFONO_SERVICE)) {
+
+ if (old_owner && *old_owner) {
+ pa_log_debug("oFono disappeared");
+ ofono_bus_id_destroy(backend);
+ }
+
+ if (new_owner && *new_owner) {
+ pa_log_debug("oFono appeared");
+ hf_audio_agent_register(backend);
+ }
+ }
+
+ } else if (dbus_message_is_signal(m, "org.ofono.HandsfreeAudioManager", "CardAdded")) {
+ const char *p;
+ DBusMessageIter arg_i, props_i;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oa{sv}")) {
+ pa_log_error("Failed to parse org.ofono.HandsfreeAudioManager.CardAdded");
+ 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, &props_i);
+
+ hf_audio_agent_card_found(backend, p, &props_i);
+ } else if (dbus_message_is_signal(m, "org.ofono.HandsfreeAudioManager", "CardRemoved")) {
+ const char *p;
+
+ if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) {
+ pa_log_error("Failed to parse org.ofono.HandsfreeAudioManager.CardRemoved: %s", err.message);
+ goto fail;
+ }
+
+ hf_audio_agent_card_removed(backend, p);
+ }
+
+fail:
+ dbus_error_free(&err);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static DBusMessage *hf_audio_agent_release(DBusConnection *c, DBusMessage *m, void *data) {
+ DBusMessage *r;
+ const char *sender;
+ pa_bluetooth_backend *backend = data;
+
+ pa_assert(backend);
+
+ sender = dbus_message_get_sender(m);
+ if (!pa_safe_streq(backend->ofono_bus_id, sender)) {
+ pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.NotAllowed", "Operation is not allowed by this sender"));
+ return r;
+ }
+
+ pa_log_debug("HF audio agent has been unregistered by oFono (%s)", backend->ofono_bus_id);
+
+ ofono_bus_id_destroy(backend);
+
+ pa_assert_se(r = dbus_message_new_method_return(m));
+
+ return r;
+}
+
+static DBusMessage *hf_audio_agent_new_connection(DBusConnection *c, DBusMessage *m, void *data) {
+ DBusMessage *r;
+ const char *sender, *path;
+ int fd;
+ uint8_t codec;
+ struct hf_audio_card *card;
+ pa_bluetooth_backend *backend = data;
+
+ pa_assert(backend);
+
+ sender = dbus_message_get_sender(m);
+ if (!pa_safe_streq(backend->ofono_bus_id, sender)) {
+ pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.NotAllowed", "Operation is not allowed by this sender"));
+ return r;
+ }
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_OBJECT_PATH, &path,
+ DBUS_TYPE_UNIX_FD, &fd,
+ DBUS_TYPE_BYTE, &codec,
+ DBUS_TYPE_INVALID) == FALSE) {
+ pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.InvalidArguments", "Invalid arguments in method call"));
+ return r;
+ }
+
+ card = pa_hashmap_get(backend->cards, path);
+
+ if (!card || codec != HFP_AUDIO_CODEC_CVSD || card->fd >= 0) {
+ pa_log_warn("New audio connection invalid arguments (path=%s fd=%d, codec=%d)", path, fd, codec);
+ pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.InvalidArguments", "Invalid arguments in method call"));
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ return r;
+ }
+
+ pa_log_debug("New audio connection on card %s (fd=%d, codec=%d)", path, fd, codec);
+
+ card->connecting = false;
+ card->fd = fd;
+ card->transport->codec = codec;
+
+ pa_bluetooth_transport_set_state(card->transport, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING);
+
+ pa_assert_se(r = dbus_message_new_method_return(m));
+
+ return r;
+}
+
+static DBusHandlerResult hf_audio_agent_handler(DBusConnection *c, DBusMessage *m, void *data) {
+ pa_bluetooth_backend *backend = data;
+ DBusMessage *r = NULL;
+ const char *path, *interface, *member;
+
+ pa_assert(backend);
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ if (!pa_streq(path, HF_AUDIO_AGENT_PATH))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = HF_AUDIO_AGENT_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, HF_AUDIO_AGENT_INTERFACE, "NewConnection"))
+ r = hf_audio_agent_new_connection(c, m, data);
+ else if (dbus_message_is_method_call(m, HF_AUDIO_AGENT_INTERFACE, "Release"))
+ r = hf_audio_agent_release(c, m, data);
+ else
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (r) {
+ pa_assert_se(dbus_connection_send(pa_dbus_connection_get(backend->connection), r, NULL));
+ dbus_message_unref(r);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+pa_bluetooth_backend *pa_bluetooth_ofono_backend_new(pa_core *c, pa_bluetooth_discovery *y) {
+ pa_bluetooth_backend *backend;
+ DBusError err;
+ static const DBusObjectPathVTable vtable_hf_audio_agent = {
+ .message_function = hf_audio_agent_handler,
+ };
+
+ pa_assert(c);
+
+ backend = pa_xnew0(pa_bluetooth_backend, 1);
+ backend->core = c;
+ backend->discovery = y;
+ backend->cards = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) hf_audio_card_free);
+
+ dbus_error_init(&err);
+
+ if (!(backend->connection = pa_dbus_bus_get(c, DBUS_BUS_SYSTEM, &err))) {
+ pa_log("Failed to get D-Bus connection: %s", err.message);
+ dbus_error_free(&err);
+ pa_xfree(backend);
+ return NULL;
+ }
+
+ /* dynamic detection of handsfree audio cards */
+ if (!dbus_connection_add_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend, NULL)) {
+ pa_log_error("Failed to add filter function");
+ pa_dbus_connection_unref(backend->connection);
+ pa_xfree(backend);
+ return NULL;
+ }
+
+ if (pa_dbus_add_matches(pa_dbus_connection_get(backend->connection), &err,
+ "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" OFONO_SERVICE "'",
+ "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'",
+ "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'",
+ NULL) < 0) {
+ pa_log("Failed to add oFono D-Bus matches: %s", err.message);
+ dbus_connection_remove_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend);
+ pa_dbus_connection_unref(backend->connection);
+ pa_xfree(backend);
+ return NULL;
+ }
+
+ pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(backend->connection), HF_AUDIO_AGENT_PATH,
+ &vtable_hf_audio_agent, backend));
+
+ hf_audio_agent_register(backend);
+
+ return backend;
+}
+
+void pa_bluetooth_ofono_backend_free(pa_bluetooth_backend *backend) {
+ pa_assert(backend);
+
+ pa_dbus_free_pending_list(&backend->pending);
+
+ hf_audio_agent_unregister(backend);
+
+ dbus_connection_unregister_object_path(pa_dbus_connection_get(backend->connection), HF_AUDIO_AGENT_PATH);
+
+ pa_dbus_remove_matches(pa_dbus_connection_get(backend->connection),
+ "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" OFONO_SERVICE "'",
+ "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'",
+ "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'",
+ NULL);
+
+ dbus_connection_remove_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend);
+
+ pa_dbus_connection_unref(backend->connection);
+
+ pa_hashmap_free(backend->cards);
+
+ pa_xfree(backend);
+}
diff --git a/src/modules/bluetooth/bluez5-util.c b/src/modules/bluetooth/bluez5-util.c
new file mode 100644
index 0000000..a21896e
--- /dev/null
+++ b/src/modules/bluetooth/bluez5-util.c
@@ -0,0 +1,1744 @@
+/***
+ 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)) {
+
+ const pa_a2dp_codec *a2dp_codec_sbc;
+ pa_bluetooth_adapter *a;
+
+ 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;
+
+ /* Currently only one A2DP codec is supported, so register only SBC
+ * Support for multiple codecs needs to use a new Bluez API which
+ * pulseaudio does not implement yet, patches are waiting in queue */
+ a2dp_codec_sbc = pa_bluetooth_get_a2dp_codec("sbc");
+ pa_assert(a2dp_codec_sbc);
+ register_endpoint(y, a2dp_codec_sbc, path, A2DP_SINK_ENDPOINT "/sbc", PA_BLUETOOTH_UUID_A2DP_SINK);
+ register_endpoint(y, a2dp_codec_sbc, path, A2DP_SOURCE_ENDPOINT "/sbc", PA_BLUETOOTH_UUID_A2DP_SOURCE);
+
+ } 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 --git a/src/modules/bluetooth/bluez5-util.h b/src/modules/bluetooth/bluez5-util.h
new file mode 100644
index 0000000..ff172e0
--- /dev/null
+++ b/src/modules/bluetooth/bluez5-util.h
@@ -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 --git a/src/modules/bluetooth/meson.build b/src/modules/bluetooth/meson.build
new file mode 100644
index 0000000..9982cba
--- /dev/null
+++ b/src/modules/bluetooth/meson.build
@@ -0,0 +1,33 @@
+libbluez5_util_sources = [
+ 'a2dp-codec-sbc.c',
+ 'a2dp-codec-util.c',
+ 'bluez5-util.c',
+]
+
+libbluez5_util_headers = [
+ 'a2dp-codec-api.h',
+ 'a2dp-codecs.h',
+ 'a2dp-codec-util.h',
+ 'bluez5-util.h',
+ 'rtp.h',
+]
+
+if get_option('bluez5-native-headset')
+ libbluez5_util_sources += [ 'backend-native.c' ]
+endif
+
+if get_option('bluez5-ofono-headset')
+ libbluez5_util_sources += [ 'backend-ofono.c' ]
+endif
+
+libbluez5_util = shared_library('bluez5-util',
+ libbluez5_util_sources,
+ libbluez5_util_headers,
+ c_args : [pa_c_args, server_c_args],
+ link_args : [nodelete_link_args],
+ include_directories : [configinc, topinc],
+ dependencies : [libpulse_dep, libpulsecommon_dep, libpulsecore_dep, dbus_dep, sbc_dep, libintl_dep],
+ install : true,
+ install_rpath : privlibdir,
+ install_dir : modlibexecdir,
+)
diff --git a/src/modules/bluetooth/module-bluetooth-discover.c b/src/modules/bluetooth/module-bluetooth-discover.c
new file mode 100644
index 0000000..cf8c7ee
--- /dev/null
+++ b/src/modules/bluetooth/module-bluetooth-discover.c
@@ -0,0 +1,76 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 João Paulo Rechi Vita
+
+ 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 <pulsecore/core-util.h>
+#include <pulsecore/macro.h>
+#include <pulsecore/module.h>
+
+PA_MODULE_AUTHOR("João Paulo Rechi Vita");
+PA_MODULE_DESCRIPTION("Detect available Bluetooth daemon and load the corresponding discovery module");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+PA_MODULE_USAGE(
+ "headset=ofono|native|auto"
+ "autodetect_mtu=<boolean>"
+);
+
+struct userdata {
+ uint32_t bluez5_module_idx;
+};
+
+int pa__init(pa_module* m) {
+ struct userdata *u;
+ pa_module *mm;
+
+ pa_assert(m);
+
+ m->userdata = u = pa_xnew0(struct userdata, 1);
+ u->bluez5_module_idx = PA_INVALID_INDEX;
+
+ if (pa_module_exists("module-bluez5-discover")) {
+ pa_module_load(&mm, m->core, "module-bluez5-discover", m->argument);
+ if (mm)
+ u->bluez5_module_idx = mm->index;
+ }
+
+ if (u->bluez5_module_idx == PA_INVALID_INDEX) {
+ pa_xfree(u);
+ return -1;
+ }
+
+ return 0;
+}
+
+void pa__done(pa_module* m) {
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ if (u->bluez5_module_idx != PA_INVALID_INDEX)
+ pa_module_unload_by_index(m->core, u->bluez5_module_idx, true);
+
+ pa_xfree(u);
+}
diff --git a/src/modules/bluetooth/module-bluetooth-policy.c b/src/modules/bluetooth/module-bluetooth-policy.c
new file mode 100644
index 0000000..ffaa140
--- /dev/null
+++ b/src/modules/bluetooth/module-bluetooth-policy.c
@@ -0,0 +1,517 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2006 Lennart Poettering
+ Copyright 2009 Canonical Ltd
+ Copyright (C) 2012 Intel Corporation
+
+ 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/xmalloc.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/source-output.h>
+#include <pulsecore/source.h>
+#include <pulsecore/core-util.h>
+
+PA_MODULE_AUTHOR("Frédéric Dalleau, Pali Rohár");
+PA_MODULE_DESCRIPTION("Policy module to make using bluetooth devices out-of-the-box easier");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+PA_MODULE_USAGE(
+ "auto_switch=<Switch between hsp and a2dp profile? (0 - never, 1 - media.role=phone, 2 - heuristic> "
+ "a2dp_source=<Handle a2dp_source card profile (sink role)?> "
+ "ag=<Handle headset_audio_gateway card profile (headset role)?> ");
+
+static const char* const valid_modargs[] = {
+ "auto_switch",
+ "a2dp_source",
+ "ag",
+ NULL
+};
+
+struct userdata {
+ uint32_t auto_switch;
+ bool enable_a2dp_source;
+ bool enable_ag;
+ pa_hook_slot *source_put_slot;
+ pa_hook_slot *sink_put_slot;
+ pa_hook_slot *source_output_put_slot;
+ pa_hook_slot *source_output_unlink_slot;
+ pa_hook_slot *card_init_profile_slot;
+ pa_hook_slot *card_unlink_slot;
+ pa_hook_slot *profile_available_changed_slot;
+ pa_hashmap *will_need_revert_card_map;
+};
+
+/* When a source is created, loopback it to default sink */
+static pa_hook_result_t source_put_hook_callback(pa_core *c, pa_source *source, void *userdata) {
+ struct userdata *u = userdata;
+ const char *s;
+ const char *role;
+ char *args;
+ pa_module *m = NULL;
+
+ pa_assert(c);
+ pa_assert(source);
+
+ /* Only consider bluetooth sinks and sources */
+ s = pa_proplist_gets(source->proplist, PA_PROP_DEVICE_BUS);
+ if (!s)
+ return PA_HOOK_OK;
+
+ if (!pa_streq(s, "bluetooth"))
+ return PA_HOOK_OK;
+
+ s = pa_proplist_gets(source->proplist, "bluetooth.protocol");
+ if (!s)
+ return PA_HOOK_OK;
+
+ if (u->enable_a2dp_source && pa_streq(s, "a2dp_source"))
+ role = "music";
+ else if (u->enable_ag && pa_streq(s, "headset_audio_gateway"))
+ role = "phone";
+ else {
+ pa_log_debug("Profile %s cannot be selected for loopback", s);
+ return PA_HOOK_OK;
+ }
+
+ /* Load module-loopback */
+ args = pa_sprintf_malloc("source=\"%s\" source_dont_move=\"true\" sink_input_properties=\"media.role=%s\"", source->name,
+ role);
+ (void) pa_module_load(&m, c, "module-loopback", args);
+ pa_xfree(args);
+
+ return PA_HOOK_OK;
+}
+
+/* When a sink is created, loopback it to default source */
+static pa_hook_result_t sink_put_hook_callback(pa_core *c, pa_sink *sink, void *userdata) {
+ struct userdata *u = userdata;
+ const char *s;
+ const char *role;
+ char *args;
+ pa_module *m = NULL;
+
+ pa_assert(c);
+ pa_assert(sink);
+
+ /* Only consider bluetooth sinks and sources */
+ s = pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_BUS);
+ if (!s)
+ return PA_HOOK_OK;
+
+ if (!pa_streq(s, "bluetooth"))
+ return PA_HOOK_OK;
+
+ s = pa_proplist_gets(sink->proplist, "bluetooth.protocol");
+ if (!s)
+ return PA_HOOK_OK;
+
+ if (u->enable_ag && pa_streq(s, "headset_audio_gateway"))
+ role = "phone";
+ else {
+ pa_log_debug("Profile %s cannot be selected for loopback", s);
+ return PA_HOOK_OK;
+ }
+
+ /* Load module-loopback */
+ args = pa_sprintf_malloc("sink=\"%s\" sink_dont_move=\"true\" source_output_properties=\"media.role=%s\"", sink->name,
+ role);
+ (void) pa_module_load(&m, c, "module-loopback", args);
+ pa_xfree(args);
+
+ return PA_HOOK_OK;
+}
+
+static void card_set_profile(struct userdata *u, pa_card *card, bool revert_to_a2dp)
+{
+ pa_card_profile *profile;
+ void *state;
+
+ /* Find available profile and activate it */
+ PA_HASHMAP_FOREACH(profile, card->profiles, state) {
+ if (profile->available == PA_AVAILABLE_NO)
+ continue;
+
+ /* Check for correct profile based on revert_to_a2dp */
+ if (revert_to_a2dp) {
+ if (!pa_streq(profile->name, "a2dp_sink"))
+ continue;
+ } else {
+ if (!pa_streq(profile->name, "headset_head_unit"))
+ continue;
+ }
+
+ pa_log_debug("Setting card '%s' to profile '%s'", card->name, profile->name);
+
+ if (pa_card_set_profile(card, profile, false) != 0) {
+ pa_log_warn("Could not set profile '%s'", profile->name);
+ continue;
+ }
+
+ /* When we are not in revert_to_a2dp phase flag this card for will_need_revert */
+ if (!revert_to_a2dp)
+ pa_hashmap_put(u->will_need_revert_card_map, card, PA_INT_TO_PTR(1));
+
+ break;
+ }
+}
+
+/* Switch profile for one card */
+static void switch_profile(pa_card *card, bool revert_to_a2dp, void *userdata) {
+ struct userdata *u = userdata;
+ const char *s;
+
+ /* Only consider bluetooth cards */
+ s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS);
+ if (!s || !pa_streq(s, "bluetooth"))
+ return;
+
+ if (revert_to_a2dp) {
+ /* In revert_to_a2dp phase only consider cards with will_need_revert flag and remove it */
+ if (!pa_hashmap_remove(u->will_need_revert_card_map, card))
+ return;
+
+ /* Skip card if does not have active hsp profile */
+ if (!pa_streq(card->active_profile->name, "headset_head_unit"))
+ return;
+
+ /* Skip card if already has active a2dp profile */
+ if (pa_streq(card->active_profile->name, "a2dp_sink"))
+ return;
+ } else {
+ /* Skip card if does not have active a2dp profile */
+ if (!pa_streq(card->active_profile->name, "a2dp_sink"))
+ return;
+
+ /* Skip card if already has active hsp profile */
+ if (pa_streq(card->active_profile->name, "headset_head_unit"))
+ return;
+ }
+
+ card_set_profile(u, card, revert_to_a2dp);
+}
+
+/* Return true if we should ignore this source output */
+static bool ignore_output(pa_source_output *source_output, void *userdata) {
+ struct userdata *u = userdata;
+ const char *s;
+
+ /* New applications could set media.role for identifying streams */
+ /* We are interested only in media.role=phone */
+ s = pa_proplist_gets(source_output->proplist, PA_PROP_MEDIA_ROLE);
+ if (s)
+ return !pa_streq(s, "phone");
+
+ /* If media.role is not set use some heuristic (if enabled) */
+ if (u->auto_switch != 2)
+ return true;
+
+ /* Ignore if resample method is peaks (used by desktop volume programs) */
+ if (pa_source_output_get_resample_method(source_output) == PA_RESAMPLER_PEAKS)
+ return true;
+
+ /* Ignore if there is no client/application assigned (used by virtual stream) */
+ if (!source_output->client)
+ return true;
+
+ /* Ignore if recording from monitor of sink */
+ if (source_output->direct_on_input)
+ return true;
+
+ return false;
+}
+
+static unsigned source_output_count(pa_core *c, void *userdata) {
+ pa_source_output *source_output;
+ uint32_t idx;
+ unsigned count = 0;
+
+ PA_IDXSET_FOREACH(source_output, c->source_outputs, idx)
+ if (!ignore_output(source_output, userdata))
+ ++count;
+
+ return count;
+}
+
+/* Switch profile for all cards */
+static void switch_profile_all(pa_idxset *cards, bool revert_to_a2dp, void *userdata) {
+ pa_card *card;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(card, cards, idx)
+ switch_profile(card, revert_to_a2dp, userdata);
+}
+
+/* When a source output is created, switch profile a2dp to profile hsp */
+static pa_hook_result_t source_output_put_hook_callback(pa_core *c, pa_source_output *source_output, void *userdata) {
+ pa_assert(c);
+ pa_assert(source_output);
+
+ if (ignore_output(source_output, userdata))
+ return PA_HOOK_OK;
+
+ switch_profile_all(c->cards, false, userdata);
+ return PA_HOOK_OK;
+}
+
+/* When all source outputs are unlinked, switch profile hsp back back to profile a2dp */
+static pa_hook_result_t source_output_unlink_hook_callback(pa_core *c, pa_source_output *source_output, void *userdata) {
+ pa_assert(c);
+ pa_assert(source_output);
+
+ if (ignore_output(source_output, userdata))
+ return PA_HOOK_OK;
+
+ /* If there are still some source outputs do nothing. */
+ if (source_output_count(c, userdata) > 0)
+ return PA_HOOK_OK;
+
+ switch_profile_all(c->cards, true, userdata);
+ return PA_HOOK_OK;
+}
+
+static pa_hook_result_t card_init_profile_hook_callback(pa_core *c, pa_card *card, void *userdata) {
+ struct userdata *u = userdata;
+ const char *s;
+
+ pa_assert(c);
+ pa_assert(card);
+
+ if (source_output_count(c, userdata) == 0)
+ return PA_HOOK_OK;
+
+ /* Only consider bluetooth cards */
+ s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS);
+ if (!s || !pa_streq(s, "bluetooth"))
+ return PA_HOOK_OK;
+
+ /* Ignore card if has already set other initial profile than a2dp */
+ if (card->active_profile &&
+ !pa_streq(card->active_profile->name, "a2dp_sink"))
+ return PA_HOOK_OK;
+
+ /* Set initial profile to hsp */
+ card_set_profile(u, card, false);
+
+ /* Flag this card for will_need_revert */
+ pa_hashmap_put(u->will_need_revert_card_map, card, PA_INT_TO_PTR(1));
+ return PA_HOOK_OK;
+}
+
+static pa_hook_result_t card_unlink_hook_callback(pa_core *c, pa_card *card, void *userdata) {
+ pa_assert(c);
+ pa_assert(card);
+ switch_profile(card, true, userdata);
+ return PA_HOOK_OK;
+}
+
+static pa_card_profile *find_best_profile(pa_card *card) {
+ void *state;
+ pa_card_profile *profile;
+ pa_card_profile *result = card->active_profile;
+
+ PA_HASHMAP_FOREACH(profile, card->profiles, state) {
+ if (profile->available == PA_AVAILABLE_NO)
+ continue;
+
+ if (result == NULL ||
+ (profile->available == PA_AVAILABLE_YES && result->available == PA_AVAILABLE_UNKNOWN) ||
+ (profile->available == result->available && profile->priority > result->priority))
+ result = profile;
+ }
+
+ return result;
+}
+
+static pa_hook_result_t profile_available_hook_callback(pa_core *c, pa_card_profile *profile, void *userdata) {
+ pa_card *card;
+ const char *s;
+ bool is_active_profile;
+ pa_card_profile *selected_profile;
+
+ pa_assert(c);
+ pa_assert(profile);
+ pa_assert_se((card = profile->card));
+
+ /* Only consider bluetooth cards */
+ s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS);
+ if (!s || !pa_streq(s, "bluetooth"))
+ return PA_HOOK_OK;
+
+ /* Do not automatically switch profiles for headsets, just in case */
+ if (pa_streq(profile->name, "a2dp_sink") || pa_streq(profile->name, "headset_head_unit"))
+ return PA_HOOK_OK;
+
+ is_active_profile = card->active_profile == profile;
+
+ if (profile->available == PA_AVAILABLE_YES) {
+ if (is_active_profile)
+ return PA_HOOK_OK;
+
+ if (card->active_profile->available == PA_AVAILABLE_YES && card->active_profile->priority >= profile->priority)
+ return PA_HOOK_OK;
+
+ selected_profile = profile;
+ } else {
+ if (!is_active_profile)
+ return PA_HOOK_OK;
+
+ pa_assert_se((selected_profile = find_best_profile(card)));
+
+ if (selected_profile == card->active_profile)
+ return PA_HOOK_OK;
+ }
+
+ pa_log_debug("Setting card '%s' to profile '%s'", card->name, selected_profile->name);
+
+ if (pa_card_set_profile(card, selected_profile, false) != 0)
+ pa_log_warn("Could not set profile '%s'", selected_profile->name);
+
+ return PA_HOOK_OK;
+}
+
+static void handle_all_profiles(pa_core *core) {
+ pa_card *card;
+ uint32_t state;
+
+ PA_IDXSET_FOREACH(card, core->cards, state) {
+ pa_card_profile *profile;
+ void *state2;
+
+ PA_HASHMAP_FOREACH(profile, card->profiles, state2)
+ profile_available_hook_callback(core, profile, NULL);
+ }
+}
+
+int pa__init(pa_module *m) {
+ pa_modargs *ma;
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log_error("Failed to parse module arguments");
+ goto fail;
+ }
+
+ m->userdata = u = pa_xnew0(struct userdata, 1);
+
+ u->auto_switch = 1;
+
+ if (pa_modargs_get_value(ma, "auto_switch", NULL)) {
+ bool auto_switch_bool;
+
+ /* auto_switch originally took a boolean value, let's keep
+ * compatibility with configuration files that still pass a boolean. */
+ if (pa_modargs_get_value_boolean(ma, "auto_switch", &auto_switch_bool) >= 0) {
+ if (auto_switch_bool)
+ u->auto_switch = 1;
+ else
+ u->auto_switch = 0;
+
+ } else if (pa_modargs_get_value_u32(ma, "auto_switch", &u->auto_switch) < 0) {
+ pa_log("Failed to parse auto_switch argument.");
+ goto fail;
+ }
+ }
+
+ u->enable_a2dp_source = true;
+ if (pa_modargs_get_value_boolean(ma, "a2dp_source", &u->enable_a2dp_source) < 0) {
+ pa_log("Failed to parse a2dp_source argument.");
+ goto fail;
+ }
+
+ u->enable_ag = true;
+ if (pa_modargs_get_value_boolean(ma, "ag", &u->enable_ag) < 0) {
+ pa_log("Failed to parse ag argument.");
+ goto fail;
+ }
+
+ u->will_need_revert_card_map = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ u->source_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT], PA_HOOK_NORMAL,
+ (pa_hook_cb_t) source_put_hook_callback, u);
+
+ u->sink_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT], PA_HOOK_NORMAL,
+ (pa_hook_cb_t) sink_put_hook_callback, u);
+
+ if (u->auto_switch) {
+ u->source_output_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_PUT], PA_HOOK_NORMAL,
+ (pa_hook_cb_t) source_output_put_hook_callback, u);
+
+ u->source_output_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_UNLINK_POST], PA_HOOK_NORMAL,
+ (pa_hook_cb_t) source_output_unlink_hook_callback, u);
+
+ u->card_init_profile_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_CHOOSE_INITIAL_PROFILE], PA_HOOK_NORMAL,
+ (pa_hook_cb_t) card_init_profile_hook_callback, u);
+
+ u->card_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_UNLINK], PA_HOOK_NORMAL,
+ (pa_hook_cb_t) card_unlink_hook_callback, u);
+ }
+
+ u->profile_available_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_PROFILE_AVAILABLE_CHANGED],
+ PA_HOOK_NORMAL, (pa_hook_cb_t) profile_available_hook_callback, u);
+
+ handle_all_profiles(m->core);
+
+ pa_modargs_free(ma);
+ return 0;
+
+fail:
+ if (ma)
+ pa_modargs_free(ma);
+ return -1;
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ if (u->source_put_slot)
+ pa_hook_slot_free(u->source_put_slot);
+
+ if (u->sink_put_slot)
+ pa_hook_slot_free(u->sink_put_slot);
+
+ if (u->source_output_put_slot)
+ pa_hook_slot_free(u->source_output_put_slot);
+
+ if (u->source_output_unlink_slot)
+ pa_hook_slot_free(u->source_output_unlink_slot);
+
+ if (u->card_init_profile_slot)
+ pa_hook_slot_free(u->card_init_profile_slot);
+
+ if (u->card_unlink_slot)
+ pa_hook_slot_free(u->card_unlink_slot);
+
+ if (u->profile_available_changed_slot)
+ pa_hook_slot_free(u->profile_available_changed_slot);
+
+ pa_hashmap_free(u->will_need_revert_card_map);
+
+ pa_xfree(u);
+}
diff --git a/src/modules/bluetooth/module-bluez5-device.c b/src/modules/bluetooth/module-bluez5-device.c
new file mode 100644
index 0000000..402053a
--- /dev/null
+++ b/src/modules/bluetooth/module-bluez5-device.c
@@ -0,0 +1,2417 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2008-2013 João Paulo Rechi Vita
+ Copyright 2011-2013 BMW Car IT GmbH.
+ Copyright 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 <errno.h>
+
+#include <arpa/inet.h>
+
+#include <pulse/rtclock.h>
+#include <pulse/timeval.h>
+#include <pulse/utf8.h>
+#include <pulse/util.h>
+
+#include <pulsecore/core-error.h>
+#include <pulsecore/core-rtclock.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/i18n.h>
+#include <pulsecore/module.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/poll.h>
+#include <pulsecore/rtpoll.h>
+#include <pulsecore/shared.h>
+#include <pulsecore/socket-util.h>
+#include <pulsecore/thread.h>
+#include <pulsecore/thread-mq.h>
+#include <pulsecore/time-smoother.h>
+
+#include "a2dp-codecs.h"
+#include "a2dp-codec-util.h"
+#include "bluez5-util.h"
+
+PA_MODULE_AUTHOR("João Paulo Rechi Vita");
+PA_MODULE_DESCRIPTION("BlueZ 5 Bluetooth audio sink and source");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(false);
+PA_MODULE_USAGE("path=<device object path>"
+ "autodetect_mtu=<boolean>");
+
+#define FIXED_LATENCY_PLAYBACK_A2DP (25 * PA_USEC_PER_MSEC)
+#define FIXED_LATENCY_PLAYBACK_SCO (25 * PA_USEC_PER_MSEC)
+#define FIXED_LATENCY_RECORD_A2DP (25 * PA_USEC_PER_MSEC)
+#define FIXED_LATENCY_RECORD_SCO (25 * PA_USEC_PER_MSEC)
+
+#define HSP_MAX_GAIN 15
+
+static const char* const valid_modargs[] = {
+ "path",
+ "autodetect_mtu",
+ NULL
+};
+
+enum {
+ BLUETOOTH_MESSAGE_IO_THREAD_FAILED,
+ BLUETOOTH_MESSAGE_STREAM_FD_HUP,
+ BLUETOOTH_MESSAGE_SET_TRANSPORT_PLAYING,
+ BLUETOOTH_MESSAGE_MAX
+};
+
+enum {
+ PA_SOURCE_MESSAGE_SETUP_STREAM = PA_SOURCE_MESSAGE_MAX,
+};
+
+enum {
+ PA_SINK_MESSAGE_SETUP_STREAM = PA_SINK_MESSAGE_MAX,
+};
+
+typedef struct bluetooth_msg {
+ pa_msgobject parent;
+ pa_card *card;
+} bluetooth_msg;
+PA_DEFINE_PRIVATE_CLASS(bluetooth_msg, pa_msgobject);
+#define BLUETOOTH_MSG(o) (bluetooth_msg_cast(o))
+
+struct userdata {
+ pa_module *module;
+ pa_core *core;
+
+ pa_hook_slot *device_connection_changed_slot;
+ pa_hook_slot *transport_state_changed_slot;
+ pa_hook_slot *transport_speaker_gain_changed_slot;
+ pa_hook_slot *transport_microphone_gain_changed_slot;
+
+ pa_bluetooth_discovery *discovery;
+ pa_bluetooth_device *device;
+ pa_bluetooth_transport *transport;
+ bool transport_acquired;
+ bool stream_setup_done;
+
+ pa_card *card;
+ pa_sink *sink;
+ pa_source *source;
+ pa_bluetooth_profile_t profile;
+ char *output_port_name;
+ char *input_port_name;
+
+ pa_thread *thread;
+ pa_thread_mq thread_mq;
+ pa_rtpoll *rtpoll;
+ pa_rtpoll_item *rtpoll_item;
+ bluetooth_msg *msg;
+
+ int stream_fd;
+ int stream_write_type;
+ size_t read_link_mtu;
+ size_t write_link_mtu;
+ size_t read_block_size;
+ size_t write_block_size;
+ uint64_t read_index;
+ uint64_t write_index;
+ pa_usec_t started_at;
+ pa_smoother *read_smoother;
+ pa_memchunk write_memchunk;
+
+ const pa_a2dp_codec *a2dp_codec;
+
+ void *encoder_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;
+ pa_sample_spec decoder_sample_spec;
+ void *decoder_buffer; /* Codec transfer buffer */
+ size_t decoder_buffer_size; /* Size of the buffer */
+};
+
+typedef enum pa_bluetooth_form_factor {
+ PA_BLUETOOTH_FORM_FACTOR_UNKNOWN,
+ PA_BLUETOOTH_FORM_FACTOR_HEADSET,
+ PA_BLUETOOTH_FORM_FACTOR_HANDSFREE,
+ PA_BLUETOOTH_FORM_FACTOR_MICROPHONE,
+ PA_BLUETOOTH_FORM_FACTOR_SPEAKER,
+ PA_BLUETOOTH_FORM_FACTOR_HEADPHONE,
+ PA_BLUETOOTH_FORM_FACTOR_PORTABLE,
+ PA_BLUETOOTH_FORM_FACTOR_CAR,
+ PA_BLUETOOTH_FORM_FACTOR_HIFI,
+ PA_BLUETOOTH_FORM_FACTOR_PHONE,
+} pa_bluetooth_form_factor_t;
+
+/* Run from main thread */
+static pa_bluetooth_form_factor_t form_factor_from_class(uint32_t class_of_device) {
+ unsigned major, minor;
+ pa_bluetooth_form_factor_t r;
+
+ static const pa_bluetooth_form_factor_t table[] = {
+ [1] = PA_BLUETOOTH_FORM_FACTOR_HEADSET,
+ [2] = PA_BLUETOOTH_FORM_FACTOR_HANDSFREE,
+ [4] = PA_BLUETOOTH_FORM_FACTOR_MICROPHONE,
+ [5] = PA_BLUETOOTH_FORM_FACTOR_SPEAKER,
+ [6] = PA_BLUETOOTH_FORM_FACTOR_HEADPHONE,
+ [7] = PA_BLUETOOTH_FORM_FACTOR_PORTABLE,
+ [8] = PA_BLUETOOTH_FORM_FACTOR_CAR,
+ [10] = PA_BLUETOOTH_FORM_FACTOR_HIFI
+ };
+
+ /*
+ * See Bluetooth Assigned Numbers:
+ * https://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm
+ */
+ major = (class_of_device >> 8) & 0x1F;
+ minor = (class_of_device >> 2) & 0x3F;
+
+ switch (major) {
+ case 2:
+ return PA_BLUETOOTH_FORM_FACTOR_PHONE;
+ case 4:
+ break;
+ default:
+ pa_log_debug("Unknown Bluetooth major device class %u", major);
+ return PA_BLUETOOTH_FORM_FACTOR_UNKNOWN;
+ }
+
+ r = minor < PA_ELEMENTSOF(table) ? table[minor] : PA_BLUETOOTH_FORM_FACTOR_UNKNOWN;
+
+ if (!r)
+ pa_log_debug("Unknown Bluetooth minor device class %u", minor);
+
+ return r;
+}
+
+/* Run from main thread */
+static const char *form_factor_to_string(pa_bluetooth_form_factor_t ff) {
+ switch (ff) {
+ case PA_BLUETOOTH_FORM_FACTOR_UNKNOWN:
+ return "unknown";
+ case PA_BLUETOOTH_FORM_FACTOR_HEADSET:
+ return "headset";
+ case PA_BLUETOOTH_FORM_FACTOR_HANDSFREE:
+ return "hands-free";
+ case PA_BLUETOOTH_FORM_FACTOR_MICROPHONE:
+ return "microphone";
+ case PA_BLUETOOTH_FORM_FACTOR_SPEAKER:
+ return "speaker";
+ case PA_BLUETOOTH_FORM_FACTOR_HEADPHONE:
+ return "headphone";
+ case PA_BLUETOOTH_FORM_FACTOR_PORTABLE:
+ return "portable";
+ case PA_BLUETOOTH_FORM_FACTOR_CAR:
+ return "car";
+ case PA_BLUETOOTH_FORM_FACTOR_HIFI:
+ return "hifi";
+ case PA_BLUETOOTH_FORM_FACTOR_PHONE:
+ return "phone";
+ }
+
+ pa_assert_not_reached();
+}
+
+/* Run from main thread */
+static void connect_ports(struct userdata *u, void *new_data, pa_direction_t direction) {
+ pa_device_port *port;
+
+ if (direction == PA_DIRECTION_OUTPUT) {
+ pa_sink_new_data *sink_new_data = new_data;
+
+ pa_assert_se(port = pa_hashmap_get(u->card->ports, u->output_port_name));
+ pa_assert_se(pa_hashmap_put(sink_new_data->ports, port->name, port) >= 0);
+ pa_device_port_ref(port);
+ } else {
+ pa_source_new_data *source_new_data = new_data;
+
+ pa_assert_se(port = pa_hashmap_get(u->card->ports, u->input_port_name));
+ pa_assert_se(pa_hashmap_put(source_new_data->ports, port->name, port) >= 0);
+ pa_device_port_ref(port);
+ }
+}
+
+/* Run from IO thread */
+static int sco_process_render(struct userdata *u) {
+ ssize_t l;
+ pa_memchunk memchunk;
+ int saved_errno;
+
+ pa_assert(u);
+ pa_assert(u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT ||
+ u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY);
+ pa_assert(u->sink);
+
+ pa_sink_render_full(u->sink, u->write_block_size, &memchunk);
+
+ pa_assert(memchunk.length == u->write_block_size);
+
+ for (;;) {
+ const void *p;
+
+ /* Now write that data to the socket. The socket is of type
+ * SEQPACKET, and we generated the data of the MTU size, so this
+ * should just work. */
+
+ p = (const uint8_t *) pa_memblock_acquire_chunk(&memchunk);
+ l = pa_write(u->stream_fd, p, memchunk.length, &u->stream_write_type);
+ pa_memblock_release(memchunk.memblock);
+
+ pa_assert(l != 0);
+
+ if (l > 0)
+ break;
+
+ saved_errno = errno;
+
+ if (saved_errno == EINTR)
+ /* Retry right away if we got interrupted */
+ continue;
+
+ pa_memblock_unref(memchunk.memblock);
+
+ if (saved_errno == EAGAIN) {
+ /* Hmm, apparently the socket was not writable, give up for now.
+ * Because the data was already rendered, let's discard the block. */
+ pa_log_debug("Got EAGAIN on write() after POLLOUT, probably there is a temporary connection loss.");
+ return 1;
+ }
+
+ pa_log_error("Failed to write data to SCO socket: %s", pa_cstrerror(saved_errno));
+ return -1;
+ }
+
+ pa_assert((size_t) l <= memchunk.length);
+
+ if ((size_t) l != memchunk.length) {
+ pa_log_error("Wrote memory block to socket only partially! %llu written, wanted to write %llu.",
+ (unsigned long long) l,
+ (unsigned long long) memchunk.length);
+
+ pa_memblock_unref(memchunk.memblock);
+ return -1;
+ }
+
+ u->write_index += (uint64_t) memchunk.length;
+ pa_memblock_unref(memchunk.memblock);
+
+ return 1;
+}
+
+/* Run from IO thread */
+static int sco_process_push(struct userdata *u) {
+ ssize_t l;
+ pa_memchunk memchunk;
+ struct cmsghdr *cm;
+ struct msghdr m;
+ bool found_tstamp = false;
+ pa_usec_t tstamp = 0;
+
+ pa_assert(u);
+ pa_assert(u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT ||
+ u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY);
+ pa_assert(u->source);
+ pa_assert(u->read_smoother);
+
+ memchunk.memblock = pa_memblock_new(u->core->mempool, u->read_block_size);
+ memchunk.index = memchunk.length = 0;
+
+ for (;;) {
+ void *p;
+ uint8_t aux[1024];
+ struct iovec iov;
+
+ pa_zero(m);
+ pa_zero(aux);
+ pa_zero(iov);
+
+ m.msg_iov = &iov;
+ m.msg_iovlen = 1;
+ m.msg_control = aux;
+ m.msg_controllen = sizeof(aux);
+
+ p = pa_memblock_acquire(memchunk.memblock);
+ iov.iov_base = p;
+ iov.iov_len = pa_memblock_get_length(memchunk.memblock);
+ l = recvmsg(u->stream_fd, &m, 0);
+ pa_memblock_release(memchunk.memblock);
+
+ if (l > 0)
+ break;
+
+ if (l < 0 && errno == EINTR)
+ /* Retry right away if we got interrupted */
+ continue;
+
+ pa_memblock_unref(memchunk.memblock);
+
+ if (l < 0 && errno == EAGAIN)
+ /* Hmm, apparently the socket was not readable, give up for now. */
+ return 0;
+
+ pa_log_error("Failed to read data from SCO socket: %s", l < 0 ? pa_cstrerror(errno) : "EOF");
+ return -1;
+ }
+
+ pa_assert((size_t) l <= pa_memblock_get_length(memchunk.memblock));
+
+ /* In some rare occasions, we might receive packets of a very strange
+ * size. This could potentially be possible if the SCO packet was
+ * received partially over-the-air, or more probably due to hardware
+ * issues in our Bluetooth adapter. In these cases, in order to avoid
+ * an assertion failure due to unaligned data, just discard the whole
+ * packet */
+ if (!pa_frame_aligned(l, &u->decoder_sample_spec)) {
+ pa_log_warn("SCO packet received of unaligned size: %zu", l);
+ pa_memblock_unref(memchunk.memblock);
+ return -1;
+ }
+
+ memchunk.length = (size_t) l;
+ u->read_index += (uint64_t) l;
+
+ for (cm = CMSG_FIRSTHDR(&m); cm; cm = CMSG_NXTHDR(&m, cm))
+ if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMP) {
+ struct timeval *tv = (struct timeval*) CMSG_DATA(cm);
+ pa_rtclock_from_wallclock(tv);
+ tstamp = pa_timeval_load(tv);
+ found_tstamp = true;
+ break;
+ }
+
+ if (!found_tstamp) {
+ PA_ONCE_BEGIN {
+ pa_log_warn("Couldn't find SO_TIMESTAMP data in auxiliary recvmsg() data!");
+ } PA_ONCE_END;
+ tstamp = pa_rtclock_now();
+ }
+
+ pa_smoother_put(u->read_smoother, tstamp, pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec));
+ pa_smoother_resume(u->read_smoother, tstamp, true);
+
+ pa_source_post(u->source, &memchunk);
+ pa_memblock_unref(memchunk.memblock);
+
+ return l;
+}
+
+/* Run from IO thread */
+static void a2dp_prepare_encoder_buffer(struct userdata *u) {
+ pa_assert(u);
+
+ if (u->encoder_buffer_size < u->write_link_mtu) {
+ pa_xfree(u->encoder_buffer);
+ u->encoder_buffer = pa_xmalloc(u->write_link_mtu);
+ }
+
+ /* Encoder buffer cannot be larger then link MTU, otherwise
+ * encode method would produce larger packets then link MTU */
+ u->encoder_buffer_size = u->write_link_mtu;
+}
+
+/* Run from IO thread */
+static void a2dp_prepare_decoder_buffer(struct userdata *u) {
+ pa_assert(u);
+
+ if (u->decoder_buffer_size < u->read_link_mtu) {
+ pa_xfree(u->decoder_buffer);
+ u->decoder_buffer = pa_xmalloc(u->read_link_mtu);
+ }
+
+ /* Decoder buffer cannot be larger then link MTU, otherwise
+ * decode method would produce larger output then read_block_size */
+ u->decoder_buffer_size = u->read_link_mtu;
+}
+
+/* Run from IO thread */
+static int a2dp_write_buffer(struct userdata *u, size_t nbytes) {
+ int ret = 0;
+
+ /* Encoder function of A2DP codec may provide empty buffer, in this case do
+ * not post any empty buffer via A2DP socket. It may be because of codec
+ * internal state, e.g. encoder is waiting for more samples so it can
+ * provide encoded data. */
+ if (PA_UNLIKELY(!nbytes)) {
+ u->write_index += (uint64_t) u->write_memchunk.length;
+ pa_memblock_unref(u->write_memchunk.memblock);
+ pa_memchunk_reset(&u->write_memchunk);
+ return 0;
+ }
+
+ for (;;) {
+ ssize_t l;
+
+ l = pa_write(u->stream_fd, u->encoder_buffer, nbytes, &u->stream_write_type);
+
+ pa_assert(l != 0);
+
+ if (l < 0) {
+
+ if (errno == EINTR)
+ /* Retry right away if we got interrupted */
+ continue;
+
+ else if (errno == EAGAIN) {
+ /* Hmm, apparently the socket was not writable, give up for now */
+ pa_log_debug("Got EAGAIN on write() after POLLOUT, probably there is a temporary connection loss.");
+ break;
+ }
+
+ pa_log_error("Failed to write data to socket: %s", pa_cstrerror(errno));
+ ret = -1;
+ break;
+ }
+
+ pa_assert((size_t) l <= nbytes);
+
+ if ((size_t) l != nbytes) {
+ pa_log_warn("Wrote memory block to socket only partially! %llu written, wanted to write %llu.",
+ (unsigned long long) l,
+ (unsigned long long) nbytes);
+ ret = -1;
+ break;
+ }
+
+ u->write_index += (uint64_t) u->write_memchunk.length;
+ pa_memblock_unref(u->write_memchunk.memblock);
+ pa_memchunk_reset(&u->write_memchunk);
+
+ ret = 1;
+
+ break;
+ }
+
+ return ret;
+}
+
+/* Run from IO thread */
+static int a2dp_process_render(struct userdata *u) {
+ 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);
+
+ /* First, render some data */
+ if (!u->write_memchunk.memblock)
+ pa_sink_render_full(u->sink, u->write_block_size, &u->write_memchunk);
+
+ pa_assert(u->write_memchunk.length == u->write_block_size);
+
+ a2dp_prepare_encoder_buffer(u);
+
+ /* 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);
+
+ pa_memblock_release(u->write_memchunk.memblock);
+
+ if (processed != u->write_memchunk.length) {
+ pa_log_error("Encoding error");
+ return -1;
+ }
+
+ return a2dp_write_buffer(u, length);
+}
+
+/* Run from IO thread */
+static int a2dp_process_push(struct userdata *u) {
+ 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);
+
+ memchunk.memblock = pa_memblock_new(u->core->mempool, u->read_block_size);
+ memchunk.index = memchunk.length = 0;
+
+ a2dp_prepare_decoder_buffer(u);
+
+ for (;;) {
+ uint8_t aux[1024];
+ struct iovec iov;
+ struct cmsghdr *cm;
+ struct msghdr m;
+ bool found_tstamp = false;
+ pa_usec_t tstamp;
+ uint8_t *ptr;
+ ssize_t l;
+ size_t processed;
+
+ pa_zero(m);
+ pa_zero(aux);
+ pa_zero(iov);
+
+ m.msg_iov = &iov;
+ m.msg_iovlen = 1;
+ m.msg_control = aux;
+ m.msg_controllen = sizeof(aux);
+
+ iov.iov_base = u->decoder_buffer;
+ iov.iov_len = u->decoder_buffer_size;
+
+ l = recvmsg(u->stream_fd, &m, 0);
+
+ if (l <= 0) {
+
+ if (l < 0 && errno == EINTR)
+ /* Retry right away if we got interrupted */
+ continue;
+
+ else if (l < 0 && errno == EAGAIN)
+ /* Hmm, apparently the socket was not readable, give up for now. */
+ break;
+
+ pa_log_error("Failed to read data from socket: %s", l < 0 ? pa_cstrerror(errno) : "EOF");
+ ret = -1;
+ break;
+ }
+
+ pa_assert((size_t) l <= u->decoder_buffer_size);
+
+ /* TODO: get timestamp from rtp */
+
+ for (cm = CMSG_FIRSTHDR(&m); cm; cm = CMSG_NXTHDR(&m, cm)) {
+ if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMP) {
+ struct timeval *tv = (struct timeval*) CMSG_DATA(cm);
+ pa_rtclock_from_wallclock(tv);
+ tstamp = pa_timeval_load(tv);
+ found_tstamp = true;
+ break;
+ }
+ }
+
+ if (!found_tstamp) {
+ PA_ONCE_BEGIN {
+ pa_log_warn("Couldn't find SO_TIMESTAMP data in auxiliary recvmsg() data!");
+ } PA_ONCE_END;
+ tstamp = pa_rtclock_now();
+ }
+
+ ptr = pa_memblock_acquire(memchunk.memblock);
+ memchunk.length = pa_memblock_get_length(memchunk.memblock);
+
+ memchunk.length = u->a2dp_codec->decode_buffer(u->decoder_info, u->decoder_buffer, l, ptr, memchunk.length, &processed);
+
+ pa_memblock_release(memchunk.memblock);
+
+ if (processed != (size_t) l) {
+ pa_log_error("Decoding error");
+ ret = -1;
+ break;
+ }
+
+ u->read_index += (uint64_t) memchunk.length;
+ pa_smoother_put(u->read_smoother, tstamp, pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec));
+ pa_smoother_resume(u->read_smoother, tstamp, true);
+
+ /* Decoding of A2DP codec data may result in empty buffer, in this case
+ * do not post empty audio samples. It may happen due to algorithmic
+ * delay of audio codec. */
+ if (PA_LIKELY(memchunk.length))
+ pa_source_post(u->source, &memchunk);
+
+ ret = l;
+ break;
+ }
+
+ pa_memblock_unref(memchunk.memblock);
+
+ return ret;
+}
+
+static void update_sink_buffer_size(struct userdata *u) {
+ int old_bufsize;
+ socklen_t len = sizeof(int);
+ int ret;
+
+ ret = getsockopt(u->stream_fd, SOL_SOCKET, SO_SNDBUF, &old_bufsize, &len);
+ if (ret == -1) {
+ pa_log_warn("Changing bluetooth buffer size: Failed to getsockopt(SO_SNDBUF): %s", pa_cstrerror(errno));
+ } else {
+ int new_bufsize;
+
+ /* Set send buffer size as small as possible. The minimum value is 1024 according to the
+ * socket man page. The data is written to the socket in chunks of write_block_size, so
+ * there should at least be room for two chunks in the buffer. Generally, write_block_size
+ * is larger than 512. If not, use the next multiple of write_block_size which is larger
+ * than 1024. */
+ new_bufsize = 2 * u->write_block_size;
+ if (new_bufsize < 1024)
+ new_bufsize = (1024 / u->write_block_size + 1) * u->write_block_size;
+
+ /* The kernel internally doubles the buffer size that was set by setsockopt and getsockopt
+ * returns the doubled value. */
+ if (new_bufsize != old_bufsize / 2) {
+ ret = setsockopt(u->stream_fd, SOL_SOCKET, SO_SNDBUF, &new_bufsize, len);
+ if (ret == -1)
+ pa_log_warn("Changing bluetooth buffer size: Failed to change from %d to %d: %s", old_bufsize / 2, new_bufsize, pa_cstrerror(errno));
+ else
+ pa_log_info("Changing bluetooth buffer size: Changed from %d to %d", old_bufsize / 2, new_bufsize);
+ }
+ }
+}
+
+static void teardown_stream(struct userdata *u) {
+ if (u->rtpoll_item) {
+ pa_rtpoll_item_free(u->rtpoll_item);
+ u->rtpoll_item = NULL;
+ }
+
+ if (u->stream_fd >= 0) {
+ pa_close(u->stream_fd);
+ u->stream_fd = -1;
+ }
+
+ if (u->read_smoother) {
+ pa_smoother_free(u->read_smoother);
+ u->read_smoother = NULL;
+ }
+
+ if (u->write_memchunk.memblock) {
+ pa_memblock_unref(u->write_memchunk.memblock);
+ pa_memchunk_reset(&u->write_memchunk);
+ }
+
+ pa_log_debug("Audio stream torn down");
+ u->stream_setup_done = false;
+}
+
+static int transport_acquire(struct userdata *u, bool optional) {
+ pa_assert(u->transport);
+
+ if (u->transport_acquired)
+ return 0;
+
+ pa_log_debug("Acquiring transport %s", u->transport->path);
+
+ u->stream_fd = u->transport->acquire(u->transport, optional, &u->read_link_mtu, &u->write_link_mtu);
+ if (u->stream_fd < 0)
+ return u->stream_fd;
+
+ /* transport_acquired must be set before calling
+ * pa_bluetooth_transport_set_state() */
+ u->transport_acquired = true;
+ pa_log_info("Transport %s acquired: fd %d", u->transport->path, u->stream_fd);
+
+ if (u->transport->state == PA_BLUETOOTH_TRANSPORT_STATE_IDLE) {
+ if (pa_thread_mq_get() != NULL)
+ pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_SET_TRANSPORT_PLAYING, NULL, 0, NULL, NULL);
+ else
+ pa_bluetooth_transport_set_state(u->transport, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING);
+ }
+
+ return 0;
+}
+
+static void transport_release(struct userdata *u) {
+ pa_assert(u->transport);
+
+ /* Ignore if already released */
+ if (!u->transport_acquired)
+ return;
+
+ pa_log_debug("Releasing transport %s", u->transport->path);
+
+ u->transport->release(u->transport);
+
+ u->transport_acquired = false;
+
+ teardown_stream(u);
+
+ /* Set transport state to idle if this was not already done by the remote end closing
+ * the file descriptor. Only do this when called from the I/O thread */
+ if (pa_thread_mq_get() != NULL && u->transport->state == PA_BLUETOOTH_TRANSPORT_STATE_PLAYING)
+ pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_STREAM_FD_HUP, NULL, 0, NULL, NULL);
+}
+
+/* Run from I/O thread */
+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 ?
+ FIXED_LATENCY_PLAYBACK_A2DP : FIXED_LATENCY_PLAYBACK_SCO) +
+ pa_bytes_to_usec(u->write_block_size, &u->encoder_sample_spec));
+
+ /* If there is still data in the memchunk, we have to discard it
+ * because the write_block_size may have changed. */
+ if (u->write_memchunk.memblock) {
+ pa_memblock_unref(u->write_memchunk.memblock);
+ pa_memchunk_reset(&u->write_memchunk);
+ }
+
+ update_sink_buffer_size(u);
+}
+
+/* Run from I/O thread */
+static void transport_config_mtu(struct userdata *u) {
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ u->read_block_size = u->read_link_mtu;
+ u->write_block_size = u->write_link_mtu;
+
+ if (!pa_frame_aligned(u->read_block_size, &u->source->sample_spec)) {
+ pa_log_debug("Got invalid read MTU: %lu, rounding down", u->read_block_size);
+ u->read_block_size = pa_frame_align(u->read_block_size, &u->source->sample_spec);
+ }
+
+ if (!pa_frame_aligned(u->write_block_size, &u->sink->sample_spec)) {
+ pa_log_debug("Got invalid write MTU: %lu, rounding down", u->write_block_size);
+ 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);
+ } else {
+ u->read_block_size = u->a2dp_codec->get_read_block_size(u->decoder_info, u->read_link_mtu);
+ }
+ }
+
+ if (u->sink)
+ handle_sink_block_size_change(u);
+
+ if (u->source)
+ pa_source_set_fixed_latency_within_thread(u->source,
+ (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE ?
+ 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) {
+ struct pollfd *pollfd;
+ int one;
+
+ pa_assert(u->stream_fd >= 0);
+
+ /* return if stream is already set up */
+ if (u->stream_setup_done)
+ return 0;
+
+ 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;
+ }
+
+ transport_config_mtu(u);
+
+ pa_make_fd_nonblock(u->stream_fd);
+ pa_make_socket_low_delay(u->stream_fd);
+
+ one = 1;
+ if (setsockopt(u->stream_fd, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0)
+ pa_log_warn("Failed to enable SO_TIMESTAMP: %s", pa_cstrerror(errno));
+
+ pa_log_debug("Stream properly set up, we're ready to roll!");
+
+ u->rtpoll_item = pa_rtpoll_item_new(u->rtpoll, PA_RTPOLL_NEVER, 1);
+ pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL);
+ pollfd->fd = u->stream_fd;
+ pollfd->events = pollfd->revents = 0;
+
+ u->read_index = u->write_index = 0;
+ u->started_at = 0;
+ u->stream_setup_done = true;
+
+ if (u->source)
+ u->read_smoother = pa_smoother_new(PA_USEC_PER_SEC, 2*PA_USEC_PER_SEC, true, true, 10, pa_rtclock_now(), true);
+
+ return 0;
+}
+
+/* Called from I/O thread, returns true if the transport was acquired or
+ * a connection was requested successfully. */
+static bool setup_transport_and_stream(struct userdata *u) {
+ int transport_error;
+
+ transport_error = transport_acquire(u, false);
+ if (transport_error < 0) {
+ if (transport_error != -EAGAIN)
+ return false;
+ } else {
+ if (setup_stream(u) < 0)
+ return false;
+ }
+ return true;
+}
+
+/* Run from IO thread */
+static int source_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) {
+ struct userdata *u = PA_SOURCE(o)->userdata;
+
+ pa_assert(u->source == PA_SOURCE(o));
+ pa_assert(u->transport);
+
+ switch (code) {
+
+ case PA_SOURCE_MESSAGE_GET_LATENCY: {
+ int64_t wi, ri;
+
+ if (u->read_smoother) {
+ wi = pa_smoother_get(u->read_smoother, pa_rtclock_now());
+ ri = pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec);
+
+ *((int64_t*) data) = u->source->thread_info.fixed_latency + wi - ri;
+ } else
+ *((int64_t*) data) = 0;
+
+ return 0;
+ }
+
+ case PA_SOURCE_MESSAGE_SETUP_STREAM:
+ /* Skip stream setup if stream_fd has been invalidated.
+ This can occur if the stream has already been set up and
+ then immediately received POLLHUP. If the stream has
+ already been set up earlier, then this setup_stream()
+ call is redundant anyway, but currently the code
+ is such that this kind of unnecessary setup_stream()
+ calls can happen. */
+ if (u->stream_fd < 0)
+ pa_log_debug("Skip source stream setup while closing");
+ else
+ setup_stream(u);
+ return 0;
+
+ }
+
+ return pa_source_process_msg(o, code, data, offset, chunk);
+}
+
+/* Called from the IO thread. */
+static int source_set_state_in_io_thread_cb(pa_source *s, pa_source_state_t new_state, pa_suspend_cause_t new_suspend_cause) {
+ struct userdata *u;
+
+ pa_assert(s);
+ pa_assert_se(u = s->userdata);
+
+ switch (new_state) {
+
+ case PA_SOURCE_SUSPENDED:
+ /* Ignore if transition is PA_SOURCE_INIT->PA_SOURCE_SUSPENDED */
+ if (!PA_SOURCE_IS_OPENED(s->thread_info.state))
+ break;
+
+ /* Stop the device if the sink is suspended as well */
+ if (!u->sink || u->sink->state == PA_SINK_SUSPENDED)
+ transport_release(u);
+
+ if (u->read_smoother)
+ pa_smoother_pause(u->read_smoother, pa_rtclock_now());
+
+ break;
+
+ case PA_SOURCE_IDLE:
+ case PA_SOURCE_RUNNING:
+ if (s->thread_info.state != PA_SOURCE_SUSPENDED)
+ break;
+
+ /* Resume the device if the sink was suspended as well */
+ if (!u->sink || !PA_SINK_IS_OPENED(u->sink->thread_info.state))
+ if (!setup_transport_and_stream(u))
+ return -1;
+
+ /* We don't resume the smoother here. Instead we
+ * wait until the first packet arrives */
+
+ break;
+
+ case PA_SOURCE_UNLINKED:
+ case PA_SOURCE_INIT:
+ case PA_SOURCE_INVALID_STATE:
+ break;
+ }
+
+ return 0;
+}
+
+/* Run from main thread */
+static void source_set_volume_cb(pa_source *s) {
+ uint16_t gain;
+ pa_volume_t volume;
+ struct userdata *u;
+
+ pa_assert(s);
+ pa_assert(s->core);
+
+ u = s->userdata;
+
+ pa_assert(u);
+ pa_assert(u->source == s);
+
+ if (u->transport->set_microphone_gain == NULL)
+ return;
+
+ gain = (pa_cvolume_max(&s->real_volume) * HSP_MAX_GAIN) / PA_VOLUME_NORM;
+
+ if (gain > HSP_MAX_GAIN)
+ gain = HSP_MAX_GAIN;
+
+ volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN);
+
+ /* increment volume by one to correct rounding errors */
+ if (volume < PA_VOLUME_NORM)
+ volume++;
+
+ pa_cvolume_set(&s->real_volume, u->decoder_sample_spec.channels, volume);
+
+ /* Set soft volume when in headset role */
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY)
+ pa_cvolume_set(&s->soft_volume, u->decoder_sample_spec.channels, volume);
+
+ /* If we are in the AG role, we send a command to the head set to change
+ * the microphone gain. In the HS role, source and sink are swapped, so
+ * in this case we notify the AG that the speaker gain has changed */
+ u->transport->set_microphone_gain(u->transport, gain);
+}
+
+/* Run from main thread */
+static int add_source(struct userdata *u) {
+ pa_source_new_data data;
+
+ pa_assert(u->transport);
+
+ pa_source_new_data_init(&data);
+ data.module = u->module;
+ data.card = u->card;
+ data.driver = __FILE__;
+ data.name = pa_sprintf_malloc("bluez_source.%s.%s", u->device->address, pa_bluetooth_profile_to_string(u->profile));
+ data.namereg_fail = false;
+ pa_proplist_sets(data.proplist, "bluetooth.protocol", pa_bluetooth_profile_to_string(u->profile));
+ pa_source_new_data_set_sample_spec(&data, &u->decoder_sample_spec);
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT)
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone");
+
+ 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:
+ 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:
+ pa_assert_not_reached();
+ break;
+ }
+
+ u->source = pa_source_new(u->core, &data, PA_SOURCE_HARDWARE|PA_SOURCE_LATENCY);
+ pa_source_new_data_done(&data);
+ if (!u->source) {
+ pa_log_error("Failed to create source");
+ return -1;
+ }
+
+ u->source->userdata = u;
+ u->source->parent.process_msg = source_process_msg;
+ u->source->set_state_in_io_thread = source_set_state_in_io_thread_cb;
+
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ pa_source_set_set_volume_callback(u->source, source_set_volume_cb);
+ u->source->n_volume_steps = 16;
+ }
+ return 0;
+}
+
+/* Run from IO thread */
+static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) {
+ struct userdata *u = PA_SINK(o)->userdata;
+
+ pa_assert(u->sink == PA_SINK(o));
+ pa_assert(u->transport);
+
+ switch (code) {
+
+ case PA_SINK_MESSAGE_GET_LATENCY: {
+ int64_t wi = 0, ri = 0;
+
+ if (u->read_smoother) {
+ ri = pa_smoother_get(u->read_smoother, pa_rtclock_now());
+ wi = pa_bytes_to_usec(u->write_index + u->write_block_size, &u->encoder_sample_spec);
+ } else if (u->started_at) {
+ ri = pa_rtclock_now() - u->started_at;
+ wi = pa_bytes_to_usec(u->write_index, &u->encoder_sample_spec);
+ }
+
+ *((int64_t*) data) = u->sink->thread_info.fixed_latency + wi - ri;
+
+ return 0;
+ }
+
+ case PA_SINK_MESSAGE_SETUP_STREAM:
+ /* Skip stream setup if stream_fd has been invalidated.
+ This can occur if the stream has already been set up and
+ then immediately received POLLHUP. If the stream has
+ already been set up earlier, then this setup_stream()
+ call is redundant anyway, but currently the code
+ is such that this kind of unnecessary setup_stream()
+ calls can happen. */
+ if (u->stream_fd < 0)
+ pa_log_debug("Skip sink stream setup while closing");
+ else
+ setup_stream(u);
+ return 0;
+ }
+
+ return pa_sink_process_msg(o, code, data, offset, chunk);
+}
+
+/* Called from the IO thread. */
+static int sink_set_state_in_io_thread_cb(pa_sink *s, pa_sink_state_t new_state, pa_suspend_cause_t new_suspend_cause) {
+ struct userdata *u;
+
+ pa_assert(s);
+ pa_assert_se(u = s->userdata);
+
+ switch (new_state) {
+
+ case PA_SINK_SUSPENDED:
+ /* Ignore if transition is PA_SINK_INIT->PA_SINK_SUSPENDED */
+ if (!PA_SINK_IS_OPENED(s->thread_info.state))
+ break;
+
+ /* Stop the device if the source is suspended as well */
+ if (!u->source || u->source->state == PA_SOURCE_SUSPENDED)
+ /* We deliberately ignore whether stopping
+ * actually worked. Since the stream_fd is
+ * closed it doesn't really matter */
+ transport_release(u);
+
+ break;
+
+ case PA_SINK_IDLE:
+ case PA_SINK_RUNNING:
+ if (s->thread_info.state != PA_SINK_SUSPENDED)
+ break;
+
+ /* Resume the device if the source was suspended as well */
+ if (!u->source || !PA_SOURCE_IS_OPENED(u->source->thread_info.state))
+ if (!setup_transport_and_stream(u))
+ return -1;
+
+ break;
+
+ case PA_SINK_UNLINKED:
+ case PA_SINK_INIT:
+ case PA_SINK_INVALID_STATE:
+ break;
+ }
+
+ return 0;
+}
+
+/* Run from main thread */
+static void sink_set_volume_cb(pa_sink *s) {
+ uint16_t gain;
+ pa_volume_t volume;
+ struct userdata *u;
+
+ pa_assert(s);
+ pa_assert(s->core);
+
+ u = s->userdata;
+
+ pa_assert(u);
+ pa_assert(u->sink == s);
+
+ if (u->transport->set_speaker_gain == NULL)
+ return;
+
+ gain = (pa_cvolume_max(&s->real_volume) * HSP_MAX_GAIN) / PA_VOLUME_NORM;
+
+ if (gain > HSP_MAX_GAIN)
+ gain = HSP_MAX_GAIN;
+
+ volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN);
+
+ /* increment volume by one to correct rounding errors */
+ if (volume < PA_VOLUME_NORM)
+ volume++;
+
+ pa_cvolume_set(&s->real_volume, u->encoder_sample_spec.channels, volume);
+
+ /* Set soft volume when in headset role */
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY)
+ pa_cvolume_set(&s->soft_volume, u->encoder_sample_spec.channels, volume);
+
+ /* If we are in the AG role, we send a command to the head set to change
+ * the speaker gain. In the HS role, source and sink are swapped, so
+ * in this case we notify the AG that the microphone gain has changed */
+ u->transport->set_speaker_gain(u->transport, gain);
+}
+
+/* Run from main thread */
+static int add_sink(struct userdata *u) {
+ pa_sink_new_data data;
+
+ pa_assert(u->transport);
+
+ pa_sink_new_data_init(&data);
+ data.module = u->module;
+ data.card = u->card;
+ data.driver = __FILE__;
+ data.name = pa_sprintf_malloc("bluez_sink.%s.%s", u->device->address, pa_bluetooth_profile_to_string(u->profile));
+ data.namereg_fail = false;
+ pa_proplist_sets(data.proplist, "bluetooth.protocol", pa_bluetooth_profile_to_string(u->profile));
+ pa_sink_new_data_set_sample_spec(&data, &u->encoder_sample_spec);
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT)
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone");
+
+ connect_ports(u, &data, PA_DIRECTION_OUTPUT);
+
+ if (!u->transport_acquired)
+ switch (u->profile) {
+ case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY:
+ 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:
+ /* Profile switch should have failed */
+ case PA_BLUETOOTH_PROFILE_A2DP_SOURCE:
+ case PA_BLUETOOTH_PROFILE_OFF:
+ pa_assert_not_reached();
+ break;
+ }
+
+ u->sink = pa_sink_new(u->core, &data, PA_SINK_HARDWARE|PA_SINK_LATENCY);
+ pa_sink_new_data_done(&data);
+ if (!u->sink) {
+ pa_log_error("Failed to create sink");
+ return -1;
+ }
+
+ u->sink->userdata = u;
+ u->sink->parent.process_msg = sink_process_msg;
+ u->sink->set_state_in_io_thread = sink_set_state_in_io_thread_cb;
+
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ pa_sink_set_set_volume_callback(u->sink, sink_set_volume_cb);
+ u->sink->n_volume_steps = 16;
+ }
+ return 0;
+}
+
+/* Run from main thread */
+static int transport_config(struct userdata *u) {
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ u->encoder_sample_spec.format = PA_SAMPLE_S16LE;
+ u->encoder_sample_spec.channels = 1;
+ u->encoder_sample_spec.rate = 8000;
+ u->decoder_sample_spec.format = PA_SAMPLE_S16LE;
+ u->decoder_sample_spec.channels = 1;
+ u->decoder_sample_spec.rate = 8000;
+ return 0;
+ } else {
+ bool is_a2dp_sink = u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK;
+ void *info;
+
+ pa_assert(u->transport);
+
+ pa_assert(!u->a2dp_codec);
+ pa_assert(!u->encoder_info);
+ pa_assert(!u->decoder_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);
+ if (is_a2dp_sink)
+ u->encoder_info = info;
+ else
+ u->decoder_info = info;
+
+ if (!info)
+ return -1;
+
+ return 0;
+ }
+}
+
+/* Run from main thread */
+static int setup_transport(struct userdata *u) {
+ pa_bluetooth_transport *t;
+
+ pa_assert(u);
+ pa_assert(!u->transport);
+ pa_assert(u->profile != PA_BLUETOOTH_PROFILE_OFF);
+
+ /* 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;
+ }
+
+ u->transport = t;
+
+ if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE || 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;
+
+ transport_error = transport_acquire(u, false);
+ if (transport_error < 0 && transport_error != -EAGAIN)
+ return -1; /* We need to fail here until the interactions with module-suspend-on-idle and alike get improved */
+ }
+
+ return transport_config(u);
+}
+
+/* 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];
+}
+
+/* Run from main thread */
+static int init_profile(struct userdata *u) {
+ int r = 0;
+ pa_assert(u);
+ pa_assert(u->profile != PA_BLUETOOTH_PROFILE_OFF);
+
+ if (setup_transport(u) < 0)
+ return -1;
+
+ pa_assert(u->transport);
+
+ if (get_profile_direction (u->profile) & PA_DIRECTION_OUTPUT)
+ if (add_sink(u) < 0)
+ r = -1;
+
+ if (get_profile_direction (u->profile) & PA_DIRECTION_INPUT)
+ if (add_source(u) < 0)
+ r = -1;
+
+ return r;
+}
+
+static int write_block(struct userdata *u) {
+ int n_written;
+
+ if (u->write_index <= 0)
+ u->started_at = pa_rtclock_now();
+
+ if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) {
+ if ((n_written = a2dp_process_render(u)) < 0)
+ return -1;
+ } else {
+ if ((n_written = sco_process_render(u)) < 0)
+ return -1;
+ }
+
+ return n_written;
+}
+
+
+/* I/O thread function */
+static void thread_func(void *userdata) {
+ struct userdata *u = userdata;
+ unsigned blocks_to_write = 0;
+ unsigned bytes_to_write = 0;
+
+ pa_assert(u);
+ pa_assert(u->transport);
+
+ pa_log_debug("IO Thread starting up");
+
+ if (u->core->realtime_scheduling)
+ pa_thread_make_realtime(u->core->realtime_priority);
+
+ pa_thread_mq_install(&u->thread_mq);
+
+ /* Setup the stream only if the transport was already acquired */
+ if (u->transport_acquired)
+ setup_stream(u);
+
+ for (;;) {
+ struct pollfd *pollfd;
+ int ret;
+ bool disable_timer = true;
+ bool writable = false;
+ bool have_source = u->source ? PA_SOURCE_IS_LINKED(u->source->thread_info.state) : false;
+ bool have_sink = u->sink ? PA_SINK_IS_LINKED(u->sink->thread_info.state) : false;
+
+ pollfd = u->rtpoll_item ? pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL) : NULL;
+
+ /* Check for stream error or close */
+ if (pollfd && (pollfd->revents & ~(POLLOUT|POLLIN))) {
+ pa_log_info("FD error: %s%s%s%s",
+ pollfd->revents & POLLERR ? "POLLERR " :"",
+ pollfd->revents & POLLHUP ? "POLLHUP " :"",
+ pollfd->revents & POLLPRI ? "POLLPRI " :"",
+ pollfd->revents & POLLNVAL ? "POLLNVAL " :"");
+
+ if (pollfd->revents & POLLHUP) {
+ pollfd = NULL;
+ teardown_stream(u);
+ blocks_to_write = 0;
+ bytes_to_write = 0;
+ pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_STREAM_FD_HUP, NULL, 0, NULL, NULL);
+ } else
+ goto fail;
+ }
+
+ /* If there is a pollfd, the stream is set up and we need to do something */
+ if (pollfd) {
+
+ /* Handle source if present */
+ if (have_source) {
+
+ /* We should send two blocks to the device before we expect a response. */
+ if (have_sink && u->write_index == 0 && u->read_index <= 0)
+ blocks_to_write = 2;
+
+ /* If we got woken up by POLLIN let's do some reading */
+ if (pollfd->revents & POLLIN) {
+ int n_read;
+
+ if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE)
+ n_read = a2dp_process_push(u);
+ else
+ n_read = sco_process_push(u);
+
+ if (n_read < 0)
+ goto fail;
+
+ if (have_sink && 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;
+ bytes_to_write = bytes_to_write % u->write_block_size;
+ }
+ }
+ }
+
+ /* Handle sink if present */
+ if (have_sink) {
+
+ /* Process rewinds */
+ if (PA_UNLIKELY(u->sink->thread_info.rewind_requested))
+ pa_sink_process_rewind(u->sink, 0);
+
+ /* Test if the stream is writable */
+ if (pollfd->revents & POLLOUT)
+ writable = true;
+
+ /* If we have a source, we let the source determine the timing
+ * for the sink */
+ if (have_source) {
+
+ if (writable && blocks_to_write > 0) {
+ int result;
+
+ if ((result = write_block(u)) < 0)
+ goto fail;
+
+ blocks_to_write -= result;
+
+ /* writable controls whether we set POLLOUT when polling - we set it to
+ * false to enable POLLOUT. If there are more blocks to write, we want to
+ * be woken up immediately when the socket becomes writable. If there
+ * aren't currently any more blocks to write, then we'll have to wait
+ * until we've received more data, so in that case we only want to set
+ * POLLIN. Note that when we are woken up the next time, POLLOUT won't be
+ * set in revents even if the socket has meanwhile become writable, which
+ * may seem bad, but in that case we'll set POLLOUT in the subsequent
+ * poll, and the poll will return immediately, so our writes won't be
+ * delayed. */
+ if (blocks_to_write > 0)
+ writable = false;
+ }
+
+ /* There is no source, we have to use the system clock for timing */
+ } else {
+ bool have_written = false;
+ pa_usec_t time_passed = 0;
+ pa_usec_t audio_sent = 0;
+
+ if (u->started_at) {
+ time_passed = pa_rtclock_now() - u->started_at;
+ audio_sent = pa_bytes_to_usec(u->write_index, &u->encoder_sample_spec);
+ }
+
+ /* A new block needs to be sent. */
+ if (audio_sent <= time_passed) {
+ size_t bytes_to_send = pa_usec_to_bytes(time_passed - audio_sent, &u->encoder_sample_spec);
+
+ /* There are more than two blocks that need to be written. It seems that
+ * the socket has not been accepting data fast enough (could be due to
+ * hiccups in the wireless transmission). We need to discard everything
+ * older than two block sizes to keep the latency from growing. */
+ if (bytes_to_send > 2 * u->write_block_size) {
+ uint64_t skip_bytes;
+ pa_memchunk tmp;
+ size_t mempool_max_block_size = pa_mempool_block_size_max(u->core->mempool);
+ pa_usec_t skip_usec;
+
+ skip_bytes = bytes_to_send - 2 * u->write_block_size;
+ skip_usec = pa_bytes_to_usec(skip_bytes, &u->encoder_sample_spec);
+
+ pa_log_debug("Skipping %llu us (= %llu bytes) in audio stream",
+ (unsigned long long) skip_usec,
+ (unsigned long long) skip_bytes);
+
+ while (skip_bytes > 0) {
+ size_t bytes_to_render;
+
+ if (skip_bytes > mempool_max_block_size)
+ bytes_to_render = mempool_max_block_size;
+ else
+ bytes_to_render = skip_bytes;
+
+ pa_sink_render_full(u->sink, bytes_to_render, &tmp);
+ pa_memblock_unref(tmp.memblock);
+ u->write_index += bytes_to_render;
+ 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 (new_write_block_size) {
+ u->write_block_size = new_write_block_size;
+ handle_sink_block_size_change(u);
+ }
+ }
+ }
+
+ blocks_to_write = 1;
+ }
+
+ /* If the stream is writable, send some data if necessary */
+ if (writable && blocks_to_write > 0) {
+ int result;
+
+ if ((result = write_block(u)) < 0)
+ goto fail;
+
+ blocks_to_write -= result;
+ writable = false;
+ if (result)
+ have_written = true;
+ }
+
+ /* If nothing was written during this iteration, either the stream
+ * is not writable or there was no write pending. Set up a timer that
+ * will wake up the thread when the next data needs to be written. */
+ if (!have_written) {
+ pa_usec_t sleep_for;
+ pa_usec_t next_write_at;
+
+ if (writable) {
+ /* There was no write pending on this iteration of the loop.
+ * Let's estimate when we need to wake up next */
+ next_write_at = pa_bytes_to_usec(u->write_index, &u->encoder_sample_spec);
+ sleep_for = time_passed < next_write_at ? next_write_at - time_passed : 0;
+ /* pa_log("Sleeping for %lu; time passed %lu, next write at %lu", (unsigned long) sleep_for, (unsigned long) time_passed, (unsigned long)next_write_at); */
+ } else
+ /* We could not write because the stream was not ready. Let's try
+ * again in 500 ms and drop audio if we still can't write. The
+ * thread will also be woken up when we can write again. */
+ sleep_for = PA_USEC_PER_MSEC * 500;
+
+ pa_rtpoll_set_timer_relative(u->rtpoll, sleep_for);
+ disable_timer = false;
+ }
+ }
+ }
+
+ /* Set events to wake up the thread */
+ pollfd->events = (short) (((have_sink && !writable) ? POLLOUT : 0) | (have_source ? POLLIN : 0));
+
+ }
+
+ if (disable_timer)
+ pa_rtpoll_set_timer_disabled(u->rtpoll);
+
+ if ((ret = pa_rtpoll_run(u->rtpoll)) < 0) {
+ pa_log_debug("pa_rtpoll_run failed with: %d", ret);
+ goto fail;
+ }
+
+ if (ret == 0) {
+ pa_log_debug("IO thread shutdown requested, stopping cleanly");
+ transport_release(u);
+ goto finish;
+ }
+ }
+
+fail:
+ /* If this was no regular exit from the loop we have to continue processing messages until we receive PA_MESSAGE_SHUTDOWN */
+ pa_log_debug("IO thread failed");
+ pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_IO_THREAD_FAILED, NULL, 0, NULL, NULL);
+ pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN);
+
+finish:
+ pa_log_debug("IO thread shutting down");
+}
+
+/* Run from main thread */
+static int start_thread(struct userdata *u) {
+ pa_assert(u);
+ pa_assert(!u->thread);
+ pa_assert(!u->rtpoll);
+ pa_assert(!u->rtpoll_item);
+
+ u->rtpoll = pa_rtpoll_new();
+
+ if (pa_thread_mq_init(&u->thread_mq, u->core->mainloop, u->rtpoll) < 0) {
+ pa_log("pa_thread_mq_init() failed.");
+ return -1;
+ }
+
+ if (!(u->thread = pa_thread_new("bluetooth", thread_func, u))) {
+ pa_log_error("Failed to create IO thread");
+ return -1;
+ }
+
+ if (u->sink) {
+ pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq);
+ pa_sink_set_rtpoll(u->sink, u->rtpoll);
+
+ /* If we are in the headset role, the sink should not become default
+ * unless there is no other sound device available. */
+ if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY)
+ u->sink->priority = 1500;
+
+ pa_sink_put(u->sink);
+
+ if (u->sink->set_volume)
+ u->sink->set_volume(u->sink);
+ }
+
+ if (u->source) {
+ pa_source_set_asyncmsgq(u->source, u->thread_mq.inq);
+ pa_source_set_rtpoll(u->source, u->rtpoll);
+
+ /* 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)
+ u->source->priority = 1500;
+
+ pa_source_put(u->source);
+
+ if (u->source->set_volume)
+ u->source->set_volume(u->source);
+ }
+
+ return 0;
+}
+
+/* Run from main thread */
+static void stop_thread(struct userdata *u) {
+ pa_assert(u);
+
+ if (u->sink)
+ pa_sink_unlink(u->sink);
+
+ if (u->source)
+ pa_source_unlink(u->source);
+
+ if (u->thread) {
+ pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL);
+ pa_thread_free(u->thread);
+ u->thread = NULL;
+ }
+
+ if (u->rtpoll_item) {
+ pa_rtpoll_item_free(u->rtpoll_item);
+ u->rtpoll_item = NULL;
+ }
+
+ if (u->rtpoll) {
+ pa_rtpoll_free(u->rtpoll);
+ u->rtpoll = NULL;
+ pa_thread_mq_done(&u->thread_mq);
+ }
+
+ if (u->transport) {
+ transport_release(u);
+ u->transport = NULL;
+ }
+
+ if (u->sink) {
+ pa_sink_unref(u->sink);
+ u->sink = NULL;
+ }
+
+ if (u->source) {
+ pa_source_unref(u->source);
+ u->source = NULL;
+ }
+
+ if (u->read_smoother) {
+ pa_smoother_free(u->read_smoother);
+ u->read_smoother = NULL;
+ }
+
+ if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) {
+ if (u->encoder_info) {
+ u->a2dp_codec->deinit(u->encoder_info);
+ u->encoder_info = NULL;
+ }
+
+ if (u->decoder_info) {
+ u->a2dp_codec->deinit(u->decoder_info);
+ u->decoder_info = NULL;
+ }
+
+ u->a2dp_codec = 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;
+
+ pa_assert(u);
+ pa_assert(u->device);
+
+ for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) {
+ pa_bluetooth_transport *transport;
+
+ if (!(get_profile_direction(i) & direction))
+ continue;
+
+ if (!(transport = u->device->transports[i]))
+ continue;
+
+ switch(transport->state) {
+ case PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED:
+ continue;
+
+ case PA_BLUETOOTH_TRANSPORT_STATE_IDLE:
+ if (result == PA_AVAILABLE_NO)
+ result = PA_AVAILABLE_UNKNOWN;
+
+ break;
+
+ case PA_BLUETOOTH_TRANSPORT_STATE_PLAYING:
+ return PA_AVAILABLE_YES;
+ }
+ }
+
+ return result;
+}
+
+/* Run from main thread */
+static pa_available_t transport_state_to_availability(pa_bluetooth_transport_state_t state) {
+ switch (state) {
+ case PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED:
+ return PA_AVAILABLE_NO;
+ case PA_BLUETOOTH_TRANSPORT_STATE_PLAYING:
+ return PA_AVAILABLE_YES;
+ default:
+ return PA_AVAILABLE_UNKNOWN;
+ }
+}
+
+/* Run from main thread */
+static void create_card_ports(struct userdata *u, pa_hashmap *ports) {
+ pa_device_port *port;
+ pa_device_port_new_data port_data;
+ pa_device_port_type_t input_type, output_type;
+ const char *name_prefix, *input_description, *output_description;
+
+ pa_assert(u);
+ pa_assert(ports);
+ pa_assert(u->device);
+
+ name_prefix = "unknown";
+ input_description = _("Bluetooth Input");
+ output_description = _("Bluetooth Output");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_BLUETOOTH;
+
+ switch (form_factor_from_class(u->device->class_of_device)) {
+ case PA_BLUETOOTH_FORM_FACTOR_HEADSET:
+ name_prefix = "headset";
+ input_description = output_description = _("Headset");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_HEADSET;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_HANDSFREE:
+ name_prefix = "handsfree";
+ input_description = output_description = _("Handsfree");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_HANDSFREE;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_MICROPHONE:
+ name_prefix = "microphone";
+ input_description = _("Microphone");
+ output_description = _("Bluetooth Output");
+ input_type = PA_DEVICE_PORT_TYPE_MIC;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_SPEAKER:
+ name_prefix = "speaker";
+ input_description = _("Bluetooth Input");
+ output_description = _("Speaker");
+ output_type = PA_DEVICE_PORT_TYPE_SPEAKER;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_HEADPHONE:
+ name_prefix = "headphone";
+ input_description = _("Bluetooth Input");
+ output_description = _("Headphone");
+ output_type = PA_DEVICE_PORT_TYPE_HEADPHONES;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_PORTABLE:
+ name_prefix = "portable";
+ input_description = output_description = _("Portable");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_PORTABLE;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_CAR:
+ name_prefix = "car";
+ input_description = output_description = _("Car");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_CAR;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_HIFI:
+ name_prefix = "hifi";
+ input_description = output_description = _("HiFi");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_HIFI;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_PHONE:
+ name_prefix = "phone";
+ input_description = output_description = _("Phone");
+ input_type = output_type = PA_DEVICE_PORT_TYPE_PHONE;
+ break;
+
+ case PA_BLUETOOTH_FORM_FACTOR_UNKNOWN:
+ break;
+ }
+
+ u->output_port_name = pa_sprintf_malloc("%s-output", name_prefix);
+ pa_device_port_new_data_init(&port_data);
+ pa_device_port_new_data_set_name(&port_data, u->output_port_name);
+ pa_device_port_new_data_set_description(&port_data, output_description);
+ pa_device_port_new_data_set_direction(&port_data, PA_DIRECTION_OUTPUT);
+ pa_device_port_new_data_set_type(&port_data, output_type);
+ pa_device_port_new_data_set_available(&port_data, get_port_availability(u, PA_DIRECTION_OUTPUT));
+ pa_assert_se(port = pa_device_port_new(u->core, &port_data, 0));
+ pa_assert_se(pa_hashmap_put(ports, port->name, port) >= 0);
+ pa_device_port_new_data_done(&port_data);
+
+ u->input_port_name = pa_sprintf_malloc("%s-input", name_prefix);
+ pa_device_port_new_data_init(&port_data);
+ pa_device_port_new_data_set_name(&port_data, u->input_port_name);
+ pa_device_port_new_data_set_description(&port_data, input_description);
+ pa_device_port_new_data_set_direction(&port_data, PA_DIRECTION_INPUT);
+ pa_device_port_new_data_set_type(&port_data, input_type);
+ pa_device_port_new_data_set_available(&port_data, get_port_availability(u, PA_DIRECTION_INPUT));
+ pa_assert_se(port = pa_device_port_new(u->core, &port_data, 0));
+ pa_assert_se(pa_hashmap_put(ports, port->name, port) >= 0);
+ pa_device_port_new_data_done(&port_data);
+}
+
+/* Run from main thread */
+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;
+ pa_card_profile *cp = NULL;
+ pa_bluetooth_profile_t *p;
+
+ pa_assert(u->input_port_name);
+ pa_assert(u->output_port_name);
+ pa_assert_se(input_port = pa_hashmap_get(ports, u->input_port_name));
+ pa_assert_se(output_port = pa_hashmap_get(ports, u->output_port_name));
+
+ 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:
+ cp = pa_card_profile_new(name, _("Headset Head Unit (HSP/HFP)"), sizeof(pa_bluetooth_profile_t));
+ cp->priority = 30;
+ cp->n_sinks = 1;
+ cp->n_sources = 1;
+ cp->max_sink_channels = 1;
+ cp->max_source_channels = 1;
+ pa_hashmap_put(input_port->profiles, cp->name, cp);
+ pa_hashmap_put(output_port->profiles, cp->name, cp);
+
+ p = PA_CARD_PROFILE_DATA(cp);
+ break;
+
+ case 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->n_sinks = 1;
+ cp->n_sources = 1;
+ cp->max_sink_channels = 1;
+ cp->max_source_channels = 1;
+ pa_hashmap_put(input_port->profiles, cp->name, cp);
+ pa_hashmap_put(output_port->profiles, cp->name, cp);
+
+ p = PA_CARD_PROFILE_DATA(cp);
+ break;
+
+ case PA_BLUETOOTH_PROFILE_OFF:
+ pa_assert_not_reached();
+ }
+
+ *p = profile;
+
+ if (u->device->transports[*p])
+ cp->available = transport_state_to_availability(u->device->transports[*p]->state);
+ else
+ cp->available = PA_AVAILABLE_NO;
+
+ return cp;
+}
+
+/* Run from main thread */
+static int set_profile_cb(pa_card *c, pa_card_profile *new_profile) {
+ struct userdata *u;
+ pa_bluetooth_profile_t *p;
+
+ pa_assert(c);
+ pa_assert(new_profile);
+ pa_assert_se(u = c->userdata);
+
+ p = PA_CARD_PROFILE_DATA(new_profile);
+
+ 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) {
+ pa_log_warn("Refused to switch profile to %s: Not connected", new_profile->name);
+ return -PA_ERR_IO;
+ }
+ }
+
+ stop_thread(u);
+
+ u->profile = *p;
+
+ 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 0;
+
+off:
+ stop_thread(u);
+
+ pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0);
+
+ 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;
+}
+
+/* Run from main thread */
+static int add_card(struct userdata *u) {
+ const pa_bluetooth_device *d;
+ pa_card_new_data data;
+ char *alias;
+ pa_bluetooth_form_factor_t ff;
+ pa_card_profile *cp;
+ pa_bluetooth_profile_t *p;
+ const char *uuid;
+ void *state;
+
+ pa_assert(u);
+ pa_assert(u->device);
+
+ d = u->device;
+
+ pa_card_new_data_init(&data);
+ data.driver = __FILE__;
+ data.module = u->module;
+
+ alias = pa_utf8_filter(d->alias);
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_DESCRIPTION, alias);
+ pa_xfree(alias);
+
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, d->address);
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_API, "bluez");
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_CLASS, "sound");
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_BUS, "bluetooth");
+
+ if ((ff = form_factor_from_class(d->class_of_device)) != PA_BLUETOOTH_FORM_FACTOR_UNKNOWN)
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_FORM_FACTOR, form_factor_to_string(ff));
+
+ pa_proplist_sets(data.proplist, "bluez.path", d->path);
+ pa_proplist_setf(data.proplist, "bluez.class", "0x%06x", d->class_of_device);
+ pa_proplist_sets(data.proplist, "bluez.alias", d->alias);
+ data.name = pa_sprintf_malloc("bluez_card.%s", d->address);
+ data.namereg_fail = false;
+
+ create_card_ports(u, data.ports);
+
+ PA_HASHMAP_FOREACH(uuid, d->uuids, state) {
+ pa_bluetooth_profile_t profile;
+
+ if (uuid_to_profile(uuid, &profile) < 0)
+ continue;
+
+ if (pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(profile)))
+ continue;
+
+ cp = create_card_profile(u, profile, data.ports);
+ pa_hashmap_put(data.profiles, cp->name, cp);
+ }
+
+ pa_assert(!pa_hashmap_isempty(data.profiles));
+
+ cp = pa_card_profile_new("off", _("Off"), sizeof(pa_bluetooth_profile_t));
+ cp->available = PA_AVAILABLE_YES;
+ p = PA_CARD_PROFILE_DATA(cp);
+ *p = PA_BLUETOOTH_PROFILE_OFF;
+ pa_hashmap_put(data.profiles, cp->name, cp);
+
+ u->card = pa_card_new(u->core, &data);
+ pa_card_new_data_done(&data);
+ if (!u->card) {
+ pa_log("Failed to allocate card.");
+ return -1;
+ }
+
+ u->card->userdata = u;
+ u->card->set_profile = set_profile_cb;
+ pa_card_choose_initial_profile(u->card);
+ pa_card_put(u->card);
+
+ p = PA_CARD_PROFILE_DATA(u->card->active_profile);
+ u->profile = *p;
+
+ return 0;
+}
+
+/* Run from main thread */
+static void handle_transport_state_change(struct userdata *u, struct pa_bluetooth_transport *t) {
+ bool acquire = false;
+ bool release = false;
+ pa_card_profile *cp;
+ pa_device_port *port;
+ pa_available_t oldavail;
+
+ 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));
+
+ /* Update port availability */
+ pa_assert_se(port = pa_hashmap_get(u->card->ports, u->output_port_name));
+ pa_device_port_set_available(port, get_port_availability(u, PA_DIRECTION_OUTPUT));
+ pa_assert_se(port = pa_hashmap_get(u->card->ports, u->input_port_name));
+ pa_device_port_set_available(port, get_port_availability(u, PA_DIRECTION_INPUT));
+
+ /* Acquire or release transport as needed */
+ acquire = (t->state == PA_BLUETOOTH_TRANSPORT_STATE_PLAYING && u->profile == t->profile);
+ release = (oldavail != PA_AVAILABLE_NO && t->state != PA_BLUETOOTH_TRANSPORT_STATE_PLAYING && u->profile == t->profile);
+
+ if (acquire && transport_acquire(u, true) >= 0) {
+ if (u->source) {
+ pa_log_debug("Resuming source %s because its transport state changed to playing", u->source->name);
+
+ /* When the ofono backend resumes source or sink when in the audio gateway role, the
+ * state of source or sink may already be RUNNING before the transport is acquired via
+ * hf_audio_agent_new_connection(), so the pa_source_suspend() call will not lead to a
+ * state change message. In this case we explicitly need to signal the I/O thread to
+ * set up the stream. */
+ if (PA_SOURCE_IS_OPENED(u->source->state))
+ pa_asyncmsgq_send(u->source->asyncmsgq, PA_MSGOBJECT(u->source), PA_SOURCE_MESSAGE_SETUP_STREAM, NULL, 0, NULL);
+
+ /* We remove the IDLE suspend cause, because otherwise
+ * module-loopback doesn't uncork its streams. FIXME: Messing with
+ * the IDLE suspend cause here is wrong, the correct way to handle
+ * this would probably be to uncork the loopback streams not only
+ * when the other end is unsuspended, but also when the other end's
+ * suspend cause changes to IDLE only (currently there's no
+ * notification mechanism for suspend cause changes, though). */
+ pa_source_suspend(u->source, false, PA_SUSPEND_IDLE|PA_SUSPEND_USER);
+ }
+
+ if (u->sink) {
+ pa_log_debug("Resuming sink %s because its transport state changed to playing", u->sink->name);
+
+ /* Same comment as above */
+ if (PA_SINK_IS_OPENED(u->sink->state))
+ pa_asyncmsgq_send(u->sink->asyncmsgq, PA_MSGOBJECT(u->sink), PA_SINK_MESSAGE_SETUP_STREAM, NULL, 0, NULL);
+
+ /* FIXME: See the previous comment. */
+ pa_sink_suspend(u->sink, false, PA_SUSPEND_IDLE|PA_SUSPEND_USER);
+ }
+ }
+
+ if (release && u->transport_acquired) {
+ /* FIXME: this release is racy, since the audio stream might have
+ * been set up again in the meantime (but not processed yet by PA).
+ * BlueZ should probably release the transport automatically, and in
+ * that case we would just mark the transport as released */
+
+ /* Remote side closed the stream so we consider it PA_SUSPEND_USER */
+ if (u->source) {
+ pa_log_debug("Suspending source %s because the remote end closed the stream", u->source->name);
+ pa_source_suspend(u->source, true, PA_SUSPEND_USER);
+ }
+
+ if (u->sink) {
+ pa_log_debug("Suspending sink %s because the remote end closed the stream", u->sink->name);
+ pa_sink_suspend(u->sink, true, PA_SUSPEND_USER);
+ }
+ }
+}
+
+/* Run from main thread */
+static pa_hook_result_t device_connection_changed_cb(pa_bluetooth_discovery *y, const pa_bluetooth_device *d, struct userdata *u) {
+ pa_assert(d);
+ pa_assert(u);
+
+ if (d != u->device || pa_bluetooth_device_any_transport_connected(d))
+ return PA_HOOK_OK;
+
+ pa_log_debug("Unloading module for device %s", d->path);
+ pa_module_unload(u->module, true);
+
+ return PA_HOOK_OK;
+}
+
+/* Run from main thread */
+static pa_hook_result_t transport_state_changed_cb(pa_bluetooth_discovery *y, pa_bluetooth_transport *t, struct userdata *u) {
+ pa_assert(t);
+ pa_assert(u);
+
+ if (t == u->transport && t->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED)
+ pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0);
+
+ if (t->device == u->device)
+ handle_transport_state_change(u, t);
+
+ return PA_HOOK_OK;
+}
+
+static pa_hook_result_t transport_speaker_gain_changed_cb(pa_bluetooth_discovery *y, pa_bluetooth_transport *t, struct userdata *u) {
+ pa_volume_t volume;
+ pa_cvolume v;
+ uint16_t gain;
+
+ pa_assert(t);
+ pa_assert(u);
+
+ if (t != u->transport)
+ return PA_HOOK_OK;
+
+ gain = t->speaker_gain;
+ volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN);
+
+ /* increment volume by one to correct rounding errors */
+ if (volume < PA_VOLUME_NORM)
+ volume++;
+
+ pa_cvolume_set(&v, u->encoder_sample_spec.channels, volume);
+ if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT)
+ pa_sink_volume_changed(u->sink, &v);
+ else
+ pa_sink_set_volume(u->sink, &v, true, true);
+
+ return PA_HOOK_OK;
+}
+
+static pa_hook_result_t transport_microphone_gain_changed_cb(pa_bluetooth_discovery *y, pa_bluetooth_transport *t, struct userdata *u) {
+ pa_volume_t volume;
+ pa_cvolume v;
+ uint16_t gain;
+
+ pa_assert(t);
+ pa_assert(u);
+
+ if (t != u->transport)
+ return PA_HOOK_OK;
+
+ gain = t->microphone_gain;
+ volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN);
+
+ /* increment volume by one to correct rounding errors */
+ if (volume < PA_VOLUME_NORM)
+ volume++;
+
+ pa_cvolume_set(&v, u->decoder_sample_spec.channels, volume);
+
+ if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT)
+ pa_source_volume_changed(u->source, &v);
+ else
+ pa_source_set_volume(u->source, &v, true, true);
+
+ return PA_HOOK_OK;
+}
+
+/* Run from main thread context */
+static int device_process_msg(pa_msgobject *obj, int code, void *data, int64_t offset, pa_memchunk *chunk) {
+ struct bluetooth_msg *m = BLUETOOTH_MSG(obj);
+ struct userdata *u = m->card->userdata;
+
+ switch (code) {
+ case BLUETOOTH_MESSAGE_IO_THREAD_FAILED:
+ if (m->card->module->unload_requested)
+ break;
+
+ pa_log_debug("Switching the profile to off due to IO thread failure.");
+ pa_assert_se(pa_card_set_profile(m->card, pa_hashmap_get(m->card->profiles, "off"), false) >= 0);
+ break;
+ case BLUETOOTH_MESSAGE_STREAM_FD_HUP:
+ if (u->transport->state > PA_BLUETOOTH_TRANSPORT_STATE_IDLE)
+ pa_bluetooth_transport_set_state(u->transport, PA_BLUETOOTH_TRANSPORT_STATE_IDLE);
+ break;
+ case BLUETOOTH_MESSAGE_SET_TRANSPORT_PLAYING:
+ /* transport_acquired needs to be checked here, because a message could have been
+ * pending when the profile was switched. If the new transport has been acquired
+ * correctly, the call below will have no effect because the transport state is
+ * already PLAYING. If transport_acquire() failed for the new profile, the transport
+ * state should not be changed. If the transport has been released for other reasons
+ * (I/O thread shutdown), transport_acquired will also be false. */
+ if (u->transport_acquired)
+ pa_bluetooth_transport_set_state(u->transport, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING);
+ break;
+ }
+
+ return 0;
+}
+
+int pa__init(pa_module* m) {
+ struct userdata *u;
+ const char *path;
+ pa_modargs *ma;
+ bool autodetect_mtu;
+
+ pa_assert(m);
+
+ m->userdata = u = pa_xnew0(struct userdata, 1);
+ u->module = m;
+ u->core = m->core;
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log_error("Failed to parse module arguments");
+ goto fail_free_modargs;
+ }
+
+ if (!(path = pa_modargs_get_value(ma, "path", NULL))) {
+ pa_log_error("Failed to get device path from module arguments");
+ goto fail_free_modargs;
+ }
+
+ if ((u->discovery = pa_shared_get(u->core, "bluetooth-discovery")))
+ pa_bluetooth_discovery_ref(u->discovery);
+ else {
+ pa_log_error("module-bluez5-discover doesn't seem to be loaded, refusing to load module-bluez5-device");
+ goto fail_free_modargs;
+ }
+
+ if (!(u->device = pa_bluetooth_discovery_get_device_by_path(u->discovery, path))) {
+ pa_log_error("%s is unknown", path);
+ goto fail_free_modargs;
+ }
+
+ autodetect_mtu = false;
+ if (pa_modargs_get_value_boolean(ma, "autodetect_mtu", &autodetect_mtu) < 0) {
+ pa_log("Invalid boolean value for autodetect_mtu parameter");
+ goto fail_free_modargs;
+ }
+
+ u->device->autodetect_mtu = autodetect_mtu;
+
+ pa_modargs_free(ma);
+
+ u->device_connection_changed_slot =
+ pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED),
+ PA_HOOK_NORMAL, (pa_hook_cb_t) device_connection_changed_cb, u);
+
+ u->transport_state_changed_slot =
+ pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED),
+ PA_HOOK_NORMAL, (pa_hook_cb_t) transport_state_changed_cb, u);
+
+ u->transport_speaker_gain_changed_slot =
+ pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED), PA_HOOK_NORMAL, (pa_hook_cb_t) transport_speaker_gain_changed_cb, u);
+
+ u->transport_microphone_gain_changed_slot =
+ pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED), PA_HOOK_NORMAL, (pa_hook_cb_t) transport_microphone_gain_changed_cb, u);
+
+ if (add_card(u) < 0)
+ goto fail;
+
+ if (!(u->msg = pa_msgobject_new(bluetooth_msg)))
+ goto fail;
+
+ u->msg->parent.process_msg = device_process_msg;
+ u->msg->card = u->card;
+ u->stream_setup_done = false;
+
+ 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 0;
+
+off:
+ stop_thread(u);
+
+ pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0);
+
+ return 0;
+
+fail_free_modargs:
+
+ if (ma)
+ pa_modargs_free(ma);
+
+fail:
+
+ pa__done(m);
+
+ return -1;
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ stop_thread(u);
+
+ if (u->device_connection_changed_slot)
+ pa_hook_slot_free(u->device_connection_changed_slot);
+
+ if (u->transport_state_changed_slot)
+ pa_hook_slot_free(u->transport_state_changed_slot);
+
+ if (u->transport_speaker_gain_changed_slot)
+ pa_hook_slot_free(u->transport_speaker_gain_changed_slot);
+
+ if (u->transport_microphone_gain_changed_slot)
+ pa_hook_slot_free(u->transport_microphone_gain_changed_slot);
+
+ if (u->encoder_buffer)
+ pa_xfree(u->encoder_buffer);
+
+ if (u->decoder_buffer)
+ pa_xfree(u->decoder_buffer);
+
+ if (u->msg)
+ pa_xfree(u->msg);
+
+ if (u->card)
+ pa_card_free(u->card);
+
+ if (u->discovery)
+ pa_bluetooth_discovery_unref(u->discovery);
+
+ pa_xfree(u->output_port_name);
+ pa_xfree(u->input_port_name);
+
+ pa_xfree(u);
+}
+
+int pa__get_n_used(pa_module *m) {
+ struct userdata *u;
+
+ pa_assert(m);
+ pa_assert_se(u = m->userdata);
+
+ return (u->sink ? pa_sink_linked_by(u->sink) : 0) + (u->source ? pa_source_linked_by(u->source) : 0);
+}
diff --git a/src/modules/bluetooth/module-bluez5-discover.c b/src/modules/bluetooth/module-bluez5-discover.c
new file mode 100644
index 0000000..47b5761
--- /dev/null
+++ b/src/modules/bluetooth/module-bluez5-discover.c
@@ -0,0 +1,173 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2008-2013 João Paulo Rechi Vita
+
+ 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 <pulsecore/core.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/macro.h>
+#include <pulsecore/module.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/shared.h>
+
+#include "bluez5-util.h"
+
+PA_MODULE_AUTHOR("João Paulo Rechi Vita");
+PA_MODULE_DESCRIPTION("Detect available BlueZ 5 Bluetooth audio devices and load BlueZ 5 Bluetooth audio drivers");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+PA_MODULE_USAGE(
+ "headset=ofono|native|auto"
+ "autodetect_mtu=<boolean>"
+);
+
+static const char* const valid_modargs[] = {
+ "headset",
+ "autodetect_mtu",
+ NULL
+};
+
+struct userdata {
+ pa_module *module;
+ pa_core *core;
+ pa_hashmap *loaded_device_paths;
+ pa_hook_slot *device_connection_changed_slot;
+ pa_bluetooth_discovery *discovery;
+ bool autodetect_mtu;
+};
+
+static pa_hook_result_t device_connection_changed_cb(pa_bluetooth_discovery *y, const pa_bluetooth_device *d, struct userdata *u) {
+ bool module_loaded;
+
+ pa_assert(d);
+ pa_assert(u);
+
+ module_loaded = pa_hashmap_get(u->loaded_device_paths, d->path) ? true : false;
+
+ if (module_loaded && !pa_bluetooth_device_any_transport_connected(d)) {
+ /* disconnection, the module unloads itself */
+ pa_log_debug("Unregistering module for %s", d->path);
+ pa_hashmap_remove(u->loaded_device_paths, d->path);
+ return PA_HOOK_OK;
+ }
+
+ if (!module_loaded && pa_bluetooth_device_any_transport_connected(d)) {
+ /* a new device has been connected */
+ pa_module *m;
+ char *args = pa_sprintf_malloc("path=%s autodetect_mtu=%i", d->path, (int)u->autodetect_mtu);
+
+ pa_log_debug("Loading module-bluez5-device %s", args);
+ pa_module_load(&m, u->module->core, "module-bluez5-device", args);
+ pa_xfree(args);
+
+ if (m)
+ /* No need to duplicate the path here since the device object will
+ * exist for the whole hashmap entry lifespan */
+ pa_hashmap_put(u->loaded_device_paths, d->path, d->path);
+ else
+ pa_log_warn("Failed to load module for device %s", d->path);
+
+ return PA_HOOK_OK;
+ }
+
+ return PA_HOOK_OK;
+}
+
+#ifdef HAVE_BLUEZ_5_NATIVE_HEADSET
+const char *default_headset_backend = "auto";
+#else
+const char *default_headset_backend = "ofono";
+#endif
+
+int pa__init(pa_module *m) {
+ struct userdata *u;
+ pa_modargs *ma;
+ const char *headset_str;
+ int headset_backend;
+ bool autodetect_mtu;
+
+ pa_assert(m);
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log("failed to parse module arguments.");
+ goto fail;
+ }
+
+ pa_assert_se(headset_str = pa_modargs_get_value(ma, "headset", default_headset_backend));
+ if (pa_streq(headset_str, "ofono"))
+ headset_backend = HEADSET_BACKEND_OFONO;
+ else if (pa_streq(headset_str, "native"))
+ headset_backend = HEADSET_BACKEND_NATIVE;
+ else if (pa_streq(headset_str, "auto"))
+ headset_backend = HEADSET_BACKEND_AUTO;
+ else {
+ pa_log("headset parameter must be either ofono, native or auto (found %s)", headset_str);
+ goto fail;
+ }
+
+ autodetect_mtu = false;
+ if (pa_modargs_get_value_boolean(ma, "autodetect_mtu", &autodetect_mtu) < 0) {
+ pa_log("Invalid boolean value for autodetect_mtu parameter");
+ goto fail;
+ }
+
+ m->userdata = u = pa_xnew0(struct userdata, 1);
+ u->module = m;
+ u->core = m->core;
+ u->autodetect_mtu = autodetect_mtu;
+ u->loaded_device_paths = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+
+ if (!(u->discovery = pa_bluetooth_discovery_get(u->core, headset_backend)))
+ goto fail;
+
+ u->device_connection_changed_slot =
+ pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED),
+ PA_HOOK_NORMAL, (pa_hook_cb_t) device_connection_changed_cb, u);
+
+ pa_modargs_free(ma);
+ return 0;
+
+fail:
+ if (ma)
+ pa_modargs_free(ma);
+ pa__done(m);
+ return -1;
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ if (u->device_connection_changed_slot)
+ pa_hook_slot_free(u->device_connection_changed_slot);
+
+ if (u->loaded_device_paths)
+ pa_hashmap_free(u->loaded_device_paths);
+
+ if (u->discovery)
+ pa_bluetooth_discovery_unref(u->discovery);
+
+ pa_xfree(u);
+}
diff --git a/src/modules/bluetooth/rtp.h b/src/modules/bluetooth/rtp.h
new file mode 100644
index 0000000..813d9e3
--- /dev/null
+++ b/src/modules/bluetooth/rtp.h
@@ -0,0 +1,80 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ * Copyright (C) 2019 Pali Rohár <pali.rohar@gmail.com>
+ *
+ *
+ * This library 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.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <endian.h>
+#include <stdint.h>
+
+#if defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && \
+ __BYTE_ORDER == __LITTLE_ENDIAN
+
+struct rtp_header {
+ uint8_t cc:4;
+ uint8_t x:1;
+ uint8_t p:1;
+ uint8_t v:2;
+
+ uint8_t pt:7;
+ uint8_t m:1;
+
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_sbc_payload {
+ uint8_t frame_count:4;
+ uint8_t rfa0:1;
+ uint8_t is_last_fragment:1;
+ uint8_t is_first_fragment:1;
+ uint8_t is_fragmented:1;
+} __attribute__ ((packed));
+
+#elif defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && \
+ __BYTE_ORDER == __BIG_ENDIAN
+
+struct rtp_header {
+ uint8_t v:2;
+ uint8_t p:1;
+ uint8_t x:1;
+ uint8_t cc:4;
+
+ uint8_t m:1;
+ uint8_t pt:7;
+
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_sbc_payload {
+ uint8_t is_fragmented:1;
+ uint8_t is_first_fragment:1;
+ uint8_t is_last_fragment:1;
+ uint8_t rfa0:1;
+ uint8_t frame_count:4;
+} __attribute__ ((packed));
+
+#else
+#error "Unknown byte order"
+#endif