Linux-Bluetooth Archive on lore.kernel.org
 help / Atom feed
* [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP
@ 2019-01-08 15:49 Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 2/6] a2dp: Expose " Luiz Augusto von Dentz
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2019-01-08 15:49 UTC (permalink / raw)
  To: linux-bluetooth

From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>

This adds the possibility to expose remote SEP using MediaEndpoint
interface to allow setting a configuration.
---
 doc/media-api.txt | 27 +++++++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/doc/media-api.txt b/doc/media-api.txt
index b5ad2db12..af9485342 100644
--- a/doc/media-api.txt
+++ b/doc/media-api.txt
@@ -500,14 +500,23 @@ Properties	object Player [readonly]
 MediaEndpoint1 hierarchy
 ========================
 
-Service		unique name
+Service		unique name (Server role)
+		org.bluez (Client role)
 Interface	org.bluez.MediaEndpoint1
-Object path	freely definable
+Object path	freely definable (Server role)
+		[variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/sepX
+		(Client role)
 
 Methods		void SetConfiguration(object transport, dict properties)
 
 			Set configuration for the transport.
 
+			For client role transport must be set with a server
+			endpoint oject which will be configured and the
+			properties must contain the following properties:
+
+				array{byte} Capabilities
+
 		array{byte} SelectConfiguration(array{byte} capabilities)
 
 			Select preferable configuration from the supported
@@ -532,6 +541,20 @@ Methods		void SetConfiguration(object transport, dict properties)
 			endpoint, because when this method gets called it has
 			already been unregistered.
 
+Properties	string UUID [readonly, optional]:
+
+			UUID of the profile which the endpoint is for.
+
+		byte Codec [readonly, optional]:
+
+			Assigned number of codec that the endpoint implements.
+			The values should match the profile specification which
+			is indicated by the UUID.
+
+		array{byte} Capabilities [readonly, optional]:
+
+			Capabilities blob, it is used as it is so the size and
+			byte order must match.
 
 MediaTransport1 hierarchy
 =========================
-- 
2.17.2


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH BlueZ 2/6] a2dp: Expose remote SEP
  2019-01-08 15:49 [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP Luiz Augusto von Dentz
@ 2019-01-08 15:49 ` " Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 3/6] doc/media-api: Add Endpoint property to MediaTransport Luiz Augusto von Dentz
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2019-01-08 15:49 UTC (permalink / raw)
  To: linux-bluetooth

From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>

This implements MediaEndpoint for remote SEP which can be used by
clients to switch configuration on demand.
---
 profiles/audio/a2dp.c  | 352 ++++++++++++++++++++++++++++++++++++++++-
 profiles/audio/a2dp.h  |   1 +
 profiles/audio/avdtp.c |  10 ++
 profiles/audio/avdtp.h |   4 +
 profiles/audio/media.c |   8 +
 5 files changed, 368 insertions(+), 7 deletions(-)

diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
index 344459332..4fa7e3d9b 100644
--- a/profiles/audio/a2dp.c
+++ b/profiles/audio/a2dp.c
@@ -27,7 +27,10 @@
 #include <config.h>
 #endif
 
+#define _GNU_SOURCE
+
 #include <stdlib.h>
+#include <stdio.h>
 #include <errno.h>
 
 #include <dbus/dbus.h>
@@ -38,14 +41,19 @@
 #include "lib/sdp_lib.h"
 #include "lib/uuid.h"
 
+#include "gdbus/gdbus.h"
+
 #include "src/plugin.h"
 #include "src/adapter.h"
 #include "src/device.h"
+#include "src/dbus-common.h"
+#include "src/error.h"
 #include "src/profile.h"
 #include "src/service.h"
 #include "src/log.h"
 #include "src/sdpd.h"
 #include "src/shared/queue.h"
+#include "src/shared/util.h"
 
 #include "btio/btio.h"
 
@@ -63,6 +71,8 @@
 
 #define AVDTP_PSM 25
 
+#define MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint1"
+
 struct a2dp_sep {
 	struct a2dp_server *server;
 	struct a2dp_endpoint *endpoint;
@@ -93,6 +103,7 @@ struct a2dp_setup_cb {
 };
 
 struct a2dp_setup {
+	struct a2dp_channel *chan;
 	struct avdtp *session;
 	struct a2dp_sep *sep;
 	struct avdtp_remote_sep *rsep;
@@ -121,6 +132,12 @@ struct a2dp_server {
 	struct queue *channels;
 };
 
+struct a2dp_remote_sep {
+	struct a2dp_channel *chan;
+	char *path;
+	struct avdtp_remote_sep *sep;
+};
+
 struct a2dp_channel {
 	struct a2dp_server *server;
 	struct btd_device *device;
@@ -129,6 +146,7 @@ struct a2dp_channel {
 	unsigned int state_id;
 	unsigned int auth_id;
 	struct avdtp *session;
+	struct queue *seps;
 };
 
 static GSList *servers = NULL;
@@ -144,12 +162,42 @@ static struct a2dp_setup *setup_ref(struct a2dp_setup *setup)
 	return setup;
 }
 
+static bool match_by_session(const void *data, const void *user_data)
+{
+	const struct a2dp_channel *chan = data;
+	const struct avdtp *session = user_data;
+
+	return chan->session == session;
+}
+
+static struct a2dp_channel *find_channel(struct avdtp *session)
+{
+	GSList *l;
+
+	for (l = servers; l; l = g_slist_next(l)) {
+		struct a2dp_server *server = l->data;
+		struct a2dp_channel *chan;
+
+		chan = queue_find(server->channels, match_by_session, session);
+		if (chan)
+			return chan;
+	}
+
+	return NULL;
+}
+
 static struct a2dp_setup *setup_new(struct avdtp *session)
 {
 	struct a2dp_setup *setup;
+	struct a2dp_channel *chan;
+
+	chan = find_channel(session);
+	if (!chan)
+		return NULL;
 
 	setup = g_new0(struct a2dp_setup, 1);
 	setup->session = avdtp_ref(session);
+	setup->chan = find_channel(session);
 	setups = g_slist_append(setups, setup);
 
 	return setup;
@@ -1299,6 +1347,14 @@ static struct a2dp_server *find_server(GSList *list, struct btd_adapter *a)
 	return NULL;
 }
 
+static void remove_remote_sep(void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+
+	g_dbus_unregister_interface(btd_get_dbus_connection(), sep->path,
+						MEDIA_ENDPOINT_INTERFACE);
+}
+
 static void channel_free(void *data)
 {
 	struct a2dp_channel *chan = data;
@@ -1316,6 +1372,7 @@ static void channel_free(void *data)
 
 	avdtp_remove_state_cb(chan->state_id);
 
+	queue_destroy(chan->seps, remove_remote_sep);
 	g_free(chan);
 }
 
@@ -1371,6 +1428,7 @@ static struct a2dp_channel *channel_new(struct a2dp_server *server,
 	chan = g_new0(struct a2dp_channel, 1);
 	chan->server = server;
 	chan->device = device;
+	chan->seps = queue_new();
 	chan->state_id = avdtp_add_state_cb(device, avdtp_state_cb, chan);
 
 	if (!queue_push_tail(server->channels, chan)) {
@@ -1805,16 +1863,11 @@ void a2dp_remove_sep(struct a2dp_sep *sep)
 	a2dp_unregister_sep(sep);
 }
 
-static void select_cb(struct a2dp_setup *setup, void *ret, int size)
+static void setup_add_caps(struct a2dp_setup *setup, uint8_t *caps, size_t size)
 {
 	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);
 
@@ -1823,13 +1876,23 @@ static void select_cb(struct a2dp_setup *setup, void *ret, int size)
 	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);
+	memcpy(cap->data, caps, 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);
+}
+
+static void select_cb(struct a2dp_setup *setup, void *ret, int size)
+{
+	if (size < 0) {
+		DBG("Endpoint replied an invalid configuration");
+		goto done;
+	}
+
+	setup_add_caps(setup, ret, size);
 
 done:
 	finalize_select(setup);
@@ -1885,6 +1948,278 @@ static struct a2dp_sep *a2dp_select_sep(struct avdtp *session, uint8_t type,
 	return a2dp_find_sep(session, l, NULL);
 }
 
+struct client {
+	const char *sender;
+	const char *path;
+};
+
+static int match_client(const void *data, const void *user_data)
+{
+	struct a2dp_sep *sep = (void *) data;
+	const struct a2dp_endpoint *endpoint = sep->endpoint;
+	const struct client *client = user_data;
+
+	if (strcmp(client->sender, endpoint->get_name(sep, sep->user_data)))
+		return -1;
+
+	return strcmp(client->path, endpoint->get_path(sep, sep->user_data));
+}
+
+static struct a2dp_sep *find_sep(struct a2dp_server *server, const char *sender,
+							const char *path)
+{
+	GSList *l;
+	struct client client = { sender, path };
+
+	l = g_slist_find_custom(server->sources, &client, match_client);
+	if (l)
+		return l->data;
+
+	l = g_slist_find_custom(server->sinks, &client, match_client);
+	if (l)
+		return l->data;
+
+	return NULL;
+}
+
+static int parse_properties(DBusMessageIter *props, uint8_t **caps, int *size)
+{
+	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, "Capabilities") == 0) {
+			DBusMessageIter array;
+
+			if (var != DBUS_TYPE_ARRAY)
+				return -EINVAL;
+
+			dbus_message_iter_recurse(&value, &array);
+			dbus_message_iter_get_fixed_array(&array, caps, size);
+			return 0;
+		}
+
+		dbus_message_iter_next(props);
+	}
+
+	return -EINVAL;
+}
+
+static int a2dp_reconfig(struct a2dp_channel *chan, const char *sender,
+			struct a2dp_sep *lsep, struct a2dp_remote_sep *rsep,
+			uint8_t *caps, int size)
+{
+	struct a2dp_setup *setup;
+	const struct queue_entry *entry;
+	int err;
+
+	setup = a2dp_setup_get(chan->session);
+	if (!setup)
+		return -ENOMEM;
+
+	setup->sep = lsep;
+	setup->rsep = rsep->sep;
+
+	setup_add_caps(setup, caps, size);
+
+	/* Check for existing stream and close it */
+	for (entry = queue_get_entries(chan->server->seps); entry;
+						entry = entry->next) {
+		struct a2dp_sep *tmp = entry->data;
+
+		/* Attempt to reconfigure if a stream already exists */
+		if (tmp->stream) {
+			/* Only allow switching sep from the same sender
+			if (strcmp(sender, sep->endpoint->get_name(tmp,
+							tmp->user_data)))
+				return btd_error_not_authorized(msg);
+			*/
+
+			err = avdtp_close(chan->session, tmp->stream, FALSE);
+			if (err < 0) {
+				error("avdtp_close: %s", strerror(-err));
+				return err;
+			}
+
+			setup->reconfigure = TRUE;
+
+			return 0;
+		}
+	}
+
+	err = avdtp_set_configuration(setup->session, setup->rsep,
+						lsep->lsep,
+						setup->caps,
+						&setup->stream);
+	if (err < 0) {
+		error("avdtp_set_configuration: %s", strerror(-err));
+		return err;
+	}
+
+	return 0;
+}
+
+static DBusMessage *set_configuration(DBusConnection *conn, DBusMessage *msg,
+								void *data)
+{
+	struct a2dp_remote_sep *rsep = data;
+	struct a2dp_channel *chan = rsep->chan;
+	struct a2dp_sep *lsep;
+	struct avdtp_service_capability *service;
+	struct avdtp_media_codec_capability *codec;
+	DBusMessageIter args, props;
+	const char *sender, *path;
+	uint8_t *caps;
+	int err, size = 0;
+
+	sender = dbus_message_get_sender(msg);
+
+	dbus_message_iter_init(msg, &args);
+
+	dbus_message_iter_get_basic(&args, &path);
+	dbus_message_iter_next(&args);
+
+	lsep = find_sep(chan->server, sender, path);
+	if (!lsep)
+		return btd_error_invalid_args(msg);
+
+	/* Check if SEPs are no the same role */
+	if (avdtp_get_type(rsep->sep) == lsep->type)
+		return btd_error_invalid_args(msg);
+
+	service = avdtp_get_codec(rsep->sep);
+	codec = (struct avdtp_media_codec_capability *) service->data;
+
+	/* Check if codec match */
+	if (!endpoint_match_codec_ind(chan->session, codec, lsep))
+		return btd_error_invalid_args(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, &caps, &size) < 0)
+		return btd_error_invalid_args(msg);
+
+	err = a2dp_reconfig(chan, sender, lsep, rsep, caps, size);
+	if (err < 0)
+		return btd_error_failed(msg, strerror(-err));
+
+	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
+}
+
+static const GDBusMethodTable sep_methods[] = {
+	{ GDBUS_EXPERIMENTAL_ASYNC_METHOD("SetConfiguration",
+					GDBUS_ARGS({ "endpoint", "o" },
+						{ "properties", "a{sv}" } ),
+					NULL, set_configuration) },
+	{ },
+};
+
+static gboolean get_uuid(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+	const char *uuid;
+
+	switch (avdtp_get_type(sep->sep)) {
+	case AVDTP_SEP_TYPE_SOURCE:
+		uuid = A2DP_SOURCE_UUID;
+		break;
+	case AVDTP_SEP_TYPE_SINK:
+		uuid = A2DP_SOURCE_UUID;
+		break;
+	default:
+		uuid = "";
+	}
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &uuid);
+
+	return TRUE;
+}
+
+static gboolean get_codec(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+	struct avdtp_service_capability *cap = avdtp_get_codec(sep->sep);
+	struct avdtp_media_codec_capability *codec = (void *) cap->data;
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
+						&codec->media_codec_type);
+
+	return TRUE;
+}
+
+static gboolean get_capabilities(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+	struct avdtp_service_capability *service = avdtp_get_codec(sep->sep);
+	struct avdtp_media_codec_capability *codec = (void *) service->data;
+	uint8_t *caps = codec->data;
+	DBusMessageIter array;
+
+	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
+					DBUS_TYPE_BYTE_AS_STRING, &array);
+
+	dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &caps,
+					service->length - sizeof(*codec));
+
+	dbus_message_iter_close_container(iter, &array);
+
+	return TRUE;
+}
+
+static const GDBusPropertyTable sep_properties[] = {
+	{ "UUID", "s", get_uuid, NULL, NULL,
+					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ "Codec", "y", get_codec, NULL, NULL,
+					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ "Capabilities", "ay", get_capabilities, NULL, NULL,
+					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ }
+};
+
+static void remote_sep_free(void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+
+	free(sep->path);
+	free(sep);
+}
+
+static void register_remote_sep(void *data, void *user_data)
+{
+	struct avdtp_remote_sep *rsep = data;
+	struct a2dp_setup *setup = user_data;
+	struct a2dp_remote_sep *sep;
+
+	sep = new0(struct a2dp_remote_sep, 1);
+	sep->chan = setup->chan;
+	sep->sep = rsep;
+	asprintf(&sep->path, "%s/sep%d", device_get_path(setup->chan->device),
+							avdtp_get_seid(rsep));
+
+	if (g_dbus_register_interface(btd_get_dbus_connection(),
+				sep->path, MEDIA_ENDPOINT_INTERFACE,
+				sep_methods, NULL, sep_properties,
+				sep, remote_sep_free) == FALSE) {
+		error("Could not register remote sep %s", sep->path);
+		remote_sep_free(sep);
+	}
+
+	queue_push_tail(setup->chan->seps, sep);
+}
+
 static void discover_cb(struct avdtp *session, GSList *seps,
 				struct avdtp_error *err, void *user_data)
 {
@@ -1895,6 +2230,9 @@ static void discover_cb(struct avdtp *session, GSList *seps,
 	setup->seps = seps;
 	setup->err = err;
 
+	if (!err && queue_isempty(setup->chan->seps))
+		g_slist_foreach(seps, register_remote_sep, setup);
+
 	finalize_discover(setup);
 }
 
diff --git a/profiles/audio/a2dp.h b/profiles/audio/a2dp.h
index 2c388bb68..7f38c75f3 100644
--- a/profiles/audio/a2dp.h
+++ b/profiles/audio/a2dp.h
@@ -32,6 +32,7 @@ typedef void (*a2dp_endpoint_config_t) (struct a2dp_setup *setup, gboolean ret);
 
 struct a2dp_endpoint {
 	const char *(*get_name) (struct a2dp_sep *sep, void *user_data);
+	const char *(*get_path) (struct a2dp_sep *sep, void *user_data);
 	size_t (*get_capabilities) (struct a2dp_sep *sep,
 						uint8_t **capabilities,
 						void *user_data);
diff --git a/profiles/audio/avdtp.c b/profiles/audio/avdtp.c
index 2cb3c8a00..cc4322d10 100644
--- a/profiles/audio/avdtp.c
+++ b/profiles/audio/avdtp.c
@@ -3161,6 +3161,16 @@ static int process_queue(struct avdtp *session)
 	return send_req(session, FALSE, req);
 }
 
+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;
diff --git a/profiles/audio/avdtp.h b/profiles/audio/avdtp.h
index 621a6e3cf..e5fc40c89 100644
--- a/profiles/audio/avdtp.h
+++ b/profiles/audio/avdtp.h
@@ -223,6 +223,10 @@ struct avdtp *avdtp_ref(struct avdtp *session);
 struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category,
 							void *data, int size);
 
+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);
 
 int avdtp_discover(struct avdtp *session, avdtp_discover_cb_t cb,
diff --git a/profiles/audio/media.c b/profiles/audio/media.c
index e2a447e56..9d7564cf0 100644
--- a/profiles/audio/media.c
+++ b/profiles/audio/media.c
@@ -489,6 +489,13 @@ static const char *get_name(struct a2dp_sep *sep, void *user_data)
 	return endpoint->sender;
 }
 
+static const char *get_path(struct a2dp_sep *sep, void *user_data)
+{
+	struct media_endpoint *endpoint = user_data;
+
+	return endpoint->path;
+}
+
 static size_t get_capabilities(struct a2dp_sep *sep, uint8_t **capabilities,
 							void *user_data)
 {
@@ -579,6 +586,7 @@ static void set_delay(struct a2dp_sep *sep, uint16_t delay, void *user_data)
 
 static struct a2dp_endpoint a2dp_endpoint = {
 	.get_name = get_name,
+	.get_path = get_path,
 	.get_capabilities = get_capabilities,
 	.select_configuration = select_config,
 	.set_configuration = set_config,
-- 
2.17.2


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH BlueZ 3/6] doc/media-api: Add Endpoint property to MediaTransport
  2019-01-08 15:49 [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 2/6] a2dp: Expose " Luiz Augusto von Dentz
@ 2019-01-08 15:49 ` Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 4/6] a2dp: Implement MediaTransport.Endpoint Luiz Augusto von Dentz
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2019-01-08 15:49 UTC (permalink / raw)
  To: linux-bluetooth

