summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Wu <jrwu@google.com>2024-04-19 08:40:12 +0000
committerChromeos LUCI <chromeos-scoped@luci-project-accounts.iam.gserviceaccount.com>2024-05-14 12:02:01 +0000
commit83d168a00d7500b17d91b92566007fe2e3889828 (patch)
treec2143d60057701aee4e559e3806d567749cd7d4b
parent2bfebafecbaace3f09707556e79d14ed00e6ecf0 (diff)
downloadadhd-83d168a00d7500b17d91b92566007fe2e3889828.tar.gz
floss: implement |cras_lea_manager|
This CL implements |cras_lea_manager|, the component that resolves events and issues requests while managing iodev resources for LEA. BUG=b:317682584 TEST=Apply full patch series and verify unicast works Change-Id: I014b5295d3740be9f25a1356faf84d234fadca04 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/adhd/+/5467607 Tested-by: chromeos-cop-builder@chromeos-cop.iam.gserviceaccount.com <chromeos-cop-builder@chromeos-cop.iam.gserviceaccount.com> Commit-Queue: Jeremy Wu <jrwu@google.com> Reviewed-by: Hsinyu Chao <hychao@chromium.org>
-rw-r--r--cras/src/server/BUILD.bazel2
-rw-r--r--cras/src/server/cras_fl_media_adapter.c13
-rw-r--r--cras/src/server/cras_fl_media_adapter.h2
-rw-r--r--cras/src/server/cras_lea_iodev.c7
-rw-r--r--cras/src/server/cras_lea_iodev.h4
-rw-r--r--cras/src/server/cras_lea_manager.c425
-rw-r--r--cras/src/server/cras_lea_manager.h101
-rw-r--r--cras/src/tests/BUILD.bazel26
-rw-r--r--cras/src/tests/lea_manager_unittest.cc316
9 files changed, 891 insertions, 5 deletions
diff --git a/cras/src/server/BUILD.bazel b/cras/src/server/BUILD.bazel
index c18f0cdf..a3fe868f 100644
--- a/cras/src/server/BUILD.bazel
+++ b/cras/src/server/BUILD.bazel
@@ -439,6 +439,8 @@ cc_library(
"cras_audio_thread_monitor.h",
"cras_lea_iodev.c",
"cras_lea_iodev.h",
+ "cras_lea_manager.c",
+ "cras_lea_manager.h",
"cras_bt_adapter.c",
"cras_bt_adapter.h",
"cras_bt_battery_provider.c",
diff --git a/cras/src/server/cras_fl_media_adapter.c b/cras/src/server/cras_fl_media_adapter.c
index 2684c3e0..c6df28c7 100644
--- a/cras/src/server/cras_fl_media_adapter.c
+++ b/cras/src/server/cras_fl_media_adapter.c
@@ -16,6 +16,7 @@
#include "cras/common/check.h"
#include "cras/server/platform/features/features.h"
#include "cras/src/server/cras_a2dp_manager.h"
+#include "cras/src/server/cras_lea_manager.h"
#include "cras/src/server/cras_bt_io.h"
#include "cras/src/server/cras_bt_log.h"
#include "cras/src/server/cras_bt_policy.h"
@@ -74,7 +75,11 @@ int handle_on_lea_group_connected(struct fl_media* active_fm,
return -EINVAL;
}
- // TODO: notify BLE manager
+ if (!active_fm->lea) {
+ active_fm->lea = cras_floss_lea_create(active_fm);
+ }
+
+ cras_floss_lea_add_group(active_fm->lea, name, group_id);
return 0;
}
@@ -91,7 +96,7 @@ int handle_on_lea_group_disconnected(struct fl_media* active_fm, int group_id) {
return -EINVAL;
}
- // TODO: notify BLE manager
+ cras_floss_lea_remove_group(active_fm->lea, group_id);
return 0;
}
@@ -117,7 +122,9 @@ int handle_on_lea_audio_conf(struct fl_media* active_fm,
return -EINVAL;
}
- // TODO: notify BLE manager
+ cras_floss_lea_audio_conf_updated(active_fm->lea, direction, group_id,
+ snk_audio_location, src_audio_location,
+ available_contexts);
return 0;
}
diff --git a/cras/src/server/cras_fl_media_adapter.h b/cras/src/server/cras_fl_media_adapter.h
index 5379996b..0334cfe8 100644
--- a/cras/src/server/cras_fl_media_adapter.h
+++ b/cras/src/server/cras_fl_media_adapter.h
@@ -36,6 +36,8 @@ struct fl_media {
DBusConnection* conn;
// Object representing the connected A2DP headset.
struct cras_a2dp* a2dp;
+ // Object representing the LEA service.
+ struct cras_lea* lea;
// Object representing the connected HFP headset.
struct cras_hfp* hfp;
struct bt_io_manager* bt_io_mgr;
diff --git a/cras/src/server/cras_lea_iodev.c b/cras/src/server/cras_lea_iodev.c
index 032a4be2..c8b17f1b 100644
--- a/cras/src/server/cras_lea_iodev.c
+++ b/cras/src/server/cras_lea_iodev.c
@@ -19,6 +19,7 @@
#include "cras/src/server/audio_thread_log.h"
#include "cras/src/server/cras_audio_area.h"
#include "cras/src/server/cras_audio_thread_monitor.h"
+#include "cras/src/server/cras_lea_manager.h"
#include "cras/src/server/cras_iodev.h"
#include "cras/src/server/cras_iodev_list.h"
#include "cras/src/server/cras_utf8.h"
@@ -41,6 +42,8 @@ struct lea_io {
// How many frames of audio samples we prefer to write in one
// socket write.
unsigned int write_block;
+ // The associated |cras_lea| object.
+ struct cras_lea* lea;
// The associated ID of the corresponding LE audio group.
int group_id;
// If the device has been configured and attached with any stream.
@@ -59,7 +62,8 @@ static void lea_free_base_resources(struct lea_io* leaio) {
free(leaio->base.supported_formats);
}
-struct cras_iodev* lea_iodev_create(const char* name,
+struct cras_iodev* lea_iodev_create(struct cras_lea* lea,
+ const char* name,
int group_id,
enum CRAS_STREAM_DIRECTION dir) {
struct lea_io* leaio;
@@ -73,6 +77,7 @@ struct cras_iodev* lea_iodev_create(const char* name,
}
leaio->started = 0;
+ leaio->lea = lea;
leaio->group_id = group_id;
iodev = &leaio->base;
diff --git a/cras/src/server/cras_lea_iodev.h b/cras/src/server/cras_lea_iodev.h
index 064bbabc..c47f56b3 100644
--- a/cras/src/server/cras_lea_iodev.h
+++ b/cras/src/server/cras_lea_iodev.h
@@ -18,11 +18,13 @@ extern "C" {
* Note that if a group supports both input and output, two `lea_iodev`s
* will be instantiated.
* Args:
+ * lea - The associated |cras_lea| object.
* name - Name associated to the LE audio group.
* group - ID of the associated group.
* dir - The direction of the iodev.
*/
-struct cras_iodev* lea_iodev_create(const char* name,
+struct cras_iodev* lea_iodev_create(struct cras_lea* lea,
+ const char* name,
int group_id,
enum CRAS_STREAM_DIRECTION dir);
diff --git a/cras/src/server/cras_lea_manager.c b/cras/src/server/cras_lea_manager.c
new file mode 100644
index 00000000..8daf8528
--- /dev/null
+++ b/cras/src/server/cras_lea_manager.c
@@ -0,0 +1,425 @@
+/* Copyright 2024 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE // for ppoll
+#endif
+
+#include "cras/src/server/cras_lea_manager.h"
+
+#include <errno.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/un.h>
+#include <syslog.h>
+
+#include "cras/server/main_message.h"
+#include "cras/src/server/cras_lea_iodev.h"
+#include "cras/src/server/cras_fl_media.h"
+#include "cras/src/server/cras_iodev_list.h"
+#include "cras/src/server/cras_system_state.h"
+#include "cras/src/server/cras_tm.h"
+#include "cras_config.h"
+#include "cras_util.h"
+#include "third_party/utlist/utlist.h"
+
+#define FLOSS_LEA_DATA_PATH "/run/bluetooth/audio/.lea_data"
+
+/* Object (list) holding information about LE-Audio groups. */
+struct lea_group {
+ char* name;
+ int group_id;
+ uint8_t direction; /* Bitmask of |LEA_AUDIO_DIRECTION| */
+ uint16_t available_contexts; /* Bitmask of |LEA_AUDIO_CONTEXT_TYPE| */
+ uint32_t data_interval_us;
+ uint32_t sample_rate;
+ uint8_t bits_per_sample;
+ uint8_t channels_count;
+
+ struct cras_iodev *idev, *odev;
+ struct lea_group *prev, *next;
+};
+
+/* Object holding information and resources of a connected LEA headset. */
+struct cras_lea {
+ // Object representing the media interface of BT adapter.
+ struct fl_media* fm;
+ // A list of connected LE-Audio groups.
+ // The first group in the list is the primary group.
+ struct lea_group* connected_groups;
+ // The file descriptor for LEA socket.
+ int fd;
+ // If the input has started. This is used to determine if
+ // a lea start or stop is required.
+ int idev_started;
+ // If the output has started. This is used to determine if
+ // a lea start or stop is required.
+ int odev_started;
+};
+
+int cras_floss_lea_configure_sink_for_voice_communication(
+ struct cras_lea* lea) {
+ return floss_media_lea_sink_metadata_changed(
+ lea->fm, FL_LEA_AUDIO_SOURCE_VOICE_COMMUNICATION, 1.0);
+}
+
+int cras_floss_lea_configure_source_for_voice_communication(
+ struct cras_lea* lea) {
+ return floss_media_lea_source_metadata_changed(
+ lea->fm, FL_LEA_AUDIO_USAGE_VOICE_COMMUNICATION,
+ FL_LEA_AUDIO_CONTENT_TYPE_MUSIC, 0.0);
+}
+
+int cras_floss_lea_configure_source_for_media(struct cras_lea* lea) {
+ return floss_media_lea_source_metadata_changed(
+ lea->fm, FL_LEA_AUDIO_USAGE_MEDIA, FL_LEA_AUDIO_CONTENT_TYPE_MUSIC, 1.0);
+}
+
+void fill_floss_lea_skt_addr(struct sockaddr_un* addr) {
+ memset(addr, 0, sizeof(*addr));
+ addr->sun_family = AF_UNIX;
+ snprintf(addr->sun_path, CRAS_MAX_SOCKET_PATH_SIZE, FLOSS_LEA_DATA_PATH);
+}
+
+static void set_dev_started(struct cras_lea* lea,
+ enum CRAS_STREAM_DIRECTION dir,
+ int started) {
+ if (dir == CRAS_STREAM_INPUT) {
+ lea->idev_started = started;
+ } else if (dir == CRAS_STREAM_OUTPUT) {
+ lea->odev_started = started;
+ }
+}
+
+/* Creates a |cras_lea| object representing the LEA service. */
+struct cras_lea* cras_floss_lea_create(struct fl_media* fm) {
+ struct cras_lea* lea;
+ lea = (struct cras_lea*)calloc(1, sizeof(*lea));
+
+ if (!lea) {
+ return NULL;
+ }
+
+ lea->fm = fm;
+ lea->fd = -1;
+
+ return lea;
+}
+
+int cras_floss_lea_start(struct cras_lea* lea,
+ thread_callback cb,
+ enum CRAS_STREAM_DIRECTION dir) {
+ int skt_fd;
+ int rc;
+ struct sockaddr_un addr;
+ struct timespec timeout = {10, 0};
+ struct pollfd poll_fd;
+ struct lea_group* group = lea->connected_groups;
+
+ if ((dir == CRAS_STREAM_INPUT && lea->idev_started) ||
+ (dir == CRAS_STREAM_OUTPUT && lea->odev_started)) {
+ return -EINVAL;
+ }
+
+ if (!group) {
+ return -EINVAL;
+ }
+
+ if (dir == CRAS_STREAM_INPUT) {
+ rc = floss_media_lea_peer_start_audio_request(
+ lea->fm, &group->data_interval_us, &group->sample_rate,
+ &group->bits_per_sample, &group->channels_count);
+ } else if (dir == CRAS_STREAM_OUTPUT) {
+ rc = floss_media_lea_host_start_audio_request(
+ lea->fm, &group->data_interval_us, &group->sample_rate,
+ &group->bits_per_sample, &group->channels_count);
+ } else {
+ syslog(LOG_ERR, "%s: unsupported direction %d", __func__, dir);
+ return -EINVAL;
+ }
+
+ if (rc < 0) {
+ return rc;
+ }
+
+ /* Check if the socket connection has been started by another
+ * direction's iodev. We can skip the data channel setup if so. */
+ if (lea->idev_started || lea->odev_started) {
+ goto start_dev;
+ }
+
+ skt_fd = socket(PF_UNIX, SOCK_STREAM | O_NONBLOCK, 0);
+ if (skt_fd < 0) {
+ syslog(LOG_WARNING, "Create LEA socket failed with error %d", errno);
+ rc = skt_fd;
+ goto error;
+ }
+
+ fill_floss_lea_skt_addr(&addr);
+
+ syslog(LOG_DEBUG, "Connect to LEA socket at %s ", addr.sun_path);
+ rc = connect(skt_fd, (struct sockaddr*)&addr, sizeof(addr));
+ if (rc < 0) {
+ syslog(LOG_WARNING, "Connect to LEA socket failed with error %d", errno);
+ goto error;
+ }
+
+ poll_fd.fd = skt_fd;
+ poll_fd.events = POLLIN | POLLOUT;
+
+ rc = ppoll(&poll_fd, 1, &timeout, NULL);
+ if (rc <= 0) {
+ syslog(LOG_WARNING, "Poll for LEA socket failed with error %d", errno);
+ goto error;
+ }
+
+ if (poll_fd.revents & (POLLERR | POLLHUP)) {
+ syslog(LOG_WARNING, "LEA socket error, revents: %u.", poll_fd.revents);
+ rc = -1;
+ goto error;
+ }
+
+ lea->fd = skt_fd;
+
+ audio_thread_add_events_callback(lea->fd, cb, lea,
+ POLLOUT | POLLIN | POLLERR | POLLHUP);
+
+start_dev:
+ set_dev_started(lea, dir, 1);
+
+ return 0;
+error:
+ if (dir == CRAS_STREAM_INPUT) {
+ floss_media_lea_peer_stop_audio_request(lea->fm);
+ } else if (dir == CRAS_STREAM_OUTPUT) {
+ floss_media_lea_host_stop_audio_request(lea->fm);
+ }
+
+ if (skt_fd >= 0) {
+ close(skt_fd);
+ unlink(addr.sun_path);
+ }
+ return rc;
+}
+
+int cras_floss_lea_stop(struct cras_lea* lea, enum CRAS_STREAM_DIRECTION dir) {
+ int rc;
+
+ // i/odev_started is only used to determine LEA status.
+ if (!(lea->idev_started || lea->odev_started)) {
+ return 0;
+ }
+
+ set_dev_started(lea, dir, 0);
+
+ if (dir == CRAS_STREAM_INPUT) {
+ rc = floss_media_lea_peer_stop_audio_request(lea->fm);
+ if (rc < 0) {
+ syslog(LOG_ERR, "%s: Failed to stop peer audio request", __func__);
+ return rc;
+ }
+ } else if (dir == CRAS_STREAM_OUTPUT) {
+ rc = floss_media_lea_host_stop_audio_request(lea->fm);
+ if (rc < 0) {
+ syslog(LOG_ERR, "%s: Failed to stop host audio request", __func__);
+ return rc;
+ }
+ }
+
+ if (lea->idev_started || lea->odev_started) {
+ return 0;
+ }
+
+ if (lea->fd >= 0) {
+ audio_thread_rm_callback_sync(cras_iodev_list_get_audio_thread(), lea->fd);
+ close(lea->fd);
+ }
+ lea->fd = -1;
+
+ return 0;
+}
+
+int cras_floss_lea_fill_format(struct cras_lea* lea,
+ size_t** rates,
+ snd_pcm_format_t** formats,
+ size_t** channel_counts) {
+ struct lea_group* group = lea->connected_groups;
+
+ if (group == NULL) {
+ return 0;
+ }
+
+ *rates = (size_t*)malloc(2 * sizeof(size_t));
+ if (!*rates) {
+ return -ENOMEM;
+ }
+ (*rates)[0] = group->sample_rate;
+ (*rates)[1] = 0;
+
+ *formats = (snd_pcm_format_t*)malloc(2 * sizeof(snd_pcm_format_t));
+ if (!*formats) {
+ return -ENOMEM;
+ }
+ switch (group->bits_per_sample) {
+ case 16:
+ (*formats)[0] = SND_PCM_FORMAT_S16_LE;
+ break;
+ case 24:
+ (*formats)[0] = SND_PCM_FORMAT_S24_3LE;
+ break;
+ case 32:
+ (*formats)[0] = SND_PCM_FORMAT_S32_LE;
+ break;
+ default:
+ syslog(LOG_ERR, "%s: Unknown bits_per_sample %d", __func__,
+ group->bits_per_sample);
+ return -EINVAL;
+ }
+ (*formats)[1] = (snd_pcm_format_t)0;
+
+ *channel_counts = (size_t*)malloc(2 * sizeof(size_t));
+ if (!*channel_counts) {
+ return -ENOMEM;
+ }
+ (*channel_counts)[0] = group->channels_count;
+ (*channel_counts)[1] = 0;
+ return 0;
+}
+
+// TODO: use software volume if VCP is missing.
+void cras_floss_lea_set_volume(struct cras_lea* lea, unsigned int volume) {
+ syslog(LOG_DEBUG, "%s: set_volume(%u)", __func__, volume);
+ struct lea_group* group = lea->connected_groups;
+ floss_media_lea_set_group_volume(lea->fm, group->group_id,
+ volume * 255 / 100);
+}
+
+void cras_floss_lea_destroy(struct cras_lea* lea) {
+ struct lea_group* group;
+ DL_FOREACH (lea->connected_groups, group) {
+ if (group->idev) {
+ lea_iodev_destroy(group->idev);
+ }
+ if (group->odev) {
+ lea_iodev_destroy(group->odev);
+ }
+ DL_DELETE(lea->connected_groups, group);
+ free(group->name);
+ free(group);
+ }
+
+ if (lea->fd >= 0) {
+ close(lea->fd);
+ }
+
+ free(lea);
+}
+
+void cras_floss_lea_set_active(struct cras_lea* lea,
+ int group_id,
+ unsigned enabled) {
+ // Action is needed (and meaningful) only when there is no stream.
+ if (lea->idev_started || lea->odev_started) {
+ return;
+ }
+
+ if (!enabled) {
+ group_id = FL_LEA_GROUP_NONE;
+ }
+
+ floss_media_lea_set_active_group(lea->fm, group_id);
+}
+
+int cras_floss_lea_get_fd(struct cras_lea* lea) {
+ return lea->fd;
+}
+
+// TODO: check I/O availability instead of adding both
+void cras_floss_lea_add_group(struct cras_lea* lea,
+ const char* name,
+ int group_id) {
+ struct lea_group* group;
+ DL_FOREACH (lea->connected_groups, group) {
+ if (group_id == group->group_id) {
+ syslog(LOG_WARNING, "%s: Skipping added group %s", __func__, name);
+ return;
+ }
+ }
+
+ group = (struct lea_group*)calloc(1, sizeof(struct lea_group));
+ group->name = strdup(name);
+ group->group_id = group_id;
+
+ DL_APPEND(lea->connected_groups, group);
+
+ group->idev = lea_iodev_create(lea, name, group_id, CRAS_STREAM_INPUT);
+ group->odev = lea_iodev_create(lea, name, group_id, CRAS_STREAM_OUTPUT);
+
+ // Set plugged and UI will see these iodevs.
+ cras_iodev_set_node_plugged(group->idev->active_node, 1);
+ cras_iodev_set_node_plugged(group->odev->active_node, 1);
+
+ cras_iodev_list_notify_nodes_changed();
+}
+
+void cras_floss_lea_remove_group(struct cras_lea* lea, int group_id) {
+ struct lea_group* group;
+ DL_FOREACH (lea->connected_groups, group) {
+ if (group_id == group->group_id) {
+ lea_iodev_destroy(group->idev);
+ lea_iodev_destroy(group->odev);
+
+ DL_DELETE(lea->connected_groups, group);
+ free(group->name);
+ free(group);
+ }
+ }
+}
+
+int cras_floss_lea_audio_conf_updated(struct cras_lea* lea,
+ uint8_t direction,
+ int group_id,
+ uint32_t snk_audio_location,
+ uint32_t src_audio_location,
+ uint16_t available_contexts) {
+ struct lea_group* group = lea->connected_groups;
+
+ if (!group || group->group_id != group_id) {
+ syslog(LOG_WARNING, "Cannot find lea_group %d to update audio conf",
+ group_id);
+ return -ENOENT;
+ }
+
+ group->available_contexts = available_contexts;
+
+ if ((group->direction & LEA_AUDIO_DIRECTION_OUTPUT) !=
+ (direction & LEA_AUDIO_DIRECTION_OUTPUT)) {
+ if (direction & LEA_AUDIO_DIRECTION_OUTPUT) {
+ group->odev->active_node->plugged = 1;
+ gettimeofday(&group->odev->active_node->plugged_time, NULL);
+ } else {
+ cras_iodev_list_disable_and_close_dev_group(group->odev);
+ }
+ }
+
+ if ((group->direction & LEA_AUDIO_DIRECTION_INPUT) !=
+ (direction & LEA_AUDIO_DIRECTION_INPUT)) {
+ if (direction & LEA_AUDIO_DIRECTION_INPUT) {
+ group->idev->active_node->plugged = 1;
+ gettimeofday(&group->idev->active_node->plugged_time, NULL);
+ } else {
+ cras_iodev_list_disable_and_close_dev_group(group->idev);
+ }
+ }
+
+ group->direction = direction;
+
+ cras_iodev_list_notify_nodes_changed();
+
+ return 0;
+}
diff --git a/cras/src/server/cras_lea_manager.h b/cras/src/server/cras_lea_manager.h
new file mode 100644
index 00000000..9585b552
--- /dev/null
+++ b/cras/src/server/cras_lea_manager.h
@@ -0,0 +1,101 @@
+/* Copyright 2024 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef CRAS_SRC_SERVER_CRAS_LEA_MANAGER_H_
+#define CRAS_SRC_SERVER_CRAS_LEA_MANAGER_H_
+
+#include "cras/src/server/audio_thread.h"
+#include "cras/src/server/cras_iodev.h"
+#include "cras_audio_format.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct cras_lea;
+struct fl_media;
+
+enum LEA_AUDIO_CONTEXT_TYPE {
+ LEA_AUDIO_CONTEXT_UNINITIALIZED = 0x0000,
+ LEA_AUDIO_CONTEXT_UNSPECIFIED = 0x0001,
+ LEA_AUDIO_CONTEXT_CONVERSATIONAL = 0x0002,
+ LEA_AUDIO_CONTEXT_MEDIA = 0x0004,
+ LEA_AUDIO_CONTEXT_GAME = 0x0008,
+ LEA_AUDIO_CONTEXT_INSTRUCTIONAL = 0x0010,
+ LEA_AUDIO_CONTEXT_VOICEASSISTANTS = 0x0020,
+ LEA_AUDIO_CONTEXT_LIVE = 0x0040,
+ LEA_AUDIO_CONTEXT_SOUNDEFFECTS = 0x0080,
+ LEA_AUDIO_CONTEXT_NOTIFICATIONS = 0x0100,
+ LEA_AUDIO_CONTEXT_RINGTONE = 0x0200,
+ LEA_AUDIO_CONTEXT_ALERTS = 0x0400,
+ LEA_AUDIO_CONTEXT_EMERGENCYALARM = 0x0800,
+ LEA_AUDIO_CONTEXT_RFU = 0x1000,
+};
+
+enum LEA_AUDIO_DIRECTION {
+ LEA_AUDIO_DIRECTION_NONE = 0,
+ LEA_AUDIO_DIRECTION_OUTPUT = (1 << 0),
+ LEA_AUDIO_DIRECTION_INPUT = (1 << 1),
+};
+
+enum LEA_GROUP_STATUS {
+ LEA_GROUP_INACTIVE,
+ LEA_GROUP_ACTIVE,
+ LEA_GROUP_TURNED_IDLE_DURING_CALL,
+};
+
+enum LEA_GROUP_NODE_STATUS {
+ LEA_GROUP_NODE_ADDED = 1,
+ LEA_GROUP_NODE_REMOVED,
+};
+
+int cras_floss_lea_configure_sink_for_voice_communication(struct cras_lea* lea);
+
+int cras_floss_lea_configure_source_for_voice_communication(
+ struct cras_lea* lea);
+
+int cras_floss_lea_configure_source_for_media(struct cras_lea* lea);
+
+struct cras_lea* cras_floss_lea_create(struct fl_media* fm);
+
+void cras_floss_lea_destroy(struct cras_lea* lea);
+
+int cras_floss_lea_start(struct cras_lea* lea,
+ thread_callback cb,
+ enum CRAS_STREAM_DIRECTION dir);
+
+int cras_floss_lea_stop(struct cras_lea* lea, enum CRAS_STREAM_DIRECTION dir);
+
+int cras_floss_lea_get_fd(struct cras_lea* lea);
+
+void cras_floss_lea_set_active(struct cras_lea* lea,
+ int group_id,
+ unsigned enabled);
+
+void cras_floss_lea_set_volume(struct cras_lea* lea, unsigned int volume);
+
+int cras_floss_lea_fill_format(struct cras_lea* lea,
+ size_t** rates,
+ snd_pcm_format_t** formats,
+ size_t** channel_counts);
+
+void cras_floss_lea_add_group(struct cras_lea* lea,
+ const char* name,
+ int group_id);
+
+void cras_floss_lea_remove_group(struct cras_lea* lea, int group_id);
+
+int cras_floss_lea_audio_conf_updated(struct cras_lea* lea,
+ uint8_t direction,
+ int group_id,
+ uint32_t snk_audio_location,
+ uint32_t src_audio_location,
+ uint16_t available_contexts);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // CRAS_SRC_SERVER_CRAS_LEA_MANAGER_H_
diff --git a/cras/src/tests/BUILD.bazel b/cras/src/tests/BUILD.bazel
index e0771ce7..d955d66f 100644
--- a/cras/src/tests/BUILD.bazel
+++ b/cras/src/tests/BUILD.bazel
@@ -411,6 +411,32 @@ cc_test(
)
cc_test(
+ name = "lea_manager_unittest",
+ srcs = [
+ ":lea_manager_unittest.cc",
+ "//cras/src/server:cras_lea_manager.c",
+ ],
+ copts = [
+ "-fdata-sections",
+ "-ffunction-sections",
+ ],
+ deps = [
+ ":test_support",
+ "//cras/common:rust_common_cc",
+ "//cras/server/platform/dlc:cc",
+ "//cras/server/platform/features:override",
+ "//cras/src/common:all_headers",
+ "//cras/src/server:all_headers",
+ "//cras/src/server/rust:cc",
+ "@iniparser",
+ "@pkg_config//alsa",
+ "@pkg_config//dbus-1",
+ "@pkg_config//gtest",
+ "@pkg_config//gtest_main",
+ ],
+)
+
+cc_test(
name = "blob_wrapper_unittest",
srcs = [
":blob_wrapper_unittest.cc",
diff --git a/cras/src/tests/lea_manager_unittest.cc b/cras/src/tests/lea_manager_unittest.cc
new file mode 100644
index 00000000..820e4e10
--- /dev/null
+++ b/cras/src/tests/lea_manager_unittest.cc
@@ -0,0 +1,316 @@
+// Copyright 2024 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+
+#include "cras/src/server/cras_lea_iodev.h"
+#include "cras/src/server/cras_lea_manager.h"
+#include "cras/src/server/cras_fl_media.h"
+#include "cras/src/server/cras_iodev.h"
+#include "cras/src/server/cras_iodev_list.h"
+#include "cras_types.h"
+
+static size_t connect_called;
+static int connect_ret;
+static struct cras_lea* lea_iodev_create_lea_val;
+static struct cras_iodev cras_idev;
+static struct cras_iodev cras_odev;
+struct cras_iodev* lea_iodev_create_idev_ret;
+struct cras_iodev* lea_iodev_create_odev_ret;
+static size_t lea_iodev_create_called;
+static size_t lea_iodev_destroy_called;
+static size_t cras_iodev_set_node_plugged_called;
+static int cras_iodev_set_node_plugged_value;
+static size_t notify_nodes_changed_called;
+static size_t floss_media_lea_host_start_audio_request_called;
+static size_t floss_media_lea_host_stop_audio_request_called;
+static size_t floss_media_lea_peer_start_audio_request_called;
+static size_t floss_media_lea_peer_stop_audio_request_called;
+static size_t floss_media_lea_set_group_volume_called;
+static unsigned int floss_media_lea_set_group_volume_volume_val;
+static int socket_ret;
+static int audio_thread_add_events_callback_called;
+static int audio_thread_add_events_callback_fd;
+static thread_callback audio_thread_add_events_callback_cb;
+static void* audio_thread_add_events_callback_data;
+static int audio_thread_config_events_callback_called;
+static enum AUDIO_THREAD_EVENTS_CB_TRIGGER
+ audio_thread_config_events_callback_trigger;
+
+void ResetStubData() {
+ connect_called = 0;
+ connect_ret = 0;
+ lea_iodev_create_lea_val = NULL;
+ lea_iodev_create_idev_ret = &cras_idev;
+ lea_iodev_create_odev_ret = &cras_odev;
+ lea_iodev_create_called = 0;
+ lea_iodev_destroy_called = 0;
+ cras_iodev_set_node_plugged_called = 0;
+ cras_iodev_set_node_plugged_value = 0;
+ notify_nodes_changed_called = 0;
+ floss_media_lea_host_start_audio_request_called = 0;
+ floss_media_lea_host_stop_audio_request_called = 0;
+ floss_media_lea_peer_start_audio_request_called = 0;
+ floss_media_lea_peer_stop_audio_request_called = 0;
+ floss_media_lea_set_group_volume_called = 0;
+ floss_media_lea_set_group_volume_volume_val = 0;
+ audio_thread_add_events_callback_called = 0;
+ audio_thread_add_events_callback_fd = 0;
+ audio_thread_add_events_callback_cb = NULL;
+ audio_thread_add_events_callback_data = NULL;
+ audio_thread_config_events_callback_called = 0;
+ socket_ret = 456;
+}
+
+namespace {
+
+class LeaManagerTestSuite : public testing::Test {
+ protected:
+ virtual void SetUp() { ResetStubData(); }
+
+ virtual void TearDown() {}
+};
+
+TEST_F(LeaManagerTestSuite, PCMCreateDestroy) {
+ struct cras_lea* lea = cras_floss_lea_create(NULL);
+ ASSERT_NE(lea, (struct cras_lea*)NULL);
+
+ cras_floss_lea_add_group(lea, "name", 0);
+ EXPECT_EQ(lea, lea_iodev_create_lea_val);
+ EXPECT_EQ(lea_iodev_create_called, 2);
+ EXPECT_EQ(cras_iodev_set_node_plugged_called, 2);
+ EXPECT_EQ(notify_nodes_changed_called, 1);
+
+ cras_floss_lea_remove_group(lea, 0);
+ EXPECT_EQ(lea_iodev_destroy_called, 2);
+
+ cras_floss_lea_destroy(lea);
+}
+
+TEST_F(LeaManagerTestSuite, StartWithSocketFail) {
+ struct cras_lea* lea = cras_floss_lea_create(NULL);
+ ASSERT_NE(lea, (struct cras_lea*)NULL);
+
+ cras_floss_lea_add_group(lea, "name", 0);
+
+ socket_ret = -1;
+
+ thread_callback rwcb = reinterpret_cast<thread_callback>(0xdeadbeef);
+ EXPECT_EQ(socket_ret, cras_floss_lea_start(lea, rwcb, CRAS_STREAM_OUTPUT));
+
+ EXPECT_EQ(floss_media_lea_host_start_audio_request_called, 1);
+ EXPECT_EQ(audio_thread_add_events_callback_called, 0);
+ EXPECT_EQ(floss_media_lea_host_stop_audio_request_called, 1);
+ EXPECT_EQ(connect_called, 0);
+ EXPECT_EQ(cras_floss_lea_get_fd(lea), -1);
+
+ cras_floss_lea_remove_group(lea, 0);
+ EXPECT_EQ(lea_iodev_destroy_called, 2);
+
+ cras_floss_lea_destroy(lea);
+}
+
+TEST_F(LeaManagerTestSuite, StartWithConnectFail) {
+ struct cras_lea* lea = cras_floss_lea_create(NULL);
+ ASSERT_NE(lea, (struct cras_lea*)NULL);
+
+ cras_floss_lea_add_group(lea, "name", 0);
+
+ connect_ret = -1;
+
+ thread_callback rwcb = reinterpret_cast<thread_callback>(0xdeadbeef);
+ EXPECT_EQ(connect_ret, cras_floss_lea_start(lea, rwcb, CRAS_STREAM_OUTPUT));
+
+ EXPECT_EQ(floss_media_lea_host_start_audio_request_called, 1);
+ EXPECT_EQ(connect_called, 1);
+ EXPECT_EQ(audio_thread_add_events_callback_called, 0);
+ EXPECT_EQ(floss_media_lea_host_stop_audio_request_called, 1);
+ EXPECT_EQ(cras_floss_lea_get_fd(lea), -1);
+
+ cras_floss_lea_remove_group(lea, 0);
+ EXPECT_EQ(lea_iodev_destroy_called, 2);
+
+ cras_floss_lea_destroy(lea);
+}
+
+TEST_F(LeaManagerTestSuite, StartStop) {
+ struct cras_lea* lea = cras_floss_lea_create(NULL);
+ ASSERT_NE(lea, (struct cras_lea*)NULL);
+
+ cras_floss_lea_add_group(lea, "name", 0);
+
+ EXPECT_EQ(cras_floss_lea_get_fd(lea), -1);
+
+ thread_callback rwcb = reinterpret_cast<thread_callback>(0xdeadbeef);
+ cras_floss_lea_start(lea, rwcb, CRAS_STREAM_OUTPUT);
+ EXPECT_EQ(floss_media_lea_host_start_audio_request_called, 1);
+ EXPECT_EQ(cras_floss_lea_get_fd(lea), socket_ret);
+
+ cras_floss_lea_start(lea, rwcb, CRAS_STREAM_INPUT);
+ EXPECT_EQ(floss_media_lea_peer_start_audio_request_called, 1);
+ EXPECT_EQ(audio_thread_add_events_callback_called, 1);
+ EXPECT_EQ(audio_thread_add_events_callback_fd, socket_ret);
+ EXPECT_EQ((struct cras_lea*)audio_thread_add_events_callback_data, lea);
+
+ cras_floss_lea_stop(lea, CRAS_STREAM_OUTPUT);
+ EXPECT_EQ(floss_media_lea_host_stop_audio_request_called, 1);
+ EXPECT_EQ(cras_floss_lea_get_fd(lea), socket_ret);
+
+ cras_floss_lea_stop(lea, CRAS_STREAM_INPUT);
+ EXPECT_EQ(floss_media_lea_peer_stop_audio_request_called, 1);
+ EXPECT_EQ(cras_floss_lea_get_fd(lea), -1);
+
+ cras_floss_lea_remove_group(lea, 0);
+ EXPECT_EQ(lea_iodev_destroy_called, 2);
+
+ cras_floss_lea_destroy(lea);
+}
+
+TEST_F(LeaManagerTestSuite, SetVolume) {
+ struct cras_lea* lea = cras_floss_lea_create(NULL);
+ ASSERT_NE(lea, (struct cras_lea*)NULL);
+
+ cras_floss_lea_add_group(lea, "name", 0);
+ EXPECT_EQ(lea, lea_iodev_create_lea_val);
+ EXPECT_EQ(lea_iodev_create_called, 2);
+
+ cras_floss_lea_set_volume(lea, 100);
+ EXPECT_EQ(floss_media_lea_set_group_volume_called, 1);
+ EXPECT_EQ(floss_media_lea_set_group_volume_volume_val, 255);
+
+ cras_floss_lea_set_volume(lea, 0);
+ EXPECT_EQ(floss_media_lea_set_group_volume_called, 2);
+ EXPECT_EQ(floss_media_lea_set_group_volume_volume_val, 0);
+
+ cras_floss_lea_set_volume(lea, 50);
+ EXPECT_EQ(floss_media_lea_set_group_volume_called, 3);
+ EXPECT_EQ(floss_media_lea_set_group_volume_volume_val, 127);
+
+ cras_floss_lea_set_volume(lea, 20);
+ EXPECT_EQ(floss_media_lea_set_group_volume_called, 4);
+ EXPECT_EQ(floss_media_lea_set_group_volume_volume_val, 51);
+
+ cras_floss_lea_remove_group(lea, 0);
+ EXPECT_EQ(lea_iodev_destroy_called, 2);
+
+ cras_floss_lea_destroy(lea);
+}
+} // namespace
+
+extern "C" {
+
+// socket and connect
+int socket(int domain, int type, int protocol) {
+ return socket_ret;
+}
+
+int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen) {
+ connect_called++;
+ return connect_ret;
+}
+
+// From audio_thread
+struct audio_thread_event_log* atlog;
+
+// From cras_bt_log
+struct cras_bt_event_log* btlog;
+
+void audio_thread_add_events_callback(int fd,
+ thread_callback cb,
+ void* data,
+ int events) {
+ audio_thread_add_events_callback_called++;
+ audio_thread_add_events_callback_fd = fd;
+ audio_thread_add_events_callback_cb = cb;
+ audio_thread_add_events_callback_data = data;
+}
+
+void audio_thread_config_events_callback(
+ int fd,
+ enum AUDIO_THREAD_EVENTS_CB_TRIGGER trigger) {
+ audio_thread_config_events_callback_called++;
+ audio_thread_config_events_callback_trigger = trigger;
+}
+
+int audio_thread_rm_callback_sync(struct audio_thread* thread, int fd) {
+ return 0;
+}
+
+// From cras iodev list
+struct audio_thread* cras_iodev_list_get_audio_thread() {
+ return NULL;
+}
+
+void cras_iodev_set_node_plugged(struct cras_ionode* ionode, int plugged) {
+ cras_iodev_set_node_plugged_called++;
+ cras_iodev_set_node_plugged_value = plugged;
+}
+
+// From cras iodev
+void cras_iodev_list_notify_nodes_changed() {
+ notify_nodes_changed_called++;
+}
+
+// From cras_lea_iodev
+struct cras_iodev* lea_iodev_create(struct cras_lea* lea,
+ const char* name,
+ int group_id,
+ enum CRAS_STREAM_DIRECTION dir) {
+ lea_iodev_create_lea_val = lea;
+ lea_iodev_create_called++;
+
+ switch (dir) {
+ case CRAS_STREAM_OUTPUT:
+ return lea_iodev_create_odev_ret;
+ case CRAS_STREAM_INPUT:
+ return lea_iodev_create_idev_ret;
+ default:
+ return NULL;
+ }
+
+ return NULL;
+}
+
+void lea_iodev_destroy(struct cras_iodev* iodev) {
+ lea_iodev_destroy_called++;
+ return;
+}
+
+// From cras_fl_media
+int floss_media_lea_host_start_audio_request(struct fl_media* fm,
+ uint32_t* data_interval_us,
+ uint32_t* sample_rate,
+ uint8_t* bits_per_sample,
+ uint8_t* channels_count) {
+ floss_media_lea_host_start_audio_request_called++;
+ return 0;
+}
+
+int floss_media_lea_peer_start_audio_request(struct fl_media* fm,
+ uint32_t* data_interval_us,
+ uint32_t* sample_rate,
+ uint8_t* bits_per_sample,
+ uint8_t* channels_count) {
+ floss_media_lea_peer_start_audio_request_called++;
+ return 0;
+}
+
+int floss_media_lea_host_stop_audio_request(struct fl_media* fm) {
+ floss_media_lea_host_stop_audio_request_called++;
+ return 0;
+}
+
+int floss_media_lea_peer_stop_audio_request(struct fl_media* fm) {
+ floss_media_lea_peer_stop_audio_request_called++;
+ return 0;
+}
+
+int floss_media_lea_set_group_volume(struct fl_media* fm,
+ int group_id,
+ uint8_t volume) {
+ floss_media_lea_set_group_volume_called++;
+ floss_media_lea_set_group_volume_volume_val = volume;
+ return 0;
+}
+} // extern "C"