diff options
Diffstat (limited to 'audio')
54 files changed, 34285 insertions, 0 deletions
diff --git a/audio/a2dp-codecs.h b/audio/a2dp-codecs.h new file mode 100644 index 0000000..e44634e --- /dev/null +++ b/audio/a2dp-codecs.h @@ -0,0 +1,116 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define A2DP_CODEC_SBC 0x00 +#define A2DP_CODEC_MPEG12 0x01 +#define A2DP_CODEC_MPEG24 0x02 +#define A2DP_CODEC_ATRAC 0x03 + +#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 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 MAX_BITPOOL 64 +#define MIN_BITPOOL 2 + +#if __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; + uint16_t bitrate; +} __attribute__ ((packed)) a2dp_mpeg_t; + +#elif __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; + uint16_t bitrate; +} __attribute__ ((packed)) a2dp_mpeg_t; + +#else +#error "Unknown byte order" +#endif diff --git a/audio/a2dp.c b/audio/a2dp.c new file mode 100644 index 0000000..9d9c61d --- /dev/null +++ b/audio/a2dp.c @@ -0,0 +1,2361 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <errno.h> + +#include <dbus/dbus.h> +#include <glib.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include "log.h" +#include "device.h" +#include "manager.h" +#include "avdtp.h" +#include "sink.h" +#include "source.h" +#include "unix.h" +#include "media.h" +#include "transport.h" +#include "a2dp.h" +#include "sdpd.h" + +/* The duration that streams without users are allowed to stay in + * STREAMING state. */ +#define SUSPEND_TIMEOUT 5 +#define RECONFIGURE_TIMEOUT 500 + +#ifndef MIN +# define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif + +#ifndef MAX +# define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +struct a2dp_sep { + struct a2dp_server *server; + struct media_endpoint *endpoint; + uint8_t type; + uint8_t codec; + struct avdtp_local_sep *lsep; + struct avdtp *session; + struct avdtp_stream *stream; + guint suspend_timer; + gboolean delay_reporting; + gboolean locked; + gboolean suspending; + gboolean starting; +}; + +struct a2dp_setup_cb { + struct a2dp_setup *setup; + a2dp_select_cb_t select_cb; + a2dp_config_cb_t config_cb; + a2dp_stream_cb_t resume_cb; + a2dp_stream_cb_t suspend_cb; + void *user_data; + unsigned int id; +}; + +struct a2dp_setup { + struct audio_device *dev; + struct avdtp *session; + struct a2dp_sep *sep; + struct avdtp_remote_sep *rsep; + struct avdtp_stream *stream; + struct avdtp_error *err; + avdtp_set_configuration_cb setconf_cb; + GSList *caps; + gboolean reconfigure; + gboolean start; + GSList *cb; + int ref; +}; + +static DBusConnection *connection = NULL; + +struct a2dp_server { + bdaddr_t src; + GSList *sinks; + GSList *sources; + uint32_t source_record_id; + uint32_t sink_record_id; + uint16_t version; + gboolean sink_enabled; + gboolean source_enabled; +}; + +static GSList *servers = NULL; +static GSList *setups = NULL; +static unsigned int cb_id = 0; + +static struct a2dp_setup *setup_ref(struct a2dp_setup *setup) +{ + setup->ref++; + + DBG("%p: ref=%d", setup, setup->ref); + + return setup; +} + +static struct audio_device *a2dp_get_dev(struct avdtp *session) +{ + bdaddr_t src, dst; + + avdtp_get_peers(session, &src, &dst); + + return manager_find_device(NULL, &src, &dst, NULL, FALSE); +} + +static struct a2dp_setup *setup_new(struct avdtp *session) +{ + struct audio_device *dev; + struct a2dp_setup *setup; + + dev = a2dp_get_dev(session); + if (!dev) { + error("Unable to create setup"); + return NULL; + } + + setup = g_new0(struct a2dp_setup, 1); + setup->session = avdtp_ref(session); + setup->dev = a2dp_get_dev(session); + setups = g_slist_append(setups, setup); + + return setup; +} + +static void setup_free(struct a2dp_setup *s) +{ + DBG("%p", s); + + setups = g_slist_remove(setups, s); + if (s->session) + avdtp_unref(s->session); + g_slist_foreach(s->cb, (GFunc) g_free, NULL); + g_slist_free(s->cb); + g_slist_foreach(s->caps, (GFunc) g_free, NULL); + g_slist_free(s->caps); + g_free(s); +} + +static void setup_unref(struct a2dp_setup *setup) +{ + setup->ref--; + + DBG("%p: ref=%d", setup, setup->ref); + + if (setup->ref > 0) + return; + + setup_free(setup); +} + +static struct a2dp_setup_cb *setup_cb_new(struct a2dp_setup *setup) +{ + struct a2dp_setup_cb *cb; + + cb = g_new0(struct a2dp_setup_cb, 1); + cb->setup = setup; + cb->id = ++cb_id; + + setup->cb = g_slist_append(setup->cb, cb); + return cb; +} + +static void setup_cb_free(struct a2dp_setup_cb *cb) +{ + struct a2dp_setup *setup = cb->setup; + + setup->cb = g_slist_remove(setup->cb, cb); + setup_unref(cb->setup); + g_free(cb); +} + +static gboolean finalize_config(struct a2dp_setup *s) +{ + GSList *l; + struct avdtp_stream *stream = s->err ? NULL : s->stream; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->config_cb) + continue; + + cb->config_cb(s->session, s->sep, stream, s->err, + cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static gboolean finalize_config_errno(struct a2dp_setup *s, int err) +{ + struct avdtp_error avdtp_err; + + avdtp_error_init(&avdtp_err, AVDTP_ERRNO, -err); + s->err = err ? &avdtp_err : NULL; + + return finalize_config(s); +} + +static gboolean finalize_resume(struct a2dp_setup *s) +{ + GSList *l; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->resume_cb) + continue; + + cb->resume_cb(s->session, s->err, cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static gboolean finalize_resume_errno(struct a2dp_setup *s, int err) +{ + struct avdtp_error avdtp_err; + + avdtp_error_init(&avdtp_err, AVDTP_ERRNO, -err); + s->err = err ? &avdtp_err : NULL; + + return finalize_resume(s); +} + +static gboolean finalize_suspend(struct a2dp_setup *s) +{ + GSList *l; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->suspend_cb) + continue; + + cb->suspend_cb(s->session, s->err, cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static gboolean finalize_suspend_errno(struct a2dp_setup *s, int err) +{ + struct avdtp_error avdtp_err; + + avdtp_error_init(&avdtp_err, AVDTP_ERRNO, -err); + s->err = err ? &avdtp_err : NULL; + + return finalize_suspend(s); +} + +static gboolean finalize_select(struct a2dp_setup *s) +{ + GSList *l; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->select_cb) + continue; + + cb->select_cb(s->session, s->sep, s->caps, cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static struct a2dp_setup *find_setup_by_session(struct avdtp *session) +{ + GSList *l; + + for (l = setups; l != NULL; l = l->next) { + struct a2dp_setup *setup = l->data; + + if (setup->session == session) + return setup; + } + + return NULL; +} + +static struct a2dp_setup *a2dp_setup_get(struct avdtp *session) +{ + struct a2dp_setup *setup; + + setup = find_setup_by_session(session); + if (!setup) { + setup = setup_new(session); + if (!setup) + return NULL; + } + + return setup_ref(setup); +} + +static struct a2dp_setup *find_setup_by_dev(struct audio_device *dev) +{ + GSList *l; + + for (l = setups; l != NULL; l = l->next) { + struct a2dp_setup *setup = l->data; + + if (setup->dev == dev) + return setup; + } + + return NULL; +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *sep = user_data; + + if (new_state != AVDTP_STATE_IDLE) + return; + + if (sep->suspend_timer) { + g_source_remove(sep->suspend_timer); + sep->suspend_timer = 0; + } + + if (sep->session) { + avdtp_unref(sep->session); + sep->session = NULL; + } + + if (sep->endpoint) + media_endpoint_clear_configuration(sep->endpoint); + + sep->stream = NULL; + +} + +static gboolean auto_config(gpointer data) +{ + struct a2dp_setup *setup = data; + struct avdtp_error *err = NULL; + + /* Check if configuration was aborted */ + if (setup->sep->stream == NULL) + return FALSE; + + if (setup->err != NULL) { + err = setup->err; + goto done; + } + + avdtp_stream_add_cb(setup->session, setup->stream, + stream_state_changed, setup->sep); + + if (setup->sep->type == AVDTP_SEP_TYPE_SOURCE) + sink_new_stream(setup->dev, setup->session, setup->stream); + else + source_new_stream(setup->dev, setup->session, setup->stream); + +done: + if (setup->setconf_cb) + setup->setconf_cb(setup->session, setup->stream, setup->err); + + finalize_config(setup); + + if (err) + g_free(err); + + setup_unref(setup); + + return FALSE; +} + +static gboolean sbc_setconf_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Ind", sep); + else + DBG("Source %p: Set_Configuration_Ind", sep); + + setup = a2dp_setup_get(session); + if (!setup) + return FALSE; + + a2dp_sep->stream = stream; + setup->sep = a2dp_sep; + setup->stream = stream; + setup->setconf_cb = cb; + + /* Check valid settings */ + for (; caps != NULL; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + struct avdtp_media_codec_capability *codec_cap; + struct sbc_codec_cap *sbc_cap; + + if (cap->category == AVDTP_DELAY_REPORTING && + !a2dp_sep->delay_reporting) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_DELAY_REPORTING, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + if (cap->length < sizeof(struct sbc_codec_cap)) + continue; + + codec_cap = (void *) cap->data; + + if (codec_cap->media_codec_type != A2DP_CODEC_SBC) + continue; + + sbc_cap = (void *) codec_cap; + + if (sbc_cap->min_bitpool < MIN_BITPOOL || + sbc_cap->max_bitpool > MAX_BITPOOL) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + } + +done: + g_idle_add(auto_config, setup); + return TRUE; +} + +static gboolean sbc_getcap_ind(struct avdtp *session, struct avdtp_local_sep *sep, + gboolean get_all, GSList **caps, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct sbc_codec_cap sbc_cap; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Capability_Ind", sep); + else + DBG("Source %p: Get_Capability_Ind", sep); + + *caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + memset(&sbc_cap, 0, sizeof(struct sbc_codec_cap)); + + sbc_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + sbc_cap.cap.media_codec_type = A2DP_CODEC_SBC; + + sbc_cap.frequency = ( SBC_SAMPLING_FREQ_48000 | + SBC_SAMPLING_FREQ_44100 | + SBC_SAMPLING_FREQ_32000 | + SBC_SAMPLING_FREQ_16000 ); + + sbc_cap.channel_mode = ( SBC_CHANNEL_MODE_JOINT_STEREO | + SBC_CHANNEL_MODE_STEREO | + SBC_CHANNEL_MODE_DUAL_CHANNEL | + SBC_CHANNEL_MODE_MONO ); + + sbc_cap.block_length = ( SBC_BLOCK_LENGTH_16 | + SBC_BLOCK_LENGTH_12 | + SBC_BLOCK_LENGTH_8 | + SBC_BLOCK_LENGTH_4 ); + + sbc_cap.subbands = ( SBC_SUBBANDS_8 | SBC_SUBBANDS_4 ); + + sbc_cap.allocation_method = ( SBC_ALLOCATION_LOUDNESS | + SBC_ALLOCATION_SNR ); + + sbc_cap.min_bitpool = MIN_BITPOOL; + sbc_cap.max_bitpool = MAX_BITPOOL; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, + sizeof(sbc_cap)); + + *caps = g_slist_append(*caps, media_codec); + + if (get_all) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static gboolean mpeg_setconf_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Ind", sep); + else + DBG("Source %p: Set_Configuration_Ind", sep); + + setup = a2dp_setup_get(session); + if (!setup) + return FALSE; + + a2dp_sep->stream = stream; + setup->sep = a2dp_sep; + setup->stream = stream; + setup->setconf_cb = cb; + + for (; caps != NULL; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + + if (cap->category == AVDTP_DELAY_REPORTING && + !a2dp_sep->delay_reporting) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_DELAY_REPORTING, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + } + +done: + g_idle_add(auto_config, setup); + return TRUE; +} + +static gboolean mpeg_getcap_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + gboolean get_all, + GSList **caps, uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct mpeg_codec_cap mpeg_cap; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Capability_Ind", sep); + else + DBG("Source %p: Get_Capability_Ind", sep); + + *caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + memset(&mpeg_cap, 0, sizeof(struct mpeg_codec_cap)); + + mpeg_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + mpeg_cap.cap.media_codec_type = A2DP_CODEC_MPEG12; + + mpeg_cap.frequency = ( MPEG_SAMPLING_FREQ_48000 | + MPEG_SAMPLING_FREQ_44100 | + MPEG_SAMPLING_FREQ_32000 | + MPEG_SAMPLING_FREQ_24000 | + MPEG_SAMPLING_FREQ_22050 | + MPEG_SAMPLING_FREQ_16000 ); + + mpeg_cap.channel_mode = ( MPEG_CHANNEL_MODE_JOINT_STEREO | + MPEG_CHANNEL_MODE_STEREO | + MPEG_CHANNEL_MODE_DUAL_CHANNEL | + MPEG_CHANNEL_MODE_MONO ); + + mpeg_cap.layer = ( MPEG_LAYER_MP3 | MPEG_LAYER_MP2 | MPEG_LAYER_MP1 ); + + mpeg_cap.bitrate = 0xFFFF; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &mpeg_cap, + sizeof(mpeg_cap)); + + *caps = g_slist_append(*caps, media_codec); + + if (get_all) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static void endpoint_setconf_cb(struct media_endpoint *endpoint, void *ret, + int size, void *user_data) +{ + struct a2dp_setup *setup = user_data; + + if (ret == NULL) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + } + + auto_config(setup); +} + +static gboolean endpoint_setconf_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Ind", sep); + else + DBG("Source %p: Set_Configuration_Ind", sep); + + setup = a2dp_setup_get(session); + if (!session) + return FALSE; + + a2dp_sep->stream = stream; + setup->sep = a2dp_sep; + setup->stream = stream; + setup->setconf_cb = cb; + + for (; caps != NULL; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + struct avdtp_media_codec_capability *codec; + gboolean ret; + + if (cap->category == AVDTP_DELAY_REPORTING && + !a2dp_sep->delay_reporting) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_DELAY_REPORTING, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + codec = (struct avdtp_media_codec_capability *) cap->data; + + if (codec->media_codec_type != a2dp_sep->codec) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + + ret = media_endpoint_set_configuration(a2dp_sep->endpoint, + setup->dev, codec->data, + cap->length - sizeof(*codec), + endpoint_setconf_cb, setup); + if (ret) + return TRUE; + + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + break; + } + +done: + g_idle_add(auto_config, setup); + return TRUE; +} + +static gboolean endpoint_getcap_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + gboolean get_all, GSList **caps, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct avdtp_media_codec_capability *codec_caps; + uint8_t *capabilities; + size_t length; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Capability_Ind", sep); + else + DBG("Source %p: Get_Capability_Ind", sep); + + *caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + length = media_endpoint_get_capabilities(a2dp_sep->endpoint, + &capabilities); + + codec_caps = g_malloc0(sizeof(*codec_caps) + length); + codec_caps->media_type = AVDTP_MEDIA_TYPE_AUDIO; + codec_caps->media_codec_type = a2dp_sep->codec; + memcpy(codec_caps->data, capabilities, length); + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, codec_caps, + sizeof(*codec_caps) + length); + + *caps = g_slist_append(*caps, media_codec); + g_free(codec_caps); + + if (get_all) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static void endpoint_open_cb(struct media_endpoint *endpoint, void *ret, + int size, void *user_data) +{ + struct a2dp_setup *setup = user_data; + int err; + + if (ret == NULL) { + setup->stream = NULL; + finalize_config_errno(setup, -EPERM); + return; + } + + err = avdtp_open(setup->session, setup->stream); + if (err == 0) + return; + + error("Error on avdtp_open %s (%d)", strerror(-err), -err); + setup->stream = NULL; + finalize_config_errno(setup, err); +} + +static void setconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + struct audio_device *dev; + int ret; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Cfm", sep); + else + DBG("Source %p: Set_Configuration_Cfm", sep); + + setup = find_setup_by_session(session); + + if (err) { + if (setup) { + setup->err = err; + finalize_config(setup); + } + return; + } + + avdtp_stream_add_cb(session, stream, stream_state_changed, a2dp_sep); + a2dp_sep->stream = stream; + + if (!setup) + return; + + dev = a2dp_get_dev(session); + + /* Notify D-Bus interface of the new stream */ + if (a2dp_sep->type == AVDTP_SEP_TYPE_SOURCE) + sink_new_stream(dev, session, setup->stream); + else + source_new_stream(dev, session, setup->stream); + + /* Notify Endpoint */ + if (a2dp_sep->endpoint) { + struct avdtp_service_capability *service; + struct avdtp_media_codec_capability *codec; + + service = avdtp_stream_get_codec(stream); + codec = (struct avdtp_media_codec_capability *) service->data; + + if (media_endpoint_set_configuration(a2dp_sep->endpoint, dev, + codec->data, service->length - + sizeof(*codec), + endpoint_open_cb, setup) == + TRUE) + return; + + setup->stream = NULL; + finalize_config_errno(setup, -EPERM); + return; + } + + ret = avdtp_open(session, stream); + if (ret < 0) { + error("Error on avdtp_open %s (%d)", strerror(-ret), -ret); + setup->stream = NULL; + finalize_config_errno(setup, ret); + } +} + +static gboolean getconf_ind(struct avdtp *session, struct avdtp_local_sep *sep, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Configuration_Ind", sep); + else + DBG("Source %p: Get_Configuration_Ind", sep); + return TRUE; +} + +static void getconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Cfm", sep); + else + DBG("Source %p: Set_Configuration_Cfm", sep); +} + +static gboolean open_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Open_Ind", sep); + else + DBG("Source %p: Open_Ind", sep); + return TRUE; +} + +static void open_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Open_Cfm", sep); + else + DBG("Source %p: Open_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (setup->reconfigure) + setup->reconfigure = FALSE; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_config(setup); +} + +static gboolean suspend_timeout(struct a2dp_sep *sep) +{ + if (avdtp_suspend(sep->session, sep->stream) == 0) + sep->suspending = TRUE; + + sep->suspend_timer = 0; + + avdtp_unref(sep->session); + sep->session = NULL; + + return FALSE; +} + +static gboolean start_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Start_Ind", sep); + else + DBG("Source %p: Start_Ind", sep); + + setup = find_setup_by_session(session); + if (setup) + finalize_resume(setup); + + if (!a2dp_sep->locked) { + a2dp_sep->session = avdtp_ref(session); + a2dp_sep->suspend_timer = g_timeout_add_seconds(SUSPEND_TIMEOUT, + (GSourceFunc) suspend_timeout, + a2dp_sep); + } + + return TRUE; +} + +static void start_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Start_Cfm", sep); + else + DBG("Source %p: Start_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_resume(setup); +} + +static gboolean suspend_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Suspend_Ind", sep); + else + DBG("Source %p: Suspend_Ind", sep); + + if (a2dp_sep->suspend_timer) { + g_source_remove(a2dp_sep->suspend_timer); + a2dp_sep->suspend_timer = 0; + avdtp_unref(a2dp_sep->session); + a2dp_sep->session = NULL; + } + + return TRUE; +} + +static void suspend_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + gboolean start; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Suspend_Cfm", sep); + else + DBG("Source %p: Suspend_Cfm", sep); + + a2dp_sep->suspending = FALSE; + + setup = find_setup_by_session(session); + if (!setup) + return; + + start = setup->start; + setup->start = FALSE; + + if (err) { + setup->stream = NULL; + setup->err = err; + finalize_suspend(setup); + } + else + finalize_suspend_errno(setup, 0); + + if (!start) + return; + + if (err) { + setup->err = err; + finalize_suspend(setup); + } else if (avdtp_start(session, a2dp_sep->stream) < 0) { + struct avdtp_error start_err; + error("avdtp_start failed"); + avdtp_error_init(&start_err, AVDTP_ERRNO, EIO); + setup->err = err; + finalize_suspend(setup); + } +} + +static gboolean close_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Close_Ind", sep); + else + DBG("Source %p: Close_Ind", sep); + + setup = find_setup_by_session(session); + if (!setup) + return TRUE; + + finalize_suspend_errno(setup, -ECONNRESET); + finalize_resume_errno(setup, -ECONNRESET); + + return TRUE; +} + +static gboolean a2dp_reconfigure(gpointer data) +{ + struct a2dp_setup *setup = data; + struct a2dp_sep *sep = setup->sep; + int posix_err; + struct avdtp_media_codec_capability *rsep_codec; + struct avdtp_service_capability *cap; + + if (setup->rsep) { + cap = avdtp_get_codec(setup->rsep); + rsep_codec = (struct avdtp_media_codec_capability *) cap->data; + } + + if (!setup->rsep || sep->codec != rsep_codec->media_codec_type) + setup->rsep = avdtp_find_remote_sep(setup->session, sep->lsep); + + posix_err = avdtp_set_configuration(setup->session, setup->rsep, + sep->lsep, + setup->caps, + &setup->stream); + if (posix_err < 0) { + error("avdtp_set_configuration: %s", strerror(-posix_err)); + goto failed; + } + + return FALSE; + +failed: + finalize_config_errno(setup, posix_err); + return FALSE; +} + +static void close_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Close_Cfm", sep); + else + DBG("Source %p: Close_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (err) { + setup->stream = NULL; + setup->err = err; + finalize_config(setup); + return; + } + + if (!setup->rsep) + setup->rsep = avdtp_stream_get_remote_sep(stream); + + if (setup->reconfigure) + g_timeout_add(RECONFIGURE_TIMEOUT, a2dp_reconfigure, setup); +} + +static gboolean abort_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Abort_Ind", sep); + else + DBG("Source %p: Abort_Ind", sep); + + a2dp_sep->stream = NULL; + + return TRUE; +} + +static void abort_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Abort_Cfm", sep); + else + DBG("Source %p: Abort_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + setup_unref(setup); +} + +static gboolean reconf_ind(struct avdtp *session, struct avdtp_local_sep *sep, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: ReConfigure_Ind", sep); + else + DBG("Source %p: ReConfigure_Ind", sep); + + return TRUE; +} + +static gboolean delayreport_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + uint8_t rseid, uint16_t delay, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct audio_device *dev = a2dp_get_dev(session); + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: DelayReport_Ind", sep); + else + DBG("Source %p: DelayReport_Ind", sep); + + unix_delay_report(dev, rseid, delay); + + return TRUE; +} + +static gboolean endpoint_delayreport_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + uint8_t rseid, uint16_t delay, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct media_transport *transport; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: DelayReport_Ind", sep); + else + DBG("Source %p: DelayReport_Ind", sep); + + transport = media_endpoint_get_transport(a2dp_sep->endpoint); + if (transport == NULL) + return FALSE; + + media_transport_update_delay(transport, delay); + + return TRUE; +} + +static void reconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: ReConfigure_Cfm", sep); + else + DBG("Source %p: ReConfigure_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_config(setup); +} + +static void delay_report_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: DelayReport_Cfm", sep); + else + DBG("Source %p: DelayReport_Cfm", sep); +} + +static struct avdtp_sep_cfm cfm = { + .set_configuration = setconf_cfm, + .get_configuration = getconf_cfm, + .open = open_cfm, + .start = start_cfm, + .suspend = suspend_cfm, + .close = close_cfm, + .abort = abort_cfm, + .reconfigure = reconf_cfm, + .delay_report = delay_report_cfm, +}; + +static struct avdtp_sep_ind sbc_ind = { + .get_capability = sbc_getcap_ind, + .set_configuration = sbc_setconf_ind, + .get_configuration = getconf_ind, + .open = open_ind, + .start = start_ind, + .suspend = suspend_ind, + .close = close_ind, + .abort = abort_ind, + .reconfigure = reconf_ind, + .delayreport = delayreport_ind, +}; + +static struct avdtp_sep_ind mpeg_ind = { + .get_capability = mpeg_getcap_ind, + .set_configuration = mpeg_setconf_ind, + .get_configuration = getconf_ind, + .open = open_ind, + .start = start_ind, + .suspend = suspend_ind, + .close = close_ind, + .abort = abort_ind, + .reconfigure = reconf_ind, + .delayreport = delayreport_ind, +}; + +static struct avdtp_sep_ind endpoint_ind = { + .get_capability = endpoint_getcap_ind, + .set_configuration = endpoint_setconf_ind, + .get_configuration = getconf_ind, + .open = open_ind, + .start = start_ind, + .suspend = suspend_ind, + .close = close_ind, + .abort = abort_ind, + .reconfigure = reconf_ind, + .delayreport = endpoint_delayreport_ind, +}; + +static sdp_record_t *a2dp_record(uint8_t type, uint16_t avdtp_ver) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, l2cap_uuid, avdtp_uuid, a2dp_uuid; + sdp_profile_desc_t profile[1]; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *psm, *version, *features; + uint16_t lp = AVDTP_UUID; + uint16_t a2dp_ver = 0x0102, feat = 0x000f; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + if (type == AVDTP_SEP_TYPE_SOURCE) + sdp_uuid16_create(&a2dp_uuid, AUDIO_SOURCE_SVCLASS_ID); + else + sdp_uuid16_create(&a2dp_uuid, AUDIO_SINK_SVCLASS_ID); + svclass_id = sdp_list_append(0, &a2dp_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile[0].uuid, ADVANCED_AUDIO_PROFILE_ID); + profile[0].version = a2dp_ver; + pfseq = sdp_list_append(0, &profile[0]); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + psm = sdp_data_alloc(SDP_UINT16, &lp); + proto[0] = sdp_list_append(proto[0], psm); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&avdtp_uuid, AVDTP_UUID); + proto[1] = sdp_list_append(0, &avdtp_uuid); + version = sdp_data_alloc(SDP_UINT16, &avdtp_ver); + proto[1] = sdp_list_append(proto[1], version); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + features = sdp_data_alloc(SDP_UINT16, &feat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + if (type == AVDTP_SEP_TYPE_SOURCE) + sdp_set_info_attr(record, "Audio Source", 0, 0); + else + sdp_set_info_attr(record, "Audio Sink", 0, 0); + + free(psm); + free(version); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static struct a2dp_server *find_server(GSList *list, const bdaddr_t *src) +{ + GSList *l; + + for (l = list; l; l = l->next) { + struct a2dp_server *server = l->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +int a2dp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config) +{ + int sbc_srcs = 1, sbc_sinks = 1; + int mpeg12_srcs = 0, mpeg12_sinks = 0; + gboolean source = TRUE, sink = FALSE, socket = TRUE; + gboolean delay_reporting = FALSE; + char *str; + GError *err = NULL; + int i; + struct a2dp_server *server; + + if (!config) + goto proceed; + + str = g_key_file_get_string(config, "General", "Enable", &err); + + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + if (strstr(str, "Sink")) + source = TRUE; + if (strstr(str, "Source")) + sink = TRUE; + g_free(str); + } + + str = g_key_file_get_string(config, "General", "Disable", &err); + + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + if (strstr(str, "Sink")) + source = FALSE; + if (strstr(str, "Source")) + sink = FALSE; + if (strstr(str, "Socket")) + socket = FALSE; + g_free(str); + } + + /* Don't register any local sep if Socket is disabled */ + if (socket == FALSE) { + sbc_srcs = 0; + sbc_sinks = 0; + mpeg12_srcs = 0; + mpeg12_sinks = 0; + goto proceed; + } + + str = g_key_file_get_string(config, "A2DP", "SBCSources", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + sbc_srcs = atoi(str); + g_free(str); + } + + str = g_key_file_get_string(config, "A2DP", "MPEG12Sources", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + mpeg12_srcs = atoi(str); + g_free(str); + } + + str = g_key_file_get_string(config, "A2DP", "SBCSinks", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + sbc_sinks = atoi(str); + g_free(str); + } + + str = g_key_file_get_string(config, "A2DP", "MPEG12Sinks", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + mpeg12_sinks = atoi(str); + g_free(str); + } + +proceed: + if (!connection) + connection = dbus_connection_ref(conn); + + server = find_server(servers, src); + if (!server) { + int av_err; + + server = g_new0(struct a2dp_server, 1); + if (!server) + return -ENOMEM; + + av_err = avdtp_init(src, config, &server->version); + if (av_err < 0) { + g_free(server); + return av_err; + } + + bacpy(&server->src, src); + servers = g_slist_append(servers, server); + } + + if (config) + delay_reporting = g_key_file_get_boolean(config, "A2DP", + "DelayReporting", NULL); + + if (delay_reporting) + server->version = 0x0103; + else + server->version = 0x0102; + + server->source_enabled = source; + if (source) { + for (i = 0; i < sbc_srcs; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SOURCE, + A2DP_CODEC_SBC, delay_reporting, NULL, NULL); + + for (i = 0; i < mpeg12_srcs; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SOURCE, + A2DP_CODEC_MPEG12, delay_reporting, + NULL, NULL); + } + server->sink_enabled = sink; + if (sink) { + for (i = 0; i < sbc_sinks; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SINK, + A2DP_CODEC_SBC, delay_reporting, NULL, NULL); + + for (i = 0; i < mpeg12_sinks; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SINK, + A2DP_CODEC_MPEG12, delay_reporting, + NULL, NULL); + } + + return 0; +} + +static void a2dp_unregister_sep(struct a2dp_sep *sep) +{ + if (sep->endpoint) { + media_endpoint_release(sep->endpoint); + sep->endpoint = NULL; + } + + avdtp_unregister_sep(sep->lsep); + g_free(sep); +} + +void a2dp_unregister(const bdaddr_t *src) +{ + struct a2dp_server *server; + + server = find_server(servers, src); + if (!server) + return; + + g_slist_foreach(server->sinks, (GFunc) a2dp_remove_sep, NULL); + g_slist_free(server->sinks); + + g_slist_foreach(server->sources, (GFunc) a2dp_remove_sep, NULL); + g_slist_free(server->sources); + + avdtp_exit(src); + + servers = g_slist_remove(servers, server); + g_free(server); + + if (servers) + return; + + dbus_connection_unref(connection); + connection = NULL; +} + +struct a2dp_sep *a2dp_add_sep(const bdaddr_t *src, uint8_t type, + uint8_t codec, gboolean delay_reporting, + struct media_endpoint *endpoint, int *err) +{ + struct a2dp_server *server; + struct a2dp_sep *sep; + GSList **l; + uint32_t *record_id; + sdp_record_t *record; + struct avdtp_sep_ind *ind; + + server = find_server(servers, src); + if (server == NULL) { + if (err) + *err = -EINVAL; + return NULL; + } + + if (type == AVDTP_SEP_TYPE_SINK && !server->sink_enabled) { + if (err) + *err = -EPROTONOSUPPORT; + return NULL; + } + + if (type == AVDTP_SEP_TYPE_SOURCE && !server->source_enabled) { + if (err) + *err = -EPROTONOSUPPORT; + return NULL; + } + + sep = g_new0(struct a2dp_sep, 1); + + if (endpoint) { + ind = &endpoint_ind; + goto proceed; + } + + ind = (codec == A2DP_CODEC_MPEG12) ? &mpeg_ind : &sbc_ind; + +proceed: + sep->lsep = avdtp_register_sep(&server->src, type, + AVDTP_MEDIA_TYPE_AUDIO, codec, + delay_reporting, ind, &cfm, sep); + if (sep->lsep == NULL) { + g_free(sep); + if (err) + *err = -EINVAL; + return NULL; + } + + sep->server = server; + sep->endpoint = endpoint; + sep->codec = codec; + sep->type = type; + sep->delay_reporting = delay_reporting; + + if (type == AVDTP_SEP_TYPE_SOURCE) { + l = &server->sources; + record_id = &server->source_record_id; + } else { + l = &server->sinks; + record_id = &server->sink_record_id; + } + + if (*record_id != 0) + goto add; + + record = a2dp_record(type, server->version); + if (!record) { + error("Unable to allocate new service record"); + avdtp_unregister_sep(sep->lsep); + g_free(sep); + if (err) + *err = -EINVAL; + return NULL; + } + + if (add_record_to_server(&server->src, record) < 0) { + error("Unable to register A2DP service record");\ + sdp_record_free(record); + avdtp_unregister_sep(sep->lsep); + g_free(sep); + if (err) + *err = -EINVAL; + return NULL; + } + *record_id = record->handle; + +add: + *l = g_slist_append(*l, sep); + + if (err) + *err = 0; + return sep; +} + +void a2dp_remove_sep(struct a2dp_sep *sep) +{ + struct a2dp_server *server = sep->server; + + if (sep->type == AVDTP_SEP_TYPE_SOURCE) { + if (g_slist_find(server->sources, sep) == NULL) + return; + server->sources = g_slist_remove(server->sources, sep); + if (server->sources == NULL && server->source_record_id) { + remove_record_from_server(server->source_record_id); + server->source_record_id = 0; + } + } else { + if (g_slist_find(server->sinks, sep) == NULL) + return; + server->sinks = g_slist_remove(server->sinks, sep); + if (server->sinks == NULL && server->sink_record_id) { + remove_record_from_server(server->sink_record_id); + server->sink_record_id = 0; + } + } + + a2dp_unregister_sep(sep); +} + +struct a2dp_sep *a2dp_get(struct avdtp *session, + struct avdtp_remote_sep *rsep) +{ + GSList *l; + struct a2dp_server *server; + struct avdtp_service_capability *cap; + struct avdtp_media_codec_capability *codec_cap = NULL; + bdaddr_t src; + + avdtp_get_peers(session, &src, NULL); + server = find_server(servers, &src); + if (!server) + return NULL; + + cap = avdtp_get_codec(rsep); + codec_cap = (void *) cap->data; + + if (avdtp_get_type(rsep) == AVDTP_SEP_TYPE_SINK) + l = server->sources; + else + l = server->sinks; + + for (; l != NULL; l = l->next) { + struct a2dp_sep *sep = l->data; + + if (sep->locked) + continue; + + if (sep->codec != codec_cap->media_codec_type) + continue; + + if (!sep->stream || avdtp_has_stream(session, sep->stream)) + return sep; + } + + return NULL; +} + +static uint8_t default_bitpool(uint8_t freq, uint8_t mode) +{ + switch (freq) { + case SBC_SAMPLING_FREQ_16000: + case SBC_SAMPLING_FREQ_32000: + return 53; + case SBC_SAMPLING_FREQ_44100: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return 31; + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return 53; + default: + error("Invalid channel mode %u", mode); + return 53; + } + case SBC_SAMPLING_FREQ_48000: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return 29; + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return 51; + default: + error("Invalid channel mode %u", mode); + return 51; + } + default: + error("Invalid sampling freq %u", freq); + return 53; + } +} + +static gboolean select_sbc_params(struct sbc_codec_cap *cap, + struct sbc_codec_cap *supported) +{ + unsigned int max_bitpool, min_bitpool; + + memset(cap, 0, sizeof(struct sbc_codec_cap)); + + cap->cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + cap->cap.media_codec_type = A2DP_CODEC_SBC; + + if (supported->frequency & SBC_SAMPLING_FREQ_44100) + cap->frequency = SBC_SAMPLING_FREQ_44100; + else if (supported->frequency & SBC_SAMPLING_FREQ_48000) + cap->frequency = SBC_SAMPLING_FREQ_48000; + else if (supported->frequency & SBC_SAMPLING_FREQ_32000) + cap->frequency = SBC_SAMPLING_FREQ_32000; + else if (supported->frequency & SBC_SAMPLING_FREQ_16000) + cap->frequency = SBC_SAMPLING_FREQ_16000; + else { + error("No supported frequencies"); + return FALSE; + } + + if (supported->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO) + cap->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO; + else if (supported->channel_mode & SBC_CHANNEL_MODE_STEREO) + cap->channel_mode = SBC_CHANNEL_MODE_STEREO; + else if (supported->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL) + cap->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL; + else if (supported->channel_mode & SBC_CHANNEL_MODE_MONO) + cap->channel_mode = SBC_CHANNEL_MODE_MONO; + else { + error("No supported channel modes"); + return FALSE; + } + + if (supported->block_length & SBC_BLOCK_LENGTH_16) + cap->block_length = SBC_BLOCK_LENGTH_16; + else if (supported->block_length & SBC_BLOCK_LENGTH_12) + cap->block_length = SBC_BLOCK_LENGTH_12; + else if (supported->block_length & SBC_BLOCK_LENGTH_8) + cap->block_length = SBC_BLOCK_LENGTH_8; + else if (supported->block_length & SBC_BLOCK_LENGTH_4) + cap->block_length = SBC_BLOCK_LENGTH_4; + else { + error("No supported block lengths"); + return FALSE; + } + + if (supported->subbands & SBC_SUBBANDS_8) + cap->subbands = SBC_SUBBANDS_8; + else if (supported->subbands & SBC_SUBBANDS_4) + cap->subbands = SBC_SUBBANDS_4; + else { + error("No supported subbands"); + return FALSE; + } + + if (supported->allocation_method & SBC_ALLOCATION_LOUDNESS) + cap->allocation_method = SBC_ALLOCATION_LOUDNESS; + else if (supported->allocation_method & SBC_ALLOCATION_SNR) + cap->allocation_method = SBC_ALLOCATION_SNR; + + min_bitpool = MAX(MIN_BITPOOL, supported->min_bitpool); + max_bitpool = MIN(default_bitpool(cap->frequency, cap->channel_mode), + supported->max_bitpool); + + cap->min_bitpool = min_bitpool; + cap->max_bitpool = max_bitpool; + + return TRUE; +} + +static gboolean select_capabilities(struct avdtp *session, + struct avdtp_remote_sep *rsep, + GSList **caps) +{ + struct avdtp_service_capability *media_transport, *media_codec; + struct sbc_codec_cap sbc_cap; + + media_codec = avdtp_get_codec(rsep); + if (!media_codec) + return FALSE; + + select_sbc_params(&sbc_cap, (struct sbc_codec_cap *) media_codec->data); + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, + sizeof(sbc_cap)); + + *caps = g_slist_append(*caps, media_codec); + + if (avdtp_get_delay_reporting(rsep)) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static void select_cb(struct media_endpoint *endpoint, void *ret, int size, + void *user_data) +{ + struct a2dp_setup *setup = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct avdtp_media_codec_capability *cap; + + if (size < 0) { + DBG("Endpoint replied an invalid configuration"); + goto done; + } + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + setup->caps = g_slist_append(setup->caps, media_transport); + + cap = g_malloc0(sizeof(*cap) + size); + cap->media_type = AVDTP_MEDIA_TYPE_AUDIO; + cap->media_codec_type = setup->sep->codec; + memcpy(cap->data, ret, size); + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, cap, + sizeof(*cap) + size); + + setup->caps = g_slist_append(setup->caps, media_codec); + g_free(cap); + +done: + finalize_select(setup); +} + +static gboolean auto_select(gpointer data) +{ + struct a2dp_setup *setup = data; + + finalize_select(setup); + + return FALSE; +} + +static struct a2dp_sep *a2dp_find_sep(struct avdtp *session, GSList *list, + const char *sender) +{ + for (; list; list = list->next) { + struct a2dp_sep *sep = list->data; + + /* Use sender's endpoint if available */ + if (sender) { + const char *name; + + if (sep->endpoint == NULL) + continue; + + name = media_endpoint_get_sender(sep->endpoint); + if (g_strcmp0(sender, name) != 0) + continue; + } + + if (avdtp_find_remote_sep(session, sep->lsep) == NULL) + continue; + + return sep; + } + + return NULL; +} + +static struct a2dp_sep *a2dp_select_sep(struct avdtp *session, uint8_t type, + const char *sender) +{ + struct a2dp_server *server; + struct a2dp_sep *sep; + GSList *l; + bdaddr_t src; + + avdtp_get_peers(session, &src, NULL); + server = find_server(servers, &src); + if (!server) + return NULL; + + l = type == AVDTP_SEP_TYPE_SINK ? server->sources : server->sinks; + + /* Check sender's seps first */ + sep = a2dp_find_sep(session, l, sender); + if (sep != NULL) + return sep; + + return a2dp_find_sep(session, l, NULL); +} + +unsigned int a2dp_select_capabilities(struct avdtp *session, + uint8_t type, const char *sender, + a2dp_select_cb_t cb, + void *user_data) +{ + struct a2dp_setup *setup; + struct a2dp_setup_cb *cb_data; + struct a2dp_sep *sep; + struct avdtp_service_capability *service; + struct avdtp_media_codec_capability *codec; + + sep = a2dp_select_sep(session, type, sender); + if (!sep) { + error("Unable to select SEP"); + return 0; + } + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->select_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->rsep = avdtp_find_remote_sep(session, sep->lsep); + + if (setup->rsep == NULL) { + error("Could not find remote sep"); + goto fail; + } + + /* FIXME: Remove auto select when it is not longer possible to register + endpoint in the configuration file */ + if (sep->endpoint == NULL) { + if (!select_capabilities(session, setup->rsep, + &setup->caps)) { + error("Unable to auto select remote SEP capabilities"); + goto fail; + } + + g_idle_add(auto_select, setup); + + return cb_data->id; + } + + service = avdtp_get_codec(setup->rsep); + codec = (struct avdtp_media_codec_capability *) service->data; + + if (media_endpoint_select_configuration(sep->endpoint, codec->data, + service->length - sizeof(*codec), + select_cb, setup) == + TRUE) + return cb_data->id; + +fail: + setup_cb_free(cb_data); + return 0; + +} + +unsigned int a2dp_config(struct avdtp *session, struct a2dp_sep *sep, + a2dp_config_cb_t cb, GSList *caps, + void *user_data) +{ + struct a2dp_setup_cb *cb_data; + GSList *l; + struct a2dp_server *server; + struct a2dp_setup *setup; + struct a2dp_sep *tmp; + struct avdtp_service_capability *cap; + struct avdtp_media_codec_capability *codec_cap = NULL; + int posix_err; + bdaddr_t src; + uint8_t remote_type; + + avdtp_get_peers(session, &src, NULL); + server = find_server(servers, &src); + if (!server) + return 0; + + for (l = caps; l != NULL; l = l->next) { + cap = l->data; + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + codec_cap = (void *) cap->data; + break; + } + + if (!codec_cap) + return 0; + + if (sep->codec != codec_cap->media_codec_type) + return 0; + + DBG("a2dp_config: selected SEP %p", sep->lsep); + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->config_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->stream = sep->stream; + + /* Copy given caps if they are different than current caps */ + if (setup->caps != caps) { + g_slist_foreach(setup->caps, (GFunc) g_free, NULL); + g_slist_free(setup->caps); + setup->caps = g_slist_copy(caps); + } + + switch (avdtp_sep_get_state(sep->lsep)) { + case AVDTP_STATE_IDLE: + if (sep->type == AVDTP_SEP_TYPE_SOURCE) { + l = server->sources; + remote_type = AVDTP_SEP_TYPE_SINK; + } else { + remote_type = AVDTP_SEP_TYPE_SOURCE; + l = server->sinks; + } + + for (; l != NULL; l = l->next) { + tmp = l->data; + if (avdtp_has_stream(session, tmp->stream)) + break; + } + + if (l != NULL) { + if (a2dp_sep_get_lock(tmp)) + goto failed; + setup->reconfigure = TRUE; + if (avdtp_close(session, tmp->stream, FALSE) < 0) { + error("avdtp_close failed"); + goto failed; + } + break; + } + + setup->rsep = avdtp_find_remote_sep(session, sep->lsep); + if (setup->rsep == NULL) { + error("No matching ACP and INT SEPs found"); + goto failed; + } + + posix_err = avdtp_set_configuration(session, setup->rsep, + sep->lsep, caps, + &setup->stream); + if (posix_err < 0) { + error("avdtp_set_configuration: %s", + strerror(-posix_err)); + goto failed; + } + break; + case AVDTP_STATE_OPEN: + case AVDTP_STATE_STREAMING: + if (avdtp_stream_has_capabilities(setup->stream, caps)) { + DBG("Configuration match: resuming"); + g_idle_add((GSourceFunc) finalize_config, setup); + } else if (!setup->reconfigure) { + setup->reconfigure = TRUE; + if (avdtp_close(session, sep->stream, FALSE) < 0) { + error("avdtp_close failed"); + goto failed; + } + } + break; + default: + error("SEP in bad state for requesting a new stream"); + goto failed; + } + + return cb_data->id; + +failed: + setup_cb_free(cb_data); + return 0; +} + +unsigned int a2dp_resume(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data) +{ + struct a2dp_setup_cb *cb_data; + struct a2dp_setup *setup; + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->resume_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->stream = sep->stream; + + switch (avdtp_sep_get_state(sep->lsep)) { + case AVDTP_STATE_IDLE: + goto failed; + break; + case AVDTP_STATE_OPEN: + if (avdtp_start(session, sep->stream) < 0) { + error("avdtp_start failed"); + goto failed; + } + break; + case AVDTP_STATE_STREAMING: + if (!sep->suspending && sep->suspend_timer) { + g_source_remove(sep->suspend_timer); + sep->suspend_timer = 0; + avdtp_unref(sep->session); + sep->session = NULL; + } + if (sep->suspending) + setup->start = TRUE; + else + g_idle_add((GSourceFunc) finalize_resume, setup); + break; + default: + error("SEP in bad state for resume"); + goto failed; + } + + return cb_data->id; + +failed: + setup_cb_free(cb_data); + return 0; +} + +unsigned int a2dp_suspend(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data) +{ + struct a2dp_setup_cb *cb_data; + struct a2dp_setup *setup; + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->suspend_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->stream = sep->stream; + + switch (avdtp_sep_get_state(sep->lsep)) { + case AVDTP_STATE_IDLE: + error("a2dp_suspend: no stream to suspend"); + goto failed; + break; + case AVDTP_STATE_OPEN: + g_idle_add((GSourceFunc) finalize_suspend, setup); + break; + case AVDTP_STATE_STREAMING: + if (avdtp_suspend(session, sep->stream) < 0) { + error("avdtp_suspend failed"); + goto failed; + } + sep->suspending = TRUE; + break; + default: + error("SEP in bad state for suspend"); + goto failed; + } + + return cb_data->id; + +failed: + setup_cb_free(cb_data); + return 0; +} + +gboolean a2dp_cancel(struct audio_device *dev, unsigned int id) +{ + struct a2dp_setup *setup; + GSList *l; + + setup = find_setup_by_dev(dev); + if (!setup) + return FALSE; + + for (l = setup->cb; l != NULL; l = g_slist_next(l)) { + struct a2dp_setup_cb *cb = l->data; + + if (cb->id != id) + continue; + + setup_ref(setup); + setup_cb_free(cb); + + if (!setup->cb) { + DBG("aborting setup %p", setup); + avdtp_abort(setup->session, setup->stream); + return TRUE; + } + + setup_unref(setup); + return TRUE; + } + + return FALSE; +} + +gboolean a2dp_sep_lock(struct a2dp_sep *sep, struct avdtp *session) +{ + if (sep->locked) + return FALSE; + + DBG("SEP %p locked", sep->lsep); + sep->locked = TRUE; + + return TRUE; +} + +gboolean a2dp_sep_unlock(struct a2dp_sep *sep, struct avdtp *session) +{ + avdtp_state_t state; + + state = avdtp_sep_get_state(sep->lsep); + + sep->locked = FALSE; + + DBG("SEP %p unlocked", sep->lsep); + + if (!sep->stream || state == AVDTP_STATE_IDLE) + return TRUE; + + switch (state) { + case AVDTP_STATE_OPEN: + /* Set timer here */ + break; + case AVDTP_STATE_STREAMING: + if (avdtp_suspend(session, sep->stream) == 0) + sep->suspending = TRUE; + break; + default: + break; + } + + return TRUE; +} + +gboolean a2dp_sep_get_lock(struct a2dp_sep *sep) +{ + return sep->locked; +} + +static int stream_cmp(gconstpointer data, gconstpointer user_data) +{ + const struct a2dp_sep *sep = data; + const struct avdtp_stream *stream = user_data; + + return (sep->stream != stream); +} + +struct a2dp_sep *a2dp_get_sep(struct avdtp *session, + struct avdtp_stream *stream) +{ + struct a2dp_server *server; + bdaddr_t src, dst; + GSList *l; + + avdtp_get_peers(session, &src, &dst); + + for (l = servers; l; l = l->next) { + server = l->data; + + if (bacmp(&src, &server->src) == 0) + break; + } + + if (!l) + return NULL; + + l = g_slist_find_custom(server->sources, stream, stream_cmp); + if (l) + return l->data; + + l = g_slist_find_custom(server->sinks, stream, stream_cmp); + if (l) + return l->data; + + return NULL; +} + +struct avdtp_stream *a2dp_sep_get_stream(struct a2dp_sep *sep) +{ + return sep->stream; +} diff --git a/audio/a2dp.h b/audio/a2dp.h new file mode 100644 index 0000000..5c4232d --- /dev/null +++ b/audio/a2dp.h @@ -0,0 +1,164 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define A2DP_CODEC_SBC 0x00 +#define A2DP_CODEC_MPEG12 0x01 +#define A2DP_CODEC_MPEG24 0x02 +#define A2DP_CODEC_ATRAC 0x03 + +#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 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 MAX_BITPOOL 64 +#define MIN_BITPOOL 2 + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct sbc_codec_cap { + struct avdtp_media_codec_capability cap; + 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)); + +struct mpeg_codec_cap { + struct avdtp_media_codec_capability cap; + 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; + uint16_t bitrate; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct sbc_codec_cap { + struct avdtp_media_codec_capability cap; + 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)); + +struct mpeg_codec_cap { + struct avdtp_media_codec_capability cap; + 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; + uint16_t bitrate; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +struct a2dp_sep; + + +typedef void (*a2dp_select_cb_t) (struct avdtp *session, + struct a2dp_sep *sep, GSList *caps, + void *user_data); +typedef void (*a2dp_config_cb_t) (struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data); +typedef void (*a2dp_stream_cb_t) (struct avdtp *session, + struct avdtp_error *err, + void *user_data); + +int a2dp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config); +void a2dp_unregister(const bdaddr_t *src); + +struct a2dp_sep *a2dp_add_sep(const bdaddr_t *src, uint8_t type, + uint8_t codec, gboolean delay_reporting, + struct media_endpoint *endpoint, int *err); +void a2dp_remove_sep(struct a2dp_sep *sep); + +struct a2dp_sep *a2dp_get(struct avdtp *session, struct avdtp_remote_sep *sep); + +unsigned int a2dp_select_capabilities(struct avdtp *session, + uint8_t type, const char *sender, + a2dp_select_cb_t cb, + void *user_data); +unsigned int a2dp_config(struct avdtp *session, struct a2dp_sep *sep, + a2dp_config_cb_t cb, GSList *caps, + void *user_data); +unsigned int a2dp_resume(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data); +unsigned int a2dp_suspend(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data); +gboolean a2dp_cancel(struct audio_device *dev, unsigned int id); + +gboolean a2dp_sep_lock(struct a2dp_sep *sep, struct avdtp *session); +gboolean a2dp_sep_unlock(struct a2dp_sep *sep, struct avdtp *session); +gboolean a2dp_sep_get_lock(struct a2dp_sep *sep); +struct avdtp_stream *a2dp_sep_get_stream(struct a2dp_sep *sep); +struct a2dp_sep *a2dp_get_sep(struct avdtp *session, + struct avdtp_stream *stream); diff --git a/audio/audio.conf b/audio/audio.conf new file mode 100644 index 0000000..302e046 --- /dev/null +++ b/audio/audio.conf @@ -0,0 +1,45 @@ +# Configuration file for the audio service + +# This section contains options which are not specific to any +# particular interface +[General] + +# Switch to master role for incoming connections (defaults to true) +#Master=true + +# If we want to disable support for specific services +# Defaults to supporting all implemented services +#Disable=Control,Source + +# SCO routing. Either PCM or HCI (in which case audio is routed to/from ALSA) +# Defaults to HCI +#SCORouting=PCM + +# Automatically connect both A2DP and HFP/HSP profiles for incoming +# connections. Some headsets that support both profiles will only connect the +# other one automatically so the default setting of true is usually a good +# idea. +#AutoConnect=true + +# Headset interface specific options (i.e. options which affect how the audio +# service interacts with remote headset devices) +[Headset] + +# Set to true to support HFP, false means only HSP is supported +# Defaults to true +HFP=true + +# Maximum number of connected HSP/HFP devices per adapter. Defaults to 1 +MaxConnected=1 + +# Set to true to enable use of fast connectable mode (faster page scanning) +# for HFP when incomming call starts. Default settings are restored after +# call is answered or rejected. Page scan interval is much shorter and page +# scan type changed to interlaced. Such allows faster connection initiated +# by a headset. +FastConnectable=false + +# Just an example of potential config options for the other interfaces +#[A2DP] +#SBCSources=1 +#MPEG12Sources=0 diff --git a/audio/avdtp.c b/audio/avdtp.c new file mode 100644 index 0000000..83b1aa2 --- /dev/null +++ b/audio/avdtp.c @@ -0,0 +1,3914 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <errno.h> +#include <unistd.h> +#include <assert.h> +#include <signal.h> +#include <netinet/in.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> + +#include "log.h" + +#include "../src/adapter.h" +#include "../src/manager.h" +#include "../src/device.h" + +#include "device.h" +#include "manager.h" +#include "control.h" +#include "avdtp.h" +#include "glib-helper.h" +#include "btio.h" +#include "sink.h" +#include "source.h" + +#define AVDTP_PSM 25 + +#define MAX_SEID 0x3E + +#define AVDTP_DISCOVER 0x01 +#define AVDTP_GET_CAPABILITIES 0x02 +#define AVDTP_SET_CONFIGURATION 0x03 +#define AVDTP_GET_CONFIGURATION 0x04 +#define AVDTP_RECONFIGURE 0x05 +#define AVDTP_OPEN 0x06 +#define AVDTP_START 0x07 +#define AVDTP_CLOSE 0x08 +#define AVDTP_SUSPEND 0x09 +#define AVDTP_ABORT 0x0A +#define AVDTP_SECURITY_CONTROL 0x0B +#define AVDTP_GET_ALL_CAPABILITIES 0x0C +#define AVDTP_DELAY_REPORT 0x0D + +#define AVDTP_PKT_TYPE_SINGLE 0x00 +#define AVDTP_PKT_TYPE_START 0x01 +#define AVDTP_PKT_TYPE_CONTINUE 0x02 +#define AVDTP_PKT_TYPE_END 0x03 + +#define AVDTP_MSG_TYPE_COMMAND 0x00 +#define AVDTP_MSG_TYPE_GEN_REJECT 0x01 +#define AVDTP_MSG_TYPE_ACCEPT 0x02 +#define AVDTP_MSG_TYPE_REJECT 0x03 + +#define REQ_TIMEOUT 6 +#define ABORT_TIMEOUT 2 +#define DISCONNECT_TIMEOUT 1 +#define STREAM_TIMEOUT 20 + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avdtp_common_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; +} __attribute__ ((packed)); + +struct avdtp_single_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; + uint8_t signal_id:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +struct avdtp_start_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; + uint8_t no_of_packets; + uint8_t signal_id:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +struct avdtp_continue_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; +} __attribute__ ((packed)); + +struct seid_info { + uint8_t rfa0:1; + uint8_t inuse:1; + uint8_t seid:6; + uint8_t rfa2:3; + uint8_t type:1; + uint8_t media_type:4; +} __attribute__ ((packed)); + +struct seid { + uint8_t rfa0:2; + uint8_t seid:6; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avdtp_common_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; +} __attribute__ ((packed)); + +struct avdtp_single_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; + uint8_t rfa0:2; + uint8_t signal_id:6; +} __attribute__ ((packed)); + +struct avdtp_start_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; + uint8_t no_of_packets; + uint8_t rfa0:2; + uint8_t signal_id:6; +} __attribute__ ((packed)); + +struct avdtp_continue_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; +} __attribute__ ((packed)); + +struct seid_info { + uint8_t seid:6; + uint8_t inuse:1; + uint8_t rfa0:1; + uint8_t media_type:4; + uint8_t type:1; + uint8_t rfa2:3; +} __attribute__ ((packed)); + +struct seid { + uint8_t seid:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +/* packets */ + +struct discover_resp { + struct seid_info seps[0]; +} __attribute__ ((packed)); + +struct getcap_resp { + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct start_req { + struct seid first_seid; + struct seid other_seids[0]; +} __attribute__ ((packed)); + +struct suspend_req { + struct seid first_seid; + struct seid other_seids[0]; +} __attribute__ ((packed)); + +struct seid_rej { + uint8_t error; +} __attribute__ ((packed)); + +struct conf_rej { + uint8_t category; + uint8_t error; +} __attribute__ ((packed)); + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct seid_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; +} __attribute__ ((packed)); + +struct setconf_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; + uint8_t rfa1:2; + uint8_t int_seid:6; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct stream_rej { + uint8_t rfa0:2; + uint8_t acp_seid:6; + uint8_t error; +} __attribute__ ((packed)); + +struct reconf_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; + + uint8_t serv_cap; + uint8_t serv_cap_len; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct delay_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; + uint16_t delay; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct seid_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +struct setconf_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; + uint8_t int_seid:6; + uint8_t rfa1:2; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct stream_rej { + uint8_t acp_seid:6; + uint8_t rfa0:2; + uint8_t error; +} __attribute__ ((packed)); + +struct reconf_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; + + uint8_t serv_cap; + uint8_t serv_cap_len; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct delay_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; + uint16_t delay; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +struct in_buf { + gboolean active; + int no_of_packets; + uint8_t transaction; + uint8_t message_type; + uint8_t signal_id; + uint8_t buf[1024]; + uint8_t data_size; +}; + +struct pending_req { + uint8_t transaction; + uint8_t signal_id; + void *data; + size_t data_size; + struct avdtp_stream *stream; /* Set if the request targeted a stream */ + guint timeout; +}; + +struct avdtp_remote_sep { + uint8_t seid; + uint8_t type; + uint8_t media_type; + struct avdtp_service_capability *codec; + gboolean delay_reporting; + GSList *caps; /* of type struct avdtp_service_capability */ + struct avdtp_stream *stream; +}; + +struct avdtp_server { + bdaddr_t src; + uint16_t version; + GIOChannel *io; + GSList *seps; + GSList *sessions; +}; + +struct avdtp_local_sep { + avdtp_state_t state; + struct avdtp_stream *stream; + struct seid_info info; + uint8_t codec; + gboolean delay_reporting; + GSList *caps; + struct avdtp_sep_ind *ind; + struct avdtp_sep_cfm *cfm; + void *user_data; + struct avdtp_server *server; +}; + +struct stream_callback { + avdtp_stream_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct avdtp_state_callback { + avdtp_session_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct avdtp_stream { + GIOChannel *io; + uint16_t imtu; + uint16_t omtu; + struct avdtp *session; + struct avdtp_local_sep *lsep; + uint8_t rseid; + GSList *caps; + GSList *callbacks; + struct avdtp_service_capability *codec; + guint io_id; /* Transport GSource ID */ + guint timer; /* Waiting for other side to close or open + * the transport channel */ + gboolean open_acp; /* If we are in ACT role for Open */ + gboolean close_int; /* If we are in INT role for Close */ + gboolean abort_int; /* If we are in INT role for Abort */ + guint idle_timer; + gboolean delay_reporting; + uint16_t delay; /* AVDTP 1.3 Delay Reporting feature */ +}; + +/* Structure describing an AVDTP connection between two devices */ + +struct avdtp { + int ref; + int free_lock; + + uint16_t version; + + struct avdtp_server *server; + bdaddr_t dst; + + avdtp_session_state_t state; + + /* True if the session should be automatically disconnected */ + gboolean auto_dc; + + /* True if the entire device is being disconnected */ + gboolean device_disconnect; + + GIOChannel *io; + guint io_id; + + GSList *seps; /* Elements of type struct avdtp_remote_sep * */ + + GSList *streams; /* Elements of type struct avdtp_stream * */ + + GSList *req_queue; /* Elements of type struct pending_req * */ + GSList *prio_queue; /* Same as req_queue but is processed before it */ + + struct avdtp_stream *pending_open; + + uint16_t imtu; + uint16_t omtu; + + struct in_buf in; + + char *buf; + + avdtp_discover_cb_t discov_cb; + void *user_data; + + struct pending_req *req; + + guint dc_timer; + + /* Attempt stream setup instead of disconnecting */ + gboolean stream_setup; + + DBusPendingCall *pending_auth; +}; + +static GSList *servers = NULL; + +static GSList *avdtp_callbacks = NULL; + +static gboolean auto_connect = TRUE; + +static int send_request(struct avdtp *session, gboolean priority, + struct avdtp_stream *stream, uint8_t signal_id, + void *buffer, size_t size); +static gboolean avdtp_parse_resp(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size); +static gboolean avdtp_parse_rej(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size); +static int process_queue(struct avdtp *session); +static void connection_lost(struct avdtp *session, int err); +static void avdtp_sep_set_state(struct avdtp *session, + struct avdtp_local_sep *sep, + avdtp_state_t state); +static void auth_cb(DBusError *derr, void *user_data); + +static struct avdtp_server *find_server(GSList *list, const bdaddr_t *src) +{ + GSList *l; + + for (l = list; l; l = l->next) { + struct avdtp_server *server = l->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +static const char *avdtp_statestr(avdtp_state_t state) +{ + switch (state) { + case AVDTP_STATE_IDLE: + return "IDLE"; + case AVDTP_STATE_CONFIGURED: + return "CONFIGURED"; + case AVDTP_STATE_OPEN: + return "OPEN"; + case AVDTP_STATE_STREAMING: + return "STREAMING"; + case AVDTP_STATE_CLOSING: + return "CLOSING"; + case AVDTP_STATE_ABORTING: + return "ABORTING"; + default: + return "<unknown state>"; + } +} + +static gboolean try_send(int sk, void *data, size_t len) +{ + int err; + + do { + err = send(sk, data, len, 0); + } while (err < 0 && errno == EINTR); + + if (err < 0) { + error("send: %s (%d)", strerror(errno), errno); + return FALSE; + } else if ((size_t) err != len) { + error("try_send: complete buffer not sent (%d/%zu bytes)", + err, len); + return FALSE; + } + + return TRUE; +} + +static gboolean avdtp_send(struct avdtp *session, uint8_t transaction, + uint8_t message_type, uint8_t signal_id, + void *data, size_t len) +{ + unsigned int cont_fragments, sent; + struct avdtp_start_header start; + struct avdtp_continue_header cont; + int sock; + + if (session->io == NULL) { + error("avdtp_send: session is closed"); + return FALSE; + } + + sock = g_io_channel_unix_get_fd(session->io); + + /* Single packet - no fragmentation */ + if (sizeof(struct avdtp_single_header) + len <= session->omtu) { + struct avdtp_single_header single; + + memset(&single, 0, sizeof(single)); + + single.transaction = transaction; + single.packet_type = AVDTP_PKT_TYPE_SINGLE; + single.message_type = message_type; + single.signal_id = signal_id; + + memcpy(session->buf, &single, sizeof(single)); + memcpy(session->buf + sizeof(single), data, len); + + return try_send(sock, session->buf, sizeof(single) + len); + } + + /* Check if there is enough space to start packet */ + if (session->omtu < sizeof(start)) { + error("No enough space to fragment packet"); + return FALSE; + } + + /* Count the number of needed fragments */ + cont_fragments = (len - (session->omtu - sizeof(start))) / + (session->omtu - sizeof(cont)) + 1; + + DBG("%zu bytes split into %d fragments", len, cont_fragments + 1); + + /* Send the start packet */ + memset(&start, 0, sizeof(start)); + start.transaction = transaction; + start.packet_type = AVDTP_PKT_TYPE_START; + start.message_type = message_type; + start.no_of_packets = cont_fragments + 1; + start.signal_id = signal_id; + + memcpy(session->buf, &start, sizeof(start)); + memcpy(session->buf + sizeof(start), data, + session->omtu - sizeof(start)); + + if (!try_send(sock, session->buf, session->omtu)) + return FALSE; + + DBG("first packet with %zu bytes sent", session->omtu - sizeof(start)); + + sent = session->omtu - sizeof(start); + + /* Send the continue fragments and the end packet */ + while (sent < len) { + int left, to_copy; + + left = len - sent; + if (left + sizeof(cont) > session->omtu) { + cont.packet_type = AVDTP_PKT_TYPE_CONTINUE; + to_copy = session->omtu - sizeof(cont); + DBG("sending continue with %d bytes", to_copy); + } else { + cont.packet_type = AVDTP_PKT_TYPE_END; + to_copy = left; + DBG("sending end with %d bytes", to_copy); + } + + cont.transaction = transaction; + cont.message_type = message_type; + + memcpy(session->buf, &cont, sizeof(cont)); + memcpy(session->buf + sizeof(cont), data + sent, to_copy); + + if (!try_send(sock, session->buf, to_copy + sizeof(cont))) + return FALSE; + + sent += to_copy; + } + + return TRUE; +} + +static void pending_req_free(struct pending_req *req) +{ + if (req->timeout) + g_source_remove(req->timeout); + g_free(req->data); + g_free(req); +} + +static void close_stream(struct avdtp_stream *stream) +{ + int sock; + + if (stream->io == NULL) + return; + + sock = g_io_channel_unix_get_fd(stream->io); + + shutdown(sock, SHUT_RDWR); + + g_io_channel_shutdown(stream->io, FALSE, NULL); + + g_io_channel_unref(stream->io); + stream->io = NULL; +} + +static gboolean stream_close_timeout(gpointer user_data) +{ + struct avdtp_stream *stream = user_data; + + DBG("Timed out waiting for peer to close the transport channel"); + + stream->timer = 0; + + close_stream(stream); + + return FALSE; +} + +static gboolean stream_open_timeout(gpointer user_data) +{ + struct avdtp_stream *stream = user_data; + + DBG("Timed out waiting for peer to open the transport channel"); + + stream->timer = 0; + + stream->session->pending_open = NULL; + + avdtp_abort(stream->session, stream); + + return FALSE; +} + +static gboolean disconnect_timeout(gpointer user_data) +{ + struct avdtp *session = user_data; + struct audio_device *dev; + gboolean stream_setup; + + session->dc_timer = 0; + stream_setup = session->stream_setup; + session->stream_setup = FALSE; + + dev = manager_get_device(&session->server->src, &session->dst, FALSE); + + if (dev && dev->sink && stream_setup) + sink_setup_stream(dev->sink, session); + else if (dev && dev->source && stream_setup) + source_setup_stream(dev->source, session); + else + connection_lost(session, ETIMEDOUT); + + return FALSE; +} + +static void remove_disconnect_timer(struct avdtp *session) +{ + g_source_remove(session->dc_timer); + session->dc_timer = 0; + session->stream_setup = FALSE; +} + +static void set_disconnect_timer(struct avdtp *session) +{ + if (session->dc_timer) + remove_disconnect_timer(session); + + if (session->device_disconnect) { + g_idle_add(disconnect_timeout, session); + return; + } + + session->dc_timer = g_timeout_add_seconds(DISCONNECT_TIMEOUT, + disconnect_timeout, + session); +} + +void avdtp_error_init(struct avdtp_error *err, uint8_t category, int id) +{ + err->category = category; + + if (category == AVDTP_ERRNO) + err->err.posix_errno = id; + else + err->err.error_code = id; +} + +uint8_t avdtp_error_category(struct avdtp_error *err) +{ + return err->category; +} + +int avdtp_error_error_code(struct avdtp_error *err) +{ + assert(err->category != AVDTP_ERRNO); + return err->err.error_code; +} + +int avdtp_error_posix_errno(struct avdtp_error *err) +{ + assert(err->category == AVDTP_ERRNO); + return err->err.posix_errno; +} + +static struct avdtp_stream *find_stream_by_rseid(struct avdtp *session, + uint8_t rseid) +{ + GSList *l; + + for (l = session->streams; l != NULL; l = g_slist_next(l)) { + struct avdtp_stream *stream = l->data; + + if (stream->rseid == rseid) + return stream; + } + + return NULL; +} + +static struct avdtp_remote_sep *find_remote_sep(GSList *seps, uint8_t seid) +{ + GSList *l; + + for (l = seps; l != NULL; l = g_slist_next(l)) { + struct avdtp_remote_sep *sep = l->data; + + if (sep->seid == seid) + return sep; + } + + return NULL; +} + +static void avdtp_set_state(struct avdtp *session, + avdtp_session_state_t new_state) +{ + GSList *l; + struct audio_device *dev; + bdaddr_t src, dst; + avdtp_session_state_t old_state = session->state; + + session->state = new_state; + + avdtp_get_peers(session, &src, &dst); + dev = manager_get_device(&src, &dst, FALSE); + if (dev == NULL) { + error("avdtp_set_state(): no matching audio device"); + return; + } + + for (l = avdtp_callbacks; l != NULL; l = l->next) { + struct avdtp_state_callback *cb = l->data; + cb->cb(dev, session, old_state, new_state, cb->user_data); + } +} + +static void stream_free(struct avdtp_stream *stream) +{ + struct avdtp_remote_sep *rsep; + + stream->lsep->info.inuse = 0; + stream->lsep->stream = NULL; + + rsep = find_remote_sep(stream->session->seps, stream->rseid); + if (rsep) + rsep->stream = NULL; + + if (stream->timer) + g_source_remove(stream->timer); + + if (stream->io) + close_stream(stream); + + if (stream->io_id) + g_source_remove(stream->io_id); + + g_slist_foreach(stream->callbacks, (GFunc) g_free, NULL); + g_slist_free(stream->callbacks); + + g_slist_foreach(stream->caps, (GFunc) g_free, NULL); + g_slist_free(stream->caps); + + g_free(stream); +} + +static gboolean stream_timeout(gpointer user_data) +{ + struct avdtp_stream *stream = user_data; + struct avdtp *session = stream->session; + + if (avdtp_close(session, stream, FALSE) < 0) + error("stream_timeout: closing AVDTP stream failed"); + + stream->idle_timer = 0; + + return FALSE; +} + +static gboolean transport_cb(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct avdtp_stream *stream = data; + struct avdtp_local_sep *sep = stream->lsep; + + if (stream->close_int && sep->cfm && sep->cfm->close) + sep->cfm->close(stream->session, sep, stream, NULL, + sep->user_data); + + if (!(cond & G_IO_NVAL)) + close_stream(stream); + + stream->io_id = 0; + + if (!stream->abort_int) + avdtp_sep_set_state(stream->session, sep, AVDTP_STATE_IDLE); + + return FALSE; +} + +static int get_send_buffer_size(int sk) +{ + int size; + socklen_t optlen = sizeof(size); + + if (getsockopt(sk, SOL_SOCKET, SO_SNDBUF, &size, &optlen) < 0) { + int err = -errno; + error("getsockopt(SO_SNDBUF) failed: %s (%d)", strerror(-err), + -err); + return err; + } + + /* + * Doubled value is returned by getsockopt since kernel uses that + * space for its own purposes (see man 7 socket, bookkeeping overhead + * for SO_SNDBUF). + */ + return size / 2; +} + +static int set_send_buffer_size(int sk, int size) +{ + socklen_t optlen = sizeof(size); + + if (setsockopt(sk, SOL_SOCKET, SO_SNDBUF, &size, optlen) < 0) { + int err = -errno; + error("setsockopt(SO_SNDBUF) failed: %s (%d)", strerror(-err), + -err); + return err; + } + + return 0; +} + +static void handle_transport_connect(struct avdtp *session, GIOChannel *io, + uint16_t imtu, uint16_t omtu) +{ + struct avdtp_stream *stream = session->pending_open; + struct avdtp_local_sep *sep = stream->lsep; + + session->pending_open = NULL; + + if (stream->timer) { + g_source_remove(stream->timer); + stream->timer = 0; + } + + if (io == NULL) { + if (!stream->open_acp && sep->cfm && sep->cfm->open) { + struct avdtp_error err; + avdtp_error_init(&err, AVDTP_ERRNO, EIO); + sep->cfm->open(session, sep, NULL, &err, + sep->user_data); + } + return; + } + + if (stream->io == NULL) + stream->io = g_io_channel_ref(io); + + stream->omtu = omtu; + stream->imtu = imtu; + + /* only if local SEP is of type SRC */ + if (sep->info.type == AVDTP_SEP_TYPE_SOURCE) { + int sk, buf_size, min_buf_size; + + sk = g_io_channel_unix_get_fd(stream->io); + buf_size = get_send_buffer_size(sk); + if (buf_size < 0) + goto proceed; + + DBG("sk %d, omtu %d, send buffer size %d", sk, omtu, buf_size); + min_buf_size = omtu * 2; + if (buf_size < min_buf_size) { + DBG("send buffer size to be increassed to %d", + min_buf_size); + set_send_buffer_size(sk, min_buf_size); + } + } + +proceed: + if (!stream->open_acp && sep->cfm && sep->cfm->open) + sep->cfm->open(session, sep, stream, NULL, sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_OPEN); + + stream->io_id = g_io_add_watch(io, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) transport_cb, stream); +} + +static int pending_req_cmp(gconstpointer a, gconstpointer b) +{ + const struct pending_req *req = a; + const struct avdtp_stream *stream = b; + + if (req->stream == stream) + return 0; + + return -1; +} + +static void cleanup_queue(struct avdtp *session, struct avdtp_stream *stream) +{ + GSList *l; + struct pending_req *req; + + while ((l = g_slist_find_custom(session->prio_queue, stream, + pending_req_cmp))) { + req = l->data; + pending_req_free(req); + session->prio_queue = g_slist_remove(session->prio_queue, req); + } + + while ((l = g_slist_find_custom(session->req_queue, stream, + pending_req_cmp))) { + req = l->data; + pending_req_free(req); + session->req_queue = g_slist_remove(session->req_queue, req); + } +} + +static void handle_unanswered_req(struct avdtp *session, + struct avdtp_stream *stream) +{ + struct pending_req *req; + struct avdtp_local_sep *lsep; + struct avdtp_error err; + + if (session->req->signal_id == AVDTP_ABORT) { + /* Avoid freeing the Abort request here */ + DBG("handle_unanswered_req: Abort req, returning"); + session->req->stream = NULL; + return; + } + + req = session->req; + session->req = NULL; + + avdtp_error_init(&err, AVDTP_ERRNO, EIO); + + lsep = stream->lsep; + + switch (req->signal_id) { + case AVDTP_RECONFIGURE: + error("No reply to Reconfigure request"); + if (lsep && lsep->cfm && lsep->cfm->reconfigure) + lsep->cfm->reconfigure(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_OPEN: + error("No reply to Open request"); + if (lsep && lsep->cfm && lsep->cfm->open) + lsep->cfm->open(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_START: + error("No reply to Start request"); + if (lsep && lsep->cfm && lsep->cfm->start) + lsep->cfm->start(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_SUSPEND: + error("No reply to Suspend request"); + if (lsep && lsep->cfm && lsep->cfm->suspend) + lsep->cfm->suspend(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_CLOSE: + error("No reply to Close request"); + if (lsep && lsep->cfm && lsep->cfm->close) + lsep->cfm->close(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_SET_CONFIGURATION: + error("No reply to SetConfiguration request"); + if (lsep && lsep->cfm && lsep->cfm->set_configuration) + lsep->cfm->set_configuration(session, lsep, stream, + &err, lsep->user_data); + } + + pending_req_free(req); +} + +static void avdtp_sep_set_state(struct avdtp *session, + struct avdtp_local_sep *sep, + avdtp_state_t state) +{ + struct avdtp_stream *stream = sep->stream; + avdtp_state_t old_state; + struct avdtp_error err, *err_ptr = NULL; + GSList *l; + + if (!stream) { + error("Error changing sep state: stream not available"); + return; + } + + if (sep->state == state) { + avdtp_error_init(&err, AVDTP_ERRNO, EIO); + DBG("stream state change failed: %s", avdtp_strerror(&err)); + err_ptr = &err; + } else { + err_ptr = NULL; + DBG("stream state changed: %s -> %s", + avdtp_statestr(sep->state), + avdtp_statestr(state)); + } + + old_state = sep->state; + sep->state = state; + + for (l = stream->callbacks; l != NULL; l = g_slist_next(l)) { + struct stream_callback *cb = l->data; + cb->cb(stream, old_state, state, err_ptr, cb->user_data); + } + + switch (state) { + case AVDTP_STATE_CONFIGURED: + if (sep->info.type == AVDTP_SEP_TYPE_SINK) + avdtp_delay_report(session, stream, stream->delay); + break; + case AVDTP_STATE_OPEN: + if (old_state > AVDTP_STATE_OPEN && session->auto_dc) + stream->idle_timer = g_timeout_add_seconds(STREAM_TIMEOUT, + stream_timeout, + stream); + break; + case AVDTP_STATE_STREAMING: + case AVDTP_STATE_CLOSING: + case AVDTP_STATE_ABORTING: + if (stream->idle_timer) { + g_source_remove(stream->idle_timer); + stream->idle_timer = 0; + } + break; + case AVDTP_STATE_IDLE: + if (stream->idle_timer) { + g_source_remove(stream->idle_timer); + stream->idle_timer = 0; + } + session->streams = g_slist_remove(session->streams, stream); + if (session->pending_open == stream) + handle_transport_connect(session, NULL, 0, 0); + if (session->req && session->req->stream == stream) + handle_unanswered_req(session, stream); + /* Remove pending commands for this stream from the queue */ + cleanup_queue(session, stream); + stream_free(stream); + break; + default: + break; + } +} + +static void finalize_discovery(struct avdtp *session, int err) +{ + struct avdtp_error avdtp_err; + + avdtp_error_init(&avdtp_err, AVDTP_ERRNO, err); + + if (!session->discov_cb) + return; + + session->discov_cb(session, session->seps, + err ? &avdtp_err : NULL, + session->user_data); + + session->discov_cb = NULL; + session->user_data = NULL; +} + +static void release_stream(struct avdtp_stream *stream, struct avdtp *session) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->abort && + (sep->state != AVDTP_STATE_ABORTING || + stream->abort_int)) + sep->cfm->abort(session, sep, stream, NULL, sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_IDLE); +} + +static void connection_lost(struct avdtp *session, int err) +{ + char address[18]; + struct audio_device *dev; + + ba2str(&session->dst, address); + DBG("Disconnected from %s", address); + + dev = manager_get_device(&session->server->src, &session->dst, FALSE); + + if (dev != NULL && session->state == AVDTP_SESSION_STATE_CONNECTING && + err != EACCES) + audio_device_cancel_authorization(dev, auth_cb, session); + + session->free_lock = 1; + + finalize_discovery(session, err); + + g_slist_foreach(session->streams, (GFunc) release_stream, session); + session->streams = NULL; + + session->free_lock = 0; + + if (session->io) { + g_io_channel_shutdown(session->io, FALSE, NULL); + g_io_channel_unref(session->io); + session->io = NULL; + } + + avdtp_set_state(session, AVDTP_SESSION_STATE_DISCONNECTED); + + if (session->io_id) { + g_source_remove(session->io_id); + session->io_id = 0; + } + + if (session->dc_timer) + remove_disconnect_timer(session); + + session->auto_dc = TRUE; + + if (session->ref != 1) + error("connection_lost: ref count not 1 after all callbacks"); + else + avdtp_unref(session); +} + +void avdtp_unref(struct avdtp *session) +{ + struct avdtp_server *server; + + if (!session) + return; + + session->ref--; + + DBG("%p: ref=%d", session, session->ref); + + if (session->ref == 1) { + if (session->state == AVDTP_SESSION_STATE_CONNECTING && + session->io) { + struct audio_device *dev; + dev = manager_get_device(&session->server->src, + &session->dst, FALSE); + audio_device_cancel_authorization(dev, auth_cb, + session); + g_io_channel_shutdown(session->io, TRUE, NULL); + g_io_channel_unref(session->io); + session->io = NULL; + avdtp_set_state(session, + AVDTP_SESSION_STATE_DISCONNECTED); + } + + if (session->io) + set_disconnect_timer(session); + else if (!session->free_lock) /* Drop the local ref if we + aren't connected */ + session->ref--; + } + + if (session->ref > 0) + return; + + server = session->server; + + DBG("%p: freeing session and removing from list", session); + + if (session->dc_timer) + remove_disconnect_timer(session); + + server->sessions = g_slist_remove(server->sessions, session); + + if (session->req) + pending_req_free(session->req); + + g_slist_foreach(session->seps, (GFunc) g_free, NULL); + g_slist_free(session->seps); + + g_free(session->buf); + + g_free(session); +} + +struct avdtp *avdtp_ref(struct avdtp *session) +{ + session->ref++; + DBG("%p: ref=%d", session, session->ref); + if (session->dc_timer) + remove_disconnect_timer(session); + return session; +} + +static struct avdtp_local_sep *find_local_sep_by_seid(struct avdtp_server *server, + uint8_t seid) +{ + GSList *l; + + for (l = server->seps; l != NULL; l = g_slist_next(l)) { + struct avdtp_local_sep *sep = l->data; + + if (sep->info.seid == seid) + return sep; + } + + return NULL; +} + +struct avdtp_remote_sep *avdtp_find_remote_sep(struct avdtp *session, + struct avdtp_local_sep *lsep) +{ + GSList *l; + + if (lsep->info.inuse) + return NULL; + + for (l = session->seps; l != NULL; l = g_slist_next(l)) { + struct avdtp_remote_sep *sep = l->data; + struct avdtp_service_capability *cap; + struct avdtp_media_codec_capability *codec_data; + + /* Type must be different: source <-> sink */ + if (sep->type == lsep->info.type) + continue; + + if (sep->media_type != lsep->info.media_type) + continue; + + if (!sep->codec) + continue; + + cap = sep->codec; + codec_data = (void *) cap->data; + + if (codec_data->media_codec_type != lsep->codec) + continue; + + if (sep->stream == NULL) + return sep; + } + + return NULL; +} + +static GSList *caps_to_list(uint8_t *data, int size, + struct avdtp_service_capability **codec, + gboolean *delay_reporting) +{ + GSList *caps; + int processed; + + if (delay_reporting) + *delay_reporting = FALSE; + + for (processed = 0, caps = NULL; processed + 2 <= size;) { + struct avdtp_service_capability *cap; + uint8_t length, category; + + category = data[0]; + length = data[1]; + + if (processed + 2 + length > size) { + error("Invalid capability data in getcap resp"); + break; + } + + cap = g_malloc(sizeof(struct avdtp_service_capability) + + length); + memcpy(cap, data, 2 + length); + + processed += 2 + length; + data += 2 + length; + + caps = g_slist_append(caps, cap); + + if (category == AVDTP_MEDIA_CODEC && + length >= + sizeof(struct avdtp_media_codec_capability)) + *codec = cap; + else if (category == AVDTP_DELAY_REPORTING && delay_reporting) + *delay_reporting = TRUE; + } + + return caps; +} + +static gboolean avdtp_unknown_cmd(struct avdtp *session, uint8_t transaction, + uint8_t signal_id) +{ + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_GEN_REJECT, + signal_id, NULL, 0); +} + +static gboolean avdtp_discover_cmd(struct avdtp *session, uint8_t transaction, + void *buf, int size) +{ + GSList *l; + unsigned int rsp_size, sep_count, i; + struct seid_info *seps; + gboolean ret; + + sep_count = g_slist_length(session->server->seps); + + if (sep_count == 0) { + uint8_t err = AVDTP_NOT_SUPPORTED_COMMAND; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_DISCOVER, &err, sizeof(err)); + } + + rsp_size = sep_count * sizeof(struct seid_info); + + seps = g_new0(struct seid_info, sep_count); + + for (l = session->server->seps, i = 0; l != NULL; l = l->next, i++) { + struct avdtp_local_sep *sep = l->data; + + memcpy(&seps[i], &sep->info, sizeof(struct seid_info)); + } + + ret = avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_DISCOVER, seps, rsp_size); + g_free(seps); + + return ret; +} + +static gboolean avdtp_getcap_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size, + gboolean get_all) +{ + GSList *l, *caps; + struct avdtp_local_sep *sep = NULL; + unsigned int rsp_size; + uint8_t err, buf[1024], *ptr = buf; + uint8_t cmd; + + cmd = get_all ? AVDTP_GET_ALL_CAPABILITIES : AVDTP_GET_CAPABILITIES; + + if (size < sizeof(struct seid_req)) { + err = AVDTP_BAD_LENGTH; + goto failed; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (get_all && session->server->version < 0x0103) + return avdtp_unknown_cmd(session, transaction, cmd); + + if (!sep->ind->get_capability(session, sep, get_all, &caps, + &err, sep->user_data)) + goto failed; + + for (l = caps, rsp_size = 0; l != NULL; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (rsp_size + cap->length + 2 > sizeof(buf)) + break; + + memcpy(ptr, cap, cap->length + 2); + rsp_size += cap->length + 2; + ptr += cap->length + 2; + + g_free(cap); + } + + g_slist_free(caps); + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, cmd, + buf, rsp_size); + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, cmd, + &err, sizeof(err)); +} + +static void setconf_cb(struct avdtp *session, struct avdtp_stream *stream, + struct avdtp_error *err) +{ + struct conf_rej rej; + struct avdtp_local_sep *sep; + + if (err != NULL) { + rej.error = AVDTP_UNSUPPORTED_CONFIGURATION; + rej.category = err->err.error_code; + avdtp_send(session, session->in.transaction, + AVDTP_MSG_TYPE_REJECT, AVDTP_SET_CONFIGURATION, + &rej, sizeof(rej)); + return; + } + + if (!avdtp_send(session, session->in.transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_SET_CONFIGURATION, NULL, 0)) { + stream_free(stream); + return; + } + + sep = stream->lsep; + sep->stream = stream; + sep->info.inuse = 1; + session->streams = g_slist_append(session->streams, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CONFIGURED); +} + +static gboolean avdtp_setconf_cmd(struct avdtp *session, uint8_t transaction, + struct setconf_req *req, unsigned int size) +{ + struct conf_rej rej; + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err, category = 0x00; + struct audio_device *dev; + bdaddr_t src, dst; + GSList *l; + + if (size < sizeof(struct setconf_req)) { + error("Too short getcap request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->stream) { + err = AVDTP_SEP_IN_USE; + goto failed; + } + + avdtp_get_peers(session, &src, &dst); + dev = manager_get_device(&src, &dst, FALSE); + if (!dev) { + error("Unable to get a audio device object"); + err = AVDTP_BAD_STATE; + goto failed; + } + + switch (sep->info.type) { + case AVDTP_SEP_TYPE_SOURCE: + if (!dev->sink) { + btd_device_add_uuid(dev->btd_dev, A2DP_SINK_UUID); + if (!dev->sink) { + error("Unable to get a audio sink object"); + err = AVDTP_BAD_STATE; + goto failed; + } + } + break; + case AVDTP_SEP_TYPE_SINK: + if (!dev->source) { + btd_device_add_uuid(dev->btd_dev, A2DP_SOURCE_UUID); + if (!dev->sink) { + error("Unable to get a audio source object"); + err = AVDTP_BAD_STATE; + goto failed; + } + } + break; + } + + stream = g_new0(struct avdtp_stream, 1); + stream->session = session; + stream->lsep = sep; + stream->rseid = req->int_seid; + stream->caps = caps_to_list(req->caps, + size - sizeof(struct setconf_req), + &stream->codec, + &stream->delay_reporting); + + /* Verify that the Media Transport capability's length = 0. Reject otherwise */ + for (l = stream->caps; l != NULL; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (cap->category == AVDTP_MEDIA_TRANSPORT && cap->length != 0) { + err = AVDTP_BAD_MEDIA_TRANSPORT_FORMAT; + goto failed_stream; + } + } + + if (stream->delay_reporting && session->version < 0x0103) + session->version = 0x0103; + + if (sep->ind && sep->ind->set_configuration) { + if (!sep->ind->set_configuration(session, sep, stream, + stream->caps, + setconf_cb, + sep->user_data)) { + err = AVDTP_UNSUPPORTED_CONFIGURATION; + category = 0x00; + goto failed_stream; + } + } else { + if (!avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_SET_CONFIGURATION, NULL, 0)) { + stream_free(stream); + return FALSE; + } + + sep->stream = stream; + sep->info.inuse = 1; + session->streams = g_slist_append(session->streams, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CONFIGURED); + } + + return TRUE; + +failed_stream: + stream_free(stream); +failed: + rej.error = err; + rej.category = category; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_SET_CONFIGURATION, &rej, sizeof(rej)); +} + +static gboolean avdtp_getconf_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, int size) +{ + GSList *l; + struct avdtp_local_sep *sep = NULL; + int rsp_size; + uint8_t err; + uint8_t buf[1024]; + uint8_t *ptr = buf; + + if (size < (int) sizeof(struct seid_req)) { + error("Too short getconf request"); + return FALSE; + } + + memset(buf, 0, sizeof(buf)); + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + if (!sep->stream || !sep->stream->caps) { + err = AVDTP_UNSUPPORTED_CONFIGURATION; + goto failed; + } + + for (l = sep->stream->caps, rsp_size = 0; l != NULL; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (rsp_size + cap->length + 2 > (int) sizeof(buf)) + break; + + memcpy(ptr, cap, cap->length + 2); + rsp_size += cap->length + 2; + ptr += cap->length + 2; + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_GET_CONFIGURATION, buf, rsp_size); + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_GET_CONFIGURATION, &err, sizeof(err)); +} + +static gboolean avdtp_reconf_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, int size) +{ + return avdtp_unknown_cmd(session, transaction, AVDTP_RECONFIGURE); +} + +static gboolean avdtp_open_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err; + + if (size < sizeof(struct seid_req)) { + error("Too short abort request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->state != AVDTP_STATE_CONFIGURED) { + err = AVDTP_BAD_STATE; + goto failed; + } + + stream = sep->stream; + + if (sep->ind && sep->ind->open) { + if (!sep->ind->open(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + if (!avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_OPEN, NULL, 0)) + return FALSE; + + stream->open_acp = TRUE; + session->pending_open = stream; + stream->timer = g_timeout_add_seconds(REQ_TIMEOUT, + stream_open_timeout, + stream); + + return TRUE; + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_OPEN, &err, sizeof(err)); +} + +static gboolean avdtp_start_cmd(struct avdtp *session, uint8_t transaction, + struct start_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + struct stream_rej rej; + struct seid *seid; + uint8_t err, failed_seid; + int seid_count, i; + + if (size < sizeof(struct start_req)) { + error("Too short start request"); + return FALSE; + } + + seid_count = 1 + size - sizeof(struct start_req); + + seid = &req->first_seid; + + for (i = 0; i < seid_count; i++, seid++) { + failed_seid = seid->seid; + + sep = find_local_sep_by_seid(session->server, + req->first_seid.seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + stream = sep->stream; + + if (sep->state != AVDTP_STATE_OPEN) { + err = AVDTP_BAD_STATE; + goto failed; + } + + if (sep->ind && sep->ind->start) { + if (!sep->ind->start(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_sep_set_state(session, sep, AVDTP_STATE_STREAMING); + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_START, NULL, 0); + +failed: + memset(&rej, 0, sizeof(rej)); + rej.acp_seid = failed_seid; + rej.error = err; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_START, &rej, sizeof(rej)); +} + +static gboolean avdtp_close_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err; + + if (size < sizeof(struct seid_req)) { + error("Too short close request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->state != AVDTP_STATE_OPEN && + sep->state != AVDTP_STATE_STREAMING) { + err = AVDTP_BAD_STATE; + goto failed; + } + + stream = sep->stream; + + if (sep->ind && sep->ind->close) { + if (!sep->ind->close(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CLOSING); + + if (!avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_CLOSE, NULL, 0)) + return FALSE; + + stream->timer = g_timeout_add_seconds(REQ_TIMEOUT, + stream_close_timeout, + stream); + + return TRUE; + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_CLOSE, &err, sizeof(err)); +} + +static gboolean avdtp_suspend_cmd(struct avdtp *session, uint8_t transaction, + struct suspend_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + struct stream_rej rej; + struct seid *seid; + uint8_t err, failed_seid; + int seid_count, i; + + if (size < sizeof(struct suspend_req)) { + error("Too short suspend request"); + return FALSE; + } + + seid_count = 1 + size - sizeof(struct suspend_req); + + seid = &req->first_seid; + + for (i = 0; i < seid_count; i++, seid++) { + failed_seid = seid->seid; + + sep = find_local_sep_by_seid(session->server, + req->first_seid.seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + stream = sep->stream; + + if (sep->state != AVDTP_STATE_STREAMING) { + err = AVDTP_BAD_STATE; + goto failed; + } + + if (sep->ind && sep->ind->suspend) { + if (!sep->ind->suspend(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_sep_set_state(session, sep, AVDTP_STATE_OPEN); + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_SUSPEND, NULL, 0); + +failed: + memset(&rej, 0, sizeof(rej)); + rej.acp_seid = failed_seid; + rej.error = err; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_SUSPEND, &rej, sizeof(rej)); +} + +static gboolean avdtp_abort_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + uint8_t err; + gboolean ret; + + if (size < sizeof(struct seid_req)) { + error("Too short abort request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->ind && sep->ind->abort) { + if (!sep->ind->abort(session, sep, sep->stream, &err, + sep->user_data)) + goto failed; + } + + ret = avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_ABORT, NULL, 0); + if (ret) + avdtp_sep_set_state(session, sep, AVDTP_STATE_ABORTING); + + return ret; + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_ABORT, &err, sizeof(err)); +} + +static gboolean avdtp_secctl_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, int size) +{ + return avdtp_unknown_cmd(session, transaction, AVDTP_SECURITY_CONTROL); +} + +static gboolean avdtp_delayreport_cmd(struct avdtp *session, + uint8_t transaction, + struct delay_req *req, + unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err; + + if (size < sizeof(struct delay_req)) { + error("Too short delay report request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + stream = sep->stream; + + if (sep->state != AVDTP_STATE_CONFIGURED && + sep->state != AVDTP_STATE_STREAMING) { + err = AVDTP_BAD_STATE; + goto failed; + } + + stream->delay = ntohs(req->delay); + + if (sep->ind && sep->ind->delayreport) { + if (!sep->ind->delayreport(session, sep, stream->rseid, + stream->delay, &err, + sep->user_data)) + goto failed; + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_DELAY_REPORT, NULL, 0); + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_DELAY_REPORT, &err, sizeof(err)); +} + +static gboolean avdtp_parse_cmd(struct avdtp *session, uint8_t transaction, + uint8_t signal_id, void *buf, int size) +{ + switch (signal_id) { + case AVDTP_DISCOVER: + DBG("Received DISCOVER_CMD"); + return avdtp_discover_cmd(session, transaction, buf, size); + case AVDTP_GET_CAPABILITIES: + DBG("Received GET_CAPABILITIES_CMD"); + return avdtp_getcap_cmd(session, transaction, buf, size, + FALSE); + case AVDTP_GET_ALL_CAPABILITIES: + DBG("Received GET_ALL_CAPABILITIES_CMD"); + return avdtp_getcap_cmd(session, transaction, buf, size, TRUE); + case AVDTP_SET_CONFIGURATION: + DBG("Received SET_CONFIGURATION_CMD"); + return avdtp_setconf_cmd(session, transaction, buf, size); + case AVDTP_GET_CONFIGURATION: + DBG("Received GET_CONFIGURATION_CMD"); + return avdtp_getconf_cmd(session, transaction, buf, size); + case AVDTP_RECONFIGURE: + DBG("Received RECONFIGURE_CMD"); + return avdtp_reconf_cmd(session, transaction, buf, size); + case AVDTP_OPEN: + DBG("Received OPEN_CMD"); + return avdtp_open_cmd(session, transaction, buf, size); + case AVDTP_START: + DBG("Received START_CMD"); + return avdtp_start_cmd(session, transaction, buf, size); + case AVDTP_CLOSE: + DBG("Received CLOSE_CMD"); + return avdtp_close_cmd(session, transaction, buf, size); + case AVDTP_SUSPEND: + DBG("Received SUSPEND_CMD"); + return avdtp_suspend_cmd(session, transaction, buf, size); + case AVDTP_ABORT: + DBG("Received ABORT_CMD"); + return avdtp_abort_cmd(session, transaction, buf, size); + case AVDTP_SECURITY_CONTROL: + DBG("Received SECURITY_CONTROL_CMD"); + return avdtp_secctl_cmd(session, transaction, buf, size); + case AVDTP_DELAY_REPORT: + DBG("Received DELAY_REPORT_CMD"); + return avdtp_delayreport_cmd(session, transaction, buf, size); + default: + DBG("Received unknown request id %u", signal_id); + return avdtp_unknown_cmd(session, transaction, signal_id); + } +} + +enum avdtp_parse_result { PARSE_ERROR, PARSE_FRAGMENT, PARSE_SUCCESS }; + +static enum avdtp_parse_result avdtp_parse_data(struct avdtp *session, + void *buf, size_t size) +{ + struct avdtp_common_header *header = buf; + struct avdtp_single_header *single = (void *) session->buf; + struct avdtp_start_header *start = (void *) session->buf; + void *payload; + gsize payload_size; + + switch (header->packet_type) { + case AVDTP_PKT_TYPE_SINGLE: + if (size < sizeof(*single)) { + error("Received too small single packet (%zu bytes)", size); + return PARSE_ERROR; + } + if (session->in.active) { + error("SINGLE: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + + payload = session->buf + sizeof(*single); + payload_size = size - sizeof(*single); + + session->in.active = TRUE; + session->in.data_size = 0; + session->in.no_of_packets = 1; + session->in.transaction = header->transaction; + session->in.message_type = header->message_type; + session->in.signal_id = single->signal_id; + + break; + case AVDTP_PKT_TYPE_START: + if (size < sizeof(*start)) { + error("Received too small start packet (%zu bytes)", size); + return PARSE_ERROR; + } + if (session->in.active) { + error("START: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + + session->in.active = TRUE; + session->in.data_size = 0; + session->in.transaction = header->transaction; + session->in.message_type = header->message_type; + session->in.no_of_packets = start->no_of_packets; + session->in.signal_id = start->signal_id; + + payload = session->buf + sizeof(*start); + payload_size = size - sizeof(*start); + + break; + case AVDTP_PKT_TYPE_CONTINUE: + if (size < sizeof(struct avdtp_continue_header)) { + error("Received too small continue packet (%zu bytes)", + size); + return PARSE_ERROR; + } + if (!session->in.active) { + error("CONTINUE: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + if (session->in.transaction != header->transaction) { + error("Continue transaction id doesn't match"); + return PARSE_ERROR; + } + if (session->in.no_of_packets <= 1) { + error("Too few continue packets"); + return PARSE_ERROR; + } + + payload = session->buf + sizeof(struct avdtp_continue_header); + payload_size = size - sizeof(struct avdtp_continue_header); + + break; + case AVDTP_PKT_TYPE_END: + if (size < sizeof(struct avdtp_continue_header)) { + error("Received too small end packet (%zu bytes)", size); + return PARSE_ERROR; + } + if (!session->in.active) { + error("END: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + if (session->in.transaction != header->transaction) { + error("End transaction id doesn't match"); + return PARSE_ERROR; + } + if (session->in.no_of_packets > 1) { + error("Got an end packet too early"); + return PARSE_ERROR; + } + + payload = session->buf + sizeof(struct avdtp_continue_header); + payload_size = size - sizeof(struct avdtp_continue_header); + + break; + default: + error("Invalid AVDTP packet type 0x%02X", header->packet_type); + return PARSE_ERROR; + } + + if (session->in.data_size + payload_size > + sizeof(session->in.buf)) { + error("Not enough incoming buffer space!"); + return PARSE_ERROR; + } + + memcpy(session->in.buf + session->in.data_size, payload, payload_size); + session->in.data_size += payload_size; + + if (session->in.no_of_packets > 1) { + session->in.no_of_packets--; + DBG("Received AVDTP fragment. %d to go", + session->in.no_of_packets); + return PARSE_FRAGMENT; + } + + session->in.active = FALSE; + + return PARSE_SUCCESS; +} + +static gboolean session_cb(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct avdtp *session = data; + struct avdtp_common_header *header; + ssize_t size; + int fd; + + DBG(""); + + if (cond & G_IO_NVAL) + return FALSE; + + header = (void *) session->buf; + + if (cond & (G_IO_HUP | G_IO_ERR)) + goto failed; + + fd = g_io_channel_unix_get_fd(chan); + size = read(fd, session->buf, session->imtu); + if (size < 0) { + error("IO Channel read error"); + goto failed; + } + + if ((size_t) size < sizeof(struct avdtp_common_header)) { + error("Received too small packet (%zu bytes)", size); + goto failed; + } + + switch (avdtp_parse_data(session, session->buf, size)) { + case PARSE_ERROR: + goto failed; + case PARSE_FRAGMENT: + return TRUE; + case PARSE_SUCCESS: + break; + } + + if (session->in.message_type == AVDTP_MSG_TYPE_COMMAND) { + if (!avdtp_parse_cmd(session, session->in.transaction, + session->in.signal_id, + session->in.buf, + session->in.data_size)) { + error("Unable to handle command. Disconnecting"); + goto failed; + } + + if (session->ref == 1 && !session->streams && !session->req) + set_disconnect_timer(session); + + if (session->streams && session->dc_timer) + remove_disconnect_timer(session); + + return TRUE; + } + + if (session->req == NULL) { + error("No pending request, ignoring message"); + return TRUE; + } + + if (header->transaction != session->req->transaction) { + error("Transaction label doesn't match"); + return TRUE; + } + + if (session->in.signal_id != session->req->signal_id) { + error("Reponse signal doesn't match"); + return TRUE; + } + + g_source_remove(session->req->timeout); + session->req->timeout = 0; + + switch (header->message_type) { + case AVDTP_MSG_TYPE_ACCEPT: + if (!avdtp_parse_resp(session, session->req->stream, + session->in.transaction, + session->in.signal_id, + session->in.buf, + session->in.data_size)) { + error("Unable to parse accept response"); + goto failed; + } + break; + case AVDTP_MSG_TYPE_REJECT: + if (!avdtp_parse_rej(session, session->req->stream, + session->in.transaction, + session->in.signal_id, + session->in.buf, + session->in.data_size)) { + error("Unable to parse reject response"); + goto failed; + } + break; + case AVDTP_MSG_TYPE_GEN_REJECT: + error("Received a General Reject message"); + break; + default: + error("Unknown message type 0x%02X", header->message_type); + break; + } + + pending_req_free(session->req); + session->req = NULL; + + process_queue(session); + + return TRUE; + +failed: + connection_lost(session, EIO); + + return FALSE; +} + +static struct avdtp *find_session(GSList *list, const bdaddr_t *dst) +{ + GSList *l; + + for (l = list; l != NULL; l = g_slist_next(l)) { + struct avdtp *s = l->data; + + if (bacmp(dst, &s->dst)) + continue; + + return s; + } + + return NULL; +} + +static uint16_t get_version(struct avdtp *session) +{ + struct btd_adapter *adapter; + struct btd_device *device; + const sdp_record_t *rec; + sdp_list_t *protos; + sdp_data_t *proto_desc; + char addr[18]; + uint16_t ver = 0x0100; + + adapter = manager_find_adapter(&session->server->src); + if (!adapter) + goto done; + + ba2str(&session->dst, addr); + device = adapter_find_device(adapter, addr); + if (!device) + goto done; + + rec = btd_device_get_record(device, A2DP_SINK_UUID); + if (!rec) + rec = btd_device_get_record(device, A2DP_SOURCE_UUID); + + if (!rec) + goto done; + + if (sdp_get_access_protos(rec, &protos) < 0) + goto done; + + proto_desc = sdp_get_proto_desc(protos, AVDTP_UUID); + if (proto_desc && proto_desc->dtd == SDP_UINT16) + ver = proto_desc->val.uint16; + + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + +done: + return ver; +} + +static struct avdtp *avdtp_get_internal(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct avdtp_server *server; + struct avdtp *session; + + assert(src != NULL); + assert(dst != NULL); + + server = find_server(servers, src); + if (server == NULL) + return NULL; + + session = find_session(server->sessions, dst); + if (session) { + if (session->pending_auth) + return NULL; + else + return session; + } + + session = g_new0(struct avdtp, 1); + + session->server = server; + bacpy(&session->dst, dst); + session->ref = 1; + /* We don't use avdtp_set_state() here since this isn't a state change + * but just setting of the initial state */ + session->state = AVDTP_SESSION_STATE_DISCONNECTED; + session->auto_dc = TRUE; + + session->version = get_version(session); + + server->sessions = g_slist_append(server->sessions, session); + + return session; +} + +struct avdtp *avdtp_get(bdaddr_t *src, bdaddr_t *dst) +{ + struct avdtp *session; + + session = avdtp_get_internal(src, dst); + + if (!session) + return NULL; + + return avdtp_ref(session); +} + +static void avdtp_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + struct avdtp *session = user_data; + char address[18]; + GError *gerr = NULL; + + if (err) { + error("%s", err->message); + goto failed; + } + + if (!session->io) + session->io = g_io_channel_ref(chan); + + bt_io_get(chan, BT_IO_L2CAP, &gerr, + BT_IO_OPT_OMTU, &session->omtu, + BT_IO_OPT_IMTU, &session->imtu, + BT_IO_OPT_INVALID); + if (gerr) { + error("%s", gerr->message); + g_error_free(gerr); + goto failed; + } + + ba2str(&session->dst, address); + DBG("AVDTP: connected %s channel to %s", + session->pending_open ? "transport" : "signaling", + address); + + if (session->state == AVDTP_SESSION_STATE_CONNECTING) { + DBG("AVDTP imtu=%u, omtu=%u", session->imtu, session->omtu); + + session->buf = g_malloc0(session->imtu); + avdtp_set_state(session, AVDTP_SESSION_STATE_CONNECTED); + + if (session->io_id) + g_source_remove(session->io_id); + + /* This watch should be low priority since otherwise the + * connect callback might be dispatched before the session + * callback if the kernel wakes us up at the same time for + * them. This could happen if a headset is very quick in + * sending the Start command after connecting the stream + * transport channel. + */ + session->io_id = g_io_add_watch_full(chan, + G_PRIORITY_LOW, + G_IO_IN | G_IO_ERR | G_IO_HUP + | G_IO_NVAL, + (GIOFunc) session_cb, session, + NULL); + + if (session->stream_setup) { + set_disconnect_timer(session); + avdtp_set_auto_disconnect(session, FALSE); + } + } else if (session->pending_open) + handle_transport_connect(session, chan, session->imtu, + session->omtu); + else + goto failed; + + process_queue(session); + + return; + +failed: + if (session->pending_open) { + struct avdtp_stream *stream = session->pending_open; + + handle_transport_connect(session, NULL, 0, 0); + + if (avdtp_abort(session, stream) < 0) + avdtp_sep_set_state(session, stream->lsep, + AVDTP_STATE_IDLE); + } else + connection_lost(session, EIO); +} + +static void auth_cb(DBusError *derr, void *user_data) +{ + struct avdtp *session = user_data; + GError *err = NULL; + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + connection_lost(session, EACCES); + return; + } + + if (!bt_io_accept(session->io, avdtp_connect_cb, session, NULL, + &err)) { + error("bt_io_accept: %s", err->message); + connection_lost(session, EACCES); + g_error_free(err); + return; + } + + /* This is so that avdtp_connect_cb will know to do the right thing + * with respect to the disconnect timer */ + session->stream_setup = TRUE; +} + +static void avdtp_confirm_cb(GIOChannel *chan, gpointer data) +{ + struct avdtp *session; + struct audio_device *dev; + char address[18]; + bdaddr_t src, dst; + int perr; + GError *err = NULL; + + bt_io_get(chan, BT_IO_L2CAP, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_DEST, address, + BT_IO_OPT_INVALID); + if (err) { + error("%s", err->message); + g_error_free(err); + goto drop; + } + + DBG("AVDTP: incoming connect from %s", address); + + session = avdtp_get_internal(&src, &dst); + if (!session) + goto drop; + + /* This state (ie, session is already *connecting*) happens when the + * device initiates a connect (really a config'd L2CAP channel) even + * though there is a connect we initiated in progress. In sink.c & + * source.c, this state is referred to as XCASE connect:connect. + * Abort the device's channel in favor of our own. + */ + if (session->state == AVDTP_SESSION_STATE_CONNECTING) { + DBG("connect already in progress (XCASE connect:connect)"); + goto drop; + } + + if (session->pending_open && session->pending_open->open_acp) { + if (!bt_io_accept(chan, avdtp_connect_cb, session, NULL, NULL)) + goto drop; + return; + } + + if (session->io) { + error("Refusing unexpected connect from %s", address); + goto drop; + } + + dev = manager_get_device(&src, &dst, FALSE); + if (!dev) { + dev = manager_get_device(&src, &dst, TRUE); + if (!dev) { + error("Unable to get audio device object for %s", + address); + goto drop; + } + btd_device_add_uuid(dev->btd_dev, ADVANCED_AUDIO_UUID); + } + + session->io = g_io_channel_ref(chan); + avdtp_set_state(session, AVDTP_SESSION_STATE_CONNECTING); + + session->io_id = g_io_add_watch(chan, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) session_cb, session); + + perr = audio_device_request_authorization(dev, ADVANCED_AUDIO_UUID, + auth_cb, session); + if (perr < 0) { + avdtp_unref(session); + goto drop; + } + + dev->auto_connect = auto_connect; + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static GIOChannel *l2cap_connect(struct avdtp *session) +{ + GError *err = NULL; + GIOChannel *io; + + io = bt_io_connect(BT_IO_L2CAP, avdtp_connect_cb, session, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &session->server->src, + BT_IO_OPT_DEST_BDADDR, &session->dst, + BT_IO_OPT_PSM, AVDTP_PSM, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return NULL; + } + + return io; +} + +static void queue_request(struct avdtp *session, struct pending_req *req, + gboolean priority) +{ + if (priority) + session->prio_queue = g_slist_append(session->prio_queue, req); + else + session->req_queue = g_slist_append(session->req_queue, req); +} + +static uint8_t req_get_seid(struct pending_req *req) +{ + if (req->signal_id == AVDTP_DISCOVER) + return 0; + + return ((struct seid_req *) (req->data))->acp_seid; +} + +static int cancel_request(struct avdtp *session, int err) +{ + struct pending_req *req; + struct seid_req sreq; + struct avdtp_local_sep *lsep; + struct avdtp_stream *stream; + uint8_t seid; + struct avdtp_error averr; + + req = session->req; + session->req = NULL; + + avdtp_error_init(&averr, AVDTP_ERRNO, err); + + seid = req_get_seid(req); + if (seid) + stream = find_stream_by_rseid(session, seid); + else + stream = NULL; + + if (stream) { + stream->abort_int = TRUE; + lsep = stream->lsep; + } else + lsep = NULL; + + switch (req->signal_id) { + case AVDTP_RECONFIGURE: + error("Reconfigure: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->reconfigure) + lsep->cfm->reconfigure(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_OPEN: + error("Open: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->open) + lsep->cfm->open(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_START: + error("Start: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->start) + lsep->cfm->start(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_SUSPEND: + error("Suspend: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->suspend) + lsep->cfm->suspend(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_CLOSE: + error("Close: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->close) { + lsep->cfm->close(session, lsep, stream, &averr, + lsep->user_data); + if (stream) + stream->close_int = FALSE; + } + break; + case AVDTP_SET_CONFIGURATION: + error("SetConfiguration: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->set_configuration) + lsep->cfm->set_configuration(session, lsep, stream, + &averr, lsep->user_data); + goto failed; + case AVDTP_DISCOVER: + error("Discover: %s (%d)", strerror(err), err); + goto failed; + case AVDTP_GET_CAPABILITIES: + error("GetCapabilities: %s (%d)", strerror(err), err); + goto failed; + case AVDTP_ABORT: + error("Abort: %s (%d)", strerror(err), err); + goto failed; + } + + if (!stream) + goto failed; + + memset(&sreq, 0, sizeof(sreq)); + sreq.acp_seid = seid; + + err = send_request(session, TRUE, stream, AVDTP_ABORT, &sreq, + sizeof(sreq)); + if (err < 0) { + error("Unable to send abort request"); + goto failed; + } + + goto done; + +failed: + connection_lost(session, err); +done: + pending_req_free(req); + return err; +} + +static gboolean request_timeout(gpointer user_data) +{ + struct avdtp *session = user_data; + + cancel_request(session, ETIMEDOUT); + + return FALSE; +} + +static int send_req(struct avdtp *session, gboolean priority, + struct pending_req *req) +{ + static int transaction = 0; + int err; + + if (session->state == AVDTP_SESSION_STATE_DISCONNECTED) { + session->io = l2cap_connect(session); + if (!session->io) + goto failed; + avdtp_set_state(session, AVDTP_SESSION_STATE_CONNECTING); + } + + if (session->state < AVDTP_SESSION_STATE_CONNECTED || + session->req != NULL) { + queue_request(session, req, priority); + return 0; + } + + req->transaction = transaction++; + transaction %= 16; + + /* FIXME: Should we retry to send if the buffer + was not totally sent or in case of EINTR? */ + if (!avdtp_send(session, req->transaction, AVDTP_MSG_TYPE_COMMAND, + req->signal_id, req->data, req->data_size)) { + err = -EIO; + goto failed; + } + + session->req = req; + + req->timeout = g_timeout_add_seconds(req->signal_id == AVDTP_ABORT ? + ABORT_TIMEOUT : REQ_TIMEOUT, + request_timeout, + session); + return 0; + +failed: + g_free(req->data); + g_free(req); + return err; +} + +static int send_request(struct avdtp *session, gboolean priority, + struct avdtp_stream *stream, uint8_t signal_id, + void *buffer, size_t size) +{ + struct pending_req *req; + + if (stream && stream->abort_int && signal_id != AVDTP_ABORT) { + DBG("Unable to send requests while aborting"); + return -EINVAL; + } + + req = g_new0(struct pending_req, 1); + req->signal_id = signal_id; + req->data = g_malloc(size); + memcpy(req->data, buffer, size); + req->data_size = size; + req->stream = stream; + + return send_req(session, priority, req); +} + +static gboolean avdtp_discover_resp(struct avdtp *session, + struct discover_resp *resp, int size) +{ + int sep_count, i; + uint8_t getcap_cmd; + + if (session->version >= 0x0103 && session->server->version >= 0x0103) + getcap_cmd = AVDTP_GET_ALL_CAPABILITIES; + else + getcap_cmd = AVDTP_GET_CAPABILITIES; + + sep_count = size / sizeof(struct seid_info); + + for (i = 0; i < sep_count; i++) { + struct avdtp_remote_sep *sep; + struct avdtp_stream *stream; + struct seid_req req; + int ret; + + DBG("seid %d type %d media %d in use %d", + resp->seps[i].seid, resp->seps[i].type, + resp->seps[i].media_type, resp->seps[i].inuse); + + stream = find_stream_by_rseid(session, resp->seps[i].seid); + + sep = find_remote_sep(session->seps, resp->seps[i].seid); + if (!sep) { + if (resp->seps[i].inuse && !stream) + continue; + sep = g_new0(struct avdtp_remote_sep, 1); + session->seps = g_slist_append(session->seps, sep); + } + + sep->stream = stream; + sep->seid = resp->seps[i].seid; + sep->type = resp->seps[i].type; + sep->media_type = resp->seps[i].media_type; + + memset(&req, 0, sizeof(req)); + req.acp_seid = sep->seid; + + ret = send_request(session, TRUE, NULL, getcap_cmd, + &req, sizeof(req)); + if (ret < 0) { + finalize_discovery(session, -ret); + break; + } + } + + return TRUE; +} + +static gboolean avdtp_get_capabilities_resp(struct avdtp *session, + struct getcap_resp *resp, + unsigned int size) +{ + struct avdtp_remote_sep *sep; + uint8_t seid; + + /* Check for minimum required packet size includes: + * 1. getcap resp header + * 2. media transport capability (2 bytes) + * 3. media codec capability type + length (2 bytes) + * 4. the actual media codec elements + * */ + if (size < (sizeof(struct getcap_resp) + 4 + + sizeof(struct avdtp_media_codec_capability))) { + error("Too short getcap resp packet"); + return FALSE; + } + + seid = ((struct seid_req *) session->req->data)->acp_seid; + + sep = find_remote_sep(session->seps, seid); + + DBG("seid %d type %d media %d", sep->seid, + sep->type, sep->media_type); + + if (sep->caps) { + g_slist_foreach(sep->caps, (GFunc) g_free, NULL); + g_slist_free(sep->caps); + sep->caps = NULL; + sep->codec = NULL; + sep->delay_reporting = FALSE; + } + + sep->caps = caps_to_list(resp->caps, size - sizeof(struct getcap_resp), + &sep->codec, &sep->delay_reporting); + + return TRUE; +} + +static gboolean avdtp_set_configuration_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct avdtp_single_header *resp, + int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->set_configuration) + sep->cfm->set_configuration(session, sep, stream, NULL, + sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CONFIGURED); + + return TRUE; +} + +static gboolean avdtp_reconfigure_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct avdtp_single_header *resp, int size) +{ + return TRUE; +} + +static gboolean avdtp_open_resp(struct avdtp *session, struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + stream->io = l2cap_connect(session); + if (!stream->io) { + avdtp_sep_set_state(session, sep, AVDTP_STATE_IDLE); + return FALSE; + } + + session->pending_open = stream; + + return TRUE; +} + +static gboolean avdtp_start_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->start) + sep->cfm->start(session, sep, stream, NULL, sep->user_data); + + /* We might be in STREAMING already if both sides send START_CMD at the + * same time and the one in SNK role doesn't reject it as it should */ + if (sep->state != AVDTP_STATE_STREAMING) + avdtp_sep_set_state(session, sep, AVDTP_STATE_STREAMING); + + return TRUE; +} + +static gboolean avdtp_close_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CLOSING); + + close_stream(stream); + + return TRUE; +} + +static gboolean avdtp_suspend_resp(struct avdtp *session, + struct avdtp_stream *stream, + void *data, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + avdtp_sep_set_state(session, sep, AVDTP_STATE_OPEN); + + if (sep->cfm && sep->cfm->suspend) + sep->cfm->suspend(session, sep, stream, NULL, sep->user_data); + + return TRUE; +} + +static gboolean avdtp_abort_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + avdtp_sep_set_state(session, sep, AVDTP_STATE_ABORTING); + + if (sep->cfm && sep->cfm->abort) + sep->cfm->abort(session, sep, stream, NULL, sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_IDLE); + + return TRUE; +} + +static gboolean avdtp_delay_report_resp(struct avdtp *session, + struct avdtp_stream *stream, + void *data, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->delay_report) + sep->cfm->delay_report(session, sep, stream, NULL, sep->user_data); + + return TRUE; +} + +static gboolean avdtp_parse_resp(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size) +{ + struct pending_req *next; + const char *get_all = ""; + + if (session->prio_queue) + next = session->prio_queue->data; + else if (session->req_queue) + next = session->req_queue->data; + else + next = NULL; + + switch (signal_id) { + case AVDTP_DISCOVER: + DBG("DISCOVER request succeeded"); + return avdtp_discover_resp(session, buf, size); + case AVDTP_GET_ALL_CAPABILITIES: + get_all = "ALL_"; + case AVDTP_GET_CAPABILITIES: + DBG("GET_%sCAPABILITIES request succeeded", get_all); + if (!avdtp_get_capabilities_resp(session, buf, size)) + return FALSE; + if (!(next && (next->signal_id == AVDTP_GET_CAPABILITIES || + next->signal_id == AVDTP_GET_ALL_CAPABILITIES))) + finalize_discovery(session, 0); + return TRUE; + } + + /* The remaining commands require an existing stream so bail out + * here if the stream got unexpectedly disconnected */ + if (!stream) { + DBG("AVDTP: stream was closed while waiting for reply"); + return TRUE; + } + + switch (signal_id) { + case AVDTP_SET_CONFIGURATION: + DBG("SET_CONFIGURATION request succeeded"); + return avdtp_set_configuration_resp(session, stream, + buf, size); + case AVDTP_RECONFIGURE: + DBG("RECONFIGURE request succeeded"); + return avdtp_reconfigure_resp(session, stream, buf, size); + case AVDTP_OPEN: + DBG("OPEN request succeeded"); + return avdtp_open_resp(session, stream, buf, size); + case AVDTP_SUSPEND: + DBG("SUSPEND request succeeded"); + return avdtp_suspend_resp(session, stream, buf, size); + case AVDTP_START: + DBG("START request succeeded"); + return avdtp_start_resp(session, stream, buf, size); + case AVDTP_CLOSE: + DBG("CLOSE request succeeded"); + return avdtp_close_resp(session, stream, buf, size); + case AVDTP_ABORT: + DBG("ABORT request succeeded"); + return avdtp_abort_resp(session, stream, buf, size); + case AVDTP_DELAY_REPORT: + DBG("DELAY_REPORT request succeeded"); + return avdtp_delay_report_resp(session, stream, buf, size); + } + + error("Unknown signal id in accept response: %u", signal_id); + return TRUE; +} + +static gboolean seid_rej_to_err(struct seid_rej *rej, unsigned int size, + struct avdtp_error *err) +{ + if (size < sizeof(struct seid_rej)) { + error("Too small packet for seid_rej"); + return FALSE; + } + + avdtp_error_init(err, 0x00, rej->error); + + return TRUE; +} + +static gboolean conf_rej_to_err(struct conf_rej *rej, unsigned int size, + struct avdtp_error *err) +{ + if (size < sizeof(struct conf_rej)) { + error("Too small packet for conf_rej"); + return FALSE; + } + + avdtp_error_init(err, rej->category, rej->error); + + return TRUE; +} + +static gboolean stream_rej_to_err(struct stream_rej *rej, unsigned int size, + struct avdtp_error *err, + uint8_t *acp_seid) +{ + if (size < sizeof(struct stream_rej)) { + error("Too small packet for stream_rej"); + return FALSE; + } + + avdtp_error_init(err, 0x00, rej->error); + + if (acp_seid) + *acp_seid = rej->acp_seid; + + return TRUE; +} + +static gboolean avdtp_parse_rej(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size) +{ + struct avdtp_error err; + uint8_t acp_seid; + struct avdtp_local_sep *sep = stream ? stream->lsep : NULL; + + switch (signal_id) { + case AVDTP_DISCOVER: + if (!seid_rej_to_err(buf, size, &err)) + return FALSE; + error("DISCOVER request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + return TRUE; + case AVDTP_GET_CAPABILITIES: + case AVDTP_GET_ALL_CAPABILITIES: + if (!seid_rej_to_err(buf, size, &err)) + return FALSE; + error("GET_CAPABILITIES request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + return TRUE; + case AVDTP_OPEN: + if (!seid_rej_to_err(buf, size, &err)) + return FALSE; + error("OPEN request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->open) + sep->cfm->open(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_SET_CONFIGURATION: + if (!conf_rej_to_err(buf, size, &err)) + return FALSE; + error("SET_CONFIGURATION request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->set_configuration) + sep->cfm->set_configuration(session, sep, stream, + &err, sep->user_data); + return TRUE; + case AVDTP_RECONFIGURE: + if (!conf_rej_to_err(buf, size, &err)) + return FALSE; + error("RECONFIGURE request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->reconfigure) + sep->cfm->reconfigure(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_START: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("START request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->start) + sep->cfm->start(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_SUSPEND: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("SUSPEND request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->suspend) + sep->cfm->suspend(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_CLOSE: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("CLOSE request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->close) { + sep->cfm->close(session, sep, stream, &err, + sep->user_data); + stream->close_int = FALSE; + } + return TRUE; + case AVDTP_ABORT: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("ABORT request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->abort) + sep->cfm->abort(session, sep, stream, &err, + sep->user_data); + return FALSE; + case AVDTP_DELAY_REPORT: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("DELAY_REPORT request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->delay_report) + sep->cfm->delay_report(session, sep, stream, &err, + sep->user_data); + return TRUE; + default: + error("Unknown reject response signal id: %u", signal_id); + return TRUE; + } +} + +gboolean avdtp_is_connected(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct avdtp_server *server; + struct avdtp *session; + + server = find_server(servers, src); + if (!server) + return FALSE; + + session = find_session(server->sessions, dst); + if (!session) + return FALSE; + + if (session->state != AVDTP_SESSION_STATE_DISCONNECTED) + return TRUE; + + return FALSE; +} + +struct avdtp_service_capability *avdtp_stream_get_codec( + struct avdtp_stream *stream) +{ + GSList *l; + + for (l = stream->caps; l; l = l->next) { + struct avdtp_service_capability *cap = l->data; + + if (cap->category == AVDTP_MEDIA_CODEC) + return cap; + } + + return NULL; +} + +gboolean avdtp_stream_has_capability(struct avdtp_stream *stream, + struct avdtp_service_capability *cap) +{ + GSList *l; + struct avdtp_service_capability *stream_cap; + + for (l = stream->caps; l; l = g_slist_next(l)) { + stream_cap = l->data; + + if (stream_cap->category != cap->category || + stream_cap->length != cap->length) + continue; + + if (memcmp(stream_cap->data, cap->data, cap->length) == 0) + return TRUE; + } + + return FALSE; +} + +gboolean avdtp_stream_has_capabilities(struct avdtp_stream *stream, + GSList *caps) +{ + GSList *l; + + for (l = caps; l; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (!avdtp_stream_has_capability(stream, cap)) + return FALSE; + } + + return TRUE; +} + +struct avdtp_remote_sep *avdtp_stream_get_remote_sep( + struct avdtp_stream *stream) +{ + return avdtp_get_remote_sep(stream->session, stream->rseid); +} + +gboolean avdtp_stream_get_transport(struct avdtp_stream *stream, int *sock, + uint16_t *imtu, uint16_t *omtu, + GSList **caps) +{ + if (stream->io == NULL) + return FALSE; + + if (sock) + *sock = g_io_channel_unix_get_fd(stream->io); + + if (omtu) + *omtu = stream->omtu; + + if (imtu) + *imtu = stream->imtu; + + if (caps) + *caps = stream->caps; + + return TRUE; +} + +static int process_queue(struct avdtp *session) +{ + GSList **queue, *l; + struct pending_req *req; + + if (session->req) + return 0; + + if (session->prio_queue) + queue = &session->prio_queue; + else + queue = &session->req_queue; + + if (!*queue) + return 0; + + l = *queue; + req = l->data; + + *queue = g_slist_remove(*queue, req); + + return send_req(session, FALSE, req); +} + +struct avdtp_remote_sep *avdtp_get_remote_sep(struct avdtp *session, + uint8_t seid) +{ + GSList *l; + + for (l = session->seps; l; l = l->next) { + struct avdtp_remote_sep *sep = l->data; + + if (sep->seid == seid) + return sep; + } + + return NULL; +} + +uint8_t avdtp_get_seid(struct avdtp_remote_sep *sep) +{ + return sep->seid; +} + +uint8_t avdtp_get_type(struct avdtp_remote_sep *sep) +{ + return sep->type; +} + +struct avdtp_service_capability *avdtp_get_codec(struct avdtp_remote_sep *sep) +{ + return sep->codec; +} + +gboolean avdtp_get_delay_reporting(struct avdtp_remote_sep *sep) +{ + return sep->delay_reporting; +} + +struct avdtp_stream *avdtp_get_stream(struct avdtp_remote_sep *sep) +{ + return sep->stream; +} + +struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category, + void *data, int length) +{ + struct avdtp_service_capability *cap; + + if (category < AVDTP_MEDIA_TRANSPORT || category > AVDTP_DELAY_REPORTING) + return NULL; + + cap = g_malloc(sizeof(struct avdtp_service_capability) + length); + cap->category = category; + cap->length = length; + memcpy(cap->data, data, length); + + return cap; +} + +static gboolean process_discover(gpointer data) +{ + struct avdtp *session = data; + + finalize_discovery(session, 0); + + return FALSE; +} + +int avdtp_discover(struct avdtp *session, avdtp_discover_cb_t cb, + void *user_data) +{ + int err; + + if (session->discov_cb) + return -EBUSY; + + if (session->seps) { + session->discov_cb = cb; + session->user_data = user_data; + g_idle_add(process_discover, session); + return 0; + } + + err = send_request(session, FALSE, NULL, AVDTP_DISCOVER, NULL, 0); + if (err == 0) { + session->discov_cb = cb; + session->user_data = user_data; + } + + return err; +} + +gboolean avdtp_stream_remove_cb(struct avdtp *session, + struct avdtp_stream *stream, + unsigned int id) +{ + GSList *l; + struct stream_callback *cb; + + if (!stream) + return FALSE; + + for (cb = NULL, l = stream->callbacks; l != NULL; l = l->next) { + struct stream_callback *tmp = l->data; + if (tmp && tmp->id == id) { + cb = tmp; + break; + } + } + + if (!cb) + return FALSE; + + stream->callbacks = g_slist_remove(stream->callbacks, cb); + g_free(cb); + + return TRUE; +} + +unsigned int avdtp_stream_add_cb(struct avdtp *session, + struct avdtp_stream *stream, + avdtp_stream_state_cb cb, void *data) +{ + struct stream_callback *stream_cb; + static unsigned int id = 0; + + stream_cb = g_new(struct stream_callback, 1); + stream_cb->cb = cb; + stream_cb->user_data = data; + stream_cb->id = ++id; + + stream->callbacks = g_slist_append(stream->callbacks, stream_cb);; + + return stream_cb->id; +} + +int avdtp_get_configuration(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + + if (session->state < AVDTP_SESSION_STATE_CONNECTED) + return -EINVAL; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_GET_CONFIGURATION, + &req, sizeof(req)); +} + +static void copy_capabilities(gpointer data, gpointer user_data) +{ + struct avdtp_service_capability *src_cap = data; + struct avdtp_service_capability *dst_cap; + GSList **l = user_data; + + dst_cap = avdtp_service_cap_new(src_cap->category, src_cap->data, + src_cap->length); + + *l = g_slist_append(*l, dst_cap); +} + +int avdtp_set_configuration(struct avdtp *session, + struct avdtp_remote_sep *rsep, + struct avdtp_local_sep *lsep, + GSList *caps, + struct avdtp_stream **stream) +{ + struct setconf_req *req; + struct avdtp_stream *new_stream; + unsigned char *ptr; + int err, caps_len; + struct avdtp_service_capability *cap; + GSList *l; + + if (session->state != AVDTP_SESSION_STATE_CONNECTED) + return -ENOTCONN; + + if (!(lsep && rsep)) + return -EINVAL; + + DBG("%p: int_seid=%u, acp_seid=%u", session, + lsep->info.seid, rsep->seid); + + new_stream = g_new0(struct avdtp_stream, 1); + new_stream->session = session; + new_stream->lsep = lsep; + new_stream->rseid = rsep->seid; + + if (rsep->delay_reporting && lsep->delay_reporting) { + struct avdtp_service_capability *delay_reporting; + + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + caps = g_slist_append(caps, delay_reporting); + new_stream->delay_reporting = TRUE; + } + + g_slist_foreach(caps, copy_capabilities, &new_stream->caps); + + /* Calculate total size of request */ + for (l = caps, caps_len = 0; l != NULL; l = g_slist_next(l)) { + cap = l->data; + caps_len += cap->length + 2; + } + + req = g_malloc0(sizeof(struct setconf_req) + caps_len); + + req->int_seid = lsep->info.seid; + req->acp_seid = rsep->seid; + + /* Copy the capabilities into the request */ + for (l = caps, ptr = req->caps; l != NULL; l = g_slist_next(l)) { + cap = l->data; + memcpy(ptr, cap, cap->length + 2); + ptr += cap->length + 2; + } + + err = send_request(session, FALSE, new_stream, + AVDTP_SET_CONFIGURATION, req, + sizeof(struct setconf_req) + caps_len); + if (err < 0) + stream_free(new_stream); + else { + lsep->info.inuse = 1; + lsep->stream = new_stream; + rsep->stream = new_stream; + session->streams = g_slist_append(session->streams, new_stream); + if (stream) + *stream = new_stream; + } + + g_free(req); + + return err; +} + +int avdtp_reconfigure(struct avdtp *session, GSList *caps, + struct avdtp_stream *stream) +{ + struct reconf_req *req; + unsigned char *ptr; + int caps_len, err; + GSList *l; + struct avdtp_service_capability *cap; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state != AVDTP_STATE_OPEN) + return -EINVAL; + + /* Calculate total size of request */ + for (l = caps, caps_len = 0; l != NULL; l = g_slist_next(l)) { + cap = l->data; + caps_len += cap->length + 2; + } + + req = g_malloc0(sizeof(struct reconf_req) + caps_len); + + req->acp_seid = stream->rseid; + + /* Copy the capabilities into the request */ + for (l = caps, ptr = req->caps; l != NULL; l = g_slist_next(l)) { + cap = l->data; + memcpy(ptr, cap, cap->length + 2); + ptr += cap->length + 2; + } + + err = send_request(session, FALSE, stream, AVDTP_RECONFIGURE, req, + sizeof(*req) + caps_len); + g_free(req); + + return err; +} + +int avdtp_open(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state > AVDTP_STATE_CONFIGURED) + return -EINVAL; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_OPEN, + &req, sizeof(req)); +} + +int avdtp_start(struct avdtp *session, struct avdtp_stream *stream) +{ + struct start_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state != AVDTP_STATE_OPEN) + return -EINVAL; + + if (stream->close_int == TRUE) { + error("avdtp_start: rejecting start since close is initiated"); + return -EINVAL; + } + + memset(&req, 0, sizeof(req)); + req.first_seid.seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_START, + &req, sizeof(req)); +} + +int avdtp_close(struct avdtp *session, struct avdtp_stream *stream, + gboolean immediate) +{ + struct seid_req req; + int ret; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state < AVDTP_STATE_OPEN) + return -EINVAL; + + if (stream->close_int == TRUE) { + error("avdtp_close: rejecting since close is already initiated"); + return -EINVAL; + } + + if (immediate && session->req && stream == session->req->stream) + return avdtp_abort(session, stream); + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + ret = send_request(session, FALSE, stream, AVDTP_CLOSE, + &req, sizeof(req)); + if (ret == 0) + stream->close_int = TRUE; + + return ret; +} + +int avdtp_suspend(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state <= AVDTP_STATE_OPEN || stream->close_int) + return -EINVAL; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_SUSPEND, + &req, sizeof(req)); +} + +int avdtp_abort(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + int ret; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state == AVDTP_STATE_ABORTING) + return -EINVAL; + + if (session->req && stream == session->req->stream) + return cancel_request(session, ECANCELED); + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + ret = send_request(session, TRUE, stream, AVDTP_ABORT, + &req, sizeof(req)); + if (ret == 0) + stream->abort_int = TRUE; + + return ret; +} + +int avdtp_delay_report(struct avdtp *session, struct avdtp_stream *stream, + uint16_t delay) +{ + struct delay_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state != AVDTP_STATE_CONFIGURED && + stream->lsep->state != AVDTP_STATE_STREAMING) + return -EINVAL; + + if (!stream->delay_reporting || session->version < 0x0103 || + session->server->version < 0x0103) + return -EINVAL; + + stream->delay = delay; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + req.delay = htons(delay); + + return send_request(session, TRUE, stream, AVDTP_DELAY_REPORT, + &req, sizeof(req)); +} + +struct avdtp_local_sep *avdtp_register_sep(const bdaddr_t *src, uint8_t type, + uint8_t media_type, + uint8_t codec_type, + gboolean delay_reporting, + struct avdtp_sep_ind *ind, + struct avdtp_sep_cfm *cfm, + void *user_data) +{ + struct avdtp_server *server; + struct avdtp_local_sep *sep; + + server = find_server(servers, src); + if (!server) + return NULL; + + if (g_slist_length(server->seps) > MAX_SEID) + return NULL; + + sep = g_new0(struct avdtp_local_sep, 1); + + sep->state = AVDTP_STATE_IDLE; + sep->info.seid = g_slist_length(server->seps) + 1; + sep->info.type = type; + sep->info.media_type = media_type; + sep->codec = codec_type; + sep->ind = ind; + sep->cfm = cfm; + sep->user_data = user_data; + sep->server = server; + sep->delay_reporting = TRUE; + + DBG("SEP %p registered: type:%d codec:%d seid:%d", sep, + sep->info.type, sep->codec, sep->info.seid); + server->seps = g_slist_append(server->seps, sep); + + return sep; +} + +int avdtp_unregister_sep(struct avdtp_local_sep *sep) +{ + struct avdtp_server *server; + + if (!sep) + return -EINVAL; + + server = sep->server; + server->seps = g_slist_remove(server->seps, sep); + + if (sep->stream) + release_stream(sep->stream, sep->stream->session); + + DBG("SEP %p unregistered: type:%d codec:%d seid:%d", sep, + sep->info.type, sep->codec, sep->info.seid); + + g_free(sep); + + return 0; +} + +static GIOChannel *avdtp_server_socket(const bdaddr_t *src, gboolean master) +{ + GError *err = NULL; + GIOChannel *io; + + io = bt_io_listen(BT_IO_L2CAP, NULL, avdtp_confirm_cb, + NULL, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, src, + BT_IO_OPT_PSM, AVDTP_PSM, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + } + + return io; +} + +const char *avdtp_strerror(struct avdtp_error *err) +{ + if (err->category == AVDTP_ERRNO) + return strerror(err->err.posix_errno); + + switch(err->err.error_code) { + case AVDTP_BAD_HEADER_FORMAT: + return "Bad Header Format"; + case AVDTP_BAD_LENGTH: + return "Bad Packet Lenght"; + case AVDTP_BAD_ACP_SEID: + return "Bad Acceptor SEID"; + case AVDTP_SEP_IN_USE: + return "Stream End Point in Use"; + case AVDTP_SEP_NOT_IN_USE: + return "Stream End Point Not in Use"; + case AVDTP_BAD_SERV_CATEGORY: + return "Bad Service Category"; + case AVDTP_BAD_PAYLOAD_FORMAT: + return "Bad Payload format"; + case AVDTP_NOT_SUPPORTED_COMMAND: + return "Command Not Supported"; + case AVDTP_INVALID_CAPABILITIES: + return "Invalid Capabilities"; + case AVDTP_BAD_RECOVERY_TYPE: + return "Bad Recovery Type"; + case AVDTP_BAD_MEDIA_TRANSPORT_FORMAT: + return "Bad Media Transport Format"; + case AVDTP_BAD_RECOVERY_FORMAT: + return "Bad Recovery Format"; + case AVDTP_BAD_ROHC_FORMAT: + return "Bad Header Compression Format"; + case AVDTP_BAD_CP_FORMAT: + return "Bad Content Protetion Format"; + case AVDTP_BAD_MULTIPLEXING_FORMAT: + return "Bad Multiplexing Format"; + case AVDTP_UNSUPPORTED_CONFIGURATION: + return "Configuration not supported"; + case AVDTP_BAD_STATE: + return "Bad State"; + default: + return "Unknow error"; + } +} + +avdtp_state_t avdtp_sep_get_state(struct avdtp_local_sep *sep) +{ + return sep->state; +} + +void avdtp_get_peers(struct avdtp *session, bdaddr_t *src, bdaddr_t *dst) +{ + if (src) + bacpy(src, &session->server->src); + if (dst) + bacpy(dst, &session->dst); +} + +int avdtp_init(const bdaddr_t *src, GKeyFile *config, uint16_t *version) +{ + GError *err = NULL; + gboolean tmp, master = TRUE; + struct avdtp_server *server; + uint16_t ver = 0x0102; + + if (!config) + goto proceed; + + tmp = g_key_file_get_boolean(config, "General", + "Master", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + master = tmp; + + tmp = g_key_file_get_boolean(config, "General", "AutoConnect", + &err); + if (err) + g_clear_error(&err); + else + auto_connect = tmp; + + if (g_key_file_get_boolean(config, "A2DP", "DelayReporting", NULL)) + ver = 0x0103; + +proceed: + server = g_new0(struct avdtp_server, 1); + if (!server) + return -ENOMEM; + + server->version = ver; + + if (version) + *version = server->version; + + server->io = avdtp_server_socket(src, master); + if (!server->io) { + g_free(server); + return -1; + } + + bacpy(&server->src, src); + + servers = g_slist_append(servers, server); + + return 0; +} + +void avdtp_exit(const bdaddr_t *src) +{ + struct avdtp_server *server; + GSList *l; + + server = find_server(servers, src); + if (!server) + return; + + for (l = server->sessions; l; l = l->next) { + struct avdtp *session = l->data; + + connection_lost(session, -ECONNABORTED); + } + + servers = g_slist_remove(servers, server); + + g_io_channel_shutdown(server->io, TRUE, NULL); + g_io_channel_unref(server->io); + g_free(server); +} + +gboolean avdtp_has_stream(struct avdtp *session, struct avdtp_stream *stream) +{ + return g_slist_find(session->streams, stream) ? TRUE : FALSE; +} + +void avdtp_set_auto_disconnect(struct avdtp *session, gboolean auto_dc) +{ + session->auto_dc = auto_dc; +} + +gboolean avdtp_stream_setup_active(struct avdtp *session) +{ + return session->stream_setup; +} + +void avdtp_set_device_disconnect(struct avdtp *session, gboolean dev_dc) +{ + session->device_disconnect = dev_dc; +} + +unsigned int avdtp_add_state_cb(avdtp_session_state_cb cb, void *user_data) +{ + struct avdtp_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct avdtp_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + avdtp_callbacks = g_slist_append(avdtp_callbacks, state_cb);; + + return state_cb->id; +} + +gboolean avdtp_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = avdtp_callbacks; l != NULL; l = l->next) { + struct avdtp_state_callback *cb = l->data; + if (cb && cb->id == id) { + avdtp_callbacks = g_slist_remove(avdtp_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/avdtp.h b/audio/avdtp.h new file mode 100644 index 0000000..5f37dc3 --- /dev/null +++ b/audio/avdtp.h @@ -0,0 +1,316 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +typedef enum { + AVDTP_SESSION_STATE_DISCONNECTED, + AVDTP_SESSION_STATE_CONNECTING, + AVDTP_SESSION_STATE_CONNECTED +} avdtp_session_state_t; + +struct avdtp; +struct avdtp_stream; +struct avdtp_local_sep; +struct avdtp_remote_sep; +struct avdtp_error { + uint8_t category; + union { + uint8_t error_code; + int posix_errno; + } err; +}; + +/* SEP capability categories */ +#define AVDTP_MEDIA_TRANSPORT 0x01 +#define AVDTP_REPORTING 0x02 +#define AVDTP_RECOVERY 0x03 +#define AVDTP_CONTENT_PROTECTION 0x04 +#define AVDTP_HEADER_COMPRESSION 0x05 +#define AVDTP_MULTIPLEXING 0x06 +#define AVDTP_MEDIA_CODEC 0x07 +#define AVDTP_DELAY_REPORTING 0x08 +#define AVDTP_ERRNO 0xff + +/* AVDTP error definitions */ +#define AVDTP_BAD_HEADER_FORMAT 0x01 +#define AVDTP_BAD_LENGTH 0x11 +#define AVDTP_BAD_ACP_SEID 0x12 +#define AVDTP_SEP_IN_USE 0x13 +#define AVDTP_SEP_NOT_IN_USE 0x14 +#define AVDTP_BAD_SERV_CATEGORY 0x17 +#define AVDTP_BAD_PAYLOAD_FORMAT 0x18 +#define AVDTP_NOT_SUPPORTED_COMMAND 0x19 +#define AVDTP_INVALID_CAPABILITIES 0x1A +#define AVDTP_BAD_RECOVERY_TYPE 0x22 +#define AVDTP_BAD_MEDIA_TRANSPORT_FORMAT 0x23 +#define AVDTP_BAD_RECOVERY_FORMAT 0x25 +#define AVDTP_BAD_ROHC_FORMAT 0x26 +#define AVDTP_BAD_CP_FORMAT 0x27 +#define AVDTP_BAD_MULTIPLEXING_FORMAT 0x28 +#define AVDTP_UNSUPPORTED_CONFIGURATION 0x29 +#define AVDTP_BAD_STATE 0x31 + +/* SEP types definitions */ +#define AVDTP_SEP_TYPE_SOURCE 0x00 +#define AVDTP_SEP_TYPE_SINK 0x01 + +/* Media types definitions */ +#define AVDTP_MEDIA_TYPE_AUDIO 0x00 +#define AVDTP_MEDIA_TYPE_VIDEO 0x01 +#define AVDTP_MEDIA_TYPE_MULTIMEDIA 0x02 + +typedef enum { + AVDTP_STATE_IDLE, + AVDTP_STATE_CONFIGURED, + AVDTP_STATE_OPEN, + AVDTP_STATE_STREAMING, + AVDTP_STATE_CLOSING, + AVDTP_STATE_ABORTING, +} avdtp_state_t; + +struct avdtp_service_capability { + uint8_t category; + uint8_t length; + uint8_t data[0]; +} __attribute__ ((packed)); + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avdtp_media_codec_capability { + uint8_t rfa0:4; + uint8_t media_type:4; + uint8_t media_codec_type; + uint8_t data[0]; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avdtp_media_codec_capability { + uint8_t media_type:4; + uint8_t rfa0:4; + uint8_t media_codec_type; + uint8_t data[0]; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +typedef void (*avdtp_session_state_cb) (struct audio_device *dev, + struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data); + +typedef void (*avdtp_stream_state_cb) (struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data); + +typedef void (*avdtp_set_configuration_cb) (struct avdtp *session, + struct avdtp_stream *stream, + struct avdtp_error *err); + +/* Callbacks for when a reply is received to a command that we sent */ +struct avdtp_sep_cfm { + void (*set_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data); + void (*get_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data); + void (*open) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data); + void (*start) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data); + void (*suspend) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*close) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*abort) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*reconfigure) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*delay_report) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); +}; + +/* Callbacks for indicating when we received a new command. The return value + * indicates whether the command should be rejected or accepted */ +struct avdtp_sep_ind { + gboolean (*get_capability) (struct avdtp *session, + struct avdtp_local_sep *sep, + gboolean get_all, + GSList **caps, uint8_t *err, + void *user_data); + gboolean (*set_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data); + gboolean (*get_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + uint8_t *err, void *user_data); + gboolean (*open) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*start) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*suspend) (struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*close) (struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*abort) (struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*reconfigure) (struct avdtp *session, + struct avdtp_local_sep *lsep, + uint8_t *err, void *user_data); + gboolean (*delayreport) (struct avdtp *session, + struct avdtp_local_sep *lsep, + uint8_t rseid, uint16_t delay, + uint8_t *err, void *user_data); +}; + +typedef void (*avdtp_discover_cb_t) (struct avdtp *session, GSList *seps, + struct avdtp_error *err, void *user_data); + +struct avdtp *avdtp_get(bdaddr_t *src, bdaddr_t *dst); + +void avdtp_unref(struct avdtp *session); +struct avdtp *avdtp_ref(struct avdtp *session); + +gboolean avdtp_is_connected(const bdaddr_t *src, const bdaddr_t *dst); + +struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category, + void *data, int size); + +struct avdtp_remote_sep *avdtp_get_remote_sep(struct avdtp *session, + uint8_t seid); + +uint8_t avdtp_get_seid(struct avdtp_remote_sep *sep); + +uint8_t avdtp_get_type(struct avdtp_remote_sep *sep); + +struct avdtp_service_capability *avdtp_get_codec(struct avdtp_remote_sep *sep); + +gboolean avdtp_get_delay_reporting(struct avdtp_remote_sep *sep); + +struct avdtp_stream *avdtp_get_stream(struct avdtp_remote_sep *sep); + +int avdtp_discover(struct avdtp *session, avdtp_discover_cb_t cb, + void *user_data); + +gboolean avdtp_has_stream(struct avdtp *session, struct avdtp_stream *stream); + +unsigned int avdtp_stream_add_cb(struct avdtp *session, + struct avdtp_stream *stream, + avdtp_stream_state_cb cb, void *data); +gboolean avdtp_stream_remove_cb(struct avdtp *session, + struct avdtp_stream *stream, + unsigned int id); + +gboolean avdtp_stream_get_transport(struct avdtp_stream *stream, int *sock, + uint16_t *imtu, uint16_t *omtu, + GSList **caps); +struct avdtp_service_capability *avdtp_stream_get_codec( + struct avdtp_stream *stream); +gboolean avdtp_stream_has_capability(struct avdtp_stream *stream, + struct avdtp_service_capability *cap); +gboolean avdtp_stream_has_capabilities(struct avdtp_stream *stream, + GSList *caps); +struct avdtp_remote_sep *avdtp_stream_get_remote_sep( + struct avdtp_stream *stream); + +unsigned int avdtp_add_state_cb(avdtp_session_state_cb cb, void *user_data); + +gboolean avdtp_remove_state_cb(unsigned int id); + +int avdtp_set_configuration(struct avdtp *session, + struct avdtp_remote_sep *rsep, + struct avdtp_local_sep *lsep, + GSList *caps, + struct avdtp_stream **stream); + +int avdtp_get_configuration(struct avdtp *session, + struct avdtp_stream *stream); + +int avdtp_open(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_reconfigure(struct avdtp *session, GSList *caps, + struct avdtp_stream *stream); +int avdtp_start(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_suspend(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_close(struct avdtp *session, struct avdtp_stream *stream, + gboolean immediate); +int avdtp_abort(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_delay_report(struct avdtp *session, struct avdtp_stream *stream, + uint16_t delay); + +struct avdtp_local_sep *avdtp_register_sep(const bdaddr_t *src, uint8_t type, + uint8_t media_type, + uint8_t codec_type, + gboolean delay_reporting, + struct avdtp_sep_ind *ind, + struct avdtp_sep_cfm *cfm, + void *user_data); + +/* Find a matching pair of local and remote SEP ID's */ +struct avdtp_remote_sep *avdtp_find_remote_sep(struct avdtp *session, + struct avdtp_local_sep *lsep); + +int avdtp_unregister_sep(struct avdtp_local_sep *sep); + +avdtp_state_t avdtp_sep_get_state(struct avdtp_local_sep *sep); + +void avdtp_error_init(struct avdtp_error *err, uint8_t type, int id); +const char *avdtp_strerror(struct avdtp_error *err); +uint8_t avdtp_error_category(struct avdtp_error *err); +int avdtp_error_error_code(struct avdtp_error *err); +int avdtp_error_posix_errno(struct avdtp_error *err); + +void avdtp_get_peers(struct avdtp *session, bdaddr_t *src, bdaddr_t *dst); + +void avdtp_set_auto_disconnect(struct avdtp *session, gboolean auto_dc); +gboolean avdtp_stream_setup_active(struct avdtp *session); +void avdtp_set_device_disconnect(struct avdtp *session, gboolean dev_dc); + +int avdtp_init(const bdaddr_t *src, GKeyFile *config, uint16_t *version); +void avdtp_exit(const bdaddr_t *src); diff --git a/audio/bluetooth.conf b/audio/bluetooth.conf new file mode 100644 index 0000000..55b51e4 --- /dev/null +++ b/audio/bluetooth.conf @@ -0,0 +1,36 @@ +# Please note that this ALSA configuration file fragment needs be enabled in +# /etc/asound.conf or a similar configuration file with directives similar to +# the following: +# +#@hooks [ +# { +# func load +# files [ +# "/etc/alsa/bluetooth.conf" +# ] +# errors false +# } +#] + +pcm.rawbluetooth { + @args [ ADDRESS ] + @args.ADDRESS { + type string + } + type bluetooth + device $ADDRESS +} + +pcm.bluetooth { + @args [ ADDRESS ] + @args.ADDRESS { + type string + } + type plug + slave { + pcm { + type bluetooth + device $ADDRESS + } + } +} diff --git a/audio/control.c b/audio/control.c new file mode 100644 index 0000000..95c0406 --- /dev/null +++ b/audio/control.c @@ -0,0 +1,1193 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdint.h> +#include <errno.h> +#include <unistd.h> +#include <assert.h> +#include <signal.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <netinet/in.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "error.h" +#include "uinput.h" +#include "adapter.h" +#include "../src/device.h" +#include "device.h" +#include "manager.h" +#include "avdtp.h" +#include "control.h" +#include "sdpd.h" +#include "glib-helper.h" +#include "btio.h" +#include "dbus-common.h" + +#define AVCTP_PSM 23 + +/* Message types */ +#define AVCTP_COMMAND 0 +#define AVCTP_RESPONSE 1 + +/* Packet types */ +#define AVCTP_PACKET_SINGLE 0 +#define AVCTP_PACKET_START 1 +#define AVCTP_PACKET_CONTINUE 2 +#define AVCTP_PACKET_END 3 + +/* ctype entries */ +#define CTYPE_CONTROL 0x0 +#define CTYPE_STATUS 0x1 +#define CTYPE_NOT_IMPLEMENTED 0x8 +#define CTYPE_ACCEPTED 0x9 +#define CTYPE_REJECTED 0xA +#define CTYPE_STABLE 0xC + +/* opcodes */ +#define OP_UNITINFO 0x30 +#define OP_SUBUNITINFO 0x31 +#define OP_PASSTHROUGH 0x7c + +/* subunits of interest */ +#define SUBUNIT_PANEL 0x09 + +/* operands in passthrough commands */ +#define VOL_UP_OP 0x41 +#define VOL_DOWN_OP 0x42 +#define MUTE_OP 0x43 +#define PLAY_OP 0x44 +#define STOP_OP 0x45 +#define PAUSE_OP 0x46 +#define RECORD_OP 0x47 +#define REWIND_OP 0x48 +#define FAST_FORWARD_OP 0x49 +#define EJECT_OP 0x4a +#define FORWARD_OP 0x4b +#define BACKWARD_OP 0x4c + +#define QUIRK_NO_RELEASE 1 << 0 + +static DBusConnection *connection = NULL; + +static GSList *servers = NULL; + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avctp_header { + uint8_t ipid:1; + uint8_t cr:1; + uint8_t packet_type:2; + uint8_t transaction:4; + uint16_t pid; +} __attribute__ ((packed)); +#define AVCTP_HEADER_LENGTH 3 + +struct avrcp_header { + uint8_t code:4; + uint8_t _hdr0:4; + uint8_t subunit_id:3; + uint8_t subunit_type:5; + uint8_t opcode; +} __attribute__ ((packed)); +#define AVRCP_HEADER_LENGTH 3 + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avctp_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t cr:1; + uint8_t ipid:1; + uint16_t pid; +} __attribute__ ((packed)); +#define AVCTP_HEADER_LENGTH 3 + +struct avrcp_header { + uint8_t _hdr0:4; + uint8_t code:4; + uint8_t subunit_type:5; + uint8_t subunit_id:3; + uint8_t opcode; +} __attribute__ ((packed)); +#define AVRCP_HEADER_LENGTH 3 + +#else +#error "Unknown byte order" +#endif + +struct avctp_state_callback { + avctp_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct avctp_server { + bdaddr_t src; + GIOChannel *io; + uint32_t tg_record_id; + uint32_t ct_record_id; +}; + +struct control { + struct audio_device *dev; + + avctp_state_t state; + + int uinput; + + GIOChannel *io; + guint io_id; + + uint16_t mtu; + + gboolean target; + + uint8_t key_quirks[256]; +}; + +static struct { + const char *name; + uint8_t avrcp; + uint16_t uinput; +} key_map[] = { + { "PLAY", PLAY_OP, KEY_PLAYCD }, + { "STOP", STOP_OP, KEY_STOPCD }, + { "PAUSE", PAUSE_OP, KEY_PAUSECD }, + { "FORWARD", FORWARD_OP, KEY_NEXTSONG }, + { "BACKWARD", BACKWARD_OP, KEY_PREVIOUSSONG }, + { "REWIND", REWIND_OP, KEY_REWIND }, + { "FAST FORWARD", FAST_FORWARD_OP, KEY_FASTFORWARD }, + { NULL } +}; + +static GSList *avctp_callbacks = NULL; + +static void auth_cb(DBusError *derr, void *user_data); + +static sdp_record_t *avrcp_ct_record() +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, l2cap, avctp, avrct; + sdp_profile_desc_t profile[1]; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *psm, *version, *features; + uint16_t lp = AVCTP_PSM; + uint16_t avrcp_ver = 0x0100, avctp_ver = 0x0103, feat = 0x000f; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + /* Service Class ID List */ + sdp_uuid16_create(&avrct, AV_REMOTE_SVCLASS_ID); + svclass_id = sdp_list_append(0, &avrct); + sdp_set_service_classes(record, svclass_id); + + /* Protocol Descriptor List */ + sdp_uuid16_create(&l2cap, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap); + psm = sdp_data_alloc(SDP_UINT16, &lp); + proto[0] = sdp_list_append(proto[0], psm); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&avctp, AVCTP_UUID); + proto[1] = sdp_list_append(0, &avctp); + version = sdp_data_alloc(SDP_UINT16, &avctp_ver); + proto[1] = sdp_list_append(proto[1], version); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + /* Bluetooth Profile Descriptor List */ + sdp_uuid16_create(&profile[0].uuid, AV_REMOTE_PROFILE_ID); + profile[0].version = avrcp_ver; + pfseq = sdp_list_append(0, &profile[0]); + sdp_set_profile_descs(record, pfseq); + + features = sdp_data_alloc(SDP_UINT16, &feat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + sdp_set_info_attr(record, "AVRCP CT", 0, 0); + + free(psm); + free(version); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static sdp_record_t *avrcp_tg_record() +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, l2cap, avctp, avrtg; + sdp_profile_desc_t profile[1]; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *psm, *version, *features; + uint16_t lp = AVCTP_PSM; + uint16_t avrcp_ver = 0x0100, avctp_ver = 0x0103, feat = 0x000f; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + /* Service Class ID List */ + sdp_uuid16_create(&avrtg, AV_REMOTE_TARGET_SVCLASS_ID); + svclass_id = sdp_list_append(0, &avrtg); + sdp_set_service_classes(record, svclass_id); + + /* Protocol Descriptor List */ + sdp_uuid16_create(&l2cap, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap); + psm = sdp_data_alloc(SDP_UINT16, &lp); + proto[0] = sdp_list_append(proto[0], psm); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&avctp, AVCTP_UUID); + proto[1] = sdp_list_append(0, &avctp); + version = sdp_data_alloc(SDP_UINT16, &avctp_ver); + proto[1] = sdp_list_append(proto[1], version); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + /* Bluetooth Profile Descriptor List */ + sdp_uuid16_create(&profile[0].uuid, AV_REMOTE_PROFILE_ID); + profile[0].version = avrcp_ver; + pfseq = sdp_list_append(0, &profile[0]); + sdp_set_profile_descs(record, pfseq); + + features = sdp_data_alloc(SDP_UINT16, &feat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + sdp_set_info_attr(record, "AVRCP TG", 0, 0); + + free(psm); + free(version); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static int send_event(int fd, uint16_t type, uint16_t code, int32_t value) +{ + struct uinput_event event; + + memset(&event, 0, sizeof(event)); + event.type = type; + event.code = code; + event.value = value; + + return write(fd, &event, sizeof(event)); +} + +static void send_key(int fd, uint16_t key, int pressed) +{ + if (fd < 0) + return; + + send_event(fd, EV_KEY, key, pressed); + send_event(fd, EV_SYN, SYN_REPORT, 0); +} + +static void handle_panel_passthrough(struct control *control, + const unsigned char *operands, + int operand_count) +{ + const char *status; + int pressed, i; + + if (operand_count == 0) + return; + + if (operands[0] & 0x80) { + status = "released"; + pressed = 0; + } else { + status = "pressed"; + pressed = 1; + } + + for (i = 0; key_map[i].name != NULL; i++) { + uint8_t key_quirks; + + if ((operands[0] & 0x7F) != key_map[i].avrcp) + continue; + + DBG("AVRCP: %s %s", key_map[i].name, status); + + key_quirks = control->key_quirks[key_map[i].avrcp]; + + if (key_quirks & QUIRK_NO_RELEASE) { + if (!pressed) { + DBG("AVRCP: Ignoring release"); + break; + } + + DBG("AVRCP: treating key press as press + release"); + send_key(control->uinput, key_map[i].uinput, 1); + send_key(control->uinput, key_map[i].uinput, 0); + break; + } + + send_key(control->uinput, key_map[i].uinput, pressed); + break; + } + + if (key_map[i].name == NULL) + DBG("AVRCP: unknown button 0x%02X %s", + operands[0] & 0x7F, status); +} + +static void avctp_disconnected(struct audio_device *dev) +{ + struct control *control = dev->control; + + if (!control) + return; + + if (control->io) { + g_io_channel_shutdown(control->io, TRUE, NULL); + g_io_channel_unref(control->io); + control->io = NULL; + } + + if (control->io_id) { + g_source_remove(control->io_id); + control->io_id = 0; + + if (control->state == AVCTP_STATE_CONNECTING) + audio_device_cancel_authorization(dev, auth_cb, + control); + } + + if (control->uinput >= 0) { + char address[18]; + + ba2str(&dev->dst, address); + DBG("AVRCP: closing uinput for %s", address); + + ioctl(control->uinput, UI_DEV_DESTROY); + close(control->uinput); + control->uinput = -1; + } +} + +static void avctp_set_state(struct control *control, avctp_state_t new_state) +{ + GSList *l; + struct audio_device *dev = control->dev; + avctp_state_t old_state = control->state; + gboolean value; + + switch (new_state) { + case AVCTP_STATE_DISCONNECTED: + DBG("AVCTP Disconnected"); + + avctp_disconnected(control->dev); + + if (old_state != AVCTP_STATE_CONNECTED) + break; + + value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, + "Disconnected", DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + + if (!audio_device_is_active(dev, NULL)) + audio_device_set_authorized(dev, FALSE); + + break; + case AVCTP_STATE_CONNECTING: + DBG("AVCTP Connecting"); + break; + case AVCTP_STATE_CONNECTED: + DBG("AVCTP Connected"); + value = TRUE; + g_dbus_emit_signal(control->dev->conn, control->dev->path, + AUDIO_CONTROL_INTERFACE, "Connected", + DBUS_TYPE_INVALID); + emit_property_changed(control->dev->conn, control->dev->path, + AUDIO_CONTROL_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + break; + default: + error("Invalid AVCTP state %d", new_state); + return; + } + + control->state = new_state; + + for (l = avctp_callbacks; l != NULL; l = l->next) { + struct avctp_state_callback *cb = l->data; + cb->cb(control->dev, old_state, new_state, cb->user_data); + } +} + +static gboolean control_cb(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct control *control = data; + unsigned char buf[1024], *operands; + struct avctp_header *avctp; + struct avrcp_header *avrcp; + int ret, packet_size, operand_count, sock; + + if (cond & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) + goto failed; + + sock = g_io_channel_unix_get_fd(control->io); + + ret = read(sock, buf, sizeof(buf)); + if (ret <= 0) + goto failed; + + DBG("Got %d bytes of data for AVCTP session %p", ret, control); + + if ((unsigned int) ret < sizeof(struct avctp_header)) { + error("Too small AVCTP packet"); + goto failed; + } + + packet_size = ret; + + avctp = (struct avctp_header *) buf; + + DBG("AVCTP transaction %u, packet type %u, C/R %u, IPID %u, " + "PID 0x%04X", + avctp->transaction, avctp->packet_type, + avctp->cr, avctp->ipid, ntohs(avctp->pid)); + + ret -= sizeof(struct avctp_header); + if ((unsigned int) ret < sizeof(struct avrcp_header)) { + error("Too small AVRCP packet"); + goto failed; + } + + avrcp = (struct avrcp_header *) (buf + sizeof(struct avctp_header)); + + ret -= sizeof(struct avrcp_header); + + operands = buf + sizeof(struct avctp_header) + sizeof(struct avrcp_header); + operand_count = ret; + + DBG("AVRCP %s 0x%01X, subunit_type 0x%02X, subunit_id 0x%01X, " + "opcode 0x%02X, %d operands", + avctp->cr ? "response" : "command", + avrcp->code, avrcp->subunit_type, avrcp->subunit_id, + avrcp->opcode, operand_count); + + if (avctp->packet_type != AVCTP_PACKET_SINGLE) { + avctp->cr = AVCTP_RESPONSE; + avrcp->code = CTYPE_NOT_IMPLEMENTED; + } else if (avctp->pid != htons(AV_REMOTE_SVCLASS_ID)) { + avctp->ipid = 1; + avctp->cr = AVCTP_RESPONSE; + avrcp->code = CTYPE_REJECTED; + } else if (avctp->cr == AVCTP_COMMAND && + avrcp->code == CTYPE_CONTROL && + avrcp->subunit_type == SUBUNIT_PANEL && + avrcp->opcode == OP_PASSTHROUGH) { + handle_panel_passthrough(control, operands, operand_count); + avctp->cr = AVCTP_RESPONSE; + avrcp->code = CTYPE_ACCEPTED; + } else if (avctp->cr == AVCTP_COMMAND && + avrcp->code == CTYPE_STATUS && + (avrcp->opcode == OP_UNITINFO + || avrcp->opcode == OP_SUBUNITINFO)) { + avctp->cr = AVCTP_RESPONSE; + avrcp->code = CTYPE_STABLE; + /* The first operand should be 0x07 for the UNITINFO response. + * Neither AVRCP (section 22.1, page 117) nor AVC Digital + * Interface Command Set (section 9.2.1, page 45) specs + * explain this value but both use it */ + if (operand_count >= 1 && avrcp->opcode == OP_UNITINFO) + operands[0] = 0x07; + if (operand_count >= 2) + operands[1] = SUBUNIT_PANEL << 3; + DBG("reply to %s", avrcp->opcode == OP_UNITINFO ? + "OP_UNITINFO" : "OP_SUBUNITINFO"); + } else { + avctp->cr = AVCTP_RESPONSE; + avrcp->code = CTYPE_REJECTED; + } + ret = write(sock, buf, packet_size); + + return TRUE; + +failed: + DBG("AVCTP session %p got disconnected", control); + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); + return FALSE; +} + +static int uinput_create(char *name) +{ + struct uinput_dev dev; + int fd, err, i; + + fd = open("/dev/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/input/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/misc/uinput", O_RDWR); + if (fd < 0) { + err = errno; + error("Can't open input device: %s (%d)", + strerror(err), err); + return -err; + } + } + } + + memset(&dev, 0, sizeof(dev)); + if (name) + strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE - 1); + + dev.id.bustype = BUS_BLUETOOTH; + dev.id.vendor = 0x0000; + dev.id.product = 0x0000; + dev.id.version = 0x0000; + + if (write(fd, &dev, sizeof(dev)) < 0) { + err = errno; + error("Can't write device information: %s (%d)", + strerror(err), err); + close(fd); + errno = err; + return -err; + } + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_EVBIT, EV_REP); + ioctl(fd, UI_SET_EVBIT, EV_SYN); + + for (i = 0; key_map[i].name != NULL; i++) + ioctl(fd, UI_SET_KEYBIT, key_map[i].uinput); + + if (ioctl(fd, UI_DEV_CREATE, NULL) < 0) { + err = errno; + error("Can't create uinput device: %s (%d)", + strerror(err), err); + close(fd); + errno = err; + return -err; + } + + return fd; +} + +static void init_uinput(struct control *control) +{ + struct audio_device *dev = control->dev; + char address[18], name[248 + 1]; + + device_get_name(dev->btd_dev, name, sizeof(name)); + if (g_str_equal(name, "Nokia CK-20W")) { + control->key_quirks[FORWARD_OP] |= QUIRK_NO_RELEASE; + control->key_quirks[BACKWARD_OP] |= QUIRK_NO_RELEASE; + control->key_quirks[PLAY_OP] |= QUIRK_NO_RELEASE; + control->key_quirks[PAUSE_OP] |= QUIRK_NO_RELEASE; + } + + ba2str(&dev->dst, address); + + control->uinput = uinput_create(address); + if (control->uinput < 0) + error("AVRCP: failed to init uinput for %s", address); + else + DBG("AVRCP: uinput initialized for %s", address); +} + +static void avctp_connect_cb(GIOChannel *chan, GError *err, gpointer data) +{ + struct control *control = data; + char address[18]; + uint16_t imtu; + GError *gerr = NULL; + + if (err) { + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); + error("%s", err->message); + return; + } + + bt_io_get(chan, BT_IO_L2CAP, &gerr, + BT_IO_OPT_DEST, &address, + BT_IO_OPT_IMTU, &imtu, + BT_IO_OPT_INVALID); + if (gerr) { + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); + error("%s", gerr->message); + g_error_free(gerr); + return; + } + + DBG("AVCTP: connected to %s", address); + + if (!control->io) + control->io = g_io_channel_ref(chan); + + init_uinput(control); + + avctp_set_state(control, AVCTP_STATE_CONNECTED); + control->mtu = imtu; + control->io_id = g_io_add_watch(chan, + G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) control_cb, control); +} + +static void auth_cb(DBusError *derr, void *user_data) +{ + struct control *control = user_data; + GError *err = NULL; + + if (control->io_id) { + g_source_remove(control->io_id); + control->io_id = 0; + } + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); + return; + } + + if (!bt_io_accept(control->io, avctp_connect_cb, control, + NULL, &err)) { + error("bt_io_accept: %s", err->message); + g_error_free(err); + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); + } +} + +static void avctp_confirm_cb(GIOChannel *chan, gpointer data) +{ + struct control *control = NULL; + struct audio_device *dev; + char address[18]; + bdaddr_t src, dst; + GError *err = NULL; + + bt_io_get(chan, BT_IO_L2CAP, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_DEST, address, + BT_IO_OPT_INVALID); + if (err) { + error("%s", err->message); + g_error_free(err); + g_io_channel_shutdown(chan, TRUE, NULL); + return; + } + + dev = manager_get_device(&src, &dst, TRUE); + if (!dev) { + error("Unable to get audio device object for %s", address); + goto drop; + } + + if (!dev->control) { + btd_device_add_uuid(dev->btd_dev, AVRCP_REMOTE_UUID); + if (!dev->control) + goto drop; + } + + control = dev->control; + + if (control->io) { + error("Refusing unexpected connect from %s", address); + goto drop; + } + + avctp_set_state(control, AVCTP_STATE_CONNECTING); + control->io = g_io_channel_ref(chan); + + if (audio_device_request_authorization(dev, AVRCP_TARGET_UUID, + auth_cb, dev->control) < 0) + goto drop; + + control->io_id = g_io_add_watch(chan, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + control_cb, control); + return; + +drop: + if (!control || !control->io) + g_io_channel_shutdown(chan, TRUE, NULL); + if (control) + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); +} + +static GIOChannel *avctp_server_socket(const bdaddr_t *src, gboolean master) +{ + GError *err = NULL; + GIOChannel *io; + + io = bt_io_listen(BT_IO_L2CAP, NULL, avctp_confirm_cb, NULL, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, src, + BT_IO_OPT_PSM, AVCTP_PSM, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + } + + return io; +} + +gboolean avrcp_connect(struct audio_device *dev) +{ + struct control *control = dev->control; + GError *err = NULL; + GIOChannel *io; + + if (control->state > AVCTP_STATE_DISCONNECTED) + return TRUE; + + avctp_set_state(control, AVCTP_STATE_CONNECTING); + + io = bt_io_connect(BT_IO_L2CAP, avctp_connect_cb, control, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_PSM, AVCTP_PSM, + BT_IO_OPT_INVALID); + if (err) { + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); + error("%s", err->message); + g_error_free(err); + return FALSE; + } + + control->io = io; + + return TRUE; +} + +void avrcp_disconnect(struct audio_device *dev) +{ + struct control *control = dev->control; + + if (!(control && control->io)) + return; + + avctp_set_state(control, AVCTP_STATE_DISCONNECTED); +} + +int avrcp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config) +{ + sdp_record_t *record; + gboolean tmp, master = TRUE; + GError *err = NULL; + struct avctp_server *server; + + if (config) { + tmp = g_key_file_get_boolean(config, "General", + "Master", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_error_free(err); + } else + master = tmp; + } + + server = g_new0(struct avctp_server, 1); + if (!server) + return -ENOMEM; + + if (!connection) + connection = dbus_connection_ref(conn); + + record = avrcp_tg_record(); + if (!record) { + error("Unable to allocate new service record"); + g_free(server); + return -1; + } + + if (add_record_to_server(src, record) < 0) { + error("Unable to register AVRCP target service record"); + g_free(server); + sdp_record_free(record); + return -1; + } + server->tg_record_id = record->handle; + + record = avrcp_ct_record(); + if (!record) { + error("Unable to allocate new service record"); + g_free(server); + return -1; + } + + if (add_record_to_server(src, record) < 0) { + error("Unable to register AVRCP controller service record"); + sdp_record_free(record); + g_free(server); + return -1; + } + server->ct_record_id = record->handle; + + server->io = avctp_server_socket(src, master); + if (!server->io) { + remove_record_from_server(server->ct_record_id); + remove_record_from_server(server->tg_record_id); + g_free(server); + return -1; + } + + bacpy(&server->src, src); + + servers = g_slist_append(servers, server); + + return 0; +} + +static struct avctp_server *find_server(GSList *list, const bdaddr_t *src) +{ + GSList *l; + + for (l = list; l; l = l->next) { + struct avctp_server *server = l->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +void avrcp_unregister(const bdaddr_t *src) +{ + struct avctp_server *server; + + server = find_server(servers, src); + if (!server) + return; + + servers = g_slist_remove(servers, server); + + remove_record_from_server(server->ct_record_id); + remove_record_from_server(server->tg_record_id); + + g_io_channel_shutdown(server->io, TRUE, NULL); + g_io_channel_unref(server->io); + g_free(server); + + if (servers) + return; + + dbus_connection_unref(connection); + connection = NULL; +} + +static DBusMessage *control_is_connected(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct control *control = device->control; + DBusMessage *reply; + dbus_bool_t connected; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + connected = (control->state == AVCTP_STATE_CONNECTED); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); + + return reply; +} + +static int avctp_send_passthrough(struct control *control, uint8_t op) +{ + unsigned char buf[AVCTP_HEADER_LENGTH + AVRCP_HEADER_LENGTH + 2]; + struct avctp_header *avctp = (void *) buf; + struct avrcp_header *avrcp = (void *) &buf[AVCTP_HEADER_LENGTH]; + uint8_t *operands = &buf[AVCTP_HEADER_LENGTH + AVRCP_HEADER_LENGTH]; + int sk = g_io_channel_unix_get_fd(control->io); + static uint8_t transaction = 0; + + memset(buf, 0, sizeof(buf)); + + avctp->transaction = transaction++; + avctp->packet_type = AVCTP_PACKET_SINGLE; + avctp->cr = AVCTP_COMMAND; + avctp->pid = htons(AV_REMOTE_SVCLASS_ID); + + avrcp->code = CTYPE_CONTROL; + avrcp->subunit_type = SUBUNIT_PANEL; + avrcp->opcode = OP_PASSTHROUGH; + + operands[0] = op & 0x7f; + operands[1] = 0; + + if (write(sk, buf, sizeof(buf)) < 0) + return -errno; + + /* Button release */ + avctp->transaction = transaction++; + operands[0] |= 0x80; + + if (write(sk, buf, sizeof(buf)) < 0) + return -errno; + + return 0; +} + +static DBusMessage *volume_up(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct control *control = device->control; + DBusMessage *reply; + int err; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (control->state != AVCTP_STATE_CONNECTED) + return btd_error_not_connected(msg); + + if (!control->target) + return btd_error_not_supported(msg); + + err = avctp_send_passthrough(control, VOL_UP_OP); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *volume_down(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct control *control = device->control; + DBusMessage *reply; + int err; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (control->state != AVCTP_STATE_CONNECTED) + return btd_error_not_connected(msg); + + if (!control->target) + return btd_error_not_supported(msg); + + err = avctp_send_passthrough(control, VOL_DOWN_OP); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *control_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + gboolean value; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* Connected */ + value = (device->control->state == AVCTP_STATE_CONNECTED); + dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static GDBusMethodTable control_methods[] = { + { "IsConnected", "", "b", control_is_connected, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "GetProperties", "", "a{sv}",control_get_properties }, + { "VolumeUp", "", "", volume_up }, + { "VolumeDown", "", "", volume_down }, + { NULL, NULL, NULL, NULL } +}; + +static GDBusSignalTable control_signals[] = { + { "Connected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED}, + { "Disconnected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED}, + { "PropertyChanged", "sv" }, + { NULL, NULL } +}; + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + struct control *control = dev->control; + + DBG("Unregistered interface %s on path %s", + AUDIO_CONTROL_INTERFACE, dev->path); + + if (control->state != AVCTP_STATE_DISCONNECTED) + avctp_disconnected(dev); + + g_free(control); + dev->control = NULL; +} + +void control_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE); +} + +void control_update(struct audio_device *dev, uint16_t uuid16) +{ + struct control *control = dev->control; + + if (uuid16 == AV_REMOTE_TARGET_SVCLASS_ID) + control->target = TRUE; +} + +struct control *control_init(struct audio_device *dev, uint16_t uuid16) +{ + struct control *control; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, + control_methods, control_signals, NULL, + dev, path_unregister)) + return NULL; + + DBG("Registered interface %s on path %s", + AUDIO_CONTROL_INTERFACE, dev->path); + + control = g_new0(struct control, 1); + control->dev = dev; + control->state = AVCTP_STATE_DISCONNECTED; + control->uinput = -1; + + if (uuid16 == AV_REMOTE_TARGET_SVCLASS_ID) + control->target = TRUE; + + return control; +} + +gboolean control_is_active(struct audio_device *dev) +{ + struct control *control = dev->control; + + if (control && control->state != AVCTP_STATE_DISCONNECTED) + return TRUE; + + return FALSE; +} + +unsigned int avctp_add_state_cb(avctp_state_cb cb, void *user_data) +{ + struct avctp_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct avctp_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + avctp_callbacks = g_slist_append(avctp_callbacks, state_cb); + + return state_cb->id; +} + +gboolean avctp_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = avctp_callbacks; l != NULL; l = l->next) { + struct avctp_state_callback *cb = l->data; + if (cb && cb->id == id) { + avctp_callbacks = g_slist_remove(avctp_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/control.h b/audio/control.h new file mode 100644 index 0000000..49f25c2 --- /dev/null +++ b/audio/control.h @@ -0,0 +1,50 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_CONTROL_INTERFACE "org.bluez.Control" + +typedef enum { + AVCTP_STATE_DISCONNECTED = 0, + AVCTP_STATE_CONNECTING, + AVCTP_STATE_CONNECTED +} avctp_state_t; + +typedef void (*avctp_state_cb) (struct audio_device *dev, + avctp_state_t old_state, + avctp_state_t new_state, + void *user_data); + +unsigned int avctp_add_state_cb(avctp_state_cb cb, void *user_data); +gboolean avctp_remove_state_cb(unsigned int id); + +int avrcp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config); +void avrcp_unregister(const bdaddr_t *src); + +gboolean avrcp_connect(struct audio_device *dev); +void avrcp_disconnect(struct audio_device *dev); + +struct control *control_init(struct audio_device *dev, uint16_t uuid16); +void control_update(struct audio_device *dev, uint16_t uuid16); +void control_unregister(struct audio_device *dev); +gboolean control_is_active(struct audio_device *dev); diff --git a/audio/ctl_bluetooth.c b/audio/ctl_bluetooth.c new file mode 100644 index 0000000..2981385 --- /dev/null +++ b/audio/ctl_bluetooth.c @@ -0,0 +1,384 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <sys/socket.h> +#include <sys/un.h> + +#include <alsa/asoundlib.h> +#include <alsa/control_external.h> + +#include <bluetooth/bluetooth.h> + +#include "ipc.h" + +#ifdef ENABLE_DEBUG +#define DBG(fmt, arg...) printf("DEBUG: %s: " fmt "\n" , __FUNCTION__ , ## arg) +#else +#define DBG(fmt, arg...) +#endif + +#define BLUETOOTH_MINVOL 0 +#define BLUETOOTH_MAXVOL 15 + +struct bluetooth_data { + snd_ctl_ext_t ext; + int sock; +}; + +enum { + BLUETOOTH_PLAYBACK, + BLUETOOTH_CAPTURE, +}; + +static const char *vol_devices[2] = { + [BLUETOOTH_PLAYBACK] = "Playback volume", + [BLUETOOTH_CAPTURE] = "Capture volume", +}; + +static void bluetooth_exit(struct bluetooth_data *data) +{ + if (data == NULL) + return; + + if (data->sock >= 0) + bt_audio_service_close(data->sock); + + free(data); +} + +static void bluetooth_close(snd_ctl_ext_t *ext) +{ + struct bluetooth_data *data = ext->private_data; + + DBG("ext %p", ext); + + bluetooth_exit(data); +} + +static int bluetooth_elem_count(snd_ctl_ext_t *ext) +{ + DBG("ext %p", ext); + + return 2; +} + +static int bluetooth_elem_list(snd_ctl_ext_t *ext, + unsigned int offset, snd_ctl_elem_id_t *id) +{ + DBG("ext %p offset %d id %p", ext, offset, id); + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + + if (offset > 1) + return -EINVAL; + + snd_ctl_elem_id_set_name(id, vol_devices[offset]); + + return 0; +} + +static snd_ctl_ext_key_t bluetooth_find_elem(snd_ctl_ext_t *ext, + const snd_ctl_elem_id_t *id) +{ + const char *name = snd_ctl_elem_id_get_name(id); + int i; + + DBG("ext %p id %p name '%s'", ext, id, name); + + for (i = 0; i < 2; i++) + if (strcmp(name, vol_devices[i]) == 0) + return i; + + return SND_CTL_EXT_KEY_NOT_FOUND; +} + +static int bluetooth_get_attribute(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + int *type, unsigned int *acc, unsigned int *count) +{ + DBG("ext %p key %ld", ext, key); + + *type = SND_CTL_ELEM_TYPE_INTEGER; + *acc = SND_CTL_EXT_ACCESS_READWRITE; + *count = 1; + + return 0; +} + +static int bluetooth_get_integer_info(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + long *imin, long *imax, long *istep) +{ + DBG("ext %p key %ld", ext, key); + + *istep = 1; + *imin = BLUETOOTH_MINVOL; + *imax = BLUETOOTH_MAXVOL; + + return 0; +} + +static int bluetooth_send_ctl(struct bluetooth_data *data, + uint8_t mode, uint8_t key, struct bt_control_rsp *rsp) +{ + int ret; + struct bt_control_req *req = (void *) rsp; + bt_audio_error_t *err = (void *) rsp; + const char *type, *name; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_CONTROL; + req->h.length = sizeof(*req); + + req->mode = mode; + req->key = key; + + ret = send(data->sock, req, BT_SUGGESTED_BUFFER_SIZE, MSG_NOSIGNAL); + if (ret <= 0) { + SYSERR("Unable to request new volume value to server"); + return -errno; + } + + ret = recv(data->sock, rsp, BT_SUGGESTED_BUFFER_SIZE, 0); + if (ret <= 0) { + SNDERR("Unable to receive new volume value from server"); + return -errno; + } + + if (rsp->h.type == BT_ERROR) { + SNDERR("BT_CONTROL failed : %s (%d)", + strerror(err->posix_errno), + err->posix_errno); + return -err->posix_errno; + } + + type = bt_audio_strtype(rsp->h.type); + if (!type) { + SNDERR("Bogus message type %d " + "received from audio service", + rsp->h.type); + return -EINVAL; + } + + name = bt_audio_strname(rsp->h.name); + if (!name) { + SNDERR("Bogus message name %d " + "received from audio service", + rsp->h.name); + return -EINVAL; + } + + if (rsp->h.name != BT_CONTROL) { + SNDERR("Unexpected message %s received", type); + return -EINVAL; + } + + return 0; +} + +static int bluetooth_read_integer(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + long *value) +{ + struct bluetooth_data *data = ext->private_data; + int ret; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_control_rsp *rsp = (void *) buf; + + DBG("ext %p key %ld", ext, key); + + memset(buf, 0, sizeof(buf)); + *value = 0; + + ret = bluetooth_send_ctl(data, key, 0, rsp); + if (ret < 0) + goto done; + + *value = rsp->key; +done: + return ret; +} + +static int bluetooth_write_integer(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + long *value) +{ + struct bluetooth_data *data = ext->private_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_control_rsp *rsp = (void *) buf; + long current; + int ret, keyvalue; + + DBG("ext %p key %ld", ext, key); + + ret = bluetooth_read_integer(ext, key, ¤t); + if (ret < 0) + return ret; + + if (*value == current) + return 0; + + while (*value != current) { + keyvalue = (*value > current) ? BT_CONTROL_KEY_VOL_UP : + BT_CONTROL_KEY_VOL_DOWN; + + ret = bluetooth_send_ctl(data, key, keyvalue, rsp); + if (ret < 0) + break; + + current = keyvalue; + } + + return ret; +} + +static int bluetooth_read_event(snd_ctl_ext_t *ext, snd_ctl_elem_id_t *id, + unsigned int *event_mask) +{ + struct bluetooth_data *data = ext->private_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_control_ind *ind = (void *) buf; + int ret; + const char *type, *name; + + DBG("ext %p id %p", ext, id); + + memset(buf, 0, sizeof(buf)); + + ret = recv(data->sock, ind, BT_SUGGESTED_BUFFER_SIZE, MSG_DONTWAIT); + if (ret < 0) { + SNDERR("Failed while receiving data: %s (%d)", + strerror(errno), errno); + return -errno; + } + + type = bt_audio_strtype(ind->h.type); + if (!type) { + SNDERR("Bogus message type %d " + "received from audio service", + ind->h.type); + return -EAGAIN; + } + + name = bt_audio_strname(ind->h.name); + if (!name) { + SNDERR("Bogus message name %d " + "received from audio service", + ind->h.name); + return -EAGAIN; + } + + if (ind->h.name != BT_CONTROL) { + SNDERR("Unexpected message %s received", name); + return -EAGAIN; + } + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, ind->mode == BLUETOOTH_PLAYBACK ? + vol_devices[BLUETOOTH_PLAYBACK] : + vol_devices[BLUETOOTH_CAPTURE]); + *event_mask = SND_CTL_EVENT_MASK_VALUE; + + return 1; +} + +static snd_ctl_ext_callback_t bluetooth_callback = { + .close = bluetooth_close, + .elem_count = bluetooth_elem_count, + .elem_list = bluetooth_elem_list, + .find_elem = bluetooth_find_elem, + .get_attribute = bluetooth_get_attribute, + .get_integer_info = bluetooth_get_integer_info, + .read_integer = bluetooth_read_integer, + .write_integer = bluetooth_write_integer, + .read_event = bluetooth_read_event, +}; + +static int bluetooth_init(struct bluetooth_data *data) +{ + int sk; + + if (!data) + return -EINVAL; + + memset(data, 0, sizeof(struct bluetooth_data)); + + data->sock = -1; + + sk = bt_audio_service_open(); + if (sk < 0) + return -errno; + + data->sock = sk; + + return 0; +} + +SND_CTL_PLUGIN_DEFINE_FUNC(bluetooth); + +SND_CTL_PLUGIN_DEFINE_FUNC(bluetooth) +{ + struct bluetooth_data *data; + int err; + + DBG("Bluetooth Control plugin"); + + data = malloc(sizeof(struct bluetooth_data)); + if (!data) { + err = -ENOMEM; + goto error; + } + + err = bluetooth_init(data); + if (err < 0) + goto error; + + data->ext.version = SND_CTL_EXT_VERSION; + data->ext.card_idx = -1; + + strncpy(data->ext.id, "bluetooth", sizeof(data->ext.id) - 1); + strncpy(data->ext.driver, "Bluetooth-Audio", sizeof(data->ext.driver) - 1); + strncpy(data->ext.name, "Bluetooth Audio", sizeof(data->ext.name) - 1); + strncpy(data->ext.longname, "Bluetooth Audio", sizeof(data->ext.longname) - 1); + strncpy(data->ext.mixername, "Bluetooth Audio", sizeof(data->ext.mixername) - 1); + + data->ext.callback = &bluetooth_callback; + data->ext.poll_fd = data->sock; + data->ext.private_data = data; + + err = snd_ctl_ext_create(&data->ext, name, mode); + if (err < 0) + goto error; + + *handlep = data->ext.handle; + + return 0; + +error: + bluetooth_exit(data); + + return err; +} + +SND_CTL_PLUGIN_SYMBOL(bluetooth); diff --git a/audio/device.c b/audio/device.c new file mode 100644 index 0000000..e38e598 --- /dev/null +++ b/audio/device.c @@ -0,0 +1,862 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <errno.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/param.h> +#include <netinet/in.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "textfile.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#include "error.h" +#include "ipc.h" +#include "dbus-common.h" +#include "device.h" +#include "unix.h" +#include "avdtp.h" +#include "control.h" +#include "headset.h" +#include "gateway.h" +#include "sink.h" +#include "source.h" + +#define AUDIO_INTERFACE "org.bluez.Audio" + +#define CONTROL_CONNECT_TIMEOUT 2 +#define AVDTP_CONNECT_TIMEOUT 1 +#define HEADSET_CONNECT_TIMEOUT 1 + +typedef enum { + AUDIO_STATE_DISCONNECTED, + AUDIO_STATE_CONNECTING, + AUDIO_STATE_CONNECTED, +} audio_state_t; + +struct service_auth { + service_auth_cb cb; + void *user_data; +}; + +struct dev_priv { + audio_state_t state; + + headset_state_t hs_state; + sink_state_t sink_state; + avctp_state_t avctp_state; + GSList *auths; + + DBusMessage *conn_req; + DBusMessage *dc_req; + + guint control_timer; + guint avdtp_timer; + guint headset_timer; + guint dc_id; + + gboolean disconnecting; + gboolean authorized; + guint auth_idle_id; +}; + +static unsigned int sink_callback_id = 0; +static unsigned int avctp_callback_id = 0; +static unsigned int avdtp_callback_id = 0; +static unsigned int headset_callback_id = 0; + +static void device_free(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (dev->conn) + dbus_connection_unref(dev->conn); + + btd_device_unref(dev->btd_dev); + + if (priv) { + if (priv->auths) + audio_device_cancel_authorization(dev, NULL, NULL); + if (priv->control_timer) + g_source_remove(priv->control_timer); + if (priv->avdtp_timer) + g_source_remove(priv->avdtp_timer); + if (priv->headset_timer) + g_source_remove(priv->headset_timer); + if (priv->dc_req) + dbus_message_unref(priv->dc_req); + if (priv->conn_req) + dbus_message_unref(priv->conn_req); + if (priv->dc_id) + device_remove_disconnect_watch(dev->btd_dev, + priv->dc_id); + g_free(priv); + } + + g_free(dev->path); + g_free(dev); +} + +static const char *state2str(audio_state_t state) +{ + switch (state) { + case AUDIO_STATE_DISCONNECTED: + return "disconnected"; + case AUDIO_STATE_CONNECTING: + return "connecting"; + case AUDIO_STATE_CONNECTED: + return "connected"; + default: + error("Invalid audio state %d", state); + return NULL; + } +} + +static gboolean control_connect_timeout(gpointer user_data) +{ + struct audio_device *dev = user_data; + + dev->priv->control_timer = 0; + + if (dev->control) + avrcp_connect(dev); + + return FALSE; +} + +static gboolean device_set_control_timer(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->control) + return FALSE; + + if (priv->control_timer) + return FALSE; + + priv->control_timer = g_timeout_add_seconds(CONTROL_CONNECT_TIMEOUT, + control_connect_timeout, + dev); + + return TRUE; +} + +static void device_remove_control_timer(struct audio_device *dev) +{ + if (dev->priv->control_timer) + g_source_remove(dev->priv->control_timer); + dev->priv->control_timer = 0; +} + +static void device_remove_avdtp_timer(struct audio_device *dev) +{ + if (dev->priv->avdtp_timer) + g_source_remove(dev->priv->avdtp_timer); + dev->priv->avdtp_timer = 0; +} + +static void device_remove_headset_timer(struct audio_device *dev) +{ + if (dev->priv->headset_timer) + g_source_remove(dev->priv->headset_timer); + dev->priv->headset_timer = 0; +} + +static void disconnect_cb(struct btd_device *btd_dev, gboolean removal, + void *user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + if (priv->state == AUDIO_STATE_DISCONNECTED) + return; + + if (priv->disconnecting) + return; + + priv->disconnecting = TRUE; + + device_remove_control_timer(dev); + device_remove_avdtp_timer(dev); + device_remove_headset_timer(dev); + + if (dev->control) + avrcp_disconnect(dev); + + if (dev->sink && priv->sink_state != SINK_STATE_DISCONNECTED) + sink_shutdown(dev->sink); + else if (priv->hs_state != HEADSET_STATE_DISCONNECTED) + headset_shutdown(dev); + else + priv->disconnecting = FALSE; +} + +static void device_set_state(struct audio_device *dev, audio_state_t new_state) +{ + struct dev_priv *priv = dev->priv; + const char *state_str; + DBusMessage *reply = NULL; + + state_str = state2str(new_state); + if (!state_str) + return; + + if (new_state == AUDIO_STATE_DISCONNECTED) { + priv->authorized = FALSE; + + if (priv->dc_id) { + device_remove_disconnect_watch(dev->btd_dev, + priv->dc_id); + priv->dc_id = 0; + } + } else if (new_state == AUDIO_STATE_CONNECTING) + priv->dc_id = device_add_disconnect_watch(dev->btd_dev, + disconnect_cb, dev, NULL); + + if (dev->priv->state == new_state) { + DBG("state change attempted from %s to %s", + state_str, state_str); + return; + } + + dev->priv->state = new_state; + + if (new_state == AUDIO_STATE_DISCONNECTED) { + if (priv->dc_req) { + reply = dbus_message_new_method_return(priv->dc_req); + dbus_message_unref(priv->dc_req); + priv->dc_req = NULL; + g_dbus_send_message(dev->conn, reply); + } + priv->disconnecting = FALSE; + } + + if (priv->conn_req && new_state != AUDIO_STATE_CONNECTING) { + if (new_state == AUDIO_STATE_CONNECTED) + reply = dbus_message_new_method_return(priv->conn_req); + else + reply = btd_error_failed(priv->conn_req, + "Connect Failed"); + + dbus_message_unref(priv->conn_req); + priv->conn_req = NULL; + g_dbus_send_message(dev->conn, reply); + } + + emit_property_changed(dev->conn, dev->path, + AUDIO_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); +} + +static gboolean avdtp_connect_timeout(gpointer user_data) +{ + struct audio_device *dev = user_data; + + dev->priv->avdtp_timer = 0; + + if (dev->sink) { + struct avdtp *session = avdtp_get(&dev->src, &dev->dst); + + if (!session) + return FALSE; + + sink_setup_stream(dev->sink, session); + avdtp_unref(session); + } + + return FALSE; +} + +static gboolean device_set_avdtp_timer(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->sink) + return FALSE; + + if (priv->avdtp_timer) + return FALSE; + + priv->avdtp_timer = g_timeout_add_seconds(AVDTP_CONNECT_TIMEOUT, + avdtp_connect_timeout, + dev); + + return TRUE; +} + +static gboolean headset_connect_timeout(gpointer user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + dev->priv->headset_timer = 0; + + if (dev->headset == NULL) + return FALSE; + + if (headset_config_stream(dev, FALSE, NULL, NULL) == 0) { + if (priv->state != AUDIO_STATE_CONNECTED && + (priv->sink_state == SINK_STATE_CONNECTED || + priv->sink_state == SINK_STATE_PLAYING)) + device_set_state(dev, AUDIO_STATE_CONNECTED); + } + + return FALSE; +} + +static gboolean device_set_headset_timer(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->headset) + return FALSE; + + if (priv->headset_timer) + return FALSE; + + priv->headset_timer = g_timeout_add_seconds(HEADSET_CONNECT_TIMEOUT, + headset_connect_timeout, dev); + + return TRUE; +} + +static void device_avdtp_cb(struct audio_device *dev, struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data) +{ + if (!dev->sink || !dev->control) + return; + + if (new_state == AVDTP_SESSION_STATE_CONNECTED) { + if (avdtp_stream_setup_active(session)) + device_set_control_timer(dev); + else + avrcp_connect(dev); + } +} + +static void device_sink_cb(struct audio_device *dev, + sink_state_t old_state, + sink_state_t new_state, + void *user_data) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->sink) + return; + + priv->sink_state = new_state; + + switch (new_state) { + case SINK_STATE_DISCONNECTED: + if (dev->control) { + device_remove_control_timer(dev); + avrcp_disconnect(dev); + } + if (priv->hs_state != HEADSET_STATE_DISCONNECTED && + (priv->dc_req || priv->disconnecting)) { + headset_shutdown(dev); + break; + } + if (priv->hs_state == HEADSET_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_DISCONNECTED); + else if (old_state == SINK_STATE_CONNECTING) { + switch (priv->hs_state) { + case HEADSET_STATE_CONNECTED: + case HEADSET_STATE_PLAY_IN_PROGRESS: + case HEADSET_STATE_PLAYING: + device_set_state(dev, AUDIO_STATE_CONNECTED); + default: + break; + } + } + break; + case SINK_STATE_CONNECTING: + device_remove_avdtp_timer(dev); + if (priv->hs_state == HEADSET_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_CONNECTING); + break; + case SINK_STATE_CONNECTED: + if (old_state == SINK_STATE_PLAYING) + break; + if (dev->auto_connect) { + if (!dev->headset) + device_set_state(dev, AUDIO_STATE_CONNECTED); + else if (priv->hs_state == HEADSET_STATE_DISCONNECTED) + device_set_headset_timer(dev); + else if (priv->hs_state == HEADSET_STATE_CONNECTED || + priv->hs_state == HEADSET_STATE_PLAY_IN_PROGRESS || + priv->hs_state == HEADSET_STATE_PLAYING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + } else if (priv->hs_state == HEADSET_STATE_DISCONNECTED || + priv->hs_state == HEADSET_STATE_CONNECTING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + break; + case SINK_STATE_PLAYING: + break; + } +} + +static void device_avctp_cb(struct audio_device *dev, + avctp_state_t old_state, + avctp_state_t new_state, + void *user_data) +{ + if (!dev->control) + return; + + dev->priv->avctp_state = new_state; + + switch (new_state) { + case AVCTP_STATE_DISCONNECTED: + break; + case AVCTP_STATE_CONNECTING: + device_remove_control_timer(dev); + break; + case AVCTP_STATE_CONNECTED: + break; + } +} + +static void device_headset_cb(struct audio_device *dev, + headset_state_t old_state, + headset_state_t new_state, + void *user_data) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->headset) + return; + + priv->hs_state = new_state; + + switch (new_state) { + case HEADSET_STATE_DISCONNECTED: + device_remove_avdtp_timer(dev); + if (priv->sink_state != SINK_STATE_DISCONNECTED && dev->sink && + (priv->dc_req || priv->disconnecting)) { + sink_shutdown(dev->sink); + break; + } + if (priv->sink_state == SINK_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_DISCONNECTED); + else if (old_state == HEADSET_STATE_CONNECTING && + (priv->sink_state == SINK_STATE_CONNECTED || + priv->sink_state == SINK_STATE_PLAYING)) + device_set_state(dev, AUDIO_STATE_CONNECTED); + break; + case HEADSET_STATE_CONNECTING: + device_remove_headset_timer(dev); + if (priv->sink_state == SINK_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_CONNECTING); + break; + case HEADSET_STATE_CONNECTED: + if (old_state == HEADSET_STATE_CONNECTED || + old_state == HEADSET_STATE_PLAY_IN_PROGRESS || + old_state == HEADSET_STATE_PLAYING) + break; + if (dev->auto_connect) { + if (!dev->sink) + device_set_state(dev, AUDIO_STATE_CONNECTED); + else if (priv->sink_state == SINK_STATE_DISCONNECTED) + device_set_avdtp_timer(dev); + else if (priv->sink_state == SINK_STATE_CONNECTED || + priv->sink_state == SINK_STATE_PLAYING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + } else if (priv->sink_state == SINK_STATE_DISCONNECTED || + priv->sink_state == SINK_STATE_CONNECTING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + break; + case HEADSET_STATE_PLAY_IN_PROGRESS: + break; + case HEADSET_STATE_PLAYING: + break; + } +} + +static DBusMessage *dev_connect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *dev = data; + struct dev_priv *priv = dev->priv; + + if (priv->state == AUDIO_STATE_CONNECTING) + return btd_error_in_progress(msg); + else if (priv->state == AUDIO_STATE_CONNECTED) + return btd_error_already_connected(msg); + + dev->auto_connect = TRUE; + + if (dev->headset) + headset_config_stream(dev, FALSE, NULL, NULL); + + if (priv->state != AUDIO_STATE_CONNECTING && dev->sink) { + struct avdtp *session = avdtp_get(&dev->src, &dev->dst); + + if (!session) + return btd_error_failed(msg, + "Failed to get AVDTP session"); + + sink_setup_stream(dev->sink, session); + avdtp_unref(session); + } + + /* The previous calls should cause a call to the state callback to + * indicate AUDIO_STATE_CONNECTING */ + if (priv->state != AUDIO_STATE_CONNECTING) + return btd_error_failed(msg, "Connect Failed"); + + priv->conn_req = dbus_message_ref(msg); + + return NULL; +} + +static DBusMessage *dev_disconnect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *dev = data; + struct dev_priv *priv = dev->priv; + + if (priv->state == AUDIO_STATE_DISCONNECTED) + return btd_error_not_connected(msg); + + if (priv->dc_req) + return dbus_message_new_method_return(msg); + + priv->dc_req = dbus_message_ref(msg); + + if (dev->control) { + device_remove_control_timer(dev); + avrcp_disconnect(dev); + } + + if (dev->sink && priv->sink_state != SINK_STATE_DISCONNECTED) + sink_shutdown(dev->sink); + else if (priv->hs_state != HEADSET_STATE_DISCONNECTED) + headset_shutdown(dev); + else { + dbus_message_unref(priv->dc_req); + priv->dc_req = NULL; + return dbus_message_new_method_return(msg); + } + + return NULL; +} + +static DBusMessage *dev_get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *state; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* State */ + state = state2str(device->priv->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static GDBusMethodTable dev_methods[] = { + { "Connect", "", "", dev_connect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "Disconnect", "", "", dev_disconnect }, + { "GetProperties", "", "a{sv}",dev_get_properties }, + { NULL, NULL, NULL, NULL } +}; + +static GDBusSignalTable dev_signals[] = { + { "PropertyChanged", "sv" }, + { NULL, NULL } +}; + +struct audio_device *audio_device_register(DBusConnection *conn, + struct btd_device *device, + const char *path, const bdaddr_t *src, + const bdaddr_t *dst) +{ + struct audio_device *dev; + + if (!conn || !path) + return NULL; + + dev = g_new0(struct audio_device, 1); + + dev->btd_dev = btd_device_ref(device); + dev->path = g_strdup(path); + bacpy(&dev->dst, dst); + bacpy(&dev->src, src); + dev->conn = dbus_connection_ref(conn); + dev->priv = g_new0(struct dev_priv, 1); + dev->priv->state = AUDIO_STATE_DISCONNECTED; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_INTERFACE, + dev_methods, dev_signals, NULL, + dev, NULL)) { + error("Unable to register %s on %s", AUDIO_INTERFACE, + dev->path); + device_free(dev); + return NULL; + } + + DBG("Registered interface %s on path %s", AUDIO_INTERFACE, + dev->path); + + if (sink_callback_id == 0) + sink_callback_id = sink_add_state_cb(device_sink_cb, NULL); + + if (avdtp_callback_id == 0) + avdtp_callback_id = avdtp_add_state_cb(device_avdtp_cb, NULL); + if (avctp_callback_id == 0) + avctp_callback_id = avctp_add_state_cb(device_avctp_cb, NULL); + + if (headset_callback_id == 0) + headset_callback_id = headset_add_state_cb(device_headset_cb, + NULL); + + return dev; +} + +gboolean audio_device_is_active(struct audio_device *dev, + const char *interface) +{ + if (!interface) { + if ((dev->sink || dev->source) && + avdtp_is_connected(&dev->src, &dev->dst)) + return TRUE; + if (dev->headset && headset_is_active(dev)) + return TRUE; + } else if (!strcmp(interface, AUDIO_SINK_INTERFACE) && dev->sink && + avdtp_is_connected(&dev->src, &dev->dst)) + return TRUE; + else if (!strcmp(interface, AUDIO_SOURCE_INTERFACE) && dev->source && + avdtp_is_connected(&dev->src, &dev->dst)) + return TRUE; + else if (!strcmp(interface, AUDIO_HEADSET_INTERFACE) && dev->headset && + headset_is_active(dev)) + return TRUE; + else if (!strcmp(interface, AUDIO_CONTROL_INTERFACE) && dev->control && + control_is_active(dev)) + return TRUE; + else if (!strcmp(interface, AUDIO_GATEWAY_INTERFACE) && dev->gateway && + gateway_is_connected(dev)) + return TRUE; + + return FALSE; +} + +void audio_device_unregister(struct audio_device *device) +{ + unix_device_removed(device); + + if (device->hs_preauth_id) { + g_source_remove(device->hs_preauth_id); + device->hs_preauth_id = 0; + } + + if (device->headset) + headset_unregister(device); + + if (device->gateway) + gateway_unregister(device); + + if (device->sink) + sink_unregister(device); + + if (device->source) + source_unregister(device); + + if (device->control) + control_unregister(device); + + g_dbus_unregister_interface(device->conn, device->path, + AUDIO_INTERFACE); + + device_free(device); +} + +static void auth_cb(DBusError *derr, void *user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + if (derr == NULL) + priv->authorized = TRUE; + + while (priv->auths) { + struct service_auth *auth = priv->auths->data; + + auth->cb(derr, auth->user_data); + priv->auths = g_slist_remove(priv->auths, auth); + g_free(auth); + } +} + +static gboolean auth_idle_cb(gpointer user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + priv->auth_idle_id = 0; + + auth_cb(NULL, dev); + + return FALSE; +} + +static gboolean audio_device_is_connected(struct audio_device *dev) +{ + if (dev->headset) { + headset_state_t state = headset_get_state(dev); + + if (state == HEADSET_STATE_CONNECTED || + state == HEADSET_STATE_PLAY_IN_PROGRESS || + state == HEADSET_STATE_PLAYING) + return TRUE; + } + + if (dev->sink) { + sink_state_t state = sink_get_state(dev); + + if (state == SINK_STATE_CONNECTED || + state == SINK_STATE_PLAYING) + return TRUE; + } + + if (dev->source) { + source_state_t state = source_get_state(dev); + + if (state == SOURCE_STATE_CONNECTED || + state == SOURCE_STATE_PLAYING) + return TRUE; + } + + return FALSE; +} + +int audio_device_request_authorization(struct audio_device *dev, + const char *uuid, service_auth_cb cb, + void *user_data) +{ + struct dev_priv *priv = dev->priv; + struct service_auth *auth; + int err; + + auth = g_try_new0(struct service_auth, 1); + if (!auth) + return -ENOMEM; + + auth->cb = cb; + auth->user_data = user_data; + + priv->auths = g_slist_append(priv->auths, auth); + if (g_slist_length(priv->auths) > 1) + return 0; + + if (priv->authorized || audio_device_is_connected(dev)) { + priv->auth_idle_id = g_idle_add(auth_idle_cb, dev); + return 0; + } + + err = btd_request_authorization(&dev->src, &dev->dst, uuid, auth_cb, + dev); + if (err < 0) { + priv->auths = g_slist_remove(priv->auths, auth); + g_free(auth); + } + + return err; +} + +int audio_device_cancel_authorization(struct audio_device *dev, + authorization_cb cb, void *user_data) +{ + struct dev_priv *priv = dev->priv; + GSList *l, *next; + + for (l = priv->auths; l != NULL; l = next) { + struct service_auth *auth = l->data; + + next = g_slist_next(l); + + if (cb && auth->cb != cb) + continue; + + if (user_data && auth->user_data != user_data) + continue; + + priv->auths = g_slist_remove(priv->auths, auth); + g_free(auth); + } + + if (g_slist_length(priv->auths) == 0) { + if (priv->auth_idle_id > 0) { + g_source_remove(priv->auth_idle_id); + priv->auth_idle_id = 0; + } else + btd_cancel_authorization(&dev->src, &dev->dst); + } + + return 0; +} + +void audio_device_set_authorized(struct audio_device *dev, gboolean auth) +{ + struct dev_priv *priv = dev->priv; + + priv->authorized = auth; +} diff --git a/audio/device.h b/audio/device.h new file mode 100644 index 0000000..35af788 --- /dev/null +++ b/audio/device.h @@ -0,0 +1,94 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define GENERIC_AUDIO_UUID "00001203-0000-1000-8000-00805f9b34fb" + +#define HSP_HS_UUID "00001108-0000-1000-8000-00805f9b34fb" +#define HSP_AG_UUID "00001112-0000-1000-8000-00805f9b34fb" + +#define HFP_HS_UUID "0000111e-0000-1000-8000-00805f9b34fb" +#define HFP_AG_UUID "0000111f-0000-1000-8000-00805f9b34fb" + +#define ADVANCED_AUDIO_UUID "0000110d-0000-1000-8000-00805f9b34fb" + +#define A2DP_SOURCE_UUID "0000110a-0000-1000-8000-00805f9b34fb" +#define A2DP_SINK_UUID "0000110b-0000-1000-8000-00805f9b34fb" + +#define AVRCP_REMOTE_UUID "0000110e-0000-1000-8000-00805f9b34fb" +#define AVRCP_TARGET_UUID "0000110c-0000-1000-8000-00805f9b34fb" + +/* Move these to respective .h files once they exist */ +#define AUDIO_SOURCE_INTERFACE "org.bluez.AudioSource" +#define AUDIO_CONTROL_INTERFACE "org.bluez.Control" + +struct source; +struct control; +struct target; +struct sink; +struct headset; +struct gateway; +struct dev_priv; + +struct audio_device { + struct btd_device *btd_dev; + + DBusConnection *conn; + char *path; + bdaddr_t src; + bdaddr_t dst; + + gboolean auto_connect; + + struct headset *headset; + struct gateway *gateway; + struct sink *sink; + struct source *source; + struct control *control; + struct target *target; + + guint hs_preauth_id; + + struct dev_priv *priv; +}; + +struct audio_device *audio_device_register(DBusConnection *conn, + struct btd_device *device, + const char *path, const bdaddr_t *src, + const bdaddr_t *dst); + +void audio_device_unregister(struct audio_device *device); + +gboolean audio_device_is_active(struct audio_device *dev, + const char *interface); + +typedef void (*authorization_cb) (DBusError *derr, void *user_data); + +int audio_device_cancel_authorization(struct audio_device *dev, + authorization_cb cb, void *user_data); + +int audio_device_request_authorization(struct audio_device *dev, + const char *uuid, authorization_cb cb, + void *user_data); + +void audio_device_set_authorized(struct audio_device *dev, gboolean auth); diff --git a/audio/gateway.c b/audio/gateway.c new file mode 100644 index 0000000..ec0ec5d --- /dev/null +++ b/audio/gateway.c @@ -0,0 +1,711 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * Copyright (C) 2008-2009 Leonid Movshovich <event.riga@gmail.org> + * Copyright (C) 2010 ProFUSION embedded systems + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <fcntl.h> +#include <errno.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include "glib-helper.h" +#include "device.h" +#include "gateway.h" +#include "log.h" +#include "error.h" +#include "btio.h" +#include "dbus-common.h" + +#ifndef DBUS_TYPE_UNIX_FD +#define DBUS_TYPE_UNIX_FD -1 +#endif + +struct hf_agent { + char *name; /* Bus id */ + char *path; /* D-Bus path */ + guint watch; /* Disconnect watch */ +}; + +struct gateway { + gateway_state_t state; + GIOChannel *rfcomm; + GIOChannel *sco; + gateway_stream_cb_t sco_start_cb; + void *sco_start_cb_data; + struct hf_agent *agent; + DBusMessage *msg; +}; + +int gateway_close(struct audio_device *device); + +static const char *state2str(gateway_state_t state) +{ + switch (state) { + case GATEWAY_STATE_DISCONNECTED: + return "disconnected"; + case GATEWAY_STATE_CONNECTING: + return "connecting"; + case GATEWAY_STATE_CONNECTED: + return "connected"; + case GATEWAY_STATE_PLAYING: + return "playing"; + default: + return ""; + } +} + +static void agent_free(struct hf_agent *agent) +{ + if (!agent) + return; + + g_free(agent->name); + g_free(agent->path); + g_free(agent); +} + +static void change_state(struct audio_device *dev, gateway_state_t new_state) +{ + struct gateway *gw = dev->gateway; + const char *val; + + if (gw->state == new_state) + return; + + val = state2str(new_state); + gw->state = new_state; + + emit_property_changed(dev->conn, dev->path, + AUDIO_GATEWAY_INTERFACE, "State", + DBUS_TYPE_STRING, &val); +} + +static void agent_disconnect(struct audio_device *dev, struct hf_agent *agent) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(agent->name, agent->path, + "org.bluez.HandsfreeAgent", "Release"); + + g_dbus_send_message(dev->conn, msg); +} + +static gboolean agent_sendfd(struct hf_agent *agent, int fd, + DBusPendingCallNotifyFunction notify, void *data) +{ + struct audio_device *dev = data; + DBusMessage *msg; + DBusPendingCall *call; + + msg = dbus_message_new_method_call(agent->name, agent->path, + "org.bluez.HandsfreeAgent", "NewConnection"); + + dbus_message_append_args(msg, DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_INVALID); + + if (dbus_connection_send_with_reply(dev->conn, msg, &call, -1) == FALSE) + return FALSE; + + dbus_pending_call_set_notify(call, notify, dev, NULL); + dbus_pending_call_unref(call); + + return TRUE; +} + +static gboolean sco_io_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_ERR | G_IO_HUP)) { + DBG("sco connection is released"); + g_io_channel_shutdown(gw->sco, TRUE, NULL); + g_io_channel_unref(gw->sco); + gw->sco = NULL; + change_state(dev, GATEWAY_STATE_CONNECTED); + return FALSE; + } + + return TRUE; +} + +static void sco_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + struct audio_device *dev = (struct audio_device *) user_data; + struct gateway *gw = dev->gateway; + + DBG("at the begin of sco_connect_cb() in gateway.c"); + + gw->sco = g_io_channel_ref(chan); + + if (gw->sco_start_cb) + gw->sco_start_cb(dev, err, gw->sco_start_cb_data); + + if (err) { + error("sco_connect_cb(): %s", err->message); + gateway_close(dev); + return; + } + + g_io_add_watch(gw->sco, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) sco_io_cb, dev); +} + +static void newconnection_reply(DBusPendingCall *call, void *data) +{ + struct audio_device *dev = data; + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + + if (!dev->gateway->rfcomm) { + DBG("RFCOMM disconnected from server before agent reply"); + goto done; + } + + dbus_error_init(&derr); + if (!dbus_set_error_from_message(&derr, reply)) { + DBG("Agent reply: file descriptor passed successfully"); + change_state(dev, GATEWAY_STATE_CONNECTED); + goto done; + } + + DBG("Agent reply: %s", derr.message); + + dbus_error_free(&derr); + gateway_close(dev); + +done: + dbus_message_unref(reply); +} + +static void rfcomm_connect_cb(GIOChannel *chan, GError *err, + gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + DBusMessage *reply; + int sk, ret; + + if (err) { + error("connect(): %s", err->message); + if (gw->sco_start_cb) + gw->sco_start_cb(dev, err, gw->sco_start_cb_data); + goto fail; + } + + if (!gw->agent) { + error("Handsfree Agent not registered"); + goto fail; + } + + sk = g_io_channel_unix_get_fd(chan); + + gw->rfcomm = g_io_channel_ref(chan); + + ret = agent_sendfd(gw->agent, sk, newconnection_reply, dev); + + if (!gw->msg) + return; + + if (ret) + reply = dbus_message_new_method_return(gw->msg); + else + reply = btd_error_failed(gw->msg, "Can't pass file descriptor"); + + g_dbus_send_message(dev->conn, reply); + + return; + +fail: + if (gw->msg) { + DBusMessage *reply; + reply = btd_error_failed(gw->msg, "Connect failed"); + g_dbus_send_message(dev->conn, reply); + } + + change_state(dev, GATEWAY_STATE_DISCONNECTED); +} + +static void get_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + int ch; + sdp_list_t *protos, *classes; + uuid_t uuid; + GIOChannel *io; + GError *gerr = NULL; + + if (err < 0) { + error("Unable to get service record: %s (%d)", strerror(-err), + -err); + goto fail; + } + + if (!recs || !recs->data) { + error("No records found"); + err = -EIO; + goto fail; + } + + if (sdp_get_service_classes(recs->data, &classes) < 0) { + error("Unable to get service classes from record"); + err = -EINVAL; + goto fail; + } + + if (sdp_get_access_protos(recs->data, &protos) < 0) { + error("Unable to get access protocols from record"); + err = -ENODATA; + goto fail; + } + + memcpy(&uuid, classes->data, sizeof(uuid)); + sdp_list_free(classes, free); + + if (!sdp_uuid128_to_uuid(&uuid) || uuid.type != SDP_UUID16 || + uuid.value.uuid16 != HANDSFREE_AGW_SVCLASS_ID) { + sdp_list_free(protos, NULL); + error("Invalid service record or not HFP"); + err = -EIO; + goto fail; + } + + ch = sdp_get_proto_port(protos, RFCOMM_UUID); + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + if (ch <= 0) { + error("Unable to extract RFCOMM channel from service record"); + err = -EIO; + goto fail; + } + + io = bt_io_connect(BT_IO_RFCOMM, rfcomm_connect_cb, dev, NULL, &gerr, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_CHANNEL, ch, + BT_IO_OPT_INVALID); + if (!io) { + error("Unable to connect: %s", gerr->message); + gateway_close(dev); + goto fail; + } + + g_io_channel_unref(io); + + change_state(dev, GATEWAY_STATE_CONNECTING); + return; + +fail: + if (gw->msg) { + DBusMessage *reply = btd_error_failed(gw->msg, + gerr ? gerr->message : strerror(-err)); + g_dbus_send_message(dev->conn, reply); + } + + change_state(dev, GATEWAY_STATE_DISCONNECTED); + + if (!gerr) + g_set_error(&gerr, BT_IO_ERROR, BT_IO_ERROR_FAILED, + "connect: %s (%d)", strerror(-err), -err); + + if (gw->sco_start_cb) + gw->sco_start_cb(dev, gerr, gw->sco_start_cb_data); + + g_error_free(gerr); +} + +static int get_records(struct audio_device *device) +{ + uuid_t uuid; + + sdp_uuid16_create(&uuid, HANDSFREE_AGW_SVCLASS_ID); + return bt_search_service(&device->src, &device->dst, &uuid, + get_record_cb, device, NULL); +} + +static DBusMessage *ag_connect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *au_dev = (struct audio_device *) data; + struct gateway *gw = au_dev->gateway; + int err; + + if (!gw->agent) + return btd_error_agent_not_available(msg); + + err = get_records(au_dev); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + gw->msg = dbus_message_ref(msg); + + return NULL; +} + +int gateway_close(struct audio_device *device) +{ + struct gateway *gw = device->gateway; + int sock; + + if (gw->rfcomm) { + sock = g_io_channel_unix_get_fd(gw->rfcomm); + shutdown(sock, SHUT_RDWR); + + g_io_channel_shutdown(gw->rfcomm, TRUE, NULL); + g_io_channel_unref(gw->rfcomm); + gw->rfcomm = NULL; + } + + if (gw->sco) { + g_io_channel_shutdown(gw->sco, TRUE, NULL); + g_io_channel_unref(gw->sco); + gw->sco = NULL; + gw->sco_start_cb = NULL; + gw->sco_start_cb_data = NULL; + } + + change_state(device, GATEWAY_STATE_DISCONNECTED); + + return 0; +} + +static DBusMessage *ag_disconnect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + DBusMessage *reply = NULL; + char gw_addr[18]; + + if (!device->conn) + return NULL; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (!gw->rfcomm) + return btd_error_not_connected(msg); + + gateway_close(device); + ba2str(&device->dst, gw_addr); + DBG("Disconnected from %s, %s", gw_addr, device->path); + + return reply; +} + +static void agent_exited(DBusConnection *conn, void *data) +{ + struct gateway *gateway = data; + struct hf_agent *agent = gateway->agent; + + DBG("Agent %s exited", agent->name); + + agent_free(agent); + gateway->agent = NULL; +} + +static DBusMessage *ag_get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *value; + + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + value = state2str(gw->state); + dict_append_entry(&dict, "State", + DBUS_TYPE_STRING, &value); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static DBusMessage *register_agent(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + struct hf_agent *agent; + const char *path, *name; + + if (gw->agent) + return btd_error_already_exists(msg); + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + name = dbus_message_get_sender(msg); + agent = g_new0(struct hf_agent, 1); + + agent->name = g_strdup(name); + agent->path = g_strdup(path); + + agent->watch = g_dbus_add_disconnect_watch(conn, name, + agent_exited, gw, NULL); + + gw->agent = agent; + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *unregister_agent(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + const char *path; + + if (!gw->agent) + goto done; + + if (strcmp(gw->agent->name, dbus_message_get_sender(msg)) != 0) + return btd_error_not_authorized(msg); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + if (strcmp(gw->agent->path, path) != 0) + return btd_error_does_not_exist(msg); + + g_dbus_remove_watch(device->conn, gw->agent->watch); + + agent_free(gw->agent); + gw->agent = NULL; + +done: + return dbus_message_new_method_return(msg); +} + +static GDBusMethodTable gateway_methods[] = { + { "Connect", "", "", ag_connect, G_DBUS_METHOD_FLAG_ASYNC }, + { "Disconnect", "", "", ag_disconnect, G_DBUS_METHOD_FLAG_ASYNC }, + { "GetProperties", "", "a{sv}", ag_get_properties }, + { "RegisterAgent", "o", "", register_agent }, + { "UnregisterAgent", "o", "", unregister_agent }, + { NULL, NULL, NULL, NULL } +}; + +static GDBusSignalTable gateway_signals[] = { + { "PropertyChanged", "sv" }, + { NULL, NULL } +}; + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + + DBG("Unregistered interface %s on path %s", + AUDIO_GATEWAY_INTERFACE, dev->path); + + gateway_close(dev); + + g_free(dev->gateway); + dev->gateway = NULL; +} + +void gateway_unregister(struct audio_device *dev) +{ + if (dev->gateway->agent) + agent_disconnect(dev, dev->gateway->agent); + + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_GATEWAY_INTERFACE); +} + +struct gateway *gateway_init(struct audio_device *dev) +{ + if (DBUS_TYPE_UNIX_FD < 0) + return NULL; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_GATEWAY_INTERFACE, + gateway_methods, gateway_signals, + NULL, dev, path_unregister)) + return NULL; + + return g_new0(struct gateway, 1); + +} + +gboolean gateway_is_connected(struct audio_device *dev) +{ + return (dev && dev->gateway && + dev->gateway->state == GATEWAY_STATE_CONNECTED); +} + +int gateway_connect_rfcomm(struct audio_device *dev, GIOChannel *io) +{ + if (!io) + return -EINVAL; + + dev->gateway->rfcomm = g_io_channel_ref(io); + + return 0; +} + +int gateway_connect_sco(struct audio_device *dev, GIOChannel *io) +{ + struct gateway *gw = dev->gateway; + + if (gw->sco) + return -EISCONN; + + gw->sco = g_io_channel_ref(io); + + g_io_add_watch(gw->sco, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) sco_io_cb, dev); + + change_state(dev, GATEWAY_STATE_PLAYING); + + return 0; +} + +void gateway_start_service(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + GError *err = NULL; + + if (gw->rfcomm == NULL) + return; + + if (!bt_io_accept(gw->rfcomm, rfcomm_connect_cb, dev, NULL, &err)) { + error("bt_io_accept: %s", err->message); + g_error_free(err); + } +} + +/* These are functions to be called from unix.c for audio system + * ifaces (alsa, gstreamer, etc.) */ +gboolean gateway_request_stream(struct audio_device *dev, + gateway_stream_cb_t cb, void *user_data) +{ + struct gateway *gw = dev->gateway; + GError *err = NULL; + GIOChannel *io; + + if (!gw->rfcomm) { + gw->sco_start_cb = cb; + gw->sco_start_cb_data = user_data; + get_records(dev); + } else if (!gw->sco) { + gw->sco_start_cb = cb; + gw->sco_start_cb_data = user_data; + io = bt_io_connect(BT_IO_SCO, sco_connect_cb, dev, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return FALSE; + } + } else if (cb) + cb(dev, err, user_data); + + return TRUE; +} + +int gateway_config_stream(struct audio_device *dev, gateway_stream_cb_t sco_cb, + void *user_data) +{ + struct gateway *gw = dev->gateway; + + if (!gw->rfcomm) { + gw->sco_start_cb = sco_cb; + gw->sco_start_cb_data = user_data; + return get_records(dev); + } + + if (sco_cb) + sco_cb(dev, NULL, user_data); + + return 0; +} + +gboolean gateway_cancel_stream(struct audio_device *dev, unsigned int id) +{ + gateway_close(dev); + return TRUE; +} + +int gateway_get_sco_fd(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (!gw || !gw->sco) + return -1; + + return g_io_channel_unix_get_fd(gw->sco); +} + +void gateway_suspend_stream(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (!gw || !gw->sco) + return; + + g_io_channel_shutdown(gw->sco, TRUE, NULL); + g_io_channel_unref(gw->sco); + gw->sco = NULL; + gw->sco_start_cb = NULL; + gw->sco_start_cb_data = NULL; + change_state(dev, GATEWAY_STATE_CONNECTED); +} diff --git a/audio/gateway.h b/audio/gateway.h new file mode 100644 index 0000000..a45ef82 --- /dev/null +++ b/audio/gateway.h @@ -0,0 +1,51 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_GATEWAY_INTERFACE "org.bluez.HandsfreeGateway" + +#define DEFAULT_HFP_HS_CHANNEL 7 + +typedef enum { + GATEWAY_STATE_DISCONNECTED, + GATEWAY_STATE_CONNECTING, + GATEWAY_STATE_CONNECTED, + GATEWAY_STATE_PLAYING, +} gateway_state_t; + +typedef void (*gateway_stream_cb_t) (struct audio_device *dev, GError *err, + void *user_data); + +void gateway_unregister(struct audio_device *dev); +struct gateway *gateway_init(struct audio_device *device); +gboolean gateway_is_connected(struct audio_device *dev); +int gateway_connect_rfcomm(struct audio_device *dev, GIOChannel *io); +int gateway_connect_sco(struct audio_device *dev, GIOChannel *chan); +void gateway_start_service(struct audio_device *device); +gboolean gateway_request_stream(struct audio_device *dev, + gateway_stream_cb_t cb, void *user_data); +int gateway_config_stream(struct audio_device *dev, gateway_stream_cb_t cb, + void *user_data); +gboolean gateway_cancel_stream(struct audio_device *dev, unsigned int id); +int gateway_get_sco_fd(struct audio_device *dev); +void gateway_suspend_stream(struct audio_device *dev); diff --git a/audio/gsta2dpsink.c b/audio/gsta2dpsink.c new file mode 100644 index 0000000..930d14e --- /dev/null +++ b/audio/gsta2dpsink.c @@ -0,0 +1,730 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <unistd.h> +#include <pthread.h> + +#include "gstpragma.h" +#include "gsta2dpsink.h" + +GST_DEBUG_CATEGORY_STATIC(gst_a2dp_sink_debug); +#define GST_CAT_DEFAULT gst_a2dp_sink_debug + +#define A2DP_SBC_RTP_PAYLOAD_TYPE 1 +#define TEMPLATE_MAX_BITPOOL_STR "64" + +#define DEFAULT_AUTOCONNECT TRUE + +enum { + PROP_0, + PROP_DEVICE, + PROP_AUTOCONNECT, + PROP_TRANSPORT +}; + +GST_BOILERPLATE(GstA2dpSink, gst_a2dp_sink, GstBin, GST_TYPE_BIN); + +static const GstElementDetails gst_a2dp_sink_details = + GST_ELEMENT_DETAILS("Bluetooth A2DP sink", + "Sink/Audio", + "Plays audio to an A2DP device", + "Marcel Holtmann <marcel@holtmann.org>"); + +static GstStaticPadTemplate gst_a2dp_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ 2, " + TEMPLATE_MAX_BITPOOL_STR " ]; " + "audio/mpeg" + )); + +static gboolean gst_a2dp_sink_handle_event(GstPad *pad, GstEvent *event); +static gboolean gst_a2dp_sink_set_caps(GstPad *pad, GstCaps *caps); +static GstCaps *gst_a2dp_sink_get_caps(GstPad *pad); +static gboolean gst_a2dp_sink_init_caps_filter(GstA2dpSink *self); +static gboolean gst_a2dp_sink_init_fakesink(GstA2dpSink *self); +static gboolean gst_a2dp_sink_remove_fakesink(GstA2dpSink *self); + +static void gst_a2dp_sink_finalize(GObject *obj) +{ + GstA2dpSink *self = GST_A2DP_SINK(obj); + + g_mutex_free(self->cb_mutex); + + G_OBJECT_CLASS(parent_class)->finalize(obj); +} + +static GstState gst_a2dp_sink_get_state(GstA2dpSink *self) +{ + GstState current, pending; + + gst_element_get_state(GST_ELEMENT(self), ¤t, &pending, 0); + if (pending == GST_STATE_VOID_PENDING) + return current; + + return pending; +} + +/* + * Helper function to create elements, add to the bin and link it + * to another element. + */ +static GstElement *gst_a2dp_sink_init_element(GstA2dpSink *self, + const gchar *elementname, const gchar *name, + GstElement *link_to) +{ + GstElement *element; + GstState state; + + GST_LOG_OBJECT(self, "Initializing %s", elementname); + + element = gst_element_factory_make(elementname, name); + if (element == NULL) { + GST_DEBUG_OBJECT(self, "Couldn't create %s", elementname); + return NULL; + } + + if (!gst_bin_add(GST_BIN(self), element)) { + GST_DEBUG_OBJECT(self, "failed to add %s to the bin", + elementname); + goto cleanup_and_fail; + } + + state = gst_a2dp_sink_get_state(self); + if (gst_element_set_state(element, state) == + GST_STATE_CHANGE_FAILURE) { + GST_DEBUG_OBJECT(self, "%s failed to go to playing", + elementname); + goto remove_element_and_fail; + } + + if (link_to != NULL) + if (!gst_element_link(link_to, element)) { + GST_DEBUG_OBJECT(self, "couldn't link %s", + elementname); + goto remove_element_and_fail; + } + + return element; + +remove_element_and_fail: + gst_element_set_state(element, GST_STATE_NULL); + gst_bin_remove(GST_BIN(self), element); + return NULL; + +cleanup_and_fail: + g_object_unref(G_OBJECT(element)); + + return NULL; +} + +static void gst_a2dp_sink_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_set_details(element_class, + &gst_a2dp_sink_details); + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_a2dp_sink_factory)); +} + +static void gst_a2dp_sink_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstA2dpSink *self = GST_A2DP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + if (self->sink != NULL) + gst_avdtp_sink_set_device(self->sink, + g_value_get_string(value)); + + if (self->device != NULL) + g_free(self->device); + self->device = g_value_dup_string(value); + break; + + case PROP_TRANSPORT: + if (self->sink != NULL) + gst_avdtp_sink_set_transport(self->sink, + g_value_get_string(value)); + + if (self->transport != NULL) + g_free(self->transport); + self->transport = g_value_dup_string(value); + break; + + case PROP_AUTOCONNECT: + self->autoconnect = g_value_get_boolean(value); + + if (self->sink != NULL) + g_object_set(G_OBJECT(self->sink), "auto-connect", + self->autoconnect, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_a2dp_sink_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstA2dpSink *self = GST_A2DP_SINK(object); + gchar *device, *transport; + + switch (prop_id) { + case PROP_DEVICE: + if (self->sink != NULL) { + device = gst_avdtp_sink_get_device(self->sink); + if (device != NULL) + g_value_take_string(value, device); + } + break; + case PROP_AUTOCONNECT: + if (self->sink != NULL) + g_object_get(G_OBJECT(self->sink), "auto-connect", + &self->autoconnect, NULL); + + g_value_set_boolean(value, self->autoconnect); + break; + case PROP_TRANSPORT: + if (self->sink != NULL) { + transport = gst_avdtp_sink_get_transport(self->sink); + if (transport != NULL) + g_value_take_string(value, transport); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static gboolean gst_a2dp_sink_init_ghost_pad(GstA2dpSink *self) +{ + GstPad *capsfilter_pad; + + /* we search for the capsfilter sinkpad */ + capsfilter_pad = gst_element_get_static_pad(self->capsfilter, "sink"); + + /* now we add a ghostpad */ + self->ghostpad = GST_GHOST_PAD(gst_ghost_pad_new("sink", + capsfilter_pad)); + g_object_unref(capsfilter_pad); + + /* the getcaps of our ghostpad must reflect the device caps */ + gst_pad_set_getcaps_function(GST_PAD(self->ghostpad), + gst_a2dp_sink_get_caps); + self->ghostpad_setcapsfunc = GST_PAD_SETCAPSFUNC(self->ghostpad); + gst_pad_set_setcaps_function(GST_PAD(self->ghostpad), + GST_DEBUG_FUNCPTR(gst_a2dp_sink_set_caps)); + + /* we need to handle events on our own and we also need the eventfunc + * of the ghostpad for forwarding calls */ + self->ghostpad_eventfunc = GST_PAD_EVENTFUNC(GST_PAD(self->ghostpad)); + gst_pad_set_event_function(GST_PAD(self->ghostpad), + gst_a2dp_sink_handle_event); + + if (!gst_element_add_pad(GST_ELEMENT(self), GST_PAD(self->ghostpad))) + GST_ERROR_OBJECT(self, "failed to add ghostpad"); + + return TRUE; +} + +static void gst_a2dp_sink_remove_dynamic_elements(GstA2dpSink *self) +{ + if (self->rtp) { + GST_LOG_OBJECT(self, "removing rtp element from the bin"); + if (!gst_bin_remove(GST_BIN(self), GST_ELEMENT(self->rtp))) + GST_WARNING_OBJECT(self, "failed to remove rtp " + "element from bin"); + else + self->rtp = NULL; + } +} + +static GstStateChangeReturn gst_a2dp_sink_change_state(GstElement *element, + GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + GstA2dpSink *self = GST_A2DP_SINK(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + self->taglist = gst_tag_list_new(); + + gst_a2dp_sink_init_fakesink(self); + break; + + case GST_STATE_CHANGE_NULL_TO_READY: + self->sink_is_in_bin = FALSE; + self->sink = GST_AVDTP_SINK(gst_element_factory_make( + "avdtpsink", "avdtpsink")); + if (self->sink == NULL) { + GST_WARNING_OBJECT(self, "failed to create avdtpsink"); + return GST_STATE_CHANGE_FAILURE; + } + + if (self->device != NULL) + gst_avdtp_sink_set_device(self->sink, + self->device); + + if (self->transport != NULL) + gst_avdtp_sink_set_transport(self->sink, + self->transport); + + g_object_set(G_OBJECT(self->sink), "auto-connect", + self->autoconnect, NULL); + + ret = gst_element_set_state(GST_ELEMENT(self->sink), + GST_STATE_READY); + break; + default: + break; + } + + if (ret == GST_STATE_CHANGE_FAILURE) + return ret; + + ret = GST_ELEMENT_CLASS(parent_class)->change_state(element, + transition); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + if (self->taglist) { + gst_tag_list_free(self->taglist); + self->taglist = NULL; + } + if (self->newseg_event != NULL) { + gst_event_unref(self->newseg_event); + self->newseg_event = NULL; + } + gst_a2dp_sink_remove_fakesink(self); + break; + + case GST_STATE_CHANGE_READY_TO_NULL: + if (self->sink_is_in_bin) { + if (!gst_bin_remove(GST_BIN(self), + GST_ELEMENT(self->sink))) + GST_WARNING_OBJECT(self, "Failed to remove " + "avdtpsink from bin"); + } else if (self->sink != NULL) { + gst_element_set_state(GST_ELEMENT(self->sink), + GST_STATE_NULL); + g_object_unref(G_OBJECT(self->sink)); + } + + self->sink = NULL; + + gst_a2dp_sink_remove_dynamic_elements(self); + break; + default: + break; + } + + return ret; +} + +static void gst_a2dp_sink_class_init(GstA2dpSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->set_property = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_get_property); + + object_class->finalize = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_finalize); + + element_class->change_state = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_change_state); + + g_object_class_install_property(object_class, PROP_DEVICE, + g_param_spec_string("device", "Device", + "Bluetooth remote device address", + NULL, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_AUTOCONNECT, + g_param_spec_boolean("auto-connect", "Auto-connect", + "Automatically attempt to connect to device", + DEFAULT_AUTOCONNECT, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_TRANSPORT, + g_param_spec_string("transport", "Transport", + "Use configured transport", + NULL, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(gst_a2dp_sink_debug, "a2dpsink", 0, + "A2DP sink element"); +} + +GstCaps *gst_a2dp_sink_get_device_caps(GstA2dpSink *self) +{ + return gst_avdtp_sink_get_device_caps(self->sink); +} + +static GstCaps *gst_a2dp_sink_get_caps(GstPad *pad) +{ + GstCaps *caps; + GstCaps *caps_aux; + GstA2dpSink *self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + + if (self->sink == NULL) { + GST_DEBUG_OBJECT(self, "a2dpsink isn't initialized " + "returning template caps"); + caps = gst_static_pad_template_get_caps( + &gst_a2dp_sink_factory); + } else { + GST_LOG_OBJECT(self, "Getting device caps"); + caps = gst_a2dp_sink_get_device_caps(self); + if (caps == NULL) + caps = gst_static_pad_template_get_caps( + &gst_a2dp_sink_factory); + } + caps_aux = gst_caps_copy(caps); + g_object_set(self->capsfilter, "caps", caps_aux, NULL); + gst_caps_unref(caps_aux); + return caps; +} + +static gboolean gst_a2dp_sink_init_avdtp_sink(GstA2dpSink *self) +{ + GstElement *sink; + + /* check if we don't need a new sink */ + if (self->sink_is_in_bin) + return TRUE; + + if (self->sink == NULL) + sink = gst_element_factory_make("avdtpsink", "avdtpsink"); + else + sink = GST_ELEMENT(self->sink); + + if (sink == NULL) { + GST_ERROR_OBJECT(self, "Couldn't create avdtpsink"); + return FALSE; + } + + if (!gst_bin_add(GST_BIN(self), sink)) { + GST_ERROR_OBJECT(self, "failed to add avdtpsink " + "to the bin"); + goto cleanup_and_fail; + } + + if (gst_element_set_state(sink, GST_STATE_READY) == + GST_STATE_CHANGE_FAILURE) { + GST_ERROR_OBJECT(self, "avdtpsink failed to go to ready"); + goto remove_element_and_fail; + } + + if (!gst_element_link(GST_ELEMENT(self->rtp), sink)) { + GST_ERROR_OBJECT(self, "couldn't link rtpsbcpay " + "to avdtpsink"); + goto remove_element_and_fail; + } + + self->sink = GST_AVDTP_SINK(sink); + self->sink_is_in_bin = TRUE; + g_object_set(G_OBJECT(self->sink), "device", self->device, NULL); + g_object_set(G_OBJECT(self->sink), "transport", self->transport, NULL); + + gst_element_set_state(sink, GST_STATE_PAUSED); + + return TRUE; + +remove_element_and_fail: + gst_element_set_state(sink, GST_STATE_NULL); + gst_bin_remove(GST_BIN(self), sink); + return FALSE; + +cleanup_and_fail: + if (sink != NULL) + g_object_unref(G_OBJECT(sink)); + + return FALSE; +} + +static gboolean gst_a2dp_sink_init_rtp_sbc_element(GstA2dpSink *self) +{ + GstElement *rtppay; + + /* if we already have a rtp, we don't need a new one */ + if (self->rtp != NULL) + return TRUE; + + rtppay = gst_a2dp_sink_init_element(self, "rtpsbcpay", "rtp", + self->capsfilter); + if (rtppay == NULL) + return FALSE; + + self->rtp = GST_BASE_RTP_PAYLOAD(rtppay); + g_object_set(G_OBJECT(self->rtp), "min-frames", -1, NULL); + + gst_element_set_state(rtppay, GST_STATE_PAUSED); + + return TRUE; +} + +static gboolean gst_a2dp_sink_init_rtp_mpeg_element(GstA2dpSink *self) +{ + GstElement *rtppay; + + /* check if we don't need a new rtp */ + if (self->rtp) + return TRUE; + + GST_LOG_OBJECT(self, "Initializing rtp mpeg element"); + /* if capsfilter is not created then we can't have our rtp element */ + if (self->capsfilter == NULL) + return FALSE; + + rtppay = gst_a2dp_sink_init_element(self, "rtpmpapay", "rtp", + self->capsfilter); + if (rtppay == NULL) + return FALSE; + + self->rtp = GST_BASE_RTP_PAYLOAD(rtppay); + + gst_element_set_state(rtppay, GST_STATE_PAUSED); + + return TRUE; +} + +static gboolean gst_a2dp_sink_init_dynamic_elements(GstA2dpSink *self, + GstCaps *caps) +{ + GstStructure *structure; + GstEvent *event; + GstPad *capsfilterpad; + gboolean crc; + gchar *mode = NULL; + + structure = gst_caps_get_structure(caps, 0); + + /* before everything we need to remove fakesink */ + gst_a2dp_sink_remove_fakesink(self); + + /* first, we need to create our rtp payloader */ + if (gst_structure_has_name(structure, "audio/x-sbc")) { + GST_LOG_OBJECT(self, "sbc media received"); + if (!gst_a2dp_sink_init_rtp_sbc_element(self)) + return FALSE; + } else if (gst_structure_has_name(structure, "audio/mpeg")) { + GST_LOG_OBJECT(self, "mp3 media received"); + if (!gst_a2dp_sink_init_rtp_mpeg_element(self)) + return FALSE; + } else { + GST_ERROR_OBJECT(self, "Unexpected media type"); + return FALSE; + } + + if (!gst_a2dp_sink_init_avdtp_sink(self)) + return FALSE; + + /* check if we should push the taglist FIXME should we push this? + * we can send the tags directly if needed */ + if (self->taglist != NULL && + gst_structure_has_name(structure, "audio/mpeg")) { + + event = gst_event_new_tag(self->taglist); + + /* send directly the crc */ + if (gst_tag_list_get_boolean(self->taglist, "has-crc", &crc)) + gst_avdtp_sink_set_crc(self->sink, crc); + + if (gst_tag_list_get_string(self->taglist, "channel-mode", + &mode)) + gst_avdtp_sink_set_channel_mode(self->sink, mode); + + capsfilterpad = gst_ghost_pad_get_target(self->ghostpad); + gst_pad_send_event(capsfilterpad, event); + self->taglist = NULL; + g_free(mode); + } + + if (!gst_avdtp_sink_set_device_caps(self->sink, caps)) + return FALSE; + + g_object_set(G_OBJECT(self->rtp), "mtu", + gst_avdtp_sink_get_link_mtu(self->sink), NULL); + + /* we forward our new segment here if we have one */ + if (self->newseg_event) { + gst_pad_send_event(GST_BASE_RTP_PAYLOAD_SINKPAD(self->rtp), + self->newseg_event); + self->newseg_event = NULL; + } + + return TRUE; +} + +static gboolean gst_a2dp_sink_set_caps(GstPad *pad, GstCaps *caps) +{ + GstA2dpSink *self; + + self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + GST_INFO_OBJECT(self, "setting caps"); + + /* now we know the caps */ + gst_a2dp_sink_init_dynamic_elements(self, caps); + + return self->ghostpad_setcapsfunc(GST_PAD(self->ghostpad), caps); +} + +/* used for catching newsegment events while we don't have a sink, for + * later forwarding it to the sink */ +static gboolean gst_a2dp_sink_handle_event(GstPad *pad, GstEvent *event) +{ + GstA2dpSink *self; + GstTagList *taglist = NULL; + GstObject *parent; + + self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + parent = gst_element_get_parent(GST_ELEMENT(self->sink)); + + if (GST_EVENT_TYPE(event) == GST_EVENT_NEWSEGMENT && + parent != GST_OBJECT_CAST(self)) { + if (self->newseg_event != NULL) + gst_event_unref(self->newseg_event); + self->newseg_event = gst_event_ref(event); + + } else if (GST_EVENT_TYPE(event) == GST_EVENT_TAG && + parent != GST_OBJECT_CAST(self)) { + if (self->taglist == NULL) + gst_event_parse_tag(event, &self->taglist); + else { + gst_event_parse_tag(event, &taglist); + gst_tag_list_insert(self->taglist, taglist, + GST_TAG_MERGE_REPLACE); + } + } + + if (parent != NULL) + gst_object_unref(GST_OBJECT(parent)); + + return self->ghostpad_eventfunc(GST_PAD(self->ghostpad), event); +} + +static gboolean gst_a2dp_sink_init_caps_filter(GstA2dpSink *self) +{ + GstElement *element; + + element = gst_element_factory_make("capsfilter", "filter"); + if (element == NULL) + goto failed; + + if (!gst_bin_add(GST_BIN(self), element)) + goto failed; + + self->capsfilter = element; + return TRUE; + +failed: + GST_ERROR_OBJECT(self, "Failed to initialize caps filter"); + return FALSE; +} + +static gboolean gst_a2dp_sink_init_fakesink(GstA2dpSink *self) +{ + if (self->fakesink != NULL) + return TRUE; + + g_mutex_lock(self->cb_mutex); + self->fakesink = gst_a2dp_sink_init_element(self, "fakesink", + "fakesink", self->capsfilter); + g_mutex_unlock(self->cb_mutex); + + if (!self->fakesink) + return FALSE; + + return TRUE; +} + +static gboolean gst_a2dp_sink_remove_fakesink(GstA2dpSink *self) +{ + g_mutex_lock(self->cb_mutex); + + if (self->fakesink != NULL) { + gst_element_set_locked_state(self->fakesink, TRUE); + gst_element_set_state(self->fakesink, GST_STATE_NULL); + + gst_bin_remove(GST_BIN(self), self->fakesink); + self->fakesink = NULL; + } + + g_mutex_unlock(self->cb_mutex); + + return TRUE; +} + +static void gst_a2dp_sink_init(GstA2dpSink *self, + GstA2dpSinkClass *klass) +{ + self->sink = NULL; + self->fakesink = NULL; + self->rtp = NULL; + self->device = NULL; + self->transport = NULL; + self->autoconnect = DEFAULT_AUTOCONNECT; + self->capsfilter = NULL; + self->newseg_event = NULL; + self->taglist = NULL; + self->ghostpad = NULL; + self->sink_is_in_bin = FALSE; + + self->cb_mutex = g_mutex_new(); + + /* we initialize our capsfilter */ + gst_a2dp_sink_init_caps_filter(self); + g_object_set(self->capsfilter, "caps", + gst_static_pad_template_get_caps(&gst_a2dp_sink_factory), + NULL); + + gst_a2dp_sink_init_fakesink(self); + + gst_a2dp_sink_init_ghost_pad(self); + +} + +gboolean gst_a2dp_sink_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "a2dpsink", + GST_RANK_MARGINAL, GST_TYPE_A2DP_SINK); +} + diff --git a/audio/gsta2dpsink.h b/audio/gsta2dpsink.h new file mode 100644 index 0000000..5f96a3e --- /dev/null +++ b/audio/gsta2dpsink.h @@ -0,0 +1,85 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifndef __GST_A2DP_SINK_H__ +#define __GST_A2DP_SINK_H__ + +#include <gst/gst.h> +#include <gst/rtp/gstbasertppayload.h> +#include "gstavdtpsink.h" + +G_BEGIN_DECLS + +#define GST_TYPE_A2DP_SINK \ + (gst_a2dp_sink_get_type()) +#define GST_A2DP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_A2DP_SINK,GstA2dpSink)) +#define GST_A2DP_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_A2DP_SINK,GstA2dpSinkClass)) +#define GST_IS_A2DP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_A2DP_SINK)) +#define GST_IS_A2DP_SINK_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_A2DP_SINK)) + +typedef struct _GstA2dpSink GstA2dpSink; +typedef struct _GstA2dpSinkClass GstA2dpSinkClass; + +struct _GstA2dpSink { + GstBin bin; + + GstBaseRTPPayload *rtp; + GstAvdtpSink *sink; + GstElement *capsfilter; + GstElement *fakesink; + + gchar *device; + gchar *transport; + gboolean autoconnect; + gboolean sink_is_in_bin; + + GstGhostPad *ghostpad; + GstPadSetCapsFunction ghostpad_setcapsfunc; + GstPadEventFunction ghostpad_eventfunc; + + GstEvent *newseg_event; + /* Store the tags received before the a2dpsender sink is created + * when it is created we forward this to it */ + GstTagList *taglist; + + GMutex *cb_mutex; +}; + +struct _GstA2dpSinkClass { + GstBinClass parent_class; +}; + +GType gst_a2dp_sink_get_type(void); + +gboolean gst_a2dp_sink_plugin_init (GstPlugin * plugin); + +GstCaps *gst_a2dp_sink_get_device_caps(GstA2dpSink *self); + +G_END_DECLS + +#endif + diff --git a/audio/gstavdtpsink.c b/audio/gstavdtpsink.c new file mode 100644 index 0000000..ce1365a --- /dev/null +++ b/audio/gstavdtpsink.c @@ -0,0 +1,2029 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <unistd.h> +#include <sys/un.h> +#include <sys/socket.h> +#include <fcntl.h> +#include <pthread.h> + +#include <netinet/in.h> + +#include <bluetooth/bluetooth.h> + +#include <gst/rtp/gstrtpbuffer.h> + +#include <dbus/dbus.h> + +#include "ipc.h" +#include "rtp.h" +#include "a2dp-codecs.h" + +#include "gstpragma.h" +#include "gstavdtpsink.h" + +GST_DEBUG_CATEGORY_STATIC(avdtp_sink_debug); +#define GST_CAT_DEFAULT avdtp_sink_debug + +#define BUFFER_SIZE 2048 +#define TEMPLATE_MAX_BITPOOL 64 +#define CRC_PROTECTED 1 +#define CRC_UNPROTECTED 0 + +#define DEFAULT_AUTOCONNECT TRUE + +#define GST_AVDTP_SINK_MUTEX_LOCK(s) G_STMT_START { \ + g_mutex_lock(s->sink_lock); \ + } G_STMT_END + +#define GST_AVDTP_SINK_MUTEX_UNLOCK(s) G_STMT_START { \ + g_mutex_unlock(s->sink_lock); \ + } G_STMT_END + +#ifndef DBUS_TYPE_UNIX_FD +#define DBUS_TYPE_UNIX_FD -1 +#endif + +struct bluetooth_data { + struct bt_get_capabilities_rsp *caps; /* Bluetooth device caps */ + guint link_mtu; + + DBusConnection *conn; + guint8 codec; /* Bluetooth transport configuration */ + gchar *uuid; + guint8 *config; + gint config_size; + + gchar buffer[BUFFER_SIZE]; /* Codec transfer buffer */ +}; + +#define IS_SBC(n) (strcmp((n), "audio/x-sbc") == 0) +#define IS_MPEG_AUDIO(n) (strcmp((n), "audio/mpeg") == 0) + +enum { + PROP_0, + PROP_DEVICE, + PROP_AUTOCONNECT, + PROP_TRANSPORT +}; + +GST_BOILERPLATE(GstAvdtpSink, gst_avdtp_sink, GstBaseSink, + GST_TYPE_BASE_SINK); + +static const GstElementDetails avdtp_sink_details = + GST_ELEMENT_DETAILS("Bluetooth AVDTP sink", + "Sink/Audio", + "Plays audio to an A2DP device", + "Marcel Holtmann <marcel@holtmann.org>"); + +static GstStaticPadTemplate avdtp_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("application/x-rtp, " + "media = (string) \"audio\"," + "payload = (int) " + GST_RTP_PAYLOAD_DYNAMIC_STRING ", " + "clock-rate = (int) { 16000, 32000, " + "44100, 48000 }, " + "encoding-name = (string) \"SBC\"; " + "application/x-rtp, " + "media = (string) \"audio\", " + "payload = (int) " + GST_RTP_PAYLOAD_MPA_STRING ", " + "clock-rate = (int) 90000; " + "application/x-rtp, " + "media = (string) \"audio\", " + "payload = (int) " + GST_RTP_PAYLOAD_DYNAMIC_STRING ", " + "clock-rate = (int) 90000, " + "encoding-name = (string) \"MPA\"" + )); + +static int gst_avdtp_sink_audioservice_send(GstAvdtpSink *self, + const bt_audio_msg_header_t *msg); +static int gst_avdtp_sink_audioservice_expect(GstAvdtpSink *self, + bt_audio_msg_header_t *outmsg, + guint8 expected_name); + + +static void gst_avdtp_sink_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&avdtp_sink_factory)); + + gst_element_class_set_details(element_class, &avdtp_sink_details); +} + +static void gst_avdtp_sink_transport_release(GstAvdtpSink *self) +{ + DBusMessage *msg; + const char *access_type = "w"; + + msg = dbus_message_new_method_call("org.bluez", self->transport, + "org.bluez.MediaTransport", + "Release"); + + dbus_message_append_args(msg, DBUS_TYPE_STRING, &access_type, + DBUS_TYPE_INVALID); + + dbus_connection_send(self->data->conn, msg, NULL); + + dbus_message_unref(msg); +} + +static gboolean gst_avdtp_sink_stop(GstBaseSink *basesink) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + + GST_INFO_OBJECT(self, "stop"); + + if (self->watch_id != 0) { + g_source_remove(self->watch_id); + self->watch_id = 0; + } + + if (self->server) { + bt_audio_service_close(g_io_channel_unix_get_fd(self->server)); + g_io_channel_unref(self->server); + self->server = NULL; + } + + if (self->stream) { + g_io_channel_shutdown(self->stream, TRUE, NULL); + g_io_channel_unref(self->stream); + self->stream = NULL; + } + + if (self->data) { + if (self->transport) + gst_avdtp_sink_transport_release(self); + if (self->data->conn) + dbus_connection_unref(self->data->conn); + g_free(self->data); + self->data = NULL; + } + + if (self->stream_caps) { + gst_caps_unref(self->stream_caps); + self->stream_caps = NULL; + } + + if (self->dev_caps) { + gst_caps_unref(self->dev_caps); + self->dev_caps = NULL; + } + + return TRUE; +} + +static void gst_avdtp_sink_finalize(GObject *object) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(object); + + if (self->data) + gst_avdtp_sink_stop(GST_BASE_SINK(self)); + + if (self->device) + g_free(self->device); + + if (self->transport) + g_free(self->transport); + + g_mutex_free(self->sink_lock); + + G_OBJECT_CLASS(parent_class)->finalize(object); +} + +static void gst_avdtp_sink_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstAvdtpSink *sink = GST_AVDTP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + if (sink->device) + g_free(sink->device); + sink->device = g_value_dup_string(value); + break; + + case PROP_AUTOCONNECT: + sink->autoconnect = g_value_get_boolean(value); + break; + + case PROP_TRANSPORT: + if (sink->transport) + g_free(sink->transport); + sink->transport = g_value_dup_string(value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_avdtp_sink_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstAvdtpSink *sink = GST_AVDTP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + g_value_set_string(value, sink->device); + break; + + case PROP_AUTOCONNECT: + g_value_set_boolean(value, sink->autoconnect); + break; + + case PROP_TRANSPORT: + g_value_set_string(value, sink->transport); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static gint gst_avdtp_sink_bluetooth_recvmsg_fd(GstAvdtpSink *sink) +{ + int err, ret; + + ret = bt_audio_service_get_data_fd( + g_io_channel_unix_get_fd(sink->server)); + + if (ret < 0) { + err = errno; + GST_ERROR_OBJECT(sink, "Unable to receive fd: %s (%d)", + strerror(err), err); + return -err; + } + + sink->stream = g_io_channel_unix_new(ret); + g_io_channel_set_encoding(sink->stream, NULL, NULL); + GST_DEBUG_OBJECT(sink, "stream_fd=%d", ret); + + return 0; +} + +static codec_capabilities_t *gst_avdtp_find_caps(GstAvdtpSink *sink, + uint8_t codec_type) +{ + struct bt_get_capabilities_rsp *rsp = sink->data->caps; + codec_capabilities_t *codec = (void *) rsp->data; + int bytes_left = rsp->h.length - sizeof(*rsp); + + while (bytes_left > 0) { + if ((codec->type == codec_type) && + !(codec->lock & BT_WRITE_LOCK)) + break; + + bytes_left -= codec->length; + codec = (void *) codec + codec->length; + } + + if (bytes_left <= 0) + return NULL; + + return codec; +} + +static gboolean gst_avdtp_sink_init_sbc_pkt_conf(GstAvdtpSink *sink, + GstCaps *caps, + sbc_capabilities_t *pkt) +{ + sbc_capabilities_t *cfg; + const GValue *value = NULL; + const char *pref, *name; + gint rate, subbands, blocks; + GstStructure *structure = gst_caps_get_structure(caps, 0); + + cfg = (void *) gst_avdtp_find_caps(sink, BT_A2DP_SBC_SINK); + name = gst_structure_get_name(structure); + + if (!(IS_SBC(name))) { + GST_ERROR_OBJECT(sink, "Unexpected format %s, " + "was expecting sbc", name); + return FALSE; + } + + value = gst_structure_get_value(structure, "rate"); + rate = g_value_get_int(value); + if (rate == 44100) + cfg->frequency = BT_SBC_SAMPLING_FREQ_44100; + else if (rate == 48000) + cfg->frequency = BT_SBC_SAMPLING_FREQ_48000; + else if (rate == 32000) + cfg->frequency = BT_SBC_SAMPLING_FREQ_32000; + else if (rate == 16000) + cfg->frequency = BT_SBC_SAMPLING_FREQ_16000; + else { + GST_ERROR_OBJECT(sink, "Invalid rate while setting caps"); + return FALSE; + } + + value = gst_structure_get_value(structure, "mode"); + pref = g_value_get_string(value); + if (strcmp(pref, "mono") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_MONO; + else if (strcmp(pref, "dual") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + else if (strcmp(pref, "stereo") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_STEREO; + else if (strcmp(pref, "joint") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + else { + GST_ERROR_OBJECT(sink, "Invalid mode %s", pref); + return FALSE; + } + + value = gst_structure_get_value(structure, "allocation"); + pref = g_value_get_string(value); + if (strcmp(pref, "loudness") == 0) + cfg->allocation_method = BT_A2DP_ALLOCATION_LOUDNESS; + else if (strcmp(pref, "snr") == 0) + cfg->allocation_method = BT_A2DP_ALLOCATION_SNR; + else { + GST_ERROR_OBJECT(sink, "Invalid allocation: %s", pref); + return FALSE; + } + + value = gst_structure_get_value(structure, "subbands"); + subbands = g_value_get_int(value); + if (subbands == 8) + cfg->subbands = BT_A2DP_SUBBANDS_8; + else if (subbands == 4) + cfg->subbands = BT_A2DP_SUBBANDS_4; + else { + GST_ERROR_OBJECT(sink, "Invalid subbands %d", subbands); + return FALSE; + } + + value = gst_structure_get_value(structure, "blocks"); + blocks = g_value_get_int(value); + if (blocks == 16) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_16; + else if (blocks == 12) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_12; + else if (blocks == 8) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_8; + else if (blocks == 4) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_4; + else { + GST_ERROR_OBJECT(sink, "Invalid blocks %d", blocks); + return FALSE; + } + + value = gst_structure_get_value(structure, "bitpool"); + cfg->max_bitpool = cfg->min_bitpool = g_value_get_int(value); + + memcpy(pkt, cfg, sizeof(*pkt)); + + return TRUE; +} + +static gboolean gst_avdtp_sink_conf_recv_stream_fd( + GstAvdtpSink *self) +{ + struct bluetooth_data *data = self->data; + gint ret; + GError *gerr = NULL; + GIOStatus status; + GIOFlags flags; + int fd; + + /* Proceed if stream was already acquired */ + if (self->stream != NULL) + goto proceed; + + ret = gst_avdtp_sink_bluetooth_recvmsg_fd(self); + if (ret < 0) + return FALSE; + + if (!self->stream) { + GST_ERROR_OBJECT(self, "Error while configuring device: " + "could not acquire audio socket"); + return FALSE; + } + +proceed: + /* set stream socket to nonblock */ + GST_LOG_OBJECT(self, "setting stream socket to nonblock"); + flags = g_io_channel_get_flags(self->stream); + flags |= G_IO_FLAG_NONBLOCK; + status = g_io_channel_set_flags(self->stream, flags, &gerr); + if (status != G_IO_STATUS_NORMAL) { + if (gerr) + GST_WARNING_OBJECT(self, "Error while " + "setting server socket to nonblock: " + "%s", gerr->message); + else + GST_WARNING_OBJECT(self, "Error while " + "setting server " + "socket to nonblock"); + } + + fd = g_io_channel_unix_get_fd(self->stream); + + /* It is possible there is some outstanding + data in the pipe - we have to empty it */ + GST_LOG_OBJECT(self, "emptying stream pipe"); + while (1) { + ssize_t bread = read(fd, data->buffer, data->link_mtu); + if (bread <= 0) + break; + } + + /* set stream socket to block */ + GST_LOG_OBJECT(self, "setting stream socket to block"); + flags = g_io_channel_get_flags(self->stream); + flags &= ~G_IO_FLAG_NONBLOCK; + status = g_io_channel_set_flags(self->stream, flags, &gerr); + if (status != G_IO_STATUS_NORMAL) { + if (gerr) + GST_WARNING_OBJECT(self, "Error while " + "setting server socket to block:" + "%s", gerr->message); + else + GST_WARNING_OBJECT(self, "Error while " + "setting server " + "socket to block"); + } + + memset(data->buffer, 0, sizeof(data->buffer)); + + return TRUE; +} + +static gboolean server_callback(GIOChannel *chan, + GIOCondition cond, gpointer data) +{ + if (cond & G_IO_HUP || cond & G_IO_NVAL) + return FALSE; + else if (cond & G_IO_ERR) + GST_WARNING_OBJECT(GST_AVDTP_SINK(data), + "Untreated callback G_IO_ERR"); + + return TRUE; +} + +static GstStructure *gst_avdtp_sink_parse_sbc_caps( + GstAvdtpSink *self, sbc_capabilities_t *sbc) +{ + GstStructure *structure; + GValue *value; + GValue *list; + gboolean mono, stereo; + + if (sbc == NULL) + return NULL; + + structure = gst_structure_empty_new("audio/x-sbc"); + value = g_value_init(g_new0(GValue, 1), G_TYPE_STRING); + + /* mode */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) { + g_value_set_static_string(value, "mono"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) { + g_value_set_static_string(value, "stereo"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) { + g_value_set_static_string(value, "dual"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) { + g_value_set_static_string(value, "joint"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "mode", list); + g_free(list); + list = NULL; + } + + /* subbands */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + value = g_value_init(value, G_TYPE_INT); + if (sbc->subbands & BT_A2DP_SUBBANDS_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + if (sbc->subbands & BT_A2DP_SUBBANDS_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "subbands", list); + g_free(list); + list = NULL; + } + + /* blocks */ + value = g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_16) { + g_value_set_int(value, 16); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_12) { + g_value_set_int(value, 12); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "blocks", list); + g_free(list); + list = NULL; + } + + /* allocation */ + g_value_init(value, G_TYPE_STRING); + list = g_value_init(g_new0(GValue,1), GST_TYPE_LIST); + if (sbc->allocation_method & BT_A2DP_ALLOCATION_LOUDNESS) { + g_value_set_static_string(value, "loudness"); + gst_value_list_prepend_value(list, value); + } + if (sbc->allocation_method & BT_A2DP_ALLOCATION_SNR) { + g_value_set_static_string(value, "snr"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "allocation", list); + g_free(list); + list = NULL; + } + + /* rate */ + g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* bitpool */ + value = g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, + MIN(sbc->min_bitpool, TEMPLATE_MAX_BITPOOL), + MIN(sbc->max_bitpool, TEMPLATE_MAX_BITPOOL)); + gst_structure_set_value(structure, "bitpool", value); + g_value_unset(value); + + /* channels */ + mono = FALSE; + stereo = FALSE; + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static GstStructure *gst_avdtp_sink_parse_mpeg_caps( + GstAvdtpSink *self, mpeg_capabilities_t *mpeg) +{ + GstStructure *structure; + GValue *value; + GValue *list; + gboolean valid_layer = FALSE; + gboolean mono, stereo; + + if (!mpeg) + return NULL; + + GST_LOG_OBJECT(self, "parsing mpeg caps"); + + structure = gst_structure_empty_new("audio/mpeg"); + value = g_new0(GValue, 1); + g_value_init(value, G_TYPE_INT); + + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + gst_structure_set_value(structure, "mpegversion", list); + g_free(list); + + /* layer */ + GST_LOG_OBJECT(self, "setting mpeg layer"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->layer & BT_MPEG_LAYER_1) { + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_2) { + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_3) { + g_value_set_int(value, 3); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (list) { + gst_structure_set_value(structure, "layer", list); + g_free(list); + list = NULL; + } + + if (!valid_layer) { + gst_structure_free(structure); + g_free(value); + return NULL; + } + + /* rate */ + GST_LOG_OBJECT(self, "setting mpeg rate"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_24000) { + g_value_set_int(value, 24000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_22050) { + g_value_set_int(value, 22050); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* channels */ + GST_LOG_OBJECT(self, "setting mpeg channels"); + mono = FALSE; + stereo = FALSE; + if (mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static GstStructure *gst_avdtp_sink_parse_sbc_raw(GstAvdtpSink *self) +{ + a2dp_sbc_t *sbc = (a2dp_sbc_t *) self->data->config; + GstStructure *structure; + GValue *value; + GValue *list; + gboolean mono, stereo; + + structure = gst_structure_empty_new("audio/x-sbc"); + value = g_value_init(g_new0(GValue, 1), G_TYPE_STRING); + + /* mode */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) { + g_value_set_static_string(value, "mono"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) { + g_value_set_static_string(value, "stereo"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) { + g_value_set_static_string(value, "dual"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) { + g_value_set_static_string(value, "joint"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "mode", list); + g_free(list); + list = NULL; + } + + /* subbands */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + value = g_value_init(value, G_TYPE_INT); + if (sbc->subbands & BT_A2DP_SUBBANDS_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + if (sbc->subbands & BT_A2DP_SUBBANDS_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "subbands", list); + g_free(list); + list = NULL; + } + + /* blocks */ + value = g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_16) { + g_value_set_int(value, 16); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_12) { + g_value_set_int(value, 12); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "blocks", list); + g_free(list); + list = NULL; + } + + /* allocation */ + g_value_init(value, G_TYPE_STRING); + list = g_value_init(g_new0(GValue,1), GST_TYPE_LIST); + if (sbc->allocation_method & BT_A2DP_ALLOCATION_LOUDNESS) { + g_value_set_static_string(value, "loudness"); + gst_value_list_prepend_value(list, value); + } + if (sbc->allocation_method & BT_A2DP_ALLOCATION_SNR) { + g_value_set_static_string(value, "snr"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "allocation", list); + g_free(list); + list = NULL; + } + + /* rate */ + g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* bitpool */ + value = g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, + MIN(sbc->min_bitpool, TEMPLATE_MAX_BITPOOL), + MIN(sbc->max_bitpool, TEMPLATE_MAX_BITPOOL)); + gst_structure_set_value(structure, "bitpool", value); + g_value_unset(value); + + /* channels */ + mono = FALSE; + stereo = FALSE; + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static GstStructure *gst_avdtp_sink_parse_mpeg_raw(GstAvdtpSink *self) +{ + a2dp_mpeg_t *mpeg = (a2dp_mpeg_t *) self->data->config; + GstStructure *structure; + GValue *value; + GValue *list; + gboolean valid_layer = FALSE; + gboolean mono, stereo; + + GST_LOG_OBJECT(self, "parsing mpeg caps"); + + structure = gst_structure_empty_new("audio/mpeg"); + value = g_new0(GValue, 1); + g_value_init(value, G_TYPE_INT); + + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + gst_structure_set_value(structure, "mpegversion", list); + g_free(list); + + /* layer */ + GST_LOG_OBJECT(self, "setting mpeg layer"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->layer & BT_MPEG_LAYER_1) { + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_2) { + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_3) { + g_value_set_int(value, 3); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (list) { + gst_structure_set_value(structure, "layer", list); + g_free(list); + list = NULL; + } + + if (!valid_layer) { + gst_structure_free(structure); + g_free(value); + return NULL; + } + + /* rate */ + GST_LOG_OBJECT(self, "setting mpeg rate"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_24000) { + g_value_set_int(value, 24000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_22050) { + g_value_set_int(value, 22050); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* channels */ + GST_LOG_OBJECT(self, "setting mpeg channels"); + mono = FALSE; + stereo = FALSE; + if (mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static gboolean gst_avdtp_sink_update_config(GstAvdtpSink *self) +{ + GstStructure *structure; + gchar *tmp; + + switch (self->data->codec) { + case A2DP_CODEC_SBC: + structure = gst_avdtp_sink_parse_sbc_raw(self); + break; + case A2DP_CODEC_MPEG12: + structure = gst_avdtp_sink_parse_mpeg_raw(self); + break; + default: + GST_ERROR_OBJECT(self, "Unsupported configuration"); + return FALSE; + } + + if (structure == NULL) + return FALSE; + + if (self->dev_caps != NULL) + gst_caps_unref(self->dev_caps); + + self->dev_caps = gst_caps_new_full(structure, NULL); + + tmp = gst_caps_to_string(self->dev_caps); + GST_DEBUG_OBJECT(self, "Transport configuration: %s", tmp); + g_free(tmp); + + return TRUE; +} + +static gboolean gst_avdtp_sink_update_caps(GstAvdtpSink *self) +{ + sbc_capabilities_t *sbc; + mpeg_capabilities_t *mpeg; + GstStructure *sbc_structure; + GstStructure *mpeg_structure; + gchar *tmp; + + GST_LOG_OBJECT(self, "updating device caps"); + + if (self->data->config_size != 0 && self->data->config != NULL) + return gst_avdtp_sink_update_config(self); + + sbc = (void *) gst_avdtp_find_caps(self, BT_A2DP_SBC_SINK); + mpeg = (void *) gst_avdtp_find_caps(self, BT_A2DP_MPEG12_SINK); + + sbc_structure = gst_avdtp_sink_parse_sbc_caps(self, sbc); + mpeg_structure = gst_avdtp_sink_parse_mpeg_caps(self, mpeg); + + if (self->dev_caps != NULL) + gst_caps_unref(self->dev_caps); + self->dev_caps = gst_caps_new_full(sbc_structure, NULL); + if (mpeg_structure != NULL) + gst_caps_append_structure(self->dev_caps, mpeg_structure); + + tmp = gst_caps_to_string(self->dev_caps); + GST_DEBUG_OBJECT(self, "Device capabilities: %s", tmp); + g_free(tmp); + + return TRUE; +} + +static gboolean gst_avdtp_sink_get_capabilities(GstAvdtpSink *self) +{ + gchar *buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_req *req = (void *) buf; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + int err; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + + req->h.type = BT_REQUEST; + req->h.name = BT_GET_CAPABILITIES; + req->h.length = sizeof(*req); + + if (self->device == NULL) + return FALSE; + strncpy(req->destination, self->device, 18); + if (self->autoconnect) + req->flags |= BT_FLAG_AUTOCONNECT; + + err = gst_avdtp_sink_audioservice_send(self, &req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while asking device caps"); + return FALSE; + } + + rsp->h.length = 0; + err = gst_avdtp_sink_audioservice_expect(self, + &rsp->h, BT_GET_CAPABILITIES); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while getting device caps"); + return FALSE; + } + + self->data->caps = g_malloc0(rsp->h.length); + memcpy(self->data->caps, rsp, rsp->h.length); + if (!gst_avdtp_sink_update_caps(self)) { + GST_WARNING_OBJECT(self, "failed to update capabilities"); + return FALSE; + } + + return TRUE; +} + +static gint gst_avdtp_sink_get_channel_mode(const gchar *mode) +{ + if (strcmp(mode, "stereo") == 0) + return BT_A2DP_CHANNEL_MODE_STEREO; + else if (strcmp(mode, "joint-stereo") == 0) + return BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + else if (strcmp(mode, "dual-channel") == 0) + return BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + else if (strcmp(mode, "mono") == 0) + return BT_A2DP_CHANNEL_MODE_MONO; + else + return -1; +} + +static void gst_avdtp_sink_tag(const GstTagList *taglist, + const gchar *tag, gpointer user_data) +{ + gboolean crc; + gchar *channel_mode = NULL; + GstAvdtpSink *self = GST_AVDTP_SINK(user_data); + + if (strcmp(tag, "has-crc") == 0) { + + if (!gst_tag_list_get_boolean(taglist, tag, &crc)) { + GST_WARNING_OBJECT(self, "failed to get crc tag"); + return; + } + + gst_avdtp_sink_set_crc(self, crc); + + } else if (strcmp(tag, "channel-mode") == 0) { + + if (!gst_tag_list_get_string(taglist, tag, &channel_mode)) { + GST_WARNING_OBJECT(self, + "failed to get channel-mode tag"); + return; + } + + self->channel_mode = gst_avdtp_sink_get_channel_mode( + channel_mode); + if (self->channel_mode == -1) + GST_WARNING_OBJECT(self, "Received invalid channel " + "mode: %s", channel_mode); + g_free(channel_mode); + + } else + GST_DEBUG_OBJECT(self, "received unused tag: %s", tag); +} + +static gboolean gst_avdtp_sink_event(GstBaseSink *basesink, + GstEvent *event) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + GstTagList *taglist = NULL; + + if (GST_EVENT_TYPE(event) == GST_EVENT_TAG) { + /* we check the tags, mp3 has tags that are importants and + * are outside caps */ + gst_event_parse_tag(event, &taglist); + gst_tag_list_foreach(taglist, gst_avdtp_sink_tag, self); + } + + return TRUE; +} + +static gboolean gst_avdtp_sink_transport_parse_property(GstAvdtpSink *self, + DBusMessageIter *i) +{ + const char *key; + DBusMessageIter variant_i; + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_STRING) { + GST_ERROR_OBJECT(self, "Property name not a string."); + return FALSE; + } + + dbus_message_iter_get_basic(i, &key); + + if (!dbus_message_iter_next(i)) { + GST_ERROR_OBJECT(self, "Property value missing"); + return FALSE; + } + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_VARIANT) { + GST_ERROR_OBJECT(self, "Property value not a variant."); + return FALSE; + } + + dbus_message_iter_recurse(i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + case DBUS_TYPE_BYTE: { + uint8_t value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (g_str_equal(key, "Codec") == TRUE) + self->data->codec = value; + + break; + } + case DBUS_TYPE_STRING: { + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (g_str_equal(key, "UUID") == TRUE) { + g_free(self->data->uuid); + self->data->uuid = g_strdup(value); + } + + break; + } + case DBUS_TYPE_ARRAY: { + DBusMessageIter array_i; + char *value; + int size; + + dbus_message_iter_recurse(&variant_i, &array_i); + dbus_message_iter_get_fixed_array(&array_i, &value, &size); + + if (g_str_equal(key, "Configuration")) { + g_free(self->data->config); + self->data->config = g_new0(guint8, size); + self->data->config_size = size; + memcpy(self->data->config, value, size); + } + + break; + } + } + + return TRUE; +} + +static gboolean gst_avdtp_sink_transport_acquire(GstAvdtpSink *self) +{ + DBusMessage *msg, *reply; + DBusError err; + const char *access_type = "w"; + int fd; + uint16_t imtu, omtu; + + dbus_error_init(&err); + + if (self->data->conn == NULL) + self->data->conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + + msg = dbus_message_new_method_call("org.bluez", self->transport, + "org.bluez.MediaTransport", + "Acquire"); + + dbus_message_append_args(msg, DBUS_TYPE_STRING, &access_type, + DBUS_TYPE_INVALID); + + reply = dbus_connection_send_with_reply_and_block(self->data->conn, + msg, -1, &err); + + if (dbus_error_is_set(&err)) + goto fail; + + if (dbus_message_get_args(reply, &err, DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID) == FALSE) + goto fail; + + dbus_message_unref(reply); + + self->stream = g_io_channel_unix_new(fd); + g_io_channel_set_encoding(self->stream, NULL, NULL); + g_io_channel_set_close_on_unref(self->stream, TRUE); + self->data->link_mtu = omtu; + GST_DEBUG_OBJECT(self, "stream_fd=%d mtu=%d", fd, omtu); + + return TRUE; + +fail: + GST_ERROR_OBJECT(self, "Failed to acquire transport stream: %s", + err.message); + + dbus_error_free(&err); + + if (reply) + dbus_message_unref(msg); + + return FALSE; +} + +static gboolean gst_avdtp_sink_transport_get_properties(GstAvdtpSink *self) +{ + DBusMessage *msg, *reply; + DBusMessageIter arg_i, ele_i; + DBusError err; + + dbus_error_init(&err); + + /* Transport need to be acquire first to make sure the MTUs are + available */ + if (gst_avdtp_sink_transport_acquire(self) == FALSE) + return FALSE; + + msg = dbus_message_new_method_call("org.bluez", self->transport, + "org.bluez.MediaTransport", + "GetProperties"); + reply = dbus_connection_send_with_reply_and_block(self->data->conn, + msg, -1, &err); + + if (dbus_error_is_set(&err) || reply == NULL) { + GST_ERROR_OBJECT(self, "Failed to get transport properties: %s", + err.message); + goto fail; + } + + if (!dbus_message_iter_init(reply, &arg_i)) { + GST_ERROR_OBJECT(self, "GetProperties reply has no arguments."); + goto fail; + } + + if (dbus_message_iter_get_arg_type(&arg_i) != DBUS_TYPE_ARRAY) { + GST_ERROR_OBJECT(self, "GetProperties argument is not an array."); + goto fail; + } + + dbus_message_iter_recurse(&arg_i, &ele_i); + while (dbus_message_iter_get_arg_type(&ele_i) != DBUS_TYPE_INVALID) { + + if (dbus_message_iter_get_arg_type(&ele_i) == + DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&ele_i, &dict_i); + + gst_avdtp_sink_transport_parse_property(self, &dict_i); + } + + if (!dbus_message_iter_next(&ele_i)) + break; + } + + return gst_avdtp_sink_update_caps(self); + +fail: + dbus_message_unref(msg); + dbus_message_unref(reply); + return FALSE; + +} + +static gboolean gst_avdtp_sink_start(GstBaseSink *basesink) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + gint sk; + gint err; + + GST_INFO_OBJECT(self, "start"); + + self->data = g_new0(struct bluetooth_data, 1); + + self->stream = NULL; + self->stream_caps = NULL; + self->mp3_using_crc = -1; + self->channel_mode = -1; + + if (self->transport != NULL) + return gst_avdtp_sink_transport_get_properties(self); + + self->watch_id = 0; + + sk = bt_audio_service_open(); + if (sk <= 0) { + err = errno; + GST_ERROR_OBJECT(self, "Cannot open connection to bt " + "audio service: %s %d", strerror(err), err); + goto failed; + } + + self->server = g_io_channel_unix_new(sk); + g_io_channel_set_encoding(self->server, NULL, NULL); + self->watch_id = g_io_add_watch(self->server, G_IO_HUP | G_IO_ERR | + G_IO_NVAL, server_callback, self); + + if (!gst_avdtp_sink_get_capabilities(self)) { + GST_ERROR_OBJECT(self, "failed to get capabilities " + "from device"); + goto failed; + } + + return TRUE; + +failed: + bt_audio_service_close(sk); + return FALSE; +} + +static gboolean gst_avdtp_sink_stream_start(GstAvdtpSink *self) +{ + gchar buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_req *req = (void *) buf; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + int err; + + if (self->transport != NULL) + return gst_avdtp_sink_conf_recv_stream_fd(self); + + memset(req, 0, sizeof(buf)); + req->h.type = BT_REQUEST; + req->h.name = BT_START_STREAM; + req->h.length = sizeof(*req); + + err = gst_avdtp_sink_audioservice_send(self, &req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error ocurred while sending " + "start packet"); + return FALSE; + } + + rsp->h.length = sizeof(*rsp); + err = gst_avdtp_sink_audioservice_expect(self, &rsp->h, + BT_START_STREAM); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while stream " + "start confirmation"); + return FALSE; + } + + ind->h.length = sizeof(*ind); + err = gst_avdtp_sink_audioservice_expect(self, &ind->h, + BT_NEW_STREAM); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while receiving " + "stream filedescriptor"); + return FALSE; + } + + if (!gst_avdtp_sink_conf_recv_stream_fd(self)) + return FALSE; + + return TRUE; +} + +static gboolean gst_avdtp_sink_init_mp3_pkt_conf( + GstAvdtpSink *self, GstCaps *caps, + mpeg_capabilities_t *pkt) +{ + const GValue *value = NULL; + gint rate, layer; + const gchar *name; + GstStructure *structure = gst_caps_get_structure(caps, 0); + + name = gst_structure_get_name(structure); + + if (!(IS_MPEG_AUDIO(name))) { + GST_ERROR_OBJECT(self, "Unexpected format %s, " + "was expecting mp3", name); + return FALSE; + } + + /* layer */ + value = gst_structure_get_value(structure, "layer"); + layer = g_value_get_int(value); + if (layer == 1) + pkt->layer = BT_MPEG_LAYER_1; + else if (layer == 2) + pkt->layer = BT_MPEG_LAYER_2; + else if (layer == 3) + pkt->layer = BT_MPEG_LAYER_3; + else { + GST_ERROR_OBJECT(self, "Unexpected layer: %d", layer); + return FALSE; + } + + /* crc */ + if (self->mp3_using_crc != -1) + pkt->crc = self->mp3_using_crc; + else { + GST_ERROR_OBJECT(self, "No info about crc was received, " + " can't proceed"); + return FALSE; + } + + /* channel mode */ + if (self->channel_mode != -1) + pkt->channel_mode = self->channel_mode; + else { + GST_ERROR_OBJECT(self, "No info about channel mode " + "received, can't proceed"); + return FALSE; + } + + /* mpf - we will only use the mandatory one */ + pkt->mpf = 0; + + value = gst_structure_get_value(structure, "rate"); + rate = g_value_get_int(value); + if (rate == 44100) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_44100; + else if (rate == 48000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_48000; + else if (rate == 32000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_32000; + else if (rate == 24000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_24000; + else if (rate == 22050) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_22050; + else if (rate == 16000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_16000; + else { + GST_ERROR_OBJECT(self, "Invalid rate while setting caps"); + return FALSE; + } + + /* vbr - we always say its vbr, we don't have how to know it */ + pkt->bitrate = 0x8000; + + return TRUE; +} + +static gboolean gst_avdtp_sink_configure(GstAvdtpSink *self, + GstCaps *caps) +{ + gchar buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_req *open_req = (void *) buf; + struct bt_open_rsp *open_rsp = (void *) buf; + struct bt_set_configuration_req *req = (void *) buf; + struct bt_set_configuration_rsp *rsp = (void *) buf; + gboolean ret; + gchar *temp; + GstStructure *structure; + codec_capabilities_t *codec = NULL; + int err; + + temp = gst_caps_to_string(caps); + GST_DEBUG_OBJECT(self, "configuring device with caps: %s", temp); + g_free(temp); + + /* Transport already configured */ + if (self->transport != NULL) + return TRUE; + + structure = gst_caps_get_structure(caps, 0); + + if (gst_structure_has_name(structure, "audio/x-sbc")) + codec = (void *) gst_avdtp_find_caps(self, BT_A2DP_SBC_SINK); + else if (gst_structure_has_name(structure, "audio/mpeg")) + codec = (void *) gst_avdtp_find_caps(self, BT_A2DP_MPEG12_SINK); + + if (codec == NULL) { + GST_ERROR_OBJECT(self, "Couldn't parse caps " + "to packet configuration"); + return FALSE; + } + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + open_req->h.type = BT_REQUEST; + open_req->h.name = BT_OPEN; + open_req->h.length = sizeof(*open_req); + + strncpy(open_req->destination, self->device, 18); + open_req->seid = codec->seid; + open_req->lock = BT_WRITE_LOCK; + + err = gst_avdtp_sink_audioservice_send(self, &open_req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error ocurred while sending " + "open packet"); + return FALSE; + } + + open_rsp->h.length = sizeof(*open_rsp); + err = gst_avdtp_sink_audioservice_expect(self, &open_rsp->h, + BT_OPEN); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while receiving device " + "confirmation"); + return FALSE; + } + + memset(req, 0, sizeof(buf)); + req->h.type = BT_REQUEST; + req->h.name = BT_SET_CONFIGURATION; + req->h.length = sizeof(*req); + memcpy(&req->codec, codec, sizeof(req->codec)); + + if (codec->type == BT_A2DP_SBC_SINK) + ret = gst_avdtp_sink_init_sbc_pkt_conf(self, caps, + (void *) &req->codec); + else + ret = gst_avdtp_sink_init_mp3_pkt_conf(self, caps, + (void *) &req->codec); + + if (!ret) { + GST_ERROR_OBJECT(self, "Couldn't parse caps " + "to packet configuration"); + return FALSE; + } + + req->h.length += req->codec.length - sizeof(req->codec); + err = gst_avdtp_sink_audioservice_send(self, &req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error ocurred while sending " + "configurarion packet"); + return FALSE; + } + + rsp->h.length = sizeof(*rsp); + err = gst_avdtp_sink_audioservice_expect(self, &rsp->h, + BT_SET_CONFIGURATION); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while receiving device " + "confirmation"); + return FALSE; + } + + self->data->link_mtu = rsp->link_mtu; + + return TRUE; +} + +static GstFlowReturn gst_avdtp_sink_preroll(GstBaseSink *basesink, + GstBuffer *buffer) +{ + GstAvdtpSink *sink = GST_AVDTP_SINK(basesink); + gboolean ret; + + GST_AVDTP_SINK_MUTEX_LOCK(sink); + + ret = gst_avdtp_sink_stream_start(sink); + + GST_AVDTP_SINK_MUTEX_UNLOCK(sink); + + if (!ret) + return GST_FLOW_ERROR; + + return GST_FLOW_OK; +} + +static GstFlowReturn gst_avdtp_sink_render(GstBaseSink *basesink, + GstBuffer *buffer) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + ssize_t ret; + int fd; + + fd = g_io_channel_unix_get_fd(self->stream); + + ret = write(fd, GST_BUFFER_DATA(buffer), GST_BUFFER_SIZE(buffer)); + if (ret < 0) { + GST_ERROR_OBJECT(self, "Error while writting to socket: %s", + strerror(errno)); + return GST_FLOW_ERROR; + } + + return GST_FLOW_OK; +} + +static gboolean gst_avdtp_sink_unlock(GstBaseSink *basesink) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + + if (self->stream != NULL) + g_io_channel_flush(self->stream, NULL); + + return TRUE; +} + +static GstFlowReturn gst_avdtp_sink_buffer_alloc(GstBaseSink *basesink, + guint64 offset, guint size, GstCaps *caps, + GstBuffer **buf) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + + *buf = gst_buffer_new_and_alloc(size); + if (!(*buf)) { + GST_ERROR_OBJECT(self, "buffer allocation failed"); + return GST_FLOW_ERROR; + } + + gst_buffer_set_caps(*buf, caps); + + GST_BUFFER_OFFSET(*buf) = offset; + + return GST_FLOW_OK; +} + +static void gst_avdtp_sink_class_init(GstAvdtpSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstBaseSinkClass *basesink_class = GST_BASE_SINK_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->finalize = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_finalize); + object_class->set_property = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_get_property); + + basesink_class->start = GST_DEBUG_FUNCPTR(gst_avdtp_sink_start); + basesink_class->stop = GST_DEBUG_FUNCPTR(gst_avdtp_sink_stop); + basesink_class->render = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_render); + basesink_class->preroll = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_preroll); + basesink_class->unlock = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_unlock); + basesink_class->event = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_event); + + basesink_class->buffer_alloc = + GST_DEBUG_FUNCPTR(gst_avdtp_sink_buffer_alloc); + + g_object_class_install_property(object_class, PROP_DEVICE, + g_param_spec_string("device", "Device", + "Bluetooth remote device address", + NULL, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_AUTOCONNECT, + g_param_spec_boolean("auto-connect", + "Auto-connect", + "Automatically attempt to connect " + "to device", DEFAULT_AUTOCONNECT, + G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_TRANSPORT, + g_param_spec_string("transport", + "Transport", + "Use configured transport", + NULL, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(avdtp_sink_debug, "avdtpsink", 0, + "A2DP headset sink element"); +} + +static void gst_avdtp_sink_init(GstAvdtpSink *self, + GstAvdtpSinkClass *klass) +{ + self->device = NULL; + self->transport = NULL; + self->data = NULL; + + self->stream = NULL; + + self->dev_caps = NULL; + + self->autoconnect = DEFAULT_AUTOCONNECT; + + self->sink_lock = g_mutex_new(); + + /* FIXME this is for not synchronizing with clock, should be tested + * with devices to see the behaviour + gst_base_sink_set_sync(GST_BASE_SINK(self), FALSE); + */ +} + +static int gst_avdtp_sink_audioservice_send(GstAvdtpSink *self, + const bt_audio_msg_header_t *msg) +{ + ssize_t written; + const char *type, *name; + uint16_t length; + int fd; + + length = msg->length ? msg->length : BT_SUGGESTED_BUFFER_SIZE; + + fd = g_io_channel_unix_get_fd(self->server); + + written = write(fd, msg, length); + if (written < 0) { + GST_ERROR_OBJECT(self, "Error sending data to audio service:" + " %s", strerror(errno)); + return -errno; + } + + type = bt_audio_strtype(msg->type); + name = bt_audio_strname(msg->name); + + GST_DEBUG_OBJECT(self, "sent: %s -> %s", type, name); + + return 0; +} + +static int gst_avdtp_sink_audioservice_recv(GstAvdtpSink *self, + bt_audio_msg_header_t *inmsg) +{ + ssize_t bytes_read; + const char *type, *name; + uint16_t length; + int fd, err; + + length = inmsg->length ? inmsg->length : BT_SUGGESTED_BUFFER_SIZE; + + fd = g_io_channel_unix_get_fd(self->server); + + bytes_read = read(fd, inmsg, length); + if (bytes_read < 0) { + GST_ERROR_OBJECT(self, "Error receiving data from " + "audio service: %s", strerror(errno)); + return -errno; + } + + type = bt_audio_strtype(inmsg->type); + if (!type) { + err = -EINVAL; + GST_ERROR_OBJECT(self, "Bogus message type %d " + "received from audio service", + inmsg->type); + } + + name = bt_audio_strname(inmsg->name); + if (!name) { + err = -EINVAL; + GST_ERROR_OBJECT(self, "Bogus message name %d " + "received from audio service", + inmsg->name); + } + + if (inmsg->type == BT_ERROR) { + bt_audio_error_t *msg = (void *) inmsg; + err = -EINVAL; + GST_ERROR_OBJECT(self, "%s failed : " + "%s(%d)", + name, + strerror(msg->posix_errno), + msg->posix_errno); + } + + GST_DEBUG_OBJECT(self, "received: %s <- %s", type, name); + + return err; +} + +static int gst_avdtp_sink_audioservice_expect(GstAvdtpSink *self, + bt_audio_msg_header_t *outmsg, + guint8 expected_name) +{ + int err; + + err = gst_avdtp_sink_audioservice_recv(self, outmsg); + if (err < 0) + return err; + + if (outmsg->name != expected_name) + return -EINVAL; + + return 0; +} + +gboolean gst_avdtp_sink_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "avdtpsink", GST_RANK_NONE, + GST_TYPE_AVDTP_SINK); +} + + +/* public functions */ +GstCaps *gst_avdtp_sink_get_device_caps(GstAvdtpSink *sink) +{ + if (sink->dev_caps == NULL) + return NULL; + + return gst_caps_copy(sink->dev_caps); +} + +gboolean gst_avdtp_sink_set_device_caps(GstAvdtpSink *self, + GstCaps *caps) +{ + gboolean ret; + + GST_DEBUG_OBJECT(self, "setting device caps"); + GST_AVDTP_SINK_MUTEX_LOCK(self); + ret = gst_avdtp_sink_configure(self, caps); + + if (self->stream_caps) + gst_caps_unref(self->stream_caps); + self->stream_caps = gst_caps_ref(caps); + + GST_AVDTP_SINK_MUTEX_UNLOCK(self); + + return ret; +} + +guint gst_avdtp_sink_get_link_mtu(GstAvdtpSink *sink) +{ + return sink->data->link_mtu; +} + +void gst_avdtp_sink_set_device(GstAvdtpSink *self, const gchar *dev) +{ + if (self->device != NULL) + g_free(self->device); + + GST_LOG_OBJECT(self, "Setting device: %s", dev); + self->device = g_strdup(dev); +} + +void gst_avdtp_sink_set_transport(GstAvdtpSink *self, const gchar *trans) +{ + if (self->transport != NULL) + g_free(self->transport); + + GST_LOG_OBJECT(self, "Setting transport: %s", trans); + self->transport = g_strdup(trans); +} + +gchar *gst_avdtp_sink_get_device(GstAvdtpSink *self) +{ + return g_strdup(self->device); +} + +gchar *gst_avdtp_sink_get_transport(GstAvdtpSink *self) +{ + return g_strdup(self->transport); +} + +void gst_avdtp_sink_set_crc(GstAvdtpSink *self, gboolean crc) +{ + gint new_crc; + + new_crc = crc ? CRC_PROTECTED : CRC_UNPROTECTED; + + /* test if we already received a different crc */ + if (self->mp3_using_crc != -1 && new_crc != self->mp3_using_crc) { + GST_WARNING_OBJECT(self, "crc changed during stream"); + return; + } + self->mp3_using_crc = new_crc; + +} + +void gst_avdtp_sink_set_channel_mode(GstAvdtpSink *self, + const gchar *mode) +{ + gint new_mode; + + new_mode = gst_avdtp_sink_get_channel_mode(mode); + + if (self->channel_mode != -1 && new_mode != self->channel_mode) { + GST_WARNING_OBJECT(self, "channel mode changed during stream"); + return; + } + + self->channel_mode = new_mode; + if (self->channel_mode == -1) + GST_WARNING_OBJECT(self, "Received invalid channel " + "mode: %s", mode); +} diff --git a/audio/gstavdtpsink.h b/audio/gstavdtpsink.h new file mode 100644 index 0000000..c4e5645 --- /dev/null +++ b/audio/gstavdtpsink.h @@ -0,0 +1,107 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifndef __GST_AVDTP_SINK_H +#define __GST_AVDTP_SINK_H + +#include <gst/gst.h> +#include <gst/base/gstbasesink.h> + +G_BEGIN_DECLS + +#define GST_TYPE_AVDTP_SINK \ + (gst_avdtp_sink_get_type()) +#define GST_AVDTP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AVDTP_SINK,\ + GstAvdtpSink)) +#define GST_AVDTP_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AVDTP_SINK,\ + GstAvdtpSinkClass)) +#define GST_IS_AVDTP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AVDTP_SINK)) +#define GST_IS_AVDTP_SINK_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AVDTP_SINK)) + +typedef struct _GstAvdtpSink GstAvdtpSink; +typedef struct _GstAvdtpSinkClass GstAvdtpSinkClass; + +struct bluetooth_data; + +struct _GstAvdtpSink { + GstBaseSink sink; + + gchar *device; + gchar *transport; + GIOChannel *stream; + + struct bluetooth_data *data; + gboolean autoconnect; + GIOChannel *server; + + /* mp3 stream data (outside caps data)*/ + gint mp3_using_crc; + gint channel_mode; + + /* stream connection data */ + GstCaps *stream_caps; + + GstCaps *dev_caps; + + GMutex *sink_lock; + + guint watch_id; +}; + +struct _GstAvdtpSinkClass { + GstBaseSinkClass parent_class; +}; + +GType gst_avdtp_sink_get_type(void); + +GstCaps *gst_avdtp_sink_get_device_caps(GstAvdtpSink *sink); +gboolean gst_avdtp_sink_set_device_caps(GstAvdtpSink *sink, + GstCaps *caps); + +guint gst_avdtp_sink_get_link_mtu(GstAvdtpSink *sink); + +void gst_avdtp_sink_set_device(GstAvdtpSink *sink, + const gchar* device); + +void gst_avdtp_sink_set_transport(GstAvdtpSink *sink, + const gchar *transport); + +gchar *gst_avdtp_sink_get_device(GstAvdtpSink *sink); + +gchar *gst_avdtp_sink_get_transport(GstAvdtpSink *sink); + +gboolean gst_avdtp_sink_plugin_init(GstPlugin *plugin); + +void gst_avdtp_sink_set_crc(GstAvdtpSink *self, gboolean crc); + +void gst_avdtp_sink_set_channel_mode(GstAvdtpSink *self, + const gchar *mode); + + +G_END_DECLS + +#endif /* __GST_AVDTP_SINK_H */ diff --git a/audio/gstbluetooth.c b/audio/gstbluetooth.c new file mode 100644 index 0000000..9930820 --- /dev/null +++ b/audio/gstbluetooth.c @@ -0,0 +1,109 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include <gst/gst.h> + +#include "gstsbcutil.h" +#include <sbc.h> + +#include "gstsbcenc.h" +#include "gstsbcdec.h" +#include "gstsbcparse.h" +#include "gstavdtpsink.h" +#include "gsta2dpsink.h" +#include "gstrtpsbcpay.h" + +static GstStaticCaps sbc_caps = GST_STATIC_CAPS("audio/x-sbc"); + +#define SBC_CAPS (gst_static_caps_get(&sbc_caps)) + +static void sbc_typefind(GstTypeFind *tf, gpointer ignore) +{ + GstCaps *caps; + guint8 *aux; + sbc_t sbc; + guint8 *data = gst_type_find_peek(tf, 0, 32); + + if (data == NULL) + return; + + if (sbc_init(&sbc, 0) < 0) + return; + + aux = g_new(guint8, 32); + memcpy(aux, data, 32); + if (sbc_parse(&sbc, aux, 32) < 0) + goto done; + + caps = gst_sbc_parse_caps_from_sbc(&sbc); + gst_type_find_suggest(tf, GST_TYPE_FIND_POSSIBLE, caps); + gst_caps_unref(caps); + +done: + g_free(aux); + sbc_finish(&sbc); +} + +static gchar *sbc_exts[] = { "sbc", NULL }; + +static gboolean plugin_init(GstPlugin *plugin) +{ + GST_INFO("Bluetooth plugin %s", VERSION); + + if (gst_type_find_register(plugin, "sbc", + GST_RANK_PRIMARY, sbc_typefind, sbc_exts, + SBC_CAPS, NULL, NULL) == FALSE) + return FALSE; + + if (!gst_sbc_enc_plugin_init(plugin)) + return FALSE; + + if (!gst_sbc_dec_plugin_init(plugin)) + return FALSE; + + if (!gst_sbc_parse_plugin_init(plugin)) + return FALSE; + + if (!gst_avdtp_sink_plugin_init(plugin)) + return FALSE; + + if (!gst_a2dp_sink_plugin_init(plugin)) + return FALSE; + + if (!gst_rtp_sbc_pay_plugin_init(plugin)) + return FALSE; + + return TRUE; +} + +extern GstPluginDesc gst_plugin_desc __attribute__ ((visibility("default"))); + +GST_PLUGIN_DEFINE(GST_VERSION_MAJOR, GST_VERSION_MINOR, + "bluetooth", "Bluetooth plugin library", + plugin_init, VERSION, "LGPL", "BlueZ", "http://www.bluez.org/") diff --git a/audio/gstpragma.h b/audio/gstpragma.h new file mode 100644 index 0000000..626311c --- /dev/null +++ b/audio/gstpragma.h @@ -0,0 +1,24 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +//#pragma GCC diagnostic warning "-Wmissing-declarations" diff --git a/audio/gstrtpsbcpay.c b/audio/gstrtpsbcpay.c new file mode 100644 index 0000000..1159bfe --- /dev/null +++ b/audio/gstrtpsbcpay.c @@ -0,0 +1,352 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "gstpragma.h" +#include "gstrtpsbcpay.h" +#include <math.h> +#include <string.h> + +#define RTP_SBC_PAYLOAD_HEADER_SIZE 1 +#define DEFAULT_MIN_FRAMES 0 +#define RTP_SBC_HEADER_TOTAL (12 + RTP_SBC_PAYLOAD_HEADER_SIZE) + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct rtp_payload { + guint8 frame_count:4; + guint8 rfa0:1; + guint8 is_last_fragment:1; + guint8 is_first_fragment:1; + guint8 is_fragmented:1; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct rtp_payload { + guint8 is_fragmented:1; + guint8 is_first_fragment:1; + guint8 is_last_fragment:1; + guint8 rfa0:1; + guint8 frame_count:4; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +enum { + PROP_0, + PROP_MIN_FRAMES +}; + +GST_DEBUG_CATEGORY_STATIC(gst_rtp_sbc_pay_debug); +#define GST_CAT_DEFAULT gst_rtp_sbc_pay_debug + +GST_BOILERPLATE(GstRtpSBCPay, gst_rtp_sbc_pay, GstBaseRTPPayload, + GST_TYPE_BASE_RTP_PAYLOAD); + +static const GstElementDetails gst_rtp_sbc_pay_details = + GST_ELEMENT_DETAILS("RTP packet payloader", + "Codec/Payloader/Network", + "Payload SBC audio as RTP packets", + "Thiago Sousa Santos " + "<thiagoss@lcc.ufcg.edu.br>"); + +static GstStaticPadTemplate gst_rtp_sbc_pay_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ 2, 64 ]") + ); + +static GstStaticPadTemplate gst_rtp_sbc_pay_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS( + "application/x-rtp, " + "media = (string) \"audio\"," + "payload = (int) " GST_RTP_PAYLOAD_DYNAMIC_STRING ", " + "clock-rate = (int) { 16000, 32000, 44100, 48000 }," + "encoding-name = (string) \"SBC\"") + ); + +static void gst_rtp_sbc_pay_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec); +static void gst_rtp_sbc_pay_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec); + +static gint gst_rtp_sbc_pay_get_frame_len(gint subbands, gint channels, + gint blocks, gint bitpool, const gchar *channel_mode) +{ + gint len; + gint join; + + len = 4 + (4 * subbands * channels)/8; + + if (strcmp(channel_mode, "mono") == 0 || + strcmp(channel_mode, "dual") == 0) + len += ((blocks * channels * bitpool) + 7) / 8; + else { + join = strcmp(channel_mode, "joint") == 0 ? 1 : 0; + len += ((join * subbands + blocks * bitpool) + 7) / 8; + } + + return len; +} + +static gboolean gst_rtp_sbc_pay_set_caps(GstBaseRTPPayload *payload, + GstCaps *caps) +{ + GstRtpSBCPay *sbcpay; + gint rate, subbands, channels, blocks, bitpool; + gint frame_len; + const gchar *channel_mode; + GstStructure *structure; + + sbcpay = GST_RTP_SBC_PAY(payload); + + structure = gst_caps_get_structure(caps, 0); + if (!gst_structure_get_int(structure, "rate", &rate)) + return FALSE; + if (!gst_structure_get_int(structure, "channels", &channels)) + return FALSE; + if (!gst_structure_get_int(structure, "blocks", &blocks)) + return FALSE; + if (!gst_structure_get_int(structure, "bitpool", &bitpool)) + return FALSE; + if (!gst_structure_get_int(structure, "subbands", &subbands)) + return FALSE; + + channel_mode = gst_structure_get_string(structure, "mode"); + if (!channel_mode) + return FALSE; + + frame_len = gst_rtp_sbc_pay_get_frame_len(subbands, channels, blocks, + bitpool, channel_mode); + + sbcpay->frame_length = frame_len; + + gst_basertppayload_set_options(payload, "audio", TRUE, "SBC", rate); + + GST_DEBUG_OBJECT(payload, "calculated frame length: %d ", frame_len); + + return gst_basertppayload_set_outcaps(payload, NULL); +} + +static GstFlowReturn gst_rtp_sbc_pay_flush_buffers(GstRtpSBCPay *sbcpay) +{ + guint available; + guint max_payload; + GstBuffer *outbuf; + guint8 *payload_data; + guint frame_count; + guint payload_length; + struct rtp_payload *payload; + + if (sbcpay->frame_length == 0) { + GST_ERROR_OBJECT(sbcpay, "Frame length is 0"); + return GST_FLOW_ERROR; + } + + available = gst_adapter_available(sbcpay->adapter); + + max_payload = gst_rtp_buffer_calc_payload_len( + GST_BASE_RTP_PAYLOAD_MTU(sbcpay) - RTP_SBC_PAYLOAD_HEADER_SIZE, + 0, 0); + + max_payload = MIN(max_payload, available); + frame_count = max_payload / sbcpay->frame_length; + payload_length = frame_count * sbcpay->frame_length; + if (payload_length == 0) /* Nothing to send */ + return GST_FLOW_OK; + + outbuf = gst_rtp_buffer_new_allocate(payload_length + + RTP_SBC_PAYLOAD_HEADER_SIZE, 0, 0); + + gst_rtp_buffer_set_payload_type(outbuf, + GST_BASE_RTP_PAYLOAD_PT(sbcpay)); + + payload_data = gst_rtp_buffer_get_payload(outbuf); + payload = (struct rtp_payload *) payload_data; + memset(payload, 0, sizeof(struct rtp_payload)); + payload->frame_count = frame_count; + + gst_adapter_copy(sbcpay->adapter, payload_data + + RTP_SBC_PAYLOAD_HEADER_SIZE, 0, payload_length); + gst_adapter_flush(sbcpay->adapter, payload_length); + + GST_BUFFER_TIMESTAMP(outbuf) = sbcpay->timestamp; + GST_DEBUG_OBJECT(sbcpay, "Pushing %d bytes", payload_length); + + return gst_basertppayload_push(GST_BASE_RTP_PAYLOAD(sbcpay), outbuf); +} + +static GstFlowReturn gst_rtp_sbc_pay_handle_buffer(GstBaseRTPPayload *payload, + GstBuffer *buffer) +{ + GstRtpSBCPay *sbcpay; + guint available; + + /* FIXME check for negotiation */ + + sbcpay = GST_RTP_SBC_PAY(payload); + sbcpay->timestamp = GST_BUFFER_TIMESTAMP(buffer); + + gst_adapter_push(sbcpay->adapter, buffer); + + available = gst_adapter_available(sbcpay->adapter); + if (available + RTP_SBC_HEADER_TOTAL >= + GST_BASE_RTP_PAYLOAD_MTU(sbcpay) || + (available > + (sbcpay->min_frames * sbcpay->frame_length))) + return gst_rtp_sbc_pay_flush_buffers(sbcpay); + + return GST_FLOW_OK; +} + +static gboolean gst_rtp_sbc_pay_handle_event(GstPad *pad, + GstEvent *event) +{ + GstRtpSBCPay *sbcpay = GST_RTP_SBC_PAY(GST_PAD_PARENT(pad)); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_EOS: + gst_rtp_sbc_pay_flush_buffers(sbcpay); + break; + default: + break; + } + + return FALSE; +} + +static void gst_rtp_sbc_pay_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_rtp_sbc_pay_sink_factory)); + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_rtp_sbc_pay_src_factory)); + + gst_element_class_set_details(element_class, &gst_rtp_sbc_pay_details); +} + +static void gst_rtp_sbc_pay_finalize(GObject *object) +{ + GstRtpSBCPay *sbcpay = GST_RTP_SBC_PAY(object); + g_object_unref(sbcpay->adapter); + + GST_CALL_PARENT(G_OBJECT_CLASS, finalize, (object)); +} + +static void gst_rtp_sbc_pay_class_init(GstRtpSBCPayClass *klass) +{ + GObjectClass *gobject_class; + GstBaseRTPPayloadClass *payload_class = + GST_BASE_RTP_PAYLOAD_CLASS(klass); + + gobject_class = G_OBJECT_CLASS(klass); + parent_class = g_type_class_peek_parent(klass); + + gobject_class->finalize = GST_DEBUG_FUNCPTR(gst_rtp_sbc_pay_finalize); + gobject_class->set_property = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_set_property); + gobject_class->get_property = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_get_property); + + payload_class->set_caps = GST_DEBUG_FUNCPTR(gst_rtp_sbc_pay_set_caps); + payload_class->handle_buffer = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_handle_buffer); + payload_class->handle_event = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_handle_event); + + /* properties */ + g_object_class_install_property(G_OBJECT_CLASS(klass), + PROP_MIN_FRAMES, + g_param_spec_int("min-frames", "minimum frame number", + "Minimum quantity of frames to send in one packet " + "(-1 for maximum allowed by the mtu)", + -1, G_MAXINT, DEFAULT_MIN_FRAMES, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(gst_rtp_sbc_pay_debug, "rtpsbcpay", 0, + "RTP SBC payloader"); +} + +static void gst_rtp_sbc_pay_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstRtpSBCPay *sbcpay; + + sbcpay = GST_RTP_SBC_PAY(object); + + switch (prop_id) { + case PROP_MIN_FRAMES: + sbcpay->min_frames = g_value_get_int(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_rtp_sbc_pay_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstRtpSBCPay *sbcpay; + + sbcpay = GST_RTP_SBC_PAY(object); + + switch (prop_id) { + case PROP_MIN_FRAMES: + g_value_set_int(value, sbcpay->min_frames); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_rtp_sbc_pay_init(GstRtpSBCPay *self, GstRtpSBCPayClass *klass) +{ + self->adapter = gst_adapter_new(); + self->frame_length = 0; + self->timestamp = 0; + + self->min_frames = DEFAULT_MIN_FRAMES; +} + +gboolean gst_rtp_sbc_pay_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "rtpsbcpay", GST_RANK_NONE, + GST_TYPE_RTP_SBC_PAY); +} + diff --git a/audio/gstrtpsbcpay.h b/audio/gstrtpsbcpay.h new file mode 100644 index 0000000..398de82 --- /dev/null +++ b/audio/gstrtpsbcpay.h @@ -0,0 +1,66 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 <gst/gst.h> +#include <gst/rtp/gstbasertppayload.h> +#include <gst/base/gstadapter.h> +#include <gst/rtp/gstrtpbuffer.h> + +G_BEGIN_DECLS + +#define GST_TYPE_RTP_SBC_PAY \ + (gst_rtp_sbc_pay_get_type()) +#define GST_RTP_SBC_PAY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_RTP_SBC_PAY,\ + GstRtpSBCPay)) +#define GST_RTP_SBC_PAY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_RTP_SBC_PAY,\ + GstRtpSBCPayClass)) +#define GST_IS_RTP_SBC_PAY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_RTP_SBC_PAY)) +#define GST_IS_RTP_SBC_PAY_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_RTP_SBC_PAY)) + +typedef struct _GstRtpSBCPay GstRtpSBCPay; +typedef struct _GstRtpSBCPayClass GstRtpSBCPayClass; + +struct _GstRtpSBCPay { + GstBaseRTPPayload base; + + GstAdapter *adapter; + GstClockTime timestamp; + + guint frame_length; + + guint min_frames; +}; + +struct _GstRtpSBCPayClass { + GstBaseRTPPayloadClass parent_class; +}; + +GType gst_rtp_sbc_pay_get_type(void); + +gboolean gst_rtp_sbc_pay_plugin_init (GstPlugin * plugin); + +G_END_DECLS diff --git a/audio/gstsbcdec.c b/audio/gstsbcdec.c new file mode 100644 index 0000000..2e5cb0a --- /dev/null +++ b/audio/gstsbcdec.c @@ -0,0 +1,223 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "gstpragma.h" +#include "gstsbcutil.h" +#include "gstsbcdec.h" + +GST_DEBUG_CATEGORY_STATIC(sbc_dec_debug); +#define GST_CAT_DEFAULT sbc_dec_debug + +GST_BOILERPLATE(GstSbcDec, gst_sbc_dec, GstElement, GST_TYPE_ELEMENT); + +static const GstElementDetails sbc_dec_details = + GST_ELEMENT_DETAILS("Bluetooth SBC decoder", + "Codec/Decoder/Audio", + "Decode a SBC audio stream", + "Marcel Holtmann <marcel@holtmann.org>"); + +static GstStaticPadTemplate sbc_dec_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc")); + +static GstStaticPadTemplate sbc_dec_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw-int, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "endianness = (int) BYTE_ORDER, " + "signed = (boolean) true, " + "width = (int) 16, " + "depth = (int) 16")); + +static GstFlowReturn sbc_dec_chain(GstPad *pad, GstBuffer *buffer) +{ + GstSbcDec *dec = GST_SBC_DEC(gst_pad_get_parent(pad)); + GstFlowReturn res = GST_FLOW_OK; + guint size, codesize, offset = 0; + guint8 *data; + + codesize = sbc_get_codesize(&dec->sbc); + + if (dec->buffer) { + GstBuffer *temp = buffer; + buffer = gst_buffer_span(dec->buffer, 0, buffer, + GST_BUFFER_SIZE(dec->buffer) + GST_BUFFER_SIZE(buffer)); + gst_buffer_unref(temp); + gst_buffer_unref(dec->buffer); + dec->buffer = NULL; + } + + data = GST_BUFFER_DATA(buffer); + size = GST_BUFFER_SIZE(buffer); + + while (offset < size) { + GstBuffer *output; + GstPadTemplate *template; + GstCaps *caps; + int consumed; + + res = gst_pad_alloc_buffer_and_set_caps(dec->srcpad, + GST_BUFFER_OFFSET_NONE, + codesize, NULL, &output); + + if (res != GST_FLOW_OK) + goto done; + + consumed = sbc_decode(&dec->sbc, data + offset, size - offset, + GST_BUFFER_DATA(output), codesize, + NULL); + if (consumed <= 0) + break; + + /* we will reuse the same caps object */ + if (dec->outcaps == NULL) { + caps = gst_caps_new_simple("audio/x-raw-int", + "rate", G_TYPE_INT, + gst_sbc_parse_rate_from_sbc( + dec->sbc.frequency), + "channels", G_TYPE_INT, + gst_sbc_get_channel_number( + dec->sbc.mode), + NULL); + + template = gst_static_pad_template_get(&sbc_dec_src_factory); + + dec->outcaps = gst_caps_intersect(caps, + gst_pad_template_get_caps(template)); + + gst_caps_unref(caps); + } + + gst_buffer_set_caps(output, dec->outcaps); + + /* FIXME get a real timestamp */ + GST_BUFFER_TIMESTAMP(output) = GST_CLOCK_TIME_NONE; + + res = gst_pad_push(dec->srcpad, output); + if (res != GST_FLOW_OK) + goto done; + + offset += consumed; + } + + if (offset < size) + dec->buffer = gst_buffer_create_sub(buffer, + offset, size - offset); + +done: + gst_buffer_unref(buffer); + gst_object_unref(dec); + + return res; +} + +static GstStateChangeReturn sbc_dec_change_state(GstElement *element, + GstStateChange transition) +{ + GstSbcDec *dec = GST_SBC_DEC(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG("Setup subband codec"); + if (dec->buffer) { + gst_buffer_unref(dec->buffer); + dec->buffer = NULL; + } + sbc_init(&dec->sbc, 0); + dec->outcaps = NULL; + break; + + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG("Finish subband codec"); + if (dec->buffer) { + gst_buffer_unref(dec->buffer); + dec->buffer = NULL; + } + sbc_finish(&dec->sbc); + if (dec->outcaps) { + gst_caps_unref(dec->outcaps); + dec->outcaps = NULL; + } + break; + + default: + break; + } + + return parent_class->change_state(element, transition); +} + +static void gst_sbc_dec_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_dec_sink_factory)); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_dec_src_factory)); + + gst_element_class_set_details(element_class, &sbc_dec_details); +} + +static void gst_sbc_dec_class_init(GstSbcDecClass *klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + element_class->change_state = GST_DEBUG_FUNCPTR(sbc_dec_change_state); + + GST_DEBUG_CATEGORY_INIT(sbc_dec_debug, "sbcdec", 0, + "SBC decoding element"); +} + +static void gst_sbc_dec_init(GstSbcDec *self, GstSbcDecClass *klass) +{ + self->sinkpad = gst_pad_new_from_static_template( + &sbc_dec_sink_factory, "sink"); + gst_pad_set_chain_function(self->sinkpad, GST_DEBUG_FUNCPTR( + sbc_dec_chain)); + gst_element_add_pad(GST_ELEMENT(self), self->sinkpad); + + self->srcpad = gst_pad_new_from_static_template( + &sbc_dec_src_factory, "src"); + gst_element_add_pad(GST_ELEMENT(self), self->srcpad); + + self->outcaps = NULL; +} + +gboolean gst_sbc_dec_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "sbcdec", GST_RANK_PRIMARY, + GST_TYPE_SBC_DEC); +} + + diff --git a/audio/gstsbcdec.h b/audio/gstsbcdec.h new file mode 100644 index 0000000..c77feae --- /dev/null +++ b/audio/gstsbcdec.h @@ -0,0 +1,66 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 <gst/gst.h> + +#include "sbc.h" + +G_BEGIN_DECLS + +#define GST_TYPE_SBC_DEC \ + (gst_sbc_dec_get_type()) +#define GST_SBC_DEC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SBC_DEC,GstSbcDec)) +#define GST_SBC_DEC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SBC_DEC,GstSbcDecClass)) +#define GST_IS_SBC_DEC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SBC_DEC)) +#define GST_IS_SBC_DEC_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SBC_DEC)) + +typedef struct _GstSbcDec GstSbcDec; +typedef struct _GstSbcDecClass GstSbcDecClass; + +struct _GstSbcDec { + GstElement element; + + GstPad *sinkpad; + GstPad *srcpad; + + GstBuffer *buffer; + + /* caps for outgoing buffers */ + GstCaps *outcaps; + + sbc_t sbc; +}; + +struct _GstSbcDecClass { + GstElementClass parent_class; +}; + +GType gst_sbc_dec_get_type(void); + +gboolean gst_sbc_dec_plugin_init(GstPlugin *plugin); + +G_END_DECLS diff --git a/audio/gstsbcenc.c b/audio/gstsbcenc.c new file mode 100644 index 0000000..d2de4ee --- /dev/null +++ b/audio/gstsbcenc.c @@ -0,0 +1,603 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "gstpragma.h" +#include "gstsbcutil.h" +#include "gstsbcenc.h" + +#define SBC_ENC_DEFAULT_MODE SBC_MODE_AUTO +#define SBC_ENC_DEFAULT_BLOCKS 0 +#define SBC_ENC_DEFAULT_SUB_BANDS 0 +#define SBC_ENC_DEFAULT_ALLOCATION SBC_AM_AUTO +#define SBC_ENC_DEFAULT_RATE 0 +#define SBC_ENC_DEFAULT_CHANNELS 0 + +#define SBC_ENC_BITPOOL_AUTO 1 +#define SBC_ENC_BITPOOL_MIN 2 +#define SBC_ENC_BITPOOL_MIN_STR "2" +#define SBC_ENC_BITPOOL_MAX 64 +#define SBC_ENC_BITPOOL_MAX_STR "64" + +GST_DEBUG_CATEGORY_STATIC(sbc_enc_debug); +#define GST_CAT_DEFAULT sbc_enc_debug + +#define GST_TYPE_SBC_MODE (gst_sbc_mode_get_type()) + +static GType gst_sbc_mode_get_type(void) +{ + static GType sbc_mode_type = 0; + static GEnumValue sbc_modes[] = { + { SBC_MODE_MONO, "Mono", "mono" }, + { SBC_MODE_DUAL_CHANNEL, "Dual Channel", "dual" }, + { SBC_MODE_STEREO, "Stereo", "stereo"}, + { SBC_MODE_JOINT_STEREO, "Joint Stereo", "joint" }, + { SBC_MODE_AUTO, "Auto", "auto" }, + { -1, NULL, NULL} + }; + + if (!sbc_mode_type) + sbc_mode_type = g_enum_register_static("GstSbcMode", sbc_modes); + + return sbc_mode_type; +} + +#define GST_TYPE_SBC_ALLOCATION (gst_sbc_allocation_get_type()) + +static GType gst_sbc_allocation_get_type(void) +{ + static GType sbc_allocation_type = 0; + static GEnumValue sbc_allocations[] = { + { SBC_AM_LOUDNESS, "Loudness", "loudness" }, + { SBC_AM_SNR, "SNR", "snr" }, + { SBC_AM_AUTO, "Auto", "auto" }, + { -1, NULL, NULL} + }; + + if (!sbc_allocation_type) + sbc_allocation_type = g_enum_register_static( + "GstSbcAllocation", sbc_allocations); + + return sbc_allocation_type; +} + +#define GST_TYPE_SBC_BLOCKS (gst_sbc_blocks_get_type()) + +static GType gst_sbc_blocks_get_type(void) +{ + static GType sbc_blocks_type = 0; + static GEnumValue sbc_blocks[] = { + { 0, "Auto", "auto" }, + { 4, "4", "4" }, + { 8, "8", "8" }, + { 12, "12", "12" }, + { 16, "16", "16" }, + { -1, NULL, NULL} + }; + + if (!sbc_blocks_type) + sbc_blocks_type = g_enum_register_static( + "GstSbcBlocks", sbc_blocks); + + return sbc_blocks_type; +} + +#define GST_TYPE_SBC_SUBBANDS (gst_sbc_subbands_get_type()) + +static GType gst_sbc_subbands_get_type(void) +{ + static GType sbc_subbands_type = 0; + static GEnumValue sbc_subbands[] = { + { 0, "Auto", "auto" }, + { 4, "4 subbands", "4" }, + { 8, "8 subbands", "8" }, + { -1, NULL, NULL} + }; + + if (!sbc_subbands_type) + sbc_subbands_type = g_enum_register_static( + "GstSbcSubbands", sbc_subbands); + + return sbc_subbands_type; +} + +enum { + PROP_0, + PROP_MODE, + PROP_ALLOCATION, + PROP_BLOCKS, + PROP_SUBBANDS, + PROP_BITPOOL +}; + +GST_BOILERPLATE(GstSbcEnc, gst_sbc_enc, GstElement, GST_TYPE_ELEMENT); + +static const GstElementDetails sbc_enc_details = + GST_ELEMENT_DETAILS("Bluetooth SBC encoder", + "Codec/Encoder/Audio", + "Encode a SBC audio stream", + "Marcel Holtmann <marcel@holtmann.org>"); + +static GstStaticPadTemplate sbc_enc_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw-int, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "endianness = (int) BYTE_ORDER, " + "signed = (boolean) true, " + "width = (int) 16, " + "depth = (int) 16")); + +static GstStaticPadTemplate sbc_enc_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ " SBC_ENC_BITPOOL_MIN_STR + ", " SBC_ENC_BITPOOL_MAX_STR " ]")); + +gboolean gst_sbc_enc_fill_sbc_params(GstSbcEnc *enc, GstCaps *caps); + +static GstCaps *sbc_enc_generate_srcpad_caps(GstSbcEnc *enc) +{ + GstCaps *src_caps; + GstStructure *structure; + GEnumValue *enum_value; + GEnumClass *enum_class; + GValue *value; + + src_caps = gst_caps_copy(gst_pad_get_pad_template_caps(enc->srcpad)); + structure = gst_caps_get_structure(src_caps, 0); + + value = g_new0(GValue, 1); + + if (enc->rate != 0) + gst_sbc_util_set_structure_int_param(structure, "rate", + enc->rate, value); + + if (enc->channels != 0) + gst_sbc_util_set_structure_int_param(structure, "channels", + enc->channels, value); + + if (enc->subbands != 0) + gst_sbc_util_set_structure_int_param(structure, "subbands", + enc->subbands, value); + + if (enc->blocks != 0) + gst_sbc_util_set_structure_int_param(structure, "blocks", + enc->blocks, value); + + if (enc->bitpool != SBC_ENC_BITPOOL_AUTO) + gst_sbc_util_set_structure_int_param(structure, "bitpool", + enc->bitpool, value); + + if (enc->mode != SBC_ENC_DEFAULT_MODE) { + enum_class = g_type_class_ref(GST_TYPE_SBC_MODE); + enum_value = g_enum_get_value(enum_class, enc->mode); + gst_sbc_util_set_structure_string_param(structure, "mode", + enum_value->value_nick, value); + g_type_class_unref(enum_class); + } + + if (enc->allocation != SBC_AM_AUTO) { + enum_class = g_type_class_ref(GST_TYPE_SBC_ALLOCATION); + enum_value = g_enum_get_value(enum_class, enc->allocation); + gst_sbc_util_set_structure_string_param(structure, "allocation", + enum_value->value_nick, value); + g_type_class_unref(enum_class); + } + + g_free(value); + + return src_caps; +} + +static GstCaps *sbc_enc_src_getcaps(GstPad *pad) +{ + GstSbcEnc *enc; + + enc = GST_SBC_ENC(GST_PAD_PARENT(pad)); + + return sbc_enc_generate_srcpad_caps(enc); +} + +static gboolean sbc_enc_src_setcaps(GstPad *pad, GstCaps *caps) +{ + GstSbcEnc *enc = GST_SBC_ENC(GST_PAD_PARENT(pad)); + + GST_LOG_OBJECT(enc, "setting srcpad caps"); + + return gst_sbc_enc_fill_sbc_params(enc, caps); +} + +static GstCaps *sbc_enc_src_caps_fixate(GstSbcEnc *enc, GstCaps *caps) +{ + gchar *error_message = NULL; + GstCaps *result; + + result = gst_sbc_util_caps_fixate(caps, &error_message); + + if (!result) { + GST_WARNING_OBJECT(enc, "Invalid input caps caused parsing " + "error: %s", error_message); + g_free(error_message); + return NULL; + } + + return result; +} + +static GstCaps *sbc_enc_get_fixed_srcpad_caps(GstSbcEnc *enc) +{ + GstCaps *caps; + gboolean res = TRUE; + GstCaps *result_caps = NULL; + + caps = gst_pad_get_allowed_caps(enc->srcpad); + if (caps == NULL) + caps = sbc_enc_src_getcaps(enc->srcpad); + + if (caps == GST_CAPS_NONE || gst_caps_is_empty(caps)) { + res = FALSE; + goto done; + } + + result_caps = sbc_enc_src_caps_fixate(enc, caps); + +done: + gst_caps_unref(caps); + + if (!res) + return NULL; + + return result_caps; +} + +static gboolean sbc_enc_sink_setcaps(GstPad *pad, GstCaps *caps) +{ + GstSbcEnc *enc; + GstStructure *structure; + GstCaps *src_caps; + gint rate, channels; + gboolean res; + + enc = GST_SBC_ENC(GST_PAD_PARENT(pad)); + structure = gst_caps_get_structure(caps, 0); + + if (!gst_structure_get_int(structure, "rate", &rate)) + return FALSE; + if (!gst_structure_get_int(structure, "channels", &channels)) + return FALSE; + + enc->rate = rate; + enc->channels = channels; + + src_caps = sbc_enc_get_fixed_srcpad_caps(enc); + if (!src_caps) + return FALSE; + res = gst_pad_set_caps(enc->srcpad, src_caps); + gst_caps_unref(src_caps); + + return res; +} + +gboolean gst_sbc_enc_fill_sbc_params(GstSbcEnc *enc, GstCaps *caps) +{ + if (!gst_caps_is_fixed(caps)) { + GST_DEBUG_OBJECT(enc, "didn't receive fixed caps, " + "returning false"); + return FALSE; + } + + if (!gst_sbc_util_fill_sbc_params(&enc->sbc, caps)) + return FALSE; + + if (enc->rate != 0 && gst_sbc_parse_rate_from_sbc(enc->sbc.frequency) + != enc->rate) + goto fail; + + if (enc->channels != 0 && gst_sbc_get_channel_number(enc->sbc.mode) + != enc->channels) + goto fail; + + if (enc->blocks != 0 && gst_sbc_parse_blocks_from_sbc(enc->sbc.blocks) + != enc->blocks) + goto fail; + + if (enc->subbands != 0 && gst_sbc_parse_subbands_from_sbc( + enc->sbc.subbands) != enc->subbands) + goto fail; + + if (enc->mode != SBC_ENC_DEFAULT_MODE && enc->sbc.mode != enc->mode) + goto fail; + + if (enc->allocation != SBC_AM_AUTO && + enc->sbc.allocation != enc->allocation) + goto fail; + + if (enc->bitpool != SBC_ENC_BITPOOL_AUTO && + enc->sbc.bitpool != enc->bitpool) + goto fail; + + enc->codesize = sbc_get_codesize(&enc->sbc); + enc->frame_length = sbc_get_frame_length(&enc->sbc); + enc->frame_duration = sbc_get_frame_duration(&enc->sbc); + + GST_DEBUG_OBJECT(enc, "codesize: %d, frame_length: %d, frame_duration:" + " %d", enc->codesize, enc->frame_length, + enc->frame_duration); + + return TRUE; + +fail: + memset(&enc->sbc, 0, sizeof(sbc_t)); + return FALSE; +} + +static GstFlowReturn sbc_enc_chain(GstPad *pad, GstBuffer *buffer) +{ + GstSbcEnc *enc = GST_SBC_ENC(gst_pad_get_parent(pad)); + GstAdapter *adapter = enc->adapter; + GstFlowReturn res = GST_FLOW_OK; + + gst_adapter_push(adapter, buffer); + + while (gst_adapter_available(adapter) >= enc->codesize && + res == GST_FLOW_OK) { + GstBuffer *output; + GstCaps *caps; + const guint8 *data; + gint consumed; + + caps = GST_PAD_CAPS(enc->srcpad); + res = gst_pad_alloc_buffer_and_set_caps(enc->srcpad, + GST_BUFFER_OFFSET_NONE, + enc->frame_length, caps, + &output); + if (res != GST_FLOW_OK) + goto done; + + data = gst_adapter_peek(adapter, enc->codesize); + + consumed = sbc_encode(&enc->sbc, (gpointer) data, + enc->codesize, + GST_BUFFER_DATA(output), + GST_BUFFER_SIZE(output), NULL); + if (consumed <= 0) { + GST_DEBUG_OBJECT(enc, "comsumed < 0, codesize: %d", + enc->codesize); + break; + } + gst_adapter_flush(adapter, consumed); + + GST_BUFFER_TIMESTAMP(output) = GST_BUFFER_TIMESTAMP(buffer); + /* we have only 1 frame */ + GST_BUFFER_DURATION(output) = enc->frame_duration; + + res = gst_pad_push(enc->srcpad, output); + + if (res != GST_FLOW_OK) + goto done; + } + +done: + gst_object_unref(enc); + + return res; +} + +static GstStateChangeReturn sbc_enc_change_state(GstElement *element, + GstStateChange transition) +{ + GstSbcEnc *enc = GST_SBC_ENC(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG("Setup subband codec"); + sbc_init(&enc->sbc, 0); + break; + + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG("Finish subband codec"); + sbc_finish(&enc->sbc); + break; + + default: + break; + } + + return parent_class->change_state(element, transition); +} + +static void gst_sbc_enc_dispose(GObject *object) +{ + GstSbcEnc *enc = GST_SBC_ENC(object); + + if (enc->adapter != NULL) + g_object_unref(G_OBJECT(enc->adapter)); + + enc->adapter = NULL; +} + +static void gst_sbc_enc_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_enc_sink_factory)); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_enc_src_factory)); + + gst_element_class_set_details(element_class, &sbc_enc_details); +} + +static void gst_sbc_enc_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstSbcEnc *enc = GST_SBC_ENC(object); + + /* changes to those properties will only happen on the next caps + * negotiation */ + + switch (prop_id) { + case PROP_MODE: + enc->mode = g_value_get_enum(value); + break; + case PROP_ALLOCATION: + enc->allocation = g_value_get_enum(value); + break; + case PROP_BLOCKS: + enc->blocks = g_value_get_enum(value); + break; + case PROP_SUBBANDS: + enc->subbands = g_value_get_enum(value); + break; + case PROP_BITPOOL: + enc->bitpool = g_value_get_int(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_sbc_enc_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstSbcEnc *enc = GST_SBC_ENC(object); + + switch (prop_id) { + case PROP_MODE: + g_value_set_enum(value, enc->mode); + break; + case PROP_ALLOCATION: + g_value_set_enum(value, enc->allocation); + break; + case PROP_BLOCKS: + g_value_set_enum(value, enc->blocks); + break; + case PROP_SUBBANDS: + g_value_set_enum(value, enc->subbands); + break; + case PROP_BITPOOL: + g_value_set_int(value, enc->bitpool); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_sbc_enc_class_init(GstSbcEncClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->set_property = GST_DEBUG_FUNCPTR(gst_sbc_enc_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR(gst_sbc_enc_get_property); + object_class->dispose = GST_DEBUG_FUNCPTR(gst_sbc_enc_dispose); + + element_class->change_state = GST_DEBUG_FUNCPTR(sbc_enc_change_state); + + g_object_class_install_property(object_class, PROP_MODE, + g_param_spec_enum("mode", "Mode", + "Encoding mode", GST_TYPE_SBC_MODE, + SBC_ENC_DEFAULT_MODE, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_ALLOCATION, + g_param_spec_enum("allocation", "Allocation", + "Allocation method", GST_TYPE_SBC_ALLOCATION, + SBC_ENC_DEFAULT_ALLOCATION, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_BLOCKS, + g_param_spec_enum("blocks", "Blocks", + "Blocks", GST_TYPE_SBC_BLOCKS, + SBC_ENC_DEFAULT_BLOCKS, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_SUBBANDS, + g_param_spec_enum("subbands", "Sub bands", + "Number of sub bands", GST_TYPE_SBC_SUBBANDS, + SBC_ENC_DEFAULT_SUB_BANDS, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_BITPOOL, + g_param_spec_int("bitpool", "Bitpool", + "Bitpool (use 1 for automatic selection)", + SBC_ENC_BITPOOL_AUTO, SBC_ENC_BITPOOL_MAX, + SBC_ENC_BITPOOL_AUTO, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(sbc_enc_debug, "sbcenc", 0, + "SBC encoding element"); +} + +static void gst_sbc_enc_init(GstSbcEnc *self, GstSbcEncClass *klass) +{ + self->sinkpad = gst_pad_new_from_static_template( + &sbc_enc_sink_factory, "sink"); + gst_pad_set_setcaps_function(self->sinkpad, + GST_DEBUG_FUNCPTR(sbc_enc_sink_setcaps)); + gst_element_add_pad(GST_ELEMENT(self), self->sinkpad); + + self->srcpad = gst_pad_new_from_static_template( + &sbc_enc_src_factory, "src"); + gst_pad_set_getcaps_function(self->srcpad, + GST_DEBUG_FUNCPTR(sbc_enc_src_getcaps)); + gst_pad_set_setcaps_function(self->srcpad, + GST_DEBUG_FUNCPTR(sbc_enc_src_setcaps)); + gst_element_add_pad(GST_ELEMENT(self), self->srcpad); + + gst_pad_set_chain_function(self->sinkpad, + GST_DEBUG_FUNCPTR(sbc_enc_chain)); + + self->subbands = SBC_ENC_DEFAULT_SUB_BANDS; + self->blocks = SBC_ENC_DEFAULT_BLOCKS; + self->mode = SBC_ENC_DEFAULT_MODE; + self->allocation = SBC_ENC_DEFAULT_ALLOCATION; + self->rate = SBC_ENC_DEFAULT_RATE; + self->channels = SBC_ENC_DEFAULT_CHANNELS; + self->bitpool = SBC_ENC_BITPOOL_AUTO; + + self->frame_length = 0; + self->frame_duration = 0; + + self->adapter = gst_adapter_new(); +} + +gboolean gst_sbc_enc_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "sbcenc", + GST_RANK_NONE, GST_TYPE_SBC_ENC); +} + + diff --git a/audio/gstsbcenc.h b/audio/gstsbcenc.h new file mode 100644 index 0000000..0329351 --- /dev/null +++ b/audio/gstsbcenc.h @@ -0,0 +1,75 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 <gst/gst.h> +#include <gst/base/gstadapter.h> + +#include "sbc.h" + +G_BEGIN_DECLS + +#define GST_TYPE_SBC_ENC \ + (gst_sbc_enc_get_type()) +#define GST_SBC_ENC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SBC_ENC,GstSbcEnc)) +#define GST_SBC_ENC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SBC_ENC,GstSbcEncClass)) +#define GST_IS_SBC_ENC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SBC_ENC)) +#define GST_IS_SBC_ENC_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SBC_ENC)) + +typedef struct _GstSbcEnc GstSbcEnc; +typedef struct _GstSbcEncClass GstSbcEncClass; + +struct _GstSbcEnc { + GstElement element; + + GstPad *sinkpad; + GstPad *srcpad; + GstAdapter *adapter; + + gint rate; + gint channels; + gint mode; + gint blocks; + gint allocation; + gint subbands; + gint bitpool; + + guint codesize; + gint frame_length; + gint frame_duration; + + sbc_t sbc; +}; + +struct _GstSbcEncClass { + GstElementClass parent_class; +}; + +GType gst_sbc_enc_get_type(void); + +gboolean gst_sbc_enc_plugin_init(GstPlugin *plugin); + +G_END_DECLS diff --git a/audio/gstsbcparse.c b/audio/gstsbcparse.c new file mode 100644 index 0000000..a44b52c --- /dev/null +++ b/audio/gstsbcparse.c @@ -0,0 +1,220 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> + +#include "gstpragma.h" +#include "gstsbcutil.h" +#include "gstsbcparse.h" + +GST_DEBUG_CATEGORY_STATIC(sbc_parse_debug); +#define GST_CAT_DEFAULT sbc_parse_debug + +GST_BOILERPLATE(GstSbcParse, gst_sbc_parse, GstElement, GST_TYPE_ELEMENT); + +static const GstElementDetails sbc_parse_details = + GST_ELEMENT_DETAILS("Bluetooth SBC parser", + "Codec/Parser/Audio", + "Parse a SBC audio stream", + "Marcel Holtmann <marcel@holtmann.org>"); + +static GstStaticPadTemplate sbc_parse_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc," + "parsed = (boolean) false")); + +static GstStaticPadTemplate sbc_parse_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }," + "bitpool = (int) [ 2, 64 ]," + "parsed = (boolean) true")); + +static GstFlowReturn sbc_parse_chain(GstPad *pad, GstBuffer *buffer) +{ + GstSbcParse *parse = GST_SBC_PARSE(gst_pad_get_parent(pad)); + GstFlowReturn res = GST_FLOW_OK; + guint size, offset = 0; + guint8 *data; + + /* FIXME use a gstadpter */ + if (parse->buffer) { + GstBuffer *temp; + temp = buffer; + buffer = gst_buffer_span(parse->buffer, 0, buffer, + GST_BUFFER_SIZE(parse->buffer) + + GST_BUFFER_SIZE(buffer)); + gst_buffer_unref(parse->buffer); + gst_buffer_unref(temp); + parse->buffer = NULL; + } + + data = GST_BUFFER_DATA(buffer); + size = GST_BUFFER_SIZE(buffer); + + while (offset < size) { + GstBuffer *output; + int consumed; + + consumed = sbc_parse(&parse->new_sbc, data + offset, + size - offset); + if (consumed <= 0) + break; + + if (parse->first_parsing || (memcmp(&parse->sbc, + &parse->new_sbc, sizeof(sbc_t)) != 0)) { + + memcpy(&parse->sbc, &parse->new_sbc, sizeof(sbc_t)); + if (parse->outcaps != NULL) + gst_caps_unref(parse->outcaps); + + parse->outcaps = gst_sbc_parse_caps_from_sbc( + &parse->sbc); + + parse->first_parsing = FALSE; + } + + res = gst_pad_alloc_buffer_and_set_caps(parse->srcpad, + GST_BUFFER_OFFSET_NONE, + consumed, parse->outcaps, &output); + + if (res != GST_FLOW_OK) + goto done; + + memcpy(GST_BUFFER_DATA(output), data + offset, consumed); + + res = gst_pad_push(parse->srcpad, output); + if (res != GST_FLOW_OK) + goto done; + + offset += consumed; + } + + if (offset < size) + parse->buffer = gst_buffer_create_sub(buffer, + offset, size - offset); + +done: + gst_buffer_unref(buffer); + gst_object_unref(parse); + + return res; +} + +static GstStateChangeReturn sbc_parse_change_state(GstElement *element, + GstStateChange transition) +{ + GstSbcParse *parse = GST_SBC_PARSE(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG("Setup subband codec"); + + parse->channels = -1; + parse->rate = -1; + parse->first_parsing = TRUE; + + sbc_init(&parse->sbc, 0); + break; + + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG("Finish subband codec"); + + if (parse->buffer) { + gst_buffer_unref(parse->buffer); + parse->buffer = NULL; + } + if (parse->outcaps != NULL) { + gst_caps_unref(parse->outcaps); + parse->outcaps = NULL; + } + + sbc_finish(&parse->sbc); + break; + + default: + break; + } + + return parent_class->change_state(element, transition); +} + +static void gst_sbc_parse_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_parse_sink_factory)); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_parse_src_factory)); + + gst_element_class_set_details(element_class, &sbc_parse_details); +} + +static void gst_sbc_parse_class_init(GstSbcParseClass *klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + element_class->change_state = GST_DEBUG_FUNCPTR(sbc_parse_change_state); + + GST_DEBUG_CATEGORY_INIT(sbc_parse_debug, "sbcparse", 0, + "SBC parsing element"); +} + +static void gst_sbc_parse_init(GstSbcParse *self, GstSbcParseClass *klass) +{ + self->sinkpad = gst_pad_new_from_static_template( + &sbc_parse_sink_factory, "sink"); + gst_pad_set_chain_function(self->sinkpad, + GST_DEBUG_FUNCPTR(sbc_parse_chain)); + gst_element_add_pad(GST_ELEMENT(self), self->sinkpad); + + self->srcpad = gst_pad_new_from_static_template( + &sbc_parse_src_factory, "src"); + gst_element_add_pad(GST_ELEMENT(self), self->srcpad); + + self->outcaps = NULL; + self->buffer = NULL; + self->channels = -1; + self->rate = -1; + self->first_parsing = TRUE; +} + +gboolean gst_sbc_parse_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "sbcparse", GST_RANK_NONE, + GST_TYPE_SBC_PARSE); +} + diff --git a/audio/gstsbcparse.h b/audio/gstsbcparse.h new file mode 100644 index 0000000..ecb8be4 --- /dev/null +++ b/audio/gstsbcparse.h @@ -0,0 +1,69 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 <gst/gst.h> + +#include "sbc.h" + +G_BEGIN_DECLS + +#define GST_TYPE_SBC_PARSE \ + (gst_sbc_parse_get_type()) +#define GST_SBC_PARSE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SBC_PARSE,GstSbcParse)) +#define GST_SBC_PARSE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SBC_PARSE,GstSbcParseClass)) +#define GST_IS_SBC_PARSE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SBC_PARSE)) +#define GST_IS_SBC_PARSE_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SBC_PARSE)) + +typedef struct _GstSbcParse GstSbcParse; +typedef struct _GstSbcParseClass GstSbcParseClass; + +struct _GstSbcParse { + GstElement element; + + GstPad *sinkpad; + GstPad *srcpad; + + GstBuffer *buffer; + + sbc_t sbc; + sbc_t new_sbc; + GstCaps *outcaps; + gboolean first_parsing; + + gint channels; + gint rate; +}; + +struct _GstSbcParseClass { + GstElementClass parent_class; +}; + +GType gst_sbc_parse_get_type(void); + +gboolean gst_sbc_parse_plugin_init(GstPlugin *plugin); + +G_END_DECLS diff --git a/audio/gstsbcutil.c b/audio/gstsbcutil.c new file mode 100644 index 0000000..63c90c2 --- /dev/null +++ b/audio/gstsbcutil.c @@ -0,0 +1,521 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <math.h> +#include "gstsbcutil.h" + +/* + * Selects one rate from a list of possible rates + * TODO - use a better approach to this (it is selecting the last element) + */ +gint gst_sbc_select_rate_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_int(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one number of channels option from a range of possible numbers + * TODO - use a better approach to this (it is selecting the maximum value) + */ +gint gst_sbc_select_channels_from_range(const GValue *value) +{ + return gst_value_get_int_range_max(value); +} + +/* + * Selects one number of blocks from a list of possible blocks + * TODO - use a better approach to this (it is selecting the last element) + */ +gint gst_sbc_select_blocks_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_int(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one number of subbands from a list + * TODO - use a better approach to this (it is selecting the last element) + */ +gint gst_sbc_select_subbands_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_int(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one bitpool option from a range + * TODO - use a better approach to this (it is selecting the maximum value) + */ +gint gst_sbc_select_bitpool_from_range(const GValue *value) +{ + return gst_value_get_int_range_max(value); +} + +/* + * Selects one allocation mode from the ones on the list + * TODO - use a better approach + */ +const gchar *gst_sbc_get_allocation_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_string(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one mode from the ones on the list + */ +const gchar *gst_sbc_get_mode_from_list(const GValue *list, gint channels) +{ + unsigned int i; + const GValue *value; + const gchar *aux; + gboolean joint, stereo, dual, mono; + guint size = gst_value_list_get_size(list); + + joint = stereo = dual = mono = FALSE; + + for (i = 0; i < size; i++) { + value = gst_value_list_get_value(list, i); + aux = g_value_get_string(value); + if (strcmp("joint", aux) == 0) + joint = TRUE; + else if (strcmp("stereo", aux) == 0) + stereo = TRUE; + else if (strcmp("dual", aux) == 0) + dual = TRUE; + else if (strcmp("mono", aux) == 0) + mono = TRUE; + } + + if (channels == 1 && mono) + return "mono"; + else if (channels == 2) { + if (joint) + return "joint"; + else if (stereo) + return "stereo"; + else if (dual) + return "dual"; + } + + return NULL; +} + +gint gst_sbc_parse_rate_from_sbc(gint frequency) +{ + switch (frequency) { + case SBC_FREQ_16000: + return 16000; + case SBC_FREQ_32000: + return 32000; + case SBC_FREQ_44100: + return 44100; + case SBC_FREQ_48000: + return 48000; + default: + return 0; + } +} + +gint gst_sbc_parse_rate_to_sbc(gint rate) +{ + switch (rate) { + case 16000: + return SBC_FREQ_16000; + case 32000: + return SBC_FREQ_32000; + case 44100: + return SBC_FREQ_44100; + case 48000: + return SBC_FREQ_48000; + default: + return -1; + } +} + +gint gst_sbc_get_channel_number(gint mode) +{ + switch (mode) { + case SBC_MODE_JOINT_STEREO: + case SBC_MODE_STEREO: + case SBC_MODE_DUAL_CHANNEL: + return 2; + case SBC_MODE_MONO: + return 1; + default: + return 0; + } +} + +gint gst_sbc_parse_subbands_from_sbc(gint subbands) +{ + switch (subbands) { + case SBC_SB_4: + return 4; + case SBC_SB_8: + return 8; + default: + return 0; + } +} + +gint gst_sbc_parse_subbands_to_sbc(gint subbands) +{ + switch (subbands) { + case 4: + return SBC_SB_4; + case 8: + return SBC_SB_8; + default: + return -1; + } +} + +gint gst_sbc_parse_blocks_from_sbc(gint blocks) +{ + switch (blocks) { + case SBC_BLK_4: + return 4; + case SBC_BLK_8: + return 8; + case SBC_BLK_12: + return 12; + case SBC_BLK_16: + return 16; + default: + return 0; + } +} + +gint gst_sbc_parse_blocks_to_sbc(gint blocks) +{ + switch (blocks) { + case 4: + return SBC_BLK_4; + case 8: + return SBC_BLK_8; + case 12: + return SBC_BLK_12; + case 16: + return SBC_BLK_16; + default: + return -1; + } +} + +const gchar *gst_sbc_parse_mode_from_sbc(gint mode) +{ + switch (mode) { + case SBC_MODE_MONO: + return "mono"; + case SBC_MODE_DUAL_CHANNEL: + return "dual"; + case SBC_MODE_STEREO: + return "stereo"; + case SBC_MODE_JOINT_STEREO: + case SBC_MODE_AUTO: + return "joint"; + default: + return NULL; + } +} + +gint gst_sbc_parse_mode_to_sbc(const gchar *mode) +{ + if (g_ascii_strcasecmp(mode, "joint") == 0) + return SBC_MODE_JOINT_STEREO; + else if (g_ascii_strcasecmp(mode, "stereo") == 0) + return SBC_MODE_STEREO; + else if (g_ascii_strcasecmp(mode, "dual") == 0) + return SBC_MODE_DUAL_CHANNEL; + else if (g_ascii_strcasecmp(mode, "mono") == 0) + return SBC_MODE_MONO; + else if (g_ascii_strcasecmp(mode, "auto") == 0) + return SBC_MODE_JOINT_STEREO; + else + return -1; +} + +const gchar *gst_sbc_parse_allocation_from_sbc(gint alloc) +{ + switch (alloc) { + case SBC_AM_LOUDNESS: + return "loudness"; + case SBC_AM_SNR: + return "snr"; + case SBC_AM_AUTO: + return "loudness"; + default: + return NULL; + } +} + +gint gst_sbc_parse_allocation_to_sbc(const gchar *allocation) +{ + if (g_ascii_strcasecmp(allocation, "loudness") == 0) + return SBC_AM_LOUDNESS; + else if (g_ascii_strcasecmp(allocation, "snr") == 0) + return SBC_AM_SNR; + else + return SBC_AM_LOUDNESS; +} + +GstCaps *gst_sbc_parse_caps_from_sbc(sbc_t *sbc) +{ + GstCaps *caps; + const gchar *mode_str; + const gchar *allocation_str; + + mode_str = gst_sbc_parse_mode_from_sbc(sbc->mode); + allocation_str = gst_sbc_parse_allocation_from_sbc(sbc->allocation); + caps = gst_caps_new_simple("audio/x-sbc", + "rate", G_TYPE_INT, + gst_sbc_parse_rate_from_sbc(sbc->frequency), + "channels", G_TYPE_INT, + gst_sbc_get_channel_number(sbc->mode), + "mode", G_TYPE_STRING, mode_str, + "subbands", G_TYPE_INT, + gst_sbc_parse_subbands_from_sbc(sbc->subbands), + "blocks", G_TYPE_INT, + gst_sbc_parse_blocks_from_sbc(sbc->blocks), + "allocation", G_TYPE_STRING, allocation_str, + "bitpool", G_TYPE_INT, sbc->bitpool, + NULL); + + return caps; +} + +/* + * Given a GstCaps, this will return a fixed GstCaps on sucessfull conversion. + * If an error occurs, it will return NULL and error_message will contain the + * error message. + * + * error_message must be passed NULL, if an error occurs, the caller has the + * ownership of the error_message, it must be freed after use. + */ +GstCaps *gst_sbc_util_caps_fixate(GstCaps *caps, gchar **error_message) +{ + GstCaps *result; + GstStructure *structure; + const GValue *value; + gboolean error = FALSE; + gint temp, rate, channels, blocks, subbands, bitpool; + const gchar *allocation = NULL; + const gchar *mode = NULL; + + g_assert(*error_message == NULL); + + structure = gst_caps_get_structure(caps, 0); + + if (!gst_structure_has_field(structure, "rate")) { + error = TRUE; + *error_message = g_strdup("no rate"); + goto error; + } else { + value = gst_structure_get_value(structure, "rate"); + if (GST_VALUE_HOLDS_LIST(value)) + temp = gst_sbc_select_rate_from_list(value); + else + temp = g_value_get_int(value); + rate = temp; + } + + if (!gst_structure_has_field(structure, "channels")) { + error = TRUE; + *error_message = g_strdup("no channels"); + goto error; + } else { + value = gst_structure_get_value(structure, "channels"); + if (GST_VALUE_HOLDS_INT_RANGE(value)) + temp = gst_sbc_select_channels_from_range(value); + else + temp = g_value_get_int(value); + channels = temp; + } + + if (!gst_structure_has_field(structure, "blocks")) { + error = TRUE; + *error_message = g_strdup("no blocks."); + goto error; + } else { + value = gst_structure_get_value(structure, "blocks"); + if (GST_VALUE_HOLDS_LIST(value)) + temp = gst_sbc_select_blocks_from_list(value); + else + temp = g_value_get_int(value); + blocks = temp; + } + + if (!gst_structure_has_field(structure, "subbands")) { + error = TRUE; + *error_message = g_strdup("no subbands"); + goto error; + } else { + value = gst_structure_get_value(structure, "subbands"); + if (GST_VALUE_HOLDS_LIST(value)) + temp = gst_sbc_select_subbands_from_list(value); + else + temp = g_value_get_int(value); + subbands = temp; + } + + if (!gst_structure_has_field(structure, "bitpool")) { + error = TRUE; + *error_message = g_strdup("no bitpool"); + goto error; + } else { + value = gst_structure_get_value(structure, "bitpool"); + if (GST_VALUE_HOLDS_INT_RANGE(value)) + temp = gst_sbc_select_bitpool_from_range(value); + else + temp = g_value_get_int(value); + bitpool = temp; + } + + if (!gst_structure_has_field(structure, "allocation")) { + error = TRUE; + *error_message = g_strdup("no allocation"); + goto error; + } else { + value = gst_structure_get_value(structure, "allocation"); + if (GST_VALUE_HOLDS_LIST(value)) + allocation = gst_sbc_get_allocation_from_list(value); + else + allocation = g_value_get_string(value); + } + + if (!gst_structure_has_field(structure, "mode")) { + error = TRUE; + *error_message = g_strdup("no mode"); + goto error; + } else { + value = gst_structure_get_value(structure, "mode"); + if (GST_VALUE_HOLDS_LIST(value)) { + mode = gst_sbc_get_mode_from_list(value, channels); + } else + mode = g_value_get_string(value); + } + + /* perform validation + * if channels is 1, we must have channel mode = mono + * if channels is 2, we can't have channel mode = mono */ + if ( (channels == 1 && (strcmp(mode, "mono") != 0) ) || + ( channels == 2 && ( strcmp(mode, "mono") == 0))) { + *error_message = g_strdup_printf("Invalid combination of " + "channels (%d) and channel mode (%s)", + channels, mode); + error = TRUE; + } + +error: + if (error) + return NULL; + + result = gst_caps_new_simple("audio/x-sbc", + "rate", G_TYPE_INT, rate, + "channels", G_TYPE_INT, channels, + "mode", G_TYPE_STRING, mode, + "blocks", G_TYPE_INT, blocks, + "subbands", G_TYPE_INT, subbands, + "allocation", G_TYPE_STRING, allocation, + "bitpool", G_TYPE_INT, bitpool, + NULL); + + return result; +} + +/** + * Sets the int field_value to the param "field" on the structure. + * value is used to do the operation, it must be a uninitialized (zero-filled) + * GValue, it will be left unitialized at the end of the function. + */ +void gst_sbc_util_set_structure_int_param(GstStructure *structure, + const gchar *field, gint field_value, + GValue *value) +{ + value = g_value_init(value, G_TYPE_INT); + g_value_set_int(value, field_value); + gst_structure_set_value(structure, field, value); + g_value_unset(value); +} + +/** + * Sets the string field_value to the param "field" on the structure. + * value is used to do the operation, it must be a uninitialized (zero-filled) + * GValue, it will be left unitialized at the end of the function. + */ +void gst_sbc_util_set_structure_string_param(GstStructure *structure, + const gchar *field, const gchar *field_value, + GValue *value) +{ + value = g_value_init(value, G_TYPE_STRING); + g_value_set_string(value, field_value); + gst_structure_set_value(structure, field, value); + g_value_unset(value); +} + +gboolean gst_sbc_util_fill_sbc_params(sbc_t *sbc, GstCaps *caps) +{ + GstStructure *structure; + gint rate, channels, subbands, blocks, bitpool; + const gchar *mode; + const gchar *allocation; + + g_assert(gst_caps_is_fixed(caps)); + + structure = gst_caps_get_structure(caps, 0); + + if (!gst_structure_get_int(structure, "rate", &rate)) + return FALSE; + if (!gst_structure_get_int(structure, "channels", &channels)) + return FALSE; + if (!gst_structure_get_int(structure, "subbands", &subbands)) + return FALSE; + if (!gst_structure_get_int(structure, "blocks", &blocks)) + return FALSE; + if (!gst_structure_get_int(structure, "bitpool", &bitpool)) + return FALSE; + + if (!(mode = gst_structure_get_string(structure, "mode"))) + return FALSE; + if (!(allocation = gst_structure_get_string(structure, "allocation"))) + return FALSE; + + if (channels == 1 && strcmp(mode, "mono") != 0) + return FALSE; + + sbc->frequency = gst_sbc_parse_rate_to_sbc(rate); + sbc->blocks = gst_sbc_parse_blocks_to_sbc(blocks); + sbc->subbands = gst_sbc_parse_subbands_to_sbc(subbands); + sbc->bitpool = bitpool; + sbc->mode = gst_sbc_parse_mode_to_sbc(mode); + sbc->allocation = gst_sbc_parse_allocation_to_sbc(allocation); + + return TRUE; +} + diff --git a/audio/gstsbcutil.h b/audio/gstsbcutil.h new file mode 100644 index 0000000..a7f84d5 --- /dev/null +++ b/audio/gstsbcutil.h @@ -0,0 +1,75 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 <gst/gst.h> + +#include "sbc.h" +#include <string.h> + +#define SBC_AM_AUTO 0x02 +#define SBC_MODE_AUTO 0x04 + +gint gst_sbc_select_rate_from_list(const GValue *value); + +gint gst_sbc_select_channels_from_range(const GValue *value); + +gint gst_sbc_select_blocks_from_list(const GValue *value); + +gint gst_sbc_select_subbands_from_list(const GValue *value); + +gint gst_sbc_select_bitpool_from_range(const GValue *value); + +const gchar *gst_sbc_get_allocation_from_list(const GValue *value); + +const gchar *gst_sbc_get_mode_from_list(const GValue *value, gint channels); + +gint gst_sbc_get_channel_number(gint mode); +gint gst_sbc_parse_rate_from_sbc(gint frequency); +gint gst_sbc_parse_rate_to_sbc(gint rate); + +gint gst_sbc_parse_subbands_from_sbc(gint subbands); +gint gst_sbc_parse_subbands_to_sbc(gint subbands); + +gint gst_sbc_parse_blocks_from_sbc(gint blocks); +gint gst_sbc_parse_blocks_to_sbc(gint blocks); + +const gchar *gst_sbc_parse_mode_from_sbc(gint mode); +gint gst_sbc_parse_mode_to_sbc(const gchar *mode); + +const gchar *gst_sbc_parse_allocation_from_sbc(gint alloc); +gint gst_sbc_parse_allocation_to_sbc(const gchar *allocation); + +GstCaps* gst_sbc_parse_caps_from_sbc(sbc_t *sbc); + +GstCaps* gst_sbc_util_caps_fixate(GstCaps *caps, gchar** error_message); + +void gst_sbc_util_set_structure_int_param(GstStructure *structure, + const gchar* field, gint field_value, + GValue *value); + +void gst_sbc_util_set_structure_string_param(GstStructure *structure, + const gchar* field, const gchar* field_value, + GValue *value); + +gboolean gst_sbc_util_fill_sbc_params(sbc_t *sbc, GstCaps *caps); + diff --git a/audio/headset.c b/audio/headset.c new file mode 100644 index 0000000..dff10d1 --- /dev/null +++ b/audio/headset.c @@ -0,0 +1,2951 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdarg.h> +#include <signal.h> +#include <string.h> +#include <getopt.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <assert.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "device.h" +#include "manager.h" +#include "error.h" +#include "telephony.h" +#include "headset.h" +#include "glib-helper.h" +#include "btio.h" +#include "dbus-common.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#define DC_TIMEOUT 3 + +#define RING_INTERVAL 3 + +#define BUF_SIZE 1024 + +#define HEADSET_GAIN_SPEAKER 'S' +#define HEADSET_GAIN_MICROPHONE 'M' + +static struct { + gboolean telephony_ready; /* Telephony plugin initialized */ + uint32_t features; /* HFP AG features */ + const struct indicator *indicators; /* Available HFP indicators */ + int er_mode; /* Event reporting mode */ + int er_ind; /* Event reporting for indicators */ + int rh; /* Response and Hold state */ + char *number; /* Incoming phone number */ + int number_type; /* Incoming number type */ + guint ring_timer; /* For incoming call indication */ + const char *chld; /* Response to AT+CHLD=? */ +} ag = { + .telephony_ready = FALSE, + .features = 0, + .er_mode = 3, + .er_ind = 0, + .rh = BTRH_NOT_SUPPORTED, + .number = NULL, + .number_type = 0, + .ring_timer = 0, +}; + +static gboolean sco_hci = TRUE; +static gboolean fast_connectable = FALSE; + +static GSList *active_devices = NULL; + +static char *str_state[] = { + "HEADSET_STATE_DISCONNECTED", + "HEADSET_STATE_CONNECTING", + "HEADSET_STATE_CONNECTED", + "HEADSET_STATE_PLAY_IN_PROGRESS", + "HEADSET_STATE_PLAYING", +}; + +struct headset_state_callback { + headset_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct headset_nrec_callback { + unsigned int id; + headset_nrec_cb cb; + void *user_data; +}; + +struct connect_cb { + unsigned int id; + headset_stream_cb_t cb; + void *cb_data; +}; + +struct pending_connect { + DBusMessage *msg; + DBusPendingCall *call; + GIOChannel *io; + int err; + headset_state_t target_state; + GSList *callbacks; + uint16_t svclass; +}; + +struct headset_slc { + char buf[BUF_SIZE]; + int data_start; + int data_length; + + gboolean cli_active; + gboolean cme_enabled; + gboolean cwa_enabled; + gboolean pending_ring; + gboolean inband_ring; + gboolean nrec; + gboolean nrec_req; + + int sp_gain; + int mic_gain; + + unsigned int hf_features; +}; + +struct headset { + uint32_t hsp_handle; + uint32_t hfp_handle; + + int rfcomm_ch; + + GIOChannel *rfcomm; + GIOChannel *tmp_rfcomm; + GIOChannel *sco; + guint sco_id; + + gboolean auto_dc; + + guint dc_timer; + + gboolean hfp_active; + gboolean search_hfp; + + headset_state_t state; + struct pending_connect *pending; + + headset_lock_t lock; + struct headset_slc *slc; + GSList *nrec_cbs; +}; + +struct event { + const char *cmd; + int (*callback) (struct audio_device *device, const char *buf); +}; + +static GSList *headset_callbacks = NULL; + +static void error_connect_failed(DBusConnection *conn, DBusMessage *msg, + int err) +{ + DBusMessage *reply = btd_error_failed(msg, + err < 0 ? strerror(-err) : "Connect failed"); + g_dbus_send_message(conn, reply); +} + +static int rfcomm_connect(struct audio_device *device, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id); +static int get_records(struct audio_device *device, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id); + +static void print_ag_features(uint32_t features) +{ + GString *gstr; + char *str; + + if (features == 0) { + DBG("HFP AG features: (none)"); + return; + } + + gstr = g_string_new("HFP AG features: "); + + if (features & AG_FEATURE_THREE_WAY_CALLING) + g_string_append(gstr, "\"Three-way calling\" "); + if (features & AG_FEATURE_EC_ANDOR_NR) + g_string_append(gstr, "\"EC and/or NR function\" "); + if (features & AG_FEATURE_VOICE_RECOGNITION) + g_string_append(gstr, "\"Voice recognition function\" "); + if (features & AG_FEATURE_INBAND_RINGTONE) + g_string_append(gstr, "\"In-band ring tone capability\" "); + if (features & AG_FEATURE_ATTACH_NUMBER_TO_VOICETAG) + g_string_append(gstr, "\"Attach a number to a voice tag\" "); + if (features & AG_FEATURE_REJECT_A_CALL) + g_string_append(gstr, "\"Ability to reject a call\" "); + if (features & AG_FEATURE_ENHANCED_CALL_STATUS) + g_string_append(gstr, "\"Enhanced call status\" "); + if (features & AG_FEATURE_ENHANCED_CALL_CONTROL) + g_string_append(gstr, "\"Enhanced call control\" "); + if (features & AG_FEATURE_EXTENDED_ERROR_RESULT_CODES) + g_string_append(gstr, "\"Extended Error Result Codes\" "); + + str = g_string_free(gstr, FALSE); + + DBG("%s", str); + + g_free(str); +} + +static void print_hf_features(uint32_t features) +{ + GString *gstr; + char *str; + + if (features == 0) { + DBG("HFP HF features: (none)"); + return; + } + + gstr = g_string_new("HFP HF features: "); + + if (features & HF_FEATURE_EC_ANDOR_NR) + g_string_append(gstr, "\"EC and/or NR function\" "); + if (features & HF_FEATURE_CALL_WAITING_AND_3WAY) + g_string_append(gstr, "\"Call waiting and 3-way calling\" "); + if (features & HF_FEATURE_CLI_PRESENTATION) + g_string_append(gstr, "\"CLI presentation capability\" "); + if (features & HF_FEATURE_VOICE_RECOGNITION) + g_string_append(gstr, "\"Voice recognition activation\" "); + if (features & HF_FEATURE_REMOTE_VOLUME_CONTROL) + g_string_append(gstr, "\"Remote volume control\" "); + if (features & HF_FEATURE_ENHANCED_CALL_STATUS) + g_string_append(gstr, "\"Enhanced call status\" "); + if (features & HF_FEATURE_ENHANCED_CALL_CONTROL) + g_string_append(gstr, "\"Enhanced call control\" "); + + str = g_string_free(gstr, FALSE); + + DBG("%s", str); + + g_free(str); +} + +static const char *state2str(headset_state_t state) +{ + switch (state) { + case HEADSET_STATE_DISCONNECTED: + return "disconnected"; + case HEADSET_STATE_CONNECTING: + return "connecting"; + case HEADSET_STATE_CONNECTED: + case HEADSET_STATE_PLAY_IN_PROGRESS: + return "connected"; + case HEADSET_STATE_PLAYING: + return "playing"; + } + + return NULL; +} + +static int headset_send_valist(struct headset *hs, char *format, va_list ap) +{ + char rsp[BUF_SIZE]; + ssize_t total_written, count; + int fd; + + count = vsnprintf(rsp, sizeof(rsp), format, ap); + + if (count < 0) + return -EINVAL; + + if (!hs->rfcomm) { + error("headset_send: the headset is not connected"); + return -EIO; + } + + total_written = 0; + fd = g_io_channel_unix_get_fd(hs->rfcomm); + + while (total_written < count) { + ssize_t written; + + written = write(fd, rsp + total_written, + count - total_written); + if (written < 0) + return -errno; + + total_written += written; + } + + return 0; +} + +static int headset_send(struct headset *hs, char *format, ...) +{ + va_list ap; + int ret; + + va_start(ap, format); + ret = headset_send_valist(hs, format, ap); + va_end(ap); + + return ret; +} + +static int supported_features(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + int err; + + if (strlen(buf) < 9) + return -EINVAL; + + slc->hf_features = strtoul(&buf[8], NULL, 10); + + print_hf_features(slc->hf_features); + + err = headset_send(hs, "\r\n+BRSF: %u\r\n", ag.features); + if (err < 0) + return err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +static char *indicator_ranges(const struct indicator *indicators) +{ + int i; + GString *gstr; + + gstr = g_string_new("\r\n+CIND: "); + + for (i = 0; indicators[i].desc != NULL; i++) { + if (i == 0) + g_string_append_printf(gstr, "(\"%s\",(%s))", + indicators[i].desc, + indicators[i].range); + else + g_string_append_printf(gstr, ",(\"%s\",(%s))", + indicators[i].desc, + indicators[i].range); + } + + g_string_append(gstr, "\r\n"); + + return g_string_free(gstr, FALSE); +} + +static char *indicator_values(const struct indicator *indicators) +{ + int i; + GString *gstr; + + gstr = g_string_new("\r\n+CIND: "); + + for (i = 0; indicators[i].desc != NULL; i++) { + if (i == 0) + g_string_append_printf(gstr, "%d", indicators[i].val); + else + g_string_append_printf(gstr, ",%d", indicators[i].val); + } + + g_string_append(gstr, "\r\n"); + + return g_string_free(gstr, FALSE); +} + +static int report_indicators(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + int err; + char *str; + + if (strlen(buf) < 8) + return -EINVAL; + + if (ag.indicators == NULL) { + error("HFP AG indicators not initialized"); + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (buf[7] == '=') + str = indicator_ranges(ag.indicators); + else + str = indicator_values(ag.indicators); + + err = headset_send(hs, str); + + g_free(str); + + if (err < 0) + return err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +static void pending_connect_complete(struct connect_cb *cb, struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (hs->pending->err < 0) + cb->cb(NULL, cb->cb_data); + else + cb->cb(dev, cb->cb_data); +} + +static void pending_connect_finalize(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + + if (p == NULL) + return; + + if (p->svclass) + bt_cancel_discovery(&dev->src, &dev->dst); + + g_slist_foreach(p->callbacks, (GFunc) pending_connect_complete, dev); + + g_slist_foreach(p->callbacks, (GFunc) g_free, NULL); + g_slist_free(p->callbacks); + + if (p->io) { + g_io_channel_shutdown(p->io, TRUE, NULL); + g_io_channel_unref(p->io); + } + + if (p->msg) + dbus_message_unref(p->msg); + + if (p->call) { + dbus_pending_call_cancel(p->call); + dbus_pending_call_unref(p->call); + } + + g_free(p); + + hs->pending = NULL; +} + +static void pending_connect_init(struct headset *hs, headset_state_t target_state) +{ + if (hs->pending) { + if (hs->pending->target_state < target_state) + hs->pending->target_state = target_state; + return; + } + + hs->pending = g_new0(struct pending_connect, 1); + hs->pending->target_state = target_state; +} + +static unsigned int connect_cb_new(struct headset *hs, + headset_state_t target_state, + headset_stream_cb_t func, + void *user_data) +{ + struct connect_cb *cb; + unsigned int free_cb_id = 1; + + pending_connect_init(hs, target_state); + + if (!func) + return 0; + + cb = g_new(struct connect_cb, 1); + + cb->cb = func; + cb->cb_data = user_data; + cb->id = free_cb_id++; + + hs->pending->callbacks = g_slist_append(hs->pending->callbacks, + cb); + + return cb->id; +} + +static void send_foreach_headset(GSList *devices, + int (*cmp) (struct headset *hs), + char *format, ...) +{ + GSList *l; + va_list ap; + + for (l = devices; l != NULL; l = l->next) { + struct audio_device *device = l->data; + struct headset *hs = device->headset; + int ret; + + assert(hs != NULL); + + if (cmp && cmp(hs) != 0) + continue; + + va_start(ap, format); + ret = headset_send_valist(hs, format, ap); + if (ret < 0) + error("Failed to send to headset: %s (%d)", + strerror(-ret), -ret); + va_end(ap); + } +} + +static int cli_cmp(struct headset *hs) +{ + struct headset_slc *slc = hs->slc; + + if (!hs->hfp_active) + return -1; + + if (slc->cli_active) + return 0; + else + return -1; +} + +static gboolean ring_timer_cb(gpointer data) +{ + send_foreach_headset(active_devices, NULL, "\r\nRING\r\n"); + + if (ag.number) + send_foreach_headset(active_devices, cli_cmp, + "\r\n+CLIP: \"%s\",%d\r\n", + ag.number, ag.number_type); + + return TRUE; +} + +static void sco_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + int sk; + struct audio_device *dev = user_data; + struct headset *hs = dev->headset; + struct headset_slc *slc = hs->slc; + struct pending_connect *p = hs->pending; + + if (err) { + error("%s", err->message); + + if (p != NULL) { + p->err = -errno; + if (p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + pending_connect_finalize(dev); + } + + if (hs->rfcomm) + headset_set_state(dev, HEADSET_STATE_CONNECTED); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + + return; + } + + DBG("SCO socket opened for headset %s", dev->path); + + sk = g_io_channel_unix_get_fd(chan); + + DBG("SCO fd=%d", sk); + + if (p) { + p->io = NULL; + if (p->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(dev->conn, reply); + } + + pending_connect_finalize(dev); + } + + fcntl(sk, F_SETFL, 0); + + headset_set_state(dev, HEADSET_STATE_PLAYING); + + if (slc->pending_ring) { + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, + ring_timer_cb, + NULL); + slc->pending_ring = FALSE; + } +} + +static int sco_connect(struct audio_device *dev, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id) +{ + struct headset *hs = dev->headset; + GError *err = NULL; + GIOChannel *io; + + if (hs->state != HEADSET_STATE_CONNECTED) + return -EINVAL; + + io = bt_io_connect(BT_IO_SCO, sco_connect_cb, dev, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return -EIO; + } + + hs->sco = io; + + headset_set_state(dev, HEADSET_STATE_PLAY_IN_PROGRESS); + + pending_connect_init(hs, HEADSET_STATE_PLAYING); + + if (cb) { + unsigned int id = connect_cb_new(hs, HEADSET_STATE_PLAYING, + cb, user_data); + if (cb_id) + *cb_id = id; + } + + return 0; +} + +static int hfp_cmp(struct headset *hs) +{ + if (hs->hfp_active) + return 0; + else + return -1; +} + +static void hfp_slc_complete(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + + DBG("HFP Service Level Connection established"); + + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + if (p == NULL) + return; + + if (p->target_state == HEADSET_STATE_CONNECTED) { + if (p->msg) { + DBusMessage *reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(dev->conn, reply); + } + pending_connect_finalize(dev); + return; + } + + p->err = sco_connect(dev, NULL, NULL, NULL); + if (p->err < 0) { + if (p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + pending_connect_finalize(dev); + } +} + +static int telephony_generic_rsp(struct audio_device *device, cme_error_t err) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err != CME_ERROR_NONE) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_event_reporting_rsp(void *telephony_device, cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + int ret; + + if (err != CME_ERROR_NONE) + return telephony_generic_rsp(telephony_device, err); + + ret = headset_send(hs, "\r\nOK\r\n"); + if (ret < 0) + return ret; + + if (hs->state != HEADSET_STATE_CONNECTING) + return 0; + + if (slc->hf_features & HF_FEATURE_CALL_WAITING_AND_3WAY && + ag.features & AG_FEATURE_THREE_WAY_CALLING) + return 0; + + hfp_slc_complete(device); + + return 0; +} + +static int event_reporting(struct audio_device *dev, const char *buf) +{ + char **tokens; /* <mode>, <keyp>, <disp>, <ind>, <bfr> */ + + if (strlen(buf) < 13) + return -EINVAL; + + tokens = g_strsplit(&buf[8], ",", 5); + if (g_strv_length(tokens) < 4) { + g_strfreev(tokens); + return -EINVAL; + } + + ag.er_mode = atoi(tokens[0]); + ag.er_ind = atoi(tokens[3]); + + g_strfreev(tokens); + tokens = NULL; + + DBG("Event reporting (CMER): mode=%d, ind=%d", + ag.er_mode, ag.er_ind); + + switch (ag.er_ind) { + case 0: + case 1: + telephony_event_reporting_req(dev, ag.er_ind); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int call_hold(struct audio_device *dev, const char *buf) +{ + struct headset *hs = dev->headset; + int err; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] != '?') { + telephony_call_hold_req(dev, &buf[8]); + return 0; + } + + err = headset_send(hs, "\r\n+CHLD: (%s)\r\n", ag.chld); + if (err < 0) + return err; + + err = headset_send(hs, "\r\nOK\r\n"); + if (err < 0) + return err; + + if (hs->state != HEADSET_STATE_CONNECTING) + return 0; + + hfp_slc_complete(dev); + + return 0; +} + +int telephony_key_press_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int key_press(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 9) + return -EINVAL; + + g_dbus_emit_signal(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, "AnswerRequested", + DBUS_TYPE_INVALID); + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + telephony_key_press_req(device, &buf[8]); + + return 0; +} + +int telephony_answer_call_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int answer_call(struct audio_device *device, const char *buf) +{ + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + if (ag.number) { + g_free(ag.number); + ag.number = NULL; + } + + telephony_answer_call_req(device); + + return 0; +} + +int telephony_terminate_call_rsp(void *telephony_device, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + + if (err != CME_ERROR_NONE) + return telephony_generic_rsp(telephony_device, err); + + g_dbus_emit_signal(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, "CallTerminated", + DBUS_TYPE_INVALID); + + return headset_send(hs, "\r\nOK\r\n"); +} + +static int terminate_call(struct audio_device *device, const char *buf) +{ + if (ag.number) { + g_free(ag.number); + ag.number = NULL; + } + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + telephony_terminate_call_req(device); + + return 0; +} + +static int cli_notification(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + slc->cli_active = buf[8] == '1' ? TRUE : FALSE; + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_response_and_hold_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int response_and_hold(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + + if (strlen(buf) < 8) + return -EINVAL; + + if (ag.rh == BTRH_NOT_SUPPORTED) + return telephony_generic_rsp(device, CME_ERROR_NOT_SUPPORTED); + + if (buf[7] == '=') { + telephony_response_and_hold_req(device, atoi(&buf[8]) < 0); + return 0; + } + + if (ag.rh >= 0) + headset_send(hs, "\r\n+BTRH: %d\r\n", ag.rh); + + return headset_send(hs, "\r\nOK\r\n", ag.rh); +} + +int telephony_last_dialed_number_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int last_dialed_number(struct audio_device *device, const char *buf) +{ + telephony_last_dialed_number_req(device); + + return 0; +} + +int telephony_dial_number_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int dial_number(struct audio_device *device, const char *buf) +{ + char number[BUF_SIZE]; + size_t buf_len; + + buf_len = strlen(buf); + + if (buf[buf_len - 1] != ';') { + DBG("Rejecting non-voice call dial request"); + return -EINVAL; + } + + memset(number, 0, sizeof(number)); + strncpy(number, &buf[3], buf_len - 4); + + telephony_dial_number_req(device, number); + + return 0; +} + +static int headset_set_gain(struct audio_device *device, uint16_t gain, char type) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + const char *name, *property; + + if (gain > 15) { + error("Invalid gain value: %u", gain); + return -EINVAL; + } + + switch (type) { + case HEADSET_GAIN_SPEAKER: + if (slc->sp_gain == gain) { + DBG("Ignoring no-change in speaker gain"); + return -EALREADY; + } + name = "SpeakerGainChanged"; + property = "SpeakerGain"; + slc->sp_gain = gain; + break; + case HEADSET_GAIN_MICROPHONE: + if (slc->mic_gain == gain) { + DBG("Ignoring no-change in microphone gain"); + return -EALREADY; + } + name = "MicrophoneGainChanged"; + property = "MicrophoneGain"; + slc->mic_gain = gain; + break; + default: + error("Unknown gain setting"); + return -EINVAL; + } + + g_dbus_emit_signal(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, name, + DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID); + + emit_property_changed(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, property, + DBUS_TYPE_UINT16, &gain); + + return 0; +} + +static int signal_gain_setting(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + dbus_uint16_t gain; + int err; + + if (strlen(buf) < 8) { + error("Too short string for Gain setting"); + return -EINVAL; + } + + gain = (dbus_uint16_t) strtol(&buf[7], NULL, 10); + + err = headset_set_gain(device, gain, buf[5]); + if (err < 0 && err != -EALREADY) + return err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_transmit_dtmf_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int dtmf_tone(struct audio_device *device, const char *buf) +{ + char tone; + + if (strlen(buf) < 8) { + error("Too short string for DTMF tone"); + return -EINVAL; + } + + tone = buf[7]; + if (tone >= '#' && tone <= 'D') + telephony_transmit_dtmf_req(device, tone); + else + return -EINVAL; + + return 0; +} + +int telephony_subscriber_number_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int subscriber_number(struct audio_device *device, const char *buf) +{ + telephony_subscriber_number_req(device); + + return 0; +} + +int telephony_list_current_calls_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int list_current_calls(struct audio_device *device, const char *buf) +{ + telephony_list_current_calls_req(device); + + return 0; +} + +static int extended_errors(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '1') { + slc->cme_enabled = TRUE; + DBG("CME errors enabled for headset %p", hs); + } else { + slc->cme_enabled = FALSE; + DBG("CME errors disabled for headset %p", hs); + } + + return headset_send(hs, "\r\nOK\r\n"); +} + +static int call_waiting_notify(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '1') { + slc->cwa_enabled = TRUE; + DBG("Call waiting notification enabled for headset %p", hs); + } else { + slc->cwa_enabled = FALSE; + DBG("Call waiting notification disabled for headset %p", hs); + } + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_operator_selection_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_call_hold_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_nr_and_ec_rsp(void *telephony_device, cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err == CME_ERROR_NONE) { + GSList *l; + + for (l = hs->nrec_cbs; l; l = l->next) { + struct headset_nrec_callback *nrec_cb = l->data; + + nrec_cb->cb(device, slc->nrec_req, nrec_cb->user_data); + } + + slc->nrec = hs->slc->nrec_req; + } + + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_voice_dial_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_operator_selection_ind(int mode, const char *oper) +{ + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+COPS: %d,0,\"%s\"\r\n", + mode, oper); + return 0; +} + +static int operator_selection(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + + if (strlen(buf) < 8) + return -EINVAL; + + switch (buf[7]) { + case '?': + telephony_operator_selection_req(device); + break; + case '=': + return headset_send(hs, "\r\nOK\r\n"); + default: + return -EINVAL; + } + + return 0; +} + +static int nr_and_ec(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '0') + slc->nrec_req = FALSE; + else + slc->nrec_req = TRUE; + + telephony_nr_and_ec_req(device, slc->nrec_req); + + return 0; +} + +static int voice_dial(struct audio_device *device, const char *buf) +{ + gboolean enable; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '0') + enable = FALSE; + else + enable = TRUE; + + telephony_voice_dial_req(device, enable); + + return 0; +} + +static struct event event_callbacks[] = { + { "ATA", answer_call }, + { "ATD", dial_number }, + { "AT+VG", signal_gain_setting }, + { "AT+BRSF", supported_features }, + { "AT+CIND", report_indicators }, + { "AT+CMER", event_reporting }, + { "AT+CHLD", call_hold }, + { "AT+CHUP", terminate_call }, + { "AT+CKPD", key_press }, + { "AT+CLIP", cli_notification }, + { "AT+BTRH", response_and_hold }, + { "AT+BLDN", last_dialed_number }, + { "AT+VTS", dtmf_tone }, + { "AT+CNUM", subscriber_number }, + { "AT+CLCC", list_current_calls }, + { "AT+CMEE", extended_errors }, + { "AT+CCWA", call_waiting_notify }, + { "AT+COPS", operator_selection }, + { "AT+NREC", nr_and_ec }, + { "AT+BVRA", voice_dial }, + { 0 } +}; + +static int handle_event(struct audio_device *device, const char *buf) +{ + struct event *ev; + + DBG("Received %s", buf); + + for (ev = event_callbacks; ev->cmd; ev++) { + if (!strncmp(buf, ev->cmd, strlen(ev->cmd))) + return ev->callback(device, buf); + } + + return -EINVAL; +} + +static void close_sco(struct audio_device *device) +{ + struct headset *hs = device->headset; + + if (hs->sco) { + int sock = g_io_channel_unix_get_fd(hs->sco); + shutdown(sock, SHUT_RDWR); + g_io_channel_shutdown(hs->sco, TRUE, NULL); + g_io_channel_unref(hs->sco); + hs->sco = NULL; + } + + if (hs->sco_id) { + g_source_remove(hs->sco_id); + hs->sco_id = 0; + } +} + +static gboolean rfcomm_io_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *device) +{ + struct headset *hs; + struct headset_slc *slc; + unsigned char buf[BUF_SIZE]; + ssize_t bytes_read; + size_t free_space; + int fd; + + if (cond & G_IO_NVAL) + return FALSE; + + hs = device->headset; + slc = hs->slc; + + if (cond & (G_IO_ERR | G_IO_HUP)) { + DBG("ERR or HUP on RFCOMM socket"); + goto failed; + } + + fd = g_io_channel_unix_get_fd(chan); + + bytes_read = read(fd, buf, sizeof(buf) - 1); + if (bytes_read < 0) + return TRUE; + + free_space = sizeof(slc->buf) - slc->data_start - + slc->data_length - 1; + + if (free_space < (size_t) bytes_read) { + /* Very likely that the HS is sending us garbage so + * just ignore the data and disconnect */ + error("Too much data to fit incomming buffer"); + goto failed; + } + + memcpy(&slc->buf[slc->data_start], buf, bytes_read); + slc->data_length += bytes_read; + + /* Make sure the data is null terminated so we can use string + * functions */ + slc->buf[slc->data_start + slc->data_length] = '\0'; + + while (slc->data_length > 0) { + char *cr; + int err; + off_t cmd_len; + + cr = strchr(&slc->buf[slc->data_start], '\r'); + if (!cr) + break; + + cmd_len = 1 + (off_t) cr - (off_t) &slc->buf[slc->data_start]; + *cr = '\0'; + + if (cmd_len > 1) + err = handle_event(device, &slc->buf[slc->data_start]); + else + /* Silently skip empty commands */ + err = 0; + + if (err == -EINVAL) { + error("Badly formated or unrecognized command: %s", + &slc->buf[slc->data_start]); + err = headset_send(hs, "\r\nERROR\r\n"); + } else if (err < 0) + error("Error handling command %s: %s (%d)", + &slc->buf[slc->data_start], + strerror(-err), -err); + + slc->data_start += cmd_len; + slc->data_length -= cmd_len; + + if (!slc->data_length) + slc->data_start = 0; + } + + return TRUE; + +failed: + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + + return FALSE; +} + +static gboolean sco_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *device) +{ + if (cond & G_IO_NVAL) + return FALSE; + + error("Audio connection got disconnected"); + + pending_connect_finalize(device); + headset_set_state(device, HEADSET_STATE_CONNECTED); + + return FALSE; +} + +void headset_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + struct audio_device *dev = user_data; + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + char hs_address[18]; + + if (err) { + error("%s", err->message); + goto failed; + } + + /* For HFP telephony isn't ready just disconnect */ + if (hs->hfp_active && !ag.telephony_ready) { + error("Unable to accept HFP connection since the telephony " + "subsystem isn't initialized"); + goto failed; + } + + hs->rfcomm = hs->tmp_rfcomm; + hs->tmp_rfcomm = NULL; + + ba2str(&dev->dst, hs_address); + + if (p) + p->io = NULL; + else + hs->auto_dc = FALSE; + + g_io_add_watch(chan, G_IO_IN | G_IO_ERR | G_IO_HUP| G_IO_NVAL, + (GIOFunc) rfcomm_io_cb, dev); + + DBG("%s: Connected to %s", dev->path, hs_address); + + hs->slc = g_new0(struct headset_slc, 1); + hs->slc->sp_gain = 15; + hs->slc->mic_gain = 15; + hs->slc->nrec = TRUE; + + /* In HFP mode wait for Service Level Connection */ + if (hs->hfp_active) + return; + + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + if (p && p->target_state == HEADSET_STATE_PLAYING) { + p->err = sco_connect(dev, NULL, NULL, NULL); + if (p->err < 0) + goto failed; + return; + } + + if (p && p->msg) { + DBusMessage *reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(dev->conn, reply); + } + + pending_connect_finalize(dev); + + return; + +failed: + if (p && p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + pending_connect_finalize(dev); + if (hs->rfcomm) + headset_set_state(dev, HEADSET_STATE_CONNECTED); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +static int headset_set_channel(struct headset *headset, + const sdp_record_t *record, uint16_t svc) +{ + int ch; + sdp_list_t *protos; + + if (sdp_get_access_protos(record, &protos) < 0) { + error("Unable to get access protos from headset record"); + return -1; + } + + ch = sdp_get_proto_port(protos, RFCOMM_UUID); + + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + + if (ch <= 0) { + error("Unable to get RFCOMM channel from Headset record"); + return -1; + } + + headset->rfcomm_ch = ch; + + if (svc == HANDSFREE_SVCLASS_ID) { + headset->hfp_handle = record->handle; + DBG("Discovered Handsfree service on channel %d", ch); + } else { + headset->hsp_handle = record->handle; + DBG("Discovered Headset service on channel %d", ch); + } + + return 0; +} + +static void get_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct audio_device *dev = user_data; + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + sdp_record_t *record = NULL; + sdp_list_t *r; + uuid_t uuid; + + assert(hs->pending != NULL); + + if (err < 0) { + error("Unable to get service record: %s (%d)", + strerror(-err), -err); + p->err = -err; + if (p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + goto failed; + } + + if (!recs || !recs->data) { + error("No records found"); + goto failed_not_supported; + } + + sdp_uuid16_create(&uuid, p->svclass); + + for (r = recs; r != NULL; r = r->next) { + sdp_list_t *classes; + uuid_t class; + + record = r->data; + + if (sdp_get_service_classes(record, &classes) < 0) { + error("Unable to get service classes from record"); + continue; + } + + memcpy(&class, classes->data, sizeof(uuid)); + + sdp_list_free(classes, free); + + + if (sdp_uuid_cmp(&class, &uuid) == 0) + break; + } + + if (r == NULL) { + error("No record found with UUID 0x%04x", p->svclass); + goto failed_not_supported; + } + + if (headset_set_channel(hs, record, p->svclass) < 0) { + error("Unable to extract RFCOMM channel from service record"); + goto failed_not_supported; + } + + /* Set svclass to 0 so we can easily check that SDP is no-longer + * going on (to know if bt_cancel_discovery needs to be called) */ + p->svclass = 0; + + err = rfcomm_connect(dev, NULL, NULL, NULL); + if (err < 0) { + error("Unable to connect: %s (%d)", strerror(-err), -err); + p->err = -err; + error_connect_failed(dev->conn, p->msg, p->err); + goto failed; + } + + return; + +failed_not_supported: + if (p->svclass == HANDSFREE_SVCLASS_ID && + get_records(dev, NULL, NULL, NULL) == 0) + return; + if (p->msg) { + DBusMessage *reply = btd_error_not_supported(p->msg); + g_dbus_send_message(dev->conn, reply); + } +failed: + p->svclass = 0; + pending_connect_finalize(dev); + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +static int get_records(struct audio_device *device, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id) +{ + struct headset *hs = device->headset; + uint16_t svclass; + uuid_t uuid; + int err; + + if (hs->pending && hs->pending->svclass == HANDSFREE_SVCLASS_ID) + svclass = HEADSET_SVCLASS_ID; + else + svclass = hs->search_hfp ? HANDSFREE_SVCLASS_ID : + HEADSET_SVCLASS_ID; + + sdp_uuid16_create(&uuid, svclass); + + err = bt_search_service(&device->src, &device->dst, &uuid, + get_record_cb, device, NULL); + if (err < 0) + return err; + + if (hs->pending) { + hs->pending->svclass = svclass; + return 0; + } + + headset_set_state(device, HEADSET_STATE_CONNECTING); + + pending_connect_init(hs, HEADSET_STATE_CONNECTED); + + hs->pending->svclass = svclass; + + if (cb) { + unsigned int id; + id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, + cb, user_data); + if (cb_id) + *cb_id = id; + } + + return 0; +} + +static int rfcomm_connect(struct audio_device *dev, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id) +{ + struct headset *hs = dev->headset; + char address[18]; + GError *err = NULL; + + if (!manager_allow_headset_connection(dev)) + return -ECONNREFUSED; + + if (hs->rfcomm_ch < 0) + return get_records(dev, cb, user_data, cb_id); + + ba2str(&dev->dst, address); + + DBG("%s: Connecting to %s channel %d", dev->path, address, + hs->rfcomm_ch); + + hs->tmp_rfcomm = bt_io_connect(BT_IO_RFCOMM, headset_connect_cb, dev, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_CHANNEL, hs->rfcomm_ch, + BT_IO_OPT_INVALID); + + hs->rfcomm_ch = -1; + + if (!hs->tmp_rfcomm) { + error("%s", err->message); + g_error_free(err); + return -EIO; + } + + hs->hfp_active = hs->hfp_handle != 0 ? TRUE : FALSE; + + headset_set_state(dev, HEADSET_STATE_CONNECTING); + + pending_connect_init(hs, HEADSET_STATE_CONNECTED); + + if (cb) { + unsigned int id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, + cb, user_data); + if (cb_id) + *cb_id = id; + } + + return 0; +} + +static DBusMessage *hs_stop(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply = NULL; + + if (hs->state < HEADSET_STATE_PLAY_IN_PROGRESS) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + headset_set_state(device, HEADSET_STATE_CONNECTED); + + return reply; +} + +static DBusMessage *hs_is_playing(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply; + dbus_bool_t playing; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + playing = (hs->state == HEADSET_STATE_PLAYING); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &playing, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_disconnect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + char hs_address[18]; + + if (hs->state == HEADSET_STATE_DISCONNECTED) + return btd_error_not_connected(msg); + + headset_shutdown(device); + ba2str(&device->dst, hs_address); + info("Disconnected from %s, %s", hs_address, device->path); + + return dbus_message_new_method_return(msg); + +} + +static DBusMessage *hs_is_connected(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + dbus_bool_t connected; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + connected = (device->headset->state >= HEADSET_STATE_CONNECTED); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_connect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + int err; + + if (hs->state == HEADSET_STATE_CONNECTING) + return btd_error_in_progress(msg); + else if (hs->state > HEADSET_STATE_CONNECTING) + return btd_error_already_connected(msg); + + if (hs->hfp_handle && !ag.telephony_ready) + return btd_error_not_ready(msg); + + device->auto_connect = FALSE; + + err = rfcomm_connect(device, NULL, NULL, NULL); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + hs->auto_dc = FALSE; + + hs->pending->msg = dbus_message_ref(msg); + + return NULL; +} + +static DBusMessage *hs_ring(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply = NULL; + int err; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (ag.ring_timer) { + DBG("IndicateCall received when already indicating"); + goto done; + } + + err = headset_send(hs, "\r\nRING\r\n"); + if (err < 0) { + dbus_message_unref(reply); + return btd_error_failed(msg, strerror(-err)); + } + + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, ring_timer_cb, + NULL); + +done: + return reply; +} + +static DBusMessage *hs_cancel_call(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply = NULL; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } else + DBG("Got CancelCall method call but no call is active"); + + return reply; +} + +static DBusMessage *hs_play(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + int err; + + if (sco_hci) { + error("Refusing Headset.Play() because SCO HCI routing " + "is enabled"); + return btd_error_not_available(msg); + } + + switch (hs->state) { + case HEADSET_STATE_DISCONNECTED: + case HEADSET_STATE_CONNECTING: + return btd_error_not_connected(msg); + case HEADSET_STATE_PLAY_IN_PROGRESS: + if (hs->pending && hs->pending->msg == NULL) { + hs->pending->msg = dbus_message_ref(msg); + return NULL; + } + return btd_error_busy(msg); + case HEADSET_STATE_PLAYING: + return btd_error_already_connected(msg); + case HEADSET_STATE_CONNECTED: + default: + break; + } + + err = sco_connect(device, NULL, NULL, NULL); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + hs->pending->msg = dbus_message_ref(msg); + + return NULL; +} + +static DBusMessage *hs_get_speaker_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + DBusMessage *reply; + dbus_uint16_t gain; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_available(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + gain = (dbus_uint16_t) slc->sp_gain; + + dbus_message_append_args(reply, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_get_mic_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + DBusMessage *reply; + dbus_uint16_t gain; + + if (hs->state < HEADSET_STATE_CONNECTED || slc == NULL) + return btd_error_not_available(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + gain = (dbus_uint16_t) slc->mic_gain; + + dbus_message_append_args(reply, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_set_gain(DBusConnection *conn, + DBusMessage *msg, + void *data, uint16_t gain, + char type) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply; + int err; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + err = headset_set_gain(device, gain, type); + if (err < 0) + return btd_error_invalid_args(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (hs->state == HEADSET_STATE_PLAYING) { + err = headset_send(hs, "\r\n+VG%c=%u\r\n", type, gain); + if (err < 0) { + dbus_message_unref(reply); + return btd_error_failed(msg, strerror(-err)); + } + } + + return reply; +} + +static DBusMessage *hs_set_speaker_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + uint16_t gain; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID)) + return NULL; + + return hs_set_gain(conn, msg, data, gain, HEADSET_GAIN_SPEAKER); +} + +static DBusMessage *hs_set_mic_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + uint16_t gain; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID)) + return NULL; + + return hs_set_gain(conn, msg, data, gain, HEADSET_GAIN_MICROPHONE); +} + +static DBusMessage *hs_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + gboolean value; + const char *state; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + + /* Playing */ + value = (device->headset->state == HEADSET_STATE_PLAYING); + dict_append_entry(&dict, "Playing", DBUS_TYPE_BOOLEAN, &value); + + /* State */ + state = state2str(device->headset->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + /* Connected */ + value = (device->headset->state >= HEADSET_STATE_CONNECTED); + dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); + + if (!value) + goto done; + + /* SpeakerGain */ + dict_append_entry(&dict, "SpeakerGain", + DBUS_TYPE_UINT16, + &device->headset->slc->sp_gain); + + /* MicrophoneGain */ + dict_append_entry(&dict, "MicrophoneGain", + DBUS_TYPE_UINT16, + &device->headset->slc->mic_gain); + +done: + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static DBusMessage *hs_set_property(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *property; + DBusMessageIter iter; + DBusMessageIter sub; + uint16_t gain; + + if (!dbus_message_iter_init(msg, &iter)) + return btd_error_invalid_args(msg); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&iter, &property); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return btd_error_invalid_args(msg); + dbus_message_iter_recurse(&iter, &sub); + + if (g_str_equal("SpeakerGain", property)) { + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_UINT16) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &gain); + return hs_set_gain(conn, msg, data, gain, + HEADSET_GAIN_SPEAKER); + } else if (g_str_equal("MicrophoneGain", property)) { + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_UINT16) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &gain); + return hs_set_gain(conn, msg, data, gain, + HEADSET_GAIN_MICROPHONE); + } + + return btd_error_invalid_args(msg); +} +static GDBusMethodTable headset_methods[] = { + { "Connect", "", "", hs_connect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "Disconnect", "", "", hs_disconnect }, + { "IsConnected", "", "b", hs_is_connected }, + { "IndicateCall", "", "", hs_ring }, + { "CancelCall", "", "", hs_cancel_call }, + { "Play", "", "", hs_play, + G_DBUS_METHOD_FLAG_ASYNC }, + { "Stop", "", "", hs_stop }, + { "IsPlaying", "", "b", hs_is_playing, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "GetSpeakerGain", "", "q", hs_get_speaker_gain, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "GetMicrophoneGain", "", "q", hs_get_mic_gain, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "SetSpeakerGain", "q", "", hs_set_speaker_gain, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "SetMicrophoneGain", "q", "", hs_set_mic_gain, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "GetProperties", "", "a{sv}",hs_get_properties }, + { "SetProperty", "sv", "", hs_set_property }, + { NULL, NULL, NULL, NULL } +}; + +static GDBusSignalTable headset_signals[] = { + { "Connected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "Disconnected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "AnswerRequested", "" }, + { "Stopped", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "Playing", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "SpeakerGainChanged", "q", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "MicrophoneGainChanged", "q", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "CallTerminated", "" }, + { "PropertyChanged", "sv" }, + { NULL, NULL } +}; + +void headset_update(struct audio_device *dev, uint16_t svc, + const char *uuidstr) +{ + struct headset *headset = dev->headset; + const sdp_record_t *record; + + record = btd_device_get_record(dev->btd_dev, uuidstr); + if (!record) + return; + + switch (svc) { + case HANDSFREE_SVCLASS_ID: + if (headset->hfp_handle && + (headset->hfp_handle != record->handle)) { + error("More than one HFP record found on device"); + return; + } + + headset->hfp_handle = record->handle; + break; + + case HEADSET_SVCLASS_ID: + if (headset->hsp_handle && + (headset->hsp_handle != record->handle)) { + error("More than one HSP record found on device"); + return; + } + + headset->hsp_handle = record->handle; + + /* Ignore this record if we already have access to HFP */ + if (headset->hfp_handle) + return; + + break; + + default: + DBG("Invalid record passed to headset_update"); + return; + } +} + +static int headset_close_rfcomm(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + GIOChannel *rfcomm = hs->tmp_rfcomm ? hs->tmp_rfcomm : hs->rfcomm; + + if (rfcomm) { + g_io_channel_shutdown(rfcomm, TRUE, NULL); + g_io_channel_unref(rfcomm); + hs->tmp_rfcomm = NULL; + hs->rfcomm = NULL; + } + + g_free(hs->slc); + hs->slc = NULL; + + return 0; +} + +static void headset_free(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + close_sco(dev); + + headset_close_rfcomm(dev); + + g_slist_foreach(hs->nrec_cbs, (GFunc) g_free, NULL); + g_slist_free(hs->nrec_cbs); + + g_free(hs); + dev->headset = NULL; +} + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + struct headset *hs = dev->headset; + + if (hs->state > HEADSET_STATE_DISCONNECTED) { + DBG("Headset unregistered while device was connected!"); + headset_shutdown(dev); + } + + DBG("Unregistered interface %s on path %s", + AUDIO_HEADSET_INTERFACE, dev->path); + + headset_free(dev); +} + +void headset_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE); +} + +struct headset *headset_init(struct audio_device *dev, uint16_t svc, + const char *uuidstr) +{ + struct headset *hs; + const sdp_record_t *record; + + hs = g_new0(struct headset, 1); + hs->rfcomm_ch = -1; + hs->search_hfp = server_is_enabled(&dev->src, HANDSFREE_SVCLASS_ID); + + record = btd_device_get_record(dev->btd_dev, uuidstr); + if (!record) + goto register_iface; + + switch (svc) { + case HANDSFREE_SVCLASS_ID: + hs->hfp_handle = record->handle; + break; + + case HEADSET_SVCLASS_ID: + hs->hsp_handle = record->handle; + break; + + default: + DBG("Invalid record passed to headset_init"); + g_free(hs); + return NULL; + } + +register_iface: + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + headset_methods, headset_signals, NULL, + dev, path_unregister)) { + g_free(hs); + return NULL; + } + + DBG("Registered interface %s on path %s", + AUDIO_HEADSET_INTERFACE, dev->path); + + return hs; +} + +uint32_t headset_config_init(GKeyFile *config) +{ + GError *err = NULL; + char *str; + + /* Use the default values if there is no config file */ + if (config == NULL) + return ag.features; + + str = g_key_file_get_string(config, "General", "SCORouting", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + if (strcmp(str, "PCM") == 0) + sco_hci = FALSE; + else if (strcmp(str, "HCI") == 0) + sco_hci = TRUE; + else + error("Invalid Headset Routing value: %s", str); + g_free(str); + } + + /* Init fast connectable option */ + str = g_key_file_get_string(config, "Headset", "FastConnectable", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + fast_connectable = strcmp(str, "true") == 0; + if (fast_connectable) + manager_set_fast_connectable(FALSE); + g_free(str); + } + + return ag.features; +} + +static gboolean hs_dc_timeout(struct audio_device *dev) +{ + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + return FALSE; +} + +gboolean headset_cancel_stream(struct audio_device *dev, unsigned int id) +{ + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + GSList *l; + struct connect_cb *cb = NULL; + + if (!p) + return FALSE; + + for (l = p->callbacks; l != NULL; l = l->next) { + struct connect_cb *tmp = l->data; + + if (tmp->id == id) { + cb = tmp; + break; + } + } + + if (!cb) + return FALSE; + + p->callbacks = g_slist_remove(p->callbacks, cb); + g_free(cb); + + if (p->callbacks || p->msg) + return TRUE; + + if (hs->auto_dc) { + if (hs->rfcomm) + hs->dc_timer = g_timeout_add_seconds(DC_TIMEOUT, + (GSourceFunc) hs_dc_timeout, + dev); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + } + + return TRUE; +} + +static gboolean dummy_connect_complete(struct audio_device *dev) +{ + pending_connect_finalize(dev); + return FALSE; +} + +unsigned int headset_request_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data) +{ + struct headset *hs = dev->headset; + unsigned int id; + + if (hs->state == HEADSET_STATE_PLAYING) { + id = connect_cb_new(hs, HEADSET_STATE_PLAYING, cb, user_data); + g_idle_add((GSourceFunc) dummy_connect_complete, dev); + return id; + } + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + if (hs->state == HEADSET_STATE_CONNECTING || + hs->state == HEADSET_STATE_PLAY_IN_PROGRESS) + return connect_cb_new(hs, HEADSET_STATE_PLAYING, cb, user_data); + + if (hs->rfcomm == NULL) { + if (rfcomm_connect(dev, cb, user_data, &id) < 0) + return 0; + hs->auto_dc = TRUE; + } else if (sco_connect(dev, cb, user_data, &id) < 0) + return 0; + + hs->pending->target_state = HEADSET_STATE_PLAYING; + + return id; +} + +unsigned int headset_config_stream(struct audio_device *dev, + gboolean auto_dc, + headset_stream_cb_t cb, + void *user_data) +{ + struct headset *hs = dev->headset; + unsigned int id = 0; + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + if (hs->state == HEADSET_STATE_CONNECTING) + return connect_cb_new(hs, HEADSET_STATE_CONNECTED, cb, + user_data); + + if (hs->rfcomm) + goto done; + + if (rfcomm_connect(dev, cb, user_data, &id) < 0) + return 0; + + hs->auto_dc = auto_dc; + hs->pending->target_state = HEADSET_STATE_CONNECTED; + + return id; + +done: + id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, cb, user_data); + g_idle_add((GSourceFunc) dummy_connect_complete, dev); + return id; +} + +unsigned int headset_suspend_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data) +{ + struct headset *hs = dev->headset; + unsigned int id; + int sock; + + if (hs->state == HEADSET_STATE_DISCONNECTED || + hs->state == HEADSET_STATE_CONNECTING) + return 0; + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + sock = g_io_channel_unix_get_fd(hs->sco); + + /* shutdown but leave the socket open and wait for hup */ + shutdown(sock, SHUT_RDWR); + + id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, cb, user_data); + + return id; +} + +gboolean get_hfp_active(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->hfp_active; +} + +void set_hfp_active(struct audio_device *dev, gboolean active) +{ + struct headset *hs = dev->headset; + + hs->hfp_active = active; +} + +GIOChannel *headset_get_rfcomm(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->tmp_rfcomm; +} + +int headset_connect_rfcomm(struct audio_device *dev, GIOChannel *io) +{ + struct headset *hs = dev->headset; + + if (hs->tmp_rfcomm) + return -EALREADY; + + hs->tmp_rfcomm = g_io_channel_ref(io); + + return 0; +} + +int headset_connect_sco(struct audio_device *dev, GIOChannel *io) +{ + struct headset *hs = dev->headset; + struct headset_slc *slc = hs->slc; + + if (hs->sco) + return -EISCONN; + + hs->sco = g_io_channel_ref(io); + + if (slc->pending_ring) { + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, + ring_timer_cb, + NULL); + slc->pending_ring = FALSE; + } + + return 0; +} + +void headset_set_state(struct audio_device *dev, headset_state_t state) +{ + struct headset *hs = dev->headset; + struct headset_slc *slc = hs->slc; + gboolean value; + const char *state_str; + headset_state_t old_state = hs->state; + GSList *l; + + if (old_state == state) + return; + + state_str = state2str(state); + + switch (state) { + case HEADSET_STATE_DISCONNECTED: + value = FALSE; + close_sco(dev); + headset_close_rfcomm(dev); + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Disconnected", + DBUS_TYPE_INVALID); + if (hs->state > HEADSET_STATE_CONNECTING) { + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + telephony_device_disconnected(dev); + } + active_devices = g_slist_remove(active_devices, dev); + break; + case HEADSET_STATE_CONNECTING: + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + break; + case HEADSET_STATE_CONNECTED: + close_sco(dev); + if (hs->state != HEADSET_STATE_PLAY_IN_PROGRESS) + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + if (hs->state < state) { + if (ag.features & AG_FEATURE_INBAND_RINGTONE) + slc->inband_ring = TRUE; + else + slc->inband_ring = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Connected", + DBUS_TYPE_INVALID); + value = TRUE; + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Connected", + DBUS_TYPE_BOOLEAN, &value); + active_devices = g_slist_append(active_devices, dev); + telephony_device_connected(dev); + } else if (hs->state == HEADSET_STATE_PLAYING) { + value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Stopped", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Playing", + DBUS_TYPE_BOOLEAN, &value); + } + break; + case HEADSET_STATE_PLAY_IN_PROGRESS: + break; + case HEADSET_STATE_PLAYING: + value = TRUE; + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + hs->sco_id = g_io_add_watch(hs->sco, + G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) sco_cb, dev); + + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "Playing", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "Playing", + DBUS_TYPE_BOOLEAN, &value); + + if (slc->sp_gain >= 0) + headset_send(hs, "\r\n+VGS=%u\r\n", slc->sp_gain); + if (slc->mic_gain >= 0) + headset_send(hs, "\r\n+VGM=%u\r\n", slc->mic_gain); + break; + } + + hs->state = state; + + DBG("State changed %s: %s -> %s", dev->path, str_state[old_state], + str_state[state]); + + for (l = headset_callbacks; l != NULL; l = l->next) { + struct headset_state_callback *cb = l->data; + cb->cb(dev, old_state, state, cb->user_data); + } +} + +headset_state_t headset_get_state(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->state; +} + +int headset_get_channel(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->rfcomm_ch; +} + +gboolean headset_is_active(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (hs->state != HEADSET_STATE_DISCONNECTED) + return TRUE; + + return FALSE; +} + +headset_lock_t headset_get_lock(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->lock; +} + +gboolean headset_lock(struct audio_device *dev, headset_lock_t lock) +{ + struct headset *hs = dev->headset; + + if (hs->lock & lock) + return FALSE; + + hs->lock |= lock; + + return TRUE; +} + +gboolean headset_unlock(struct audio_device *dev, headset_lock_t lock) +{ + struct headset *hs = dev->headset; + + if (!(hs->lock & lock)) + return FALSE; + + hs->lock &= ~lock; + + if (hs->lock) + return TRUE; + + if (hs->state == HEADSET_STATE_PLAYING) + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + if (hs->auto_dc) { + if (hs->state == HEADSET_STATE_CONNECTED) + hs->dc_timer = g_timeout_add_seconds(DC_TIMEOUT, + (GSourceFunc) hs_dc_timeout, + dev); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + } + + return TRUE; +} + +gboolean headset_suspend(struct audio_device *dev, void *data) +{ + return TRUE; +} + +gboolean headset_play(struct audio_device *dev, void *data) +{ + return TRUE; +} + +int headset_get_sco_fd(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (!hs->sco) + return -1; + + return g_io_channel_unix_get_fd(hs->sco); +} + +gboolean headset_get_nrec(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (!hs->slc) + return TRUE; + + return hs->slc->nrec; +} + +unsigned int headset_add_nrec_cb(struct audio_device *dev, + headset_nrec_cb cb, void *user_data) +{ + struct headset *hs = dev->headset; + struct headset_nrec_callback *nrec_cb; + static unsigned int id = 0; + + nrec_cb = g_new(struct headset_nrec_callback, 1); + nrec_cb->cb = cb; + nrec_cb->user_data = user_data; + nrec_cb->id = ++id; + + hs->nrec_cbs = g_slist_prepend(hs->nrec_cbs, nrec_cb); + + return nrec_cb->id; +} + +gboolean headset_remove_nrec_cb(struct audio_device *dev, unsigned int id) +{ + struct headset *hs = dev->headset; + GSList *l; + + for (l = hs->nrec_cbs; l != NULL; l = l->next) { + struct headset_nrec_callback *cb = l->data; + if (cb && cb->id == id) { + hs->nrec_cbs = g_slist_remove(hs->nrec_cbs, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} + +gboolean headset_get_inband(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (!hs->slc) + return TRUE; + + return hs->slc->inband_ring; +} + +gboolean headset_get_sco_hci(struct audio_device *dev) +{ + return sco_hci; +} + +void headset_shutdown(struct audio_device *dev) +{ + struct pending_connect *p = dev->headset->pending; + + if (p && p->msg) + error_connect_failed(dev->conn, p->msg, ECANCELED); + + pending_connect_finalize(dev); + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +int telephony_event_ind(int index) +{ + if (!active_devices) + return -ENODEV; + + if (!ag.er_ind) { + DBG("telephony_report_event called but events are disabled"); + return -EINVAL; + } + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CIEV: %d,%d\r\n", index + 1, + ag.indicators[index].val); + + return 0; +} + +int telephony_response_and_hold_ind(int rh) +{ + if (!active_devices) + return -ENODEV; + + ag.rh = rh; + + /* If we aren't in any response and hold state don't send anything */ + if (ag.rh < 0) + return 0; + + send_foreach_headset(active_devices, hfp_cmp, "\r\n+BTRH: %d\r\n", + ag.rh); + + return 0; +} + +int telephony_incoming_call_ind(const char *number, int type) +{ + struct audio_device *dev; + struct headset *hs; + struct headset_slc *slc; + + if (fast_connectable) + manager_set_fast_connectable(TRUE); + + if (!active_devices) + return -ENODEV; + + /* Get the latest connected device */ + dev = active_devices->data; + hs = dev->headset; + slc = hs->slc; + + if (ag.ring_timer) { + DBG("telephony_incoming_call_ind: already calling"); + return -EBUSY; + } + + /* With HSP 1.2 the RING messages should *not* be sent if inband + * ringtone is being used */ + if (!hs->hfp_active && slc->inband_ring) + return 0; + + g_free(ag.number); + ag.number = g_strdup(number); + ag.number_type = type; + + if (slc->inband_ring && hs->hfp_active && + hs->state != HEADSET_STATE_PLAYING) { + slc->pending_ring = TRUE; + return 0; + } + + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, ring_timer_cb, + NULL); + + return 0; +} + +int telephony_calling_stopped_ind(void) +{ + struct audio_device *dev; + + if (fast_connectable) + manager_set_fast_connectable(FALSE); + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + if (!active_devices) + return 0; + + /* In case SCO isn't fully up yet */ + dev = active_devices->data; + + if (!dev->headset->slc->pending_ring && !ag.ring_timer) + return -EINVAL; + + dev->headset->slc->pending_ring = FALSE; + + return 0; +} + +int telephony_ready_ind(uint32_t features, + const struct indicator *indicators, int rh, + const char *chld) +{ + ag.telephony_ready = TRUE; + ag.features = features; + ag.indicators = indicators; + ag.rh = rh; + ag.chld = chld; + + DBG("Telephony plugin initialized"); + + print_ag_features(ag.features); + + return 0; +} + +int telephony_deinit(void) +{ + g_free(ag.number); + + memset(&ag, 0, sizeof(ag)); + + ag.er_mode = 3; + ag.rh = BTRH_NOT_SUPPORTED; + + DBG("Telephony deinitialized"); + + return 0; +} + +int telephony_list_current_call_ind(int idx, int dir, int status, int mode, + int mprty, const char *number, + int type) +{ + if (!active_devices) + return -ENODEV; + + if (number && strlen(number) > 0) + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CLCC: %d,%d,%d,%d,%d,\"%s\",%d\r\n", + idx, dir, status, mode, mprty, number, type); + else + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CLCC: %d,%d,%d,%d,%d\r\n", + idx, dir, status, mode, mprty); + + return 0; +} + +int telephony_subscriber_number_ind(const char *number, int type, int service) +{ + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CNUM: ,%s,%d,,%d\r\n", + number, type, service); + + return 0; +} + +static int cwa_cmp(struct headset *hs) +{ + if (!hs->hfp_active) + return -1; + + if (hs->slc->cwa_enabled) + return 0; + else + return -1; +} + +int telephony_call_waiting_ind(const char *number, int type) +{ + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, cwa_cmp, + "\r\n+CCWA: \"%s\",%d\r\n", + number, type); + + return 0; +} + +unsigned int headset_add_state_cb(headset_state_cb cb, void *user_data) +{ + struct headset_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct headset_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + headset_callbacks = g_slist_append(headset_callbacks, state_cb); + + return state_cb->id; +} + +gboolean headset_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = headset_callbacks; l != NULL; l = l->next) { + struct headset_state_callback *cb = l->data; + if (cb && cb->id == id) { + headset_callbacks = g_slist_remove(headset_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/headset.h b/audio/headset.h new file mode 100644 index 0000000..7ce88c8 --- /dev/null +++ b/audio/headset.h @@ -0,0 +1,109 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_HEADSET_INTERFACE "org.bluez.Headset" + +#define DEFAULT_HS_AG_CHANNEL 12 +#define DEFAULT_HF_AG_CHANNEL 13 + +typedef enum { + HEADSET_STATE_DISCONNECTED, + HEADSET_STATE_CONNECTING, + HEADSET_STATE_CONNECTED, + HEADSET_STATE_PLAY_IN_PROGRESS, + HEADSET_STATE_PLAYING +} headset_state_t; + +typedef enum { + HEADSET_LOCK_READ = 1, + HEADSET_LOCK_WRITE = 1 << 1, +} headset_lock_t; + +typedef void (*headset_state_cb) (struct audio_device *dev, + headset_state_t old_state, + headset_state_t new_state, + void *user_data); +typedef void (*headset_nrec_cb) (struct audio_device *dev, + gboolean nrec, + void *user_data); + +unsigned int headset_add_state_cb(headset_state_cb cb, void *user_data); +gboolean headset_remove_state_cb(unsigned int id); + +typedef void (*headset_stream_cb_t) (struct audio_device *dev, void *user_data); + +void headset_connect_cb(GIOChannel *chan, GError *err, gpointer user_data); + +GIOChannel *headset_get_rfcomm(struct audio_device *dev); + +struct headset *headset_init(struct audio_device *dev, uint16_t svc, + const char *uuidstr); + +void headset_unregister(struct audio_device *dev); + +uint32_t headset_config_init(GKeyFile *config); + +void headset_update(struct audio_device *dev, uint16_t svc, + const char *uuidstr); + +unsigned int headset_config_stream(struct audio_device *dev, + gboolean auto_dc, + headset_stream_cb_t cb, + void *user_data); +unsigned int headset_request_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data); +unsigned int headset_suspend_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data); +gboolean headset_cancel_stream(struct audio_device *dev, unsigned int id); + +gboolean get_hfp_active(struct audio_device *dev); +void set_hfp_active(struct audio_device *dev, gboolean active); + +void headset_set_authorized(struct audio_device *dev); +int headset_connect_rfcomm(struct audio_device *dev, GIOChannel *chan); +int headset_connect_sco(struct audio_device *dev, GIOChannel *io); + +headset_state_t headset_get_state(struct audio_device *dev); +void headset_set_state(struct audio_device *dev, headset_state_t state); + +int headset_get_channel(struct audio_device *dev); + +int headset_get_sco_fd(struct audio_device *dev); +gboolean headset_get_nrec(struct audio_device *dev); +unsigned int headset_add_nrec_cb(struct audio_device *dev, + headset_nrec_cb cb, void *user_data); +gboolean headset_remove_nrec_cb(struct audio_device *dev, unsigned int id); +gboolean headset_get_inband(struct audio_device *dev); +gboolean headset_get_sco_hci(struct audio_device *dev); + +gboolean headset_is_active(struct audio_device *dev); + +headset_lock_t headset_get_lock(struct audio_device *dev); +gboolean headset_lock(struct audio_device *dev, headset_lock_t lock); +gboolean headset_unlock(struct audio_device *dev, headset_lock_t lock); +gboolean headset_suspend(struct audio_device *dev, void *data); +gboolean headset_play(struct audio_device *dev, void *data); +void headset_shutdown(struct audio_device *dev); diff --git a/audio/ipc.c b/audio/ipc.c new file mode 100644 index 0000000..1bdad78 --- /dev/null +++ b/audio/ipc.c @@ -0,0 +1,133 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * 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 "ipc.h" + +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +/* This table contains the string representation for messages types */ +static const char *strtypes[] = { + "BT_REQUEST", + "BT_RESPONSE", + "BT_INDICATION", + "BT_ERROR", +}; + +/* This table contains the string representation for messages names */ +static const char *strnames[] = { + "BT_GET_CAPABILITIES", + "BT_OPEN", + "BT_SET_CONFIGURATION", + "BT_NEW_STREAM", + "BT_START_STREAM", + "BT_STOP_STREAM", + "BT_SUSPEND_STREAM", + "BT_RESUME_STREAM", + "BT_CONTROL", +}; + +int bt_audio_service_open(void) +{ + int sk; + int err; + struct sockaddr_un addr = { + AF_UNIX, BT_IPC_SOCKET_NAME + }; + + sk = socket(PF_LOCAL, SOCK_STREAM, 0); + if (sk < 0) { + err = errno; + fprintf(stderr, "%s: Cannot open socket: %s (%d)\n", + __FUNCTION__, strerror(err), err); + errno = err; + return -1; + } + + if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + err = errno; + fprintf(stderr, "%s: connect() failed: %s (%d)\n", + __FUNCTION__, strerror(err), err); + close(sk); + errno = err; + return -1; + } + + return sk; +} + +int bt_audio_service_close(int sk) +{ + return close(sk); +} + +int bt_audio_service_get_data_fd(int sk) +{ + char cmsg_b[CMSG_SPACE(sizeof(int))], m; + int err, ret; + struct iovec iov = { &m, sizeof(m) }; + struct msghdr msgh; + struct cmsghdr *cmsg; + + memset(&msgh, 0, sizeof(msgh)); + msgh.msg_iov = &iov; + msgh.msg_iovlen = 1; + msgh.msg_control = &cmsg_b; + msgh.msg_controllen = CMSG_LEN(sizeof(int)); + + ret = recvmsg(sk, &msgh, 0); + if (ret < 0) { + err = errno; + fprintf(stderr, "%s: Unable to receive fd: %s (%d)\n", + __FUNCTION__, strerror(err), err); + errno = err; + return -1; + } + + /* Receive auxiliary data in msgh */ + for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL; + cmsg = CMSG_NXTHDR(&msgh, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET + && cmsg->cmsg_type == SCM_RIGHTS) { + memcpy(&ret, CMSG_DATA(cmsg), sizeof(int)); + return ret; + } + } + + errno = EINVAL; + return -1; +} + +const char *bt_audio_strtype(uint8_t type) +{ + if (type >= ARRAY_SIZE(strtypes)) + return NULL; + + return strtypes[type]; +} + +const char *bt_audio_strname(uint8_t name) +{ + if (name >= ARRAY_SIZE(strnames)) + return NULL; + + return strnames[name]; +} diff --git a/audio/ipc.h b/audio/ipc.h new file mode 100644 index 0000000..d69b97e --- /dev/null +++ b/audio/ipc.h @@ -0,0 +1,360 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * 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 + * + */ + +/* + Message sequence chart of streaming sequence for A2DP transport + + Audio daemon User + on snd_pcm_open + <--BT_GET_CAPABILITIES_REQ + + BT_GET_CAPABILITIES_RSP--> + + on snd_pcm_hw_params + <--BT_SETCONFIGURATION_REQ + + BT_SET_CONFIGURATION_RSP--> + + on snd_pcm_prepare + <--BT_START_STREAM_REQ + + <Moves to streaming state> + BT_START_STREAM_RSP--> + + BT_NEW_STREAM_IND --> + + < streams data > + .......... + + on snd_pcm_drop/snd_pcm_drain + + <--BT_STOP_STREAM_REQ + + <Moves to open state> + BT_STOP_STREAM_RSP--> + + on IPC close or appl crash + <Moves to idle> + + */ + +#ifndef BT_AUDIOCLIENT_H +#define BT_AUDIOCLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stdint.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <errno.h> + +#define BT_SUGGESTED_BUFFER_SIZE 512 +#define BT_IPC_SOCKET_NAME "\0/org/bluez/audio" + +/* Generic message header definition, except for RESPONSE messages */ +typedef struct { + uint8_t type; + uint8_t name; + uint16_t length; +} __attribute__ ((packed)) bt_audio_msg_header_t; + +typedef struct { + bt_audio_msg_header_t h; + uint8_t posix_errno; +} __attribute__ ((packed)) bt_audio_error_t; + +/* Message types */ +#define BT_REQUEST 0 +#define BT_RESPONSE 1 +#define BT_INDICATION 2 +#define BT_ERROR 3 + +/* Messages names */ +#define BT_GET_CAPABILITIES 0 +#define BT_OPEN 1 +#define BT_SET_CONFIGURATION 2 +#define BT_NEW_STREAM 3 +#define BT_START_STREAM 4 +#define BT_STOP_STREAM 5 +#define BT_CLOSE 6 +#define BT_CONTROL 7 +#define BT_DELAY_REPORT 8 + +#define BT_CAPABILITIES_TRANSPORT_A2DP 0 +#define BT_CAPABILITIES_TRANSPORT_SCO 1 +#define BT_CAPABILITIES_TRANSPORT_ANY 2 + +#define BT_CAPABILITIES_ACCESS_MODE_READ 1 +#define BT_CAPABILITIES_ACCESS_MODE_WRITE 2 +#define BT_CAPABILITIES_ACCESS_MODE_READWRITE 3 + +#define BT_FLAG_AUTOCONNECT 1 + +struct bt_get_capabilities_req { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ + uint8_t transport; /* Requested transport */ + uint8_t flags; /* Requested flags */ + uint8_t seid; /* Requested capability configuration */ +} __attribute__ ((packed)); + +/** + * SBC Codec parameters as per A2DP profile 1.0 § 4.3 + */ + +/* A2DP seid are 6 bytes long so HSP/HFP are assigned to 7-8 bits */ +#define BT_A2DP_SEID_RANGE (1 << 6) - 1 + +#define BT_A2DP_SBC_SOURCE 0x00 +#define BT_A2DP_SBC_SINK 0x01 +#define BT_A2DP_MPEG12_SOURCE 0x02 +#define BT_A2DP_MPEG12_SINK 0x03 +#define BT_A2DP_MPEG24_SOURCE 0x04 +#define BT_A2DP_MPEG24_SINK 0x05 +#define BT_A2DP_ATRAC_SOURCE 0x06 +#define BT_A2DP_ATRAC_SINK 0x07 +#define BT_A2DP_UNKNOWN_SOURCE 0x08 +#define BT_A2DP_UNKNOWN_SINK 0x09 + +#define BT_SBC_SAMPLING_FREQ_16000 (1 << 3) +#define BT_SBC_SAMPLING_FREQ_32000 (1 << 2) +#define BT_SBC_SAMPLING_FREQ_44100 (1 << 1) +#define BT_SBC_SAMPLING_FREQ_48000 1 + +#define BT_A2DP_CHANNEL_MODE_MONO (1 << 3) +#define BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define BT_A2DP_CHANNEL_MODE_STEREO (1 << 1) +#define BT_A2DP_CHANNEL_MODE_JOINT_STEREO 1 + +#define BT_A2DP_BLOCK_LENGTH_4 (1 << 3) +#define BT_A2DP_BLOCK_LENGTH_8 (1 << 2) +#define BT_A2DP_BLOCK_LENGTH_12 (1 << 1) +#define BT_A2DP_BLOCK_LENGTH_16 1 + +#define BT_A2DP_SUBBANDS_4 (1 << 1) +#define BT_A2DP_SUBBANDS_8 1 + +#define BT_A2DP_ALLOCATION_SNR (1 << 1) +#define BT_A2DP_ALLOCATION_LOUDNESS 1 + +#define BT_MPEG_SAMPLING_FREQ_16000 (1 << 5) +#define BT_MPEG_SAMPLING_FREQ_22050 (1 << 4) +#define BT_MPEG_SAMPLING_FREQ_24000 (1 << 3) +#define BT_MPEG_SAMPLING_FREQ_32000 (1 << 2) +#define BT_MPEG_SAMPLING_FREQ_44100 (1 << 1) +#define BT_MPEG_SAMPLING_FREQ_48000 1 + +#define BT_MPEG_LAYER_1 (1 << 2) +#define BT_MPEG_LAYER_2 (1 << 1) +#define BT_MPEG_LAYER_3 1 + +#define BT_HFP_CODEC_PCM 0x00 + +#define BT_PCM_FLAG_NREC 0x01 +#define BT_PCM_FLAG_PCM_ROUTING 0x02 + +#define BT_WRITE_LOCK (1 << 1) +#define BT_READ_LOCK 1 + +typedef struct { + uint8_t seid; + uint8_t transport; + uint8_t type; + uint8_t length; + uint8_t configured; + uint8_t lock; + uint8_t data[0]; +} __attribute__ ((packed)) codec_capabilities_t; + +typedef struct { + codec_capabilities_t capability; + uint8_t channel_mode; + uint8_t frequency; + uint8_t allocation_method; + uint8_t subbands; + uint8_t block_length; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) sbc_capabilities_t; + +typedef struct { + codec_capabilities_t capability; + uint8_t channel_mode; + uint8_t crc; + uint8_t layer; + uint8_t frequency; + uint8_t mpf; + uint16_t bitrate; +} __attribute__ ((packed)) mpeg_capabilities_t; + +typedef struct { + codec_capabilities_t capability; + uint8_t flags; + uint16_t sampling_rate; +} __attribute__ ((packed)) pcm_capabilities_t; + +struct bt_get_capabilities_rsp { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ + uint8_t data[0]; /* First codec_capabilities_t */ +} __attribute__ ((packed)); + +struct bt_open_req { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ + uint8_t seid; /* Requested capability configuration to lock */ + uint8_t lock; /* Requested lock */ +} __attribute__ ((packed)); + +struct bt_open_rsp { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ +} __attribute__ ((packed)); + +struct bt_set_configuration_req { + bt_audio_msg_header_t h; + codec_capabilities_t codec; /* Requested codec */ +} __attribute__ ((packed)); + +struct bt_set_configuration_rsp { + bt_audio_msg_header_t h; + uint16_t link_mtu; /* Max length that transport supports */ +} __attribute__ ((packed)); + +#define BT_STREAM_ACCESS_READ 0 +#define BT_STREAM_ACCESS_WRITE 1 +#define BT_STREAM_ACCESS_READWRITE 2 +struct bt_start_stream_req { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_start_stream_rsp { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +/* This message is followed by one byte of data containing the stream data fd + as ancilliary data */ +struct bt_new_stream_ind { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_stop_stream_req { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_stop_stream_rsp { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_close_req { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_close_rsp { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_suspend_stream_ind { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_resume_stream_ind { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +#define BT_CONTROL_KEY_POWER 0x40 +#define BT_CONTROL_KEY_VOL_UP 0x41 +#define BT_CONTROL_KEY_VOL_DOWN 0x42 +#define BT_CONTROL_KEY_MUTE 0x43 +#define BT_CONTROL_KEY_PLAY 0x44 +#define BT_CONTROL_KEY_STOP 0x45 +#define BT_CONTROL_KEY_PAUSE 0x46 +#define BT_CONTROL_KEY_RECORD 0x47 +#define BT_CONTROL_KEY_REWIND 0x48 +#define BT_CONTROL_KEY_FAST_FORWARD 0x49 +#define BT_CONTROL_KEY_EJECT 0x4A +#define BT_CONTROL_KEY_FORWARD 0x4B +#define BT_CONTROL_KEY_BACKWARD 0x4C + +struct bt_control_req { + bt_audio_msg_header_t h; + uint8_t mode; /* Control Mode */ + uint8_t key; /* Control Key */ +} __attribute__ ((packed)); + +struct bt_control_rsp { + bt_audio_msg_header_t h; + uint8_t mode; /* Control Mode */ + uint8_t key; /* Control Key */ +} __attribute__ ((packed)); + +struct bt_control_ind { + bt_audio_msg_header_t h; + uint8_t mode; /* Control Mode */ + uint8_t key; /* Control Key */ +} __attribute__ ((packed)); + +struct bt_delay_report_req { + bt_audio_msg_header_t h; + uint16_t delay; +} __attribute__ ((packed)); + +struct bt_delay_report_ind { + bt_audio_msg_header_t h; + uint16_t delay; +} __attribute__ ((packed)); + +/* Function declaration */ + +/* Opens a connection to the audio service: return a socket descriptor */ +int bt_audio_service_open(void); + +/* Closes a connection to the audio service */ +int bt_audio_service_close(int sk); + +/* Receives stream data file descriptor : must be called after a +BT_STREAMFD_IND message is returned */ +int bt_audio_service_get_data_fd(int sk); + +/* Human readable message type string */ +const char *bt_audio_strtype(uint8_t type); + +/* Human readable message name string */ +const char *bt_audio_strname(uint8_t name); + +#ifdef __cplusplus +} +#endif + +#endif /* BT_AUDIOCLIENT_H */ diff --git a/audio/main.c b/audio/main.c new file mode 100644 index 0000000..745c307 --- /dev/null +++ b/audio/main.c @@ -0,0 +1,194 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <errno.h> +#include <sys/socket.h> +#include <unistd.h> +#include <fcntl.h> +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> + +#include "glib-helper.h" +#include "btio.h" +#include "plugin.h" +#include "log.h" +#include "device.h" +#include "headset.h" +#include "manager.h" +#include "gateway.h" + +static GIOChannel *sco_server = NULL; + +static GKeyFile *load_config_file(const char *file) +{ + GError *err = NULL; + GKeyFile *keyfile; + + keyfile = g_key_file_new(); + + g_key_file_set_list_separator(keyfile, ','); + + if (!g_key_file_load_from_file(keyfile, file, 0, &err)) { + error("Parsing %s failed: %s", file, err->message); + g_error_free(err); + g_key_file_free(keyfile); + return NULL; + } + + return keyfile; +} + +static void sco_server_cb(GIOChannel *chan, GError *err, gpointer data) +{ + int sk; + struct audio_device *device; + char addr[18]; + bdaddr_t src, dst; + + if (err) { + error("sco_server_cb: %s", err->message); + return; + } + + bt_io_get(chan, BT_IO_SCO, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_DEST, addr, + BT_IO_OPT_INVALID); + if (err) { + error("bt_io_get: %s", err->message); + goto drop; + } + + device = manager_find_device(NULL, &src, &dst, AUDIO_HEADSET_INTERFACE, + FALSE); + if (!device) + device = manager_find_device(NULL, &src, &dst, + AUDIO_GATEWAY_INTERFACE, + FALSE); + + if (!device) + goto drop; + + if (device->headset) { + if (headset_get_state(device) < HEADSET_STATE_CONNECTED) { + DBG("Refusing SCO from non-connected headset"); + goto drop; + } + + if (!get_hfp_active(device)) { + error("Refusing non-HFP SCO connect attempt from %s", + addr); + goto drop; + } + + if (headset_connect_sco(device, chan) < 0) + goto drop; + + headset_set_state(device, HEADSET_STATE_PLAYING); + } else if (device->gateway) { + if (!gateway_is_connected(device)) { + DBG("Refusing SCO from non-connected AG"); + goto drop; + } + + if (gateway_connect_sco(device, chan) < 0) + goto drop; + } else + goto drop; + + sk = g_io_channel_unix_get_fd(chan); + fcntl(sk, F_SETFL, 0); + + DBG("Accepted SCO connection from %s", addr); + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static DBusConnection *connection; + +static int audio_init(void) +{ + GKeyFile *config; + gboolean enable_sco; + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + if (connection == NULL) + return -EIO; + + config = load_config_file(CONFIGDIR "/audio.conf"); + + if (audio_manager_init(connection, config, &enable_sco) < 0) + goto failed; + + if (!enable_sco) + return 0; + + sco_server = bt_io_listen(BT_IO_SCO, sco_server_cb, NULL, NULL, + NULL, NULL, + BT_IO_OPT_INVALID); + if (!sco_server) { + error("Unable to start SCO server socket"); + goto failed; + } + + return 0; + +failed: + audio_manager_exit(); + + if (connection) { + dbus_connection_unref(connection); + connection = NULL; + } + + return -EIO; +} + +static void audio_exit(void) +{ + if (sco_server) { + g_io_channel_shutdown(sco_server, TRUE, NULL); + g_io_channel_unref(sco_server); + sco_server = NULL; + } + + audio_manager_exit(); + + dbus_connection_unref(connection); +} + +BLUETOOTH_PLUGIN_DEFINE(audio, VERSION, + BLUETOOTH_PLUGIN_PRIORITY_DEFAULT, audio_init, audio_exit) diff --git a/audio/manager.c b/audio/manager.c new file mode 100644 index 0000000..7e206be --- /dev/null +++ b/audio/manager.c @@ -0,0 +1,1415 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdint.h> +#include <sys/stat.h> +#include <dirent.h> +#include <ctype.h> +#include <signal.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "glib-helper.h" +#include "btio.h" +#include "../src/adapter.h" +#include "../src/manager.h" +#include "../src/device.h" + +#include "log.h" +#include "textfile.h" +#include "ipc.h" +#include "device.h" +#include "error.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "headset.h" +#include "gateway.h" +#include "sink.h" +#include "source.h" +#include "control.h" +#include "manager.h" +#include "sdpd.h" +#include "telephony.h" +#include "unix.h" + +#ifndef DBUS_TYPE_UNIX_FD +#define DBUS_TYPE_UNIX_FD -1 +#endif + +typedef enum { + HEADSET = 1 << 0, + GATEWAY = 1 << 1, + SINK = 1 << 2, + SOURCE = 1 << 3, + CONTROL = 1 << 4, + TARGET = 1 << 5, + INVALID = 1 << 6 +} audio_service_type; + +typedef enum { + GENERIC_AUDIO = 0, + ADVANCED_AUDIO, + AV_REMOTE, + GET_RECORDS +} audio_sdp_state_t; + +struct audio_adapter { + struct btd_adapter *btd_adapter; + gboolean powered; + uint32_t hsp_ag_record_id; + uint32_t hfp_ag_record_id; + uint32_t hfp_hs_record_id; + GIOChannel *hsp_ag_server; + GIOChannel *hfp_ag_server; + GIOChannel *hfp_hs_server; + gint ref; +}; + +static gboolean auto_connect = TRUE; +static int max_connected_headsets = 1; +static DBusConnection *connection = NULL; +static GKeyFile *config = NULL; +static GSList *adapters = NULL; +static GSList *devices = NULL; + +static struct enabled_interfaces enabled = { + .hfp = TRUE, + .headset = TRUE, + .gateway = FALSE, + .sink = TRUE, + .source = FALSE, + .control = TRUE, + .socket = TRUE, + .media = FALSE +}; + +static struct audio_adapter *find_adapter(GSList *list, + struct btd_adapter *btd_adapter) +{ + GSList *l; + + for (l = list; l; l = l->next) { + struct audio_adapter *adapter = l->data; + + if (adapter->btd_adapter == btd_adapter) + return adapter; + } + + return NULL; +} + +gboolean server_is_enabled(bdaddr_t *src, uint16_t svc) +{ + switch (svc) { + case HEADSET_SVCLASS_ID: + return enabled.headset; + case HEADSET_AGW_SVCLASS_ID: + return FALSE; + case HANDSFREE_SVCLASS_ID: + return enabled.headset && enabled.hfp; + case HANDSFREE_AGW_SVCLASS_ID: + return enabled.gateway; + case AUDIO_SINK_SVCLASS_ID: + return enabled.sink; + case AUDIO_SOURCE_SVCLASS_ID: + return enabled.source; + case AV_REMOTE_TARGET_SVCLASS_ID: + case AV_REMOTE_SVCLASS_ID: + return enabled.control; + default: + return FALSE; + } +} + +static void handle_uuid(const char *uuidstr, struct audio_device *device) +{ + uuid_t uuid; + uint16_t uuid16; + + if (bt_string2uuid(&uuid, uuidstr) < 0) { + error("%s not detected as an UUID-128", uuidstr); + return; + } + + if (!sdp_uuid128_to_uuid(&uuid) && uuid.type != SDP_UUID16) { + error("Could not convert %s to a UUID-16", uuidstr); + return; + } + + uuid16 = uuid.value.uuid16; + + if (!server_is_enabled(&device->src, uuid16)) { + DBG("server not enabled for %s (0x%04x)", uuidstr, uuid16); + return; + } + + switch (uuid16) { + case HEADSET_SVCLASS_ID: + DBG("Found Headset record"); + if (device->headset) + headset_update(device, uuid16, uuidstr); + else + device->headset = headset_init(device, uuid16, + uuidstr); + break; + case HEADSET_AGW_SVCLASS_ID: + DBG("Found Headset AG record"); + break; + case HANDSFREE_SVCLASS_ID: + DBG("Found Handsfree record"); + if (device->headset) + headset_update(device, uuid16, uuidstr); + else + device->headset = headset_init(device, uuid16, + uuidstr); + break; + case HANDSFREE_AGW_SVCLASS_ID: + DBG("Found Handsfree AG record"); + if (enabled.gateway && (device->gateway == NULL)) + device->gateway = gateway_init(device); + break; + case AUDIO_SINK_SVCLASS_ID: + DBG("Found Audio Sink"); + if (device->sink == NULL) + device->sink = sink_init(device); + break; + case AUDIO_SOURCE_SVCLASS_ID: + DBG("Found Audio Source"); + if (device->source == NULL) + device->source = source_init(device); + break; + case AV_REMOTE_SVCLASS_ID: + case AV_REMOTE_TARGET_SVCLASS_ID: + DBG("Found AV %s", uuid16 == AV_REMOTE_SVCLASS_ID ? + "Remote" : "Target"); + if (device->control) + control_update(device, uuid16); + else + device->control = control_init(device, uuid16); + if (device->sink && sink_is_active(device)) + avrcp_connect(device); + break; + default: + DBG("Unrecognized UUID: 0x%04X", uuid16); + break; + } +} + +static sdp_record_t *hsp_ag_record(uint8_t ch) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, svclass_uuid, ga_svclass_uuid; + uuid_t l2cap_uuid, rfcomm_uuid; + sdp_profile_desc_t profile; + sdp_record_t *record; + sdp_list_t *aproto, *proto[2]; + sdp_data_t *channel; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + sdp_uuid16_create(&svclass_uuid, HEADSET_AGW_SVCLASS_ID); + svclass_id = sdp_list_append(0, &svclass_uuid); + sdp_uuid16_create(&ga_svclass_uuid, GENERIC_AUDIO_SVCLASS_ID); + svclass_id = sdp_list_append(svclass_id, &ga_svclass_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile.uuid, HEADSET_PROFILE_ID); + profile.version = 0x0102; + pfseq = sdp_list_append(0, &profile); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + proto[1] = sdp_list_append(0, &rfcomm_uuid); + channel = sdp_data_alloc(SDP_UINT8, &ch); + proto[1] = sdp_list_append(proto[1], channel); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + sdp_set_info_attr(record, "Headset Audio Gateway", 0, 0); + + sdp_data_free(channel); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static sdp_record_t *hfp_hs_record(uint8_t ch) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, svclass_uuid, ga_svclass_uuid; + uuid_t l2cap_uuid, rfcomm_uuid; + sdp_profile_desc_t profile; + sdp_record_t *record; + sdp_list_t *aproto, *proto[2]; + sdp_data_t *channel; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + sdp_uuid16_create(&svclass_uuid, HANDSFREE_SVCLASS_ID); + svclass_id = sdp_list_append(0, &svclass_uuid); + sdp_uuid16_create(&ga_svclass_uuid, GENERIC_AUDIO_SVCLASS_ID); + svclass_id = sdp_list_append(svclass_id, &ga_svclass_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile.uuid, HANDSFREE_PROFILE_ID); + profile.version = 0x0105; + pfseq = sdp_list_append(0, &profile); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + proto[1] = sdp_list_append(0, &rfcomm_uuid); + channel = sdp_data_alloc(SDP_UINT8, &ch); + proto[1] = sdp_list_append(proto[1], channel); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + sdp_set_info_attr(record, "Hands-Free", 0, 0); + + sdp_data_free(channel); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static sdp_record_t *hfp_ag_record(uint8_t ch, uint32_t feat) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, svclass_uuid, ga_svclass_uuid; + uuid_t l2cap_uuid, rfcomm_uuid; + sdp_profile_desc_t profile; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *channel, *features; + uint8_t netid = 0x01; + uint16_t sdpfeat; + sdp_data_t *network; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + network = sdp_data_alloc(SDP_UINT8, &netid); + if (!network) { + sdp_record_free(record); + return NULL; + } + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + sdp_uuid16_create(&svclass_uuid, HANDSFREE_AGW_SVCLASS_ID); + svclass_id = sdp_list_append(0, &svclass_uuid); + sdp_uuid16_create(&ga_svclass_uuid, GENERIC_AUDIO_SVCLASS_ID); + svclass_id = sdp_list_append(svclass_id, &ga_svclass_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile.uuid, HANDSFREE_PROFILE_ID); + profile.version = 0x0105; + pfseq = sdp_list_append(0, &profile); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + proto[1] = sdp_list_append(0, &rfcomm_uuid); + channel = sdp_data_alloc(SDP_UINT8, &ch); + proto[1] = sdp_list_append(proto[1], channel); + apseq = sdp_list_append(apseq, proto[1]); + + sdpfeat = (uint16_t) feat & 0xF; + features = sdp_data_alloc(SDP_UINT16, &sdpfeat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + sdp_set_info_attr(record, "Hands-Free Audio Gateway", 0, 0); + + sdp_attr_add(record, SDP_ATTR_EXTERNAL_NETWORK, network); + + sdp_data_free(channel); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static void headset_auth_cb(DBusError *derr, void *user_data) +{ + struct audio_device *device = user_data; + GError *err = NULL; + GIOChannel *io; + + if (device->hs_preauth_id) { + g_source_remove(device->hs_preauth_id); + device->hs_preauth_id = 0; + } + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + return; + } + + io = headset_get_rfcomm(device); + + if (!bt_io_accept(io, headset_connect_cb, device, NULL, &err)) { + error("bt_io_accept: %s", err->message); + g_error_free(err); + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + return; + } +} + +static gboolean hs_preauth_cb(GIOChannel *chan, GIOCondition cond, + gpointer user_data) +{ + struct audio_device *device = user_data; + + DBG("Headset disconnected during authorization"); + + audio_device_cancel_authorization(device, headset_auth_cb, device); + + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + + device->hs_preauth_id = 0; + + return FALSE; +} + +static void ag_confirm(GIOChannel *chan, gpointer data) +{ + const char *server_uuid, *remote_uuid; + struct audio_device *device; + gboolean hfp_active; + bdaddr_t src, dst; + int perr; + GError *err = NULL; + uint8_t ch; + + bt_io_get(chan, BT_IO_RFCOMM, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_CHANNEL, &ch, + BT_IO_OPT_INVALID); + if (err) { + error("%s", err->message); + g_error_free(err); + goto drop; + } + + if (ch == DEFAULT_HS_AG_CHANNEL) { + hfp_active = FALSE; + server_uuid = HSP_AG_UUID; + remote_uuid = HSP_HS_UUID; + } else { + hfp_active = TRUE; + server_uuid = HFP_AG_UUID; + remote_uuid = HFP_HS_UUID; + } + + device = manager_get_device(&src, &dst, TRUE); + if (!device) + goto drop; + + if (!manager_allow_headset_connection(device)) { + DBG("Refusing headset: too many existing connections"); + goto drop; + } + + if (!device->headset) { + btd_device_add_uuid(device->btd_dev, remote_uuid); + if (!device->headset) + goto drop; + } + + if (headset_get_state(device) > HEADSET_STATE_DISCONNECTED) { + DBG("Refusing new connection since one already exists"); + goto drop; + } + + set_hfp_active(device, hfp_active); + + if (headset_connect_rfcomm(device, chan) < 0) { + error("headset_connect_rfcomm failed"); + goto drop; + } + + headset_set_state(device, HEADSET_STATE_CONNECTING); + + perr = audio_device_request_authorization(device, server_uuid, + headset_auth_cb, device); + if (perr < 0) { + DBG("Authorization denied: %s", strerror(-perr)); + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + return; + } + + device->hs_preauth_id = g_io_add_watch(chan, + G_IO_NVAL | G_IO_HUP | G_IO_ERR, + hs_preauth_cb, device); + + device->auto_connect = auto_connect; + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static void gateway_auth_cb(DBusError *derr, void *user_data) +{ + struct audio_device *device = user_data; + + if (derr && dbus_error_is_set(derr)) + error("Access denied: %s", derr->message); + else { + char ag_address[18]; + + ba2str(&device->dst, ag_address); + DBG("Accepted AG connection from %s for %s", + ag_address, device->path); + + gateway_start_service(device); + } +} + +static void hf_io_cb(GIOChannel *chan, gpointer data) +{ + bdaddr_t src, dst; + GError *err = NULL; + uint8_t ch; + const char *server_uuid, *remote_uuid; + uint16_t svclass; + struct audio_device *device; + int perr; + + bt_io_get(chan, BT_IO_RFCOMM, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_CHANNEL, &ch, + BT_IO_OPT_INVALID); + + if (err) { + error("%s", err->message); + g_error_free(err); + return; + } + + server_uuid = HFP_AG_UUID; + remote_uuid = HFP_HS_UUID; + svclass = HANDSFREE_AGW_SVCLASS_ID; + + device = manager_get_device(&src, &dst, TRUE); + if (!device) + goto drop; + + if (!device->gateway) { + btd_device_add_uuid(device->btd_dev, remote_uuid); + if (!device->gateway) + goto drop; + } + + if (gateway_is_connected(device)) { + DBG("Refusing new connection since one already exists"); + goto drop; + } + + if (gateway_connect_rfcomm(device, chan) < 0) { + error("Allocating new GIOChannel failed!"); + goto drop; + } + + perr = audio_device_request_authorization(device, server_uuid, + gateway_auth_cb, device); + if (perr < 0) { + DBG("Authorization denied!"); + goto drop; + } + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static int headset_server_init(struct audio_adapter *adapter) +{ + uint8_t chan = DEFAULT_HS_AG_CHANNEL; + sdp_record_t *record; + gboolean master = TRUE; + GError *err = NULL; + uint32_t features; + GIOChannel *io; + bdaddr_t src; + + if (config) { + gboolean tmp; + + tmp = g_key_file_get_boolean(config, "General", "Master", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + master = tmp; + } + + adapter_get_address(adapter->btd_adapter, &src); + + io = bt_io_listen(BT_IO_RFCOMM, NULL, ag_confirm, adapter, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_CHANNEL, chan, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) + goto failed; + + adapter->hsp_ag_server = io; + + record = hsp_ag_record(chan); + if (!record) { + error("Unable to allocate new service record"); + goto failed; + } + + if (add_record_to_server(&src, record) < 0) { + error("Unable to register HS AG service record"); + sdp_record_free(record); + goto failed; + } + adapter->hsp_ag_record_id = record->handle; + + features = headset_config_init(config); + + if (!enabled.hfp) + return 0; + + chan = DEFAULT_HF_AG_CHANNEL; + + io = bt_io_listen(BT_IO_RFCOMM, NULL, ag_confirm, adapter, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_CHANNEL, chan, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) + goto failed; + + adapter->hfp_ag_server = io; + + record = hfp_ag_record(chan, features); + if (!record) { + error("Unable to allocate new service record"); + goto failed; + } + + if (add_record_to_server(&src, record) < 0) { + error("Unable to register HF AG service record"); + sdp_record_free(record); + goto failed; + } + adapter->hfp_ag_record_id = record->handle; + + return 0; + +failed: + error("%s", err->message); + g_error_free(err); + if (adapter->hsp_ag_server) { + g_io_channel_shutdown(adapter->hsp_ag_server, TRUE, NULL); + g_io_channel_unref(adapter->hsp_ag_server); + adapter->hsp_ag_server = NULL; + } + + if (adapter->hfp_ag_server) { + g_io_channel_shutdown(adapter->hfp_ag_server, TRUE, NULL); + g_io_channel_unref(adapter->hfp_ag_server); + adapter->hfp_ag_server = NULL; + } + + return -1; +} + +static int gateway_server_init(struct audio_adapter *adapter) +{ + uint8_t chan = DEFAULT_HFP_HS_CHANNEL; + sdp_record_t *record; + gboolean master = TRUE; + GError *err = NULL; + GIOChannel *io; + bdaddr_t src; + + if (config) { + gboolean tmp; + + tmp = g_key_file_get_boolean(config, "General", "Master", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + master = tmp; + } + + adapter_get_address(adapter->btd_adapter, &src); + + io = bt_io_listen(BT_IO_RFCOMM, NULL, hf_io_cb, adapter, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_CHANNEL, chan, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return -1; + } + + adapter->hfp_hs_server = io; + record = hfp_hs_record(chan); + if (!record) { + error("Unable to allocate new service record"); + return -1; + } + + if (add_record_to_server(&src, record) < 0) { + error("Unable to register HFP HS service record"); + sdp_record_free(record); + g_io_channel_unref(adapter->hfp_hs_server); + adapter->hfp_hs_server = NULL; + return -1; + } + + adapter->hfp_hs_record_id = record->handle; + + return 0; +} + +static int audio_probe(struct btd_device *device, GSList *uuids) +{ + struct btd_adapter *adapter = device_get_adapter(device); + bdaddr_t src, dst; + struct audio_device *audio_dev; + + adapter_get_address(adapter, &src); + device_get_address(device, &dst); + + audio_dev = manager_get_device(&src, &dst, TRUE); + if (!audio_dev) { + DBG("unable to get a device object"); + return -1; + } + + g_slist_foreach(uuids, (GFunc) handle_uuid, audio_dev); + + return 0; +} + +static void audio_remove(struct btd_device *device) +{ + struct audio_device *dev; + const char *path; + + path = device_get_path(device); + + dev = manager_find_device(path, NULL, NULL, NULL, FALSE); + if (!dev) + return; + + devices = g_slist_remove(devices, dev); + + audio_device_unregister(dev); + +} + +static struct audio_adapter *audio_adapter_ref(struct audio_adapter *adp) +{ + adp->ref++; + + DBG("%p: ref=%d", adp, adp->ref); + + return adp; +} + +static void audio_adapter_unref(struct audio_adapter *adp) +{ + adp->ref--; + + DBG("%p: ref=%d", adp, adp->ref); + + if (adp->ref > 0) + return; + + adapters = g_slist_remove(adapters, adp); + btd_adapter_unref(adp->btd_adapter); + g_free(adp); +} + +static struct audio_adapter *audio_adapter_create(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + + adp = g_new0(struct audio_adapter, 1); + adp->btd_adapter = btd_adapter_ref(adapter); + + return audio_adapter_ref(adp); +} + +static struct audio_adapter *audio_adapter_get(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + + adp = find_adapter(adapters, adapter); + if (!adp) { + adp = audio_adapter_create(adapter); + if (!adp) + return NULL; + adapters = g_slist_append(adapters, adp); + } else + audio_adapter_ref(adp); + + return adp; +} + +static void state_changed(struct btd_adapter *adapter, gboolean powered) +{ + struct audio_adapter *adp; + static gboolean telephony = FALSE; + GSList *l; + + DBG("%s powered %s", adapter_get_path(adapter), + powered ? "on" : "off"); + + /* ignore powered change, adapter is powering down */ + if (powered && adapter_powering_down(adapter)) + return; + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + adp->powered = powered; + + if (powered) { + /* telephony driver already initialized*/ + if (telephony == TRUE) + return; + telephony_init(); + telephony = TRUE; + return; + } + + /* telephony not initialized just ignore power down */ + if (telephony == FALSE) + return; + + for (l = adapters; l; l = l->next) { + adp = l->data; + + if (adp->powered == TRUE) + return; + } + + telephony_exit(); + telephony = FALSE; +} + +static int headset_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + int err; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + err = headset_server_init(adp); + if (err < 0) { + audio_adapter_unref(adp); + return err; + } + + btd_adapter_register_powered_callback(adapter, state_changed); + state_changed(adapter, TRUE); + + return 0; +} + +static void headset_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + if (adp->hsp_ag_record_id) { + remove_record_from_server(adp->hsp_ag_record_id); + adp->hsp_ag_record_id = 0; + } + + if (adp->hsp_ag_server) { + g_io_channel_shutdown(adp->hsp_ag_server, TRUE, NULL); + g_io_channel_unref(adp->hsp_ag_server); + adp->hsp_ag_server = NULL; + } + + if (adp->hfp_ag_record_id) { + remove_record_from_server(adp->hfp_ag_record_id); + adp->hfp_ag_record_id = 0; + } + + if (adp->hfp_ag_server) { + g_io_channel_shutdown(adp->hfp_ag_server, TRUE, NULL); + g_io_channel_unref(adp->hfp_ag_server); + adp->hfp_ag_server = NULL; + } + + btd_adapter_unregister_powered_callback(adapter, state_changed); + + audio_adapter_unref(adp); +} + +static int gateway_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + return gateway_server_init(adp); +} + +static void gateway_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + if (adp->hfp_hs_record_id) { + remove_record_from_server(adp->hfp_hs_record_id); + adp->hfp_hs_record_id = 0; + } + + if (adp->hfp_hs_server) { + g_io_channel_unref(adp->hfp_hs_server); + adp->hfp_hs_server = NULL; + } + + audio_adapter_unref(adp); +} + +static int a2dp_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + int err; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + adapter_get_address(adapter, &src); + + err = a2dp_register(connection, &src, config); + if (err < 0) + audio_adapter_unref(adp); + + return err; +} + +static void a2dp_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + adapter_get_address(adapter, &src); + a2dp_unregister(&src); + audio_adapter_unref(adp); +} + +static int avrcp_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + adapter_get_address(adapter, &src); + + return avrcp_register(connection, &src, config); +} + +static void avrcp_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + adapter_get_address(adapter, &src); + avrcp_unregister(&src); + audio_adapter_unref(adp); +} + +static int media_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + adapter_get_address(adapter, &src); + + return media_register(connection, path, &src); +} + +static void media_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + media_unregister(path); + audio_adapter_unref(adp); +} + +static struct btd_device_driver audio_driver = { + .name = "audio", + .uuids = BTD_UUIDS(HSP_HS_UUID, HFP_HS_UUID, HSP_AG_UUID, HFP_AG_UUID, + ADVANCED_AUDIO_UUID, A2DP_SOURCE_UUID, A2DP_SINK_UUID, + AVRCP_TARGET_UUID, AVRCP_REMOTE_UUID), + .probe = audio_probe, + .remove = audio_remove, +}; + +static struct btd_adapter_driver headset_server_driver = { + .name = "audio-headset", + .probe = headset_server_probe, + .remove = headset_server_remove, +}; + +static struct btd_adapter_driver gateway_server_driver = { + .name = "audio-gateway", + .probe = gateway_server_probe, + .remove = gateway_server_remove, +}; + +static struct btd_adapter_driver a2dp_server_driver = { + .name = "audio-a2dp", + .probe = a2dp_server_probe, + .remove = a2dp_server_remove, +}; + +static struct btd_adapter_driver avrcp_server_driver = { + .name = "audio-control", + .probe = avrcp_server_probe, + .remove = avrcp_server_remove, +}; + +static struct btd_adapter_driver media_server_driver = { + .name = "media", + .probe = media_server_probe, + .remove = media_server_remove, +}; + +int audio_manager_init(DBusConnection *conn, GKeyFile *conf, + gboolean *enable_sco) +{ + char **list; + int i; + gboolean b; + GError *err = NULL; + + connection = dbus_connection_ref(conn); + + if (!conf) + goto proceed; + + config = conf; + + list = g_key_file_get_string_list(config, "General", "Enable", + NULL, NULL); + for (i = 0; list && list[i] != NULL; i++) { + if (g_str_equal(list[i], "Headset")) + enabled.headset = TRUE; + else if (g_str_equal(list[i], "Gateway")) + enabled.gateway = TRUE; + else if (g_str_equal(list[i], "Sink")) + enabled.sink = TRUE; + else if (g_str_equal(list[i], "Source")) + enabled.source = TRUE; + else if (g_str_equal(list[i], "Control")) + enabled.control = TRUE; + else if (g_str_equal(list[i], "Socket")) + enabled.socket = TRUE; + else if (g_str_equal(list[i], "Media")) + enabled.media = TRUE; + } + g_strfreev(list); + + list = g_key_file_get_string_list(config, "General", "Disable", + NULL, NULL); + for (i = 0; list && list[i] != NULL; i++) { + if (g_str_equal(list[i], "Headset")) + enabled.headset = FALSE; + else if (g_str_equal(list[i], "Gateway")) + enabled.gateway = FALSE; + else if (g_str_equal(list[i], "Sink")) + enabled.sink = FALSE; + else if (g_str_equal(list[i], "Source")) + enabled.source = FALSE; + else if (g_str_equal(list[i], "Control")) + enabled.control = FALSE; + else if (g_str_equal(list[i], "Socket")) + enabled.socket = FALSE; + else if (g_str_equal(list[i], "Media")) + enabled.media = FALSE; + } + g_strfreev(list); + + b = g_key_file_get_boolean(config, "General", "AutoConnect", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + auto_connect = b; + + b = g_key_file_get_boolean(config, "Headset", "HFP", + &err); + if (err) + g_clear_error(&err); + else + enabled.hfp = b; + + err = NULL; + i = g_key_file_get_integer(config, "Headset", "MaxConnected", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + max_connected_headsets = i; + +proceed: + if (enabled.socket) + unix_init(); + + if (enabled.media) + btd_register_adapter_driver(&media_server_driver); + + if (enabled.headset) + btd_register_adapter_driver(&headset_server_driver); + + if (enabled.gateway) + btd_register_adapter_driver(&gateway_server_driver); + + if (enabled.source || enabled.sink) + btd_register_adapter_driver(&a2dp_server_driver); + + if (enabled.control) + btd_register_adapter_driver(&avrcp_server_driver); + + btd_register_device_driver(&audio_driver); + + *enable_sco = (enabled.gateway || enabled.headset); + + return 0; +} + +void audio_manager_exit(void) +{ + /* Bail out early if we haven't been initialized */ + if (connection == NULL) + return; + + dbus_connection_unref(connection); + connection = NULL; + + if (config) { + g_key_file_free(config); + config = NULL; + } + + if (enabled.socket) + unix_exit(); + + if (enabled.media) + btd_unregister_adapter_driver(&media_server_driver); + + if (enabled.headset) + btd_unregister_adapter_driver(&headset_server_driver); + + if (enabled.gateway) + btd_unregister_adapter_driver(&gateway_server_driver); + + if (enabled.source || enabled.sink) + btd_unregister_adapter_driver(&a2dp_server_driver); + + if (enabled.control) + btd_unregister_adapter_driver(&avrcp_server_driver); + + btd_unregister_device_driver(&audio_driver); +} + +struct audio_device *manager_find_device(const char *path, + const bdaddr_t *src, + const bdaddr_t *dst, + const char *interface, + gboolean connected) +{ + GSList *l; + + for (l = devices; l != NULL; l = l->next) { + struct audio_device *dev = l->data; + + if ((path && (strcmp(path, "")) && strcmp(dev->path, path))) + continue; + + if ((src && bacmp(src, BDADDR_ANY)) && bacmp(&dev->src, src)) + continue; + + if ((dst && bacmp(dst, BDADDR_ANY)) && bacmp(&dev->dst, dst)) + continue; + + if (interface && !strcmp(AUDIO_HEADSET_INTERFACE, interface) + && !dev->headset) + continue; + + if (interface && !strcmp(AUDIO_GATEWAY_INTERFACE, interface) + && !dev->gateway) + continue; + + if (interface && !strcmp(AUDIO_SINK_INTERFACE, interface) + && !dev->sink) + continue; + + if (interface && !strcmp(AUDIO_SOURCE_INTERFACE, interface) + && !dev->source) + continue; + + if (interface && !strcmp(AUDIO_CONTROL_INTERFACE, interface) + && !dev->control) + continue; + + if (connected && !audio_device_is_active(dev, interface)) + continue; + + return dev; + } + + return NULL; +} + +struct audio_device *manager_get_device(const bdaddr_t *src, + const bdaddr_t *dst, + gboolean create) +{ + struct audio_device *dev; + struct btd_adapter *adapter; + struct btd_device *device; + char addr[18]; + const char *path; + + dev = manager_find_device(NULL, src, dst, NULL, FALSE); + if (dev) + return dev; + + if (!create) + return NULL; + + ba2str(src, addr); + + adapter = manager_find_adapter(src); + if (!adapter) { + error("Unable to get a btd_adapter object for %s", + addr); + return NULL; + } + + ba2str(dst, addr); + + device = adapter_get_device(connection, adapter, addr); + if (!device) { + error("Unable to get btd_device object for %s", addr); + return NULL; + } + + path = device_get_path(device); + + dev = audio_device_register(connection, device, path, src, dst); + if (!dev) + return NULL; + + devices = g_slist_append(devices, dev); + + return dev; +} + +gboolean manager_allow_headset_connection(struct audio_device *device) +{ + GSList *l; + int connected = 0; + + for (l = devices; l != NULL; l = l->next) { + struct audio_device *dev = l->data; + struct headset *hs = dev->headset; + + if (dev == device) + continue; + + if (bacmp(&dev->src, &device->src)) + continue; + + if (!hs) + continue; + + if (headset_get_state(dev) > HEADSET_STATE_DISCONNECTED) + connected++; + + if (connected >= max_connected_headsets) + return FALSE; + } + + return TRUE; +} + +void manager_set_fast_connectable(gboolean enable) +{ + GSList *l; + + for (l = adapters; l != NULL; l = l->next) { + struct audio_adapter *adapter = l->data; + + if (btd_adapter_set_fast_connectable(adapter->btd_adapter, + enable)) + error("Changing fast connectable for hci%d failed", + adapter_get_dev_id(adapter->btd_adapter)); + } +} diff --git a/audio/manager.h b/audio/manager.h new file mode 100644 index 0000000..0bf7663 --- /dev/null +++ b/audio/manager.h @@ -0,0 +1,56 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct enabled_interfaces { + gboolean hfp; + gboolean headset; + gboolean gateway; + gboolean sink; + gboolean source; + gboolean control; + gboolean socket; + gboolean media; +}; + +int audio_manager_init(DBusConnection *conn, GKeyFile *config, + gboolean *enable_sco); +void audio_manager_exit(void); + +gboolean server_is_enabled(bdaddr_t *src, uint16_t svc); + +struct audio_device *manager_find_device(const char *path, + const bdaddr_t *src, + const bdaddr_t *dst, + const char *interface, + gboolean connected); + +struct audio_device *manager_get_device(const bdaddr_t *src, + const bdaddr_t *dst, + gboolean create); + +gboolean manager_allow_headset_connection(struct audio_device *device); + +/* TRUE to enable fast connectable and FALSE to disable fast connectable for all + * audio adapters. */ +void manager_set_fast_connectable(gboolean enable); diff --git a/audio/media.c b/audio/media.c new file mode 100644 index 0000000..0efb0a8 --- /dev/null +++ b/audio/media.c @@ -0,0 +1,714 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <errno.h> + +#include <glib.h> +#include <gdbus.h> + +#include "../src/adapter.h" +#include "../src/dbus-common.h" + +#include "log.h" +#include "error.h" +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "transport.h" +#include "a2dp.h" +#include "headset.h" +#include "manager.h" + +#ifndef DBUS_TYPE_UNIX_FD +#define DBUS_TYPE_UNIX_FD -1 +#endif + +#define MEDIA_INTERFACE "org.bluez.Media" +#define MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint" + +#define REQUEST_TIMEOUT (3 * 1000) /* 3 seconds */ + +struct media_adapter { + bdaddr_t src; /* Adapter address */ + char *path; /* Adapter path */ + DBusConnection *conn; /* Adapter connection */ + GSList *endpoints; /* Endpoints list */ +}; + +struct endpoint_request { + DBusMessage *msg; + DBusPendingCall *call; + media_endpoint_cb_t cb; + void *user_data; +}; + +struct media_endpoint { + struct a2dp_sep *sep; + char *sender; /* Endpoint DBus bus id */ + char *path; /* Endpoint object path */ + char *uuid; /* Endpoint property UUID */ + uint8_t codec; /* Endpoint codec */ + uint8_t *capabilities; /* Endpoint property capabilities */ + size_t size; /* Endpoint capabilities size */ + guint hs_watch; + guint watch; + struct endpoint_request *request; + struct media_transport *transport; + struct media_adapter *adapter; +}; + +static GSList *adapters = NULL; + +static void endpoint_request_free(struct endpoint_request *request) +{ + if (request->call) + dbus_pending_call_unref(request->call); + + dbus_message_unref(request->msg); + g_free(request); +} + +static void media_endpoint_cancel(struct media_endpoint *endpoint) +{ + struct endpoint_request *request = endpoint->request; + + if (request->call) + dbus_pending_call_cancel(request->call); + + endpoint_request_free(request); + endpoint->request = NULL; +} + +static void media_endpoint_remove(struct media_endpoint *endpoint) +{ + struct media_adapter *adapter = endpoint->adapter; + + if (g_slist_find(adapter->endpoints, endpoint) == NULL) + return; + + info("Endpoint unregistered: sender=%s path=%s", endpoint->sender, + endpoint->path); + + adapter->endpoints = g_slist_remove(adapter->endpoints, endpoint); + + if (endpoint->sep) + a2dp_remove_sep(endpoint->sep); + + if (endpoint->hs_watch) + headset_remove_state_cb(endpoint->hs_watch); + + if (endpoint->request) + media_endpoint_cancel(endpoint); + + if (endpoint->transport) + media_transport_destroy(endpoint->transport); + + g_dbus_remove_watch(adapter->conn, endpoint->watch); + g_free(endpoint->capabilities); + g_free(endpoint->sender); + g_free(endpoint->path); + g_free(endpoint->uuid); + g_free(endpoint); +} + +static void media_endpoint_exit(DBusConnection *connection, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + endpoint->watch = 0; + media_endpoint_remove(endpoint); +} + +static void headset_setconf_cb(struct media_endpoint *endpoint, void *ret, + int size, void *user_data) +{ + struct audio_device *dev = user_data; + + if (ret != NULL) + return; + + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +static void headset_state_changed(struct audio_device *dev, + headset_state_t old_state, + headset_state_t new_state, + void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + DBG(""); + + switch (new_state) { + case HEADSET_STATE_DISCONNECTED: + media_endpoint_clear_configuration(endpoint); + break; + case HEADSET_STATE_CONNECTING: + media_endpoint_set_configuration(endpoint, dev, NULL, 0, + headset_setconf_cb, dev); + break; + case HEADSET_STATE_CONNECTED: + break; + case HEADSET_STATE_PLAY_IN_PROGRESS: + break; + case HEADSET_STATE_PLAYING: + break; + } +} + +static struct media_endpoint *media_endpoint_create(struct media_adapter *adapter, + const char *sender, + const char *path, + const char *uuid, + gboolean delay_reporting, + uint8_t codec, + uint8_t *capabilities, + int size, + int *err) +{ + struct media_endpoint *endpoint; + + endpoint = g_new0(struct media_endpoint, 1); + endpoint->sender = g_strdup(sender); + endpoint->path = g_strdup(path); + endpoint->uuid = g_strdup(uuid); + endpoint->codec = codec; + + if (size > 0) { + endpoint->capabilities = g_new(uint8_t, size); + memcpy(endpoint->capabilities, capabilities, size); + endpoint->size = size; + } + + endpoint->adapter = adapter; + + if (strcasecmp(uuid, A2DP_SOURCE_UUID) == 0) { + endpoint->sep = a2dp_add_sep(&adapter->src, + AVDTP_SEP_TYPE_SOURCE, codec, + delay_reporting, endpoint, err); + if (endpoint->sep == NULL) + goto failed; + } else if (strcasecmp(uuid, A2DP_SINK_UUID) == 0) { + endpoint->sep = a2dp_add_sep(&adapter->src, + AVDTP_SEP_TYPE_SINK, codec, + delay_reporting, endpoint, err); + if (endpoint->sep == NULL) + goto failed; + } else if (strcasecmp(uuid, HFP_AG_UUID) == 0 || + g_strcmp0(uuid, HSP_AG_UUID) == 0) { + struct audio_device *dev; + + endpoint->hs_watch = headset_add_state_cb(headset_state_changed, + endpoint); + dev = manager_find_device(NULL, &adapter->src, BDADDR_ANY, + AUDIO_HEADSET_INTERFACE, TRUE); + if (dev) + media_endpoint_set_configuration(endpoint, dev, NULL, + 0, headset_setconf_cb, + dev); + } else { + if (err) + *err = -EINVAL; + goto failed; + } + + endpoint->watch = g_dbus_add_disconnect_watch(adapter->conn, sender, + media_endpoint_exit, endpoint, + NULL); + + adapter->endpoints = g_slist_append(adapter->endpoints, endpoint); + info("Endpoint registered: sender=%s path=%s", sender, path); + + if (err) + *err = 0; + return endpoint; + +failed: + g_free(endpoint); + return NULL; +} + +static struct media_endpoint *media_adapter_find_endpoint( + struct media_adapter *adapter, + const char *sender, + const char *path, + const char *uuid) +{ + GSList *l; + + for (l = adapter->endpoints; l; l = l->next) { + struct media_endpoint *endpoint = l->data; + + if (sender && g_strcmp0(endpoint->sender, sender) != 0) + continue; + + if (path && g_strcmp0(endpoint->path, path) != 0) + continue; + + if (uuid && g_strcmp0(endpoint->uuid, uuid) != 0) + continue; + + return endpoint; + } + + return NULL; +} + +const char *media_endpoint_get_sender(struct media_endpoint *endpoint) +{ + return endpoint->sender; +} + +static int parse_properties(DBusMessageIter *props, const char **uuid, + gboolean *delay_reporting, uint8_t *codec, + uint8_t **capabilities, int *size) +{ + gboolean has_uuid = FALSE; + gboolean has_codec = FALSE; + + 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 (strcasecmp(key, "UUID") == 0) { + if (var != DBUS_TYPE_STRING) + return -EINVAL; + dbus_message_iter_get_basic(&value, uuid); + has_uuid = TRUE; + } else if (strcasecmp(key, "Codec") == 0) { + if (var != DBUS_TYPE_BYTE) + return -EINVAL; + dbus_message_iter_get_basic(&value, codec); + has_codec = TRUE; + } else if (strcasecmp(key, "DelayReporting") == 0) { + if (var != DBUS_TYPE_BOOLEAN) + return -EINVAL; + dbus_message_iter_get_basic(&value, delay_reporting); + } else if (strcasecmp(key, "Capabilities") == 0) { + DBusMessageIter array; + + if (var != DBUS_TYPE_ARRAY) + return -EINVAL; + + dbus_message_iter_recurse(&value, &array); + dbus_message_iter_get_fixed_array(&array, capabilities, + size); + } + + dbus_message_iter_next(props); + } + + return (has_uuid && has_codec) ? 0 : -EINVAL; +} + +static DBusMessage *register_endpoint(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_adapter *adapter = data; + DBusMessageIter args, props; + const char *sender, *path, *uuid; + gboolean delay_reporting = FALSE; + uint8_t codec; + uint8_t *capabilities; + int size = 0; + int err; + + sender = dbus_message_get_sender(msg); + + dbus_message_iter_init(msg, &args); + + dbus_message_iter_get_basic(&args, &path); + dbus_message_iter_next(&args); + + if (media_adapter_find_endpoint(adapter, sender, path, NULL) != NULL) + return btd_error_already_exists(msg); + + dbus_message_iter_recurse(&args, &props); + if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY) + return btd_error_invalid_args(msg); + + if (parse_properties(&props, &uuid, &delay_reporting, &codec, + &capabilities, &size) < 0) + return btd_error_invalid_args(msg); + + if (media_endpoint_create(adapter, sender, path, uuid, delay_reporting, + codec, capabilities, size, &err) == FALSE) { + if (err == -EPROTONOSUPPORT) + return btd_error_not_supported(msg); + else + return btd_error_invalid_args(msg); + } + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *unregister_endpoint(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_adapter *adapter = data; + struct media_endpoint *endpoint; + const char *sender, *path; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + endpoint = media_adapter_find_endpoint(adapter, sender, path, NULL); + if (endpoint == NULL) + return btd_error_does_not_exist(msg); + + media_endpoint_remove(endpoint); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static GDBusMethodTable media_methods[] = { + { "RegisterEndpoint", "oa{sv}", "", register_endpoint }, + { "UnregisterEndpoint", "o", "", unregister_endpoint }, + { }, +}; + +static void path_free(void *data) +{ + struct media_adapter *adapter = data; + + g_slist_foreach(adapter->endpoints, (GFunc) media_endpoint_release, + NULL); + g_slist_free(adapter->endpoints); + + dbus_connection_unref(adapter->conn); + + adapters = g_slist_remove(adapters, adapter); + + g_free(adapter->path); + g_free(adapter); +} + +int media_register(DBusConnection *conn, const char *path, const bdaddr_t *src) +{ + struct media_adapter *adapter; + + if (DBUS_TYPE_UNIX_FD < 0) + return -EPERM; + + adapter = g_new0(struct media_adapter, 1); + adapter->conn = dbus_connection_ref(conn); + bacpy(&adapter->src, src); + adapter->path = g_strdup(path); + + if (!g_dbus_register_interface(conn, path, MEDIA_INTERFACE, + media_methods, NULL, NULL, + adapter, path_free)) { + error("D-Bus failed to register %s path", path); + path_free(adapter); + return -1; + } + + adapters = g_slist_append(adapters, adapter); + + return 0; +} + +void media_unregister(const char *path) +{ + GSList *l; + + for (l = adapters; l; l = l->next) { + struct media_adapter *adapter = l->data; + + if (g_strcmp0(path, adapter->path) == 0) { + g_dbus_unregister_interface(adapter->conn, path, + MEDIA_INTERFACE); + return; + } + } +} + +size_t media_endpoint_get_capabilities(struct media_endpoint *endpoint, + uint8_t **capabilities) +{ + *capabilities = endpoint->capabilities; + return endpoint->size; +} + +static void endpoint_reply(DBusPendingCall *call, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + struct endpoint_request *request = endpoint->request; + DBusMessage *reply; + DBusError err; + gboolean value; + void *ret = NULL; + int size = -1; + + /* steal_reply will always return non-NULL since the callback + * is only called after a reply has been received */ + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("Endpoint replied with an error: %s", + err.name); + + /* Clear endpoint configuration in case of NO_REPLY error */ + if (dbus_error_has_name(&err, DBUS_ERROR_NO_REPLY)) { + if (request->cb) + request->cb(endpoint, NULL, size, + request->user_data); + media_endpoint_clear_configuration(endpoint); + dbus_message_unref(reply); + dbus_error_free(&err); + return; + } + + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (dbus_message_is_method_call(request->msg, MEDIA_ENDPOINT_INTERFACE, + "SelectConfiguration")) { + DBusMessageIter args, array; + uint8_t *configuration; + + dbus_message_iter_init(reply, &args); + + dbus_message_iter_recurse(&args, &array); + + dbus_message_iter_get_fixed_array(&array, &configuration, &size); + + ret = configuration; + goto done; + } else if (!dbus_message_get_args(reply, &err, DBUS_TYPE_INVALID)) { + error("Wrong reply signature: %s", err.message); + dbus_error_free(&err); + goto done; + } + + size = 1; + value = TRUE; + ret = &value; + +done: + dbus_message_unref(reply); + + if (request->cb) + request->cb(endpoint, ret, size, request->user_data); + + endpoint_request_free(request); + endpoint->request = NULL; +} + +static gboolean media_endpoint_async_call(DBusConnection *conn, + DBusMessage *msg, + struct media_endpoint *endpoint, + media_endpoint_cb_t cb, + void *user_data) +{ + struct endpoint_request *request; + + if (endpoint->request) + return FALSE; + + request = g_new0(struct endpoint_request, 1); + + /* Timeout should be less than avdtp request timeout (4 seconds) */ + if (dbus_connection_send_with_reply(conn, msg, &request->call, + REQUEST_TIMEOUT) == FALSE) { + error("D-Bus send failed"); + g_free(request); + return FALSE; + } + + dbus_pending_call_set_notify(request->call, endpoint_reply, endpoint, NULL); + + request->msg = msg; + request->cb = cb; + request->user_data = user_data; + endpoint->request = request; + + DBG("Calling %s: name = %s path = %s", dbus_message_get_member(msg), + dbus_message_get_destination(msg), + dbus_message_get_path(msg)); + + return TRUE; +} + +gboolean media_endpoint_set_configuration(struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, size_t size, + media_endpoint_cb_t cb, + void *user_data) +{ + DBusConnection *conn; + DBusMessage *msg; + const char *path; + DBusMessageIter iter; + + if (endpoint->transport != NULL || endpoint->request != NULL) + return FALSE; + + conn = endpoint->adapter->conn; + + endpoint->transport = media_transport_create(conn, endpoint, device, + configuration, size); + if (endpoint->transport == NULL) + return FALSE; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "SetConfiguration"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return FALSE; + } + + dbus_message_iter_init_append(msg, &iter); + + path = media_transport_get_path(endpoint->transport); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); + + transport_get_properties(endpoint->transport, &iter); + + return media_endpoint_async_call(conn, msg, endpoint, cb, user_data); +} + +gboolean media_endpoint_select_configuration(struct media_endpoint *endpoint, + uint8_t *capabilities, + size_t length, + media_endpoint_cb_t cb, + void *user_data) +{ + DBusConnection *conn; + DBusMessage *msg; + + if (endpoint->request != NULL) + return FALSE; + + conn = endpoint->adapter->conn; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "SelectConfiguration"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return FALSE; + } + + dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, + &capabilities, length, + DBUS_TYPE_INVALID); + + return media_endpoint_async_call(conn, msg, endpoint, cb, user_data); +} + +void media_endpoint_clear_configuration(struct media_endpoint *endpoint) +{ + DBusConnection *conn; + DBusMessage *msg; + const char *path; + + if (endpoint->transport == NULL) + return; + + if (endpoint->request) + media_endpoint_cancel(endpoint); + + conn = endpoint->adapter->conn; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "ClearConfiguration"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + goto done; + } + + path = media_transport_get_path(endpoint->transport); + dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + g_dbus_send_message(conn, msg); +done: + media_transport_destroy(endpoint->transport); + endpoint->transport = NULL; +} + +void media_endpoint_release(struct media_endpoint *endpoint) +{ + DBusMessage *msg; + + DBG("sender=%s path=%s", endpoint->sender, endpoint->path); + + /* already exit */ + if (endpoint->watch == 0) + return; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "Release"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return; + } + + g_dbus_send_message(endpoint->adapter->conn, msg); + + media_endpoint_remove(endpoint); +} + +struct a2dp_sep *media_endpoint_get_sep(struct media_endpoint *endpoint) +{ + return endpoint->sep; +} + +const char *media_endpoint_get_uuid(struct media_endpoint *endpoint) +{ + return endpoint->uuid; +} + +uint8_t media_endpoint_get_codec(struct media_endpoint *endpoint) +{ + return endpoint->codec; +} + +struct media_transport *media_endpoint_get_transport( + struct media_endpoint *endpoint) +{ + return endpoint->transport; +} diff --git a/audio/media.h b/audio/media.h new file mode 100644 index 0000000..d089103 --- /dev/null +++ b/audio/media.h @@ -0,0 +1,54 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct media_endpoint; + +typedef void (*media_endpoint_cb_t) (struct media_endpoint *endpoint, + void *ret, int size, void *user_data); + +int media_register(DBusConnection *conn, const char *path, const bdaddr_t *src); +void media_unregister(const char *path); + +const char *media_endpoint_get_sender(struct media_endpoint *endpoint); + +size_t media_endpoint_get_capabilities(struct media_endpoint *endpoint, + uint8_t **capabilities); +gboolean media_endpoint_set_configuration(struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, size_t size, + media_endpoint_cb_t cb, + void *user_data); +gboolean media_endpoint_select_configuration(struct media_endpoint *endpoint, + uint8_t *capabilities, + size_t length, + media_endpoint_cb_t cb, + void *user_data); +void media_endpoint_clear_configuration(struct media_endpoint *endpoint); +void media_endpoint_release(struct media_endpoint *endpoint); + +struct a2dp_sep *media_endpoint_get_sep(struct media_endpoint *endpoint); +const char *media_endpoint_get_uuid(struct media_endpoint *endpoint); +uint8_t media_endpoint_get_codec(struct media_endpoint *endpoint); +struct media_transport *media_endpoint_get_transport( + struct media_endpoint *endpoint); diff --git a/audio/pcm_bluetooth.c b/audio/pcm_bluetooth.c new file mode 100644 index 0000000..799f17f --- /dev/null +++ b/audio/pcm_bluetooth.c @@ -0,0 +1,1784 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdint.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <time.h> +#include <sys/time.h> +#include <pthread.h> +#include <signal.h> +#include <limits.h> + +#include <netinet/in.h> + +#include <alsa/asoundlib.h> +#include <alsa/pcm_external.h> + +#include "ipc.h" +#include "sbc.h" +#include "rtp.h" + +//#define ENABLE_DEBUG + +#define UINT_SECS_MAX (UINT_MAX / 1000000 - 1) + +#define MIN_PERIOD_TIME 1 + +#define BUFFER_SIZE 2048 + +#ifdef ENABLE_DEBUG +#define DBG(fmt, arg...) printf("DEBUG: %s: " fmt "\n" , __FUNCTION__ , ## arg) +#else +#define DBG(fmt, arg...) +#endif + +#ifndef SOL_SCO +#define SOL_SCO 17 +#endif + +#ifndef SCO_TXBUFS +#define SCO_TXBUFS 0x03 +#endif + +#ifndef SCO_RXBUFS +#define SCO_RXBUFS 0x04 +#endif + +#ifndef MIN +# define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif + +#ifndef MAX +# define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +#define MAX_BITPOOL 64 +#define MIN_BITPOOL 2 + +/* adapted from glibc sys/time.h timersub() macro */ +#define priv_timespecsub(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \ + if ((result)->tv_nsec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_nsec += 1000000000; \ + } \ + } while (0) + +struct bluetooth_a2dp { + sbc_capabilities_t sbc_capabilities; + sbc_t sbc; /* Codec data */ + int sbc_initialized; /* Keep track if the encoder is initialized */ + unsigned int codesize; /* SBC codesize */ + int samples; /* Number of encoded samples */ + uint8_t buffer[BUFFER_SIZE]; /* Codec transfer buffer */ + unsigned int count; /* Codec transfer buffer counter */ + + int nsamples; /* Cumulative number of codec samples */ + uint16_t seq_num; /* Cumulative packet sequence */ + int frame_count; /* Current frames in buffer*/ +}; + +struct bluetooth_alsa_config { + char device[18]; /* Address of the remote Device */ + int has_device; + uint8_t transport; /* Requested transport */ + int has_transport; + uint16_t rate; + int has_rate; + uint8_t channel_mode; /* A2DP only */ + int has_channel_mode; + uint8_t allocation_method; /* A2DP only */ + int has_allocation_method; + uint8_t subbands; /* A2DP only */ + int has_subbands; + uint8_t block_length; /* A2DP only */ + int has_block_length; + uint8_t bitpool; /* A2DP only */ + int has_bitpool; + int autoconnect; +}; + +struct bluetooth_data { + snd_pcm_ioplug_t io; + struct bluetooth_alsa_config alsa_config; /* ALSA resource file parameters */ + volatile snd_pcm_sframes_t hw_ptr; + int transport; /* chosen transport SCO or AD2P */ + unsigned int link_mtu; /* MTU for selected transport channel */ + volatile struct pollfd stream; /* Audio stream filedescriptor */ + struct pollfd server; /* Audio daemon filedescriptor */ + uint8_t buffer[BUFFER_SIZE]; /* Encoded transfer buffer */ + unsigned int count; /* Transfer buffer counter */ + struct bluetooth_a2dp a2dp; /* A2DP data */ + + pthread_t hw_thread; /* Makes virtual hw pointer move */ + int pipefd[2]; /* Inter thread communication */ + int stopped; + sig_atomic_t reset; /* Request XRUN handling */ +}; + +static int audioservice_send(int sk, const bt_audio_msg_header_t *msg); +static int audioservice_expect(int sk, bt_audio_msg_header_t *outmsg, + int expected_type); + +static int bluetooth_start(snd_pcm_ioplug_t *io) +{ + DBG("bluetooth_start %p", io); + + return 0; +} + +static int bluetooth_stop(snd_pcm_ioplug_t *io) +{ + DBG("bluetooth_stop %p", io); + + return 0; +} + +static void *playback_hw_thread(void *param) +{ + struct bluetooth_data *data = param; + unsigned int prev_periods; + double period_time; + struct timespec start; + struct pollfd fds[2]; + int poll_timeout; + + data->server.events = POLLIN; + /* note: only errors for data->stream.events */ + + fds[0] = data->server; + fds[1] = data->stream; + + prev_periods = 0; + period_time = 1000000.0 * data->io.period_size / data->io.rate; + if (period_time > (int) (MIN_PERIOD_TIME * 1000)) + poll_timeout = (int) (period_time / 1000.0f); + else + poll_timeout = MIN_PERIOD_TIME; + + clock_gettime(CLOCK_MONOTONIC, &start); + + while (1) { + unsigned int dtime, periods; + struct timespec cur, delta; + int ret; + + if (data->stopped) + goto iter_sleep; + + if (data->reset) { + DBG("Handle XRUN in hw-thread."); + data->reset = 0; + clock_gettime(CLOCK_MONOTONIC, &start); + prev_periods = 0; + } + + clock_gettime(CLOCK_MONOTONIC, &cur); + + priv_timespecsub(&cur, &start, &delta); + + dtime = delta.tv_sec * 1000000 + delta.tv_nsec / 1000; + periods = 1.0 * dtime / period_time; + + if (periods > prev_periods) { + char c = 'w'; + int frags = periods - prev_periods, n; + + data->hw_ptr += frags * data->io.period_size; + data->hw_ptr %= data->io.buffer_size; + + for (n = 0; n < frags; n++) { + /* Notify user that hardware pointer + * has moved * */ + if (write(data->pipefd[1], &c, 1) < 0) + pthread_testcancel(); + } + + /* Reset point of reference to avoid too big values + * that wont fit an unsigned int */ + if ((unsigned int) delta.tv_sec < UINT_SECS_MAX) + prev_periods = periods; + else { + prev_periods = 0; + clock_gettime(CLOCK_MONOTONIC, &start); + } + } + +iter_sleep: + /* sleep up to one period interval */ + ret = poll(fds, 2, poll_timeout); + + if (ret < 0) { + if (errno != EINTR) { + SNDERR("poll error: %s (%d)", strerror(errno), + errno); + break; + } + } else if (ret > 0) { + ret = (fds[0].revents) ? 0 : 1; + SNDERR("poll fd %d revents %d", ret, fds[ret].revents); + if (fds[ret].revents & (POLLERR | POLLHUP | POLLNVAL)) + break; + } + + /* Offer opportunity to be canceled by main thread */ + pthread_testcancel(); + } + + data->hw_thread = 0; + pthread_exit(NULL); +} + +static int bluetooth_playback_start(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + int err; + + DBG("%p", io); + + data->stopped = 0; + + if (data->hw_thread) + return 0; + + err = pthread_create(&data->hw_thread, 0, playback_hw_thread, data); + + return -err; +} + +static int bluetooth_playback_stop(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + + DBG("%p", io); + + data->stopped = 1; + + return 0; +} + +static snd_pcm_sframes_t bluetooth_pointer(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + + return data->hw_ptr; +} + +static void bluetooth_exit(struct bluetooth_data *data) +{ + struct bluetooth_a2dp *a2dp = &data->a2dp; + + if (data->server.fd >= 0) + bt_audio_service_close(data->server.fd); + + if (data->stream.fd >= 0) + close(data->stream.fd); + + if (data->hw_thread) { + pthread_cancel(data->hw_thread); + pthread_join(data->hw_thread, 0); + } + + if (a2dp->sbc_initialized) + sbc_finish(&a2dp->sbc); + + if (data->pipefd[0] > 0) + close(data->pipefd[0]); + + if (data->pipefd[1] > 0) + close(data->pipefd[1]); + + free(data); +} + +static int bluetooth_close(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + + DBG("%p", io); + + bluetooth_exit(data); + + return 0; +} + +static int bluetooth_prepare(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + char c = 'w'; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_req *req = (void *) buf; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + uint32_t period_count = io->buffer_size / io->period_size; + int opt_name, err; + struct timeval t = { 0, period_count }; + + DBG("Preparing with io->period_size=%lu io->buffer_size=%lu", + io->period_size, io->buffer_size); + + data->reset = 0; + + /* As we're gonna receive messages on the server socket, we have to stop the + hw thread that is polling on it, if any */ + if (data->hw_thread) { + pthread_cancel(data->hw_thread); + pthread_join(data->hw_thread, 0); + data->hw_thread = 0; + } + + if (io->stream == SND_PCM_STREAM_PLAYBACK) + /* If not null for playback, xmms doesn't display time + * correctly */ + data->hw_ptr = 0; + else + /* ALSA library is really picky on the fact hw_ptr is not null. + * If it is, capture won't start */ + data->hw_ptr = io->period_size; + + /* send start */ + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_START_STREAM; + req->h.length = sizeof(*req); + + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + return err; + + rsp->h.length = sizeof(*rsp); + err = audioservice_expect(data->server.fd, &rsp->h, + BT_START_STREAM); + if (err < 0) + return err; + + ind->h.length = sizeof(*ind); + err = audioservice_expect(data->server.fd, &ind->h, + BT_NEW_STREAM); + if (err < 0) + return err; + + if (data->stream.fd >= 0) + close(data->stream.fd); + + data->stream.fd = bt_audio_service_get_data_fd(data->server.fd); + if (data->stream.fd < 0) { + return -errno; + } + + if (data->transport == BT_CAPABILITIES_TRANSPORT_A2DP) { + opt_name = (io->stream == SND_PCM_STREAM_PLAYBACK) ? + SO_SNDTIMEO : SO_RCVTIMEO; + + if (setsockopt(data->stream.fd, SOL_SOCKET, opt_name, &t, + sizeof(t)) < 0) + return -errno; + } else { + opt_name = (io->stream == SND_PCM_STREAM_PLAYBACK) ? + SCO_TXBUFS : SCO_RXBUFS; + + if (setsockopt(data->stream.fd, SOL_SCO, opt_name, &period_count, + sizeof(period_count)) == 0) + return 0; + + opt_name = (io->stream == SND_PCM_STREAM_PLAYBACK) ? + SO_SNDBUF : SO_RCVBUF; + + if (setsockopt(data->stream.fd, SOL_SCO, opt_name, &period_count, + sizeof(period_count)) == 0) + return 0; + + /* FIXME : handle error codes */ + } + + /* wake up any client polling at us */ + err = write(data->pipefd[1], &c, 1); + if (err < 0) + return err; + + return 0; +} + +static int bluetooth_hsp_hw_params(snd_pcm_ioplug_t *io, + snd_pcm_hw_params_t *params) +{ + struct bluetooth_data *data = io->private_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_req *open_req = (void *) buf; + struct bt_open_rsp *open_rsp = (void *) buf; + struct bt_set_configuration_req *req = (void *) buf; + struct bt_set_configuration_rsp *rsp = (void *) buf; + int err; + + DBG("Preparing with io->period_size=%lu io->buffer_size=%lu", + io->period_size, io->buffer_size); + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + open_req->h.type = BT_REQUEST; + open_req->h.name = BT_OPEN; + open_req->h.length = sizeof(*open_req); + + strncpy(open_req->destination, data->alsa_config.device, 18); + open_req->seid = BT_A2DP_SEID_RANGE + 1; + open_req->lock = (io->stream == SND_PCM_STREAM_PLAYBACK ? + BT_WRITE_LOCK : BT_READ_LOCK); + + err = audioservice_send(data->server.fd, &open_req->h); + if (err < 0) + return err; + + open_rsp->h.length = sizeof(*open_rsp); + err = audioservice_expect(data->server.fd, &open_rsp->h, + BT_OPEN); + if (err < 0) + return err; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_SET_CONFIGURATION; + req->h.length = sizeof(*req); + + req->codec.transport = BT_CAPABILITIES_TRANSPORT_SCO; + req->codec.seid = BT_A2DP_SEID_RANGE + 1; + req->codec.length = sizeof(pcm_capabilities_t); + + req->h.length += req->codec.length - sizeof(req->codec); + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + return err; + + rsp->h.length = sizeof(*rsp); + err = audioservice_expect(data->server.fd, &rsp->h, + BT_SET_CONFIGURATION); + if (err < 0) + return err; + + data->transport = BT_CAPABILITIES_TRANSPORT_SCO; + data->link_mtu = rsp->link_mtu; + + return 0; +} + +static uint8_t default_bitpool(uint8_t freq, uint8_t mode) +{ + switch (freq) { + case BT_SBC_SAMPLING_FREQ_16000: + case BT_SBC_SAMPLING_FREQ_32000: + return 53; + case BT_SBC_SAMPLING_FREQ_44100: + switch (mode) { + case BT_A2DP_CHANNEL_MODE_MONO: + case BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL: + return 31; + case BT_A2DP_CHANNEL_MODE_STEREO: + case BT_A2DP_CHANNEL_MODE_JOINT_STEREO: + return 53; + default: + DBG("Invalid channel mode %u", mode); + return 53; + } + case BT_SBC_SAMPLING_FREQ_48000: + switch (mode) { + case BT_A2DP_CHANNEL_MODE_MONO: + case BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL: + return 29; + case BT_A2DP_CHANNEL_MODE_STEREO: + case BT_A2DP_CHANNEL_MODE_JOINT_STEREO: + return 51; + default: + DBG("Invalid channel mode %u", mode); + return 51; + } + default: + DBG("Invalid sampling freq %u", freq); + return 53; + } +} + +static int bluetooth_a2dp_init(struct bluetooth_data *data, + snd_pcm_hw_params_t *params) +{ + struct bluetooth_alsa_config *cfg = &data->alsa_config; + sbc_capabilities_t *cap = &data->a2dp.sbc_capabilities; + unsigned int max_bitpool, min_bitpool, rate, channels; + int dir; + + snd_pcm_hw_params_get_rate(params, &rate, &dir); + snd_pcm_hw_params_get_channels(params, &channels); + + switch (rate) { + case 48000: + cap->frequency = BT_SBC_SAMPLING_FREQ_48000; + break; + case 44100: + cap->frequency = BT_SBC_SAMPLING_FREQ_44100; + break; + case 32000: + cap->frequency = BT_SBC_SAMPLING_FREQ_32000; + break; + case 16000: + cap->frequency = BT_SBC_SAMPLING_FREQ_16000; + break; + default: + DBG("Rate %d not supported", rate); + return -1; + } + + if (cfg->has_channel_mode) + cap->channel_mode = cfg->channel_mode; + else if (channels == 2) { + if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + else if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_STEREO; + else if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + } else { + if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_MONO; + } + + if (!cap->channel_mode) { + DBG("No supported channel modes"); + return -1; + } + + if (cfg->has_block_length) + cap->block_length = cfg->block_length; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_16) + cap->block_length = BT_A2DP_BLOCK_LENGTH_16; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_12) + cap->block_length = BT_A2DP_BLOCK_LENGTH_12; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_8) + cap->block_length = BT_A2DP_BLOCK_LENGTH_8; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_4) + cap->block_length = BT_A2DP_BLOCK_LENGTH_4; + else { + DBG("No supported block lengths"); + return -1; + } + + if (cfg->has_subbands) + cap->subbands = cfg->subbands; + if (cap->subbands & BT_A2DP_SUBBANDS_8) + cap->subbands = BT_A2DP_SUBBANDS_8; + else if (cap->subbands & BT_A2DP_SUBBANDS_4) + cap->subbands = BT_A2DP_SUBBANDS_4; + else { + DBG("No supported subbands"); + return -1; + } + + if (cfg->has_allocation_method) + cap->allocation_method = cfg->allocation_method; + if (cap->allocation_method & BT_A2DP_ALLOCATION_LOUDNESS) + cap->allocation_method = BT_A2DP_ALLOCATION_LOUDNESS; + else if (cap->allocation_method & BT_A2DP_ALLOCATION_SNR) + cap->allocation_method = BT_A2DP_ALLOCATION_SNR; + + if (cfg->has_bitpool) + min_bitpool = max_bitpool = cfg->bitpool; + else { + min_bitpool = MAX(MIN_BITPOOL, cap->min_bitpool); + max_bitpool = MIN(default_bitpool(cap->frequency, + cap->channel_mode), + cap->max_bitpool); + } + + cap->min_bitpool = min_bitpool; + cap->max_bitpool = max_bitpool; + + return 0; +} + +static void bluetooth_a2dp_setup(struct bluetooth_a2dp *a2dp) +{ + sbc_capabilities_t active_capabilities = a2dp->sbc_capabilities; + + if (a2dp->sbc_initialized) + sbc_reinit(&a2dp->sbc, 0); + else + sbc_init(&a2dp->sbc, 0); + a2dp->sbc_initialized = 1; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_16000) + a2dp->sbc.frequency = SBC_FREQ_16000; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_32000) + a2dp->sbc.frequency = SBC_FREQ_32000; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_44100) + a2dp->sbc.frequency = SBC_FREQ_44100; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_48000) + a2dp->sbc.frequency = SBC_FREQ_48000; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + a2dp->sbc.mode = SBC_MODE_MONO; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) + a2dp->sbc.mode = SBC_MODE_DUAL_CHANNEL; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) + a2dp->sbc.mode = SBC_MODE_STEREO; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) + a2dp->sbc.mode = SBC_MODE_JOINT_STEREO; + + a2dp->sbc.allocation = active_capabilities.allocation_method + == BT_A2DP_ALLOCATION_SNR ? SBC_AM_SNR + : SBC_AM_LOUDNESS; + + switch (active_capabilities.subbands) { + case BT_A2DP_SUBBANDS_4: + a2dp->sbc.subbands = SBC_SB_4; + break; + case BT_A2DP_SUBBANDS_8: + a2dp->sbc.subbands = SBC_SB_8; + break; + } + + switch (active_capabilities.block_length) { + case BT_A2DP_BLOCK_LENGTH_4: + a2dp->sbc.blocks = SBC_BLK_4; + break; + case BT_A2DP_BLOCK_LENGTH_8: + a2dp->sbc.blocks = SBC_BLK_8; + break; + case BT_A2DP_BLOCK_LENGTH_12: + a2dp->sbc.blocks = SBC_BLK_12; + break; + case BT_A2DP_BLOCK_LENGTH_16: + a2dp->sbc.blocks = SBC_BLK_16; + break; + } + + a2dp->sbc.bitpool = active_capabilities.max_bitpool; + a2dp->codesize = sbc_get_codesize(&a2dp->sbc); + a2dp->count = sizeof(struct rtp_header) + sizeof(struct rtp_payload); +} + +static int bluetooth_a2dp_hw_params(snd_pcm_ioplug_t *io, + snd_pcm_hw_params_t *params) +{ + struct bluetooth_data *data = io->private_data; + struct bluetooth_a2dp *a2dp = &data->a2dp; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_req *open_req = (void *) buf; + struct bt_open_rsp *open_rsp = (void *) buf; + struct bt_set_configuration_req *req = (void *) buf; + struct bt_set_configuration_rsp *rsp = (void *) buf; + int err; + + DBG("Preparing with io->period_size=%lu io->buffer_size=%lu", + io->period_size, io->buffer_size); + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + open_req->h.type = BT_REQUEST; + open_req->h.name = BT_OPEN; + open_req->h.length = sizeof(*open_req); + + strncpy(open_req->destination, data->alsa_config.device, 18); + open_req->seid = a2dp->sbc_capabilities.capability.seid; + open_req->lock = (io->stream == SND_PCM_STREAM_PLAYBACK ? + BT_WRITE_LOCK : BT_READ_LOCK); + + err = audioservice_send(data->server.fd, &open_req->h); + if (err < 0) + return err; + + open_rsp->h.length = sizeof(*open_rsp); + err = audioservice_expect(data->server.fd, &open_rsp->h, + BT_OPEN); + if (err < 0) + return err; + + err = bluetooth_a2dp_init(data, params); + if (err < 0) + return err; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_SET_CONFIGURATION; + req->h.length = sizeof(*req); + + memcpy(&req->codec, &a2dp->sbc_capabilities, + sizeof(a2dp->sbc_capabilities)); + + req->codec.transport = BT_CAPABILITIES_TRANSPORT_A2DP; + req->codec.length = sizeof(a2dp->sbc_capabilities); + req->h.length += req->codec.length - sizeof(req->codec); + + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + return err; + + rsp->h.length = sizeof(*rsp); + err = audioservice_expect(data->server.fd, &rsp->h, + BT_SET_CONFIGURATION); + if (err < 0) + return err; + + data->transport = BT_CAPABILITIES_TRANSPORT_A2DP; + data->link_mtu = rsp->link_mtu; + + /* Setup SBC encoder now we agree on parameters */ + bluetooth_a2dp_setup(a2dp); + + DBG("\tallocation=%u\n\tsubbands=%u\n\tblocks=%u\n\tbitpool=%u\n", + a2dp->sbc.allocation, a2dp->sbc.subbands, a2dp->sbc.blocks, + a2dp->sbc.bitpool); + + return 0; +} + +static int bluetooth_poll_descriptors(snd_pcm_ioplug_t *io, + struct pollfd *pfd, unsigned int space) +{ + struct bluetooth_data *data = io->private_data; + + assert(io); + + if (space < 1) + return 0; + + pfd[0].fd = data->stream.fd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + + return 1; +} + +static int bluetooth_poll_revents(snd_pcm_ioplug_t *io ATTRIBUTE_UNUSED, + struct pollfd *pfds, unsigned int nfds, + unsigned short *revents) +{ + assert(pfds && nfds == 1 && revents); + + *revents = pfds[0].revents; + + return 0; +} + +static int bluetooth_playback_poll_descriptors_count(snd_pcm_ioplug_t *io) +{ + return 2; +} + +static int bluetooth_playback_poll_descriptors(snd_pcm_ioplug_t *io, + struct pollfd *pfd, unsigned int space) +{ + struct bluetooth_data *data = io->private_data; + + DBG(""); + + assert(data->pipefd[0] >= 0); + + if (space < 2) + return 0; + + pfd[0].fd = data->pipefd[0]; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = data->stream.fd; + pfd[1].events = POLLERR | POLLHUP | POLLNVAL; + pfd[1].revents = 0; + + return 2; +} + +static int bluetooth_playback_poll_revents(snd_pcm_ioplug_t *io, + struct pollfd *pfds, unsigned int nfds, + unsigned short *revents) +{ + static char buf[1]; + int ret; + + DBG(""); + + assert(pfds); + assert(nfds == 2); + assert(revents); + assert(pfds[0].fd >= 0); + assert(pfds[1].fd >= 0); + + if (io->state != SND_PCM_STATE_PREPARED) + ret = read(pfds[0].fd, buf, 1); + + if (pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) + io->state = SND_PCM_STATE_DISCONNECTED; + + *revents = (pfds[0].revents & POLLIN) ? POLLOUT : 0; + + return 0; +} + + +static snd_pcm_sframes_t bluetooth_hsp_read(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, + snd_pcm_uframes_t size) +{ + struct bluetooth_data *data = io->private_data; + snd_pcm_uframes_t frames_to_write, ret; + unsigned char *buff; + unsigned int frame_size = 0; + int nrecv; + + DBG("areas->step=%u areas->first=%u offset=%lu size=%lu io->nonblock=%u", + areas->step, areas->first, offset, size, io->nonblock); + + frame_size = areas->step / 8; + + if (data->count > 0) + goto proceed; + + nrecv = recv(data->stream.fd, data->buffer, data->link_mtu, + io->nonblock ? MSG_DONTWAIT : 0); + + if (nrecv < 0) { + ret = (errno == EPIPE) ? -EIO : -errno; + goto done; + } + + if ((unsigned int) nrecv != data->link_mtu) { + ret = -EIO; + SNDERR(strerror(-ret)); + goto done; + } + + /* Increment hardware transmition pointer */ + data->hw_ptr = (data->hw_ptr + data->link_mtu / frame_size) % + io->buffer_size; + +proceed: + buff = (unsigned char *) areas->addr + + (areas->first + areas->step * offset) / 8; + + if ((data->count + size * frame_size) <= data->link_mtu) + frames_to_write = size; + else + frames_to_write = (data->link_mtu - data->count) / frame_size; + + memcpy(buff, data->buffer + data->count, frame_size * frames_to_write); + data->count += (frame_size * frames_to_write); + data->count %= data->link_mtu; + + /* Return written frames count */ + ret = frames_to_write; + +done: + DBG("returning %lu", ret); + return ret; +} + +static snd_pcm_sframes_t bluetooth_hsp_write(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, + snd_pcm_uframes_t size) +{ + struct bluetooth_data *data = io->private_data; + snd_pcm_sframes_t ret = 0; + snd_pcm_uframes_t frames_to_read; + uint8_t *buff; + int rsend, frame_size; + + DBG("areas->step=%u areas->first=%u offset=%lu, size=%lu io->nonblock=%u", + areas->step, areas->first, offset, size, io->nonblock); + + if (io->hw_ptr > io->appl_ptr) { + ret = bluetooth_playback_stop(io); + if (ret == 0) + ret = -EPIPE; + goto done; + } + + frame_size = areas->step / 8; + if ((data->count + size * frame_size) <= data->link_mtu) + frames_to_read = size; + else + frames_to_read = (data->link_mtu - data->count) / frame_size; + + DBG("count=%d frames_to_read=%lu", data->count, frames_to_read); + + /* Ready for more data */ + buff = (uint8_t *) areas->addr + + (areas->first + areas->step * offset) / 8; + memcpy(data->buffer + data->count, buff, frame_size * frames_to_read); + + /* Remember we have some frames in the pipe now */ + data->count += frames_to_read * frame_size; + if (data->count != data->link_mtu) { + ret = frames_to_read; + goto done; + } + + rsend = send(data->stream.fd, data->buffer, data->link_mtu, + io->nonblock ? MSG_DONTWAIT : 0); + if (rsend > 0) { + /* Reset count pointer */ + data->count = 0; + + ret = frames_to_read; + } else if (rsend < 0) + ret = (errno == EPIPE) ? -EIO : -errno; + else + ret = -EIO; + +done: + DBG("returning %ld", ret); + return ret; +} + +static snd_pcm_sframes_t bluetooth_a2dp_read(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, snd_pcm_uframes_t size) +{ + snd_pcm_uframes_t ret = 0; + return ret; +} + +static int avdtp_write(struct bluetooth_data *data) +{ + int ret = 0; + struct rtp_header *header; + struct rtp_payload *payload; + struct bluetooth_a2dp *a2dp = &data->a2dp; + + header = (void *) a2dp->buffer; + payload = (void *) (a2dp->buffer + sizeof(*header)); + + memset(a2dp->buffer, 0, sizeof(*header) + sizeof(*payload)); + + payload->frame_count = a2dp->frame_count; + header->v = 2; + header->pt = 1; + header->sequence_number = htons(a2dp->seq_num); + header->timestamp = htonl(a2dp->nsamples); + header->ssrc = htonl(1); + + ret = send(data->stream.fd, a2dp->buffer, a2dp->count, MSG_DONTWAIT); + if (ret < 0) { + DBG("send returned %d errno %s.", ret, strerror(errno)); + ret = -errno; + } + + /* Reset buffer of data to send */ + a2dp->count = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + a2dp->frame_count = 0; + a2dp->samples = 0; + a2dp->seq_num++; + + return ret; +} + +static snd_pcm_sframes_t bluetooth_a2dp_write(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, snd_pcm_uframes_t size) +{ + struct bluetooth_data *data = io->private_data; + struct bluetooth_a2dp *a2dp = &data->a2dp; + snd_pcm_sframes_t ret = 0; + unsigned int bytes_left; + int frame_size, encoded; + ssize_t written; + uint8_t *buff; + + DBG("areas->step=%u areas->first=%u offset=%lu size=%lu", + areas->step, areas->first, offset, size); + DBG("hw_ptr=%lu appl_ptr=%lu diff=%lu", io->hw_ptr, io->appl_ptr, + io->appl_ptr - io->hw_ptr); + + /* Calutate starting pointers */ + frame_size = areas->step / 8; + bytes_left = size * frame_size; + buff = (uint8_t *) areas->addr + + (areas->first + areas->step * (offset)) / 8; + + /* Check for underrun */ + if (io->hw_ptr > io->appl_ptr) { + ret = bluetooth_playback_stop(io); + if (ret == 0) + ret = -EPIPE; + data->reset = 1; + return ret; + } + + /* Check if we should autostart */ + if (io->state == SND_PCM_STATE_PREPARED) { + snd_pcm_sw_params_t *swparams; + snd_pcm_uframes_t threshold; + + snd_pcm_sw_params_malloc(&swparams); + if (!snd_pcm_sw_params_current(io->pcm, swparams) && + !snd_pcm_sw_params_get_start_threshold(swparams, + &threshold)) { + if (io->appl_ptr >= threshold) { + ret = snd_pcm_start(io->pcm); + if (ret != 0) + return ret; + } + } + + snd_pcm_sw_params_free(swparams); + } + + /* Check if we have any left over data from the last write */ + if (data->count > 0) { + unsigned int additional_bytes_needed = + a2dp->codesize - data->count; + if (additional_bytes_needed > bytes_left) + goto out; + + memcpy(data->buffer + data->count, buff, + additional_bytes_needed); + + /* Enough data to encode (sbc wants 1k blocks) */ + encoded = sbc_encode(&a2dp->sbc, data->buffer, a2dp->codesize, + a2dp->buffer + a2dp->count, + sizeof(a2dp->buffer) - a2dp->count, + &written); + if (encoded <= 0) { + DBG("Encoding error %d", encoded); + goto done; + } + + /* Increment a2dp buffers */ + a2dp->count += written; + a2dp->frame_count++; + a2dp->samples += encoded / frame_size; + a2dp->nsamples += encoded / frame_size; + + /* No space left for another frame then send */ + if (a2dp->count + written >= data->link_mtu) { + avdtp_write(data); + DBG("sending packet %d, count %d, link_mtu %u", + a2dp->seq_num, a2dp->count, + data->link_mtu); + } + + /* Increment up buff pointer to take into account + * the data processed */ + buff += additional_bytes_needed; + bytes_left -= additional_bytes_needed; + + /* Since data has been process mark it as zero */ + data->count = 0; + } + + + /* Process this buffer in full chunks */ + while (bytes_left >= a2dp->codesize) { + /* Enough data to encode (sbc wants 1k blocks) */ + encoded = sbc_encode(&a2dp->sbc, buff, a2dp->codesize, + a2dp->buffer + a2dp->count, + sizeof(a2dp->buffer) - a2dp->count, + &written); + if (encoded <= 0) { + DBG("Encoding error %d", encoded); + goto done; + } + + /* Increment up buff pointer to take into account + * the data processed */ + buff += a2dp->codesize; + bytes_left -= a2dp->codesize; + + /* Increment a2dp buffers */ + a2dp->count += written; + a2dp->frame_count++; + a2dp->samples += encoded / frame_size; + a2dp->nsamples += encoded / frame_size; + + /* No space left for another frame then send */ + if (a2dp->count + written >= data->link_mtu) { + avdtp_write(data); + DBG("sending packet %d, count %d, link_mtu %u", + a2dp->seq_num, a2dp->count, + data->link_mtu); + } + } + +out: + /* Copy the extra to our temp buffer for the next write */ + if (bytes_left > 0) { + memcpy(data->buffer + data->count, buff, bytes_left); + data->count += bytes_left; + bytes_left = 0; + } + +done: + DBG("returning %ld", size - bytes_left / frame_size); + + return size - bytes_left / frame_size; +} + +static int bluetooth_playback_delay(snd_pcm_ioplug_t *io, + snd_pcm_sframes_t *delayp) +{ + DBG(""); + + /* This updates io->hw_ptr value using pointer() function */ + snd_pcm_hwsync(io->pcm); + + *delayp = io->appl_ptr - io->hw_ptr; + if ((io->state == SND_PCM_STATE_RUNNING) && (*delayp < 0)) { + io->callback->stop(io); + io->state = SND_PCM_STATE_XRUN; + *delayp = 0; + } + + /* This should never fail, ALSA API is really not + prepared to handle a non zero return value */ + return 0; +} + +static snd_pcm_ioplug_callback_t bluetooth_hsp_playback = { + .start = bluetooth_playback_start, + .stop = bluetooth_playback_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_hsp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_hsp_write, + .poll_descriptors_count = bluetooth_playback_poll_descriptors_count, + .poll_descriptors = bluetooth_playback_poll_descriptors, + .poll_revents = bluetooth_playback_poll_revents, + .delay = bluetooth_playback_delay, +}; + +static snd_pcm_ioplug_callback_t bluetooth_hsp_capture = { + .start = bluetooth_start, + .stop = bluetooth_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_hsp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_hsp_read, + .poll_descriptors = bluetooth_poll_descriptors, + .poll_revents = bluetooth_poll_revents, +}; + +static snd_pcm_ioplug_callback_t bluetooth_a2dp_playback = { + .start = bluetooth_playback_start, + .stop = bluetooth_playback_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_a2dp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_a2dp_write, + .poll_descriptors_count = bluetooth_playback_poll_descriptors_count, + .poll_descriptors = bluetooth_playback_poll_descriptors, + .poll_revents = bluetooth_playback_poll_revents, + .delay = bluetooth_playback_delay, +}; + +static snd_pcm_ioplug_callback_t bluetooth_a2dp_capture = { + .start = bluetooth_start, + .stop = bluetooth_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_a2dp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_a2dp_read, + .poll_descriptors = bluetooth_poll_descriptors, + .poll_revents = bluetooth_poll_revents, +}; + +#define ARRAY_NELEMS(a) (sizeof((a)) / sizeof((a)[0])) + +static int bluetooth_hsp_hw_constraint(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + snd_pcm_access_t access_list[] = { + SND_PCM_ACCESS_RW_INTERLEAVED, + /* Mmap access is really useless fo this driver, but we + * support it because some pieces of software out there + * insist on using it */ + SND_PCM_ACCESS_MMAP_INTERLEAVED + }; + unsigned int format_list[] = { + SND_PCM_FORMAT_S16 + }; + int err; + + /* access type */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_ACCESS, + ARRAY_NELEMS(access_list), access_list); + if (err < 0) + return err; + + /* supported formats */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_FORMAT, + ARRAY_NELEMS(format_list), format_list); + if (err < 0) + return err; + + /* supported channels */ + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_CHANNELS, + 1, 1); + if (err < 0) + return err; + + /* supported rate */ + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_RATE, + 8000, 8000); + if (err < 0) + return err; + + /* supported block size */ + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_PERIOD_BYTES, + data->link_mtu, data->link_mtu); + if (err < 0) + return err; + + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_PERIODS, + 2, 200); + if (err < 0) + return err; + + return 0; +} + +static int bluetooth_a2dp_hw_constraint(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + struct bluetooth_a2dp *a2dp = &data->a2dp; + struct bluetooth_alsa_config *cfg = &data->alsa_config; + snd_pcm_access_t access_list[] = { + SND_PCM_ACCESS_RW_INTERLEAVED, + /* Mmap access is really useless fo this driver, but we + * support it because some pieces of software out there + * insist on using it */ + SND_PCM_ACCESS_MMAP_INTERLEAVED + }; + unsigned int format_list[] = { + SND_PCM_FORMAT_S16 + }; + unsigned int rate_list[4]; + unsigned int rate_count; + int err, min_channels, max_channels; + unsigned int period_list[] = { + 2048, + 4096, /* e.g. 23.2msec/period (stereo 16bit at 44.1kHz) */ + 8192 + }; + + /* access type */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_ACCESS, + ARRAY_NELEMS(access_list), access_list); + if (err < 0) + return err; + + /* supported formats */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_FORMAT, + ARRAY_NELEMS(format_list), format_list); + if (err < 0) + return err; + + /* supported channels */ + if (cfg->has_channel_mode) + a2dp->sbc_capabilities.channel_mode = cfg->channel_mode; + + if (a2dp->sbc_capabilities.channel_mode & + BT_A2DP_CHANNEL_MODE_MONO) + min_channels = 1; + else + min_channels = 2; + + if (a2dp->sbc_capabilities.channel_mode & + (~BT_A2DP_CHANNEL_MODE_MONO)) + max_channels = 2; + else + max_channels = 1; + + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_CHANNELS, + min_channels, max_channels); + if (err < 0) + return err; + + /* supported buffer sizes + * (can be used as 3*8192, 6*4096, 12*2048, ...) */ + err = snd_pcm_ioplug_set_param_minmax(io, + SND_PCM_IOPLUG_HW_BUFFER_BYTES, + 8192*3, 8192*3); + if (err < 0) + return err; + + /* supported block sizes: */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_PERIOD_BYTES, + ARRAY_NELEMS(period_list), period_list); + if (err < 0) + return err; + + /* supported rates */ + rate_count = 0; + if (cfg->has_rate) { + rate_list[rate_count] = cfg->rate; + rate_count++; + } else { + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_16000) { + rate_list[rate_count] = 16000; + rate_count++; + } + + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_32000) { + rate_list[rate_count] = 32000; + rate_count++; + } + + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_44100) { + rate_list[rate_count] = 44100; + rate_count++; + } + + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_48000) { + rate_list[rate_count] = 48000; + rate_count++; + } + } + + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_RATE, + rate_count, rate_list); + if (err < 0) + return err; + + return 0; +} + +static int bluetooth_parse_config(snd_config_t *conf, + struct bluetooth_alsa_config *bt_config) +{ + snd_config_iterator_t i, next; + + memset(bt_config, 0, sizeof(struct bluetooth_alsa_config)); + + /* Set defaults */ + bt_config->autoconnect = 1; + + snd_config_for_each(i, next, conf) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *id, *value; + + if (snd_config_get_id(n, &id) < 0) + continue; + + if (strcmp(id, "comment") == 0 || strcmp(id, "type") == 0) + continue; + + if (strcmp(id, "autoconnect") == 0) { + int b; + + b = snd_config_get_bool(n); + if (b < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->autoconnect = b; + continue; + } + + if (strcmp(id, "device") == 0 || strcmp(id, "bdaddr") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->has_device = 1; + strncpy(bt_config->device, value, 18); + continue; + } + + if (strcmp(id, "profile") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + if (strcmp(value, "auto") == 0) { + bt_config->transport = BT_CAPABILITIES_TRANSPORT_ANY; + bt_config->has_transport = 1; + } else if (strcmp(value, "voice") == 0 || + strcmp(value, "hfp") == 0) { + bt_config->transport = BT_CAPABILITIES_TRANSPORT_SCO; + bt_config->has_transport = 1; + } else if (strcmp(value, "hifi") == 0 || + strcmp(value, "a2dp") == 0) { + bt_config->transport = BT_CAPABILITIES_TRANSPORT_A2DP; + bt_config->has_transport = 1; + } + continue; + } + + if (strcmp(id, "rate") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->rate = atoi(value); + bt_config->has_rate = 1; + continue; + } + + if (strcmp(id, "mode") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + if (strcmp(value, "mono") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_MONO; + bt_config->has_channel_mode = 1; + } else if (strcmp(value, "dual") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + bt_config->has_channel_mode = 1; + } else if (strcmp(value, "stereo") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_STEREO; + bt_config->has_channel_mode = 1; + } else if (strcmp(value, "joint") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + bt_config->has_channel_mode = 1; + } + continue; + } + + if (strcmp(id, "allocation") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + if (strcmp(value, "loudness") == 0) { + bt_config->allocation_method = BT_A2DP_ALLOCATION_LOUDNESS; + bt_config->has_allocation_method = 1; + } else if (strcmp(value, "snr") == 0) { + bt_config->allocation_method = BT_A2DP_ALLOCATION_SNR; + bt_config->has_allocation_method = 1; + } + continue; + } + + if (strcmp(id, "subbands") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->subbands = atoi(value); + bt_config->has_subbands = 1; + continue; + } + + if (strcmp(id, "blocks") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->block_length = atoi(value); + bt_config->has_block_length = 1; + continue; + } + + if (strcmp(id, "bitpool") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->bitpool = atoi(value); + bt_config->has_bitpool = 1; + continue; + } + + SNDERR("Unknown field %s", id); + return -EINVAL; + } + + return 0; +} + +static int audioservice_send(int sk, const bt_audio_msg_header_t *msg) +{ + int err; + uint16_t length; + + length = msg->length ? msg->length : BT_SUGGESTED_BUFFER_SIZE; + + DBG("sending %s:%s", bt_audio_strtype(msg->type), + bt_audio_strname(msg->name)); + if (send(sk, msg, length, 0) > 0) + err = 0; + else { + err = -errno; + SNDERR("Error sending data to audio service: %s(%d)", + strerror(errno), errno); + } + + return err; +} + +static int audioservice_recv(int sk, bt_audio_msg_header_t *inmsg) +{ + int err; + ssize_t ret; + const char *type, *name; + uint16_t length; + + length = inmsg->length ? inmsg->length : BT_SUGGESTED_BUFFER_SIZE; + + DBG("trying to receive msg from audio service..."); + + ret = recv(sk, inmsg, length, 0); + if (ret < 0) { + err = -errno; + SNDERR("Error receiving IPC data from bluetoothd: %s (%d)", + strerror(errno), errno); + } else if ((size_t) ret < sizeof(bt_audio_msg_header_t)) { + SNDERR("Too short (%d bytes) IPC packet from bluetoothd", ret); + err = -EINVAL; + } else { + type = bt_audio_strtype(inmsg->type); + name = bt_audio_strname(inmsg->name); + if (type && name) { + DBG("Received %s - %s", type, name); + err = 0; + } else { + err = -EINVAL; + SNDERR("Bogus message type %d - name %d" + " received from audio service", + inmsg->type, inmsg->name); + } + + } + + return err; +} + +static int audioservice_expect(int sk, bt_audio_msg_header_t *rsp, + int expected_name) +{ + bt_audio_error_t *error; + int err = audioservice_recv(sk, rsp); + + if (err != 0) + return err; + + if (rsp->name != expected_name) { + err = -EINVAL; + SNDERR("Bogus message %s received while %s was expected", + bt_audio_strname(rsp->name), + bt_audio_strname(expected_name)); + } + + if (rsp->type == BT_ERROR) { + error = (void *) rsp; + SNDERR("%s failed : %s(%d)", + bt_audio_strname(rsp->name), + strerror(error->posix_errno), + error->posix_errno); + return -error->posix_errno; + } + + return err; +} + +static int bluetooth_parse_capabilities(struct bluetooth_data *data, + struct bt_get_capabilities_rsp *rsp) +{ + int bytes_left = rsp->h.length - sizeof(*rsp); + codec_capabilities_t *codec = (void *) rsp->data; + + data->transport = codec->transport; + + if (codec->transport != BT_CAPABILITIES_TRANSPORT_A2DP) + return 0; + + while (bytes_left > 0) { + if ((codec->type == BT_A2DP_SBC_SINK) && + !(codec->lock & BT_WRITE_LOCK)) + break; + + bytes_left -= codec->length; + codec = (void *) codec + codec->length; + } + + if (bytes_left <= 0 || + codec->length != sizeof(data->a2dp.sbc_capabilities)) + return -EINVAL; + + memcpy(&data->a2dp.sbc_capabilities, codec, codec->length); + + return 0; +} + +static int bluetooth_init(struct bluetooth_data *data, + snd_pcm_stream_t stream, snd_config_t *conf) +{ + int sk, err; + struct bluetooth_alsa_config *alsa_conf = &data->alsa_config; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_req *req = (void *) buf; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + + memset(data, 0, sizeof(struct bluetooth_data)); + + err = bluetooth_parse_config(conf, alsa_conf); + if (err < 0) + return err; + + data->server.fd = -1; + data->stream.fd = -1; + + sk = bt_audio_service_open(); + if (sk <= 0) { + err = -errno; + goto failed; + } + + data->server.fd = sk; + data->server.events = POLLIN; + + data->pipefd[0] = -1; + data->pipefd[1] = -1; + + if (pipe(data->pipefd) < 0) { + err = -errno; + goto failed; + } + if (fcntl(data->pipefd[0], F_SETFL, O_NONBLOCK) < 0) { + err = -errno; + goto failed; + } + if (fcntl(data->pipefd[1], F_SETFL, O_NONBLOCK) < 0) { + err = -errno; + goto failed; + } + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_GET_CAPABILITIES; + req->h.length = sizeof(*req); + + if (alsa_conf->autoconnect) + req->flags |= BT_FLAG_AUTOCONNECT; + strncpy(req->destination, alsa_conf->device, 18); + if (alsa_conf->has_transport) + req->transport = alsa_conf->transport; + else + req->transport = BT_CAPABILITIES_TRANSPORT_ANY; + + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + goto failed; + + rsp->h.length = 0; + err = audioservice_expect(data->server.fd, &rsp->h, + BT_GET_CAPABILITIES); + if (err < 0) + goto failed; + + bluetooth_parse_capabilities(data, rsp); + + return 0; + +failed: + if (sk >= 0) + bt_audio_service_close(sk); + return err; +} + +SND_PCM_PLUGIN_DEFINE_FUNC(bluetooth); + +SND_PCM_PLUGIN_DEFINE_FUNC(bluetooth) +{ + struct bluetooth_data *data; + int err; + + DBG("Bluetooth PCM plugin (%s)", + stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture"); + + data = malloc(sizeof(struct bluetooth_data)); + if (!data) { + err = -ENOMEM; + goto error; + } + + err = bluetooth_init(data, stream, conf); + if (err < 0) + goto error; + + data->io.version = SND_PCM_IOPLUG_VERSION; + data->io.name = "Bluetooth Audio Device"; + data->io.mmap_rw = 0; /* No direct mmap communication */ + data->io.private_data = data; + + if (data->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + data->io.callback = stream == SND_PCM_STREAM_PLAYBACK ? + &bluetooth_a2dp_playback : + &bluetooth_a2dp_capture; + else + data->io.callback = stream == SND_PCM_STREAM_PLAYBACK ? + &bluetooth_hsp_playback : + &bluetooth_hsp_capture; + + err = snd_pcm_ioplug_create(&data->io, name, stream, mode); + if (err < 0) + goto error; + + if (data->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + err = bluetooth_a2dp_hw_constraint(&data->io); + else + err = bluetooth_hsp_hw_constraint(&data->io); + + if (err < 0) { + snd_pcm_ioplug_delete(&data->io); + goto error; + } + + *pcmp = data->io.pcm; + + return 0; + +error: + if (data) + bluetooth_exit(data); + + return err; +} + +SND_PCM_PLUGIN_SYMBOL(bluetooth); diff --git a/audio/rtp.h b/audio/rtp.h new file mode 100644 index 0000000..45fddcf --- /dev/null +++ b/audio/rtp.h @@ -0,0 +1,76 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * 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 + * + */ + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct rtp_header { + unsigned cc:4; + unsigned x:1; + unsigned p:1; + unsigned v:2; + + unsigned pt:7; + unsigned m:1; + + uint16_t sequence_number; + uint32_t timestamp; + uint32_t ssrc; + uint32_t csrc[0]; +} __attribute__ ((packed)); + +struct rtp_payload { + unsigned frame_count:4; + unsigned rfa0:1; + unsigned is_last_fragment:1; + unsigned is_first_fragment:1; + unsigned is_fragmented:1; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct rtp_header { + unsigned v:2; + unsigned p:1; + unsigned x:1; + unsigned cc:4; + + unsigned m:1; + unsigned pt:7; + + uint16_t sequence_number; + uint32_t timestamp; + uint32_t ssrc; + uint32_t csrc[0]; +} __attribute__ ((packed)); + +struct rtp_payload { + unsigned is_fragmented:1; + unsigned is_first_fragment:1; + unsigned is_last_fragment:1; + unsigned rfa0:1; + unsigned frame_count:4; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif diff --git a/audio/sink.c b/audio/sink.c new file mode 100644 index 0000000..2d5db18 --- /dev/null +++ b/audio/sink.c @@ -0,0 +1,743 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdint.h> +#include <errno.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" + +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "error.h" +#include "sink.h" +#include "dbus-common.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#define STREAM_SETUP_RETRY_TIMER 2 + +struct pending_request { + DBusConnection *conn; + DBusMessage *msg; + unsigned int id; +}; + +struct sink { + struct audio_device *dev; + struct avdtp *session; + struct avdtp_stream *stream; + unsigned int cb_id; + guint retry_id; + avdtp_session_state_t session_state; + avdtp_state_t stream_state; + sink_state_t state; + struct pending_request *connect; + struct pending_request *disconnect; + DBusConnection *conn; +}; + +struct sink_state_callback { + sink_state_cb cb; + void *user_data; + unsigned int id; +}; + +static GSList *sink_callbacks = NULL; + +static unsigned int avdtp_callback_id = 0; + +static char *str_state[] = { + "SINK_STATE_DISCONNECTED", + "SINK_STATE_CONNECTING", + "SINK_STATE_CONNECTED", + "SINK_STATE_PLAYING", +}; + +static const char *state2str(sink_state_t state) +{ + switch (state) { + case SINK_STATE_DISCONNECTED: + return "disconnected"; + case SINK_STATE_CONNECTING: + return "connecting"; + case SINK_STATE_CONNECTED: + return "connected"; + case SINK_STATE_PLAYING: + return "playing"; + default: + error("Invalid sink state %d", state); + return NULL; + } +} + +static void sink_set_state(struct audio_device *dev, sink_state_t new_state) +{ + struct sink *sink = dev->sink; + const char *state_str; + sink_state_t old_state = sink->state; + GSList *l; + + sink->state = new_state; + + state_str = state2str(new_state); + if (state_str) + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + + DBG("State changed %s: %s -> %s", dev->path, str_state[old_state], + str_state[new_state]); + + for (l = sink_callbacks; l != NULL; l = l->next) { + struct sink_state_callback *cb = l->data; + cb->cb(dev, old_state, new_state, cb->user_data); + } +} + +static void avdtp_state_callback(struct audio_device *dev, + struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data) +{ + struct sink *sink = dev->sink; + + if (sink == NULL) + return; + + switch (new_state) { + case AVDTP_SESSION_STATE_DISCONNECTED: + if (sink->state != SINK_STATE_CONNECTING) { + gboolean value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "Disconnected", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + } + sink_set_state(dev, SINK_STATE_DISCONNECTED); + break; + case AVDTP_SESSION_STATE_CONNECTING: + sink_set_state(dev, SINK_STATE_CONNECTING); + break; + case AVDTP_SESSION_STATE_CONNECTED: + break; + } + + sink->session_state = new_state; +} + +static void pending_request_free(struct audio_device *dev, + struct pending_request *pending) +{ + if (pending->conn) + dbus_connection_unref(pending->conn); + if (pending->msg) + dbus_message_unref(pending->msg); + if (pending->id) + a2dp_cancel(dev, pending->id); + + g_free(pending); +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct audio_device *dev = user_data; + struct sink *sink = dev->sink; + gboolean value; + + if (err) + return; + + switch (new_state) { + case AVDTP_STATE_IDLE: + if (sink->disconnect) { + DBusMessage *reply; + struct pending_request *p; + + p = sink->disconnect; + sink->disconnect = NULL; + + reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(p->conn, reply); + pending_request_free(dev, p); + } + + if (sink->session) { + avdtp_unref(sink->session); + sink->session = NULL; + } + sink->stream = NULL; + sink->cb_id = 0; + break; + case AVDTP_STATE_OPEN: + if (old_state == AVDTP_STATE_CONFIGURED && + sink->state == SINK_STATE_CONNECTING) { + value = TRUE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Connected", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Connected", + DBUS_TYPE_BOOLEAN, &value); + } else if (old_state == AVDTP_STATE_STREAMING) { + value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Stopped", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Playing", + DBUS_TYPE_BOOLEAN, &value); + } + sink_set_state(dev, SINK_STATE_CONNECTED); + break; + case AVDTP_STATE_STREAMING: + value = TRUE; + g_dbus_emit_signal(dev->conn, dev->path, AUDIO_SINK_INTERFACE, + "Playing", DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "Playing", + DBUS_TYPE_BOOLEAN, &value); + sink_set_state(dev, SINK_STATE_PLAYING); + break; + case AVDTP_STATE_CONFIGURED: + case AVDTP_STATE_CLOSING: + case AVDTP_STATE_ABORTING: + default: + break; + } + + sink->stream_state = new_state; +} + +static void error_failed(DBusConnection *conn, DBusMessage *msg, + const char *desc) +{ + DBusMessage *reply = btd_error_failed(msg, desc); + g_dbus_send_message(conn, reply); +} + +static gboolean stream_setup_retry(gpointer user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending = sink->connect; + + sink->retry_id = 0; + + if (sink->stream_state >= AVDTP_STATE_OPEN) { + DBG("Stream successfully created, after XCASE connect:connect"); + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + } else { + DBG("Stream setup failed, after XCASE connect:connect"); + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + } + + sink->connect = NULL; + pending_request_free(sink->dev, pending); + + return FALSE; +} + +static void stream_setup_complete(struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending; + + pending = sink->connect; + + pending->id = 0; + + if (stream) { + DBG("Stream successfully created"); + + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + + sink->connect = NULL; + pending_request_free(sink->dev, pending); + + return; + } + + avdtp_unref(sink->session); + sink->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + sink->retry_id = g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + sink); + } else { + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + sink->connect = NULL; + pending_request_free(sink->dev, pending); + DBG("Stream setup failed : %s", avdtp_strerror(err)); + } +} + +static void select_complete(struct avdtp *session, struct a2dp_sep *sep, + GSList *caps, void *user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending; + int id; + + pending = sink->connect; + pending->id = 0; + + id = a2dp_config(session, sep, stream_setup_complete, caps, sink); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(sink->dev, pending); + sink->connect = NULL; + avdtp_unref(sink->session); + sink->session = NULL; +} + +static void discovery_complete(struct avdtp *session, GSList *seps, struct avdtp_error *err, + void *user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending; + int id; + + if (!sink->connect) { + avdtp_unref(sink->session); + sink->session = NULL; + return; + } + + pending = sink->connect; + + if (err) { + avdtp_unref(sink->session); + sink->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + sink->retry_id = + g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + sink); + } else + goto failed; + return; + } + + DBG("Discovery complete"); + + id = a2dp_select_capabilities(sink->session, AVDTP_SEP_TYPE_SINK, NULL, + select_complete, sink); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(sink->dev, pending); + sink->connect = NULL; + avdtp_unref(sink->session); + sink->session = NULL; +} + +gboolean sink_setup_stream(struct sink *sink, struct avdtp *session) +{ + if (sink->connect || sink->disconnect) + return FALSE; + + if (session && !sink->session) + sink->session = avdtp_ref(session); + + if (!sink->session) + return FALSE; + + avdtp_set_auto_disconnect(sink->session, FALSE); + + if (avdtp_discover(sink->session, discovery_complete, sink) < 0) + return FALSE; + + sink->connect = g_new0(struct pending_request, 1); + + return TRUE; +} + +static DBusMessage *sink_connect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *dev = data; + struct sink *sink = dev->sink; + struct pending_request *pending; + + if (!sink->session) + sink->session = avdtp_get(&dev->src, &dev->dst); + + if (!sink->session) + return btd_error_failed(msg, "Unable to get a session"); + + if (sink->connect || sink->disconnect) + return btd_error_busy(msg); + + if (sink->stream_state >= AVDTP_STATE_OPEN) + return btd_error_already_connected(msg); + + if (!sink_setup_stream(sink, NULL)) + return btd_error_failed(msg, "Failed to create a stream"); + + dev->auto_connect = FALSE; + + pending = sink->connect; + + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + + DBG("stream creation in progress"); + + return NULL; +} + +static DBusMessage *sink_disconnect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct sink *sink = device->sink; + struct pending_request *pending; + int err; + + if (!sink->session) + return btd_error_not_connected(msg); + + if (sink->connect || sink->disconnect) + return btd_error_busy(msg); + + if (sink->stream_state < AVDTP_STATE_OPEN) { + DBusMessage *reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + avdtp_unref(sink->session); + sink->session = NULL; + return reply; + } + + err = avdtp_close(sink->session, sink->stream, FALSE); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + pending = g_new0(struct pending_request, 1); + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + sink->disconnect = pending; + + return NULL; +} + +static DBusMessage *sink_is_connected(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct sink *sink = device->sink; + DBusMessage *reply; + dbus_bool_t connected; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + connected = (sink->stream_state >= AVDTP_STATE_CONFIGURED); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *sink_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct sink *sink = device->sink; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *state; + gboolean value; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* Playing */ + value = (sink->stream_state == AVDTP_STATE_STREAMING); + dict_append_entry(&dict, "Playing", DBUS_TYPE_BOOLEAN, &value); + + /* Connected */ + value = (sink->stream_state >= AVDTP_STATE_CONFIGURED); + dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); + + /* State */ + state = state2str(sink->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static GDBusMethodTable sink_methods[] = { + { "Connect", "", "", sink_connect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "Disconnect", "", "", sink_disconnect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "IsConnected", "", "b", sink_is_connected, + G_DBUS_METHOD_FLAG_DEPRECATED }, + { "GetProperties", "", "a{sv}",sink_get_properties }, + { NULL, NULL, NULL, NULL } +}; + +static GDBusSignalTable sink_signals[] = { + { "Connected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "Disconnected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "Playing", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "Stopped", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, + { "PropertyChanged", "sv" }, + { NULL, NULL } +}; + +static void sink_free(struct audio_device *dev) +{ + struct sink *sink = dev->sink; + + if (sink->cb_id) + avdtp_stream_remove_cb(sink->session, sink->stream, + sink->cb_id); + + if (sink->session) + avdtp_unref(sink->session); + + if (sink->connect) + pending_request_free(dev, sink->connect); + + if (sink->disconnect) + pending_request_free(dev, sink->disconnect); + + if (sink->retry_id) + g_source_remove(sink->retry_id); + + g_free(sink); + dev->sink = NULL; +} + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + + DBG("Unregistered interface %s on path %s", + AUDIO_SINK_INTERFACE, dev->path); + + sink_free(dev); +} + +void sink_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_SINK_INTERFACE); +} + +struct sink *sink_init(struct audio_device *dev) +{ + struct sink *sink; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + sink_methods, sink_signals, NULL, + dev, path_unregister)) + return NULL; + + DBG("Registered interface %s on path %s", + AUDIO_SINK_INTERFACE, dev->path); + + if (avdtp_callback_id == 0) + avdtp_callback_id = avdtp_add_state_cb(avdtp_state_callback, + NULL); + + sink = g_new0(struct sink, 1); + + sink->dev = dev; + + return sink; +} + +gboolean sink_is_active(struct audio_device *dev) +{ + struct sink *sink = dev->sink; + + if (sink->session) + return TRUE; + + return FALSE; +} + +avdtp_state_t sink_get_state(struct audio_device *dev) +{ + struct sink *sink = dev->sink; + + return sink->stream_state; +} + +gboolean sink_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream) +{ + struct sink *sink = dev->sink; + + if (sink->stream) + return FALSE; + + if (!sink->session) + sink->session = avdtp_ref(session); + + sink->stream = stream; + + sink->cb_id = avdtp_stream_add_cb(session, stream, + stream_state_changed, dev); + + return TRUE; +} + +gboolean sink_shutdown(struct sink *sink) +{ + if (!sink->session) + return FALSE; + + avdtp_set_device_disconnect(sink->session, TRUE); + + /* cancel pending connect */ + if (sink->connect) { + struct pending_request *pending = sink->connect; + + if (pending->msg) + error_failed(pending->conn, pending->msg, + "Stream setup failed"); + pending_request_free(sink->dev, pending); + sink->connect = NULL; + + avdtp_unref(sink->session); + sink->session = NULL; + + return TRUE; + } + + /* disconnect already ongoing */ + if (sink->disconnect) + return TRUE; + + if (!sink->stream) + return FALSE; + + if (avdtp_close(sink->session, sink->stream, FALSE) < 0) + return FALSE; + + return TRUE; +} + +unsigned int sink_add_state_cb(sink_state_cb cb, void *user_data) +{ + struct sink_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct sink_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + sink_callbacks = g_slist_append(sink_callbacks, state_cb); + + return state_cb->id; +} + +gboolean sink_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = sink_callbacks; l != NULL; l = l->next) { + struct sink_state_callback *cb = l->data; + if (cb && cb->id == id) { + sink_callbacks = g_slist_remove(sink_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/sink.h b/audio/sink.h new file mode 100644 index 0000000..7b1902b --- /dev/null +++ b/audio/sink.h @@ -0,0 +1,49 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_SINK_INTERFACE "org.bluez.AudioSink" + +typedef enum { + SINK_STATE_DISCONNECTED, + SINK_STATE_CONNECTING, + SINK_STATE_CONNECTED, + SINK_STATE_PLAYING, +} sink_state_t; + +typedef void (*sink_state_cb) (struct audio_device *dev, + sink_state_t old_state, + sink_state_t new_state, + void *user_data); + +unsigned int sink_add_state_cb(sink_state_cb cb, void *user_data); +gboolean sink_remove_state_cb(unsigned int id); + +struct sink *sink_init(struct audio_device *dev); +void sink_unregister(struct audio_device *dev); +gboolean sink_is_active(struct audio_device *dev); +avdtp_state_t sink_get_state(struct audio_device *dev); +gboolean sink_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream); +gboolean sink_setup_stream(struct sink *sink, struct avdtp *session); +gboolean sink_shutdown(struct sink *sink); diff --git a/audio/source.c b/audio/source.c new file mode 100644 index 0000000..6d266f2 --- /dev/null +++ b/audio/source.c @@ -0,0 +1,633 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * Copyright (C) 2009 Joao Paulo Rechi Vita + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdint.h> +#include <errno.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> + +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" + +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "error.h" +#include "source.h" +#include "dbus-common.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#define STREAM_SETUP_RETRY_TIMER 2 + +struct pending_request { + DBusConnection *conn; + DBusMessage *msg; + unsigned int id; +}; + +struct source { + struct audio_device *dev; + struct avdtp *session; + struct avdtp_stream *stream; + unsigned int cb_id; + guint retry_id; + avdtp_session_state_t session_state; + avdtp_state_t stream_state; + source_state_t state; + struct pending_request *connect; + struct pending_request *disconnect; + DBusConnection *conn; +}; + +struct source_state_callback { + source_state_cb cb; + void *user_data; + unsigned int id; +}; + +static GSList *source_callbacks = NULL; + +static unsigned int avdtp_callback_id = 0; + +static const char *state2str(source_state_t state) +{ + switch (state) { + case SOURCE_STATE_DISCONNECTED: + return "disconnected"; + case SOURCE_STATE_CONNECTING: + return "connecting"; + case SOURCE_STATE_CONNECTED: + return "connected"; + case SOURCE_STATE_PLAYING: + return "playing"; + default: + error("Invalid source state %d", state); + return NULL; + } +} + +static void source_set_state(struct audio_device *dev, source_state_t new_state) +{ + struct source *source = dev->source; + const char *state_str; + source_state_t old_state = source->state; + GSList *l; + + source->state = new_state; + + state_str = state2str(new_state); + if (state_str) + emit_property_changed(dev->conn, dev->path, + AUDIO_SOURCE_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + + for (l = source_callbacks; l != NULL; l = l->next) { + struct source_state_callback *cb = l->data; + cb->cb(dev, old_state, new_state, cb->user_data); + } +} + +static void avdtp_state_callback(struct audio_device *dev, + struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data) +{ + struct source *source = dev->source; + + if (source == NULL) + return; + + switch (new_state) { + case AVDTP_SESSION_STATE_DISCONNECTED: + source_set_state(dev, SOURCE_STATE_DISCONNECTED); + break; + case AVDTP_SESSION_STATE_CONNECTING: + source_set_state(dev, SOURCE_STATE_CONNECTING); + break; + case AVDTP_SESSION_STATE_CONNECTED: + break; + } + + source->session_state = new_state; +} + +static void pending_request_free(struct audio_device *dev, + struct pending_request *pending) +{ + if (pending->conn) + dbus_connection_unref(pending->conn); + if (pending->msg) + dbus_message_unref(pending->msg); + if (pending->id) + a2dp_cancel(dev, pending->id); + + g_free(pending); +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct audio_device *dev = user_data; + struct source *source = dev->source; + + if (err) + return; + + switch (new_state) { + case AVDTP_STATE_IDLE: + if (source->disconnect) { + DBusMessage *reply; + struct pending_request *p; + + p = source->disconnect; + source->disconnect = NULL; + + reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(p->conn, reply); + pending_request_free(dev, p); + } + + if (source->session) { + avdtp_unref(source->session); + source->session = NULL; + } + source->stream = NULL; + source->cb_id = 0; + break; + case AVDTP_STATE_OPEN: + source_set_state(dev, SOURCE_STATE_CONNECTED); + break; + case AVDTP_STATE_STREAMING: + source_set_state(dev, SOURCE_STATE_PLAYING); + break; + case AVDTP_STATE_CONFIGURED: + case AVDTP_STATE_CLOSING: + case AVDTP_STATE_ABORTING: + default: + break; + } + + source->stream_state = new_state; +} + +static void error_failed(DBusConnection *conn, DBusMessage *msg, + const char *desc) +{ + DBusMessage *reply = btd_error_failed(msg, desc); + g_dbus_send_message(conn, reply); +} + +static gboolean stream_setup_retry(gpointer user_data) +{ + struct source *source = user_data; + struct pending_request *pending = source->connect; + + source->retry_id = 0; + + if (source->stream_state >= AVDTP_STATE_OPEN) { + DBG("Stream successfully created, after XCASE connect:connect"); + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + } else { + DBG("Stream setup failed, after XCASE connect:connect"); + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + } + + source->connect = NULL; + pending_request_free(source->dev, pending); + + return FALSE; +} + +static void stream_setup_complete(struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct source *source = user_data; + struct pending_request *pending; + + pending = source->connect; + + pending->id = 0; + + if (stream) { + DBG("Stream successfully created"); + + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + + source->connect = NULL; + pending_request_free(source->dev, pending); + + return; + } + + avdtp_unref(source->session); + source->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + source->retry_id = g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + source); + } else { + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + source->connect = NULL; + pending_request_free(source->dev, pending); + DBG("Stream setup failed : %s", avdtp_strerror(err)); + } +} + +static void select_complete(struct avdtp *session, struct a2dp_sep *sep, + GSList *caps, void *user_data) +{ + struct source *source = user_data; + struct pending_request *pending; + int id; + + pending = source->connect; + + pending->id = 0; + + if (caps == NULL) + goto failed; + + id = a2dp_config(session, sep, stream_setup_complete, caps, source); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(source->dev, pending); + source->connect = NULL; + avdtp_unref(source->session); + source->session = NULL; +} + +static void discovery_complete(struct avdtp *session, GSList *seps, struct avdtp_error *err, + void *user_data) +{ + struct source *source = user_data; + struct pending_request *pending; + int id; + + pending = source->connect; + + if (err) { + avdtp_unref(source->session); + source->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + source->retry_id = + g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + source); + } else + goto failed; + return; + } + + DBG("Discovery complete"); + + id = a2dp_select_capabilities(source->session, AVDTP_SEP_TYPE_SOURCE, NULL, + select_complete, source); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(source->dev, pending); + source->connect = NULL; + avdtp_unref(source->session); + source->session = NULL; +} + +gboolean source_setup_stream(struct source *source, struct avdtp *session) +{ + if (source->connect || source->disconnect) + return FALSE; + + if (session && !source->session) + source->session = avdtp_ref(session); + + if (!source->session) + return FALSE; + + avdtp_set_auto_disconnect(source->session, FALSE); + + if (avdtp_discover(source->session, discovery_complete, source) < 0) + return FALSE; + + source->connect = g_new0(struct pending_request, 1); + + return TRUE; +} + +static DBusMessage *source_connect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *dev = data; + struct source *source = dev->source; + struct pending_request *pending; + + if (!source->session) + source->session = avdtp_get(&dev->src, &dev->dst); + + if (!source->session) + return btd_error_failed(msg, "Unable to get a session"); + + if (source->connect || source->disconnect) + return btd_error_busy(msg); + + if (source->stream_state >= AVDTP_STATE_OPEN) + return btd_error_already_connected(msg); + + if (!source_setup_stream(source, NULL)) + return btd_error_failed(msg, "Failed to create a stream"); + + dev->auto_connect = FALSE; + + pending = source->connect; + + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + + DBG("stream creation in progress"); + + return NULL; +} + +static DBusMessage *source_disconnect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct source *source = device->source; + struct pending_request *pending; + int err; + + if (!source->session) + return btd_error_not_connected(msg); + + if (source->connect || source->disconnect) + return btd_error_busy(msg); + + if (source->stream_state < AVDTP_STATE_OPEN) { + DBusMessage *reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + avdtp_unref(source->session); + source->session = NULL; + return reply; + } + + err = avdtp_close(source->session, source->stream, FALSE); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + pending = g_new0(struct pending_request, 1); + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + source->disconnect = pending; + + return NULL; +} + +static DBusMessage *source_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct source *source = device->source; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *state; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* State */ + state = state2str(source->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static GDBusMethodTable source_methods[] = { + { "Connect", "", "", source_connect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "Disconnect", "", "", source_disconnect, + G_DBUS_METHOD_FLAG_ASYNC }, + { "GetProperties", "", "a{sv}",source_get_properties }, + { NULL, NULL, NULL, NULL } +}; + +static GDBusSignalTable source_signals[] = { + { "PropertyChanged", "sv" }, + { NULL, NULL } +}; + +static void source_free(struct audio_device *dev) +{ + struct source *source = dev->source; + + if (source->cb_id) + avdtp_stream_remove_cb(source->session, source->stream, + source->cb_id); + + if (source->session) + avdtp_unref(source->session); + + if (source->connect) + pending_request_free(dev, source->connect); + + if (source->disconnect) + pending_request_free(dev, source->disconnect); + + if (source->retry_id) + g_source_remove(source->retry_id); + + g_free(source); + dev->source = NULL; +} + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + + DBG("Unregistered interface %s on path %s", + AUDIO_SOURCE_INTERFACE, dev->path); + + source_free(dev); +} + +void source_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_SOURCE_INTERFACE); +} + +struct source *source_init(struct audio_device *dev) +{ + struct source *source; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_SOURCE_INTERFACE, + source_methods, source_signals, NULL, + dev, path_unregister)) + return NULL; + + DBG("Registered interface %s on path %s", + AUDIO_SOURCE_INTERFACE, dev->path); + + if (avdtp_callback_id == 0) + avdtp_callback_id = avdtp_add_state_cb(avdtp_state_callback, + NULL); + + source = g_new0(struct source, 1); + + source->dev = dev; + + return source; +} + +gboolean source_is_active(struct audio_device *dev) +{ + struct source *source = dev->source; + + if (source->session) + return TRUE; + + return FALSE; +} + +avdtp_state_t source_get_state(struct audio_device *dev) +{ + struct source *source = dev->source; + + return source->stream_state; +} + +gboolean source_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream) +{ + struct source *source = dev->source; + + if (source->stream) + return FALSE; + + if (!source->session) + source->session = avdtp_ref(session); + + source->stream = stream; + + source->cb_id = avdtp_stream_add_cb(session, stream, + stream_state_changed, dev); + + return TRUE; +} + +gboolean source_shutdown(struct source *source) +{ + if (!source->stream) + return FALSE; + + if (avdtp_close(source->session, source->stream, FALSE) < 0) + return FALSE; + + return TRUE; +} + +unsigned int source_add_state_cb(source_state_cb cb, void *user_data) +{ + struct source_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct source_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + source_callbacks = g_slist_append(source_callbacks, state_cb); + + return state_cb->id; +} + +gboolean source_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = source_callbacks; l != NULL; l = l->next) { + struct source_state_callback *cb = l->data; + if (cb && cb->id == id) { + source_callbacks = g_slist_remove(source_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/source.h b/audio/source.h new file mode 100644 index 0000000..7837284 --- /dev/null +++ b/audio/source.h @@ -0,0 +1,50 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * Copyright (C) 2009 Joao Paulo Rechi Vita + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_SOURCE_INTERFACE "org.bluez.AudioSource" + +typedef enum { + SOURCE_STATE_DISCONNECTED, + SOURCE_STATE_CONNECTING, + SOURCE_STATE_CONNECTED, + SOURCE_STATE_PLAYING, +} source_state_t; + +typedef void (*source_state_cb) (struct audio_device *dev, + source_state_t old_state, + source_state_t new_state, + void *user_data); + +unsigned int source_add_state_cb(source_state_cb cb, void *user_data); +gboolean source_remove_state_cb(unsigned int id); + +struct source *source_init(struct audio_device *dev); +void source_unregister(struct audio_device *dev); +gboolean source_is_active(struct audio_device *dev); +avdtp_state_t source_get_state(struct audio_device *dev); +gboolean source_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream); +gboolean source_setup_stream(struct source *source, struct avdtp *session); +gboolean source_shutdown(struct source *source); diff --git a/audio/telephony-dummy.c b/audio/telephony-dummy.c new file mode 100644 index 0000000..1f89079 --- /dev/null +++ b/audio/telephony-dummy.c @@ -0,0 +1,433 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "telephony.h" +#include "error.h" + +#define TELEPHONY_DUMMY_IFACE "org.bluez.TelephonyTest" +#define TELEPHONY_DUMMY_PATH "/org/bluez/test" + +static DBusConnection *connection = NULL; + +static const char *chld_str = "0,1,1x,2,2x,3,4"; +static char *subscriber_number = NULL; +static char *active_call_number = NULL; +static int active_call_status = 0; +static int active_call_dir = 0; + +static gboolean events_enabled = FALSE; + +static struct indicator dummy_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + { "signal", "0-5", 5, TRUE }, + { "service", "0,1", 1, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +void telephony_device_connected(void *telephony_device) +{ + DBG("telephony-dummy: device %p connected", telephony_device); +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-dummy: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + telephony_last_dialed_number_rsp(telephony_device, CME_ERROR_NONE); + + /* Notify outgoing call set-up successfully initiated */ + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + + active_call_status = CALL_STATUS_ALERTING; + active_call_dir = CALL_DIR_OUTGOING; +} + +void telephony_terminate_call_req(void *telephony_device) +{ + g_free(active_call_number); + active_call_number = NULL; + + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); + + if (telephony_get_indicator(dummy_indicators, "callsetup") > 0) + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + else + telephony_update_indicator(dummy_indicators, "call", + EV_CALL_INACTIVE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); + + telephony_update_indicator(dummy_indicators, "call", EV_CALL_ACTIVE); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + + active_call_status = CALL_STATUS_ACTIVE; +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + g_free(active_call_number); + active_call_number = g_strdup(number); + + DBG("telephony-dummy: dial request to %s", active_call_number); + + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); + + /* Notify outgoing call set-up successfully initiated */ + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + + active_call_status = CALL_STATUS_ALERTING; + active_call_dir = CALL_DIR_OUTGOING; +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + DBG("telephony-dummy: transmit dtmf: %c", tone); + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-dummy: subscriber number request"); + if (subscriber_number) + telephony_subscriber_number_ind(subscriber_number, + NUMBER_TYPE_TELEPHONY, + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + DBG("telephony-dummy: list current calls request"); + if (active_call_number) + telephony_list_current_call_ind(1, active_call_dir, + active_call_status, + CALL_MODE_VOICE, + CALL_MULTIPARTY_NO, + active_call_number, + NUMBER_TYPE_TELEPHONY); + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, "DummyOperator"); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + DBG("telephony-dymmy: got call hold request %s", cmd); + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-dummy: got %s NR and EC request", + enable ? "enable" : "disable"); + + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-dummy: got %s voice dial request", + enable ? "enable" : "disable"); + + g_dbus_emit_signal(connection, TELEPHONY_DUMMY_PATH, + TELEPHONY_DUMMY_IFACE, "VoiceDial", + DBUS_TYPE_INVALID); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + DBG("telephony-dummy: got key press request for %s", keys); + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +/* D-Bus method handlers */ +static DBusMessage *outgoing_call(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *number; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + DBG("telephony-dummy: outgoing call to %s", number); + + g_free(active_call_number); + active_call_number = g_strdup(number); + + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + + active_call_status = CALL_STATUS_ALERTING; + active_call_dir = CALL_DIR_OUTGOING; + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *incoming_call(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *number; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + DBG("telephony-dummy: incoming call to %s", number); + + g_free(active_call_number); + active_call_number = g_strdup(number); + + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + + active_call_status = CALL_STATUS_INCOMING; + active_call_dir = CALL_DIR_INCOMING; + + telephony_incoming_call_ind(number, NUMBER_TYPE_TELEPHONY); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *cancel_call(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + DBG("telephony-dummy: cancel call"); + + g_free(active_call_number); + active_call_number = NULL; + + if (telephony_get_indicator(dummy_indicators, "callsetup") > 0) { + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + telephony_calling_stopped_ind(); + } + + if (telephony_get_indicator(dummy_indicators, "call") > 0) + telephony_update_indicator(dummy_indicators, "call", + EV_CALL_INACTIVE); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *signal_strength(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_uint32_t strength; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT32, &strength, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + if (strength > 5) + return btd_error_invalid_args(msg); + + telephony_update_indicator(dummy_indicators, "signal", strength); + + DBG("telephony-dummy: signal strength set to %u", strength); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *battery_level(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_uint32_t level; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT32, &level, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + if (level > 5) + return btd_error_invalid_args(msg); + + telephony_update_indicator(dummy_indicators, "battchg", level); + + DBG("telephony-dummy: battery level set to %u", level); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *roaming_status(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_bool_t roaming; + int val; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BOOLEAN, &roaming, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + val = roaming ? EV_ROAM_ACTIVE : EV_ROAM_INACTIVE; + + telephony_update_indicator(dummy_indicators, "roam", val); + + DBG("telephony-dummy: roaming status set to %d", val); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *registration_status(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_bool_t registration; + int val; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BOOLEAN, ®istration, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + val = registration ? EV_SERVICE_PRESENT : EV_SERVICE_NONE; + + telephony_update_indicator(dummy_indicators, "service", val); + + DBG("telephony-dummy: registration status set to %d", val); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *set_subscriber_number(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + const char *number; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + g_free(subscriber_number); + subscriber_number = g_strdup(number); + + DBG("telephony-dummy: subscriber number set to %s", number); + + return dbus_message_new_method_return(msg); +} + +static GDBusMethodTable dummy_methods[] = { + { "OutgoingCall", "s", "", outgoing_call }, + { "IncomingCall", "s", "", incoming_call }, + { "CancelCall", "", "", cancel_call }, + { "SignalStrength", "u", "", signal_strength }, + { "BatteryLevel", "u", "", battery_level }, + { "RoamingStatus", "b", "", roaming_status }, + { "RegistrationStatus", "b", "", registration_status }, + { "SetSubscriberNumber","s", "", set_subscriber_number }, + { } +}; + +static GDBusSignalTable dummy_signals[] = { + { "VoiceDial", "" }, + { } +}; + +int telephony_init(void) +{ + uint32_t features = AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES; + + DBG(""); + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + if (g_dbus_register_interface(connection, TELEPHONY_DUMMY_PATH, + TELEPHONY_DUMMY_IFACE, + dummy_methods, dummy_signals, + NULL, NULL, NULL) == FALSE) { + error("telephony-dummy interface %s init failed on path %s", + TELEPHONY_DUMMY_IFACE, TELEPHONY_DUMMY_PATH); + return -1; + } + + telephony_ready_ind(features, dummy_indicators, BTRH_NOT_SUPPORTED, + chld_str); + + return 0; +} + +void telephony_exit(void) +{ + DBG(""); + + g_dbus_unregister_interface(connection, TELEPHONY_DUMMY_PATH, + TELEPHONY_DUMMY_IFACE); + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-maemo5.c b/audio/telephony-maemo5.c new file mode 100644 index 0000000..350df9e --- /dev/null +++ b/audio/telephony-maemo5.c @@ -0,0 +1,2104 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2008-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdint.h> +#include <string.h> +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "telephony.h" +#include "error.h" + +/* SSC D-Bus definitions */ +#define SSC_DBUS_NAME "com.nokia.phone.SSC" +#define SSC_DBUS_IFACE "com.nokia.phone.SSC" +#define SSC_DBUS_PATH "/com/nokia/phone/SSC" + +/* libcsnet D-Bus definitions */ +#define NETWORK_BUS_NAME "com.nokia.phone.net" +#define NETWORK_INTERFACE "Phone.Net" +#define NETWORK_PATH "/com/nokia/phone/net" + +/* Mask bits for supported services */ +#define NETWORK_MASK_GPRS_SUPPORT 0x01 +#define NETWORK_MASK_CS_SERVICES 0x02 +#define NETWORK_MASK_EGPRS_SUPPORT 0x04 +#define NETWORK_MASK_HSDPA_AVAIL 0x08 +#define NETWORK_MASK_HSUPA_AVAIL 0x10 + +/* network get cell info: cell type */ +#define NETWORK_UNKNOWN_CELL 0 +#define NETWORK_GSM_CELL 1 +#define NETWORK_WCDMA_CELL 2 + +enum net_registration_status { + NETWORK_REG_STATUS_HOME = 0x00, + NETWORK_REG_STATUS_ROAM, + NETWORK_REG_STATUS_ROAM_BLINK, + NETWORK_REG_STATUS_NOSERV, + NETWORK_REG_STATUS_NOSERV_SEARCHING, + NETWORK_REG_STATUS_NOSERV_NOTSEARCHING, + NETWORK_REG_STATUS_NOSERV_NOSIM, + NETWORK_REG_STATUS_POWER_OFF = 0x08, + NETWORK_REG_STATUS_NSPS, + NETWORK_REG_STATUS_NSPS_NO_COVERAGE, + NETWORK_REG_STATUS_NOSERV_SIM_REJECTED_BY_NW +}; + +enum network_types { + NETWORK_GSM_HOME_PLMN = 0, + NETWORK_GSM_PREFERRED_PLMN, + NETWORK_GSM_FORBIDDEN_PLMN, + NETWORK_GSM_OTHER_PLMN, + NETWORK_GSM_NO_PLMN_AVAIL +}; + +enum network_alpha_tag_name_type { + NETWORK_HARDCODED_LATIN_OPER_NAME = 0, + NETWORK_HARDCODED_USC2_OPER_NAME, + NETWORK_NITZ_SHORT_OPER_NAME, + NETWORK_NITZ_FULL_OPER_NAME, +}; + +#define TELEPHONY_MAEMO_PATH "/com/nokia/MaemoTelephony" +#define TELEPHONY_MAEMO_INTERFACE "com.nokia.MaemoTelephony" + +#define CALLERID_BASE "/var/lib/bluetooth/maemo-callerid-" +#define ALLOWED_FLAG_FILE "/var/lib/bluetooth/maemo-callerid-allowed" +#define RESTRICTED_FLAG_FILE "/var/lib/bluetooth/maemo-callerid-restricted" +#define NONE_FLAG_FILE "/var/lib/bluetooth/maemo-callerid-none" + +static uint32_t callerid = 0; + +/* CSD CALL plugin D-Bus definitions */ +#define CSD_CALL_BUS_NAME "com.nokia.csd.Call" +#define CSD_CALL_INTERFACE "com.nokia.csd.Call" +#define CSD_CALL_INSTANCE "com.nokia.csd.Call.Instance" +#define CSD_CALL_CONFERENCE "com.nokia.csd.Call.Conference" +#define CSD_CALL_PATH "/com/nokia/csd/call" +#define CSD_CALL_CONFERENCE_PATH "/com/nokia/csd/call/conference" + +/* Call status values as exported by the CSD CALL plugin */ +#define CSD_CALL_STATUS_IDLE 0 +#define CSD_CALL_STATUS_CREATE 1 +#define CSD_CALL_STATUS_COMING 2 +#define CSD_CALL_STATUS_PROCEEDING 3 +#define CSD_CALL_STATUS_MO_ALERTING 4 +#define CSD_CALL_STATUS_MT_ALERTING 5 +#define CSD_CALL_STATUS_WAITING 6 +#define CSD_CALL_STATUS_ANSWERED 7 +#define CSD_CALL_STATUS_ACTIVE 8 +#define CSD_CALL_STATUS_MO_RELEASE 9 +#define CSD_CALL_STATUS_MT_RELEASE 10 +#define CSD_CALL_STATUS_HOLD_INITIATED 11 +#define CSD_CALL_STATUS_HOLD 12 +#define CSD_CALL_STATUS_RETRIEVE_INITIATED 13 +#define CSD_CALL_STATUS_RECONNECT_PENDING 14 +#define CSD_CALL_STATUS_TERMINATED 15 +#define CSD_CALL_STATUS_SWAP_INITIATED 16 + +#define CALL_FLAG_NONE 0 +#define CALL_FLAG_PRESENTATION_ALLOWED 0x01 +#define CALL_FLAG_PRESENTATION_RESTRICTED 0x02 + +/* SIM Phonebook D-Bus definitions */ +#define SIM_PHONEBOOK_BUS_NAME "com.nokia.phone.SIM" +#define SIM_PHONEBOOK_INTERFACE "Phone.Sim.Phonebook" +#define SIM_PHONEBOOK_PATH "/com/nokia/phone/SIM/phonebook" + +#define PHONEBOOK_INDEX_FIRST_ENTRY 0xFFFF +#define PHONEBOOK_INDEX_NEXT_FREE_LOCATION 0xFFFE + +enum sim_phonebook_type { + SIM_PHONEBOOK_TYPE_ADN = 0x0, + SIM_PHONEBOOK_TYPE_SDN, + SIM_PHONEBOOK_TYPE_FDN, + SIM_PHONEBOOK_TYPE_VMBX, + SIM_PHONEBOOK_TYPE_MBDN, + SIM_PHONEBOOK_TYPE_EN, + SIM_PHONEBOOK_TYPE_MSISDN +}; + +enum sim_phonebook_location_type { + SIM_PHONEBOOK_LOCATION_EXACT = 0x0, + SIM_PHONEBOOK_LOCATION_NEXT +}; + +struct csd_call { + char *object_path; + int status; + gboolean originating; + gboolean emergency; + gboolean on_hold; + gboolean conference; + char *number; + gboolean setup; +}; + +static struct { + uint8_t status; + uint16_t lac; + uint32_t cell_id; + uint32_t operator_code; + uint32_t country_code; + uint8_t network_type; + uint8_t supported_services; + uint16_t signals_bar; + char *operator_name; +} net = { + .status = NETWORK_REG_STATUS_NOSERV, + .lac = 0, + .cell_id = 0, + .operator_code = 0, + .country_code = 0, + .network_type = NETWORK_GSM_NO_PLMN_AVAIL, + .supported_services = 0, + .signals_bar = 0, + .operator_name = NULL, +}; + +static DBusConnection *connection = NULL; + +static GSList *calls = NULL; + +/* Reference count for determining the call indicator status */ +static GSList *active_calls = NULL; + +static char *msisdn = NULL; /* Subscriber number */ +static char *vmbx = NULL; /* Voice mailbox number */ + +/* HAL battery namespace key values */ +static int battchg_cur = -1; /* "battery.charge_level.current" */ +static int battchg_last = -1; /* "battery.charge_level.last_full" */ +static int battchg_design = -1; /* "battery.charge_level.design" */ + +static gboolean get_calls_active = FALSE; + +static gboolean events_enabled = FALSE; + +/* Supported set of call hold operations */ +static const char *chld_str = "0,1,1x,2,2x,3,4"; + +static char *last_dialed_number = NULL; + +/* Timer for tracking call creation requests */ +static guint create_request_timer = 0; + +static struct indicator maemo_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + { "signal", "0-5", 0, TRUE }, + { "service", "0,1", 0, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static char *call_status_str[] = { + "IDLE", + "CREATE", + "COMING", + "PROCEEDING", + "MO_ALERTING", + "MT_ALERTING", + "WAITING", + "ANSWERED", + "ACTIVE", + "MO_RELEASE", + "MT_RELEASE", + "HOLD_INITIATED", + "HOLD", + "RETRIEVE_INITIATED", + "RECONNECT_PENDING", + "TERMINATED", + "SWAP_INITIATED", + "???" +}; + +static struct csd_call *find_call(const char *path) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (g_str_equal(call->object_path, path)) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_held_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + if (call->status != CSD_CALL_STATUS_HOLD) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_idle_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status != CSD_CALL_STATUS_IDLE) + return call; + } + + return NULL; +} + +static struct csd_call *find_call_with_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + return call; + } + + return NULL; +} + +static int release_conference(void) +{ + DBusMessage *msg; + + DBG("telephony-maemo: releasing conference call"); + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + CSD_CALL_CONFERENCE_PATH, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int release_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int answer_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Answer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int split_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Split"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int unhold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Unhold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int hold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Hold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int swap_calls(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Swap"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int create_conference(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Conference"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int call_transfer(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Transfer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +void telephony_device_connected(void *telephony_device) +{ + struct csd_call *coming; + + DBG("telephony-maemo: device %p connected", telephony_device); + + coming = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (coming) { + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(coming->number, + number_type(coming->number)); + else + telephony_incoming_call_ind(coming->number, + number_type(coming->number)); + } +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-maemo: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + DBG("telephony-maemo: last dialed number request"); + + if (last_dialed_number) + telephony_dial_number_req(telephony_device, + last_dialed_number); + else + telephony_last_dialed_number_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); +} + +void telephony_terminate_call_req(void *telephony_device) +{ + struct csd_call *call; + int err; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + if (!call) + call = find_non_idle_call(); + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct csd_call *call; + + call = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (!call) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (answer_call(call) < 0) + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); +} + +static int send_method_call(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + dbus_pending_call_unref(call); + dbus_message_unref(msg); + + return 0; +} + +static const char *memory_dial_lookup(int location) +{ + if (location == 1) + return vmbx; + else + return NULL; +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + uint32_t flags = callerid; + int ret; + + DBG("telephony-maemo: dial request to %s", number); + + if (strncmp(number, "*31#", 4) == 0) { + number += 4; + flags = CALL_FLAG_PRESENTATION_ALLOWED; + } else if (strncmp(number, "#31#", 4) == 0) { + number += 4; + flags = CALL_FLAG_PRESENTATION_RESTRICTED; + } else if (number[0] == '>') { + const char *location = &number[1]; + + number = memory_dial_lookup(strtol(&number[1], NULL, 0)); + if (!number) { + error("No number at memory location %s", location); + telephony_dial_number_rsp(telephony_device, + CME_ERROR_INVALID_INDEX); + return; + } + } + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "CreateWith", + NULL, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_UINT32, &flags, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + int ret; + char buf[2] = { tone, '\0' }, *buf_ptr = buf; + + DBG("telephony-maemo: transmit dtmf: %s", buf); + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "SendDTMF", + NULL, NULL, + DBUS_TYPE_STRING, &buf_ptr, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-maemo: subscriber number request"); + if (msisdn) + telephony_subscriber_number_ind(msisdn, + number_type(msisdn), + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +static int csd_status_to_hfp(struct csd_call *call) +{ + switch (call->status) { + case CSD_CALL_STATUS_IDLE: + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + case CSD_CALL_STATUS_TERMINATED: + return -1; + case CSD_CALL_STATUS_CREATE: + return CALL_STATUS_DIALING; + case CSD_CALL_STATUS_WAITING: + return CALL_STATUS_WAITING; + case CSD_CALL_STATUS_PROCEEDING: + /* PROCEEDING can happen in outgoing/incoming */ + if (call->originating) + return CALL_STATUS_DIALING; + else + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_COMING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_MO_ALERTING: + return CALL_STATUS_ALERTING; + case CSD_CALL_STATUS_MT_ALERTING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_ANSWERED: + case CSD_CALL_STATUS_ACTIVE: + case CSD_CALL_STATUS_RECONNECT_PENDING: + case CSD_CALL_STATUS_SWAP_INITIATED: + case CSD_CALL_STATUS_HOLD_INITIATED: + return CALL_STATUS_ACTIVE; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + case CSD_CALL_STATUS_HOLD: + return CALL_STATUS_HELD; + default: + return -1; + } +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-maemo: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct csd_call *call = l->data; + int status, direction, multiparty; + + status = csd_status_to_hfp(call); + if (status < 0) + continue; + + direction = call->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = call->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + telephony_list_current_call_ind(i, direction, status, + CALL_MODE_VOICE, multiparty, + call->number, + number_type(call->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + net.operator_name ? net.operator_name : ""); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +static void foreach_call_with_status(int status, + int (*func)(struct csd_call *call)) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + func(call); + } +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct csd_call *call; + int err = 0; + + DBG("telephony-maemo: got call hold request %s", cmd); + + if (strlen(cmd) > 1) + idx = &cmd[1]; + else + idx = NULL; + + if (idx) + call = g_slist_nth_data(calls, strtol(idx, NULL, 0) - 1); + else + call = NULL; + + switch (cmd[0]) { + case '0': + foreach_call_with_status(CSD_CALL_STATUS_HOLD, release_call); + foreach_call_with_status(CSD_CALL_STATUS_WAITING, + release_call); + break; + case '1': + if (idx) { + if (call) + err = release_call(call); + break; + } + foreach_call_with_status(CSD_CALL_STATUS_ACTIVE, release_call); + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + if (call) + err = answer_call(call); + break; + case '2': + if (idx) { + if (call) + err = split_call(call); + } else { + struct csd_call *held, *wait; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + held = find_call_with_status(CSD_CALL_STATUS_HOLD); + wait = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (wait) + err = answer_call(wait); + else if (call && held) + err = swap_calls(); + else { + if (call) + err = hold_call(call); + if (held) + err = unhold_call(held); + } + } + break; + case '3': + if (find_call_with_status(CSD_CALL_STATUS_HOLD) || + find_call_with_status(CSD_CALL_STATUS_WAITING)) + err = create_conference(); + break; + case '4': + err = call_transfer(); + break; + default: + DBG("Unknown call hold request"); + break; + } + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo: got %s NR and EC request", + enable ? "enable" : "disable"); + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct csd_call *active, *waiting; + int err; + + DBG("telephony-maemo: got key press request for %s", keys); + + waiting = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + active = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + + if (waiting) + err = answer_call(waiting); + else if (active) + err = release_call(active); + else + err = 0; + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo: got %s voice dial request", + enable ? "enable" : "disable"); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NOT_SUPPORTED); +} + +static void handle_incoming_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Coming() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Incoming call to %s from number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(call->number, + number_type(call->number)); + else + telephony_incoming_call_ind(call->number, + number_type(call->number)); +} + +static void handle_outgoing_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Created() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Outgoing call from %s to number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + g_free(last_dialed_number); + last_dialed_number = g_strdup(number); + + if (create_request_timer) { + g_source_remove(create_request_timer); + create_request_timer = 0; + } +} + +static gboolean create_timeout(gpointer user_data) +{ + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + create_request_timer = 0; + return FALSE; +} + +static void handle_create_requested(DBusMessage *msg) +{ + DBG("Call.CreateRequested()"); + + if (create_request_timer) + g_source_remove(create_request_timer); + + create_request_timer = g_timeout_add_seconds(5, create_timeout, NULL); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); +} + +static void handle_call_status(DBusMessage *msg, const char *call_path) +{ + struct csd_call *call; + dbus_uint32_t status, cause_type, cause; + int callheld = telephony_get_indicator(maemo_indicators, "callheld"); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_UINT32, &cause_type, + DBUS_TYPE_UINT32, &cause, + DBUS_TYPE_INVALID)) { + error("Unexpected paramters in Instance.CallStatus() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + if (status > 16) { + error("Invalid call status %u", status); + return; + } + + DBG("Call %s changed from %s to %s", call_path, + call_status_str[call->status], call_status_str[status]); + + if (call->status == (int) status) { + DBG("Ignoring CSD Call state change to existing state"); + return; + } + + call->status = (int) status; + + switch (status) { + case CSD_CALL_STATUS_IDLE: + if (call->setup) { + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + } + + g_free(call->number); + call->number = NULL; + call->originating = FALSE; + call->emergency = FALSE; + call->on_hold = FALSE; + call->conference = FALSE; + call->setup = FALSE; + break; + case CSD_CALL_STATUS_CREATE: + call->originating = TRUE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_COMING: + call->originating = FALSE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_PROCEEDING: + break; + case CSD_CALL_STATUS_MO_ALERTING: + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CSD_CALL_STATUS_MT_ALERTING: + break; + case CSD_CALL_STATUS_WAITING: + break; + case CSD_CALL_STATUS_ANSWERED: + break; + case CSD_CALL_STATUS_ACTIVE: + if (call->on_hold) { + call->on_hold = FALSE; + if (find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + } else { + if (!g_slist_find(active_calls, call)) + active_calls = g_slist_prepend(active_calls, call); + if (g_slist_length(active_calls) == 1) + telephony_update_indicator(maemo_indicators, + "call", + EV_CALL_ACTIVE); + /* Upgrade callheld status if necessary */ + if (callheld == EV_CALLHELD_ON_HOLD) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + call->setup = FALSE; + } + break; + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + active_calls = g_slist_remove(active_calls, call); + if (g_slist_length(active_calls) == 0) + telephony_update_indicator(maemo_indicators, "call", + EV_CALL_INACTIVE); + break; + case CSD_CALL_STATUS_HOLD_INITIATED: + break; + case CSD_CALL_STATUS_HOLD: + call->on_hold = TRUE; + if (find_non_held_call()) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + break; + case CSD_CALL_STATUS_RECONNECT_PENDING: + break; + case CSD_CALL_STATUS_TERMINATED: + if (call->on_hold && + !find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + else if (callheld == EV_CALLHELD_MULTIPLE && + find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_SWAP_INITIATED: + break; + default: + error("Unknown call status %u", status); + break; + } +} + +static void handle_conference(DBusMessage *msg, gboolean joined) +{ + const char *path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Conference.%s", + dbus_message_get_member(msg)); + return; + } + + call = find_call(path); + if (!call) { + error("Conference signal for unknown call %s", path); + return; + } + + DBG("Call %s %s the conference", path, joined ? "joined" : "left"); + + call->conference = joined; +} + +static void get_operator_name_reply(DBusPendingCall *pending_call, + void *user_data) +{ + DBusMessage *reply; + DBusError err; + const char *name; + dbus_int32_t net_err; + + reply = dbus_pending_call_steal_reply(pending_call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("get_operator_name failed: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (!dbus_message_get_args(reply, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INT32, &net_err, + DBUS_TYPE_INVALID)) { + error("Unexpected get_operator_name reply parameters: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + if (net_err != 0) { + error("get_operator_name failed with code %d", net_err); + goto done; + } + + if (strlen(name) == 0) + goto done; + + g_free(net.operator_name); + net.operator_name = g_strdup(name); + + DBG("telephony-maemo: operator name updated: %s", name); + +done: + dbus_message_unref(reply); +} + +static void resolve_operator_name(uint32_t operator, uint32_t country) +{ + uint8_t name_type = NETWORK_HARDCODED_LATIN_OPER_NAME; + + send_method_call(NETWORK_BUS_NAME, NETWORK_PATH, + NETWORK_INTERFACE, "get_operator_name", + get_operator_name_reply, NULL, + DBUS_TYPE_BYTE, &name_type, + DBUS_TYPE_UINT32, &operator, + DBUS_TYPE_UINT32, &country, + DBUS_TYPE_INVALID); +} + +static void update_registration_status(uint8_t status, uint16_t lac, + uint32_t cell_id, + uint32_t operator_code, + uint32_t country_code, + uint8_t network_type, + uint8_t supported_services) +{ + if (net.status != status) { + switch (status) { + case NETWORK_REG_STATUS_HOME: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_INACTIVE); + if (net.status >= NETWORK_REG_STATUS_NOSERV) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_ROAM: + case NETWORK_REG_STATUS_ROAM_BLINK: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_ACTIVE); + if (net.status >= NETWORK_REG_STATUS_NOSERV) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_NOSERV: + case NETWORK_REG_STATUS_NOSERV_SEARCHING: + case NETWORK_REG_STATUS_NOSERV_NOTSEARCHING: + case NETWORK_REG_STATUS_NOSERV_NOSIM: + case NETWORK_REG_STATUS_POWER_OFF: + case NETWORK_REG_STATUS_NSPS: + case NETWORK_REG_STATUS_NSPS_NO_COVERAGE: + case NETWORK_REG_STATUS_NOSERV_SIM_REJECTED_BY_NW: + if (net.status < NETWORK_REG_STATUS_NOSERV) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_NONE); + break; + } + + net.status = status; + } + + net.lac = lac; + net.cell_id = cell_id; + + if (net.operator_code != operator_code || + net.country_code != country_code) { + g_free(net.operator_name); + net.operator_name = NULL; + resolve_operator_name(operator_code, country_code); + net.operator_code = operator_code; + net.country_code = country_code; + } + + net.network_type = network_type; + net.supported_services = supported_services; +} + +static void handle_registration_status_change(DBusMessage *msg) +{ + uint8_t status; + dbus_uint16_t lac, network_type, supported_services; + dbus_uint32_t cell_id, operator_code, country_code; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_BYTE, &status, + DBUS_TYPE_UINT16, &lac, + DBUS_TYPE_UINT32, &cell_id, + DBUS_TYPE_UINT32, &operator_code, + DBUS_TYPE_UINT32, &country_code, + DBUS_TYPE_BYTE, &network_type, + DBUS_TYPE_BYTE, &supported_services, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in registration_status_change"); + return; + } + + update_registration_status(status, lac, cell_id, operator_code, + country_code, network_type, + supported_services); +} + +static void update_signal_strength(uint8_t signals_bar) +{ + int signal; + + if (signals_bar > 100) { + DBG("signals_bar greater than expected: %u", signals_bar); + signals_bar = 100; + } + + if (net.signals_bar == signals_bar) + return; + + /* A simple conversion from 0-100 to 0-5 (used by HFP) */ + signal = (signals_bar + 20) / 21; + + telephony_update_indicator(maemo_indicators, "signal", signal); + + net.signals_bar = signals_bar; + + DBG("Signal strength updated: %u/100, %d/5", signals_bar, signal); +} + +static void handle_signal_strength_change(DBusMessage *msg) +{ + uint8_t signals_bar, rssi_in_dbm; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_BYTE, &signals_bar, + DBUS_TYPE_BYTE, &rssi_in_dbm, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in signal_strength_change"); + return; + } + + update_signal_strength(signals_bar); +} + +static gboolean iter_get_basic_args(DBusMessageIter *iter, + int first_arg_type, ...) +{ + int type; + va_list ap; + + va_start(ap, first_arg_type); + + for (type = first_arg_type; type != DBUS_TYPE_INVALID; + type = va_arg(ap, int)) { + void *value = va_arg(ap, void *); + int real_type = dbus_message_iter_get_arg_type(iter); + + if (real_type != type) { + error("iter_get_basic_args: expected %c but got %c", + (char) type, (char) real_type); + break; + } + + dbus_message_iter_get_basic(iter, value); + dbus_message_iter_next(iter); + } + + va_end(ap); + + return type == DBUS_TYPE_INVALID ? TRUE : FALSE; +} + +static void hal_battery_level_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + dbus_int32_t level; + int *value = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (dbus_message_get_args(reply, &err, + DBUS_TYPE_INT32, &level, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse GetPropertyInteger reply: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + *value = (int) level; + + if (value == &battchg_last) + DBG("telephony-maemo: battery.charge_level.last_full is %d", + *value); + else if (value == &battchg_design) + DBG("telephony-maemo: battery.charge_level.design is %d", + *value); + else + DBG("telephony-maemo: battery.charge_level.current is %d", + *value); + + if ((battchg_design > 0 || battchg_last > 0) && battchg_cur >= 0) { + int new, max; + + if (battchg_last > 0) + max = battchg_last; + else + max = battchg_design; + + new = battchg_cur * 5 / max; + + telephony_update_indicator(maemo_indicators, "battchg", new); + } +done: + dbus_message_unref(reply); +} + +static void hal_get_integer(const char *path, const char *key, void *user_data) +{ + send_method_call("org.freedesktop.Hal", path, + "org.freedesktop.Hal.Device", + "GetPropertyInteger", + hal_battery_level_reply, user_data, + DBUS_TYPE_STRING, &key, + DBUS_TYPE_INVALID); +} + +static void handle_hal_property_modified(DBusMessage *msg) +{ + DBusMessageIter iter, array; + dbus_int32_t num_changes; + const char *path; + + path = dbus_message_get_path(msg); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_get_basic(&iter, &num_changes); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_recurse(&iter, &array); + + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) { + DBusMessageIter prop; + const char *name; + dbus_bool_t added, removed; + + dbus_message_iter_recurse(&array, &prop); + + if (!iter_get_basic_args(&prop, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_BOOLEAN, &added, + DBUS_TYPE_BOOLEAN, &removed, + DBUS_TYPE_INVALID)) { + error("Invalid hal PropertyModified parameters"); + break; + } + + if (g_str_equal(name, "battery.charge_level.last_full")) + hal_get_integer(path, name, &battchg_last); + else if (g_str_equal(name, "battery.charge_level.current")) + hal_get_integer(path, name, &battchg_cur); + else if (g_str_equal(name, "battery.charge_level.design")) + hal_get_integer(path, name, &battchg_design); + + dbus_message_iter_next(&array); + } +} + +static void csd_call_free(struct csd_call *call) +{ + if (!call) + return; + + g_free(call->object_path); + g_free(call->number); + + g_free(call); +} + +static void parse_call_list(DBusMessageIter *iter) +{ + do { + DBusMessageIter call_iter; + struct csd_call *call; + const char *object_path, *number; + dbus_uint32_t status; + dbus_bool_t originating, terminating, emerg, on_hold, conf; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) { + error("Unexpected signature in GetCallInfoAll reply"); + break; + } + + dbus_message_iter_recurse(iter, &call_iter); + + if (!iter_get_basic_args(&call_iter, + DBUS_TYPE_OBJECT_PATH, &object_path, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_BOOLEAN, &originating, + DBUS_TYPE_BOOLEAN, &terminating, + DBUS_TYPE_BOOLEAN, &emerg, + DBUS_TYPE_BOOLEAN, &on_hold, + DBUS_TYPE_BOOLEAN, &conf, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Parsing call D-Bus parameters failed"); + break; + } + + call = find_call(object_path); + if (!call) { + call = g_new0(struct csd_call, 1); + call->object_path = g_strdup(object_path); + call->status = (int) status; + calls = g_slist_append(calls, call); + DBG("telephony-maemo: new csd call instance at %s", + object_path); + } + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + /* CSD gives incorrect call_hold property sometimes */ + if ((call->status != CSD_CALL_STATUS_HOLD && on_hold) || + (call->status == CSD_CALL_STATUS_HOLD && + !on_hold)) { + error("Conflicting call status and on_hold property!"); + on_hold = call->status == CSD_CALL_STATUS_HOLD; + } + + call->originating = originating; + call->on_hold = on_hold; + call->conference = conf; + g_free(call->number); + call->number = g_strdup(number); + + } while (dbus_message_iter_next(iter)); +} + +static void signal_strength_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + uint8_t signals_bar, rssi_in_dbm; + dbus_int32_t net_err; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("Unable to get signal strength: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (!dbus_message_get_args(reply, &err, + DBUS_TYPE_BYTE, &signals_bar, + DBUS_TYPE_BYTE, &rssi_in_dbm, + DBUS_TYPE_INT32, &net_err, + DBUS_TYPE_INVALID)) { + error("Unable to parse signal_strength reply: %s, %s", + err.name, err.message); + dbus_error_free(&err); + return; + } + + if (net_err != 0) { + error("get_signal_strength failed with code %d", net_err); + return; + } + + update_signal_strength(signals_bar); + +done: + dbus_message_unref(reply); +} + +static int get_signal_strength(void) +{ + return send_method_call(NETWORK_BUS_NAME, NETWORK_PATH, + NETWORK_INTERFACE, "get_signal_strength", + signal_strength_reply, NULL, + DBUS_TYPE_INVALID); +} + +static void registration_status_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + uint8_t status; + dbus_uint16_t lac, network_type, supported_services; + dbus_uint32_t cell_id, operator_code, country_code; + dbus_int32_t net_err; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("Unable to get registration status: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (!dbus_message_get_args(reply, &err, + DBUS_TYPE_BYTE, &status, + DBUS_TYPE_UINT16, &lac, + DBUS_TYPE_UINT32, &cell_id, + DBUS_TYPE_UINT32, &operator_code, + DBUS_TYPE_UINT32, &country_code, + DBUS_TYPE_BYTE, &network_type, + DBUS_TYPE_BYTE, &supported_services, + DBUS_TYPE_INT32, &net_err, + DBUS_TYPE_INVALID)) { + error("Unable to parse registration_status_change reply:" + " %s, %s", err.name, err.message); + dbus_error_free(&err); + return; + } + + if (net_err != 0) { + error("get_registration_status failed with code %d", net_err); + return; + } + + update_registration_status(status, lac, cell_id, operator_code, + country_code, network_type, + supported_services); + + get_signal_strength(); + +done: + dbus_message_unref(reply); +} + +static int get_registration_status(void) +{ + return send_method_call(NETWORK_BUS_NAME, NETWORK_PATH, + NETWORK_INTERFACE, "get_registration_status", + registration_status_reply, NULL, + DBUS_TYPE_INVALID); +} + +static void call_info_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub;; + + get_calls_active = FALSE; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in GetCallInfoAll return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + parse_call_list(&sub); + + get_registration_status(); + +done: + dbus_message_unref(reply); +} + +static void hal_find_device_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + const char *path; + char match_string[256]; + int type; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in FindDeviceByCapability return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + type = dbus_message_iter_get_arg_type(&sub); + + if (type != DBUS_TYPE_OBJECT_PATH && type != DBUS_TYPE_STRING) { + error("No hal device with battery capability found"); + goto done; + } + + dbus_message_iter_get_basic(&sub, &path); + + DBG("telephony-maemo: found battery device at %s", path); + + snprintf(match_string, sizeof(match_string), + "type='signal'," + "path='%s'," + "interface='org.freedesktop.Hal.Device'," + "member='PropertyModified'", path); + dbus_bus_add_match(connection, match_string, NULL); + + hal_get_integer(path, "battery.charge_level.last_full", &battchg_last); + hal_get_integer(path, "battery.charge_level.current", &battchg_cur); + hal_get_integer(path, "battery.charge_level.design", &battchg_design); + +done: + dbus_message_unref(reply); +} + +static void phonebook_read_reply(DBusPendingCall *call, void *user_data) +{ + DBusError derr; + DBusMessage *reply; + const char *name, *number; + char **number_type = user_data; + dbus_int32_t current_location, err; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + error("SIM.Phonebook replied with an error: %s, %s", + derr.name, derr.message); + dbus_error_free(&derr); + goto done; + } + + dbus_error_init(&derr); + if (dbus_message_get_args(reply, &derr, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INT32, ¤t_location, + DBUS_TYPE_INT32, &err, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse SIM.Phonebook.read arguments: %s, %s", + derr.name, derr.message); + dbus_error_free(&derr); + goto done; + } + + if (err != 0) { + error("SIM.Phonebook.read failed with error %d", err); + if (number_type == &vmbx) + vmbx = g_strdup(getenv("VMBX_NUMBER")); + goto done; + } + + if (number_type == &msisdn) { + g_free(msisdn); + msisdn = g_strdup(number); + DBG("Got MSISDN %s (%s)", number, name); + } else { + g_free(vmbx); + vmbx = g_strdup(number); + DBG("Got voice mailbox number %s (%s)", number, name); + } + +done: + dbus_message_unref(reply); +} + +static void csd_init(void) +{ + dbus_uint32_t location; + uint8_t pb_type, location_type; + int ret; + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "GetCallInfoAll", + call_info_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to sent GetCallInfoAll method call"); + return; + } + + get_calls_active = TRUE; + + pb_type = SIM_PHONEBOOK_TYPE_MSISDN; + location = PHONEBOOK_INDEX_FIRST_ENTRY; + location_type = SIM_PHONEBOOK_LOCATION_NEXT; + + ret = send_method_call(SIM_PHONEBOOK_BUS_NAME, SIM_PHONEBOOK_PATH, + SIM_PHONEBOOK_INTERFACE, "read", + phonebook_read_reply, &msisdn, + DBUS_TYPE_BYTE, &pb_type, + DBUS_TYPE_INT32, &location, + DBUS_TYPE_BYTE, &location_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " SIM_PHONEBOOK_INTERFACE ".read()"); + return; + } + + pb_type = SIM_PHONEBOOK_TYPE_MBDN; + location = PHONEBOOK_INDEX_FIRST_ENTRY; + location_type = SIM_PHONEBOOK_LOCATION_NEXT; + + ret = send_method_call(SIM_PHONEBOOK_BUS_NAME, SIM_PHONEBOOK_PATH, + SIM_PHONEBOOK_INTERFACE, "read", + phonebook_read_reply, &vmbx, + DBUS_TYPE_BYTE, &pb_type, + DBUS_TYPE_INT32, &location, + DBUS_TYPE_BYTE, &location_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " SIM_PHONEBOOK_INTERFACE ".read()"); + return; + } +} + +static uint32_t get_callflag(const char *callerid_setting) +{ + if (callerid_setting != NULL) { + if (g_str_equal(callerid_setting, "allowed")) + return CALL_FLAG_PRESENTATION_ALLOWED; + else if (g_str_equal(callerid_setting, "restricted")) + return CALL_FLAG_PRESENTATION_RESTRICTED; + else + return CALL_FLAG_NONE; + } else + return CALL_FLAG_NONE; +} + +static void generate_flag_file(const char *filename) +{ + int fd; + + if (g_file_test(ALLOWED_FLAG_FILE, G_FILE_TEST_EXISTS) || + g_file_test(RESTRICTED_FLAG_FILE, G_FILE_TEST_EXISTS) || + g_file_test(NONE_FLAG_FILE, G_FILE_TEST_EXISTS)) + return; + + fd = open(filename, O_WRONLY | O_CREAT, 0); + if (fd >= 0) + close(fd); +} + +static void save_callerid_to_file(const char *callerid_setting) +{ + char callerid_file[FILENAME_MAX]; + + snprintf(callerid_file, sizeof(callerid_file), "%s%s", + CALLERID_BASE, callerid_setting); + + if (g_file_test(ALLOWED_FLAG_FILE, G_FILE_TEST_EXISTS)) + rename(ALLOWED_FLAG_FILE, callerid_file); + else if (g_file_test(RESTRICTED_FLAG_FILE, G_FILE_TEST_EXISTS)) + rename(RESTRICTED_FLAG_FILE, callerid_file); + else if (g_file_test(NONE_FLAG_FILE, G_FILE_TEST_EXISTS)) + rename(NONE_FLAG_FILE, callerid_file); + else + generate_flag_file(callerid_file); +} + +static uint32_t callerid_from_file(void) +{ + if (g_file_test(ALLOWED_FLAG_FILE, G_FILE_TEST_EXISTS)) + return CALL_FLAG_PRESENTATION_ALLOWED; + else if (g_file_test(RESTRICTED_FLAG_FILE, G_FILE_TEST_EXISTS)) + return CALL_FLAG_PRESENTATION_RESTRICTED; + else if (g_file_test(NONE_FLAG_FILE, G_FILE_TEST_EXISTS)) + return CALL_FLAG_NONE; + else + return CALL_FLAG_NONE; +} + +static DBusMessage *set_callerid(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *callerid_setting; + + if (dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, + &callerid_setting, + DBUS_TYPE_INVALID) == FALSE) + return btd_error_invalid_args(msg); + + if (g_str_equal(callerid_setting, "allowed") || + g_str_equal(callerid_setting, "restricted") || + g_str_equal(callerid_setting, "none")) { + save_callerid_to_file(callerid_setting); + callerid = get_callflag(callerid_setting); + DBG("telephony-maemo setting callerid flag: %s", + callerid_setting); + return dbus_message_new_method_return(msg); + } + + error("telephony-maemo: invalid argument %s for method call" + " SetCallerId", callerid_setting); + return btd_error_invalid_args(msg); +} + +static GDBusMethodTable telephony_maemo_methods[] = { + {"SetCallerId", "s", "", set_callerid, + G_DBUS_METHOD_FLAG_ASYNC}, + { } +}; + +static void handle_modem_state(DBusMessage *msg) +{ + const char *state; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &state, + DBUS_TYPE_INVALID)) { + error("Unexpected modem state parameters"); + return; + } + + DBG("SSC modem state: %s", state); + + if (calls != NULL || get_calls_active) + return; + + if (g_str_equal(state, "cmt_ready") || g_str_equal(state, "online")) + csd_init(); +} + +static void modem_state_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError err; + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("get_modem_status: %s, %s", err.name, err.message); + dbus_error_free(&err); + } else + handle_modem_state(reply); + + dbus_message_unref(reply); +} + +static DBusHandlerResult signal_filter(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path = dbus_message_get_path(msg); + + if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_SIGNAL) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Coming")) + handle_incoming_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Created")) + handle_outgoing_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, + "CreateRequested")) + handle_create_requested(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INSTANCE, "CallStatus")) + handle_call_status(msg, path); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Joined")) + handle_conference(msg, TRUE); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Left")) + handle_conference(msg, FALSE); + else if (dbus_message_is_signal(msg, NETWORK_INTERFACE, + "registration_status_change")) + handle_registration_status_change(msg); + else if (dbus_message_is_signal(msg, NETWORK_INTERFACE, + "signal_strength_change")) + handle_signal_strength_change(msg); + else if (dbus_message_is_signal(msg, "org.freedesktop.Hal.Device", + "PropertyModified")) + handle_hal_property_modified(msg); + else if (dbus_message_is_signal(msg, SSC_DBUS_IFACE, + "modem_state_changed_ind")) + handle_modem_state(msg); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +int telephony_init(void) +{ + const char *battery_cap = "battery"; + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_INBAND_RINGTONE | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_ENHANCED_CALL_CONTROL | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES | + AG_FEATURE_THREE_WAY_CALLING; + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + if (!dbus_connection_add_filter(connection, signal_filter, + NULL, NULL)) + error("Can't add signal filter"); + + dbus_bus_add_match(connection, + "type=signal,interface=" CSD_CALL_INTERFACE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" CSD_CALL_INSTANCE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" CSD_CALL_CONFERENCE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" NETWORK_INTERFACE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" SSC_DBUS_IFACE + ",member=modem_state_changed_ind", NULL); + + if (send_method_call(SSC_DBUS_NAME, SSC_DBUS_PATH, SSC_DBUS_IFACE, + "get_modem_state", modem_state_reply, + NULL, DBUS_TYPE_INVALID) < 0) + error("Unable to send " SSC_DBUS_IFACE ".get_modem_state()"); + + generate_flag_file(NONE_FLAG_FILE); + callerid = callerid_from_file(); + + if (!g_dbus_register_interface(connection, TELEPHONY_MAEMO_PATH, + TELEPHONY_MAEMO_INTERFACE, telephony_maemo_methods, + NULL, NULL, NULL, NULL)) { + error("telephony-maemo interface %s init failed on path %s", + TELEPHONY_MAEMO_INTERFACE, TELEPHONY_MAEMO_PATH); + } + + DBG("telephony-maemo registering %s interface on path %s", + TELEPHONY_MAEMO_INTERFACE, TELEPHONY_MAEMO_PATH); + + telephony_ready_ind(features, maemo_indicators, BTRH_NOT_SUPPORTED, + chld_str); + if (send_method_call("org.freedesktop.Hal", + "/org/freedesktop/Hal/Manager", + "org.freedesktop.Hal.Manager", + "FindDeviceByCapability", + hal_find_device_reply, NULL, + DBUS_TYPE_STRING, &battery_cap, + DBUS_TYPE_INVALID) < 0) + error("Unable to send HAL method call"); + + return 0; +} + +void telephony_exit(void) +{ + g_slist_foreach(calls, (GFunc) csd_call_free, NULL); + g_slist_free(calls); + calls = NULL; + + dbus_connection_remove_filter(connection, signal_filter, NULL); + + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-maemo6.c b/audio/telephony-maemo6.c new file mode 100644 index 0000000..0cef7dd --- /dev/null +++ b/audio/telephony-maemo6.c @@ -0,0 +1,1993 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2008-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdint.h> +#include <string.h> +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "telephony.h" +#include "error.h" + +/* SSC D-Bus definitions */ +#define SSC_DBUS_NAME "com.nokia.phone.SSC" +#define SSC_DBUS_IFACE "com.nokia.phone.SSC" +#define SSC_DBUS_PATH "/com/nokia/phone/SSC" + +/* libcsnet D-Bus definitions */ +#define CSD_CSNET_BUS_NAME "com.nokia.csd.CSNet" +#define CSD_CSNET_PATH "/com/nokia/csd/csnet" +#define CSD_CSNET_IFACE "com.nokia.csd.CSNet" +#define CSD_CSNET_REGISTRATION "com.nokia.csd.CSNet.NetworkRegistration" +#define CSD_CSNET_OPERATOR "com.nokia.csd.CSNet.NetworkOperator" +#define CSD_CSNET_SIGNAL "com.nokia.csd.CSNet.SignalStrength" + +enum net_registration_status { + NETWORK_REG_STATUS_HOME, + NETWORK_REG_STATUS_ROAMING, + NETWORK_REG_STATUS_OFFLINE, + NETWORK_REG_STATUS_SEARCHING, + NETWORK_REG_STATUS_NO_SIM, + NETWORK_REG_STATUS_POWEROFF, + NETWORK_REG_STATUS_POWERSAFE, + NETWORK_REG_STATUS_NO_COVERAGE, + NETWORK_REG_STATUS_REJECTED, + NETWORK_REG_STATUS_UNKOWN +}; + +/* CSD CALL plugin D-Bus definitions */ +#define CSD_CALL_BUS_NAME "com.nokia.csd.Call" +#define CSD_CALL_INTERFACE "com.nokia.csd.Call" +#define CSD_CALL_INSTANCE "com.nokia.csd.Call.Instance" +#define CSD_CALL_CONFERENCE "com.nokia.csd.Call.Conference" +#define CSD_CALL_PATH "/com/nokia/csd/call" +#define CSD_CALL_CONFERENCE_PATH "/com/nokia/csd/call/conference" + +/* Call status values as exported by the CSD CALL plugin */ +#define CSD_CALL_STATUS_IDLE 0 +#define CSD_CALL_STATUS_CREATE 1 +#define CSD_CALL_STATUS_COMING 2 +#define CSD_CALL_STATUS_PROCEEDING 3 +#define CSD_CALL_STATUS_MO_ALERTING 4 +#define CSD_CALL_STATUS_MT_ALERTING 5 +#define CSD_CALL_STATUS_WAITING 6 +#define CSD_CALL_STATUS_ANSWERED 7 +#define CSD_CALL_STATUS_ACTIVE 8 +#define CSD_CALL_STATUS_MO_RELEASE 9 +#define CSD_CALL_STATUS_MT_RELEASE 10 +#define CSD_CALL_STATUS_HOLD_INITIATED 11 +#define CSD_CALL_STATUS_HOLD 12 +#define CSD_CALL_STATUS_RETRIEVE_INITIATED 13 +#define CSD_CALL_STATUS_RECONNECT_PENDING 14 +#define CSD_CALL_STATUS_TERMINATED 15 +#define CSD_CALL_STATUS_SWAP_INITIATED 16 + +#define CALL_FLAG_NONE 0 +#define CALL_FLAG_PRESENTATION_ALLOWED 0x01 +#define CALL_FLAG_PRESENTATION_RESTRICTED 0x02 + +/* SIM Phonebook D-Bus definitions */ +#define CSD_SIMPB_BUS_NAME "com.nokia.csd.SIM" +#define CSD_SIMPB_INTERFACE "com.nokia.csd.SIM.Phonebook" +#define CSD_SIMPB_PATH "/com/nokia/csd/sim/phonebook" + +#define CSD_SIMPB_TYPE_ADN "ADN" +#define CSD_SIMPB_TYPE_FDN "FDN" +#define CSD_SIMPB_TYPE_SDN "SDN" +#define CSD_SIMPB_TYPE_VMBX "VMBX" +#define CSD_SIMPB_TYPE_MBDN "MBDN" +#define CSD_SIMPB_TYPE_EN "EN" +#define CSD_SIMPB_TYPE_MSISDN "MSISDN" + +struct csd_call { + char *object_path; + int status; + gboolean originating; + gboolean emergency; + gboolean on_hold; + gboolean conference; + char *number; + gboolean setup; +}; + +static struct { + char *operator_name; + uint8_t status; + int32_t signal_bars; +} net = { + .operator_name = NULL, + .status = NETWORK_REG_STATUS_UNKOWN, + /* Init as 0 meaning inactive mode. In modem power off state + * can be be -1, but we treat all values as 0s regardless + * inactive or power off. */ + .signal_bars = 0, +}; + +struct pending_req { + DBusPendingCall *call; + void *user_data; +}; + +static int get_property(const char *iface, const char *prop); + +static DBusConnection *connection = NULL; + +static GSList *calls = NULL; +static GSList *watches = NULL; +static GSList *pending = NULL; + +/* Reference count for determining the call indicator status */ +static GSList *active_calls = NULL; + +static char *msisdn = NULL; /* Subscriber number */ +static char *vmbx = NULL; /* Voice mailbox number */ + +/* HAL battery namespace key values */ +static int battchg_cur = -1; /* "battery.charge_level.current" */ +static int battchg_last = -1; /* "battery.charge_level.last_full" */ +static int battchg_design = -1; /* "battery.charge_level.design" */ + +static gboolean get_calls_active = FALSE; + +static gboolean events_enabled = FALSE; + +/* Supported set of call hold operations */ +static const char *chld_str = "0,1,1x,2,2x,3,4"; + +/* Timer for tracking call creation requests */ +static guint create_request_timer = 0; + +static struct indicator maemo_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + /* signal strength in terms of bars */ + { "signal", "0-5", 0, TRUE }, + { "service", "0,1", 0, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static char *call_status_str[] = { + "IDLE", + "CREATE", + "COMING", + "PROCEEDING", + "MO_ALERTING", + "MT_ALERTING", + "WAITING", + "ANSWERED", + "ACTIVE", + "MO_RELEASE", + "MT_RELEASE", + "HOLD_INITIATED", + "HOLD", + "RETRIEVE_INITIATED", + "RECONNECT_PENDING", + "TERMINATED", + "SWAP_INITIATED", + "???" +}; + +static struct csd_call *find_call(const char *path) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (g_str_equal(call->object_path, path)) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_held_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + if (call->status != CSD_CALL_STATUS_HOLD) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_idle_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status != CSD_CALL_STATUS_IDLE) + return call; + } + + return NULL; +} + +static struct csd_call *find_call_with_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + return call; + } + + return NULL; +} + +static int release_conference(void) +{ + DBusMessage *msg; + + DBG("telephony-maemo6: releasing conference call"); + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + CSD_CALL_CONFERENCE_PATH, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int release_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int answer_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Answer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int split_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Split"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int unhold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Unhold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int hold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Hold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int swap_calls(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Swap"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int create_conference(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Conference"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int call_transfer(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Transfer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +void telephony_device_connected(void *telephony_device) +{ + struct csd_call *coming; + + DBG("telephony-maemo6: device %p connected", telephony_device); + + coming = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (coming) { + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(coming->number, + number_type(coming->number)); + else + telephony_incoming_call_ind(coming->number, + number_type(coming->number)); + } +} + +static void pending_req_finalize(struct pending_req *req) +{ + if (!dbus_pending_call_get_completed(req->call)) + dbus_pending_call_cancel(req->call); + + dbus_pending_call_unref(req->call); + g_free(req); +} + +static void remove_pending_by_data(gpointer data, gpointer user_data) +{ + struct pending_req *req = data; + + if (req->user_data == user_data) { + pending = g_slist_remove(pending, req); + pending_req_finalize(req); + } +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-maemo6: device %p disconnected", telephony_device); + events_enabled = FALSE; + + g_slist_foreach(pending, remove_pending_by_data, telephony_device); +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_terminate_call_req(void *telephony_device) +{ + struct csd_call *call; + struct csd_call *alerting; + int err; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + if (!call) + call = find_non_idle_call(); + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + alerting = find_call_with_status(CSD_CALL_STATUS_MO_ALERTING); + if (call->on_hold && alerting) + err = release_call(alerting); + else if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct csd_call *call; + + call = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (!call) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (answer_call(call) < 0) + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); +} + +static int send_method_call(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + struct pending_req *req; + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + + req = g_new0(struct pending_req, 1); + req->call = call; + req->user_data = user_data; + + pending = g_slist_prepend(pending, req); + dbus_message_unref(msg); + + return 0; +} + +static struct pending_req *find_request(const DBusPendingCall *call) +{ + GSList *l; + + for (l = pending; l; l = l->next) { + struct pending_req *req = l->data; + + if (req->call == call) + return req; + } + + return NULL; +} + +static void remove_pending(DBusPendingCall *call) +{ + struct pending_req *req = find_request(call); + + pending = g_slist_remove(pending, req); + pending_req_finalize(req); +} + +static void last_number_call_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + void *telephony_device = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + } else + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); + + dbus_message_unref(reply); + remove_pending(call); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + int ret; + + DBG("telephony-maemo6: last dialed number request"); + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "CreateFromLast", + last_number_call_reply, telephony_device, + DBUS_TYPE_INVALID); + if (ret < 0) + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); +} + +static const char *memory_dial_lookup(int location) +{ + if (location == 1) + return vmbx; + else + return NULL; +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + int ret; + + DBG("telephony-maemo6: dial request to %s", number); + + if (strncmp(number, "*31#", 4) == 0) + number += 4; + else if (strncmp(number, "#31#", 4) == 0) + number += 4; + else if (number[0] == '>') { + const char *location = &number[1]; + + number = memory_dial_lookup(strtol(&number[1], NULL, 0)); + if (!number) { + error("No number at memory location %s", location); + telephony_dial_number_rsp(telephony_device, + CME_ERROR_INVALID_INDEX); + return; + } + } + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "Create", + NULL, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + int ret; + char buf[2] = { tone, '\0' }, *buf_ptr = buf; + + DBG("telephony-maemo6: transmit dtmf: %s", buf); + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "SendDTMF", + NULL, NULL, + DBUS_TYPE_STRING, &buf_ptr, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-maemo6: subscriber number request"); + if (msisdn) + telephony_subscriber_number_ind(msisdn, + number_type(msisdn), + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +static int csd_status_to_hfp(struct csd_call *call) +{ + switch (call->status) { + case CSD_CALL_STATUS_IDLE: + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + case CSD_CALL_STATUS_TERMINATED: + return -1; + case CSD_CALL_STATUS_CREATE: + return CALL_STATUS_DIALING; + case CSD_CALL_STATUS_WAITING: + return CALL_STATUS_WAITING; + case CSD_CALL_STATUS_PROCEEDING: + /* PROCEEDING can happen in outgoing/incoming */ + if (call->originating) + return CALL_STATUS_DIALING; + else + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_COMING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_MO_ALERTING: + return CALL_STATUS_ALERTING; + case CSD_CALL_STATUS_MT_ALERTING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_ANSWERED: + case CSD_CALL_STATUS_ACTIVE: + case CSD_CALL_STATUS_RECONNECT_PENDING: + case CSD_CALL_STATUS_SWAP_INITIATED: + case CSD_CALL_STATUS_HOLD_INITIATED: + return CALL_STATUS_ACTIVE; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + case CSD_CALL_STATUS_HOLD: + return CALL_STATUS_HELD; + default: + return -1; + } +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-maemo6: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct csd_call *call = l->data; + int status, direction, multiparty; + + status = csd_status_to_hfp(call); + if (status < 0) + continue; + + direction = call->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = call->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + telephony_list_current_call_ind(i, direction, status, + CALL_MODE_VOICE, multiparty, + call->number, + number_type(call->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + net.operator_name ? net.operator_name : ""); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +static void foreach_call_with_status(int status, + int (*func)(struct csd_call *call)) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + func(call); + } +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct csd_call *call; + int err = 0; + + DBG("telephony-maemo6: got call hold request %s", cmd); + + if (strlen(cmd) > 1) + idx = &cmd[1]; + else + idx = NULL; + + if (idx) + call = g_slist_nth_data(calls, strtol(idx, NULL, 0) - 1); + else + call = NULL; + + switch (cmd[0]) { + case '0': + if (find_call_with_status(CSD_CALL_STATUS_WAITING)) + foreach_call_with_status(CSD_CALL_STATUS_WAITING, + release_call); + else + foreach_call_with_status(CSD_CALL_STATUS_HOLD, + release_call); + break; + case '1': + if (idx) { + if (call) + err = release_call(call); + break; + } + foreach_call_with_status(CSD_CALL_STATUS_ACTIVE, release_call); + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + if (call) + err = answer_call(call); + break; + case '2': + if (idx) { + if (call) + err = split_call(call); + } else { + struct csd_call *held, *wait; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + held = find_call_with_status(CSD_CALL_STATUS_HOLD); + wait = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (wait) + err = answer_call(wait); + else if (call && held) + err = swap_calls(); + else { + if (call) + err = hold_call(call); + if (held) + err = unhold_call(held); + } + } + break; + case '3': + if (find_call_with_status(CSD_CALL_STATUS_HOLD) || + find_call_with_status(CSD_CALL_STATUS_WAITING)) + err = create_conference(); + break; + case '4': + err = call_transfer(); + break; + default: + DBG("Unknown call hold request"); + break; + } + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo6: got %s NR and EC request", + enable ? "enable" : "disable"); + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct csd_call *active, *waiting; + int err; + + DBG("telephony-maemo6: got key press request for %s", keys); + + waiting = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + active = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + + if (waiting) + err = answer_call(waiting); + else if (active) + err = release_call(active); + else + err = 0; + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo6: got %s voice dial request", + enable ? "enable" : "disable"); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NOT_SUPPORTED); +} + +static void handle_incoming_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Coming() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Incoming call to %s from number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE) || + find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_call_waiting_ind(call->number, + number_type(call->number)); + else + telephony_incoming_call_ind(call->number, + number_type(call->number)); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INCOMING); +} + +static void handle_outgoing_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Created() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Outgoing call from %s to number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + if (create_request_timer) { + g_source_remove(create_request_timer); + create_request_timer = 0; + } +} + +static gboolean create_timeout(gpointer user_data) +{ + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + create_request_timer = 0; + return FALSE; +} + +static void handle_create_requested(DBusMessage *msg) +{ + DBG("Call.CreateRequested()"); + + if (create_request_timer) + g_source_remove(create_request_timer); + + create_request_timer = g_timeout_add_seconds(5, create_timeout, NULL); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); +} + +static void call_set_status(struct csd_call *call, dbus_uint32_t status) +{ + dbus_uint32_t prev_status; + int callheld = telephony_get_indicator(maemo_indicators, "callheld"); + + prev_status = call->status; + DBG("Call %s changed from %s to %s", call->object_path, + call_status_str[prev_status], call_status_str[status]); + + if (prev_status == status) { + DBG("Ignoring CSD Call state change to existing state"); + return; + } + + call->status = (int) status; + + switch (status) { + case CSD_CALL_STATUS_IDLE: + if (call->setup) { + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + } + + g_free(call->number); + call->number = NULL; + call->originating = FALSE; + call->emergency = FALSE; + call->on_hold = FALSE; + call->conference = FALSE; + call->setup = FALSE; + break; + case CSD_CALL_STATUS_CREATE: + call->originating = TRUE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_COMING: + call->originating = FALSE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_PROCEEDING: + break; + case CSD_CALL_STATUS_MO_ALERTING: + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CSD_CALL_STATUS_MT_ALERTING: + /* Some headsets expect incoming call notification before they + * can send ATA command. When call changed status from waiting + * to alerting we need to send missing notification. Otherwise + * headsets like Nokia BH-108 or BackBeat 903 are unable to + * answer incoming call that was previously waiting. */ + if (prev_status == CSD_CALL_STATUS_WAITING) + telephony_incoming_call_ind(call->number, + number_type(call->number)); + break; + case CSD_CALL_STATUS_WAITING: + break; + case CSD_CALL_STATUS_ANSWERED: + break; + case CSD_CALL_STATUS_ACTIVE: + if (call->on_hold) { + call->on_hold = FALSE; + if (find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + } else { + if (!g_slist_find(active_calls, call)) + active_calls = g_slist_prepend(active_calls, call); + if (g_slist_length(active_calls) == 1) + telephony_update_indicator(maemo_indicators, + "call", + EV_CALL_ACTIVE); + /* Upgrade callheld status if necessary */ + if (callheld == EV_CALLHELD_ON_HOLD) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + call->setup = FALSE; + } + break; + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + active_calls = g_slist_remove(active_calls, call); + if (g_slist_length(active_calls) == 0) + telephony_update_indicator(maemo_indicators, "call", + EV_CALL_INACTIVE); + break; + case CSD_CALL_STATUS_HOLD_INITIATED: + break; + case CSD_CALL_STATUS_HOLD: + call->on_hold = TRUE; + if (find_non_held_call()) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + break; + case CSD_CALL_STATUS_RECONNECT_PENDING: + break; + case CSD_CALL_STATUS_TERMINATED: + if (call->on_hold && + !find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + else if (callheld == EV_CALLHELD_MULTIPLE && + find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_SWAP_INITIATED: + break; + default: + error("Unknown call status %u", status); + break; + } +} + +static void handle_call_status(DBusMessage *msg, const char *call_path) +{ + struct csd_call *call; + dbus_uint32_t status, cause_type, cause; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_UINT32, &cause_type, + DBUS_TYPE_UINT32, &cause, + DBUS_TYPE_INVALID)) { + error("Unexpected paramters in Instance.CallStatus() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + if (status > 16) { + error("Invalid call status %u", status); + return; + } + + call_set_status(call, status); +} + +static void handle_conference(DBusMessage *msg, gboolean joined) +{ + const char *path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Conference.%s", + dbus_message_get_member(msg)); + return; + } + + call = find_call(path); + if (!call) { + error("Conference signal for unknown call %s", path); + return; + } + + DBG("Call %s %s the conference", path, joined ? "joined" : "left"); + + call->conference = joined; +} + +static uint8_t str2status(const char *state) +{ + if (g_strcmp0(state, "Home") == 0) + return NETWORK_REG_STATUS_HOME; + else if (g_strcmp0(state, "Roaming") == 0) + return NETWORK_REG_STATUS_ROAMING; + else if (g_strcmp0(state, "Offline") == 0) + return NETWORK_REG_STATUS_OFFLINE; + else if (g_strcmp0(state, "Searching") == 0) + return NETWORK_REG_STATUS_SEARCHING; + else if (g_strcmp0(state, "NoSim") == 0) + return NETWORK_REG_STATUS_NO_SIM; + else if (g_strcmp0(state, "Poweroff") == 0) + return NETWORK_REG_STATUS_POWEROFF; + else if (g_strcmp0(state, "Powersafe") == 0) + return NETWORK_REG_STATUS_POWERSAFE; + else if (g_strcmp0(state, "NoCoverage") == 0) + return NETWORK_REG_STATUS_NO_COVERAGE; + else if (g_strcmp0(state, "Reject") == 0) + return NETWORK_REG_STATUS_REJECTED; + else + return NETWORK_REG_STATUS_UNKOWN; +} + +static void update_registration_status(const char *status) +{ + uint8_t new_status; + + new_status = str2status(status); + + if (net.status == new_status) + return; + + switch (new_status) { + case NETWORK_REG_STATUS_HOME: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_INACTIVE); + if (net.status > NETWORK_REG_STATUS_ROAMING) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_ROAMING: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_ACTIVE); + if (net.status > NETWORK_REG_STATUS_ROAMING) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_OFFLINE: + case NETWORK_REG_STATUS_SEARCHING: + case NETWORK_REG_STATUS_NO_SIM: + case NETWORK_REG_STATUS_POWEROFF: + case NETWORK_REG_STATUS_POWERSAFE: + case NETWORK_REG_STATUS_NO_COVERAGE: + case NETWORK_REG_STATUS_REJECTED: + case NETWORK_REG_STATUS_UNKOWN: + if (net.status < NETWORK_REG_STATUS_OFFLINE) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_NONE); + break; + } + + net.status = new_status; + + DBG("telephony-maemo6: registration status changed: %s", status); +} + +static void handle_registration_changed(DBusMessage *msg) +{ + const char *status; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &status, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in RegistrationChanged"); + return; + } + + update_registration_status(status); +} + +static void update_signal_strength(int32_t signal_bars) +{ + if (signal_bars < 0) { + DBG("signal strength smaller than expected: %d < 0", + signal_bars); + signal_bars = 0; + } else if (signal_bars > 5) { + DBG("signal strength greater than expected: %d > 5", + signal_bars); + signal_bars = 5; + } + + if (net.signal_bars == signal_bars) + return; + + telephony_update_indicator(maemo_indicators, "signal", signal_bars); + + net.signal_bars = signal_bars; + DBG("telephony-maemo6: signal strength updated: %d/5", signal_bars); +} + +static void handle_signal_bars_changed(DBusMessage *msg) +{ + int32_t signal_bars; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_INT32, &signal_bars, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in SignalBarsChanged"); + return; + } + + update_signal_strength(signal_bars); +} + +static gboolean iter_get_basic_args(DBusMessageIter *iter, + int first_arg_type, ...) +{ + int type; + va_list ap; + + va_start(ap, first_arg_type); + + for (type = first_arg_type; type != DBUS_TYPE_INVALID; + type = va_arg(ap, int)) { + void *value = va_arg(ap, void *); + int real_type = dbus_message_iter_get_arg_type(iter); + + if (real_type != type) { + error("iter_get_basic_args: expected %c but got %c", + (char) type, (char) real_type); + break; + } + + dbus_message_iter_get_basic(iter, value); + dbus_message_iter_next(iter); + } + + va_end(ap); + + return type == DBUS_TYPE_INVALID ? TRUE : FALSE; +} + +static void hal_battery_level_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + dbus_int32_t level; + int *value = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + if (!dbus_message_get_args(reply, NULL, + DBUS_TYPE_INT32, &level, + DBUS_TYPE_INVALID)) { + error("Unexpected args in hald reply"); + goto done; + } + + *value = (int) level; + + if (value == &battchg_last) + DBG("telephony-maemo6: battery.charge_level.last_full is %d", + *value); + else if (value == &battchg_design) + DBG("telephony-maemo6: battery.charge_level.design is %d", + *value); + else + DBG("telephony-maemo6: battery.charge_level.current is %d", + *value); + + if ((battchg_design > 0 || battchg_last > 0) && battchg_cur >= 0) { + int new, max; + + if (battchg_last > 0) + max = battchg_last; + else + max = battchg_design; + + new = battchg_cur * 5 / max; + + telephony_update_indicator(maemo_indicators, "battchg", new); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void hal_get_integer(const char *path, const char *key, void *user_data) +{ + send_method_call("org.freedesktop.Hal", path, + "org.freedesktop.Hal.Device", + "GetPropertyInteger", + hal_battery_level_reply, user_data, + DBUS_TYPE_STRING, &key, + DBUS_TYPE_INVALID); +} + +static void handle_hal_property_modified(DBusMessage *msg) +{ + DBusMessageIter iter, array; + dbus_int32_t num_changes; + const char *path; + + path = dbus_message_get_path(msg); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_get_basic(&iter, &num_changes); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_recurse(&iter, &array); + + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) { + DBusMessageIter prop; + const char *name; + dbus_bool_t added, removed; + + dbus_message_iter_recurse(&array, &prop); + + if (!iter_get_basic_args(&prop, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_BOOLEAN, &added, + DBUS_TYPE_BOOLEAN, &removed, + DBUS_TYPE_INVALID)) { + error("Invalid hal PropertyModified parameters"); + break; + } + + if (g_str_equal(name, "battery.charge_level.last_full")) + hal_get_integer(path, name, &battchg_last); + else if (g_str_equal(name, "battery.charge_level.current")) + hal_get_integer(path, name, &battchg_cur); + else if (g_str_equal(name, "battery.charge_level.design")) + hal_get_integer(path, name, &battchg_design); + + dbus_message_iter_next(&array); + } +} + +static void csd_call_free(struct csd_call *call) +{ + if (!call) + return; + + g_free(call->object_path); + g_free(call->number); + + g_free(call); +} + +static void parse_call_list(DBusMessageIter *iter) +{ + do { + DBusMessageIter call_iter; + struct csd_call *call; + const char *object_path, *number; + dbus_uint32_t status; + dbus_bool_t originating, terminating, emerg, on_hold, conf; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) { + error("Unexpected signature in GetCallInfoAll reply"); + break; + } + + dbus_message_iter_recurse(iter, &call_iter); + + if (!iter_get_basic_args(&call_iter, + DBUS_TYPE_OBJECT_PATH, &object_path, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_BOOLEAN, &originating, + DBUS_TYPE_BOOLEAN, &terminating, + DBUS_TYPE_BOOLEAN, &emerg, + DBUS_TYPE_BOOLEAN, &on_hold, + DBUS_TYPE_BOOLEAN, &conf, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Parsing call D-Bus parameters failed"); + break; + } + + call = find_call(object_path); + if (!call) { + call = g_new0(struct csd_call, 1); + call->object_path = g_strdup(object_path); + calls = g_slist_append(calls, call); + DBG("telephony-maemo6: new csd call instance at %s", + object_path); + } + + if (status == CSD_CALL_STATUS_IDLE) + continue; + + /* CSD gives incorrect call_hold property sometimes */ + if ((call->status != CSD_CALL_STATUS_HOLD && on_hold) || + (call->status == CSD_CALL_STATUS_HOLD && + !on_hold)) { + error("Conflicting call status and on_hold property!"); + on_hold = call->status == CSD_CALL_STATUS_HOLD; + } + + call->originating = originating; + call->on_hold = on_hold; + call->conference = conf; + g_free(call->number); + call->number = g_strdup(number); + + /* Update indicators */ + call_set_status(call, status); + + } while (dbus_message_iter_next(iter)); +} + +static void update_operator_name(const char *name) +{ + if (name == NULL) + return; + + g_free(net.operator_name); + net.operator_name = g_strndup(name, 16); + DBG("telephony-maemo6: operator name updated: %s", name); +} + +static void get_property_reply(DBusPendingCall *call, void *user_data) +{ + char *prop = user_data; + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + error("Unexpected signature in Get return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + if (g_strcmp0(prop, "RegistrationStatus") == 0) { + const char *status; + + dbus_message_iter_get_basic(&sub, &status); + update_registration_status(status); + + get_property(CSD_CSNET_OPERATOR, "OperatorName"); + get_property(CSD_CSNET_SIGNAL, "SignalBars"); + } else if (g_strcmp0(prop, "OperatorName") == 0) { + const char *name; + + dbus_message_iter_get_basic(&sub, &name); + update_operator_name(name); + } else if (g_strcmp0(prop, "SignalBars") == 0) { + int32_t signal_bars; + + dbus_message_iter_get_basic(&sub, &signal_bars); + update_signal_strength(signal_bars); + } + +done: + g_free(prop); + dbus_message_unref(reply); + remove_pending(call); +} + +static int get_property(const char *iface, const char *prop) +{ + return send_method_call(CSD_CSNET_BUS_NAME, CSD_CSNET_PATH, + DBUS_INTERFACE_PROPERTIES, "Get", + get_property_reply, g_strdup(prop), + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &prop, + DBUS_TYPE_INVALID); +} + +static void handle_operator_name_changed(DBusMessage *msg) +{ + const char *name; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in OperatorNameChanged"); + return; + } + + update_operator_name(name); +} + +static void call_info_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub;; + + get_calls_active = FALSE; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in GetCallInfoAll return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + parse_call_list(&sub); + + get_property(CSD_CSNET_REGISTRATION, "RegistrationStatus"); + +done: + dbus_message_unref(reply); + remove_pending(call); +} + + +static void phonebook_read_reply(DBusPendingCall *call, void *user_data) +{ + DBusError derr; + DBusMessage *reply; + const char *name, *number, *secondname, *additionalnumber, *email; + int index; + char **number_type = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + error("%s.ReadFirst replied with an error: %s, %s", + CSD_SIMPB_INTERFACE, derr.name, derr.message); + dbus_error_free(&derr); + if (number_type == &vmbx) + vmbx = g_strdup(getenv("VMBX_NUMBER")); + goto done; + } + + dbus_error_init(&derr); + if (dbus_message_get_args(reply, NULL, + DBUS_TYPE_INT32, &index, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_STRING, &secondname, + DBUS_TYPE_STRING, &additionalnumber, + DBUS_TYPE_STRING, &email, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse %s.ReadFirst arguments: %s, %s", + CSD_SIMPB_INTERFACE, derr.name, derr.message); + dbus_error_free(&derr); + goto done; + } + + if (number_type == &msisdn) { + g_free(msisdn); + msisdn = g_strdup(number); + DBG("Got MSISDN %s (%s)", number, name); + } else { + g_free(vmbx); + vmbx = g_strdup(number); + DBG("Got voice mailbox number %s (%s)", number, name); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void csd_init(void) +{ + const char *pb_type; + int ret; + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "GetCallInfoAll", + call_info_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to sent GetCallInfoAll method call"); + return; + } + + get_calls_active = TRUE; + + pb_type = CSD_SIMPB_TYPE_MSISDN; + + ret = send_method_call(CSD_SIMPB_BUS_NAME, CSD_SIMPB_PATH, + CSD_SIMPB_INTERFACE, "ReadFirst", + phonebook_read_reply, &msisdn, + DBUS_TYPE_STRING, &pb_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " CSD_SIMPB_INTERFACE ".read()"); + return; + } + + /* Voicemail should be in MBDN index 0 */ + pb_type = CSD_SIMPB_TYPE_MBDN; + + ret = send_method_call(CSD_SIMPB_BUS_NAME, CSD_SIMPB_PATH, + CSD_SIMPB_INTERFACE, "ReadFirst", + phonebook_read_reply, &vmbx, + DBUS_TYPE_STRING, &pb_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " CSD_SIMPB_INTERFACE ".read()"); + return; + } +} + +static void handle_modem_state(DBusMessage *msg) +{ + const char *state; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &state, + DBUS_TYPE_INVALID)) { + error("Unexpected modem state parameters"); + return; + } + + DBG("SSC modem state: %s", state); + + if (calls != NULL || get_calls_active) + return; + + if (g_str_equal(state, "cmt_ready") || g_str_equal(state, "online")) + csd_init(); +} + +static void modem_state_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError err; + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("get_modem_state: %s, %s", err.name, err.message); + dbus_error_free(&err); + } else + handle_modem_state(reply); + + dbus_message_unref(reply); + remove_pending(call); +} + +static gboolean signal_filter(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *path = dbus_message_get_path(msg); + + if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Coming")) + handle_incoming_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Created")) + handle_outgoing_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, + "CreateRequested")) + handle_create_requested(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INSTANCE, "CallStatus")) + handle_call_status(msg, path); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Joined")) + handle_conference(msg, TRUE); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Left")) + handle_conference(msg, FALSE); + else if (dbus_message_is_signal(msg, CSD_CSNET_REGISTRATION, + "RegistrationChanged")) + handle_registration_changed(msg); + else if (dbus_message_is_signal(msg, CSD_CSNET_OPERATOR, + "OperatorNameChanged")) + handle_operator_name_changed(msg); + else if (dbus_message_is_signal(msg, CSD_CSNET_SIGNAL, + "SignalBarsChanged")) + handle_signal_bars_changed(msg); + else if (dbus_message_is_signal(msg, "org.freedesktop.Hal.Device", + "PropertyModified")) + handle_hal_property_modified(msg); + else if (dbus_message_is_signal(msg, SSC_DBUS_IFACE, + "modem_state_changed_ind")) + handle_modem_state(msg); + + return TRUE; +} + +static void add_watch(const char *sender, const char *path, + const char *interface, const char *member) +{ + guint watch; + + watch = g_dbus_add_signal_watch(connection, sender, path, interface, + member, signal_filter, NULL, NULL); + + watches = g_slist_prepend(watches, GUINT_TO_POINTER(watch)); +} + +static void hal_find_device_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + const char *path; + int type; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in FindDeviceByCapability return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + type = dbus_message_iter_get_arg_type(&sub); + + if (type != DBUS_TYPE_OBJECT_PATH && type != DBUS_TYPE_STRING) { + error("No hal device with battery capability found"); + goto done; + } + + dbus_message_iter_get_basic(&sub, &path); + + DBG("telephony-maemo6: found battery device at %s", path); + + add_watch(NULL, path, "org.freedesktop.Hal.Device", + "PropertyModified"); + + hal_get_integer(path, "battery.charge_level.last_full", &battchg_last); + hal_get_integer(path, "battery.charge_level.current", &battchg_cur); + hal_get_integer(path, "battery.charge_level.design", &battchg_design); + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +int telephony_init(void) +{ + const char *battery_cap = "battery"; + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_INBAND_RINGTONE | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_ENHANCED_CALL_CONTROL | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES | + AG_FEATURE_THREE_WAY_CALLING; + int i; + + DBG(""); + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + add_watch(NULL, NULL, CSD_CALL_INTERFACE, NULL); + add_watch(NULL, NULL, CSD_CALL_INSTANCE, NULL); + add_watch(NULL, NULL, CSD_CALL_CONFERENCE, NULL); + add_watch(NULL, NULL, CSD_CSNET_REGISTRATION, "RegistrationChanged"); + add_watch(NULL, NULL, CSD_CSNET_OPERATOR, "OperatorNameChanged"); + add_watch(NULL, NULL, CSD_CSNET_SIGNAL, "SignalBarsChanged"); + add_watch(NULL, NULL, SSC_DBUS_IFACE, "modem_state_changed_ind"); + + if (send_method_call(SSC_DBUS_NAME, SSC_DBUS_PATH, SSC_DBUS_IFACE, + "get_modem_state", modem_state_reply, + NULL, DBUS_TYPE_INVALID) < 0) + error("Unable to send " SSC_DBUS_IFACE ".get_modem_state()"); + + /* Reset indicators */ + for (i = 0; maemo_indicators[i].desc != NULL; i++) { + if (g_str_equal(maemo_indicators[i].desc, "battchg")) + maemo_indicators[i].val = 5; + else + maemo_indicators[i].val = 0; + } + + telephony_ready_ind(features, maemo_indicators, BTRH_NOT_SUPPORTED, + chld_str); + if (send_method_call("org.freedesktop.Hal", + "/org/freedesktop/Hal/Manager", + "org.freedesktop.Hal.Manager", + "FindDeviceByCapability", + hal_find_device_reply, NULL, + DBUS_TYPE_STRING, &battery_cap, + DBUS_TYPE_INVALID) < 0) + error("Unable to send HAL method call"); + + return 0; +} + +static void remove_watch(gpointer data) +{ + g_dbus_remove_watch(connection, GPOINTER_TO_UINT(data)); +} + +void telephony_exit(void) +{ + DBG(""); + + g_free(net.operator_name); + net.operator_name = NULL; + + net.status = NETWORK_REG_STATUS_UNKOWN; + net.signal_bars = 0; + + g_slist_free(active_calls); + active_calls = NULL; + + g_slist_foreach(calls, (GFunc) csd_call_free, NULL); + g_slist_free(calls); + calls = NULL; + + g_slist_foreach(pending, (GFunc) pending_req_finalize, NULL); + g_slist_free(pending); + pending = NULL; + + g_slist_foreach(watches, (GFunc) remove_watch, NULL); + g_slist_free(watches); + watches = NULL; + + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-ofono.c b/audio/telephony-ofono.c new file mode 100644 index 0000000..6f5685b --- /dev/null +++ b/audio/telephony-ofono.c @@ -0,0 +1,1627 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2009-2010 Intel Corporation + * Copyright (C) 2006-2009 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <stdint.h> +#include <glib.h> +#include <dbus/dbus.h> +#include <gdbus.h> + +#include "log.h" +#include "telephony.h" + +enum net_registration_status { + NETWORK_REG_STATUS_HOME = 0x00, + NETWORK_REG_STATUS_ROAM, + NETWORK_REG_STATUS_NOSERV +}; + +struct voice_call { + char *obj_path; + int status; + gboolean originating; + gboolean conference; + char *number; + guint watch; +}; + +static DBusConnection *connection = NULL; +static char *modem_obj_path = NULL; +static char *last_dialed_number = NULL; +static GSList *calls = NULL; +static GSList *watches = NULL; +static GSList *pending = NULL; + +#define OFONO_BUS_NAME "org.ofono" +#define OFONO_PATH "/" +#define OFONO_MODEM_INTERFACE "org.ofono.Modem" +#define OFONO_MANAGER_INTERFACE "org.ofono.Manager" +#define OFONO_NETWORKREG_INTERFACE "org.ofono.NetworkRegistration" +#define OFONO_VCMANAGER_INTERFACE "org.ofono.VoiceCallManager" +#define OFONO_VC_INTERFACE "org.ofono.VoiceCall" + +/* HAL battery namespace key values */ +static int battchg_cur = -1; /* "battery.charge_level.current" */ +static int battchg_last = -1; /* "battery.charge_level.last_full" */ +static int battchg_design = -1; /* "battery.charge_level.design" */ + +static struct { + uint8_t status; + uint32_t signals_bar; + char *operator_name; +} net = { + .status = NETWORK_REG_STATUS_NOSERV, + .signals_bar = 0, + .operator_name = NULL, +}; + +static const char *chld_str = "0,1,1x,2,2x,3,4"; +static char *subscriber_number = NULL; + +static gboolean events_enabled = FALSE; + +static struct indicator ofono_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + { "signal", "0-5", 5, TRUE }, + { "service", "0,1", 1, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static struct voice_call *find_vc(const char *path) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *vc = l->data; + + if (g_str_equal(vc->obj_path, path)) + return vc; + } + + return NULL; +} + +static struct voice_call *find_vc_with_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *vc = l->data; + + if (vc->status == status) + return vc; + } + + return NULL; +} + +static struct voice_call *find_vc_without_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *call = l->data; + + if (call->status != status) + return call; + } + + return NULL; +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +void telephony_device_connected(void *telephony_device) +{ + struct voice_call *coming; + + DBG("telephony-ofono: device %p connected", telephony_device); + + coming = find_vc_with_status(CALL_STATUS_ALERTING); + if (coming) { + if (find_vc_with_status(CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(coming->number, + number_type(coming->number)); + else + telephony_incoming_call_ind(coming->number, + number_type(coming->number)); + } +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-ofono: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + DBG("telephony-ofono: last dialed number request"); + + if (last_dialed_number) + telephony_dial_number_req(telephony_device, last_dialed_number); + else + telephony_last_dialed_number_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); +} + +static int send_method_call(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + pending = g_slist_prepend(pending, call); + dbus_message_unref(msg); + + return 0; +} + +static int answer_call(struct voice_call *vc) +{ + DBG("%s", vc->number); + return send_method_call(OFONO_BUS_NAME, vc->obj_path, + OFONO_VC_INTERFACE, "Answer", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int release_call(struct voice_call *vc) +{ + DBG("%s", vc->number); + return send_method_call(OFONO_BUS_NAME, vc->obj_path, + OFONO_VC_INTERFACE, "Hangup", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int release_answer_calls() +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "ReleaseAndAnswer", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int split_call(struct voice_call *call) +{ + DBG("%s", call->number); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "PrivateChat", + NULL, NULL, + DBUS_TYPE_OBJECT_PATH, + call->obj_path, + DBUS_TYPE_INVALID); + return -1; +} + +static int swap_calls(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "SwapCalls", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int create_conference(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "CreateMultiparty", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int release_conference(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "HangupMultiparty", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int call_transfer(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "Transfer", + NULL, NULL, DBUS_TYPE_INVALID); +} + +void telephony_terminate_call_req(void *telephony_device) +{ + struct voice_call *call; + struct voice_call *alerting; + int err; + + call = find_vc_with_status(CALL_STATUS_ACTIVE); + if (!call) + call = calls->data; + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + alerting = find_vc_with_status(CALL_STATUS_ALERTING); + if (call->status == CALL_STATUS_HELD && alerting) + err = release_call(alerting); + else if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct voice_call *vc; + int ret; + + vc = find_vc_with_status(CALL_STATUS_INCOMING); + if (!vc) + vc = find_vc_with_status(CALL_STATUS_ALERTING); + + if (!vc) + vc = find_vc_with_status(CALL_STATUS_WAITING); + + if (!vc) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + ret = answer_call(vc); + if (ret < 0) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + const char *clir; + int ret; + + DBG("telephony-ofono: dial request to %s", number); + + if (!modem_obj_path) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + if (!strncmp(number, "*31#", 4)) { + number += 4; + clir = "enabled"; + } else if (!strncmp(number, "#31#", 4)) { + number += 4; + clir = "disabled"; + } else + clir = "default"; + + ret = send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "Dial", NULL, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_STRING, &clir, + DBUS_TYPE_INVALID); + + if (ret < 0) + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + char *tone_string; + int ret; + + DBG("telephony-ofono: transmit dtmf: %c", tone); + + if (!modem_obj_path) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + tone_string = g_strdup_printf("%c", tone); + ret = send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "SendTones", NULL, NULL, + DBUS_TYPE_STRING, &tone_string, + DBUS_TYPE_INVALID); + g_free(tone_string); + + if (ret < 0) + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-ofono: subscriber number request"); + + if (subscriber_number) + telephony_subscriber_number_ind(subscriber_number, + NUMBER_TYPE_TELEPHONY, + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-ofono: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct voice_call *vc = l->data; + int direction, multiparty; + + direction = vc->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = vc->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + DBG("call %s direction %d multiparty %d", vc->number, + direction, multiparty); + + telephony_list_current_call_ind(i, direction, vc->status, + CALL_MODE_VOICE, multiparty, + vc->number, number_type(vc->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + DBG("telephony-ofono: operator selection request"); + + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + net.operator_name ? net.operator_name : ""); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +static void foreach_vc_with_status(int status, + int (*func)(struct voice_call *vc)) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *call = l->data; + + if (call->status == status) + func(call); + } +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct voice_call *call; + int err = 0; + + DBG("telephony-ofono: got call hold request %s", cmd); + + if (strlen(cmd) > 1) + idx = &cmd[1]; + else + idx = NULL; + + if (idx) + call = g_slist_nth_data(calls, strtol(idx, NULL, 0) - 1); + else + call = NULL; + + switch (cmd[0]) { + case '0': + if (find_vc_with_status(CALL_STATUS_WAITING)) + foreach_vc_with_status(CALL_STATUS_WAITING, + release_call); + else + foreach_vc_with_status(CALL_STATUS_HELD, release_call); + break; + case '1': + if (idx) { + if (call) + err = release_call(call); + break; + } + err = release_answer_calls(); + break; + case '2': + if (idx) { + if (call) + err = split_call(call); + } else { + call = find_vc_with_status(CALL_STATUS_WAITING); + + if (call) + err = answer_call(call); + else + err = swap_calls(); + } + break; + case '3': + if (find_vc_with_status(CALL_STATUS_HELD) || + find_vc_with_status(CALL_STATUS_WAITING)) + err = create_conference(); + break; + case '4': + err = call_transfer(); + break; + default: + DBG("Unknown call hold request"); + break; + } + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-ofono: got %s NR and EC request", + enable ? "enable" : "disable"); + + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct voice_call *active, *incoming; + int err; + + DBG("telephony-ofono: got key press request for %s", keys); + + incoming = find_vc_with_status(CALL_STATUS_INCOMING); + + active = find_vc_with_status(CALL_STATUS_ACTIVE); + + if (incoming) + err = answer_call(incoming); + else if (active) + err = release_call(active); + else + err = 0; + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-ofono: got %s voice dial request", + enable ? "enable" : "disable"); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NOT_SUPPORTED); +} + +static gboolean iter_get_basic_args(DBusMessageIter *iter, + int first_arg_type, ...) +{ + int type; + va_list ap; + + va_start(ap, first_arg_type); + + for (type = first_arg_type; type != DBUS_TYPE_INVALID; + type = va_arg(ap, int)) { + void *value = va_arg(ap, void *); + int real_type = dbus_message_iter_get_arg_type(iter); + + if (real_type != type) { + error("iter_get_basic_args: expected %c but got %c", + (char) type, (char) real_type); + break; + } + + dbus_message_iter_get_basic(iter, value); + dbus_message_iter_next(iter); + } + + va_end(ap); + + return type == DBUS_TYPE_INVALID ? TRUE : FALSE; +} + +static void call_free(struct voice_call *vc) +{ + DBG("%s", vc->obj_path); + + if (vc->status == CALL_STATUS_ACTIVE) + telephony_update_indicator(ofono_indicators, "call", + EV_CALL_INACTIVE); + else + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + + if (vc->status == CALL_STATUS_INCOMING) + telephony_calling_stopped_ind(); + + g_dbus_remove_watch(connection, vc->watch); + g_free(vc->obj_path); + g_free(vc->number); + g_free(vc); +} + +static gboolean handle_vc_property_changed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct voice_call *vc = data; + const char *obj_path = dbus_message_get_path(msg); + DBusMessageIter iter, sub; + const char *property, *state; + + DBG("path %s", obj_path); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in vc PropertyChanged signal"); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &property); + DBG("property %s", property); + + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &sub); + if (g_str_equal(property, "State")) { + dbus_message_iter_get_basic(&sub, &state); + DBG("State %s", state); + if (g_str_equal(state, "disconnected")) { + calls = g_slist_remove(calls, vc); + call_free(vc); + } else if (g_str_equal(state, "active")) { + telephony_update_indicator(ofono_indicators, + "call", EV_CALL_ACTIVE); + telephony_update_indicator(ofono_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (vc->status == CALL_STATUS_INCOMING) + telephony_calling_stopped_ind(); + vc->status = CALL_STATUS_ACTIVE; + } else if (g_str_equal(state, "alerting")) { + telephony_update_indicator(ofono_indicators, + "callsetup", EV_CALLSETUP_ALERTING); + vc->status = CALL_STATUS_ALERTING; + vc->originating = TRUE; + } else if (g_str_equal(state, "incoming")) { + /* state change from waiting to incoming */ + telephony_update_indicator(ofono_indicators, + "callsetup", EV_CALLSETUP_INCOMING); + telephony_incoming_call_ind(vc->number, + NUMBER_TYPE_TELEPHONY); + vc->status = CALL_STATUS_INCOMING; + vc->originating = FALSE; + } else if (g_str_equal(state, "held")) { + vc->status = CALL_STATUS_HELD; + if (find_vc_without_status(CALL_STATUS_HELD)) + telephony_update_indicator(ofono_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(ofono_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + } + } else if (g_str_equal(property, "Multiparty")) { + dbus_bool_t multiparty; + + dbus_message_iter_get_basic(&sub, &multiparty); + DBG("Multiparty %s", multiparty ? "True" : "False"); + vc->conference = multiparty; + } + + return TRUE; +} + +static struct voice_call *call_new(const char *path, DBusMessageIter *properties) +{ + struct voice_call *vc; + + DBG("%s", path); + + vc = g_new0(struct voice_call, 1); + vc->obj_path = g_strdup(path); + vc->watch = g_dbus_add_signal_watch(connection, NULL, path, + OFONO_VC_INTERFACE, "PropertyChanged", + handle_vc_property_changed, vc, NULL); + + while (dbus_message_iter_get_arg_type(properties) + == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter entry, value; + const char *property, *cli, *state; + dbus_bool_t multiparty; + + dbus_message_iter_recurse(properties, &entry); + dbus_message_iter_get_basic(&entry, &property); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + if (g_str_equal(property, "LineIdentification")) { + dbus_message_iter_get_basic(&value, &cli); + DBG("cli %s", cli); + vc->number = g_strdup(cli); + } else if (g_str_equal(property, "State")) { + dbus_message_iter_get_basic(&value, &state); + DBG("state %s", state); + if (g_str_equal(state, "incoming")) + vc->status = CALL_STATUS_INCOMING; + else if (g_str_equal(state, "dialing")) + vc->status = CALL_STATUS_DIALING; + else if (g_str_equal(state, "alerting")) + vc->status = CALL_STATUS_ALERTING; + else if (g_str_equal(state, "waiting")) + vc->status = CALL_STATUS_WAITING; + else if (g_str_equal(state, "held")) + vc->status = CALL_STATUS_HELD; + } else if (g_str_equal(property, "Multiparty")) { + dbus_message_iter_get_basic(&value, &multiparty); + DBG("Multipary %s", multiparty ? "True" : "False"); + vc->conference = multiparty; + } + + dbus_message_iter_next(properties); + } + + switch (vc->status) { + case CALL_STATUS_INCOMING: + DBG("CALL_STATUS_INCOMING"); + vc->originating = FALSE; + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + telephony_incoming_call_ind(vc->number, NUMBER_TYPE_TELEPHONY); + break; + case CALL_STATUS_DIALING: + DBG("CALL_STATUS_DIALING"); + vc->originating = TRUE; + g_free(last_dialed_number); + last_dialed_number = g_strdup(vc->number); + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + break; + case CALL_STATUS_ALERTING: + DBG("CALL_STATUS_ALERTING"); + vc->originating = TRUE; + g_free(last_dialed_number); + last_dialed_number = g_strdup(vc->number); + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CALL_STATUS_WAITING: + DBG("CALL_STATUS_WAITING"); + vc->originating = FALSE; + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + telephony_call_waiting_ind(vc->number, NUMBER_TYPE_TELEPHONY); + break; + } + + return vc; +} + +static void remove_pending(DBusPendingCall *call) +{ + pending = g_slist_remove(pending, call); + dbus_pending_call_unref(call); +} + +static void call_added(const char *path, DBusMessageIter *properties) +{ + struct voice_call *vc; + + DBG("%s", path); + + vc = find_vc(path); + if (vc) + return; + + vc = call_new(path, properties); + calls = g_slist_prepend(calls, vc); +} + +static void get_calls_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, entry; + + DBG(""); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("ofono replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature"); + goto done; + } + + dbus_message_iter_recurse(&iter, &entry); + + while (dbus_message_iter_get_arg_type(&entry) + == DBUS_TYPE_STRUCT) { + const char *path; + DBusMessageIter value, properties; + + dbus_message_iter_recurse(&entry, &value); + dbus_message_iter_get_basic(&value, &path); + + dbus_message_iter_next(&value); + dbus_message_iter_recurse(&value, &properties); + + call_added(path, &properties); + + dbus_message_iter_next(&entry); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void handle_network_property(const char *property, DBusMessageIter *variant) +{ + const char *status, *operator; + unsigned int signals_bar; + + if (g_str_equal(property, "Status")) { + dbus_message_iter_get_basic(variant, &status); + DBG("Status is %s", status); + if (g_str_equal(status, "registered")) { + net.status = NETWORK_REG_STATUS_HOME; + telephony_update_indicator(ofono_indicators, + "roam", EV_ROAM_INACTIVE); + telephony_update_indicator(ofono_indicators, + "service", EV_SERVICE_PRESENT); + } else if (g_str_equal(status, "roaming")) { + net.status = NETWORK_REG_STATUS_ROAM; + telephony_update_indicator(ofono_indicators, + "roam", EV_ROAM_ACTIVE); + telephony_update_indicator(ofono_indicators, + "service", EV_SERVICE_PRESENT); + } else { + net.status = NETWORK_REG_STATUS_NOSERV; + telephony_update_indicator(ofono_indicators, + "roam", EV_ROAM_INACTIVE); + telephony_update_indicator(ofono_indicators, + "service", EV_SERVICE_NONE); + } + } else if (g_str_equal(property, "Name")) { + dbus_message_iter_get_basic(variant, &operator); + DBG("Operator is %s", operator); + g_free(net.operator_name); + net.operator_name = g_strdup(operator); + } else if (g_str_equal(property, "SignalStrength")) { + dbus_message_iter_get_basic(variant, &signals_bar); + DBG("SignalStrength is %d", signals_bar); + net.signals_bar = signals_bar; + telephony_update_indicator(ofono_indicators, "signal", + (signals_bar + 20) / 21); + } +} + +static int parse_network_properties(DBusMessageIter *properties) +{ + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_INBAND_RINGTONE | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_ENHANCED_CALL_CONTROL | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES | + AG_FEATURE_THREE_WAY_CALLING; + int i; + + /* Reset indicators */ + for (i = 0; ofono_indicators[i].desc != NULL; i++) { + if (g_str_equal(ofono_indicators[i].desc, "battchg")) + ofono_indicators[i].val = 5; + else + ofono_indicators[i].val = 0; + } + + while (dbus_message_iter_get_arg_type(properties) + == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter value, entry; + + dbus_message_iter_recurse(properties, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + handle_network_property(key, &value); + + dbus_message_iter_next(properties); + } + + telephony_ready_ind(features, ofono_indicators, BTRH_NOT_SUPPORTED, + chld_str); + + return 0; +} + +static void get_properties_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, properties; + int ret = 0; + + DBG(""); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("ofono replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature"); + goto done; + } + + dbus_message_iter_recurse(&iter, &properties); + + ret = parse_network_properties(&properties); + if (ret < 0) { + error("Unable to parse %s.GetProperty reply", + OFONO_NETWORKREG_INTERFACE); + goto done; + } + + ret = send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, "GetCalls", + get_calls_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) + error("Unable to send %s.GetCalls", + OFONO_VCMANAGER_INTERFACE); + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void network_found(const char *path) +{ + int ret; + + DBG("%s", path); + + modem_obj_path = g_strdup(path); + + ret = send_method_call(OFONO_BUS_NAME, path, + OFONO_NETWORKREG_INTERFACE, "GetProperties", + get_properties_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) + error("Unable to send %s.GetProperties", + OFONO_NETWORKREG_INTERFACE); +} + +static void modem_removed(const char *path) +{ + if (g_strcmp0(modem_obj_path, path) != 0) + return; + + DBG("%s", path); + + g_slist_foreach(calls, (GFunc) call_free, NULL); + g_slist_free(calls); + calls = NULL; + + g_free(net.operator_name); + net.operator_name = NULL; + net.status = NETWORK_REG_STATUS_NOSERV; + net.signals_bar = 0; + + g_free(modem_obj_path); + modem_obj_path = NULL; +} + +static void parse_modem_interfaces(const char *path, DBusMessageIter *ifaces) +{ + DBG("%s", path); + + while (dbus_message_iter_get_arg_type(ifaces) == DBUS_TYPE_STRING) { + const char *iface; + + dbus_message_iter_get_basic(ifaces, &iface); + + if (g_str_equal(iface, OFONO_NETWORKREG_INTERFACE)) { + network_found(path); + return; + } + + dbus_message_iter_next(ifaces); + } + + modem_removed(path); +} + +static void modem_added(const char *path, DBusMessageIter *properties) +{ + if (modem_obj_path != NULL) { + DBG("Ignoring, modem already exist"); + return; + } + + DBG("%s", path); + + while (dbus_message_iter_get_arg_type(properties) + == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter interfaces, value, entry; + + dbus_message_iter_recurse(properties, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + if (strcasecmp(key, "Interfaces") != 0) + goto next; + + if (dbus_message_iter_get_arg_type(&value) + != DBUS_TYPE_ARRAY) { + error("Invalid Signature"); + return; + } + + dbus_message_iter_recurse(&value, &interfaces); + + parse_modem_interfaces(path, &interfaces); + + if (modem_obj_path != NULL) + return; + + next: + dbus_message_iter_next(properties); + } +} + +static void get_modems_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, entry; + + DBG(""); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("ofono replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + /* Skip modem selection if a modem already exist */ + if (modem_obj_path != NULL) + goto done; + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature"); + goto done; + } + + dbus_message_iter_recurse(&iter, &entry); + + while (dbus_message_iter_get_arg_type(&entry) + == DBUS_TYPE_STRUCT) { + const char *path; + DBusMessageIter item, properties; + + dbus_message_iter_recurse(&entry, &item); + dbus_message_iter_get_basic(&item, &path); + + dbus_message_iter_next(&item); + dbus_message_iter_recurse(&item, &properties); + + modem_added(path, &properties); + if (modem_obj_path != NULL) + break; + + dbus_message_iter_next(&entry); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static gboolean handle_network_property_changed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, variant; + const char *property; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in networkregistration" + " PropertyChanged signal"); + return TRUE; + } + dbus_message_iter_get_basic(&iter, &property); + DBG("in handle_registration_property_changed()," + " the property is %s", property); + + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &variant); + + handle_network_property(property, &variant); + + return TRUE; +} + +static void handle_modem_property(const char *path, const char *property, + DBusMessageIter *variant) +{ + DBG("%s", property); + + if (g_str_equal(property, "Interfaces")) { + DBusMessageIter interfaces; + + if (dbus_message_iter_get_arg_type(variant) + != DBUS_TYPE_ARRAY) { + error("Invalid signature"); + return; + } + + dbus_message_iter_recurse(variant, &interfaces); + parse_modem_interfaces(path, &interfaces); + } +} + +static gboolean handle_modem_property_changed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, variant; + const char *property, *path; + + path = dbus_message_get_path(msg); + + /* Ignore if modem already exist and paths doesn't match */ + if (modem_obj_path != NULL && + g_str_equal(path, modem_obj_path) == FALSE) + return TRUE; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in %s.%s PropertyChanged signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &property); + + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &variant); + + handle_modem_property(path, property, &variant); + + return TRUE; +} + +static gboolean handle_vcmanager_call_added(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, properties; + const char *path = dbus_message_get_path(msg); + + /* Ignore call if modem path doesn't math */ + if (g_strcmp0(modem_obj_path, path) != 0) + return TRUE; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) + != DBUS_TYPE_OBJECT_PATH) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &path); + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &properties); + + call_added(path, &properties); + + return TRUE; +} + +static void call_removed(const char *path) +{ + struct voice_call *vc; + + DBG("%s", path); + + vc = find_vc(path); + if (vc == NULL) + return; + + calls = g_slist_remove(calls, vc); + call_free(vc); +} + +static gboolean handle_vcmanager_call_removed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path = dbus_message_get_path(msg); + + /* Ignore call if modem path doesn't math */ + if (g_strcmp0(modem_obj_path, path) != 0) + return TRUE; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + call_removed(path); + + return TRUE; +} + +static gboolean handle_manager_modem_added(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, properties; + const char *path; + + if (modem_obj_path != NULL) + return TRUE; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) + != DBUS_TYPE_OBJECT_PATH) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &path); + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &properties); + + modem_added(path, &properties); + + return TRUE; +} + +static gboolean handle_manager_modem_removed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + modem_removed(path); + + return TRUE; +} + +static void hal_battery_level_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply; + DBusError err; + dbus_int32_t level; + int *value = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (dbus_message_get_args(reply, &err, + DBUS_TYPE_INT32, &level, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse GetPropertyInteger reply: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + *value = (int) level; + + if (value == &battchg_last) + DBG("telephony-ofono: battery.charge_level.last_full" + " is %d", *value); + else if (value == &battchg_design) + DBG("telephony-ofono: battery.charge_level.design" + " is %d", *value); + else + DBG("telephony-ofono: battery.charge_level.current" + " is %d", *value); + + if ((battchg_design > 0 || battchg_last > 0) && battchg_cur >= 0) { + int new, max; + + if (battchg_last > 0) + max = battchg_last; + else + max = battchg_design; + + new = battchg_cur * 5 / max; + + telephony_update_indicator(ofono_indicators, "battchg", new); + } +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void hal_get_integer(const char *path, const char *key, void *user_data) +{ + send_method_call("org.freedesktop.Hal", path, + "org.freedesktop.Hal.Device", + "GetPropertyInteger", + hal_battery_level_reply, user_data, + DBUS_TYPE_STRING, &key, + DBUS_TYPE_INVALID); +} + +static gboolean handle_hal_property_modified(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path; + DBusMessageIter iter, array; + dbus_int32_t num_changes; + + path = dbus_message_get_path(msg); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) { + error("Unexpected signature in hal PropertyModified signal"); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &num_changes); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal PropertyModified signal"); + return TRUE; + } + + dbus_message_iter_recurse(&iter, &array); + + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) { + DBusMessageIter prop; + const char *name; + dbus_bool_t added, removed; + + dbus_message_iter_recurse(&array, &prop); + + if (!iter_get_basic_args(&prop, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_BOOLEAN, &added, + DBUS_TYPE_BOOLEAN, &removed, + DBUS_TYPE_INVALID)) { + error("Invalid hal PropertyModified parameters"); + break; + } + + if (g_str_equal(name, "battery.charge_level.last_full")) + hal_get_integer(path, name, &battchg_last); + else if (g_str_equal(name, "battery.charge_level.current")) + hal_get_integer(path, name, &battchg_cur); + else if (g_str_equal(name, "battery.charge_level.design")) + hal_get_integer(path, name, &battchg_design); + + dbus_message_iter_next(&array); + } + + return TRUE; +} + +static void add_watch(const char *sender, const char *path, + const char *interface, const char *member, + GDBusSignalFunction function) +{ + guint watch; + + watch = g_dbus_add_signal_watch(connection, sender, path, interface, + member, function, NULL, NULL); + + watches = g_slist_prepend(watches, GUINT_TO_POINTER(watch)); +} + +static void hal_find_device_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply; + DBusError err; + DBusMessageIter iter, sub; + int type; + const char *path; + + DBG("begin of hal_find_device_reply()"); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal_find_device_reply()"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + type = dbus_message_iter_get_arg_type(&sub); + + if (type != DBUS_TYPE_OBJECT_PATH && type != DBUS_TYPE_STRING) { + error("No hal device with battery capability found"); + goto done; + } + + dbus_message_iter_get_basic(&sub, &path); + + DBG("telephony-ofono: found battery device at %s", path); + + add_watch(NULL, path, "org.freedesktop.Hal.Device", + "PropertyModified", handle_hal_property_modified); + + hal_get_integer(path, "battery.charge_level.last_full", &battchg_last); + hal_get_integer(path, "battery.charge_level.current", &battchg_cur); + hal_get_integer(path, "battery.charge_level.design", &battchg_design); +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void handle_service_connect(DBusConnection *conn, void *user_data) +{ + DBG("telephony-ofono: %s found", OFONO_BUS_NAME); + + send_method_call(OFONO_BUS_NAME, OFONO_PATH, + OFONO_MANAGER_INTERFACE, "GetModems", + get_modems_reply, NULL, DBUS_TYPE_INVALID); +} + +static void handle_service_disconnect(DBusConnection *conn, void *user_data) +{ + DBG("telephony-ofono: %s exitted", OFONO_BUS_NAME); + + if (modem_obj_path) + modem_removed(modem_obj_path); +} + +int telephony_init(void) +{ + const char *battery_cap = "battery"; + int ret; + guint watch; + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + add_watch(OFONO_BUS_NAME, NULL, OFONO_MODEM_INTERFACE, + "PropertyChanged", handle_modem_property_changed); + add_watch(OFONO_BUS_NAME, NULL, OFONO_NETWORKREG_INTERFACE, + "PropertyChanged", handle_network_property_changed); + add_watch(OFONO_BUS_NAME, NULL, OFONO_MANAGER_INTERFACE, + "ModemAdded", handle_manager_modem_added); + add_watch(OFONO_BUS_NAME, NULL, OFONO_MANAGER_INTERFACE, + "ModemRemoved", handle_manager_modem_removed); + add_watch(OFONO_BUS_NAME, NULL, OFONO_VCMANAGER_INTERFACE, + "CallAdded", handle_vcmanager_call_added); + add_watch(OFONO_BUS_NAME, NULL, OFONO_VCMANAGER_INTERFACE, + "CallRemoved", handle_vcmanager_call_removed); + + watch = g_dbus_add_service_watch(connection, OFONO_BUS_NAME, + handle_service_connect, + handle_service_disconnect, + NULL, NULL); + if (watch == 0) + return -ENOMEM; + + watches = g_slist_prepend(watches, GUINT_TO_POINTER(watch)); + + ret = send_method_call("org.freedesktop.Hal", + "/org/freedesktop/Hal/Manager", + "org.freedesktop.Hal.Manager", + "FindDeviceByCapability", + hal_find_device_reply, NULL, + DBUS_TYPE_STRING, &battery_cap, + DBUS_TYPE_INVALID); + if (ret < 0) + return ret; + + DBG("telephony_init() successfully"); + + return ret; +} + +static void remove_watch(gpointer data) +{ + g_dbus_remove_watch(connection, GPOINTER_TO_UINT(data)); +} + +void telephony_exit(void) +{ + DBG(""); + + g_free(last_dialed_number); + last_dialed_number = NULL; + + if (modem_obj_path) + modem_removed(modem_obj_path); + + g_slist_foreach(watches, (GFunc) remove_watch, NULL); + g_slist_free(watches); + watches = NULL; + + g_slist_foreach(pending, (GFunc) dbus_pending_call_cancel, NULL); + g_slist_foreach(pending, (GFunc) dbus_pending_call_unref, NULL); + g_slist_free(pending); + pending = NULL; + + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony.h b/audio/telephony.h new file mode 100644 index 0000000..73b390c --- /dev/null +++ b/audio/telephony.h @@ -0,0 +1,244 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include <stdint.h> +#include <errno.h> +#include <glib.h> + +/* Response and hold values */ +#define BTRH_NOT_SUPPORTED -2 +#define BTRH_NONE -1 +#define BTRH_HOLD 0 +#define BTRH_ACCEPT 1 +#define BTRH_REJECT 2 + +/* HFP feature bits */ +#define AG_FEATURE_THREE_WAY_CALLING 0x0001 +#define AG_FEATURE_EC_ANDOR_NR 0x0002 +#define AG_FEATURE_VOICE_RECOGNITION 0x0004 +#define AG_FEATURE_INBAND_RINGTONE 0x0008 +#define AG_FEATURE_ATTACH_NUMBER_TO_VOICETAG 0x0010 +#define AG_FEATURE_REJECT_A_CALL 0x0020 +#define AG_FEATURE_ENHANCED_CALL_STATUS 0x0040 +#define AG_FEATURE_ENHANCED_CALL_CONTROL 0x0080 +#define AG_FEATURE_EXTENDED_ERROR_RESULT_CODES 0x0100 + +#define HF_FEATURE_EC_ANDOR_NR 0x0001 +#define HF_FEATURE_CALL_WAITING_AND_3WAY 0x0002 +#define HF_FEATURE_CLI_PRESENTATION 0x0004 +#define HF_FEATURE_VOICE_RECOGNITION 0x0008 +#define HF_FEATURE_REMOTE_VOLUME_CONTROL 0x0010 +#define HF_FEATURE_ENHANCED_CALL_STATUS 0x0020 +#define HF_FEATURE_ENHANCED_CALL_CONTROL 0x0040 + +/* Indicator event values */ +#define EV_SERVICE_NONE 0 +#define EV_SERVICE_PRESENT 1 + +#define EV_CALL_INACTIVE 0 +#define EV_CALL_ACTIVE 1 + +#define EV_CALLSETUP_INACTIVE 0 +#define EV_CALLSETUP_INCOMING 1 +#define EV_CALLSETUP_OUTGOING 2 +#define EV_CALLSETUP_ALERTING 3 + +#define EV_CALLHELD_NONE 0 +#define EV_CALLHELD_MULTIPLE 1 +#define EV_CALLHELD_ON_HOLD 2 + +#define EV_ROAM_INACTIVE 0 +#define EV_ROAM_ACTIVE 1 + +/* Call parameters */ +#define CALL_DIR_OUTGOING 0 +#define CALL_DIR_INCOMING 1 + +#define CALL_STATUS_ACTIVE 0 +#define CALL_STATUS_HELD 1 +#define CALL_STATUS_DIALING 2 +#define CALL_STATUS_ALERTING 3 +#define CALL_STATUS_INCOMING 4 +#define CALL_STATUS_WAITING 5 + +#define CALL_MODE_VOICE 0 +#define CALL_MODE_DATA 1 +#define CALL_MODE_FAX 2 + +#define CALL_MULTIPARTY_NO 0 +#define CALL_MULTIPARTY_YES 1 + +/* Subscriber number parameters */ +#define SUBSCRIBER_SERVICE_VOICE 4 +#define SUBSCRIBER_SERVICE_FAX 5 + +/* Operator selection mode values */ +#define OPERATOR_MODE_AUTO 0 +#define OPERATOR_MODE_MANUAL 1 +#define OPERATOR_MODE_DEREGISTER 2 +#define OPERATOR_MODE_MANUAL_AUTO 4 + +/* Some common number types */ +#define NUMBER_TYPE_UNKNOWN 128 +#define NUMBER_TYPE_TELEPHONY 129 +#define NUMBER_TYPE_INTERNATIONAL 145 +#define NUMBER_TYPE_NATIONAL 161 +#define NUMBER_TYPE_VOIP 255 + +/* Extended Audio Gateway Error Result Codes */ +typedef enum { + CME_ERROR_NONE = -1, + CME_ERROR_AG_FAILURE = 0, + CME_ERROR_NO_PHONE_CONNECTION = 1, + CME_ERROR_NOT_ALLOWED = 3, + CME_ERROR_NOT_SUPPORTED = 4, + CME_ERROR_PH_SIM_PIN_REQUIRED = 5, + CME_ERROR_SIM_NOT_INSERTED = 10, + CME_ERROR_SIM_PIN_REQUIRED = 11, + CME_ERROR_SIM_PUK_REQUIRED = 12, + CME_ERROR_SIM_FAILURE = 13, + CME_ERROR_SIM_BUSY = 14, + CME_ERROR_INCORRECT_PASSWORD = 16, + CME_ERROR_SIM_PIN2_REQUIRED = 17, + CME_ERROR_SIM_PUK2_REQUIRED = 18, + CME_ERROR_MEMORY_FULL = 20, + CME_ERROR_INVALID_INDEX = 21, + CME_ERROR_MEMORY_FAILURE = 23, + CME_ERROR_TEXT_STRING_TOO_LONG = 24, + CME_ERROR_INVALID_TEXT_STRING = 25, + CME_ERROR_DIAL_STRING_TOO_LONG = 26, + CME_ERROR_INVALID_DIAL_STRING = 27, + CME_ERROR_NO_NETWORK_SERVICE = 30, + CME_ERROR_NETWORK_TIMEOUT = 31, + CME_ERROR_NETWORK_NOT_ALLOWED = 32, +} cme_error_t; + +struct indicator { + const char *desc; + const char *range; + int val; + gboolean ignore_redundant; +}; + +/* Notify telephony-*.c of connected/disconnected devices. Implemented by + * telephony-*.c + */ +void telephony_device_connected(void *telephony_device); +void telephony_device_disconnected(void *telephony_device); + +/* HF requests (sent by the handsfree device). These are implemented by + * telephony-*.c + */ +void telephony_event_reporting_req(void *telephony_device, int ind); +void telephony_response_and_hold_req(void *telephony_device, int rh); +void telephony_last_dialed_number_req(void *telephony_device); +void telephony_terminate_call_req(void *telephony_device); +void telephony_answer_call_req(void *telephony_device); +void telephony_dial_number_req(void *telephony_device, const char *number); +void telephony_transmit_dtmf_req(void *telephony_device, char tone); +void telephony_subscriber_number_req(void *telephony_device); +void telephony_list_current_calls_req(void *telephony_device); +void telephony_operator_selection_req(void *telephony_device); +void telephony_call_hold_req(void *telephony_device, const char *cmd); +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable); +void telephony_voice_dial_req(void *telephony_device, gboolean enable); +void telephony_key_press_req(void *telephony_device, const char *keys); + +/* AG responses to HF requests. These are implemented by headset.c */ +int telephony_event_reporting_rsp(void *telephony_device, cme_error_t err); +int telephony_response_and_hold_rsp(void *telephony_device, cme_error_t err); +int telephony_last_dialed_number_rsp(void *telephony_device, cme_error_t err); +int telephony_terminate_call_rsp(void *telephony_device, cme_error_t err); +int telephony_answer_call_rsp(void *telephony_device, cme_error_t err); +int telephony_dial_number_rsp(void *telephony_device, cme_error_t err); +int telephony_transmit_dtmf_rsp(void *telephony_device, cme_error_t err); +int telephony_subscriber_number_rsp(void *telephony_device, cme_error_t err); +int telephony_list_current_calls_rsp(void *telephony_device, cme_error_t err); +int telephony_operator_selection_rsp(void *telephony_device, cme_error_t err); +int telephony_call_hold_rsp(void *telephony_device, cme_error_t err); +int telephony_nr_and_ec_rsp(void *telephony_device, cme_error_t err); +int telephony_voice_dial_rsp(void *telephony_device, cme_error_t err); +int telephony_key_press_rsp(void *telephony_device, cme_error_t err); + +/* Event indications by AG. These are implemented by headset.c */ +int telephony_event_ind(int index); +int telephony_response_and_hold_ind(int rh); +int telephony_incoming_call_ind(const char *number, int type); +int telephony_calling_stopped_ind(void); +int telephony_ready_ind(uint32_t features, const struct indicator *indicators, + int rh, const char *chld); +int telephony_deinit(void); +int telephony_list_current_call_ind(int idx, int dir, int status, int mode, + int mprty, const char *number, + int type); +int telephony_subscriber_number_ind(const char *number, int type, + int service); +int telephony_call_waiting_ind(const char *number, int type); +int telephony_operator_selection_ind(int mode, const char *oper); + +/* Helper function for quick indicator updates */ +static inline int telephony_update_indicator(struct indicator *indicators, + const char *desc, + int new_val) +{ + int i; + struct indicator *ind = NULL; + + for (i = 0; indicators[i].desc != NULL; i++) { + if (g_str_equal(indicators[i].desc, desc)) { + ind = &indicators[i]; + break; + } + } + + if (!ind) + return -ENOENT; + + DBG("Telephony indicator \"%s\" %d->%d", desc, ind->val, new_val); + + if (ind->ignore_redundant && ind->val == new_val) { + DBG("Ignoring no-change indication"); + return 0; + } + + ind->val = new_val; + + return telephony_event_ind(i); +} + +static inline int telephony_get_indicator(const struct indicator *indicators, + const char *desc) +{ + int i; + + for (i = 0; indicators[i].desc != NULL; i++) { + if (g_str_equal(indicators[i].desc, desc)) + return indicators[i].val; + } + + return -ENOENT; +} + +int telephony_init(void); +void telephony_exit(void); diff --git a/audio/transport.c b/audio/transport.c new file mode 100644 index 0000000..8ff6c85 --- /dev/null +++ b/audio/transport.c @@ -0,0 +1,927 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <errno.h> + +#include <glib.h> +#include <gdbus.h> + +#include "../src/adapter.h" +#include "../src/dbus-common.h" + +#include "log.h" +#include "error.h" +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "transport.h" +#include "a2dp.h" +#include "headset.h" + +#ifndef DBUS_TYPE_UNIX_FD +#define DBUS_TYPE_UNIX_FD -1 +#endif + +#define MEDIA_TRANSPORT_INTERFACE "org.bluez.MediaTransport" + +struct media_request { + DBusMessage *msg; + guint id; +}; + +struct media_owner { + struct media_transport *transport; + struct media_request *pending; + char *name; + char *accesstype; + guint watch; +}; + +struct media_transport { + DBusConnection *conn; + char *path; /* Transport object path */ + struct audio_device *device; /* Transport device */ + struct avdtp *session; /* Signalling session (a2dp only) */ + struct media_endpoint *endpoint; /* Transport endpoint */ + GSList *owners; /* Transport owners */ + uint8_t *configuration; /* Transport configuration */ + int size; /* Transport configuration size */ + int fd; /* Transport file descriptor */ + uint16_t imtu; /* Transport input mtu */ + uint16_t omtu; /* Transport output mtu */ + uint16_t delay; /* Transport delay (a2dp only) */ + unsigned int nrec_id; /* Transport nrec watch (headset only) */ + gboolean read_lock; + gboolean write_lock; + gboolean in_use; + guint (*resume) (struct media_transport *transport, + struct media_owner *owner); + guint (*suspend) (struct media_transport *transport, + struct media_owner *owner); + void (*cancel) (struct media_transport *transport, + guint id); + void (*get_properties) ( + struct media_transport *transport, + DBusMessageIter *dict); + int (*set_property) ( + struct media_transport *transport, + const char *property, + DBusMessageIter *value); +}; + +void media_transport_destroy(struct media_transport *transport) +{ + char *path; + + path = g_strdup(transport->path); + + g_dbus_unregister_interface(transport->conn, path, + MEDIA_TRANSPORT_INTERFACE); + + g_free(path); +} + +static struct media_request *media_request_create(DBusMessage *msg, guint id) +{ + struct media_request *req; + + req = g_new0(struct media_request, 1); + req->msg = dbus_message_ref(msg); + req->id = id; + + DBG("Request created: method=%s id=%u", dbus_message_get_member(msg), + id); + + return req; +} + +static void media_request_reply(struct media_request *req, + DBusConnection *conn, int err) +{ + DBusMessage *reply; + + DBG("Request %s Reply %s", dbus_message_get_member(req->msg), + strerror(err)); + + if (!err) + reply = g_dbus_create_reply(req->msg, DBUS_TYPE_INVALID); + else + reply = g_dbus_create_error(req->msg, + ERROR_INTERFACE ".Failed", + "%s", strerror(err)); + + g_dbus_send_message(conn, reply); +} + +static gboolean media_transport_release(struct media_transport *transport, + const char *accesstype) +{ + if (g_strstr_len(accesstype, -1, "r") != NULL) { + transport->read_lock = FALSE; + DBG("Transport %s: read lock released", transport->path); + } + + if (g_strstr_len(accesstype, -1, "w") != NULL) { + transport->write_lock = FALSE; + DBG("Transport %s: write lock released", transport->path); + } + + return TRUE; +} + +static void media_owner_remove(struct media_owner *owner) +{ + struct media_transport *transport = owner->transport; + struct media_request *req = owner->pending; + + if (!req) + return; + + DBG("Owner %s Request %s", owner->name, + dbus_message_get_member(req->msg)); + + if (req->id) + transport->cancel(transport, req->id); + + owner->pending = NULL; + if (req->msg) + dbus_message_unref(req->msg); + + g_free(req); +} + +static void media_owner_free(struct media_owner *owner) +{ + DBG("Owner %s", owner->name); + + media_owner_remove(owner); + + g_free(owner->name); + g_free(owner->accesstype); + g_free(owner); +} + +static void media_transport_remove(struct media_transport *transport, + struct media_owner *owner) +{ + DBG("Transport %s Owner %s", transport->path, owner->name); + + media_transport_release(transport, owner->accesstype); + + /* Reply if owner has a pending request */ + if (owner->pending) + media_request_reply(owner->pending, transport->conn, EIO); + + transport->owners = g_slist_remove(transport->owners, owner); + + if (owner->watch) + g_dbus_remove_watch(transport->conn, owner->watch); + + media_owner_free(owner); + + /* Suspend if there is no longer any owner */ + if (transport->owners == NULL && transport->in_use) + transport->suspend(transport, NULL); +} + +static gboolean media_transport_set_fd(struct media_transport *transport, + int fd, uint16_t imtu, uint16_t omtu) +{ + if (transport->fd == fd) + return TRUE; + + transport->fd = fd; + transport->imtu = imtu; + transport->omtu = omtu; + + info("%s: fd(%d) ready", transport->path, fd); + + return TRUE; +} + +static gboolean remove_owner(gpointer data) +{ + struct media_owner *owner = data; + + media_transport_remove(owner->transport, owner); + + return FALSE; +} + +static void a2dp_resume_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_request *req = owner->pending; + struct media_transport *transport = owner->transport; + struct a2dp_sep *sep = media_endpoint_get_sep(transport->endpoint); + struct avdtp_stream *stream; + int fd; + uint16_t imtu, omtu; + gboolean ret; + + req->id = 0; + + if (err) + goto fail; + + stream = a2dp_sep_get_stream(sep); + if (stream == NULL) + goto fail; + + ret = avdtp_stream_get_transport(stream, &fd, &imtu, &omtu, NULL); + if (ret == FALSE) + goto fail; + + media_transport_set_fd(transport, fd, imtu, omtu); + + if (g_strstr_len(owner->accesstype, -1, "r") == NULL) + imtu = 0; + + if (g_strstr_len(owner->accesstype, -1, "w") == NULL) + omtu = 0; + + ret = g_dbus_send_reply(transport->conn, req->msg, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID); + if (ret == FALSE) + goto fail; + + media_owner_remove(owner); + + return; + +fail: + /* Let the stream state change before removing the owner */ + g_idle_add(remove_owner, owner); +} + +static guint resume_a2dp(struct media_transport *transport, + struct media_owner *owner) +{ + struct media_endpoint *endpoint = transport->endpoint; + struct audio_device *device = transport->device; + struct a2dp_sep *sep = media_endpoint_get_sep(endpoint); + + if (transport->session == NULL) { + transport->session = avdtp_get(&device->src, &device->dst); + if (transport->session == NULL) + return 0; + } + + if (transport->in_use == TRUE) + goto done; + + transport->in_use = a2dp_sep_lock(sep, transport->session); + if (transport->in_use == FALSE) + return 0; + +done: + return a2dp_resume(transport->session, sep, a2dp_resume_complete, + owner); +} + +static void a2dp_suspend_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_transport *transport = owner->transport; + struct a2dp_sep *sep = media_endpoint_get_sep(transport->endpoint); + + /* Release always succeeds */ + if (owner->pending) { + owner->pending->id = 0; + media_request_reply(owner->pending, transport->conn, 0); + media_owner_remove(owner); + } + + a2dp_sep_unlock(sep, transport->session); + transport->in_use = FALSE; + media_transport_remove(transport, owner); +} + +static guint suspend_a2dp(struct media_transport *transport, + struct media_owner *owner) +{ + struct media_endpoint *endpoint = transport->endpoint; + struct a2dp_sep *sep = media_endpoint_get_sep(endpoint); + + if (!owner) { + a2dp_sep_unlock(sep, transport->session); + transport->in_use = FALSE; + return 0; + } + + return a2dp_suspend(transport->session, sep, a2dp_suspend_complete, + owner); +} + +static void cancel_a2dp(struct media_transport *transport, guint id) +{ + a2dp_cancel(transport->device, id); +} + +static void headset_resume_complete(struct audio_device *dev, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_request *req = owner->pending; + struct media_transport *transport = owner->transport; + int fd; + uint16_t imtu, omtu; + gboolean ret; + + req->id = 0; + + if (dev == NULL) + goto fail; + + fd = headset_get_sco_fd(dev); + if (fd < 0) + goto fail; + + imtu = 48; + omtu = 48; + + media_transport_set_fd(transport, fd, imtu, omtu); + + if (g_strstr_len(owner->accesstype, -1, "r") == NULL) + imtu = 0; + + if (g_strstr_len(owner->accesstype, -1, "w") == NULL) + omtu = 0; + + ret = g_dbus_send_reply(transport->conn, req->msg, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID); + if (ret == FALSE) + goto fail; + + media_owner_remove(owner); + + return; + +fail: + media_transport_remove(transport, owner); +} + +static guint resume_headset(struct media_transport *transport, + struct media_owner *owner) +{ + struct audio_device *device = transport->device; + + if (transport->in_use == TRUE) + goto done; + + transport->in_use = headset_lock(device, HEADSET_LOCK_READ | + HEADSET_LOCK_WRITE); + if (transport->in_use == FALSE) + return 0; + +done: + return headset_request_stream(device, headset_resume_complete, + owner); +} + +static void headset_suspend_complete(struct audio_device *dev, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_transport *transport = owner->transport; + + /* Release always succeeds */ + if (owner->pending) { + owner->pending->id = 0; + media_request_reply(owner->pending, transport->conn, 0); + media_owner_remove(owner); + } + + headset_unlock(dev, HEADSET_LOCK_READ | HEADSET_LOCK_WRITE); + transport->in_use = FALSE; + media_transport_remove(transport, owner); +} + +static guint suspend_headset(struct media_transport *transport, + struct media_owner *owner) +{ + struct audio_device *device = transport->device; + + if (!owner) { + headset_unlock(device, HEADSET_LOCK_READ | HEADSET_LOCK_WRITE); + transport->in_use = FALSE; + return 0; + } + + return headset_suspend_stream(device, headset_suspend_complete, owner); +} + +static void cancel_headset(struct media_transport *transport, guint id) +{ + headset_cancel_stream(transport->device, id); +} + +static void media_owner_exit(DBusConnection *connection, void *user_data) +{ + struct media_owner *owner = user_data; + + owner->watch = 0; + + media_owner_remove(owner); + + media_transport_remove(owner->transport, owner); +} + +static gboolean media_transport_acquire(struct media_transport *transport, + const char *accesstype) +{ + gboolean read_lock = FALSE, write_lock = FALSE; + + if (g_strstr_len(accesstype, -1, "r") != NULL) { + if (transport->read_lock == TRUE) + return FALSE; + read_lock = TRUE; + } + + if (g_strstr_len(accesstype, -1, "w") != NULL) { + if (transport->write_lock == TRUE) + return FALSE; + write_lock = TRUE; + } + + /* Check invalid accesstype */ + if (read_lock == FALSE && write_lock == FALSE) + return FALSE; + + if (read_lock) { + transport->read_lock = read_lock; + DBG("Transport %s: read lock acquired", transport->path); + } + + if (write_lock) { + transport->write_lock = write_lock; + DBG("Transport %s: write lock acquired", transport->path); + } + + + return TRUE; +} + +static void media_transport_add(struct media_transport *transport, + struct media_owner *owner) +{ + DBG("Transport %s Owner %s", transport->path, owner->name); + transport->owners = g_slist_append(transport->owners, owner); + owner->transport = transport; +} + +static struct media_owner *media_owner_create(DBusConnection *conn, + DBusMessage *msg, + const char *accesstype) +{ + struct media_owner *owner; + + owner = g_new0(struct media_owner, 1); + owner->name = g_strdup(dbus_message_get_sender(msg)); + owner->accesstype = g_strdup(accesstype); + owner->watch = g_dbus_add_disconnect_watch(conn, owner->name, + media_owner_exit, + owner, NULL); + + DBG("Owner created: sender=%s accesstype=%s", owner->name, + accesstype); + + return owner; +} + +static void media_owner_add(struct media_owner *owner, + struct media_request *req) +{ + DBG("Owner %s Request %s", owner->name, + dbus_message_get_member(req->msg)); + + owner->pending = req; +} + +static struct media_owner *media_transport_find_owner( + struct media_transport *transport, + const char *name) +{ + GSList *l; + + for (l = transport->owners; l; l = l->next) { + struct media_owner *owner = l->data; + + if (g_strcmp0(owner->name, name) == 0) + return owner; + } + + return NULL; +} + +static DBusMessage *acquire(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + struct media_owner *owner; + struct media_request *req; + const char *accesstype, *sender; + guint id; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &accesstype, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + owner = media_transport_find_owner(transport, sender); + if (owner != NULL) + return btd_error_not_authorized(msg); + + if (media_transport_acquire(transport, accesstype) == FALSE) + return btd_error_not_authorized(msg); + + owner = media_owner_create(conn, msg, accesstype); + id = transport->resume(transport, owner); + if (id == 0) { + media_owner_free(owner); + return btd_error_not_authorized(msg); + } + + req = media_request_create(msg, id); + media_owner_add(owner, req); + media_transport_add(transport, owner); + + return NULL; +} + +static DBusMessage *release(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + struct media_owner *owner; + const char *accesstype, *sender; + struct media_request *req; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &accesstype, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + owner = media_transport_find_owner(transport, sender); + if (owner == NULL) + return btd_error_not_authorized(msg); + + if (g_strcmp0(owner->accesstype, accesstype) == 0) { + guint id; + + /* Not the last owner, no need to suspend */ + if (g_slist_length(transport->owners) != 1) { + media_transport_remove(transport, owner); + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); + } + + if (owner->pending) { + const char *member; + + member = dbus_message_get_member(owner->pending->msg); + /* Cancel Acquire request if that exist */ + if (g_str_equal(member, "Acquire")) + media_owner_remove(owner); + else + return btd_error_in_progress(msg); + } + + id = transport->suspend(transport, owner); + if (id == 0) { + media_transport_remove(transport, owner); + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); + } + + req = media_request_create(msg, id); + media_owner_add(owner, req); + + return NULL; + } else if (g_strstr_len(owner->accesstype, -1, accesstype) != NULL) { + media_transport_release(transport, accesstype); + g_strdelimit(owner->accesstype, accesstype, ' '); + } else + return btd_error_not_authorized(msg); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static int set_property_a2dp(struct media_transport *transport, + const char *property, + DBusMessageIter *value) +{ + if (g_strcmp0(property, "Delay") == 0) { + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_UINT16) + return -EINVAL; + dbus_message_iter_get_basic(value, &transport->delay); + + /* FIXME: send new delay */ + return 0; + } + + return -EINVAL; +} + +static int set_property_headset(struct media_transport *transport, + const char *property, + DBusMessageIter *value) +{ + if (g_strcmp0(property, "NREC") == 0) { + gboolean nrec; + + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_BOOLEAN) + return -EINVAL; + dbus_message_iter_get_basic(value, &nrec); + + /* FIXME: set new nrec */ + return 0; + } else if (g_strcmp0(property, "InbandRingtone") == 0) { + gboolean inband; + + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_BOOLEAN) + return -EINVAL; + dbus_message_iter_get_basic(value, &inband); + + /* FIXME: set new inband */ + return 0; + } + + return -EINVAL; +} + +static DBusMessage *set_property(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + DBusMessageIter iter; + DBusMessageIter value; + const char *property, *sender; + GSList *l; + int err; + + if (!dbus_message_iter_init(msg, &iter)) + return btd_error_invalid_args(msg); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&iter, &property); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return btd_error_invalid_args(msg); + dbus_message_iter_recurse(&iter, &value); + + sender = dbus_message_get_sender(msg); + err = -EINVAL; + + /* Check if sender has acquired the transport */ + for (l = transport->owners; l; l = l->next) { + struct media_owner *owner = l->data; + + if (g_strcmp0(owner->name, sender) == 0) { + err = transport->set_property(transport, property, + &value); + break; + } + } + + if (err < 0) { + if (err == -EINVAL) + return btd_error_invalid_args(msg); + return btd_error_failed(msg, strerror(-err)); + } + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static void get_properties_a2dp(struct media_transport *transport, + DBusMessageIter *dict) +{ + dict_append_entry(dict, "Delay", DBUS_TYPE_UINT16, &transport->delay); +} + +static void get_properties_headset(struct media_transport *transport, + DBusMessageIter *dict) +{ + gboolean nrec, inband; + const char *routing; + + nrec = headset_get_nrec(transport->device); + dict_append_entry(dict, "NREC", DBUS_TYPE_BOOLEAN, &nrec); + + inband = headset_get_inband(transport->device); + dict_append_entry(dict, "InbandRingtone", DBUS_TYPE_BOOLEAN, &inband); + + routing = headset_get_sco_hci(transport->device) ? "HCI" : "PCM"; + dict_append_entry(dict, "Routing", DBUS_TYPE_STRING, &routing); +} + +void transport_get_properties(struct media_transport *transport, + DBusMessageIter *iter) +{ + DBusMessageIter dict; + const char *uuid; + uint8_t codec; + + dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* Device */ + dict_append_entry(&dict, "Device", DBUS_TYPE_OBJECT_PATH, + &transport->device->path); + + uuid = media_endpoint_get_uuid(transport->endpoint); + dict_append_entry(&dict, "UUID", DBUS_TYPE_STRING, &uuid); + + codec = media_endpoint_get_codec(transport->endpoint); + dict_append_entry(&dict, "Codec", DBUS_TYPE_BYTE, &codec); + + dict_append_array(&dict, "Configuration", DBUS_TYPE_BYTE, + &transport->configuration, transport->size); + + if (transport->get_properties) + transport->get_properties(transport, &dict); + + dbus_message_iter_close_container(iter, &dict); +} + +static DBusMessage *get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + DBusMessage *reply; + DBusMessageIter iter; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + transport_get_properties(transport, &iter); + + return reply; +} + +static GDBusMethodTable transport_methods[] = { + { "GetProperties", "", "a{sv}", get_properties }, + { "Acquire", "s", "h", acquire, + G_DBUS_METHOD_FLAG_ASYNC}, + { "Release", "s", "", release, + G_DBUS_METHOD_FLAG_ASYNC}, + { "SetProperty", "sv", "", set_property }, + { }, +}; + +static GDBusSignalTable transport_signals[] = { + { "PropertyChanged", "sv" }, + { } +}; + +static void media_transport_free(void *data) +{ + struct media_transport *transport = data; + GSList *l; + + for (l = transport->owners; l; l = l->next) + media_transport_remove(transport, l->data); + + g_slist_free(transport->owners); + + if (transport->session) + avdtp_unref(transport->session); + + if (transport->nrec_id) + headset_remove_nrec_cb(transport->device, transport->nrec_id); + + if (transport->conn) + dbus_connection_unref(transport->conn); + + g_free(transport->configuration); + g_free(transport->path); + g_free(transport); +} + +static void headset_nrec_changed(struct audio_device *dev, gboolean nrec, + void *user_data) +{ + struct media_transport *transport = user_data; + + DBG(""); + + emit_property_changed(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, "NREC", + DBUS_TYPE_BOOLEAN, &nrec); +} + +struct media_transport *media_transport_create(DBusConnection *conn, + struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, + size_t size) +{ + struct media_transport *transport; + const char *uuid; + static int fd = 0; + + transport = g_new0(struct media_transport, 1); + transport->conn = dbus_connection_ref(conn); + transport->device = device; + transport->endpoint = endpoint; + transport->configuration = g_new(uint8_t, size); + memcpy(transport->configuration, configuration, size); + transport->size = size; + transport->path = g_strdup_printf("%s/fd%d", device->path, fd++); + transport->fd = -1; + + uuid = media_endpoint_get_uuid(endpoint); + if (strcasecmp(uuid, A2DP_SOURCE_UUID) == 0 || + strcasecmp(uuid, A2DP_SINK_UUID) == 0) { + transport->resume = resume_a2dp; + transport->suspend = suspend_a2dp; + transport->cancel = cancel_a2dp; + transport->get_properties = get_properties_a2dp; + transport->set_property = set_property_a2dp; + } else if (strcasecmp(uuid, HFP_AG_UUID) == 0 || + strcasecmp(uuid, HSP_AG_UUID) == 0) { + transport->resume = resume_headset; + transport->suspend = suspend_headset; + transport->cancel = cancel_headset; + transport->get_properties = get_properties_headset; + transport->set_property = set_property_headset; + transport->nrec_id = headset_add_nrec_cb(device, + headset_nrec_changed, + transport); + } else + goto fail; + + if (g_dbus_register_interface(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, + transport_methods, transport_signals, NULL, + transport, media_transport_free) == FALSE) { + error("Could not register transport %s", transport->path); + goto fail; + } + + return transport; + +fail: + media_transport_free(transport); + return NULL; +} + +const char *media_transport_get_path(struct media_transport *transport) +{ + return transport->path; +} + +void media_transport_update_delay(struct media_transport *transport, + uint16_t delay) +{ + /* Check if delay really changed */ + if (transport->delay == delay) + return; + + transport->delay = delay; + + emit_property_changed(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, "Delay", + DBUS_TYPE_UINT16, &transport->delay); +} diff --git a/audio/transport.h b/audio/transport.h new file mode 100644 index 0000000..be4d666 --- /dev/null +++ b/audio/transport.h @@ -0,0 +1,38 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct media_transport; + +struct media_transport *media_transport_create(DBusConnection *conn, + struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, + size_t size); + +void media_transport_destroy(struct media_transport *transport); +const char *media_transport_get_path(struct media_transport *transport); +void media_transport_update_delay(struct media_transport *transport, + uint16_t delay); +void transport_get_properties(struct media_transport *transport, + DBusMessageIter *iter); diff --git a/audio/unix.c b/audio/unix.c new file mode 100644 index 0000000..37c772d --- /dev/null +++ b/audio/unix.c @@ -0,0 +1,1917 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <fcntl.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> +#include <stdint.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sdp.h> +#include <dbus/dbus.h> +#include <glib.h> + +#include "log.h" +#include "ipc.h" +#include "device.h" +#include "manager.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "headset.h" +#include "sink.h" +#include "gateway.h" +#include "unix.h" +#include "glib-helper.h" + +#define check_nul(str) (str[sizeof(str) - 1] == '\0') + +typedef enum { + TYPE_NONE, + TYPE_HEADSET, + TYPE_GATEWAY, + TYPE_SINK, + TYPE_SOURCE +} service_type_t; + +typedef void (*notify_cb_t) (struct audio_device *dev, void *data); + +struct a2dp_data { + struct avdtp *session; + struct avdtp_stream *stream; + struct a2dp_sep *sep; +}; + +struct headset_data { + gboolean locked; +}; + +struct unix_client { + struct audio_device *dev; + GSList *caps; + service_type_t type; + char *interface; + uint8_t seid; + union { + struct a2dp_data a2dp; + struct headset_data hs; + } d; + int sock; + int lock; + int data_fd; /* To be deleted once two phase configuration is fully implemented */ + unsigned int req_id; + unsigned int cb_id; + gboolean (*cancel) (struct audio_device *dev, unsigned int id); +}; + +static GSList *clients = NULL; + +static int unix_sock = -1; + +static void client_free(struct unix_client *client) +{ + DBG("client_free(%p)", client); + + if (client->cancel && client->dev && client->req_id > 0) + client->cancel(client->dev, client->req_id); + + if (client->sock >= 0) + close(client->sock); + + if (client->caps) { + g_slist_foreach(client->caps, (GFunc) g_free, NULL); + g_slist_free(client->caps); + } + + g_free(client->interface); + g_free(client); +} + +static int set_nonblocking(int fd) +{ + long arg; + + arg = fcntl(fd, F_GETFL); + if (arg < 0) + return -errno; + + /* Return if already nonblocking */ + if (arg & O_NONBLOCK) + return 0; + + arg |= O_NONBLOCK; + if (fcntl(fd, F_SETFL, arg) < 0) + return -errno; + + return 0; +} + +/* Pass file descriptor through local domain sockets (AF_LOCAL, formerly + * AF_UNIX) and the sendmsg() system call with the cmsg_type field of a "struct + * cmsghdr" set to SCM_RIGHTS and the data being an integer value equal to the + * handle of the file descriptor to be passed. */ +static int unix_sendmsg_fd(int sock, int fd) +{ + char cmsg_b[CMSG_SPACE(sizeof(int))], m = 'm'; + struct cmsghdr *cmsg; + struct iovec iov = { &m, sizeof(m) }; + struct msghdr msgh; + + memset(&msgh, 0, sizeof(msgh)); + msgh.msg_iov = &iov; + msgh.msg_iovlen = 1; + msgh.msg_control = &cmsg_b; + msgh.msg_controllen = CMSG_LEN(sizeof(int)); + + cmsg = CMSG_FIRSTHDR(&msgh); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + /* Initialize the payload */ + memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); + + return sendmsg(sock, &msgh, MSG_NOSIGNAL); +} + +static void unix_ipc_sendmsg(struct unix_client *client, + const bt_audio_msg_header_t *msg) +{ + const char *type = bt_audio_strtype(msg->type); + const char *name = bt_audio_strname(msg->name); + + DBG("Audio API: %s -> %s", type, name); + + if (send(client->sock, msg, msg->length, 0) < 0) + error("Error %s(%d)", strerror(errno), errno); +} + +static void unix_ipc_error(struct unix_client *client, uint8_t name, int err) +{ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + bt_audio_error_t *rsp = (void *) buf; + + if (!g_slist_find(clients, client)) + return; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_ERROR; + rsp->h.name = name; + rsp->h.length = sizeof(*rsp); + + rsp->posix_errno = err; + + DBG("sending error %s(%d)", strerror(err), err); + unix_ipc_sendmsg(client, &rsp->h); +} + +static service_type_t select_service(struct audio_device *dev, const char *interface) +{ + if (!interface) { + if (dev->sink && avdtp_is_connected(&dev->src, &dev->dst)) + return TYPE_SINK; + else if (dev->source && avdtp_is_connected(&dev->src, + &dev->dst)) + return TYPE_SOURCE; + else if (dev->headset && headset_is_active(dev)) + return TYPE_HEADSET; + else if (dev->sink) + return TYPE_SINK; + else if (dev->source) + return TYPE_SOURCE; + else if (dev->headset) + return TYPE_HEADSET; + } else if (!strcmp(interface, AUDIO_SOURCE_INTERFACE) && dev->source) + return TYPE_SOURCE; + else if (!strcmp(interface, AUDIO_SINK_INTERFACE) && dev->sink) + return TYPE_SINK; + else if (!strcmp(interface, AUDIO_HEADSET_INTERFACE) && dev->headset) + return TYPE_HEADSET; + else if (!strcmp(interface, AUDIO_GATEWAY_INTERFACE) && dev->gateway) + return TYPE_GATEWAY; + + return TYPE_NONE; +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct unix_client *client = user_data; + struct a2dp_data *a2dp = &client->d.a2dp; + + switch (new_state) { + case AVDTP_STATE_IDLE: + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + if (a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + a2dp->stream = NULL; + client->cb_id = 0; + break; + default: + break; + } +} + +static uint8_t headset_generate_capability(struct audio_device *dev, + codec_capabilities_t *codec) +{ + pcm_capabilities_t *pcm; + + codec->seid = BT_A2DP_SEID_RANGE + 1; + codec->transport = BT_CAPABILITIES_TRANSPORT_SCO; + codec->type = BT_HFP_CODEC_PCM; + codec->length = sizeof(*pcm); + + pcm = (void *) codec; + pcm->sampling_rate = 8000; + if (dev->headset) { + if (headset_get_nrec(dev)) + pcm->flags |= BT_PCM_FLAG_NREC; + if (!headset_get_sco_hci(dev)) + pcm->flags |= BT_PCM_FLAG_PCM_ROUTING; + codec->configured = headset_is_active(dev); + codec->lock = headset_get_lock(dev); + } else { + pcm->flags |= BT_PCM_FLAG_NREC; + codec->configured = TRUE; + codec->lock = 0; + } + + return codec->length; +} + +static void headset_discovery_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + uint8_t length; + + client->req_id = 0; + + if (!dev) + goto failed; + + memset(buf, 0, sizeof(buf)); + + length = headset_generate_capability(dev, (void *) rsp->data); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_GET_CAPABILITIES; + rsp->h.length = sizeof(*rsp) + length; + + ba2str(&dev->src, rsp->source); + ba2str(&dev->dst, rsp->destination); + strncpy(rsp->object, dev->path, sizeof(rsp->object)); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("discovery failed"); + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); +} + +static void headset_setup_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + + client->req_id = 0; + + if (!dev) + goto failed; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_SET_CONFIGURATION; + rsp->h.length = sizeof(*rsp); + + rsp->link_mtu = 48; + + client->data_fd = headset_get_sco_fd(dev); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("config failed"); + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); +} + +static void gateway_setup_complete(struct audio_device *dev, GError *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + + if (err) { + unix_ipc_error(client, BT_SET_CONFIGURATION, err->code); + return; + } + + client->req_id = 0; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_SET_CONFIGURATION; + rsp->h.length = sizeof(*rsp); + + rsp->link_mtu = 48; + + client->data_fd = gateway_get_sco_fd(dev); + + unix_ipc_sendmsg(client, &rsp->h); +} + +static void headset_resume_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + + client->req_id = 0; + + if (!dev) + goto failed; + + client->data_fd = headset_get_sco_fd(dev); + if (client->data_fd < 0) { + error("Unable to get a SCO fd"); + goto failed; + } + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_START_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + memset(buf, 0, sizeof(buf)); + ind->h.type = BT_INDICATION; + ind->h.name = BT_NEW_STREAM; + ind->h.length = sizeof(*ind); + + unix_ipc_sendmsg(client, &ind->h); + + if (unix_sendmsg_fd(client->sock, client->data_fd) < 0) { + error("unix_sendmsg_fd: %s(%d)", strerror(errno), errno); + goto failed; + } + + return; + +failed: + error("headset_resume_complete: resume failed"); + unix_ipc_error(client, BT_START_STREAM, EIO); +} + +static void gateway_resume_complete(struct audio_device *dev, GError *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + + if (err) { + unix_ipc_error(client, BT_START_STREAM, err->code); + return; + } + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_START_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + memset(buf, 0, sizeof(buf)); + ind->h.type = BT_INDICATION; + ind->h.name = BT_NEW_STREAM; + ind->h.length = sizeof(*ind); + + unix_ipc_sendmsg(client, &ind->h); + + client->data_fd = gateway_get_sco_fd(dev); + if (unix_sendmsg_fd(client->sock, client->data_fd) < 0) { + error("unix_sendmsg_fd: %s(%d)", strerror(errno), errno); + unix_ipc_error(client, BT_START_STREAM, EIO); + } + + client->req_id = 0; +} + +static void headset_suspend_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_stop_stream_rsp *rsp = (void *) buf; + + if (!dev) + goto failed; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_STOP_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("suspend failed"); + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void print_mpeg12(struct mpeg_codec_cap *mpeg) +{ + DBG("Media Codec: MPEG12" + " Channel Modes: %s%s%s%s" + " Frequencies: %s%s%s%s%s%s" + " Layers: %s%s%s" + " CRC: %s", + mpeg->channel_mode & MPEG_CHANNEL_MODE_MONO ? "Mono " : "", + mpeg->channel_mode & MPEG_CHANNEL_MODE_DUAL_CHANNEL ? + "DualChannel " : "", + mpeg->channel_mode & MPEG_CHANNEL_MODE_STEREO ? "Stereo " : "", + mpeg->channel_mode & MPEG_CHANNEL_MODE_JOINT_STEREO ? + "JointStereo " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_16000 ? "16Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_22050 ? "22.05Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_24000 ? "24Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_32000 ? "32Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_44100 ? "44.1Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_48000 ? "48Khz " : "", + mpeg->layer & MPEG_LAYER_MP1 ? "1 " : "", + mpeg->layer & MPEG_LAYER_MP2 ? "2 " : "", + mpeg->layer & MPEG_LAYER_MP3 ? "3 " : "", + mpeg->crc ? "Yes" : "No"); +} + +static void print_sbc(struct sbc_codec_cap *sbc) +{ + DBG("Media Codec: SBC" + " Channel Modes: %s%s%s%s" + " Frequencies: %s%s%s%s" + " Subbands: %s%s" + " Blocks: %s%s%s%s" + " Bitpool: %d-%d", + sbc->channel_mode & SBC_CHANNEL_MODE_MONO ? "Mono " : "", + sbc->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL ? + "DualChannel " : "", + sbc->channel_mode & SBC_CHANNEL_MODE_STEREO ? "Stereo " : "", + sbc->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO ? "JointStereo" : "", + sbc->frequency & SBC_SAMPLING_FREQ_16000 ? "16Khz " : "", + sbc->frequency & SBC_SAMPLING_FREQ_32000 ? "32Khz " : "", + sbc->frequency & SBC_SAMPLING_FREQ_44100 ? "44.1Khz " : "", + sbc->frequency & SBC_SAMPLING_FREQ_48000 ? "48Khz " : "", + sbc->subbands & SBC_SUBBANDS_4 ? "4 " : "", + sbc->subbands & SBC_SUBBANDS_8 ? "8 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_4 ? "4 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_8 ? "8 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_12 ? "12 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_16 ? "16 " : "", + sbc->min_bitpool, sbc->max_bitpool); +} + +static int a2dp_append_codec(struct bt_get_capabilities_rsp *rsp, + struct avdtp_service_capability *cap, + uint8_t seid, + uint8_t type, + uint8_t configured, + uint8_t lock) +{ + struct avdtp_media_codec_capability *codec_cap = (void *) cap->data; + codec_capabilities_t *codec = (void *) rsp + rsp->h.length; + size_t space_left; + + if (rsp->h.length > BT_SUGGESTED_BUFFER_SIZE) + return -ENOMEM; + + space_left = BT_SUGGESTED_BUFFER_SIZE - rsp->h.length; + + /* endianess prevent direct cast */ + if (codec_cap->media_codec_type == A2DP_CODEC_SBC) { + struct sbc_codec_cap *sbc_cap = (void *) codec_cap; + sbc_capabilities_t *sbc = (void *) codec; + + if (space_left < sizeof(sbc_capabilities_t)) + return -ENOMEM; + + if (type == AVDTP_SEP_TYPE_SINK) + codec->type = BT_A2DP_SBC_SINK; + else if (type == AVDTP_SEP_TYPE_SOURCE) + codec->type = BT_A2DP_SBC_SOURCE; + else + return -EINVAL; + + codec->length = sizeof(sbc_capabilities_t); + + sbc->channel_mode = sbc_cap->channel_mode; + sbc->frequency = sbc_cap->frequency; + sbc->allocation_method = sbc_cap->allocation_method; + sbc->subbands = sbc_cap->subbands; + sbc->block_length = sbc_cap->block_length; + sbc->min_bitpool = sbc_cap->min_bitpool; + sbc->max_bitpool = sbc_cap->max_bitpool; + + print_sbc(sbc_cap); + } else if (codec_cap->media_codec_type == A2DP_CODEC_MPEG12) { + struct mpeg_codec_cap *mpeg_cap = (void *) codec_cap; + mpeg_capabilities_t *mpeg = (void *) codec; + + if (space_left < sizeof(mpeg_capabilities_t)) + return -ENOMEM; + + if (type == AVDTP_SEP_TYPE_SINK) + codec->type = BT_A2DP_MPEG12_SINK; + else if (type == AVDTP_SEP_TYPE_SOURCE) + codec->type = BT_A2DP_MPEG12_SOURCE; + else + return -EINVAL; + + codec->length = sizeof(mpeg_capabilities_t); + + mpeg->channel_mode = mpeg_cap->channel_mode; + mpeg->crc = mpeg_cap->crc; + mpeg->layer = mpeg_cap->layer; + mpeg->frequency = mpeg_cap->frequency; + mpeg->mpf = mpeg_cap->mpf; + mpeg->bitrate = mpeg_cap->bitrate; + + print_mpeg12(mpeg_cap); + } else { + size_t codec_length, type_length, total_length; + + codec_length = cap->length - (sizeof(struct avdtp_service_capability) + + sizeof(struct avdtp_media_codec_capability)); + type_length = sizeof(codec_cap->media_codec_type); + total_length = type_length + codec_length + + sizeof(codec_capabilities_t); + + if (space_left < total_length) + return -ENOMEM; + + if (type == AVDTP_SEP_TYPE_SINK) + codec->type = BT_A2DP_UNKNOWN_SINK; + else if (type == AVDTP_SEP_TYPE_SOURCE) + codec->type = BT_A2DP_UNKNOWN_SOURCE; + else + return -EINVAL; + + codec->length = total_length; + memcpy(codec->data, &codec_cap->media_codec_type, type_length); + memcpy(codec->data + type_length, codec_cap->data, + codec_length); + } + + codec->seid = seid; + codec->configured = configured; + codec->lock = lock; + rsp->h.length += codec->length; + + DBG("Append %s seid %d - length %d - total %d", + configured ? "configured" : "", seid, codec->length, + rsp->h.length); + + return 0; +} + +static void a2dp_discovery_complete(struct avdtp *session, GSList *seps, + struct avdtp_error *err, + void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + struct a2dp_data *a2dp = &client->d.a2dp; + GSList *l; + + if (!g_slist_find(clients, client)) { + DBG("Client disconnected during discovery"); + return; + } + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + client->req_id = 0; + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_GET_CAPABILITIES; + rsp->h.length = sizeof(*rsp); + ba2str(&client->dev->src, rsp->source); + ba2str(&client->dev->dst, rsp->destination); + strncpy(rsp->object, client->dev->path, sizeof(rsp->object)); + + for (l = seps; l; l = g_slist_next(l)) { + struct avdtp_remote_sep *rsep = l->data; + struct a2dp_sep *sep; + struct avdtp_service_capability *cap; + struct avdtp_stream *stream; + uint8_t type, seid, configured = 0, lock = 0; + GSList *cl; + + type = avdtp_get_type(rsep); + + if (type != AVDTP_SEP_TYPE_SINK && + type != AVDTP_SEP_TYPE_SOURCE) + continue; + + cap = avdtp_get_codec(rsep); + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + seid = avdtp_get_seid(rsep); + + if (client->seid != 0 && client->seid != seid) + continue; + + stream = avdtp_get_stream(rsep); + if (stream) { + configured = 1; + if (client->seid == seid) + cap = avdtp_stream_get_codec(stream); + } + + for (cl = clients; cl; cl = cl->next) { + struct unix_client *c = cl->data; + struct a2dp_data *ca2dp = &c->d.a2dp; + + if (ca2dp->session == session && c->seid == seid) { + lock = c->lock; + break; + } + } + + sep = a2dp_get_sep(session, stream); + if (sep && a2dp_sep_get_lock(sep)) + lock = BT_WRITE_LOCK; + + a2dp_append_codec(rsp, cap, seid, type, configured, lock); + } + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("discovery failed"); + unix_ipc_error(client, BT_GET_CAPABILITIES, EIO); + + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + + avdtp_unref(a2dp->session); + a2dp->session = NULL; + a2dp->stream = NULL; +} + +static void a2dp_config_complete(struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + struct a2dp_data *a2dp = &client->d.a2dp; + uint16_t imtu, omtu; + GSList *caps; + + client->req_id = 0; + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + + if (!stream) + goto failed; + + if (client->cb_id > 0) + avdtp_stream_remove_cb(a2dp->session, a2dp->stream, + client->cb_id); + + a2dp->sep = sep; + a2dp->stream = stream; + + if (!avdtp_stream_get_transport(stream, &client->data_fd, &imtu, &omtu, + &caps)) { + error("Unable to get stream transport"); + goto failed; + } + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_SET_CONFIGURATION; + rsp->h.length = sizeof(*rsp); + + /* FIXME: Use imtu when fd_opt is CFG_FD_OPT_READ */ + rsp->link_mtu = omtu; + + unix_ipc_sendmsg(client, &rsp->h); + + client->cb_id = avdtp_stream_add_cb(session, stream, + stream_state_changed, client); + + return; + +failed: + error("config failed"); + + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); + + avdtp_unref(a2dp->session); + + a2dp->session = NULL; + a2dp->stream = NULL; + a2dp->sep = NULL; +} + +static void a2dp_resume_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + struct a2dp_data *a2dp = &client->d.a2dp; + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_START_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + memset(buf, 0, sizeof(buf)); + ind->h.type = BT_RESPONSE; + ind->h.name = BT_NEW_STREAM; + rsp->h.length = sizeof(*ind); + + unix_ipc_sendmsg(client, &ind->h); + + if (unix_sendmsg_fd(client->sock, client->data_fd) < 0) { + error("unix_sendmsg_fd: %s(%d)", strerror(errno), errno); + goto failed; + } + + return; + +failed: + error("resume failed"); + + unix_ipc_error(client, BT_START_STREAM, EIO); + + if (client->cb_id > 0) { + avdtp_stream_remove_cb(a2dp->session, a2dp->stream, + client->cb_id); + client->cb_id = 0; + } + + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + + avdtp_unref(a2dp->session); + a2dp->session = NULL; + a2dp->stream = NULL; +} + +static void a2dp_suspend_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_stop_stream_rsp *rsp = (void *) buf; + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_STOP_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("suspend failed"); + + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void start_discovery(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp; + int err = 0; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->session) + a2dp->session = avdtp_get(&dev->src, &dev->dst); + + if (!a2dp->session) { + error("Unable to get a session"); + goto failed; + } + + err = avdtp_discover(a2dp->session, a2dp_discovery_complete, + client); + if (err) { + if (a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + goto failed; + } + break; + + case TYPE_HEADSET: + case TYPE_GATEWAY: + headset_discovery_complete(dev, client); + break; + + default: + error("No known services for device"); + goto failed; + } + + client->dev = dev; + + return; + +failed: + unix_ipc_error(client, BT_GET_CAPABILITIES, err ? : EIO); +} + +static void open_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_rsp *rsp = (void *) buf; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_OPEN; + rsp->h.length = sizeof(*rsp); + + ba2str(&dev->src, rsp->source); + ba2str(&dev->dst, rsp->destination); + strncpy(rsp->object, dev->path, sizeof(rsp->object)); + + unix_ipc_sendmsg(client, &rsp->h); +} + +static void start_open(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp; + struct headset_data *hs; + struct avdtp_remote_sep *rsep; + gboolean unref_avdtp_on_fail = FALSE; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->session) { + a2dp->session = avdtp_get(&dev->src, &dev->dst); + unref_avdtp_on_fail = TRUE; + } + + if (!a2dp->session) { + error("Unable to get a session"); + goto failed; + } + + if (a2dp->sep) { + error("Client already has an opened session"); + goto failed; + } + + rsep = avdtp_get_remote_sep(a2dp->session, client->seid); + if (!rsep) { + error("Invalid seid %d", client->seid); + goto failed; + } + + a2dp->sep = a2dp_get(a2dp->session, rsep); + if (!a2dp->sep) { + error("seid %d not available or locked", client->seid); + goto failed; + } + + if (!a2dp_sep_lock(a2dp->sep, a2dp->session)) { + error("Unable to open seid %d", client->seid); + a2dp->sep = NULL; + goto failed; + } + + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (hs->locked) { + error("Client already has an opened session"); + goto failed; + } + + hs->locked = headset_lock(dev, client->lock); + if (!hs->locked) { + error("Unable to open seid %d", client->seid); + goto failed; + } + break; + + case TYPE_GATEWAY: + break; + default: + error("No known services for device"); + goto failed; + } + + client->dev = dev; + + open_complete(dev, client); + + return; + +failed: + if (unref_avdtp_on_fail && a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + unix_ipc_error(client, BT_OPEN, EINVAL); +} + +static void start_config(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp; + struct headset_data *hs; + unsigned int id; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->session) + a2dp->session = avdtp_get(&dev->src, &dev->dst); + + if (!a2dp->session) { + error("Unable to get a session"); + goto failed; + } + + if (!a2dp->sep) { + error("seid %d not opened", client->seid); + goto failed; + } + + id = a2dp_config(a2dp->session, a2dp->sep, a2dp_config_complete, + client->caps, client); + client->cancel = a2dp_cancel; + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (!hs->locked) { + error("seid %d not opened", client->seid); + goto failed; + } + + id = headset_config_stream(dev, TRUE, headset_setup_complete, + client); + client->cancel = headset_cancel_stream; + break; + case TYPE_GATEWAY: + if (gateway_config_stream(dev, gateway_setup_complete, client) >= 0) { + client->cancel = gateway_cancel_stream; + id = 1; + } else + id = 0; + break; + + default: + error("No known services for device"); + goto failed; + } + + if (id == 0) { + error("config failed"); + goto failed; + } + + client->req_id = id; + g_slist_free(client->caps); + client->caps = NULL; + + return; + +failed: + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); +} + +static void start_resume(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp = NULL; + struct headset_data *hs; + unsigned int id; + struct avdtp *session = NULL; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->sep) { + error("seid not opened"); + goto failed; + } + + if (!a2dp->session) { + session = avdtp_get(&dev->src, &dev->dst); + if (!session) { + error("Unable to get a session"); + goto failed; + } + a2dp->session = session; + } + + id = a2dp_resume(a2dp->session, a2dp->sep, a2dp_resume_complete, + client); + client->cancel = a2dp_cancel; + + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (!hs->locked) { + error("seid not opened"); + goto failed; + } + + id = headset_request_stream(dev, headset_resume_complete, + client); + client->cancel = headset_cancel_stream; + break; + + case TYPE_GATEWAY: + if (gateway_request_stream(dev, gateway_resume_complete, client)) + id = 1; + else + id = 0; + client->cancel = gateway_cancel_stream; + break; + + default: + error("No known services for device"); + goto failed; + } + + if (id == 0) { + error("start_resume: resume failed"); + goto failed; + } + + client->req_id = id; + + return; + +failed: + if (session) { + avdtp_unref(session); + a2dp->session = NULL; + } + + unix_ipc_error(client, BT_START_STREAM, EIO); +} + +static void start_suspend(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp = NULL; + struct headset_data *hs; + unsigned int id; + struct avdtp *session = NULL; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->sep) { + error("seid not opened"); + goto failed; + } + + if (!a2dp->session) { + session = avdtp_get(&dev->src, &dev->dst); + if (!session) { + error("Unable to get a session"); + goto failed; + } + a2dp->session = session; + } + + if (!a2dp->sep) { + error("Unable to get a sep"); + goto failed; + } + + id = a2dp_suspend(a2dp->session, a2dp->sep, + a2dp_suspend_complete, client); + client->cancel = a2dp_cancel; + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (!hs->locked) { + error("seid not opened"); + goto failed; + } + + id = headset_suspend_stream(dev, headset_suspend_complete, + client); + client->cancel = headset_cancel_stream; + break; + + case TYPE_GATEWAY: + gateway_suspend_stream(dev); + client->cancel = gateway_cancel_stream; + headset_suspend_complete(dev, client); + id = 1; + break; + + default: + error("No known services for device"); + goto failed; + } + + if (id == 0) { + error("suspend failed"); + goto failed; + } + + return; + +failed: + if (session) { + avdtp_unref(session); + a2dp->session = NULL; + } + + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void close_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_close_rsp *rsp = (void *) buf; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_CLOSE; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; +} + +static void start_close(struct audio_device *dev, struct unix_client *client, + gboolean reply) +{ + struct a2dp_data *a2dp; + struct headset_data *hs; + + if (!client->dev) + goto failed; + + switch (client->type) { + case TYPE_HEADSET: + hs = &client->d.hs; + + if (client->dev && hs->locked) { + headset_unlock(client->dev, client->lock); + hs->locked = FALSE; + } + break; + case TYPE_GATEWAY: + break; + case TYPE_SOURCE: + case TYPE_SINK: + a2dp = &client->d.a2dp; + + if (client->cb_id > 0) { + avdtp_stream_remove_cb(a2dp->session, a2dp->stream, + client->cb_id); + client->cb_id = 0; + } + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + if (a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + a2dp->stream = NULL; + break; + default: + error("No known services for device"); + goto failed; + } + + if (!reply) + return; + + close_complete(dev, client); + client->dev = NULL; + + return; + +failed: + if (reply) + unix_ipc_error(client, BT_STOP_STREAM, EINVAL); +} + +static void handle_getcapabilities_req(struct unix_client *client, + struct bt_get_capabilities_req *req) +{ + struct audio_device *dev; + bdaddr_t src, dst; + int err = EIO; + const char *interface; + + if (!check_nul(req->source) || !check_nul(req->destination) || + !check_nul(req->object)) { + err = EINVAL; + goto failed; + } + + str2ba(req->source, &src); + str2ba(req->destination, &dst); + + if (!manager_find_device(req->object, &src, &dst, NULL, FALSE)) + goto failed; + + if (req->transport == BT_CAPABILITIES_TRANSPORT_SCO) + interface = AUDIO_HEADSET_INTERFACE; + else if (req->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + interface = AUDIO_SINK_INTERFACE; + else + interface = client->interface; + + dev = manager_find_device(req->object, &src, &dst, interface, TRUE); + if (!dev && (req->flags & BT_FLAG_AUTOCONNECT)) + dev = manager_find_device(req->object, &src, &dst, + interface, FALSE); + + if (!dev) { + if (req->transport == BT_CAPABILITIES_TRANSPORT_SCO) + interface = AUDIO_GATEWAY_INTERFACE; + else if (req->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + interface = AUDIO_SOURCE_INTERFACE; + else + interface = NULL; + dev = manager_find_device(req->object, &src, &dst, + interface, TRUE); + if (!dev && (req->flags & BT_FLAG_AUTOCONNECT)) + dev = manager_find_device(req->object, &src, &dst, + interface, FALSE); + } + + if (!dev) { + error("Unable to find a matching device"); + goto failed; + } + + client->type = select_service(dev, interface); + if (client->type == TYPE_NONE) { + error("No matching service found"); + goto failed; + } + + if (g_strcmp0(interface, client->interface) != 0) { + g_free(client->interface); + client->interface = g_strdup(interface); + } + + client->seid = req->seid; + + start_discovery(dev, client); + + return; + +failed: + unix_ipc_error(client, BT_GET_CAPABILITIES, err); +} + +static int handle_sco_open(struct unix_client *client, struct bt_open_req *req) +{ + if (!client->interface) + client->interface = g_strdup(AUDIO_HEADSET_INTERFACE); + else if (!g_str_equal(client->interface, AUDIO_HEADSET_INTERFACE) && + !g_str_equal(client->interface, AUDIO_GATEWAY_INTERFACE)) + return -EIO; + + DBG("open sco - object=%s source=%s destination=%s lock=%s%s", + strcmp(req->object, "") ? req->object : "ANY", + strcmp(req->source, "") ? req->source : "ANY", + strcmp(req->destination, "") ? req->destination : "ANY", + req->lock & BT_READ_LOCK ? "read" : "", + req->lock & BT_WRITE_LOCK ? "write" : ""); + + return 0; +} + +static int handle_a2dp_open(struct unix_client *client, struct bt_open_req *req) +{ + if (!client->interface) + /* FIXME: are we treating a sink or a source? */ + client->interface = g_strdup(AUDIO_SINK_INTERFACE); + else if (!g_str_equal(client->interface, AUDIO_SINK_INTERFACE) && + !g_str_equal(client->interface, AUDIO_SOURCE_INTERFACE)) + return -EIO; + + DBG("open a2dp - object=%s source=%s destination=%s lock=%s%s", + strcmp(req->object, "") ? req->object : "ANY", + strcmp(req->source, "") ? req->source : "ANY", + strcmp(req->destination, "") ? req->destination : "ANY", + req->lock & BT_READ_LOCK ? "read" : "", + req->lock & BT_WRITE_LOCK ? "write" : ""); + + return 0; +} + +static void handle_open_req(struct unix_client *client, struct bt_open_req *req) +{ + struct audio_device *dev; + bdaddr_t src, dst; + int err = 0; + + if (!check_nul(req->source) || !check_nul(req->destination) || + !check_nul(req->object)) { + err = EINVAL; + goto failed; + } + + str2ba(req->source, &src); + str2ba(req->destination, &dst); + + if (req->seid > BT_A2DP_SEID_RANGE) { + err = handle_sco_open(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } else { + err = handle_a2dp_open(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } + + if (!manager_find_device(req->object, &src, &dst, NULL, FALSE)) + goto failed; + + dev = manager_find_device(req->object, &src, &dst, client->interface, + TRUE); + if (!dev) + dev = manager_find_device(req->object, &src, &dst, + client->interface, FALSE); + + if (!dev) + goto failed; + + client->seid = req->seid; + client->lock = req->lock; + + start_open(dev, client); + + return; + +failed: + unix_ipc_error(client, BT_OPEN, err ? : EIO); +} + +static int handle_sco_transport(struct unix_client *client, + struct bt_set_configuration_req *req) +{ + struct audio_device *dev = client->dev; + + if (!client->interface) { + if (dev->headset) + client->interface = g_strdup(AUDIO_HEADSET_INTERFACE); + else if (dev->gateway) + client->interface = g_strdup(AUDIO_GATEWAY_INTERFACE); + else + return -EIO; + } else if (!g_str_equal(client->interface, AUDIO_HEADSET_INTERFACE) && + !g_str_equal(client->interface, AUDIO_GATEWAY_INTERFACE)) + return -EIO; + + return 0; +} + +static int handle_a2dp_transport(struct unix_client *client, + struct bt_set_configuration_req *req) +{ + struct avdtp_service_capability *media_transport, *media_codec; + struct sbc_codec_cap sbc_cap; + struct mpeg_codec_cap mpeg_cap; + + if (!client->interface) + /* FIXME: are we treating a sink or a source? */ + client->interface = g_strdup(AUDIO_SINK_INTERFACE); + else if (!g_str_equal(client->interface, AUDIO_SINK_INTERFACE) && + !g_str_equal(client->interface, AUDIO_SOURCE_INTERFACE)) + return -EIO; + + if (client->caps) { + g_slist_foreach(client->caps, (GFunc) g_free, NULL); + g_slist_free(client->caps); + client->caps = NULL; + } + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + client->caps = g_slist_append(client->caps, media_transport); + + if (req->codec.type == BT_A2DP_MPEG12_SINK || + req->codec.type == BT_A2DP_MPEG12_SOURCE) { + mpeg_capabilities_t *mpeg = (void *) &req->codec; + + memset(&mpeg_cap, 0, sizeof(mpeg_cap)); + + mpeg_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + mpeg_cap.cap.media_codec_type = A2DP_CODEC_MPEG12; + mpeg_cap.channel_mode = mpeg->channel_mode; + mpeg_cap.crc = mpeg->crc; + mpeg_cap.layer = mpeg->layer; + mpeg_cap.frequency = mpeg->frequency; + mpeg_cap.mpf = mpeg->mpf; + mpeg_cap.bitrate = mpeg->bitrate; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &mpeg_cap, + sizeof(mpeg_cap)); + + print_mpeg12(&mpeg_cap); + } else if (req->codec.type == BT_A2DP_SBC_SINK || + req->codec.type == BT_A2DP_SBC_SOURCE) { + sbc_capabilities_t *sbc = (void *) &req->codec; + + memset(&sbc_cap, 0, sizeof(sbc_cap)); + + sbc_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + sbc_cap.cap.media_codec_type = A2DP_CODEC_SBC; + sbc_cap.channel_mode = sbc->channel_mode; + sbc_cap.frequency = sbc->frequency; + sbc_cap.allocation_method = sbc->allocation_method; + sbc_cap.subbands = sbc->subbands; + sbc_cap.block_length = sbc->block_length; + sbc_cap.min_bitpool = sbc->min_bitpool; + sbc_cap.max_bitpool = sbc->max_bitpool; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, + sizeof(sbc_cap)); + + print_sbc(&sbc_cap); + } else + return -EINVAL; + + client->caps = g_slist_append(client->caps, media_codec); + + return 0; +} + +static void handle_setconfiguration_req(struct unix_client *client, + struct bt_set_configuration_req *req) +{ + int err = 0; + + if (req->codec.seid != client->seid) { + error("Unable to set configuration: seid %d not opened", + req->codec.seid); + goto failed; + } + + if (!client->dev) + goto failed; + + if (req->codec.transport == BT_CAPABILITIES_TRANSPORT_SCO) { + err = handle_sco_transport(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } else if (req->codec.transport == BT_CAPABILITIES_TRANSPORT_A2DP) { + err = handle_a2dp_transport(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } + + start_config(client->dev, client); + + return; + +failed: + unix_ipc_error(client, BT_SET_CONFIGURATION, err ? : EIO); +} + +static void handle_streamstart_req(struct unix_client *client, + struct bt_start_stream_req *req) +{ + if (!client->dev) + goto failed; + + start_resume(client->dev, client); + + return; + +failed: + unix_ipc_error(client, BT_START_STREAM, EIO); +} + +static void handle_streamstop_req(struct unix_client *client, + struct bt_stop_stream_req *req) +{ + if (!client->dev) + goto failed; + + start_suspend(client->dev, client); + + return; + +failed: + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void handle_close_req(struct unix_client *client, + struct bt_close_req *req) +{ + if (!client->dev) + goto failed; + + start_close(client->dev, client, TRUE); + + return; + +failed: + unix_ipc_error(client, BT_CLOSE, EIO); +} + +static void handle_control_req(struct unix_client *client, + struct bt_control_req *req) +{ + /* FIXME: really implement that */ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_CONTROL; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); +} + +static void handle_delay_report_req(struct unix_client *client, + struct bt_delay_report_req *req) +{ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + struct a2dp_data *a2dp; + int err; + + if (!client->dev) { + err = -ENODEV; + goto failed; + } + + switch (client->type) { + case TYPE_HEADSET: + case TYPE_GATEWAY: + err = -EINVAL; + goto failed; + case TYPE_SOURCE: + case TYPE_SINK: + a2dp = &client->d.a2dp; + if (a2dp->session && a2dp->stream) { + err = avdtp_delay_report(a2dp->session, a2dp->stream, + req->delay); + if (err < 0) + goto failed; + } else { + err = -EINVAL; + goto failed; + } + break; + default: + error("No known services for device"); + err = -EINVAL; + goto failed; + } + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_DELAY_REPORT; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + unix_ipc_error(client, BT_DELAY_REPORT, -err); +} + +static gboolean client_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + bt_audio_msg_header_t *msghdr = (void *) buf; + struct unix_client *client = data; + int len; + const char *type, *name; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) { + DBG("Unix client disconnected (fd=%d)", client->sock); + + goto failed; + } + + memset(buf, 0, sizeof(buf)); + + len = recv(client->sock, buf, sizeof(buf), 0); + if (len < 0) { + error("recv: %s (%d)", strerror(errno), errno); + goto failed; + } + + type = bt_audio_strtype(msghdr->type); + name = bt_audio_strname(msghdr->name); + + DBG("Audio API: %s <- %s", type, name); + + if (msghdr->length != len) { + error("Invalid message: length mismatch"); + goto failed; + } + + switch (msghdr->name) { + case BT_GET_CAPABILITIES: + handle_getcapabilities_req(client, + (struct bt_get_capabilities_req *) msghdr); + break; + case BT_OPEN: + handle_open_req(client, + (struct bt_open_req *) msghdr); + break; + case BT_SET_CONFIGURATION: + handle_setconfiguration_req(client, + (struct bt_set_configuration_req *) msghdr); + break; + case BT_START_STREAM: + handle_streamstart_req(client, + (struct bt_start_stream_req *) msghdr); + break; + case BT_STOP_STREAM: + handle_streamstop_req(client, + (struct bt_stop_stream_req *) msghdr); + break; + case BT_CLOSE: + handle_close_req(client, + (struct bt_close_req *) msghdr); + break; + case BT_CONTROL: + handle_control_req(client, + (struct bt_control_req *) msghdr); + break; + case BT_DELAY_REPORT: + handle_delay_report_req(client, + (struct bt_delay_report_req *) msghdr); + break; + default: + error("Audio API: received unexpected message name %d", + msghdr->name); + } + + return TRUE; + +failed: + clients = g_slist_remove(clients, client); + start_close(client->dev, client, FALSE); + client_free(client); + return FALSE; +} + +static gboolean server_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + struct sockaddr_un addr; + socklen_t addrlen; + int sk, cli_sk; + struct unix_client *client; + GIOChannel *io; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) { + g_io_channel_shutdown(chan, TRUE, NULL); + return FALSE; + } + + sk = g_io_channel_unix_get_fd(chan); + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + cli_sk = accept(sk, (struct sockaddr *) &addr, &addrlen); + if (cli_sk < 0) { + error("accept: %s (%d)", strerror(errno), errno); + return TRUE; + } + + DBG("Accepted new client connection on unix socket (fd=%d)", cli_sk); + set_nonblocking(cli_sk); + + client = g_new0(struct unix_client, 1); + client->sock = cli_sk; + clients = g_slist_append(clients, client); + + io = g_io_channel_unix_new(cli_sk); + g_io_add_watch(io, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + client_cb, client); + g_io_channel_unref(io); + + return TRUE; +} + +void unix_device_removed(struct audio_device *dev) +{ + GSList *l; + + DBG("unix_device_removed(%p)", dev); + + l = clients; + while (l) { + struct unix_client *client = l->data; + + l = l->next; + + if (client->dev == dev) { + clients = g_slist_remove(clients, client); + start_close(client->dev, client, FALSE); + client_free(client); + } + } +} + +void unix_delay_report(struct audio_device *dev, uint8_t seid, uint16_t delay) +{ + GSList *l; + struct bt_delay_report_ind ind; + + DBG("unix_delay_report(%p): %u.%ums", dev, delay / 10, delay % 10); + + memset(&ind, 0, sizeof(ind)); + ind.h.type = BT_INDICATION; + ind.h.name = BT_DELAY_REPORT; + ind.h.length = sizeof(ind); + ind.delay = delay; + + for (l = clients; l != NULL; l = g_slist_next(l)) { + struct unix_client *client = l->data; + + if (client->dev != dev || client->seid != seid) + continue; + + unix_ipc_sendmsg(client, (void *) &ind); + } +} + +int unix_init(void) +{ + GIOChannel *io; + struct sockaddr_un addr = { + AF_UNIX, BT_IPC_SOCKET_NAME + }; + + int sk, err; + + sk = socket(PF_LOCAL, SOCK_STREAM, 0); + if (sk < 0) { + err = errno; + error("Can't create unix socket: %s (%d)", strerror(err), err); + return -err; + } + + if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + error("Can't bind unix socket: %s (%d)", strerror(errno), + errno); + close(sk); + return -1; + } + + set_nonblocking(sk); + + if (listen(sk, 1) < 0) { + error("Can't listen on unix socket: %s (%d)", + strerror(errno), errno); + close(sk); + return -1; + } + + unix_sock = sk; + + io = g_io_channel_unix_new(sk); + g_io_add_watch(io, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + server_cb, NULL); + g_io_channel_unref(io); + + DBG("Unix socket created: %d", sk); + + return 0; +} + +void unix_exit(void) +{ + g_slist_foreach(clients, (GFunc) client_free, NULL); + g_slist_free(clients); + if (unix_sock >= 0) { + close(unix_sock); + unix_sock = -1; + } +} diff --git a/audio/unix.h b/audio/unix.h new file mode 100644 index 0000000..74ca16d --- /dev/null +++ b/audio/unix.h @@ -0,0 +1,30 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +void unix_device_removed(struct audio_device *dev); + +void unix_delay_report(struct audio_device *dev, uint8_t seid, uint16_t delay); + +int unix_init(void); +void unix_exit(void); |