From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>

Adds endpoint object to MediaTransport so application can resolve which
MediaEndpoint is in use.
---
 doc/media-api.txt | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/doc/media-api.txt b/doc/media-api.txt
index af9485342..292e9034c 100644
--- a/doc/media-api.txt
+++ b/doc/media-api.txt
@@ -627,3 +627,8 @@ Properties	object Device [readonly]
 			acquired by the sender.
 
 			Possible Values: 0-127
+
+		object Endpoint [readonly, optional, experimental]
+
+			Endpoint object which the transport is associated
+			with.
-- 
2.17.2


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH BlueZ 4/6] a2dp: Implement MediaTransport.Endpoint
  2019-01-08 15:49 [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 2/6] a2dp: Expose " Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 3/6] doc/media-api: Add Endpoint property to MediaTransport Luiz Augusto von Dentz
@ 2019-01-08 15:49 ` Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 5/6] doc/settings-storage: Add Endpoint group to cache Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 6/6] a2dp: Cache remote endpoints Luiz Augusto von Dentz
  4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2019-01-08 15:49 UTC (permalink / raw)
  To: linux-bluetooth

From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>

This implements MediaTransport.Endpoint property which exposes what
endpoint is being used by the transport.
---
 profiles/audio/a2dp.c      | 91 +++++++++++++++++++++++++++++---------
 profiles/audio/a2dp.h      |  1 +
 profiles/audio/media.c     |  5 ++-
 profiles/audio/transport.c | 28 +++++++++++-
 profiles/audio/transport.h |  1 +
 5 files changed, 102 insertions(+), 24 deletions(-)

diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
index 4fa7e3d9b..2ceb73617 100644
--- a/profiles/audio/a2dp.c
+++ b/profiles/audio/a2dp.c
@@ -106,7 +106,7 @@ struct a2dp_setup {
 	struct a2dp_channel *chan;
 	struct avdtp *session;
 	struct a2dp_sep *sep;
-	struct avdtp_remote_sep *rsep;
+	struct a2dp_remote_sep *rsep;
 	struct avdtp_stream *stream;
 	struct avdtp_error *err;
 	avdtp_set_configuration_cb setconf_cb;
@@ -1065,6 +1065,24 @@ static gboolean close_ind(struct avdtp *session, struct avdtp_local_sep *sep,
 	return TRUE;
 }
 
+static bool match_remote_sep(const void *data, const void *user_data)
+{
+	const struct a2dp_remote_sep *sep = data;
+	const struct avdtp_remote_sep *rsep = user_data;
+
+	return sep->sep == rsep;
+}
+
+static struct a2dp_remote_sep *find_remote_sep(struct a2dp_channel *chan,
+						struct a2dp_sep *sep)
+{
+	struct avdtp_remote_sep *rsep;
+
+	rsep = avdtp_find_remote_sep(chan->session, sep->lsep);
+
+	return queue_find(chan->seps, match_remote_sep, rsep);
+}
+
 static gboolean a2dp_reconfigure(gpointer data)
 {
 	struct a2dp_setup *setup = data;
@@ -1074,14 +1092,14 @@ static gboolean a2dp_reconfigure(gpointer data)
 	struct avdtp_service_capability *cap;
 
 	if (setup->rsep) {
-		cap = avdtp_get_codec(setup->rsep);
+		cap = avdtp_get_codec(setup->rsep->sep);
 		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);
+		setup->rsep = find_remote_sep(setup->chan, sep);
 
-	posix_err = avdtp_set_configuration(setup->session, setup->rsep,
+	posix_err = avdtp_set_configuration(setup->session, setup->rsep->sep,
 						sep->lsep,
 						setup->caps,
 						&setup->stream);
@@ -1097,6 +1115,16 @@ failed:
 	return FALSE;
 }
 
+static struct a2dp_remote_sep *get_remote_sep(struct a2dp_channel *chan,
+						struct avdtp_stream *stream)
+{
+	struct avdtp_remote_sep *rsep;
+
+	rsep = avdtp_stream_get_remote_sep(stream);
+
+	return queue_find(chan->seps, match_remote_sep, rsep);
+}
+
 static void close_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
 			struct avdtp_stream *stream, struct avdtp_error *err,
 			void *user_data)
@@ -1121,7 +1149,7 @@ static void close_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
 	}
 
 	if (!setup->rsep)
-		setup->rsep = avdtp_stream_get_remote_sep(stream);
+		setup->rsep = get_remote_sep(setup->chan, stream);
 
 	if (setup->reconfigure)
 		g_timeout_add(RECONFIGURE_TIMEOUT, a2dp_reconfigure, setup);
@@ -1347,10 +1375,23 @@ static struct a2dp_server *find_server(GSList *list, struct btd_adapter *a)
 	return NULL;
 }
 
+static void remote_sep_free(void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+
+	free(sep->path);
+	free(sep);
+}
+
 static void remove_remote_sep(void *data)
 {
 	struct a2dp_remote_sep *sep = data;
 
+	if (!sep->path) {
+		remote_sep_free(sep);
+		return;
+	}
+
 	g_dbus_unregister_interface(btd_get_dbus_connection(), sep->path,
 						MEDIA_ENDPOINT_INTERFACE);
 }
@@ -2026,7 +2067,7 @@ static int a2dp_reconfig(struct a2dp_channel *chan, const char *sender,
 		return -ENOMEM;
 
 	setup->sep = lsep;
-	setup->rsep = rsep->sep;
+	setup->rsep = rsep;
 
 	setup_add_caps(setup, caps, size);
 
@@ -2055,7 +2096,7 @@ static int a2dp_reconfig(struct a2dp_channel *chan, const char *sender,
 		}
 	}
 
-	err = avdtp_set_configuration(setup->session, setup->rsep,
+	err = avdtp_set_configuration(setup->session, setup->rsep->sep,
 						lsep->lsep,
 						setup->caps,
 						&setup->stream);
@@ -2189,14 +2230,6 @@ static const GDBusPropertyTable sep_properties[] = {
 	{ }
 };
 
-static void remote_sep_free(void *data)
-{
-	struct a2dp_remote_sep *sep = data;
-
-	free(sep->path);
-	free(sep);
-}
-
 static void register_remote_sep(void *data, void *user_data)
 {
 	struct avdtp_remote_sep *rsep = data;
@@ -2206,6 +2239,10 @@ static void register_remote_sep(void *data, void *user_data)
 	sep = new0(struct a2dp_remote_sep, 1);
 	sep->chan = setup->chan;
 	sep->sep = rsep;
+
+	if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL))
+		goto done;
+
 	asprintf(&sep->path, "%s/sep%d", device_get_path(setup->chan->device),
 							avdtp_get_seid(rsep));
 
@@ -2214,9 +2251,13 @@ static void register_remote_sep(void *data, void *user_data)
 				sep_methods, NULL, sep_properties,
 				sep, remote_sep_free) == FALSE) {
 		error("Could not register remote sep %s", sep->path);
-		remote_sep_free(sep);
+		free(sep->path);
+		sep->path = NULL;
 	}
 
+	DBG("Found remote SEP: %s", sep->path);
+
+done:
 	queue_push_tail(setup->chan->seps, sep);
 }
 
@@ -2284,14 +2325,14 @@ unsigned int a2dp_select_capabilities(struct avdtp *session,
 	cb_data->user_data = user_data;
 
 	setup->sep = sep;
-	setup->rsep = avdtp_find_remote_sep(session, sep->lsep);
+	setup->rsep = find_remote_sep(setup->chan, sep);
 
 	if (setup->rsep == NULL) {
 		error("Could not find remote sep");
 		goto fail;
 	}
 
-	service = avdtp_get_codec(setup->rsep);
+	service = avdtp_get_codec(setup->rsep->sep);
 	codec = (struct avdtp_media_codec_capability *) service->data;
 
 	err = sep->endpoint->select_configuration(sep, codec->data,
@@ -2385,13 +2426,13 @@ unsigned int a2dp_config(struct avdtp *session, struct a2dp_sep *sep,
 			break;
 		}
 
-		setup->rsep = avdtp_find_remote_sep(session, sep->lsep);
+		setup->rsep = find_remote_sep(setup->chan, sep);
 		if (setup->rsep == NULL) {
 			error("No matching ACP and INT SEPs found");
 			goto failed;
 		}
 
-		posix_err = avdtp_set_configuration(session, setup->rsep,
+		posix_err = avdtp_set_configuration(session, setup->rsep->sep,
 							sep->lsep, caps,
 							&setup->stream);
 		if (posix_err < 0) {
@@ -2633,6 +2674,16 @@ struct btd_device *a2dp_setup_get_device(struct a2dp_setup *setup)
 	return avdtp_get_device(setup->session);
 }
 
+const char *a2dp_setup_remote_path(struct a2dp_setup *setup)
+{
+	if (setup->rsep) {
+		if (setup->rsep->path)
+			return setup->rsep->path;
+	}
+
+	return NULL;
+}
+
 static int a2dp_source_probe(struct btd_service *service)
 {
 	struct btd_device *dev = btd_service_get_device(service);
diff --git a/profiles/audio/a2dp.h b/profiles/audio/a2dp.h
index 7f38c75f3..19466a428 100644
--- a/profiles/audio/a2dp.h
+++ b/profiles/audio/a2dp.h
@@ -91,4 +91,5 @@ gboolean a2dp_sep_lock(struct a2dp_sep *sep, struct avdtp *session);
 gboolean a2dp_sep_unlock(struct a2dp_sep *sep, struct avdtp *session);
 struct avdtp_stream *a2dp_sep_get_stream(struct a2dp_sep *sep);
 struct btd_device *a2dp_setup_get_device(struct a2dp_setup *setup);
+const char *a2dp_setup_remote_path(struct a2dp_setup *setup);
 struct avdtp *a2dp_avdtp_get(struct btd_device *device);
diff --git a/profiles/audio/media.c b/profiles/audio/media.c
index 9d7564cf0..28fa70668 100644
--- a/profiles/audio/media.c
+++ b/profiles/audio/media.c
@@ -430,8 +430,9 @@ static gboolean set_configuration(struct media_endpoint *endpoint,
 	if (transport != NULL)
 		return FALSE;
 
-	transport = media_transport_create(device, configuration, size,
-								endpoint);
+	transport = media_transport_create(device,
+					a2dp_setup_remote_path(data->setup),
+					configuration, size, endpoint);
 	if (transport == NULL)
 		return FALSE;
 
diff --git a/profiles/audio/transport.c b/profiles/audio/transport.c
index 98f4e1ffd..48fabba9b 100644
--- a/profiles/audio/transport.c
+++ b/profiles/audio/transport.c
@@ -92,6 +92,7 @@ struct a2dp_transport {
 struct media_transport {
 	char			*path;		/* Transport object path */
 	struct btd_device	*device;	/* Transport device */
+	const char		*remote_endpoint; /* Transport remote SEP */
 	struct media_endpoint	*endpoint;	/* Transport endpoint */
 	struct media_owner	*owner;		/* Transport owner */
 	uint8_t			*configuration; /* Transport configuration */
@@ -689,6 +690,24 @@ static void set_volume(const GDBusPropertyTable *property,
 	avrcp_set_volume(transport->device, volume, notify);
 }
 
+static gboolean endpoint_exists(const GDBusPropertyTable *property, void *data)
+{
+	struct media_transport *transport = data;
+
+	return transport->remote_endpoint != NULL;
+}
+
+static gboolean get_endpoint(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct media_transport *transport = data;
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
+					&transport->remote_endpoint);
+
+	return TRUE;
+}
+
 static const GDBusMethodTable transport_methods[] = {
 	{ GDBUS_ASYNC_METHOD("Acquire",
 			NULL,
@@ -712,6 +731,8 @@ static const GDBusPropertyTable transport_properties[] = {
 	{ "State", "s", get_state },
 	{ "Delay", "q", get_delay, NULL, delay_exists },
 	{ "Volume", "q", get_volume, set_volume, volume_exists },
+	{ "Endpoint", "o", get_endpoint, NULL, endpoint_exists,
+				G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
 	{ }
 };
 
@@ -836,6 +857,7 @@ static int media_transport_init_sink(struct media_transport *transport)
 }
 
 struct media_transport *media_transport_create(struct btd_device *device,
+						const char *remote_endpoint,
 						uint8_t *configuration,
 						size_t size, void *data)
 {
@@ -850,8 +872,10 @@ struct media_transport *media_transport_create(struct btd_device *device,
 	transport->configuration = g_new(uint8_t, size);
 	memcpy(transport->configuration, configuration, size);
 	transport->size = size;
-	transport->path = g_strdup_printf("%s/fd%d", device_get_path(device),
-									fd++);
+	transport->remote_endpoint = remote_endpoint;
+	transport->path = g_strdup_printf("%s/fd%d",
+				remote_endpoint ? remote_endpoint :
+				device_get_path(device), fd++);
 	transport->fd = -1;
 
 	uuid = media_endpoint_get_uuid(endpoint);
diff --git a/profiles/audio/transport.h b/profiles/audio/transport.h
index 505ad5c54..ac542bf6c 100644
--- a/profiles/audio/transport.h
+++ b/profiles/audio/transport.h
@@ -25,6 +25,7 @@
 struct media_transport;
 
 struct media_transport *media_transport_create(struct btd_device *device,
+						const char *remote_endpoint,
 						uint8_t *configuration,
 						size_t size, void *data);
 
-- 
2.17.2


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH BlueZ 5/6] doc/settings-storage: Add Endpoint group to cache
  2019-01-08 15:49 [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP Luiz Augusto von Dentz
                   ` (2 preceding siblings ...)
  2019-01-08 15:49 ` [PATCH BlueZ 4/6] a2dp: Implement MediaTransport.Endpoint Luiz Augusto von Dentz
@ 2019-01-08 15:49 ` Luiz Augusto von Dentz
  2019-01-08 15:49 ` [PATCH BlueZ 6/6] a2dp: Cache remote endpoints Luiz Augusto von Dentz
  4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2019-01-08 15:49 UTC (permalink / raw)
  To: linux-bluetooth

From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>

This documents how A2DP endpoints should be stored in the cache.
---
 doc/settings-storage.txt | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/doc/settings-storage.txt b/doc/settings-storage.txt
index 90357f735..44b0b4961 100644
--- a/doc/settings-storage.txt
+++ b/doc/settings-storage.txt
@@ -159,7 +159,7 @@ Cache directory file format
 ============================
 
 Each file, named by remote device address, may includes multiple groups
-(General, ServiceRecords, Attributes).
+(General, ServiceRecords, Attributes, Endpoints).
 
 In ServiceRecords, SDP records are stored using their handle as key
 (hexadecimal format).
@@ -168,6 +168,9 @@ In "Attributes" group GATT database is stored using attribute handle as key
 (hexadecimal format). Value associated with this handle is serialized form of
 all data required to re-create given attribute. ":" is used to separate fields.
 
+In "Endpoints" group A2DP remote endpoints are stored using the seid as key
+(hexadecimal format) and ":" is used to separate fields.
+
 [General] group contains:
 
   Name		String		Remote device friendly name
@@ -211,6 +214,12 @@ Sample Attributes section:
   002b=2803:002c:02:00002a38-0000-1000-8000-00805f9b34fb
   002d=2803:002e:08:00002a39-0000-1000-8000-00805f9b34fb
 
+[Endpoints] group contains:
+
+	<xx>:<xx>:<xxxxxxxx...>	String	First field is the endpoint type,
+					followed by codec type and its
+					capabilies as hexadecimal encoded
+					string.
 
 Info file format
 ================
-- 
2.17.2


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH BlueZ 6/6] a2dp: Cache remote endpoints
  2019-01-08 15:49 [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP Luiz Augusto von Dentz
                   ` (3 preceding siblings ...)
  2019-01-08 15:49 ` [PATCH BlueZ 5/6] doc/settings-storage: Add Endpoint group to cache Luiz Augusto von Dentz
@ 2019-01-08 15:49 ` Luiz Augusto von Dentz
  4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2019-01-08 15:49 UTC (permalink / raw)
  To: linux-bluetooth

