/*
|
* BlueALSA - bluez.c
|
* Copyright (c) 2016-2018 Arkadiusz Bokowy
|
*
|
* This file is a part of bluez-alsa.
|
*
|
* This project is licensed under the terms of the MIT license.
|
*
|
*/
|
|
#include "bluez.h"
|
|
#include <errno.h>
|
#include <stdio.h>
|
#include <stdlib.h>
|
#include <string.h>
|
|
#include <gio/gunixfdlist.h>
|
|
#include "a2dp-codecs.h"
|
#include "bluealsa.h"
|
#include "bluez-a2dp.h"
|
#include "bluez-iface.h"
|
#include "ctl.h"
|
#include "transport.h"
|
#include "utils.h"
|
#include "shared/log.h"
|
|
|
/**
|
* Get D-Bus object reference count for given profile. */
|
static int bluez_get_dbus_object_count(
|
enum bluetooth_profile profile,
|
uint16_t codec) {
|
|
GHashTableIter iter;
|
struct ba_dbus_object *obj;
|
int count = 0;
|
|
g_hash_table_iter_init(&iter, config.dbus_objects);
|
while (g_hash_table_iter_next(&iter, NULL, (gpointer)&obj))
|
if (obj->profile == profile && obj->codec == codec && obj->connected)
|
count++;
|
|
return count;
|
}
|
|
/**
|
* Check whether channel mode configuration is valid. */
|
static bool bluez_a2dp_codec_check_channel_mode(
|
const struct bluez_a2dp_codec *codec,
|
unsigned int capabilities) {
|
|
size_t i;
|
|
for (i = 0; i < codec->channels_size; i++)
|
if (capabilities == codec->channels[i].value)
|
return true;
|
|
return false;
|
}
|
|
/**
|
* Check whether sampling frequency configuration is valid. */
|
static bool bluez_a2dp_codec_check_sampling_freq(
|
const struct bluez_a2dp_codec *codec,
|
unsigned int capabilities) {
|
|
size_t i;
|
|
for (i = 0; i < codec->samplings_size; i++)
|
if (capabilities == codec->samplings[i].value)
|
return true;
|
|
return false;
|
}
|
|
/**
|
* Select (best) channel mode configuration. */
|
static unsigned int bluez_a2dp_codec_select_channel_mode(
|
const struct bluez_a2dp_codec *codec,
|
unsigned int capabilities) {
|
|
size_t i;
|
|
/* If monophonic sound has been forced, check whether given codec supports
|
* such a channel mode. Since mono channel mode shall be stored at index 0
|
* we can simply check for its existence with a simple index lookup. */
|
if (config.a2dp.force_mono &&
|
codec->channels[0].mode == BLUEZ_A2DP_CHM_MONO &&
|
capabilities & codec->channels[0].value)
|
return codec->channels[0].value;
|
|
/* favor higher number of channels */
|
for (i = codec->channels_size; i > 0; i--)
|
if (capabilities & codec->channels[i - 1].value)
|
return codec->channels[i - 1].value;
|
|
return 0;
|
}
|
|
/**
|
* Select (best) sampling frequency configuration. */
|
static unsigned int bluez_a2dp_codec_select_sampling_freq(
|
const struct bluez_a2dp_codec *codec,
|
unsigned int capabilities) {
|
|
size_t i;
|
|
if (config.a2dp.force_44100)
|
for (i = 0; i < codec->samplings_size; i++)
|
if (codec->samplings[i].frequency == 44100) {
|
if (capabilities & codec->samplings[i].value)
|
return codec->samplings[i].value;
|
break;
|
}
|
|
/* favor higher sampling frequencies */
|
for (i = codec->samplings_size; i > 0; i--)
|
if (capabilities & codec->samplings[i - 1].value)
|
return codec->samplings[i - 1].value;
|
|
return 0;
|
}
|
|
static void bluez_endpoint_select_configuration(GDBusMethodInvocation *inv, void *userdata) {
|
|
const char *path = g_dbus_method_invocation_get_object_path(inv);
|
GVariant *params = g_dbus_method_invocation_get_parameters(inv);
|
const struct bluez_a2dp_codec *codec = userdata;
|
|
const uint8_t *data;
|
uint8_t *capabilities;
|
size_t size = 0;
|
|
params = g_variant_get_child_value(params, 0);
|
data = g_variant_get_fixed_array(params, &size, sizeof(uint8_t));
|
capabilities = g_memdup(data, size);
|
g_variant_unref(params);
|
|
if (size != codec->cfg_size) {
|
error("Invalid capabilities size: %zu != %zu", size, codec->cfg_size);
|
goto fail;
|
}
|
|
switch (codec->id) {
|
case A2DP_CODEC_SBC: {
|
|
a2dp_sbc_t *cap = (a2dp_sbc_t *)capabilities;
|
unsigned int cap_chm = cap->channel_mode;
|
unsigned int cap_freq = cap->frequency;
|
|
if ((cap->channel_mode = bluez_a2dp_codec_select_channel_mode(codec, cap_chm)) == 0) {
|
error("No supported channel modes: %#x", cap_chm);
|
goto fail;
|
}
|
|
if ((cap->frequency = bluez_a2dp_codec_select_sampling_freq(codec, cap_freq)) == 0) {
|
error("No supported sampling frequencies: %#x", cap_freq);
|
goto fail;
|
}
|
|
if (cap->block_length & SBC_BLOCK_LENGTH_16)
|
cap->block_length = SBC_BLOCK_LENGTH_16;
|
else if (cap->block_length & SBC_BLOCK_LENGTH_12)
|
cap->block_length = SBC_BLOCK_LENGTH_12;
|
else if (cap->block_length & SBC_BLOCK_LENGTH_8)
|
cap->block_length = SBC_BLOCK_LENGTH_8;
|
else if (cap->block_length & SBC_BLOCK_LENGTH_4)
|
cap->block_length = SBC_BLOCK_LENGTH_4;
|
else {
|
error("No supported block lengths: %#x", cap->block_length);
|
goto fail;
|
}
|
|
if (cap->subbands & SBC_SUBBANDS_8)
|
cap->subbands = SBC_SUBBANDS_8;
|
else if (cap->subbands & SBC_SUBBANDS_4)
|
cap->subbands = SBC_SUBBANDS_4;
|
else {
|
error("No supported subbands: %#x", cap->subbands);
|
goto fail;
|
}
|
|
if (cap->allocation_method & SBC_ALLOCATION_LOUDNESS)
|
cap->allocation_method = SBC_ALLOCATION_LOUDNESS;
|
else if (cap->allocation_method & SBC_ALLOCATION_SNR)
|
cap->allocation_method = SBC_ALLOCATION_SNR;
|
else {
|
error("No supported allocation: %#x", cap->allocation_method);
|
goto fail;
|
}
|
|
int bitpool = a2dp_sbc_default_bitpool(cap->frequency, cap->channel_mode);
|
cap->min_bitpool = MAX(SBC_MIN_BITPOOL, cap->min_bitpool);
|
cap->max_bitpool = MIN(bitpool, cap->max_bitpool);
|
|
break;
|
}
|
|
#if ENABLE_MPEG
|
case A2DP_CODEC_MPEG12: {
|
|
a2dp_mpeg_t *cap = (a2dp_mpeg_t *)capabilities;
|
unsigned int cap_chm = cap->channel_mode;
|
unsigned int cap_freq = cap->frequency;
|
|
if ((cap->channel_mode = bluez_a2dp_codec_select_channel_mode(codec, cap_chm)) == 0) {
|
error("No supported channel modes: %#x", cap_chm);
|
goto fail;
|
}
|
|
if ((cap->frequency = bluez_a2dp_codec_select_sampling_freq(codec, cap_freq)) == 0) {
|
error("No supported sampling frequencies: %#x", cap_freq);
|
goto fail;
|
}
|
|
break;
|
}
|
#endif
|
|
#if ENABLE_AAC
|
case A2DP_CODEC_MPEG24: {
|
|
a2dp_aac_t *cap = (a2dp_aac_t *)capabilities;
|
unsigned int cap_chm = cap->channels;
|
unsigned int cap_freq = AAC_GET_FREQUENCY(*cap);
|
|
if (cap->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_SCA)
|
cap->object_type = AAC_OBJECT_TYPE_MPEG4_AAC_SCA;
|
else if (cap->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LTP)
|
cap->object_type = AAC_OBJECT_TYPE_MPEG4_AAC_LTP;
|
else if (cap->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC)
|
cap->object_type = AAC_OBJECT_TYPE_MPEG4_AAC_LC;
|
else if (cap->object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC)
|
cap->object_type = AAC_OBJECT_TYPE_MPEG2_AAC_LC;
|
else {
|
error("No supported object type: %#x", cap->object_type);
|
goto fail;
|
}
|
|
if ((cap->channels = bluez_a2dp_codec_select_channel_mode(codec, cap_chm)) == 0) {
|
error("No supported channels: %#x", cap_chm);
|
goto fail;
|
}
|
|
unsigned int freq;
|
if ((freq = bluez_a2dp_codec_select_sampling_freq(codec, cap_freq)) != 0)
|
AAC_SET_FREQUENCY(*cap, freq);
|
else {
|
error("No supported sampling frequencies: %#x", cap_freq);
|
goto fail;
|
}
|
|
break;
|
}
|
#endif
|
|
#if ENABLE_APTX
|
case A2DP_CODEC_VENDOR_APTX: {
|
|
a2dp_aptx_t *cap = (a2dp_aptx_t *)capabilities;
|
unsigned int cap_chm = cap->channel_mode;
|
unsigned int cap_freq = cap->frequency;
|
|
if ((cap->channel_mode = bluez_a2dp_codec_select_channel_mode(codec, cap_chm)) == 0) {
|
error("No supported channel modes: %#x", cap_chm);
|
goto fail;
|
}
|
|
if ((cap->frequency = bluez_a2dp_codec_select_sampling_freq(codec, cap_freq)) == 0) {
|
error("No supported sampling frequencies: %#x", cap_freq);
|
goto fail;
|
}
|
|
break;
|
}
|
#endif
|
|
#if ENABLE_LDAC
|
case A2DP_CODEC_VENDOR_LDAC: {
|
|
a2dp_ldac_t *cap = (a2dp_ldac_t *)capabilities;
|
unsigned int cap_chm = cap->channel_mode;
|
unsigned int cap_freq = cap->frequency;
|
|
if ((cap->channel_mode = bluez_a2dp_codec_select_channel_mode(codec, cap_chm)) == 0) {
|
error("No supported channel modes: %#x", cap_chm);
|
goto fail;
|
}
|
|
if ((cap->frequency = bluez_a2dp_codec_select_sampling_freq(codec, cap_freq)) == 0) {
|
error("No supported sampling frequencies: %#x", cap_freq);
|
goto fail;
|
}
|
|
break;
|
}
|
#endif
|
|
default:
|
debug("Endpoint path not supported: %s", path);
|
g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR,
|
G_DBUS_ERROR_UNKNOWN_OBJECT, "Not supported");
|
goto final;
|
}
|
|
GVariantBuilder caps;
|
size_t i;
|
|
g_variant_builder_init(&caps, G_VARIANT_TYPE("ay"));
|
for (i = 0; i < size; i++)
|
g_variant_builder_add(&caps, "y", capabilities[i]);
|
|
g_dbus_method_invocation_return_value(inv, g_variant_new("(ay)", &caps));
|
g_variant_builder_clear(&caps);
|
|
goto final;
|
|
fail:
|
g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR,
|
G_DBUS_ERROR_INVALID_ARGS, "Invalid capabilities");
|
|
final:
|
g_free(capabilities);
|
}
|
|
static int bluez_endpoint_set_configuration(GDBusMethodInvocation *inv, void *userdata) {
|
|
const gchar *sender = g_dbus_method_invocation_get_sender(inv);
|
const gchar *path = g_dbus_method_invocation_get_object_path(inv);
|
GVariant *params = g_dbus_method_invocation_get_parameters(inv);
|
const struct bluez_a2dp_codec *codec = userdata;
|
struct ba_transport *t;
|
struct ba_device *d;
|
|
const int profile = g_dbus_object_path_to_profile(path);
|
const uint16_t codec_id = codec->id;
|
|
const char *transport;
|
char *device = NULL, *state = NULL;
|
uint8_t *configuration = NULL;
|
uint16_t volume = 127;
|
uint16_t delay = 150;
|
size_t size = 0;
|
int ret = 0;
|
|
GVariantIter *properties;
|
GVariant *value = NULL;
|
const char *key;
|
|
g_variant_get(params, "(&oa{sv})", &transport, &properties);
|
|
if (transport_lookup(config.devices, transport) != NULL) {
|
error("Transport already configured: %s", transport);
|
goto fail;
|
}
|
|
/* read transport properties */
|
while (g_variant_iter_next(properties, "{&sv}", &key, &value)) {
|
|
if (strcmp(key, "Device") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "o");
|
goto fail;
|
}
|
|
device = g_variant_dup_string(value, NULL);
|
|
}
|
else if (strcmp(key, "UUID") == 0) {
|
}
|
else if (strcmp(key, "Codec") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "y");
|
goto fail;
|
}
|
|
if ((codec_id & 0xFF) != g_variant_get_byte(value)) {
|
error("Invalid configuration: %s", "Codec mismatch");
|
goto fail;
|
}
|
|
}
|
else if (strcmp(key, "Configuration") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "ay");
|
goto fail;
|
}
|
|
const guchar *capabilities = g_variant_get_fixed_array(value, &size, sizeof(uint8_t));
|
unsigned int cap_chm = 0;
|
unsigned int cap_freq = 0;
|
|
configuration = g_memdup(capabilities, size);
|
|
if (size != codec->cfg_size) {
|
error("Invalid configuration: %s", "Invalid size");
|
goto fail;
|
}
|
|
switch (codec_id) {
|
case A2DP_CODEC_SBC: {
|
|
const a2dp_sbc_t *cap = (a2dp_sbc_t *)capabilities;
|
cap_chm = cap->channel_mode;
|
cap_freq = cap->frequency;
|
|
if (cap->allocation_method != SBC_ALLOCATION_SNR &&
|
cap->allocation_method != SBC_ALLOCATION_LOUDNESS) {
|
error("Invalid configuration: %s", "Invalid allocation method");
|
goto fail;
|
}
|
|
if (cap->subbands != SBC_SUBBANDS_4 &&
|
cap->subbands != SBC_SUBBANDS_8) {
|
error("Invalid configuration: %s", "Invalid SBC subbands");
|
goto fail;
|
}
|
|
if (cap->block_length != SBC_BLOCK_LENGTH_4 &&
|
cap->block_length != SBC_BLOCK_LENGTH_8 &&
|
cap->block_length != SBC_BLOCK_LENGTH_12 &&
|
cap->block_length != SBC_BLOCK_LENGTH_16) {
|
error("Invalid configuration: %s", "Invalid block length");
|
goto fail;
|
}
|
|
break;
|
}
|
|
#if ENABLE_MPEG
|
case A2DP_CODEC_MPEG12: {
|
a2dp_mpeg_t *cap = (a2dp_mpeg_t *)capabilities;
|
cap_chm = cap->channel_mode;
|
cap_freq = cap->frequency;
|
break;
|
}
|
#endif
|
|
#if ENABLE_AAC
|
case A2DP_CODEC_MPEG24: {
|
|
const a2dp_aac_t *cap = (a2dp_aac_t *)capabilities;
|
cap_chm = cap->channels;
|
cap_freq = AAC_GET_FREQUENCY(*cap);
|
|
if (cap->object_type != AAC_OBJECT_TYPE_MPEG2_AAC_LC &&
|
cap->object_type != AAC_OBJECT_TYPE_MPEG4_AAC_LC &&
|
cap->object_type != AAC_OBJECT_TYPE_MPEG4_AAC_LTP &&
|
cap->object_type != AAC_OBJECT_TYPE_MPEG4_AAC_SCA) {
|
error("Invalid configuration: %s", "Invalid object type");
|
goto fail;
|
}
|
|
break;
|
}
|
#endif
|
|
#if ENABLE_APTX
|
case A2DP_CODEC_VENDOR_APTX: {
|
a2dp_aptx_t *cap = (a2dp_aptx_t *)capabilities;
|
cap_chm = cap->channel_mode;
|
cap_freq = cap->frequency;
|
break;
|
}
|
#endif
|
|
#if ENABLE_LDAC
|
case A2DP_CODEC_VENDOR_LDAC: {
|
a2dp_ldac_t *cap = (a2dp_ldac_t *)capabilities;
|
cap_chm = cap->channel_mode;
|
cap_freq = cap->frequency;
|
break;
|
}
|
#endif
|
|
default:
|
error("Invalid configuration: %s", "Unsupported codec");
|
goto fail;
|
}
|
|
if (!bluez_a2dp_codec_check_channel_mode(codec, cap_chm)) {
|
error("Invalid configuration: %s", "Invalid channel mode");
|
goto fail;
|
}
|
|
if (!bluez_a2dp_codec_check_sampling_freq(codec, cap_freq)) {
|
error("Invalid configuration: %s", "Invalid sampling frequency");
|
goto fail;
|
}
|
|
}
|
else if (strcmp(key, "State") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "s");
|
goto fail;
|
}
|
|
state = g_variant_dup_string(value, NULL);
|
|
}
|
else if (strcmp(key, "Delay") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT16)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "q");
|
goto fail;
|
}
|
|
delay = g_variant_get_uint16(value);
|
|
}
|
else if (strcmp(key, "Volume") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT16)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "q");
|
goto fail;
|
}
|
|
/* received volume is in range [0, 127]*/
|
volume = g_variant_get_uint16(value);
|
|
}
|
|
g_variant_unref(value);
|
value = NULL;
|
}
|
|
/* we are going to modify the devices hash-map */
|
pthread_mutex_lock(&config.devices_mutex);
|
|
/* get the device structure for obtained device path */
|
if ((d = device_get(config.devices, device)) == NULL) {
|
error("Couldn't get device: %s", strerror(errno));
|
goto fail;
|
}
|
|
/* Create a new transport with a human-readable name. Since the transport
|
* name can not be obtained from the client, we will use a fall-back one. */
|
if ((t = transport_new_a2dp(d, sender, transport, profile, codec_id,
|
configuration, size)) == NULL) {
|
error("Couldn't create new transport: %s", strerror(errno));
|
goto fail;
|
}
|
|
t->a2dp.ch1_volume = volume;
|
t->a2dp.ch2_volume = volume;
|
t->a2dp.delay = delay;
|
|
debug("%s (%s) configured for device %s",
|
bluetooth_profile_to_string(profile),
|
bluetooth_a2dp_codec_to_string(codec_id),
|
batostr_(&d->addr));
|
debug("Configuration: channels: %u, sampling: %u",
|
transport_get_channels(t), transport_get_sampling(t));
|
|
transport_set_state_from_string(t, state);
|
|
g_dbus_method_invocation_return_value(inv, NULL);
|
goto final;
|
|
fail:
|
g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR,
|
G_DBUS_ERROR_INVALID_ARGS, "Unable to set configuration");
|
ret = -1;
|
|
final:
|
pthread_mutex_unlock(&config.devices_mutex);
|
g_variant_iter_free(properties);
|
if (value != NULL)
|
g_variant_unref(value);
|
g_free(device);
|
g_free(configuration);
|
g_free(state);
|
return ret;
|
}
|
|
static void bluez_endpoint_clear_configuration(GDBusMethodInvocation *inv, void *userdata) {
|
(void)userdata;
|
|
GVariant *params = g_dbus_method_invocation_get_parameters(inv);
|
const char *transport;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
g_variant_get(params, "(&o)", &transport);
|
transport_remove(config.devices, transport);
|
|
pthread_mutex_unlock(&config.devices_mutex);
|
g_object_unref(inv);
|
}
|
|
static void bluez_endpoint_release(GDBusMethodInvocation *inv, void *userdata) {
|
(void)userdata;
|
|
GDBusConnection *conn = g_dbus_method_invocation_get_connection(inv);
|
const char *path = g_dbus_method_invocation_get_object_path(inv);
|
gpointer hash = GINT_TO_POINTER(g_str_hash(path));
|
struct ba_dbus_object *obj;
|
|
debug("Releasing endpoint: %s", path);
|
|
if ((obj = g_hash_table_lookup(config.dbus_objects, hash)) != NULL) {
|
g_dbus_connection_unregister_object(conn, obj->id);
|
g_hash_table_remove(config.dbus_objects, hash);
|
}
|
|
g_object_unref(inv);
|
}
|
|
static void bluez_endpoint_method_call(GDBusConnection *conn, const gchar *sender,
|
const gchar *path, const gchar *interface, const gchar *method, GVariant *params,
|
GDBusMethodInvocation *invocation, void *userdata) {
|
(void)conn;
|
(void)sender;
|
(void)interface;
|
(void)params;
|
|
struct ba_dbus_object *obj;
|
|
debug("Endpoint method call: %s.%s()", interface, method);
|
|
gpointer hash = GINT_TO_POINTER(g_str_hash(path));
|
obj = g_hash_table_lookup(config.dbus_objects, hash);
|
|
if (strcmp(method, "SelectConfiguration") == 0)
|
bluez_endpoint_select_configuration(invocation, userdata);
|
else if (strcmp(method, "SetConfiguration") == 0) {
|
if (bluez_endpoint_set_configuration(invocation, userdata) == 0) {
|
obj->connected = true;
|
bluez_register_a2dp();
|
}
|
}
|
else if (strcmp(method, "ClearConfiguration") == 0) {
|
bluez_endpoint_clear_configuration(invocation, userdata);
|
obj->connected = false;
|
}
|
else if (strcmp(method, "Release") == 0)
|
bluez_endpoint_release(invocation, userdata);
|
else
|
warn("Unsupported endpoint method: %s", method);
|
|
}
|
|
static void endpoint_free(gpointer data) {
|
(void)data;
|
}
|
|
static const GDBusInterfaceVTable endpoint_vtable = {
|
.method_call = bluez_endpoint_method_call,
|
};
|
|
/**
|
* Register A2DP endpoint.
|
*
|
* @param uuid
|
* @param profile
|
* @param codec
|
* @return On success this function returns 0. Otherwise -1 is returned. */
|
static int bluez_register_a2dp_endpoint(
|
const char *uuid,
|
enum bluetooth_profile profile,
|
const struct bluez_a2dp_codec *codec) {
|
|
gchar *path = g_strdup_printf("%s/%d",
|
g_dbus_get_profile_object_path(profile, codec->id),
|
bluez_get_dbus_object_count(profile, codec->id) + 1);
|
gpointer hash = GINT_TO_POINTER(g_str_hash(path));
|
|
if (g_hash_table_contains(config.dbus_objects, hash)) {
|
debug("Endpoint already registered: %s", path);
|
g_free(path);
|
return 0;
|
}
|
|
GDBusConnection *conn = config.dbus;
|
GDBusMessage *msg = NULL, *rep = NULL;
|
GError *err = NULL;
|
gchar *dev = NULL;
|
int ret = 0;
|
size_t i;
|
|
struct ba_dbus_object dbus_object = {
|
.profile = profile,
|
.codec = codec->id,
|
};
|
|
debug("Registering endpoint: %s", path);
|
if ((dbus_object.id = g_dbus_connection_register_object(conn, path,
|
(GDBusInterfaceInfo *)&bluez_iface_endpoint, &endpoint_vtable,
|
(void *)codec, endpoint_free, &err)) == 0)
|
goto fail;
|
|
dev = g_strdup_printf("/org/bluez/%s", config.hci_dev.name);
|
msg = g_dbus_message_new_method_call("org.bluez", dev,
|
"org.bluez.Media1", "RegisterEndpoint");
|
|
GVariantBuilder caps;
|
GVariantBuilder properties;
|
|
g_variant_builder_init(&caps, G_VARIANT_TYPE("ay"));
|
g_variant_builder_init(&properties, G_VARIANT_TYPE("a{sv}"));
|
|
for (i = 0; i < codec->cfg_size; i++)
|
g_variant_builder_add(&caps, "y", ((uint8_t *)codec->cfg)[i]);
|
|
g_variant_builder_add(&properties, "{sv}", "UUID", g_variant_new_string(uuid));
|
g_variant_builder_add(&properties, "{sv}", "DelayReporting", g_variant_new_boolean(TRUE));
|
g_variant_builder_add(&properties, "{sv}", "Codec", g_variant_new_byte(codec->id));
|
g_variant_builder_add(&properties, "{sv}", "Capabilities", g_variant_builder_end(&caps));
|
|
g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", path, &properties));
|
g_variant_builder_clear(&properties);
|
|
if ((rep = g_dbus_connection_send_message_with_reply_sync(conn, msg,
|
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL, &err)) == NULL)
|
goto fail;
|
|
if (g_dbus_message_get_message_type(rep) == G_DBUS_MESSAGE_TYPE_ERROR) {
|
g_dbus_message_to_gerror(rep, &err);
|
goto fail;
|
}
|
|
g_hash_table_insert(config.dbus_objects, hash,
|
g_memdup(&dbus_object, sizeof(dbus_object)));
|
|
goto final;
|
|
fail:
|
ret = -1;
|
|
final:
|
g_free(path);
|
if (msg != NULL)
|
g_object_unref(msg);
|
if (rep != NULL)
|
g_object_unref(rep);
|
if (dev != NULL)
|
g_free(dev);
|
if (err != NULL) {
|
warn("Couldn't register endpoint: %s", err->message);
|
g_dbus_connection_unregister_object(conn, dbus_object.id);
|
g_error_free(err);
|
}
|
|
return ret;
|
}
|
|
/**
|
* Register A2DP endpoints. */
|
void bluez_register_a2dp(void) {
|
|
const struct bluez_a2dp_codec **cc = config.a2dp.codecs;
|
|
while (*cc != NULL) {
|
const struct bluez_a2dp_codec *c = *cc++;
|
switch (c->dir) {
|
case BLUEZ_A2DP_SOURCE:
|
if (config.enable.a2dp_source)
|
bluez_register_a2dp_endpoint(BLUETOOTH_UUID_A2DP_SOURCE, BLUETOOTH_PROFILE_A2DP_SOURCE, c);
|
break;
|
case BLUEZ_A2DP_SINK:
|
if (config.enable.a2dp_sink)
|
bluez_register_a2dp_endpoint(BLUETOOTH_UUID_A2DP_SINK, BLUETOOTH_PROFILE_A2DP_SINK, c);
|
break;
|
}
|
}
|
|
}
|
|
static void bluez_profile_new_connection(GDBusMethodInvocation *inv, void *userdata) {
|
(void)userdata;
|
|
GDBusMessage *msg = g_dbus_method_invocation_get_message(inv);
|
const gchar *sender = g_dbus_method_invocation_get_sender(inv);
|
const gchar *path = g_dbus_method_invocation_get_object_path(inv);
|
GVariant *params = g_dbus_method_invocation_get_parameters(inv);
|
struct ba_transport *t;
|
struct ba_device *d;
|
|
const int profile = g_dbus_object_path_to_profile(path);
|
|
GVariantIter *properties;
|
GUnixFDList *fd_list;
|
const char *device;
|
GError *err = NULL;
|
int fd;
|
|
g_variant_get(params, "(&oha{sv})", &device, &fd, &properties);
|
|
fd_list = g_dbus_message_get_unix_fd_list(msg);
|
if ((fd = g_unix_fd_list_get(fd_list, 0, &err)) == -1) {
|
error("Couldn't obtain RFCOMM socket: %s", err->message);
|
goto fail;
|
}
|
|
if ((d = device_get(config.devices, device)) == NULL) {
|
error("Couldn't get device: %s", strerror(errno));
|
goto fail;
|
}
|
|
if ((t = transport_new_rfcomm(d, sender, device, profile)) == NULL) {
|
error("Couldn't create new transport: %s", strerror(errno));
|
goto fail;
|
}
|
|
t->bt_fd = fd;
|
t->release = transport_release_bt_rfcomm;
|
|
debug("%s configured for device %s",
|
bluetooth_profile_to_string(profile), batostr_(&d->addr));
|
|
transport_set_state(t, TRANSPORT_ACTIVE);
|
transport_set_state(t->rfcomm.sco, TRANSPORT_ACTIVE);
|
|
g_dbus_method_invocation_return_value(inv, NULL);
|
goto final;
|
|
fail:
|
g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR,
|
G_DBUS_ERROR_INVALID_ARGS, "Unable to connect profile");
|
|
final:
|
g_variant_iter_free(properties);
|
if (err != NULL)
|
g_error_free(err);
|
}
|
|
static void bluez_profile_request_disconnection(GDBusMethodInvocation *inv, void *userdata) {
|
(void)userdata;
|
|
GVariant *params = g_dbus_method_invocation_get_parameters(inv);
|
const char *device;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
g_variant_get(params, "(&o)", &device);
|
transport_remove(config.devices, device);
|
|
pthread_mutex_unlock(&config.devices_mutex);
|
|
g_object_unref(inv);
|
}
|
|
static void bluez_profile_release(GDBusMethodInvocation *inv, void *userdata) {
|
(void)userdata;
|
|
GDBusConnection *conn = g_dbus_method_invocation_get_connection(inv);
|
const char *path = g_dbus_method_invocation_get_object_path(inv);
|
gpointer hash = GINT_TO_POINTER(g_str_hash(path));
|
struct ba_dbus_object *obj;
|
|
debug("Releasing profile: %s", path);
|
|
if ((obj = g_hash_table_lookup(config.dbus_objects, hash)) != NULL) {
|
g_dbus_connection_unregister_object(conn, obj->id);
|
g_hash_table_remove(config.dbus_objects, hash);
|
}
|
|
g_object_unref(inv);
|
}
|
|
static void bluez_profile_method_call(GDBusConnection *conn, const gchar *sender,
|
const gchar *path, const gchar *interface, const gchar *method, GVariant *params,
|
GDBusMethodInvocation *invocation, void *userdata) {
|
(void)conn;
|
(void)sender;
|
(void)path;
|
(void)interface;
|
(void)params;
|
|
debug("Profile method call: %s.%s()", interface, method);
|
|
if (strcmp(method, "NewConnection") == 0)
|
bluez_profile_new_connection(invocation, userdata);
|
else if (strcmp(method, "RequestDisconnection") == 0)
|
bluez_profile_request_disconnection(invocation, userdata);
|
else if (strcmp(method, "Release") == 0)
|
bluez_profile_release(invocation, userdata);
|
else
|
warn("Unsupported profile method: %s", method);
|
|
}
|
|
void profile_free(gpointer data) {
|
(void)data;
|
}
|
|
static const GDBusInterfaceVTable profile_vtable = {
|
.method_call = bluez_profile_method_call,
|
};
|
|
/**
|
* Register Bluetooth Audio Profile.
|
*
|
* @param uuid
|
* @param profile
|
* @param version
|
* @param features
|
* @return On success this function returns 0. Otherwise -1 is returned. */
|
static int bluez_register_profile(
|
const char *uuid,
|
enum bluetooth_profile profile,
|
uint16_t version,
|
uint16_t features) {
|
|
const char *path = g_dbus_get_profile_object_path(profile, -1);
|
gpointer hash = GINT_TO_POINTER(g_str_hash(path));
|
|
if (g_hash_table_contains(config.dbus_objects, hash)) {
|
debug("Profile already registered: %s", path);
|
return 0;
|
}
|
|
GDBusConnection *conn = config.dbus;
|
GDBusMessage *msg = NULL, *rep = NULL;
|
GError *err = NULL;
|
int ret = 0;
|
|
struct ba_dbus_object dbus_object = {
|
.profile = profile,
|
.codec = 0,
|
};
|
|
debug("Registering profile: %s", path);
|
if ((dbus_object.id = g_dbus_connection_register_object(conn, path,
|
(GDBusInterfaceInfo *)&bluez_iface_profile, &profile_vtable,
|
NULL, profile_free, &err)) == 0)
|
goto fail;
|
|
msg = g_dbus_message_new_method_call("org.bluez", "/org/bluez",
|
"org.bluez.ProfileManager1", "RegisterProfile");
|
|
GVariantBuilder options;
|
|
g_variant_builder_init(&options, G_VARIANT_TYPE("a{sv}"));
|
if (version)
|
g_variant_builder_add(&options, "{sv}", "Version", g_variant_new_uint16(version));
|
if (features)
|
g_variant_builder_add(&options, "{sv}", "Features", g_variant_new_uint16(features));
|
|
g_dbus_message_set_body(msg, g_variant_new("(osa{sv})", path, uuid, &options));
|
g_variant_builder_clear(&options);
|
|
if ((rep = g_dbus_connection_send_message_with_reply_sync(conn, msg,
|
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL, &err)) == NULL)
|
goto fail;
|
|
if (g_dbus_message_get_message_type(rep) == G_DBUS_MESSAGE_TYPE_ERROR) {
|
g_dbus_message_to_gerror(rep, &err);
|
goto fail;
|
}
|
|
g_hash_table_insert(config.dbus_objects, hash,
|
g_memdup(&dbus_object, sizeof(dbus_object)));
|
|
goto final;
|
|
fail:
|
ret = -1;
|
|
final:
|
if (msg != NULL)
|
g_object_unref(msg);
|
if (rep != NULL)
|
g_object_unref(rep);
|
if (err != NULL) {
|
warn("Couldn't register profile: %s", err->message);
|
g_dbus_connection_unregister_object(conn, dbus_object.id);
|
g_error_free(err);
|
}
|
|
return ret;
|
}
|
|
/**
|
* Register Bluetooth Hands-Free Audio Profiles.
|
*
|
* This function also registers deprecated HSP profile. Profiles registration
|
* is controlled by the global configuration structure - if none is enabled,
|
* this function will do nothing. */
|
void bluez_register_hfp(void) {
|
if (config.enable.hsp_hs)
|
bluez_register_profile(BLUETOOTH_UUID_HSP_HS, BLUETOOTH_PROFILE_HSP_HS, 0x0, 0x0);
|
if (config.enable.hsp_ag)
|
bluez_register_profile(BLUETOOTH_UUID_HSP_AG, BLUETOOTH_PROFILE_HSP_AG, 0x0, 0x0);
|
if (config.enable.hfp_hf)
|
bluez_register_profile(BLUETOOTH_UUID_HFP_HF, BLUETOOTH_PROFILE_HFP_HF,
|
0x0107 /* HFP 1.7 */, config.hfp.features_sdp_hf);
|
if (config.enable.hfp_ag)
|
bluez_register_profile(BLUETOOTH_UUID_HFP_AG, BLUETOOTH_PROFILE_HFP_AG,
|
0x0107 /* HFP 1.7 */, config.hfp.features_sdp_ag);
|
}
|
|
static void bluez_signal_interfaces_added(GDBusConnection *conn, const gchar *sender,
|
const gchar *path, const gchar *interface, const gchar *signal, GVariant *params,
|
void *userdata) {
|
(void)conn;
|
(void)sender;
|
(void)path;
|
(void)interface;
|
(void)signal;
|
(void)userdata;
|
|
char *device_path = g_strdup_printf("/org/bluez/%s", config.hci_dev.name);
|
GVariantIter *interfaces;
|
const char *object;
|
|
g_variant_get(params, "(&oa{sa{sv}})", &object, &interfaces);
|
|
if (strcmp(object, device_path) == 0)
|
bluez_register_a2dp();
|
if (strcmp(object, "/org/bluez") == 0)
|
bluez_register_hfp();
|
|
g_variant_iter_free(interfaces);
|
g_free(device_path);
|
}
|
|
static void bluez_signal_transport_changed(GDBusConnection *conn, const gchar *sender,
|
const gchar *path, const gchar *interface, const gchar *signal, GVariant *params,
|
void *userdata) {
|
(void)conn;
|
(void)sender;
|
(void)interface;
|
(void)userdata;
|
|
const gchar *signature = g_variant_get_type_string(params);
|
GVariantIter *properties = NULL;
|
GVariantIter *unknown = NULL;
|
GVariant *value = NULL;
|
struct ba_transport *t;
|
const char *iface;
|
const char *key;
|
|
if (strcmp(signature, "(sa{sv}as)") != 0) {
|
error("Invalid signature for %s: %s != %s", signal, signature, "(sa{sv}as)");
|
goto fail;
|
}
|
|
if ((t = transport_lookup(config.devices, path)) == NULL) {
|
error("Transport not available: %s", path);
|
goto fail;
|
}
|
|
g_variant_get(params, "(&sa{sv}as)", &iface, &properties, &unknown);
|
while (g_variant_iter_next(properties, "{&sv}", &key, &value)) {
|
debug("Signal: %s: %s: %s", signal, iface, key);
|
|
if (strcmp(key, "State") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "s");
|
goto fail;
|
}
|
|
transport_set_state_from_string(t, g_variant_get_string(value, NULL));
|
|
}
|
else if (strcmp(key, "Delay") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT16)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "q");
|
goto fail;
|
}
|
|
t->a2dp.delay = g_variant_get_uint16(value);
|
|
}
|
else if (strcmp(key, "Volume") == 0) {
|
|
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT16)) {
|
error("Invalid argument type for %s: %s != %s", key,
|
g_variant_get_type_string(value), "q");
|
goto fail;
|
}
|
|
/* received volume is in range [0, 127]*/
|
t->a2dp.ch1_volume = t->a2dp.ch2_volume = g_variant_get_uint16(value);
|
bluealsa_ctl_event(BA_EVENT_UPDATE_VOLUME);
|
}
|
|
g_variant_unref(value);
|
value = NULL;
|
}
|
|
fail:
|
if (properties != NULL)
|
g_variant_iter_free(properties);
|
if (value != NULL)
|
g_variant_unref(value);
|
}
|
|
/**
|
* Subscribe to BlueZ related signals.
|
*
|
* @return On success this function returns 0. Otherwise -1 is returned. */
|
int bluez_subscribe_signals(void) {
|
|
GDBusConnection *conn = config.dbus;
|
|
/* Note, that we do not have to subscribe for the interfaces remove signal,
|
* because prior to removal, BlueZ will emit appropriate "clear" signal. */
|
g_dbus_connection_signal_subscribe(conn, "org.bluez", "org.freedesktop.DBus.ObjectManager",
|
"InterfacesAdded", NULL, NULL, G_DBUS_SIGNAL_FLAGS_NONE,
|
/* TODO: Use arg0 filtering, but is seems it doesn't work... */
|
/* "InterfacesAdded", NULL, "/org/bluez/hci0", G_DBUS_SIGNAL_FLAGS_NONE, */
|
bluez_signal_interfaces_added, NULL, NULL);
|
|
g_dbus_connection_signal_subscribe(conn, "org.bluez", "org.freedesktop.DBus.Properties",
|
"PropertiesChanged", NULL, "org.bluez.MediaTransport1", G_DBUS_SIGNAL_FLAGS_NONE,
|
bluez_signal_transport_changed, NULL, NULL);
|
|
return 0;
|
}
|