blob: 402cd75a7e54b743694d8e1b627cf6182fe4720c [file] [log] [blame]
/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <dbus/dbus.h>
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "cras_bt_device.h"
#include "cras_bt_endpoint.h"
#include "cras_bt_log.h"
#include "cras_bt_transport.h"
#include "cras_bt_constants.h"
#include "cras_system_state.h"
#include "utlist.h"
struct cras_bt_transport {
DBusConnection *conn;
char *object_path;
struct cras_bt_device *device;
enum cras_bt_device_profile profile;
int codec;
void *configuration;
int configuration_len;
enum cras_bt_transport_state state;
int fd;
uint16_t read_mtu;
uint16_t write_mtu;
int volume;
int removed;
struct cras_bt_endpoint *endpoint;
struct cras_bt_transport *prev, *next;
};
static struct cras_bt_transport *transports;
struct cras_bt_transport *cras_bt_transport_create(DBusConnection *conn,
const char *object_path)
{
struct cras_bt_transport *transport;
transport = calloc(1, sizeof(*transport));
if (transport == NULL)
return NULL;
transport->object_path = strdup(object_path);
if (transport->object_path == NULL) {
free(transport);
return NULL;
}
transport->conn = conn;
dbus_connection_ref(transport->conn);
transport->fd = -1;
transport->volume = -1;
DL_APPEND(transports, transport);
return transport;
}
void cras_bt_transport_set_endpoint(struct cras_bt_transport *transport,
struct cras_bt_endpoint *endpoint)
{
transport->endpoint = endpoint;
}
int cras_bt_transport_is_removed(struct cras_bt_transport *transport)
{
return transport->removed;
}
void cras_bt_transport_remove(struct cras_bt_transport *transport)
{
/*
* If the transport object is still associated with a valid
* endpoint. Flag it as removed and wait for the ClearConfiguration
* message from BT to actually suspend this A2DP connection and
* destroy the transport.
*/
if (transport->endpoint)
transport->removed = 1;
else
cras_bt_transport_destroy(transport);
}
void cras_bt_transport_destroy(struct cras_bt_transport *transport)
{
DL_DELETE(transports, transport);
dbus_connection_unref(transport->conn);
if (transport->fd >= 0)
close(transport->fd);
cras_bt_device_set_use_hardware_volume(transport->device, 0);
free(transport->object_path);
free(transport->configuration);
free(transport);
}
void cras_bt_transport_reset()
{
while (transports) {
syslog(LOG_INFO, "Bluetooth Transport: %s removed",
transports->object_path);
cras_bt_transport_destroy(transports);
}
}
struct cras_bt_transport *cras_bt_transport_get(const char *object_path)
{
struct cras_bt_transport *transport;
DL_FOREACH (transports, transport) {
if (strcmp(transport->object_path, object_path) == 0)
return transport;
}
return NULL;
}
size_t
cras_bt_transport_get_list(struct cras_bt_transport ***transport_list_out)
{
struct cras_bt_transport *transport;
struct cras_bt_transport **transport_list = NULL;
size_t num_transports = 0;
DL_FOREACH (transports, transport) {
struct cras_bt_transport **tmp;
tmp = realloc(transport_list,
sizeof(transport_list[0]) * (num_transports + 1));
if (!tmp) {
free(transport_list);
return -ENOMEM;
}
transport_list = tmp;
transport_list[num_transports++] = transport;
}
*transport_list_out = transport_list;
return num_transports;
}
const char *
cras_bt_transport_object_path(const struct cras_bt_transport *transport)
{
return transport->object_path;
}
struct cras_bt_device *
cras_bt_transport_device(const struct cras_bt_transport *transport)
{
return transport->device;
}
enum cras_bt_device_profile
cras_bt_transport_profile(const struct cras_bt_transport *transport)
{
return transport->profile;
}
int cras_bt_transport_configuration(const struct cras_bt_transport *transport,
void *configuration, int len)
{
if (len < transport->configuration_len)
return -ENOSPC;
memcpy(configuration, transport->configuration,
transport->configuration_len);
return 0;
}
enum cras_bt_transport_state
cras_bt_transport_state(const struct cras_bt_transport *transport)
{
return transport->state;
}
int cras_bt_transport_fd(const struct cras_bt_transport *transport)
{
return transport->fd;
}
uint16_t cras_bt_transport_write_mtu(const struct cras_bt_transport *transport)
{
return transport->write_mtu;
}
static enum cras_bt_transport_state
cras_bt_transport_state_from_string(const char *value)
{
if (strcmp("idle", value) == 0)
return CRAS_BT_TRANSPORT_STATE_IDLE;
else if (strcmp("pending", value) == 0)
return CRAS_BT_TRANSPORT_STATE_PENDING;
else if (strcmp("active", value) == 0)
return CRAS_BT_TRANSPORT_STATE_ACTIVE;
else
return CRAS_BT_TRANSPORT_STATE_IDLE;
}
static void cras_bt_transport_state_changed(struct cras_bt_transport *transport)
{
if (transport->endpoint && transport->endpoint->transport_state_changed)
transport->endpoint->transport_state_changed(
transport->endpoint, transport);
}
/* Updates bt_device when certain transport property has changed. */
static void cras_bt_transport_update_device(struct cras_bt_transport *transport)
{
if (!transport->device)
return;
/* When the transport has non-negaive volume, it means the remote
* BT audio devices supports AVRCP absolute volume. Set the flag in bt
* device to use hardware volume. Also map the volume value from 0-127
* to 0-100.
*/
if (transport->volume != -1) {
cras_bt_device_set_use_hardware_volume(transport->device, 1);
cras_bt_device_update_hardware_volume(
transport->device, transport->volume * 100 / 127);
}
}
void cras_bt_transport_update_properties(struct cras_bt_transport *transport,
DBusMessageIter *properties_array_iter,
DBusMessageIter *invalidated_array_iter)
{
while (dbus_message_iter_get_arg_type(properties_array_iter) !=
DBUS_TYPE_INVALID) {
DBusMessageIter properties_dict_iter, variant_iter;
const char *key;
int type;
dbus_message_iter_recurse(properties_array_iter,
&properties_dict_iter);
dbus_message_iter_get_basic(&properties_dict_iter, &key);
dbus_message_iter_next(&properties_dict_iter);
dbus_message_iter_recurse(&properties_dict_iter, &variant_iter);
type = dbus_message_iter_get_arg_type(&variant_iter);
if (type == DBUS_TYPE_STRING) {
const char *value;
dbus_message_iter_get_basic(&variant_iter, &value);
if (strcmp(key, "UUID") == 0) {
transport->profile =
cras_bt_device_profile_from_uuid(value);
} else if (strcmp(key, "State") == 0) {
enum cras_bt_transport_state old_state =
transport->state;
transport->state =
cras_bt_transport_state_from_string(
value);
if (transport->state != old_state)
cras_bt_transport_state_changed(
transport);
}
} else if (type == DBUS_TYPE_BYTE) {
int value;
dbus_message_iter_get_basic(&variant_iter, &value);
if (strcmp(key, "Codec") == 0)
transport->codec = value;
} else if (type == DBUS_TYPE_OBJECT_PATH) {
const char *obj_path;
if (strcmp(key, "Device") == 0) {
/* Property: object Device [readonly] */
dbus_message_iter_get_basic(&variant_iter,
&obj_path);
transport->device =
cras_bt_device_get(obj_path);
if (!transport->device) {
syslog(LOG_ERR,
"Device %s not found at update "
"transport properties",
obj_path);
transport->device =
cras_bt_device_create(
transport->conn,
obj_path);
cras_bt_transport_update_device(
transport);
}
}
} else if (strcmp(dbus_message_iter_get_signature(&variant_iter),
"ay") == 0 &&
strcmp(key, "Configuration") == 0) {
DBusMessageIter value_iter;
char *value;
int len;
dbus_message_iter_recurse(&variant_iter, &value_iter);
dbus_message_iter_get_fixed_array(&value_iter, &value,
&len);
free(transport->configuration);
transport->configuration_len = 0;
transport->configuration = malloc(len);
if (transport->configuration) {
memcpy(transport->configuration, value, len);
transport->configuration_len = len;
}
} else if (strcmp(key, "Volume") == 0) {
uint16_t volume;
dbus_message_iter_get_basic(&variant_iter, &volume);
transport->volume = volume;
BTLOG(btlog, BT_TRANSPORT_UPDATE_VOLUME, volume, 0);
cras_bt_transport_update_device(transport);
}
dbus_message_iter_next(properties_array_iter);
}
while (invalidated_array_iter &&
dbus_message_iter_get_arg_type(invalidated_array_iter) !=
DBUS_TYPE_INVALID) {
const char *key;
dbus_message_iter_get_basic(invalidated_array_iter, &key);
if (strcmp(key, "Device") == 0) {
transport->device = NULL;
} else if (strcmp(key, "UUID") == 0) {
transport->profile = 0;
} else if (strcmp(key, "State") == 0) {
transport->state = CRAS_BT_TRANSPORT_STATE_IDLE;
} else if (strcmp(key, "Codec") == 0) {
transport->codec = 0;
} else if (strcmp(key, "Configuration") == 0) {
free(transport->configuration);
transport->configuration = NULL;
transport->configuration_len = 0;
}
dbus_message_iter_next(invalidated_array_iter);
}
}
static void on_transport_volume_set(DBusPendingCall *pending_call, void *data)
{
DBusMessage *reply;
reply = dbus_pending_call_steal_reply(pending_call);
dbus_pending_call_unref(pending_call);
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR)
syslog(LOG_ERR, "Set absolute volume returned error: %s",
dbus_message_get_error_name(reply));
dbus_message_unref(reply);
}
int cras_bt_transport_set_volume(struct cras_bt_transport *transport,
uint16_t volume)
{
const char *key = "Volume";
const char *interface = BLUEZ_INTERFACE_MEDIA_TRANSPORT;
DBusMessage *method_call;
DBusMessageIter message_iter, variant;
DBusPendingCall *pending_call;
BTLOG(btlog, BT_TRANSPORT_SET_VOLUME, volume, 0);
method_call =
dbus_message_new_method_call(BLUEZ_SERVICE,
transport->object_path,
DBUS_INTERFACE_PROPERTIES, "Set");
if (!method_call)
return -ENOMEM;
dbus_message_iter_init_append(method_call, &message_iter);
dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_STRING,
&interface);
dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_STRING, &key);
dbus_message_iter_open_container(&message_iter, DBUS_TYPE_VARIANT,
DBUS_TYPE_UINT16_AS_STRING, &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT16, &volume);
dbus_message_iter_close_container(&message_iter, &variant);
if (!dbus_connection_send_with_reply(transport->conn, method_call,
&pending_call,
DBUS_TIMEOUT_USE_DEFAULT)) {
dbus_message_unref(method_call);
return -ENOMEM;
}
dbus_message_unref(method_call);
if (!pending_call)
return -EIO;
if (!dbus_pending_call_set_notify(pending_call, on_transport_volume_set,
NULL, NULL)) {
dbus_pending_call_cancel(pending_call);
dbus_pending_call_unref(pending_call);
return -ENOMEM;
}
return 0;
}
int cras_bt_transport_acquire(struct cras_bt_transport *transport)
{
DBusMessage *method_call, *reply;
DBusError dbus_error;
int rc = 0;
if (transport->fd >= 0)
return 0;
method_call = dbus_message_new_method_call(
BLUEZ_SERVICE, transport->object_path,
BLUEZ_INTERFACE_MEDIA_TRANSPORT, "Acquire");
if (!method_call)
return -ENOMEM;
dbus_error_init(&dbus_error);
reply = dbus_connection_send_with_reply_and_block(
transport->conn, method_call, DBUS_TIMEOUT_USE_DEFAULT,
&dbus_error);
if (!reply) {
syslog(LOG_ERR, "Failed to acquire transport %s: %s",
transport->object_path, dbus_error.message);
dbus_error_free(&dbus_error);
dbus_message_unref(method_call);
rc = -EIO;
goto acquire_fail;
}
dbus_message_unref(method_call);
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
syslog(LOG_ERR, "Acquire returned error: %s",
dbus_message_get_error_name(reply));
dbus_message_unref(reply);
rc = -EIO;
goto acquire_fail;
}
if (!dbus_message_get_args(
reply, &dbus_error, DBUS_TYPE_UNIX_FD, &(transport->fd),
DBUS_TYPE_UINT16, &(transport->read_mtu), DBUS_TYPE_UINT16,
&(transport->write_mtu), DBUS_TYPE_INVALID)) {
syslog(LOG_ERR, "Bad Acquire reply received: %s",
dbus_error.message);
dbus_error_free(&dbus_error);
dbus_message_unref(reply);
rc = -EINVAL;
goto acquire_fail;
}
if (cras_system_get_bt_fix_a2dp_packet_size_enabled() &&
transport->write_mtu > A2DP_FIX_PACKET_SIZE)
transport->write_mtu = A2DP_FIX_PACKET_SIZE;
BTLOG(btlog, BT_TRANSPORT_ACQUIRE, 1, transport->fd);
dbus_message_unref(reply);
return 0;
acquire_fail:
BTLOG(btlog, BT_TRANSPORT_ACQUIRE, 0, 0);
return rc;
}
int cras_bt_transport_try_acquire(struct cras_bt_transport *transport)
{
DBusMessage *method_call, *reply;
DBusError dbus_error;
int fd, read_mtu, write_mtu;
method_call = dbus_message_new_method_call(
BLUEZ_SERVICE, transport->object_path,
BLUEZ_INTERFACE_MEDIA_TRANSPORT, "TryAcquire");
if (!method_call)
return -ENOMEM;
dbus_error_init(&dbus_error);
reply = dbus_connection_send_with_reply_and_block(
transport->conn, method_call, DBUS_TIMEOUT_USE_DEFAULT,
&dbus_error);
if (!reply) {
syslog(LOG_ERR, "Failed to try acquire transport %s: %s",
transport->object_path, dbus_error.message);
dbus_error_free(&dbus_error);
dbus_message_unref(method_call);
return -EIO;
}
dbus_message_unref(method_call);
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
syslog(LOG_ERR, "TryAcquire returned error: %s",
dbus_message_get_error_name(reply));
dbus_message_unref(reply);
return -EIO;
}
if (!dbus_message_get_args(reply, &dbus_error, DBUS_TYPE_UNIX_FD, &fd,
DBUS_TYPE_UINT16, &read_mtu,
DBUS_TYPE_UINT16, &write_mtu,
DBUS_TYPE_INVALID)) {
syslog(LOG_ERR, "Bad TryAcquire reply received: %s",
dbus_error.message);
dbus_error_free(&dbus_error);
dbus_message_unref(reply);
return -EINVAL;
}
/* Done TryAcquired the transport so it won't be released in bluez,
* no need for the new file descriptor so close it. */
if (transport->fd != fd)
close(fd);
dbus_message_unref(reply);
return 0;
}
/* Callback to trigger when transport release completed. */
static void cras_bt_on_transport_release(DBusPendingCall *pending_call,
void *data)
{
DBusMessage *reply;
reply = dbus_pending_call_steal_reply(pending_call);
dbus_pending_call_unref(pending_call);
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
syslog(LOG_WARNING, "Release transport returned error: %s",
dbus_message_get_error_name(reply));
dbus_message_unref(reply);
return;
}
dbus_message_unref(reply);
}
int cras_bt_transport_release(struct cras_bt_transport *transport,
unsigned int blocking)
{
DBusMessage *method_call, *reply;
DBusPendingCall *pending_call;
DBusError dbus_error;
if (transport->fd < 0)
return 0;
BTLOG(btlog, BT_TRANSPORT_RELEASE, transport->fd, 0);
/* Close the transport on our end no matter whether or not the server
* gives us an error.
*/
close(transport->fd);
transport->fd = -1;
method_call = dbus_message_new_method_call(
BLUEZ_SERVICE, transport->object_path,
BLUEZ_INTERFACE_MEDIA_TRANSPORT, "Release");
if (!method_call)
return -ENOMEM;
if (blocking) {
dbus_error_init(&dbus_error);
reply = dbus_connection_send_with_reply_and_block(
transport->conn, method_call, DBUS_TIMEOUT_USE_DEFAULT,
&dbus_error);
if (!reply) {
syslog(LOG_ERR, "Failed to release transport %s: %s",
transport->object_path, dbus_error.message);
dbus_error_free(&dbus_error);
dbus_message_unref(method_call);
return -EIO;
}
dbus_message_unref(method_call);
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
syslog(LOG_ERR, "Release returned error: %s",
dbus_message_get_error_name(reply));
dbus_message_unref(reply);
return -EIO;
}
dbus_message_unref(reply);
} else {
if (!dbus_connection_send_with_reply(
transport->conn, method_call, &pending_call,
DBUS_TIMEOUT_USE_DEFAULT)) {
dbus_message_unref(method_call);
return -ENOMEM;
}
dbus_message_unref(method_call);
if (!pending_call)
return -EIO;
if (!dbus_pending_call_set_notify(pending_call,
cras_bt_on_transport_release,
transport, NULL)) {
dbus_pending_call_cancel(pending_call);
dbus_pending_call_unref(pending_call);
return -ENOMEM;
}
}
return 0;
}