From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>

In order to always have the Endpoint interface available the remote
endpoints needs to be cached since the remote stack may config a stream
on its own there may not be a chance to discover the endpoits available
which would make it impossible to switch endpoints.
---
 profiles/audio/a2dp.c  | 725 +++++++++++++++++++++++++----------------
 profiles/audio/avdtp.c |  46 ++-
 profiles/audio/avdtp.h |   5 +
 3 files changed, 492 insertions(+), 284 deletions(-)

diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
index 2ceb73617..cda2b6c64 100644
--- a/profiles/audio/a2dp.c
+++ b/profiles/audio/a2dp.c
@@ -1441,6 +1441,410 @@ static gboolean disconnect_cb(GIOChannel *io, GIOCondition cond, gpointer data)
 	return FALSE;
 }
 
+static void caps_add_codec(GSList **l, uint8_t codec, uint8_t *caps,
+							size_t size)
+{
+	struct avdtp_service_capability *media_transport, *media_codec;
+	struct avdtp_media_codec_capability *cap;
+
+	media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT,
+						NULL, 0);
+
+	*l = g_slist_append(*l, media_transport);
+
+	cap = g_malloc0(sizeof(*cap) + size);
+	cap->media_type = AVDTP_MEDIA_TYPE_AUDIO;
+	cap->media_codec_type = codec;
+	memcpy(cap->data, caps, size);
+
+	media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, cap,
+						sizeof(*cap) + size);
+
+	*l = g_slist_append(*l, media_codec);
+	g_free(cap);
+}
+
+struct client {
+	const char *sender;
+	const char *path;
+};
+
+static int match_client(const void *data, const void *user_data)
+{
+	struct a2dp_sep *sep = (void *) data;
+	const struct a2dp_endpoint *endpoint = sep->endpoint;
+	const struct client *client = user_data;
+
+	if (strcmp(client->sender, endpoint->get_name(sep, sep->user_data)))
+		return -1;
+
+	return strcmp(client->path, endpoint->get_path(sep, sep->user_data));
+}
+
+static struct a2dp_sep *find_sep(struct a2dp_server *server, uint8_t type,
+					const char *sender, const char *path)
+{
+	GSList *l;
+	struct client client = { sender, path };
+
+	l = type == AVDTP_SEP_TYPE_SINK ? server->sources : server->sinks;
+
+	l = g_slist_find_custom(l, &client, match_client);
+	if (l)
+		return l->data;
+
+	return NULL;
+}
+
+static int parse_properties(DBusMessageIter *props, uint8_t **caps, int *size)
+{
+	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, "Capabilities") == 0) {
+			DBusMessageIter array;
+
+			if (var != DBUS_TYPE_ARRAY)
+				return -EINVAL;
+
+			dbus_message_iter_recurse(&value, &array);
+			dbus_message_iter_get_fixed_array(&array, caps, size);
+			return 0;
+		}
+
+		dbus_message_iter_next(props);
+	}
+
+	return -EINVAL;
+}
+
+static void reconfig_cb(struct avdtp *session, struct a2dp_sep *sep,
+			struct avdtp_stream *stream, int err, void *user_data)
+{
+	DBusMessage *msg = user_data;
+
+	if (err)
+		g_dbus_send_message(btd_get_dbus_connection(),
+					btd_error_failed(msg, strerror(-err)));
+	else
+		g_dbus_send_reply(btd_get_dbus_connection(), msg,
+					DBUS_TYPE_INVALID);
+
+	dbus_message_unref(msg);
+}
+
+static int a2dp_reconfig(struct a2dp_channel *chan, const char *sender,
+			struct a2dp_sep *lsep, struct a2dp_remote_sep *rsep,
+			uint8_t *caps, int size, void *user_data)
+{
+	struct a2dp_setup *setup;
+	struct a2dp_setup_cb *cb_data;
+	GSList *l;
+	int err;
+
+	setup = a2dp_setup_get(chan->session);
+	if (!setup)
+		return -ENOMEM;
+
+	cb_data = setup_cb_new(setup);
+	cb_data->config_cb = reconfig_cb;
+	cb_data->user_data = user_data;
+
+	setup->sep = lsep;
+	setup->rsep = rsep;
+
+	caps_add_codec(&setup->caps, setup->sep->codec, caps, size);
+
+	l = avdtp_get_type(rsep->sep) == AVDTP_SEP_TYPE_SINK ?
+					chan->server->sources :
+					chan->server->sinks;
+
+	/* Check for existing stream and close it */
+	for (; l; l = g_slist_next(l)) {
+		struct a2dp_sep *tmp = l->data;
+
+		/* Attempt to reconfigure if a stream already exists */
+		if (tmp->stream) {
+			/* Only allow switching sep from the same sender */
+			if (strcmp(sender, lsep->endpoint->get_name(tmp,
+							tmp->user_data)))
+				return -EPERM;
+
+			err = avdtp_close(chan->session, tmp->stream, FALSE);
+			if (err < 0) {
+				error("avdtp_close: %s", strerror(-err));
+				goto fail;
+			}
+
+			setup->reconfigure = TRUE;
+
+			return 0;
+		}
+	}
+
+	err = avdtp_set_configuration(setup->session, setup->rsep->sep,
+						lsep->lsep,
+						setup->caps,
+						&setup->stream);
+	if (err < 0) {
+		error("avdtp_set_configuration: %s", strerror(-err));
+		goto fail;
+	}
+
+	return 0;
+
+fail:
+	setup_unref(setup);
+	return err;
+}
+
+static DBusMessage *set_configuration(DBusConnection *conn, DBusMessage *msg,
+								void *data)
+{
+	struct a2dp_remote_sep *rsep = data;
+	struct a2dp_channel *chan = rsep->chan;
+	struct a2dp_sep *lsep = NULL;
+	struct avdtp_service_capability *service;
+	struct avdtp_media_codec_capability *codec;
+	DBusMessageIter args, props;
+	const char *sender, *path;
+	uint8_t *caps;
+	int err, size = 0;
+
+	sender = dbus_message_get_sender(msg);
+
+	dbus_message_iter_init(msg, &args);
+
+	dbus_message_iter_get_basic(&args, &path);
+	dbus_message_iter_next(&args);
+
+	lsep = find_sep(chan->server, avdtp_get_type(rsep->sep), sender, path);
+	if (!lsep)
+		return btd_error_invalid_args(msg);
+
+	service = avdtp_get_codec(rsep->sep);
+	codec = (struct avdtp_media_codec_capability *) service->data;
+
+	/* Check if codec really matches */
+	if (!endpoint_match_codec_ind(chan->session, codec, lsep))
+		return btd_error_invalid_args(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, &caps, &size) < 0)
+		return btd_error_invalid_args(msg);
+
+	err = a2dp_reconfig(chan, sender, lsep, rsep, caps, size,
+					dbus_message_ref(msg));
+	if (err < 0) {
+		dbus_message_unref(msg);
+		return btd_error_failed(msg, strerror(-err));
+	}
+
+	return NULL;
+}
+
+static const GDBusMethodTable sep_methods[] = {
+	{ GDBUS_EXPERIMENTAL_ASYNC_METHOD("SetConfiguration",
+					GDBUS_ARGS({ "endpoint", "o" },
+						{ "properties", "a{sv}" } ),
+					NULL, set_configuration) },
+	{ },
+};
+
+static gboolean get_uuid(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+	const char *uuid;
+
+	switch (avdtp_get_type(sep->sep)) {
+	case AVDTP_SEP_TYPE_SOURCE:
+		uuid = A2DP_SOURCE_UUID;
+		break;
+	case AVDTP_SEP_TYPE_SINK:
+		uuid = A2DP_SOURCE_UUID;
+		break;
+	default:
+		uuid = "";
+	}
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &uuid);
+
+	return TRUE;
+}
+
+static gboolean get_codec(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+	struct avdtp_service_capability *cap = avdtp_get_codec(sep->sep);
+	struct avdtp_media_codec_capability *codec = (void *) cap->data;
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
+						&codec->media_codec_type);
+
+	return TRUE;
+}
+
+static gboolean get_capabilities(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct a2dp_remote_sep *sep = data;
+	struct avdtp_service_capability *service = avdtp_get_codec(sep->sep);
+	struct avdtp_media_codec_capability *codec = (void *) service->data;
+	uint8_t *caps = codec->data;
+	DBusMessageIter array;
+
+	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
+					DBUS_TYPE_BYTE_AS_STRING, &array);
+
+	dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &caps,
+					service->length - sizeof(*codec));
+
+	dbus_message_iter_close_container(iter, &array);
+
+	return TRUE;
+}
+
+static const GDBusPropertyTable sep_properties[] = {
+	{ "UUID", "s", get_uuid, NULL, NULL,
+					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ "Codec", "y", get_codec, NULL, NULL,
+					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ "Capabilities", "ay", get_capabilities, NULL, NULL,
+					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ }
+};
+
+static void register_remote_sep(void *data, void *user_data)
+{
+	struct avdtp_remote_sep *rsep = data;
+	struct a2dp_channel *chan = user_data;
+	struct a2dp_remote_sep *sep;
+
+	sep = queue_find(chan->seps, match_remote_sep, rsep);
+	if (sep)
+		return;
+
+	sep = new0(struct a2dp_remote_sep, 1);
+	sep->chan = chan;
+	sep->sep = rsep;
+
+	if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL))
+		goto done;
+
+	asprintf(&sep->path, "%s/sep%d", device_get_path(chan->device),
+							avdtp_get_seid(rsep));
+
+	if (g_dbus_register_interface(btd_get_dbus_connection(),
+				sep->path, MEDIA_ENDPOINT_INTERFACE,
+				sep_methods, NULL, sep_properties,
+				sep, remote_sep_free) == FALSE) {
+		error("Could not register remote sep %s", sep->path);
+		free(sep->path);
+		sep->path = NULL;
+		goto done;
+	}
+
+	DBG("Found remote SEP: %s", sep->path);
+
+done:
+	queue_push_tail(chan->seps, sep);
+}
+
+static void load_remote_sep(struct a2dp_channel *chan, GKeyFile *key_file,
+								char **seids)
+{
+	struct avdtp_remote_sep *sep;
+
+	if (!seids)
+		return;
+
+	for (; *seids; seids++) {
+		uint8_t seid;
+		uint8_t type;
+		uint8_t codec;
+		char *value, caps[256];
+		uint8_t data[128];
+		int i, size;
+		GSList *l = NULL;
+
+		if (sscanf(*seids, "%02hhx", &seid) != 1)
+			continue;
+
+		value = g_key_file_get_string(key_file, "Endpoints", *seids,
+								NULL);
+		if (!value)
+			continue;
+
+		if (sscanf(value, "%02hhx:%02hhx:%s", &type, &codec,
+								caps) != 3) {
+			warn("Unable to load Endpoint: seid %u", seid);
+			g_free(value);
+			continue;
+		}
+
+		for (i = 0, size = strlen(caps); i < size; i += 2) {
+			uint8_t *tmp = data + i / 2;
+
+			if (sscanf(caps + i, "%02hhx", tmp) != 1) {
+				warn("Unable to load Endpoint: seid %u", seid);
+				break;
+			}
+		}
+
+		g_free(value);
+
+		if (i != size)
+			continue;
+
+		caps_add_codec(&l, codec, data, size / 2);
+
+		sep = avdtp_register_remote_sep(chan->session, seid, type, l);
+		if (!sep) {
+			warn("Unable to register Endpoint: seid %u", seid);
+			continue;
+		}
+
+		register_remote_sep(sep, chan);
+	}
+}
+
+static void load_remote_seps(struct a2dp_channel *chan)
+{
+	struct btd_device *device = chan->device;
+	char filename[PATH_MAX];
+	char dst_addr[18];
+	char **keys;
+	GKeyFile *key_file;
+
+	ba2str(device_get_address(device), dst_addr);
+
+	snprintf(filename, PATH_MAX, STORAGEDIR "/%s/cache/%s",
+			btd_adapter_get_storage_dir(device_get_adapter(device)),
+			dst_addr);
+	key_file = g_key_file_new();
+	g_key_file_load_from_file(key_file, filename, 0, NULL);
+	keys = g_key_file_get_keys(key_file, "Endpoints", NULL, NULL);
+
+	load_remote_sep(chan, key_file, keys);
+
+	g_strfreev(keys);
+	g_key_file_free(key_file);
+}
+
 static void avdtp_state_cb(struct btd_device *dev, struct avdtp *session,
 					avdtp_session_state_t old_state,
 					avdtp_session_state_t new_state,
@@ -1456,6 +1860,9 @@ static void avdtp_state_cb(struct btd_device *dev, struct avdtp *session,
 	case AVDTP_SESSION_STATE_CONNECTING:
 		break;
 	case AVDTP_SESSION_STATE_CONNECTED:
+		if (!chan->session)
+			chan->session = session;
+		load_remote_seps(chan);
 		break;
 	}
 }
