/* * 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 #include #include #include #include #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; }