mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
Codec probe connections can trigger bad behavior from oFono if done when device is busy (e.g. at connect), and they might be done at the same time as A2DP transport is acquired which cannot work. Also, oFono will not reply to DBus Acquire, if device does not complete codec negotiation correctly. This is most likely to happen just after device connect, when it is busy with other stuff (eg A2DP). Remove codec probe connections altogether: instead, we guess mSBC if mSBC is enabled and otherwise CVSD. If the guess turns out to be wrong, which is unlikely (almost all devices have mSBC), we recreate the transport with correct codec (from main loop, must not be done in *_acquire because that can destroy nodes + unload the spa libs while we're being called from there). To avoid oFono DBus hangs at startup, add delay before marking the profile connected, enforcing a time difference to A2DP operations.
947 lines
27 KiB
C
947 lines
27 KiB
C
/* Spa oFono backend
|
||
*
|
||
* Copyright © 2020 Collabora Ltd.
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||
* copy of this software and associated documentation files (the "Software"),
|
||
* to deal in the Software without restriction, including without limitation
|
||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||
* and/or sell copies of the Software, and to permit persons to whom the
|
||
* Software is furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice (including the next
|
||
* paragraph) shall be included in all copies or substantial portions of the
|
||
* Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||
* DEALINGS IN THE SOFTWARE.
|
||
*/
|
||
|
||
#include <errno.h>
|
||
#include <unistd.h>
|
||
#include <poll.h>
|
||
#include <sys/socket.h>
|
||
|
||
#include <bluetooth/bluetooth.h>
|
||
#include <bluetooth/sco.h>
|
||
|
||
#include <dbus/dbus.h>
|
||
|
||
#include <spa/support/log.h>
|
||
#include <spa/support/loop.h>
|
||
#include <spa/support/dbus.h>
|
||
#include <spa/support/plugin.h>
|
||
#include <spa/utils/string.h>
|
||
#include <spa/utils/type.h>
|
||
#include <spa/utils/result.h>
|
||
#include <spa/param/audio/raw.h>
|
||
|
||
#include "defs.h"
|
||
|
||
#define INITIAL_INTERVAL_NSEC (500 * SPA_NSEC_PER_MSEC)
|
||
#define ACTION_INTERVAL_NSEC (3000 * SPA_NSEC_PER_MSEC)
|
||
|
||
static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.ofono");
|
||
#undef SPA_LOG_TOPIC_DEFAULT
|
||
#define SPA_LOG_TOPIC_DEFAULT &log_topic
|
||
|
||
struct impl {
|
||
struct spa_bt_backend this;
|
||
|
||
struct spa_bt_monitor *monitor;
|
||
|
||
struct spa_log *log;
|
||
struct spa_loop *main_loop;
|
||
struct spa_system *main_system;
|
||
struct spa_dbus *dbus;
|
||
struct spa_loop_utils *loop_utils;
|
||
DBusConnection *conn;
|
||
|
||
const struct spa_bt_quirks *quirks;
|
||
|
||
struct spa_source *timer;
|
||
|
||
unsigned int filters_added:1;
|
||
unsigned int msbc_supported:1;
|
||
};
|
||
|
||
struct transport_data {
|
||
struct spa_source sco;
|
||
unsigned int broken:1;
|
||
unsigned int activated:1;
|
||
};
|
||
|
||
#define OFONO_HF_AUDIO_MANAGER_INTERFACE OFONO_SERVICE ".HandsfreeAudioManager"
|
||
#define OFONO_HF_AUDIO_CARD_INTERFACE OFONO_SERVICE ".HandsfreeAudioCard"
|
||
#define OFONO_HF_AUDIO_AGENT_INTERFACE OFONO_SERVICE ".HandsfreeAudioAgent"
|
||
|
||
#define OFONO_AUDIO_CLIENT "/Profile/ofono"
|
||
|
||
#define OFONO_INTROSPECT_XML \
|
||
DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
|
||
"<node>" \
|
||
" <interface name=\"" OFONO_HF_AUDIO_AGENT_INTERFACE "\">" \
|
||
" <method name=\"Release\">" \
|
||
" </method>" \
|
||
" <method name=\"NewConnection\">" \
|
||
" <arg name=\"card\" direction=\"in\" type=\"o\"/>" \
|
||
" <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \
|
||
" <arg name=\"codec\" direction=\"in\" type=\"b\"/>" \
|
||
" </method>" \
|
||
" </interface>" \
|
||
" <interface name=\"org.freedesktop.DBus.Introspectable\">" \
|
||
" <method name=\"Introspect\">" \
|
||
" <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
|
||
" </method>" \
|
||
" </interface>" \
|
||
"</node>"
|
||
|
||
#define OFONO_ERROR_INVALID_ARGUMENTS "org.ofono.Error.InvalidArguments"
|
||
#define OFONO_ERROR_NOT_IMPLEMENTED "org.ofono.Error.NotImplemented"
|
||
#define OFONO_ERROR_IN_USE "org.ofono.Error.InUse"
|
||
#define OFONO_ERROR_FAILED "org.ofono.Error.Failed"
|
||
|
||
static void ofono_transport_get_mtu(struct impl *backend, struct spa_bt_transport *t)
|
||
{
|
||
struct sco_options sco_opt;
|
||
socklen_t len;
|
||
|
||
/* Fallback values */
|
||
t->read_mtu = 48;
|
||
t->write_mtu = 48;
|
||
|
||
len = sizeof(sco_opt);
|
||
memset(&sco_opt, 0, len);
|
||
|
||
if (getsockopt(t->fd, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0)
|
||
spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults");
|
||
else {
|
||
spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu);
|
||
t->read_mtu = sco_opt.mtu;
|
||
t->write_mtu = sco_opt.mtu;
|
||
}
|
||
}
|
||
|
||
static struct spa_bt_transport *_transport_create(struct impl *backend,
|
||
const char *path,
|
||
struct spa_bt_device *device,
|
||
enum spa_bt_profile profile,
|
||
int codec,
|
||
struct spa_callbacks *impl)
|
||
{
|
||
struct spa_bt_transport *t = NULL;
|
||
char *t_path = strdup(path);
|
||
|
||
t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct transport_data));
|
||
if (t == NULL) {
|
||
spa_log_warn(backend->log, "can't create transport: %m");
|
||
free(t_path);
|
||
goto finish;
|
||
}
|
||
spa_bt_transport_set_implementation(t, impl, t);
|
||
|
||
t->device = device;
|
||
spa_list_append(&t->device->transport_list, &t->device_link);
|
||
t->backend = &backend->this;
|
||
t->profile = profile;
|
||
t->codec = codec;
|
||
t->n_channels = 1;
|
||
t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
|
||
|
||
finish:
|
||
return t;
|
||
}
|
||
|
||
static int _audio_acquire(struct impl *backend, const char *path, uint8_t *codec)
|
||
{
|
||
DBusMessage *m, *r;
|
||
DBusError err;
|
||
int ret = 0;
|
||
|
||
m = dbus_message_new_method_call(OFONO_SERVICE, path,
|
||
OFONO_HF_AUDIO_CARD_INTERFACE,
|
||
"Acquire");
|
||
if (m == NULL)
|
||
return -ENOMEM;
|
||
|
||
dbus_error_init(&err);
|
||
|
||
/*
|
||
* XXX: We assume here oFono replies. It however can happen that the headset does
|
||
* XXX: not properly respond to the codec negotiation RFCOMM commands.
|
||
* XXX: oFono (1.34) fails to handle this condition, and will not send DBus reply
|
||
* XXX: in this case. The transport acquire API is synchronous, so we can't
|
||
* XXX: do better here right now.
|
||
*/
|
||
r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
|
||
dbus_message_unref(m);
|
||
m = NULL;
|
||
|
||
if (r == NULL) {
|
||
spa_log_error(backend->log, "Transport Acquire() failed for transport %s (%s)",
|
||
path, err.message);
|
||
dbus_error_free(&err);
|
||
return -EIO;
|
||
}
|
||
|
||
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
|
||
spa_log_error(backend->log, "Acquire returned error: %s", dbus_message_get_error_name(r));
|
||
ret = -EIO;
|
||
goto finish;
|
||
}
|
||
|
||
if (!dbus_message_get_args(r, &err,
|
||
DBUS_TYPE_UNIX_FD, &ret,
|
||
DBUS_TYPE_BYTE, codec,
|
||
DBUS_TYPE_INVALID)) {
|
||
spa_log_error(backend->log, "Failed to parse Acquire() reply: %s", err.message);
|
||
dbus_error_free(&err);
|
||
ret = -EIO;
|
||
goto finish;
|
||
}
|
||
|
||
finish:
|
||
dbus_message_unref(r);
|
||
return ret;
|
||
}
|
||
|
||
static int ofono_audio_acquire(void *data, bool optional)
|
||
{
|
||
struct spa_bt_transport *transport = data;
|
||
struct transport_data *td = transport->user_data;
|
||
struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
|
||
uint8_t codec;
|
||
int ret = 0;
|
||
|
||
if (transport->fd >= 0)
|
||
goto finish;
|
||
if (td->broken) {
|
||
ret = -EIO;
|
||
goto finish;
|
||
}
|
||
|
||
spa_bt_device_update_last_bluez_action_time(transport->device);
|
||
|
||
ret = _audio_acquire(backend, transport->path, &codec);
|
||
if (ret < 0)
|
||
goto finish;
|
||
|
||
transport->fd = ret;
|
||
|
||
if (transport->codec != codec) {
|
||
struct timespec ts;
|
||
|
||
spa_log_info(backend->log, "transport %p: acquired codec (%d) differs from transport one (%d)",
|
||
transport, codec, transport->codec);
|
||
|
||
/* shutdown to make sure connection is dropped immediately */
|
||
shutdown(transport->fd, SHUT_RDWR);
|
||
close(transport->fd);
|
||
transport->fd = -1;
|
||
|
||
/* schedule immediate profile update, from main loop */
|
||
transport->codec = codec;
|
||
td->broken = true;
|
||
ts.tv_sec = 0;
|
||
ts.tv_nsec = 1;
|
||
spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
|
||
&ts, NULL, false);
|
||
return -EIO;
|
||
}
|
||
|
||
td->broken = false;
|
||
|
||
spa_log_debug(backend->log, "transport %p: Acquire %s, fd %d codec %d", transport,
|
||
transport->path, transport->fd, transport->codec);
|
||
|
||
ofono_transport_get_mtu(backend, transport);
|
||
ret = 0;
|
||
|
||
finish:
|
||
return ret;
|
||
}
|
||
|
||
static int ofono_audio_release(void *data)
|
||
{
|
||
struct spa_bt_transport *transport = data;
|
||
struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
|
||
|
||
spa_log_debug(backend->log, "transport %p: Release %s",
|
||
transport, transport->path);
|
||
|
||
if (transport->sco_io) {
|
||
spa_bt_sco_io_destroy(transport->sco_io);
|
||
transport->sco_io = NULL;
|
||
}
|
||
|
||
/* shutdown to make sure connection is dropped immediately */
|
||
shutdown(transport->fd, SHUT_RDWR);
|
||
close(transport->fd);
|
||
transport->fd = -1;
|
||
|
||
return 0;
|
||
}
|
||
|
||
static DBusHandlerResult ofono_audio_card_removed(struct impl *backend, const char *path)
|
||
{
|
||
struct spa_bt_transport *transport;
|
||
|
||
spa_assert(backend);
|
||
spa_assert(path);
|
||
|
||
spa_log_debug(backend->log, "card removed: %s", path);
|
||
|
||
transport = spa_bt_transport_find(backend->monitor, path);
|
||
|
||
if (transport != NULL) {
|
||
struct spa_bt_device *device = transport->device;
|
||
|
||
spa_log_debug(backend->log, "transport %p: free %s",
|
||
transport, transport->path);
|
||
|
||
spa_bt_transport_free(transport);
|
||
if (device != NULL)
|
||
spa_bt_device_check_profiles(device, false);
|
||
}
|
||
|
||
return DBUS_HANDLER_RESULT_HANDLED;
|
||
}
|
||
|
||
static const struct spa_bt_transport_implementation ofono_transport_impl = {
|
||
SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
|
||
.acquire = ofono_audio_acquire,
|
||
.release = ofono_audio_release,
|
||
};
|
||
|
||
bool activate_transport(struct spa_bt_transport *t, const void *data)
|
||
{
|
||
struct impl *backend = (void *)data;
|
||
struct transport_data *td = t->user_data;
|
||
struct timespec ts;
|
||
uint64_t now, threshold;
|
||
|
||
if (t->backend != &backend->this)
|
||
return false;
|
||
|
||
/* Check device-specific rate limit */
|
||
spa_system_clock_gettime(backend->main_system, CLOCK_MONOTONIC, &ts);
|
||
now = SPA_TIMESPEC_TO_NSEC(&ts);
|
||
threshold = t->device->last_bluez_action_time + ACTION_INTERVAL_NSEC;
|
||
if (now < threshold) {
|
||
ts.tv_sec = (threshold - now) / SPA_NSEC_PER_SEC;
|
||
ts.tv_nsec = (threshold - now) % SPA_NSEC_PER_SEC;
|
||
spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
|
||
&ts, NULL, false);
|
||
return false;
|
||
}
|
||
|
||
if (!td->activated) {
|
||
/* Connect profile */
|
||
spa_log_debug(backend->log, "Transport %s activated", t->path);
|
||
td->activated = true;
|
||
spa_bt_device_connect_profile(t->device, t->profile);
|
||
}
|
||
|
||
if (td->broken) {
|
||
/* Recreate the transport */
|
||
struct spa_bt_transport *t_copy;
|
||
|
||
t_copy = _transport_create(backend, t->path, t->device,
|
||
t->profile, t->codec, (struct spa_callbacks *)&ofono_transport_impl);
|
||
spa_bt_transport_free(t);
|
||
|
||
if (t_copy)
|
||
spa_bt_device_connect_profile(t_copy->device, t_copy->profile);
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static void activate_transports(struct impl *backend)
|
||
{
|
||
while (spa_bt_transport_find_full(backend->monitor, activate_transport, backend));
|
||
}
|
||
|
||
static void activate_timer_event(void *userdata, uint64_t expirations)
|
||
{
|
||
struct impl *backend = userdata;
|
||
spa_loop_utils_update_timer(backend->loop_utils, backend->timer, NULL, NULL, false);
|
||
activate_transports(backend);
|
||
}
|
||
|
||
static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path, DBusMessageIter *props_i)
|
||
{
|
||
const char *remote_address = NULL;
|
||
const char *local_address = NULL;
|
||
struct spa_bt_device *d;
|
||
struct spa_bt_transport *t;
|
||
struct transport_data *td;
|
||
enum spa_bt_profile profile = SPA_BT_PROFILE_HFP_AG;
|
||
uint8_t codec = backend->msbc_supported ?
|
||
HFP_AUDIO_CODEC_MSBC : HFP_AUDIO_CODEC_CVSD;
|
||
|
||
spa_assert(backend);
|
||
spa_assert(path);
|
||
spa_assert(props_i);
|
||
|
||
spa_log_debug(backend->log, "new card: %s", path);
|
||
|
||
while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
|
||
DBusMessageIter i, value_i;
|
||
const char *key, *value;
|
||
char c;
|
||
|
||
dbus_message_iter_recurse(props_i, &i);
|
||
|
||
dbus_message_iter_get_basic(&i, &key);
|
||
dbus_message_iter_next(&i);
|
||
dbus_message_iter_recurse(&i, &value_i);
|
||
|
||
if ((c = dbus_message_iter_get_arg_type(&value_i)) != DBUS_TYPE_STRING) {
|
||
spa_log_error(backend->log, "Invalid properties for %s: expected 's', received '%c'", path, c);
|
||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||
}
|
||
|
||
dbus_message_iter_get_basic(&value_i, &value);
|
||
|
||
if (spa_streq(key, "RemoteAddress")) {
|
||
remote_address = value;
|
||
} else if (spa_streq(key, "LocalAddress")) {
|
||
local_address = value;
|
||
} else if (spa_streq(key, "Type")) {
|
||
if (spa_streq(value, "gateway"))
|
||
profile = SPA_BT_PROFILE_HFP_HF;
|
||
}
|
||
|
||
spa_log_debug(backend->log, "%s: %s", key, value);
|
||
|
||
dbus_message_iter_next(props_i);
|
||
}
|
||
|
||
if (!remote_address || !local_address) {
|
||
spa_log_error(backend->log, "Missing addresses for %s", path);
|
||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||
}
|
||
|
||
d = spa_bt_device_find_by_address(backend->monitor, remote_address, local_address);
|
||
if (!d || !d->adapter) {
|
||
spa_log_error(backend->log, "Device doesn’t exist for %s", path);
|
||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||
}
|
||
spa_bt_device_add_profile(d, profile);
|
||
|
||
t = _transport_create(backend, path, d, profile, codec, (struct spa_callbacks *)&ofono_transport_impl);
|
||
if (t == NULL) {
|
||
spa_log_error(backend->log, "failed to create transport: %s", spa_strerror(-errno));
|
||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||
}
|
||
|
||
td = t->user_data;
|
||
|
||
/*
|
||
* For HF profile, delay profile connect, so that we likely don't do it at the
|
||
* same time as the device is busy with A2DP connect. This avoids some oFono
|
||
* misbehavior (see comment in _audio_acquire above).
|
||
*
|
||
* For AG mode, we delay the emission of the nodes, so it is not necessary
|
||
* to know the codec in advance.
|
||
*/
|
||
if (profile == SPA_BT_PROFILE_HFP_HF) {
|
||
struct timespec ts;
|
||
ts.tv_sec = INITIAL_INTERVAL_NSEC / SPA_NSEC_PER_SEC;
|
||
ts.tv_nsec = INITIAL_INTERVAL_NSEC % SPA_NSEC_PER_SEC;
|
||
spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
|
||
&ts, NULL, false);
|
||
} else {
|
||
td->activated = true;
|
||
spa_bt_device_connect_profile(t->device, t->profile);
|
||
}
|
||
|
||
spa_log_debug(backend->log, "Transport %s available, codec %d", t->path, t->codec);
|
||
|
||
return DBUS_HANDLER_RESULT_HANDLED;
|
||
}
|
||
|
||
static DBusHandlerResult ofono_release(DBusConnection *conn, DBusMessage *m, void *userdata)
|
||
{
|
||
struct impl *backend = userdata;
|
||
DBusMessage *r;
|
||
|
||
spa_log_warn(backend->log, "release");
|
||
|
||
r = dbus_message_new_error(m, OFONO_HF_AUDIO_AGENT_INTERFACE ".Error.NotImplemented",
|
||
"Method not implemented");
|
||
if (r == NULL)
|
||
return DBUS_HANDLER_RESULT_NEED_MEMORY;
|
||
if (!dbus_connection_send(conn, r, NULL))
|
||
return DBUS_HANDLER_RESULT_NEED_MEMORY;
|
||
|
||
dbus_message_unref(r);
|
||
return DBUS_HANDLER_RESULT_HANDLED;
|
||
}
|
||
|
||
static void sco_event(struct spa_source *source)
|
||
{
|
||
struct spa_bt_transport *t = source->data;
|
||
struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
|
||
|
||
if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
|
||
spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno));
|
||
if (t->fd >= 0) {
|
||
if (source->loop)
|
||
spa_loop_remove_source(source->loop, source);
|
||
shutdown(t->fd, SHUT_RDWR);
|
||
close (t->fd);
|
||
t->fd = -1;
|
||
spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE);
|
||
}
|
||
}
|
||
}
|
||
|
||
static int enable_sco_socket(int sock)
|
||
{
|
||
char c;
|
||
struct pollfd pfd;
|
||
|
||
if (sock < 0)
|
||
return ENOTCONN;
|
||
|
||
memset(&pfd, 0, sizeof(pfd));
|
||
pfd.fd = sock;
|
||
pfd.events = POLLOUT;
|
||
|
||
if (poll(&pfd, 1, 0) < 0)
|
||
return errno;
|
||
|
||
/*
|
||
* If socket already writable then it is not in defer setup state,
|
||
* otherwise it needs to be read to authorize the connection.
|
||
*/
|
||
if ((pfd.revents & POLLOUT))
|
||
return 0;
|
||
|
||
/* Enable socket by reading 1 byte */
|
||
if (read(sock, &c, 1) < 0)
|
||
return errno;
|
||
|
||
return 0;
|
||
}
|
||
|
||
static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMessage *m, void *userdata)
|
||
{
|
||
struct impl *backend = userdata;
|
||
const char *path;
|
||
int fd;
|
||
uint8_t codec;
|
||
struct spa_bt_transport *t;
|
||
struct transport_data *td;
|
||
DBusMessage *r = NULL;
|
||
|
||
if (dbus_message_get_args(m, NULL,
|
||
DBUS_TYPE_OBJECT_PATH, &path,
|
||
DBUS_TYPE_UNIX_FD, &fd,
|
||
DBUS_TYPE_BYTE, &codec,
|
||
DBUS_TYPE_INVALID) == FALSE) {
|
||
r = dbus_message_new_error(m, OFONO_ERROR_INVALID_ARGUMENTS, "Invalid arguments in method call");
|
||
goto fail;
|
||
}
|
||
|
||
t = spa_bt_transport_find(backend->monitor, path);
|
||
if (t && (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) {
|
||
int err;
|
||
|
||
err = enable_sco_socket(fd);
|
||
if (err) {
|
||
spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(err));
|
||
r = dbus_message_new_error(m, OFONO_ERROR_FAILED, "SCO authorization failed");
|
||
shutdown(fd, SHUT_RDWR);
|
||
close(fd);
|
||
goto fail;
|
||
}
|
||
|
||
t->fd = fd;
|
||
t->codec = codec;
|
||
|
||
spa_log_debug(backend->log, "transport %p: NewConnection %s, fd %d codec %d",
|
||
t, t->path, t->fd, t->codec);
|
||
|
||
td = t->user_data;
|
||
td->sco.func = sco_event;
|
||
td->sco.data = t;
|
||
td->sco.fd = fd;
|
||
td->sco.mask = SPA_IO_HUP | SPA_IO_ERR;
|
||
td->sco.rmask = 0;
|
||
spa_loop_add_source(backend->main_loop, &td->sco);
|
||
|
||
ofono_transport_get_mtu(backend, t);
|
||
spa_bt_transport_set_state (t, SPA_BT_TRANSPORT_STATE_PENDING);
|
||
}
|
||
else if (fd) {
|
||
spa_log_debug(backend->log, "ignoring NewConnection");
|
||
r = dbus_message_new_error(m, OFONO_ERROR_NOT_IMPLEMENTED, "Method not implemented");
|
||
shutdown(fd, SHUT_RDWR);
|
||
close(fd);
|
||
}
|
||
|
||
fail:
|
||
if (r) {
|
||
DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED;
|
||
if (!dbus_connection_send(backend->conn, r, NULL))
|
||
res = DBUS_HANDLER_RESULT_NEED_MEMORY;
|
||
dbus_message_unref(r);
|
||
return res;
|
||
}
|
||
|
||
return DBUS_HANDLER_RESULT_HANDLED;
|
||
}
|
||
|
||
static DBusHandlerResult ofono_handler(DBusConnection *c, DBusMessage *m, void *userdata)
|
||
{
|
||
struct impl *backend = userdata;
|
||
const char *path, *interface, *member;
|
||
DBusMessage *r;
|
||
DBusHandlerResult res;
|
||
|
||
path = dbus_message_get_path(m);
|
||
interface = dbus_message_get_interface(m);
|
||
member = dbus_message_get_member(m);
|
||
|
||
spa_log_debug(backend->log, "path=%s, interface=%s, member=%s", path, interface, member);
|
||
|
||
if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
|
||
const char *xml = OFONO_INTROSPECT_XML;
|
||
|
||
if ((r = dbus_message_new_method_return(m)) == NULL)
|
||
return DBUS_HANDLER_RESULT_NEED_MEMORY;
|
||
if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
|
||
return DBUS_HANDLER_RESULT_NEED_MEMORY;
|
||
if (!dbus_connection_send(backend->conn, r, NULL))
|
||
return DBUS_HANDLER_RESULT_NEED_MEMORY;
|
||
|
||
dbus_message_unref(r);
|
||
res = DBUS_HANDLER_RESULT_HANDLED;
|
||
}
|
||
else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "Release"))
|
||
res = ofono_release(c, m, userdata);
|
||
else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "NewConnection"))
|
||
res = ofono_new_audio_connection(c, m, userdata);
|
||
else
|
||
res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||
|
||
return res;
|
||
}
|
||
|
||
static void ofono_getcards_reply(DBusPendingCall *pending, void *user_data)
|
||
{
|
||
struct impl *backend = user_data;
|
||
DBusMessage *r;
|
||
DBusMessageIter i, array_i, struct_i, props_i;
|
||
|
||
r = dbus_pending_call_steal_reply(pending);
|
||
if (r == NULL)
|
||
return;
|
||
|
||
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
|
||
spa_log_error(backend->log, "Failed to get a list of handsfree audio cards: %s",
|
||
dbus_message_get_error_name(r));
|
||
goto finish;
|
||
}
|
||
|
||
if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a(oa{sv})")) {
|
||
spa_log_error(backend->log, "Invalid arguments in GetCards() reply");
|
||
goto finish;
|
||
}
|
||
|
||
dbus_message_iter_recurse(&i, &array_i);
|
||
while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
|
||
char *path;
|
||
|
||
dbus_message_iter_recurse(&array_i, &struct_i);
|
||
dbus_message_iter_get_basic(&struct_i, &path);
|
||
dbus_message_iter_next(&struct_i);
|
||
|
||
dbus_message_iter_recurse(&struct_i, &props_i);
|
||
|
||
ofono_audio_card_found(backend, path, &props_i);
|
||
|
||
dbus_message_iter_next(&array_i);
|
||
}
|
||
|
||
finish:
|
||
dbus_message_unref(r);
|
||
dbus_pending_call_unref(pending);
|
||
}
|
||
|
||
static int backend_ofono_register(void *data)
|
||
{
|
||
struct impl *backend = data;
|
||
|
||
DBusMessage *m, *r;
|
||
const char *path = OFONO_AUDIO_CLIENT;
|
||
uint8_t codecs[2];
|
||
const uint8_t *pcodecs = codecs;
|
||
int ncodecs = 0, res;
|
||
DBusPendingCall *call;
|
||
DBusError err;
|
||
|
||
spa_log_debug(backend->log, "Registering");
|
||
|
||
m = dbus_message_new_method_call(OFONO_SERVICE, "/",
|
||
OFONO_HF_AUDIO_MANAGER_INTERFACE, "Register");
|
||
if (m == NULL)
|
||
return -ENOMEM;
|
||
|
||
codecs[ncodecs++] = HFP_AUDIO_CODEC_CVSD;
|
||
if (backend->msbc_supported)
|
||
codecs[ncodecs++] = HFP_AUDIO_CODEC_MSBC;
|
||
|
||
dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path,
|
||
DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &pcodecs, ncodecs,
|
||
DBUS_TYPE_INVALID);
|
||
|
||
dbus_error_init(&err);
|
||
|
||
r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
|
||
dbus_message_unref(m);
|
||
|
||
if (r == NULL) {
|
||
if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) {
|
||
spa_log_info(backend->log, "oFono not available: %s",
|
||
err.message);
|
||
res = -ENOTSUP;
|
||
} else {
|
||
spa_log_warn(backend->log, "Registering Profile %s failed: %s (%s)",
|
||
path, err.message, err.name);
|
||
res = -EIO;
|
||
}
|
||
dbus_error_free(&err);
|
||
return res;
|
||
}
|
||
|
||
if (dbus_message_is_error(r, OFONO_ERROR_INVALID_ARGUMENTS)) {
|
||
spa_log_warn(backend->log, "invalid arguments");
|
||
goto finish;
|
||
}
|
||
if (dbus_message_is_error(r, OFONO_ERROR_IN_USE)) {
|
||
spa_log_warn(backend->log, "already in use");
|
||
goto finish;
|
||
}
|
||
if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
|
||
spa_log_warn(backend->log, "Error registering profile");
|
||
goto finish;
|
||
}
|
||
if (dbus_message_is_error(r, DBUS_ERROR_SERVICE_UNKNOWN)) {
|
||
spa_log_info(backend->log, "oFono not available, disabling");
|
||
goto finish;
|
||
}
|
||
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
|
||
spa_log_error(backend->log, "Register() failed: %s",
|
||
dbus_message_get_error_name(r));
|
||
goto finish;
|
||
}
|
||
dbus_message_unref(r);
|
||
|
||
spa_log_debug(backend->log, "registered");
|
||
|
||
m = dbus_message_new_method_call(OFONO_SERVICE, "/",
|
||
OFONO_HF_AUDIO_MANAGER_INTERFACE, "GetCards");
|
||
if (m == NULL)
|
||
goto finish;
|
||
|
||
dbus_connection_send_with_reply(backend->conn, m, &call, -1);
|
||
dbus_pending_call_set_notify(call, ofono_getcards_reply, backend, NULL);
|
||
dbus_message_unref(m);
|
||
|
||
return 0;
|
||
|
||
finish:
|
||
dbus_message_unref(r);
|
||
return -EIO;
|
||
}
|
||
|
||
static DBusHandlerResult ofono_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
|
||
{
|
||
struct impl *backend = user_data;
|
||
DBusError err;
|
||
|
||
dbus_error_init(&err);
|
||
|
||
if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardAdded")) {
|
||
char *p;
|
||
DBusMessageIter arg_i, props_i;
|
||
|
||
if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sv}")) {
|
||
spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardAdded");
|
||
goto fail;
|
||
}
|
||
|
||
dbus_message_iter_get_basic(&arg_i, &p);
|
||
|
||
dbus_message_iter_next(&arg_i);
|
||
spa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY);
|
||
|
||
dbus_message_iter_recurse(&arg_i, &props_i);
|
||
|
||
return ofono_audio_card_found(backend, p, &props_i);
|
||
} else if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardRemoved")) {
|
||
const char *p;
|
||
|
||
if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) {
|
||
spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardRemoved: %s", err.message);
|
||
goto fail;
|
||
}
|
||
|
||
return ofono_audio_card_removed(backend, p);
|
||
}
|
||
|
||
fail:
|
||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||
}
|
||
|
||
static int add_filters(struct impl *backend)
|
||
{
|
||
DBusError err;
|
||
|
||
if (backend->filters_added)
|
||
return 0;
|
||
|
||
dbus_error_init(&err);
|
||
|
||
if (!dbus_connection_add_filter(backend->conn, ofono_filter_cb, backend, NULL)) {
|
||
spa_log_error(backend->log, "failed to add filter function");
|
||
goto fail;
|
||
}
|
||
|
||
dbus_bus_add_match(backend->conn,
|
||
"type='signal',sender='" OFONO_SERVICE "',"
|
||
"interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'", &err);
|
||
dbus_bus_add_match(backend->conn,
|
||
"type='signal',sender='" OFONO_SERVICE "',"
|
||
"interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'", &err);
|
||
|
||
backend->filters_added = true;
|
||
|
||
return 0;
|
||
|
||
fail:
|
||
dbus_error_free(&err);
|
||
return -EIO;
|
||
}
|
||
|
||
static int backend_ofono_free(void *data)
|
||
{
|
||
struct impl *backend = data;
|
||
|
||
if (backend->filters_added) {
|
||
dbus_connection_remove_filter(backend->conn, ofono_filter_cb, backend);
|
||
backend->filters_added = false;
|
||
}
|
||
|
||
if (backend->timer)
|
||
spa_loop_utils_destroy_source(backend->loop_utils, backend->timer);
|
||
|
||
dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT);
|
||
|
||
free(backend);
|
||
|
||
return 0;
|
||
}
|
||
|
||
static const struct spa_bt_backend_implementation backend_impl = {
|
||
SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
|
||
.free = backend_ofono_free,
|
||
.register_profiles = backend_ofono_register,
|
||
};
|
||
|
||
static bool is_available(struct impl *backend)
|
||
{
|
||
DBusMessage *m, *r;
|
||
DBusError err;
|
||
bool success = false;
|
||
|
||
m = dbus_message_new_method_call(OFONO_SERVICE, "/",
|
||
DBUS_INTERFACE_INTROSPECTABLE, "Introspect");
|
||
if (m == NULL)
|
||
return false;
|
||
|
||
dbus_error_init(&err);
|
||
r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
|
||
dbus_message_unref(m);
|
||
|
||
if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN)
|
||
success = true;
|
||
|
||
if (r)
|
||
dbus_message_unref(r);
|
||
else
|
||
dbus_error_free(&err);
|
||
|
||
return success;
|
||
}
|
||
|
||
struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor,
|
||
void *dbus_connection,
|
||
const struct spa_dict *info,
|
||
const struct spa_bt_quirks *quirks,
|
||
const struct spa_support *support,
|
||
uint32_t n_support)
|
||
{
|
||
struct impl *backend;
|
||
const char *str;
|
||
static const DBusObjectPathVTable vtable_profile = {
|
||
.message_function = ofono_handler,
|
||
};
|
||
|
||
backend = calloc(1, sizeof(struct impl));
|
||
if (backend == NULL)
|
||
return NULL;
|
||
|
||
spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
|
||
|
||
backend->this.name = "ofono";
|
||
backend->this.exclusive = true;
|
||
backend->monitor = monitor;
|
||
backend->quirks = quirks;
|
||
backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
|
||
backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
|
||
backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
|
||
backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
|
||
backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
|
||
backend->conn = dbus_connection;
|
||
if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc")))
|
||
backend->msbc_supported = spa_atob(str);
|
||
else
|
||
backend->msbc_supported = false;
|
||
|
||
spa_log_topic_init(backend->log, &log_topic);
|
||
|
||
backend->timer = spa_loop_utils_add_timer(backend->loop_utils, activate_timer_event, backend);
|
||
if (backend->timer == NULL) {
|
||
free(backend);
|
||
return NULL;
|
||
}
|
||
|
||
if (!dbus_connection_register_object_path(backend->conn,
|
||
OFONO_AUDIO_CLIENT,
|
||
&vtable_profile, backend)) {
|
||
free(backend);
|
||
return NULL;
|
||
}
|
||
|
||
if (add_filters(backend) < 0) {
|
||
dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT);
|
||
free(backend);
|
||
return NULL;
|
||
}
|
||
|
||
backend->this.available = is_available(backend);
|
||
|
||
return &backend->this;
|
||
}
|