@@ -1904,28 +2311,6 @@ void a2dp_remove_sep(struct a2dp_sep *sep)
 	a2dp_unregister_sep(sep);
 }
 
-static void setup_add_caps(struct a2dp_setup *setup, uint8_t *caps, size_t size)
-{
-	struct avdtp_service_capability *media_transport, *media_codec;
-	struct avdtp_media_codec_capability *cap;
-
-	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, caps, 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);
-}
-
 static void select_cb(struct a2dp_setup *setup, void *ret, int size)
 {
 	if (size < 0) {
@@ -1933,7 +2318,7 @@ static void select_cb(struct a2dp_setup *setup, void *ret, int size)
 		goto done;
 	}
 
-	setup_add_caps(setup, ret, size);
+	caps_add_codec(&setup->caps, setup->sep->codec, ret, size);
 
 done:
 	finalize_select(setup);
@@ -1989,276 +2374,58 @@ static struct a2dp_sep *a2dp_select_sep(struct avdtp *session, uint8_t type,
 	return a2dp_find_sep(session, l, NULL);
 }
 
-struct client {
-	const char *sender;
-	const char *path;
-};
-
-static int match_client(const void *data, const void *user_data)
-{
-	struct a2dp_sep *sep = (void *) data;
-	const struct a2dp_endpoint *endpoint = sep->endpoint;
-	const struct client *client = user_data;
-
-	if (strcmp(client->sender, endpoint->get_name(sep, sep->user_data)))
-		return -1;
-
-	return strcmp(client->path, endpoint->get_path(sep, sep->user_data));
-}
-
-static struct a2dp_sep *find_sep(struct a2dp_server *server, const char *sender,
-							const char *path)
-{
-	GSList *l;
-	struct client client = { sender, path };
-
-	l = g_slist_find_custom(server->sources, &client, match_client);
-	if (l)
-		return l->data;
-
-	l = g_slist_find_custom(server->sinks, &client, match_client);
-	if (l)
-		return l->data;
-
-	return NULL;
-}
-
-static int parse_properties(DBusMessageIter *props, uint8_t **caps, int *size)
-{
-	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, "Capabilities") == 0) {
-			DBusMessageIter array;
-
-			if (var != DBUS_TYPE_ARRAY)
-				return -EINVAL;
-
-			dbus_message_iter_recurse(&value, &array);
-			dbus_message_iter_get_fixed_array(&array, caps, size);
-			return 0;
-		}
-
-		dbus_message_iter_next(props);
-	}
-
-	return -EINVAL;
-}
-
-static int a2dp_reconfig(struct a2dp_channel *chan, const char *sender,
-			struct a2dp_sep *lsep, struct a2dp_remote_sep *rsep,
-			uint8_t *caps, int size)
-{
-	struct a2dp_setup *setup;
-	const struct queue_entry *entry;
-	int err;
-
-	setup = a2dp_setup_get(chan->session);
-	if (!setup)
-		return -ENOMEM;
-
-	setup->sep = lsep;
-	setup->rsep = rsep;
-
-	setup_add_caps(setup, caps, size);
-
-	/* Check for existing stream and close it */
-	for (entry = queue_get_entries(chan->server->seps); entry;
-						entry = entry->next) {
-		struct a2dp_sep *tmp = entry->data;
-
-		/* Attempt to reconfigure if a stream already exists */
-		if (tmp->stream) {
-			/* Only allow switching sep from the same sender
-			if (strcmp(sender, sep->endpoint->get_name(tmp,
-							tmp->user_data)))
-				return btd_error_not_authorized(msg);
-			*/
-
-			err = avdtp_close(chan->session, tmp->stream, FALSE);
-			if (err < 0) {
-				error("avdtp_close: %s", strerror(-err));
-				return err;
-			}
-
-			setup->reconfigure = TRUE;
-
-			return 0;
-		}
-	}
-
-	err = avdtp_set_configuration(setup->session, setup->rsep->sep,
-						lsep->lsep,
-						setup->caps,
-						&setup->stream);
-	if (err < 0) {
-		error("avdtp_set_configuration: %s", strerror(-err));
-		return err;
-	}
-
-	return 0;
-}
-
-static DBusMessage *set_configuration(DBusConnection *conn, DBusMessage *msg,
-								void *data)
-{
-	struct a2dp_remote_sep *rsep = data;
-	struct a2dp_channel *chan = rsep->chan;
-	struct a2dp_sep *lsep;
-	struct avdtp_service_capability *service;
-	struct avdtp_media_codec_capability *codec;
-	DBusMessageIter args, props;
-	const char *sender, *path;
-	uint8_t *caps;
-	int err, size = 0;
-
-	sender = dbus_message_get_sender(msg);
-
-	dbus_message_iter_init(msg, &args);
-
-	dbus_message_iter_get_basic(&args, &path);
-	dbus_message_iter_next(&args);
-
-	lsep = find_sep(chan->server, sender, path);
-	if (!lsep)
-		return btd_error_invalid_args(msg);
-
-	/* Check if SEPs are no the same role */
-	if (avdtp_get_type(rsep->sep) == lsep->type)
-		return btd_error_invalid_args(msg);
-
-	service = avdtp_get_codec(rsep->sep);
-	codec = (struct avdtp_media_codec_capability *) service->data;
-
-	/* Check if codec match */
-	if (!endpoint_match_codec_ind(chan->session, codec, lsep))
-		return btd_error_invalid_args(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, &caps, &size) < 0)
-		return btd_error_invalid_args(msg);
-
-	err = a2dp_reconfig(chan, sender, lsep, rsep, caps, size);
-	if (err < 0)
-		return btd_error_failed(msg, strerror(-err));
-
-	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
-}
-
-static const GDBusMethodTable sep_methods[] = {
-	{ GDBUS_EXPERIMENTAL_ASYNC_METHOD("SetConfiguration",
-					GDBUS_ARGS({ "endpoint", "o" },
-						{ "properties", "a{sv}" } ),
-					NULL, set_configuration) },
-	{ },
-};
-
-static gboolean get_uuid(const GDBusPropertyTable *property,
-					DBusMessageIter *iter, void *data)
-{
-	struct a2dp_remote_sep *sep = data;
-	const char *uuid;
-
-	switch (avdtp_get_type(sep->sep)) {
-	case AVDTP_SEP_TYPE_SOURCE:
-		uuid = A2DP_SOURCE_UUID;
-		break;
-	case AVDTP_SEP_TYPE_SINK:
-		uuid = A2DP_SOURCE_UUID;
-		break;
-	default:
-		uuid = "";
-	}
-
-	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &uuid);
-
-	return TRUE;
-}
-
-static gboolean get_codec(const GDBusPropertyTable *property,
-					DBusMessageIter *iter, void *data)
-{
-	struct a2dp_remote_sep *sep = data;
-	struct avdtp_service_capability *cap = avdtp_get_codec(sep->sep);
-	struct avdtp_media_codec_capability *codec = (void *) cap->data;
-
-	dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
-						&codec->media_codec_type);
-
-	return TRUE;
-}
-
-static gboolean get_capabilities(const GDBusPropertyTable *property,
-					DBusMessageIter *iter, void *data)
+static void store_remote_sep(void *data, void *user_data)
 {
 	struct a2dp_remote_sep *sep = data;
+	GKeyFile *key_file = (void *) user_data;
+	char seid[4], value[256];
 	struct avdtp_service_capability *service = avdtp_get_codec(sep->sep);
 	struct avdtp_media_codec_capability *codec = (void *) service->data;
-	uint8_t *caps = codec->data;
-	DBusMessageIter array;
+	unsigned int i;
+	ssize_t offset;
 
-	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
-					DBUS_TYPE_BYTE_AS_STRING, &array);
+	sprintf(seid, "%02hhx", avdtp_get_seid(sep->sep));
 
-	dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &caps,
-					service->length - sizeof(*codec));
+	offset = sprintf(value, "%02hhx:%02hhx:", avdtp_get_type(sep->sep),
+						codec->media_codec_type);
 
-	dbus_message_iter_close_container(iter, &array);
+	for (i = 0; i < service->length - sizeof(*codec); i++)
+		offset += sprintf(value + offset, "%02hhx", codec->data[i]);
 
-	return TRUE;
-}
 
-static const GDBusPropertyTable sep_properties[] = {
-	{ "UUID", "s", get_uuid, NULL, NULL,
-					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
-	{ "Codec", "y", get_codec, NULL, NULL,
-					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
-	{ "Capabilities", "ay", get_capabilities, NULL, NULL,
-					G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
-	{ }
-};
+	g_key_file_set_string(key_file, "Endpoints", seid, value);
+}
 
-static void register_remote_sep(void *data, void *user_data)
+static void store_remote_seps(struct a2dp_channel *chan)
 {
-	struct avdtp_remote_sep *rsep = data;
-	struct a2dp_setup *setup = user_data;
-	struct a2dp_remote_sep *sep;
+	struct btd_device *device = chan->device;
+	char filename[PATH_MAX];
+	char dst_addr[18];
+	GKeyFile *key_file;
+	char *data;
+	gsize length = 0;
 
-	sep = new0(struct a2dp_remote_sep, 1);
-	sep->chan = setup->chan;
-	sep->sep = rsep;
+	if (queue_isempty(chan->seps))
+		return;
 
-	if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL))
-		goto done;
+	ba2str(device_get_address(device), dst_addr);
 
-	asprintf(&sep->path, "%s/sep%d", device_get_path(setup->chan->device),
-							avdtp_get_seid(rsep));
+	snprintf(filename, PATH_MAX, STORAGEDIR "/%s/cache/%s",
+			btd_adapter_get_storage_dir(device_get_adapter(device)),
+			dst_addr);
+	key_file = g_key_file_new();
+	g_key_file_load_from_file(key_file, filename, 0, NULL);
 
-	if (g_dbus_register_interface(btd_get_dbus_connection(),
-				sep->path, MEDIA_ENDPOINT_INTERFACE,
-				sep_methods, NULL, sep_properties,
-				sep, remote_sep_free) == FALSE) {
-		error("Could not register remote sep %s", sep->path);
-		free(sep->path);
-		sep->path = NULL;
-	}
+	/* Remove current endpoints since it might have changed */
+	g_key_file_remove_group(key_file, "Endpoints", NULL);
 
-	DBG("Found remote SEP: %s", sep->path);
+	queue_foreach(chan->seps, store_remote_sep, key_file);
 
-done:
-	queue_push_tail(setup->chan->seps, sep);
+	data = g_key_file_to_data(key_file, &length, NULL);
+	g_file_set_contents(filename, data, length, NULL);
+
+	g_free(data);
+	g_key_file_free(key_file);
 }
 
 static void discover_cb(struct avdtp *session, GSList *seps,
@@ -2271,8 +2438,10 @@ static void discover_cb(struct avdtp *session, GSList *seps,
 	setup->seps = seps;
 	setup->err = err;
 
-	if (!err && queue_isempty(setup->chan->seps))
-		g_slist_foreach(seps, register_remote_sep, setup);
+	if (!err) {
+		g_slist_foreach(seps, register_remote_sep, setup->chan);
+		store_remote_seps(setup->chan);
+	}
 
 	finalize_discover(setup);
 }
diff --git a/profiles/audio/avdtp.c b/profiles/audio/avdtp.c
index cc4322d10..4f964feb5 100644
--- a/profiles/audio/avdtp.c
+++ b/profiles/audio/avdtp.c
@@ -2638,12 +2638,15 @@ static gboolean avdtp_discover_resp(struct avdtp *session,
 		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);
-		}
+		if (sep && sep->type == resp->seps[i].type &&
+				sep->media_type == resp->seps[i].media_type)
+			continue;
+
+		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;
@@ -3192,6 +3195,37 @@ struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category,
 	return cap;
 }
 
+struct avdtp_remote_sep *avdtp_register_remote_sep(struct avdtp *session,
+							uint8_t seid,
+							uint8_t type,
+							GSList *caps)
+{
+	struct avdtp_remote_sep *sep;
+	GSList *l;
+
+	sep = find_remote_sep(session->seps, seid);
+	if (sep)
+		return sep;
+
+	sep = g_new0(struct avdtp_remote_sep, 1);
+	session->seps = g_slist_append(session->seps, sep);
+	sep->seid = seid;
+	sep->type = type;
+	sep->media_type = AVDTP_MEDIA_TYPE_AUDIO;
+	sep->caps = caps;
+
+	for (l = caps; l; l = g_slist_next(l)) {
+		struct avdtp_service_capability *cap = l->data;
+
+		if (cap->category == AVDTP_MEDIA_CODEC)
+			sep->codec = cap;
+	}
+
+	DBG("seid %d type %d media %d", sep->seid, sep->type, sep->media_type);
+
+	return sep;
+}
+
 static gboolean process_discover(gpointer data)
 {
 	struct avdtp *session = data;
diff --git a/profiles/audio/avdtp.h b/profiles/audio/avdtp.h
index e5fc40c89..b03ca9030 100644
--- a/profiles/audio/avdtp.h
+++ b/profiles/audio/avdtp.h
@@ -223,6 +223,11 @@ struct avdtp *avdtp_ref(struct avdtp *session);
 struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category,
 							void *data, int size);
 
+struct avdtp_remote_sep *avdtp_register_remote_sep(struct avdtp *session,
+							uint8_t seid,
+							uint8_t type,
+							GSList *caps);
+
 uint8_t avdtp_get_seid(struct avdtp_remote_sep *sep);
 
 uint8_t avdtp_get_type(struct avdtp_remote_sep *sep);
-- 
2.17.2


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, back to index

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-01-08 15:49 [PATCH BlueZ 1/6] doc/media-api: Enable MediaEndpoint to expose remote SEP Luiz Augusto von Dentz
2019-01-08 15:49 ` [PATCH BlueZ 2/6] a2dp: Expose " Luiz Augusto von Dentz
2019-01-08 15:49 ` [PATCH BlueZ 3/6] doc/media-api: Add Endpoint property to MediaTransport Luiz Augusto von Dentz
2019-01-08 15:49 ` [PATCH BlueZ 4/6] a2dp: Implement MediaTransport.Endpoint Luiz Augusto von Dentz
2019-01-08 15:49 ` [PATCH BlueZ 5/6] doc/settings-storage: Add Endpoint group to cache Luiz Augusto von Dentz
2019-01-08 15:49 ` [PATCH BlueZ 6/6] a2dp: Cache remote endpoints Luiz Augusto von Dentz

Linux-Bluetooth Archive on lore.kernel.org

Archives are clonable:
	git clone --mirror https://lore.kernel.org/linux-bluetooth/0 linux-bluetooth/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 linux-bluetooth linux-bluetooth/ https://lore.kernel.org/linux-bluetooth \
		linux-bluetooth@vger.kernel.org linux-bluetooth@archiver.kernel.org
	public-inbox-index linux-bluetooth


Newsgroup available over NNTP:
	nntp://nntp.lore.kernel.org/org.kernel.vger.linux-bluetooth


AGPL code for this site: git clone https://public-inbox.org/ public-inbox