2012-07-04 16:48:27 +02:00
|
|
|
/***
|
|
|
|
|
This file is part of PulseAudio.
|
|
|
|
|
|
|
|
|
|
Copyright 2006 Lennart Poettering
|
|
|
|
|
Copyright 2009 Canonical Ltd
|
|
|
|
|
Copyright (C) 2012 Intel Corporation
|
|
|
|
|
|
|
|
|
|
PulseAudio is free software; you can redistribute it and/or modify
|
|
|
|
|
it under the terms of the GNU Lesser General Public License as published
|
|
|
|
|
by the Free Software Foundation; either version 2.1 of the License,
|
|
|
|
|
or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
PulseAudio is distributed in the hope that it will be useful, but
|
|
|
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
|
General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Lesser General Public License
|
2014-11-26 14:14:51 +01:00
|
|
|
along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
|
2012-07-04 16:48:27 +02:00
|
|
|
***/
|
|
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
|
#include <config.h>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include <pulse/xmalloc.h>
|
|
|
|
|
|
|
|
|
|
#include <pulsecore/core.h>
|
2012-08-22 09:04:21 +02:00
|
|
|
#include <pulsecore/modargs.h>
|
2012-07-04 16:48:27 +02:00
|
|
|
#include <pulsecore/source-output.h>
|
|
|
|
|
#include <pulsecore/source.h>
|
|
|
|
|
#include <pulsecore/core-util.h>
|
|
|
|
|
|
2016-07-02 22:26:17 +02:00
|
|
|
PA_MODULE_AUTHOR("Frédéric Dalleau, Pali Rohár");
|
|
|
|
|
PA_MODULE_DESCRIPTION("Policy module to make using bluetooth devices out-of-the-box easier");
|
2012-07-04 16:48:27 +02:00
|
|
|
PA_MODULE_VERSION(PACKAGE_VERSION);
|
2013-01-11 22:11:04 +02:00
|
|
|
PA_MODULE_LOAD_ONCE(true);
|
2012-08-22 09:04:21 +02:00
|
|
|
PA_MODULE_USAGE(
|
2016-09-11 16:41:02 +02:00
|
|
|
"auto_switch=<Switch between hsp and a2dp profile? (0 - never, 1 - media.role=phone, 2 - heuristic> "
|
2012-08-22 09:04:22 +02:00
|
|
|
"a2dp_source=<Handle a2dp_source card profile (sink role)?> "
|
2018-03-26 17:15:51 +03:00
|
|
|
"ag=<Handle headset_audio_gateway card profile (headset role)?> ");
|
2012-08-22 09:04:21 +02:00
|
|
|
|
|
|
|
|
static const char* const valid_modargs[] = {
|
2016-07-02 22:26:17 +02:00
|
|
|
"auto_switch",
|
2012-08-22 09:04:21 +02:00
|
|
|
"a2dp_source",
|
2014-08-22 11:07:13 +03:00
|
|
|
"ag",
|
2012-08-22 09:04:21 +02:00
|
|
|
NULL
|
|
|
|
|
};
|
2012-07-04 16:48:27 +02:00
|
|
|
|
|
|
|
|
struct userdata {
|
2016-09-11 16:41:02 +02:00
|
|
|
uint32_t auto_switch;
|
2012-08-22 09:04:21 +02:00
|
|
|
bool enable_a2dp_source;
|
2014-08-22 11:07:13 +03:00
|
|
|
bool enable_ag;
|
2012-08-22 09:04:21 +02:00
|
|
|
pa_hook_slot *source_put_slot;
|
2012-08-22 09:04:22 +02:00
|
|
|
pa_hook_slot *sink_put_slot;
|
2016-07-02 22:26:17 +02:00
|
|
|
pa_hook_slot *source_output_put_slot;
|
|
|
|
|
pa_hook_slot *source_output_unlink_slot;
|
|
|
|
|
pa_hook_slot *card_init_profile_slot;
|
|
|
|
|
pa_hook_slot *card_unlink_slot;
|
2013-02-18 09:10:34 +01:00
|
|
|
pa_hook_slot *profile_available_changed_slot;
|
2016-07-02 22:26:17 +02:00
|
|
|
pa_hashmap *will_need_revert_card_map;
|
2012-07-04 16:48:27 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* When a source is created, loopback it to default sink */
|
2012-08-22 09:04:20 +02:00
|
|
|
static pa_hook_result_t source_put_hook_callback(pa_core *c, pa_source *source, void *userdata) {
|
2012-08-22 09:04:21 +02:00
|
|
|
struct userdata *u = userdata;
|
2012-07-04 16:48:27 +02:00
|
|
|
const char *s;
|
|
|
|
|
const char *role;
|
|
|
|
|
char *args;
|
2017-08-26 11:21:15 +02:00
|
|
|
pa_module *m = NULL;
|
2012-07-04 16:48:27 +02:00
|
|
|
|
|
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(source);
|
|
|
|
|
|
|
|
|
|
/* Only consider bluetooth sinks and sources */
|
|
|
|
|
s = pa_proplist_gets(source->proplist, PA_PROP_DEVICE_BUS);
|
|
|
|
|
if (!s)
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
if (!pa_streq(s, "bluetooth"))
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
s = pa_proplist_gets(source->proplist, "bluetooth.protocol");
|
|
|
|
|
if (!s)
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
2014-08-22 11:07:13 +03:00
|
|
|
if (u->enable_a2dp_source && pa_streq(s, "a2dp_source"))
|
2012-07-04 16:48:27 +02:00
|
|
|
role = "music";
|
2018-03-26 17:15:51 +03:00
|
|
|
else if (u->enable_ag && pa_streq(s, "headset_audio_gateway"))
|
2012-08-22 09:04:22 +02:00
|
|
|
role = "phone";
|
2012-07-04 16:48:27 +02:00
|
|
|
else {
|
|
|
|
|
pa_log_debug("Profile %s cannot be selected for loopback", s);
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Load module-loopback */
|
2014-02-04 19:03:46 -03:00
|
|
|
args = pa_sprintf_malloc("source=\"%s\" source_dont_move=\"true\" sink_input_properties=\"media.role=%s\"", source->name,
|
|
|
|
|
role);
|
2017-08-26 11:21:15 +02:00
|
|
|
(void) pa_module_load(&m, c, "module-loopback", args);
|
2012-07-04 16:48:27 +02:00
|
|
|
pa_xfree(args);
|
|
|
|
|
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2012-08-22 09:04:22 +02:00
|
|
|
/* When a sink is created, loopback it to default source */
|
|
|
|
|
static pa_hook_result_t sink_put_hook_callback(pa_core *c, pa_sink *sink, void *userdata) {
|
|
|
|
|
struct userdata *u = userdata;
|
|
|
|
|
const char *s;
|
|
|
|
|
const char *role;
|
|
|
|
|
char *args;
|
2017-08-26 11:21:15 +02:00
|
|
|
pa_module *m = NULL;
|
2012-08-22 09:04:22 +02:00
|
|
|
|
|
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(sink);
|
|
|
|
|
|
|
|
|
|
/* Only consider bluetooth sinks and sources */
|
|
|
|
|
s = pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_BUS);
|
|
|
|
|
if (!s)
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
if (!pa_streq(s, "bluetooth"))
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
s = pa_proplist_gets(sink->proplist, "bluetooth.protocol");
|
|
|
|
|
if (!s)
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
2018-03-26 17:15:51 +03:00
|
|
|
if (u->enable_ag && pa_streq(s, "headset_audio_gateway"))
|
2012-08-22 09:04:22 +02:00
|
|
|
role = "phone";
|
|
|
|
|
else {
|
|
|
|
|
pa_log_debug("Profile %s cannot be selected for loopback", s);
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Load module-loopback */
|
2014-02-04 19:03:46 -03:00
|
|
|
args = pa_sprintf_malloc("sink=\"%s\" sink_dont_move=\"true\" source_output_properties=\"media.role=%s\"", sink->name,
|
|
|
|
|
role);
|
2017-08-26 11:21:15 +02:00
|
|
|
(void) pa_module_load(&m, c, "module-loopback", args);
|
2012-08-22 09:04:22 +02:00
|
|
|
pa_xfree(args);
|
|
|
|
|
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-11 17:16:38 +02:00
|
|
|
static void card_set_profile(struct userdata *u, pa_card *card, bool revert_to_a2dp)
|
2016-07-02 22:26:17 +02:00
|
|
|
{
|
|
|
|
|
pa_card_profile *profile;
|
|
|
|
|
void *state;
|
|
|
|
|
|
2016-09-11 17:16:38 +02:00
|
|
|
/* Find available profile and activate it */
|
2016-07-02 22:26:17 +02:00
|
|
|
PA_HASHMAP_FOREACH(profile, card->profiles, state) {
|
|
|
|
|
if (profile->available == PA_AVAILABLE_NO)
|
|
|
|
|
continue;
|
|
|
|
|
|
2016-09-11 17:16:38 +02:00
|
|
|
/* Check for correct profile based on revert_to_a2dp */
|
|
|
|
|
if (revert_to_a2dp) {
|
2019-04-06 11:15:56 +02:00
|
|
|
if (!pa_streq(profile->name, "a2dp_sink"))
|
2016-09-11 17:16:38 +02:00
|
|
|
continue;
|
|
|
|
|
} else {
|
2016-08-18 08:48:48 -07:00
|
|
|
if (!pa_streq(profile->name, "headset_head_unit") && !pa_streq(profile->name, "headset_handsfree"))
|
2016-09-11 17:16:38 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pa_log_debug("Setting card '%s' to profile '%s'", card->name, profile->name);
|
2016-07-02 22:26:17 +02:00
|
|
|
|
|
|
|
|
if (pa_card_set_profile(card, profile, false) != 0) {
|
2016-09-11 17:16:38 +02:00
|
|
|
pa_log_warn("Could not set profile '%s'", profile->name);
|
2016-07-02 22:26:17 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* When we are not in revert_to_a2dp phase flag this card for will_need_revert */
|
|
|
|
|
if (!revert_to_a2dp)
|
|
|
|
|
pa_hashmap_put(u->will_need_revert_card_map, card, PA_INT_TO_PTR(1));
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Switch profile for one card */
|
|
|
|
|
static void switch_profile(pa_card *card, bool revert_to_a2dp, void *userdata) {
|
|
|
|
|
struct userdata *u = userdata;
|
|
|
|
|
const char *s;
|
|
|
|
|
|
|
|
|
|
/* Only consider bluetooth cards */
|
|
|
|
|
s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS);
|
|
|
|
|
if (!s || !pa_streq(s, "bluetooth"))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (revert_to_a2dp) {
|
|
|
|
|
/* In revert_to_a2dp phase only consider cards with will_need_revert flag and remove it */
|
|
|
|
|
if (!pa_hashmap_remove(u->will_need_revert_card_map, card))
|
|
|
|
|
return;
|
|
|
|
|
|
2016-09-11 17:16:38 +02:00
|
|
|
/* Skip card if does not have active hsp profile */
|
2016-08-18 08:48:48 -07:00
|
|
|
if (!pa_streq(card->active_profile->name, "headset_head_unit") && !pa_streq(card->active_profile->name, "headset_handsfree"))
|
2016-09-11 17:16:38 +02:00
|
|
|
return;
|
2016-07-02 22:26:17 +02:00
|
|
|
|
2016-09-11 17:16:38 +02:00
|
|
|
/* Skip card if already has active a2dp profile */
|
2019-04-06 11:15:56 +02:00
|
|
|
if (pa_streq(card->active_profile->name, "a2dp_sink"))
|
2016-09-11 17:16:38 +02:00
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
/* Skip card if does not have active a2dp profile */
|
2019-04-06 11:15:56 +02:00
|
|
|
if (!pa_streq(card->active_profile->name, "a2dp_sink"))
|
2016-09-11 17:16:38 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
/* Skip card if already has active hsp profile */
|
2016-08-18 08:48:48 -07:00
|
|
|
if (pa_streq(card->active_profile->name, "headset_head_unit") || pa_streq(card->active_profile->name, "headset_handsfree"))
|
2016-09-11 17:16:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2016-07-02 22:26:17 +02:00
|
|
|
|
2016-09-11 17:16:38 +02:00
|
|
|
card_set_profile(u, card, revert_to_a2dp);
|
2016-07-02 22:26:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Return true if we should ignore this source output */
|
2016-09-11 16:41:02 +02:00
|
|
|
static bool ignore_output(pa_source_output *source_output, void *userdata) {
|
|
|
|
|
struct userdata *u = userdata;
|
2016-07-02 22:26:17 +02:00
|
|
|
const char *s;
|
|
|
|
|
|
|
|
|
|
/* New applications could set media.role for identifying streams */
|
|
|
|
|
/* We are interested only in media.role=phone */
|
|
|
|
|
s = pa_proplist_gets(source_output->proplist, PA_PROP_MEDIA_ROLE);
|
|
|
|
|
if (s)
|
|
|
|
|
return !pa_streq(s, "phone");
|
|
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
/* If media.role is not set use some heuristic (if enabled) */
|
|
|
|
|
if (u->auto_switch != 2)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
/* Ignore if resample method is peaks (used by desktop volume programs) */
|
|
|
|
|
if (pa_source_output_get_resample_method(source_output) == PA_RESAMPLER_PEAKS)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
/* Ignore if there is no client/application assigned (used by virtual stream) */
|
|
|
|
|
if (!source_output->client)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
/* Ignore if recording from monitor of sink */
|
|
|
|
|
if (source_output->direct_on_input)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
return false;
|
2016-07-02 22:26:17 +02:00
|
|
|
}
|
|
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
static unsigned source_output_count(pa_core *c, void *userdata) {
|
2016-07-02 22:26:17 +02:00
|
|
|
pa_source_output *source_output;
|
|
|
|
|
uint32_t idx;
|
|
|
|
|
unsigned count = 0;
|
|
|
|
|
|
|
|
|
|
PA_IDXSET_FOREACH(source_output, c->source_outputs, idx)
|
2016-09-11 16:41:02 +02:00
|
|
|
if (!ignore_output(source_output, userdata))
|
2016-07-02 22:26:17 +02:00
|
|
|
++count;
|
|
|
|
|
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Switch profile for all cards */
|
|
|
|
|
static void switch_profile_all(pa_idxset *cards, bool revert_to_a2dp, void *userdata) {
|
|
|
|
|
pa_card *card;
|
|
|
|
|
uint32_t idx;
|
|
|
|
|
|
|
|
|
|
PA_IDXSET_FOREACH(card, cards, idx)
|
|
|
|
|
switch_profile(card, revert_to_a2dp, userdata);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* When a source output is created, switch profile a2dp to profile hsp */
|
|
|
|
|
static pa_hook_result_t source_output_put_hook_callback(pa_core *c, pa_source_output *source_output, void *userdata) {
|
|
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(source_output);
|
|
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
if (ignore_output(source_output, userdata))
|
2016-07-02 22:26:17 +02:00
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
switch_profile_all(c->cards, false, userdata);
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* When all source outputs are unlinked, switch profile hsp back back to profile a2dp */
|
|
|
|
|
static pa_hook_result_t source_output_unlink_hook_callback(pa_core *c, pa_source_output *source_output, void *userdata) {
|
|
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(source_output);
|
|
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
if (ignore_output(source_output, userdata))
|
2016-07-02 22:26:17 +02:00
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
bluetooth-policy: do A2DP profile restoring a bit later
This fixes a crash that happens if the bluetooth headset is the only
non-monitor source in the system and the last "phone" stream dies.
When the stream dies, the native protocol calls pa_source_output_unlink()
and would call pa_source_output_unref() next, but without this patch,
things happen during the unlinking, and the unreffing ends up being
performed on a stream that is already freed.
pa_source_output_unlink() fires the "unlink" hook before doing anything
else. module-bluetooth-policy then switches the headset profile from HSP
to A2DP within that hook. The HSP source gets removed, and at this point
the dying stream is still connected to it, and needs to be rescued.
Rescuing fails, because there are no other sources in the system, so the
stream gets killed. The native protocol has a kill callback, which again
calls pa_source_output_unlink() and pa_source_output_unref(). This is
the point where the native protocol drops its own reference to the
stream, but another unref call is waiting to be executed once we return
from the original unlink call.
I first tried to avoid the double unreffing by making it safe to do
unlinking recursively, but I found out that there's code that assumes
that once unlink() returns, unlinking has actually occurred (a
reasonable assumption), and at least with my implementation this was not
guaranteed. I now think that we must avoid situations where unlinking
happens recursively. It's just too hairy to deal with. This patch moves
the bluetooth profile switch to happen at a time when the dead stream
isn't any more connected to the source, so it doesn't have to be
rescued or killed.
BugLink: https://bugs.freedesktop.org/show_bug.cgi?id=97906
2016-10-12 17:20:39 +03:00
|
|
|
/* If there are still some source outputs do nothing. */
|
2016-09-11 16:41:02 +02:00
|
|
|
if (source_output_count(c, userdata) > 0)
|
2016-07-02 22:26:17 +02:00
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
switch_profile_all(c->cards, true, userdata);
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static pa_hook_result_t card_init_profile_hook_callback(pa_core *c, pa_card *card, void *userdata) {
|
|
|
|
|
struct userdata *u = userdata;
|
|
|
|
|
const char *s;
|
|
|
|
|
|
|
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(card);
|
|
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
if (source_output_count(c, userdata) == 0)
|
2016-07-02 22:26:17 +02:00
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
/* Only consider bluetooth cards */
|
|
|
|
|
s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS);
|
|
|
|
|
if (!s || !pa_streq(s, "bluetooth"))
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
/* Ignore card if has already set other initial profile than a2dp */
|
2016-09-11 17:16:38 +02:00
|
|
|
if (card->active_profile &&
|
|
|
|
|
!pa_streq(card->active_profile->name, "a2dp_sink"))
|
2016-07-02 22:26:17 +02:00
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
/* Set initial profile to hsp */
|
2016-09-11 17:16:38 +02:00
|
|
|
card_set_profile(u, card, false);
|
2016-07-02 22:26:17 +02:00
|
|
|
|
|
|
|
|
/* Flag this card for will_need_revert */
|
|
|
|
|
pa_hashmap_put(u->will_need_revert_card_map, card, PA_INT_TO_PTR(1));
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static pa_hook_result_t card_unlink_hook_callback(pa_core *c, pa_card *card, void *userdata) {
|
|
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(card);
|
|
|
|
|
switch_profile(card, true, userdata);
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
static pa_card_profile *find_best_profile(pa_card *card) {
|
2012-08-31 12:51:13 +02:00
|
|
|
void *state;
|
|
|
|
|
pa_card_profile *profile;
|
2013-02-18 09:10:34 +01:00
|
|
|
pa_card_profile *result = card->active_profile;
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
PA_HASHMAP_FOREACH(profile, card->profiles, state) {
|
2014-09-17 13:05:00 +03:00
|
|
|
if (profile->available == PA_AVAILABLE_NO)
|
2013-02-18 09:10:34 +01:00
|
|
|
continue;
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
if (result == NULL ||
|
|
|
|
|
(profile->available == PA_AVAILABLE_YES && result->available == PA_AVAILABLE_UNKNOWN) ||
|
2013-12-14 09:29:24 +02:00
|
|
|
(profile->available == result->available && profile->priority > result->priority))
|
2013-02-18 09:10:34 +01:00
|
|
|
result = profile;
|
2012-08-31 12:51:13 +02:00
|
|
|
}
|
2013-02-18 09:10:34 +01:00
|
|
|
|
2014-09-17 13:05:00 +03:00
|
|
|
return result;
|
2012-08-31 12:51:13 +02:00
|
|
|
}
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
static pa_hook_result_t profile_available_hook_callback(pa_core *c, pa_card_profile *profile, void *userdata) {
|
2012-08-31 12:51:13 +02:00
|
|
|
pa_card *card;
|
|
|
|
|
const char *s;
|
2013-01-11 22:11:04 +02:00
|
|
|
bool is_active_profile;
|
2013-02-18 09:10:34 +01:00
|
|
|
pa_card_profile *selected_profile;
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
pa_assert(c);
|
|
|
|
|
pa_assert(profile);
|
|
|
|
|
pa_assert_se((card = profile->card));
|
2012-08-31 12:51:13 +02:00
|
|
|
|
|
|
|
|
/* Only consider bluetooth cards */
|
|
|
|
|
s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS);
|
|
|
|
|
if (!s || !pa_streq(s, "bluetooth"))
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
2012-11-26 18:32:08 +01:00
|
|
|
/* Do not automatically switch profiles for headsets, just in case */
|
2016-08-18 08:48:48 -07:00
|
|
|
if (pa_streq(profile->name, "a2dp_sink") ||
|
|
|
|
|
pa_streq(profile->name, "headset_head_unit") ||
|
|
|
|
|
pa_streq(profile->name, "headset_handsfree"))
|
2012-11-26 18:32:08 +01:00
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
is_active_profile = card->active_profile == profile;
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
if (profile->available == PA_AVAILABLE_YES) {
|
|
|
|
|
if (is_active_profile)
|
|
|
|
|
return PA_HOOK_OK;
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
if (card->active_profile->available == PA_AVAILABLE_YES && card->active_profile->priority >= profile->priority)
|
|
|
|
|
return PA_HOOK_OK;
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
selected_profile = profile;
|
|
|
|
|
} else {
|
|
|
|
|
if (!is_active_profile)
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
|
|
|
|
|
pa_assert_se((selected_profile = find_best_profile(card)));
|
|
|
|
|
|
|
|
|
|
if (selected_profile == card->active_profile)
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pa_log_debug("Setting card '%s' to profile '%s'", card->name, selected_profile->name);
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-11-20 15:42:26 +02:00
|
|
|
if (pa_card_set_profile(card, selected_profile, false) != 0)
|
2013-02-18 09:10:34 +01:00
|
|
|
pa_log_warn("Could not set profile '%s'", selected_profile->name);
|
2012-08-31 12:51:13 +02:00
|
|
|
|
|
|
|
|
return PA_HOOK_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
static void handle_all_profiles(pa_core *core) {
|
2012-08-31 12:51:13 +02:00
|
|
|
pa_card *card;
|
|
|
|
|
uint32_t state;
|
|
|
|
|
|
|
|
|
|
PA_IDXSET_FOREACH(card, core->cards, state) {
|
2013-02-18 09:10:34 +01:00
|
|
|
pa_card_profile *profile;
|
2012-08-31 12:51:13 +02:00
|
|
|
void *state2;
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
PA_HASHMAP_FOREACH(profile, card->profiles, state2)
|
|
|
|
|
profile_available_hook_callback(core, profile, NULL);
|
2012-08-31 12:51:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-08-22 09:04:20 +02:00
|
|
|
int pa__init(pa_module *m) {
|
2012-08-22 09:04:21 +02:00
|
|
|
pa_modargs *ma;
|
2012-07-04 16:48:27 +02:00
|
|
|
struct userdata *u;
|
|
|
|
|
|
|
|
|
|
pa_assert(m);
|
|
|
|
|
|
2012-08-22 09:04:21 +02:00
|
|
|
if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
|
|
|
|
|
pa_log_error("Failed to parse module arguments");
|
2016-01-13 20:27:39 -08:00
|
|
|
goto fail;
|
2012-08-22 09:04:21 +02:00
|
|
|
}
|
|
|
|
|
|
2012-08-31 12:50:58 +02:00
|
|
|
m->userdata = u = pa_xnew0(struct userdata, 1);
|
2012-07-04 16:48:27 +02:00
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
u->auto_switch = 1;
|
2017-03-15 20:02:48 +02:00
|
|
|
|
|
|
|
|
if (pa_modargs_get_value(ma, "auto_switch", NULL)) {
|
|
|
|
|
bool auto_switch_bool;
|
|
|
|
|
|
|
|
|
|
/* auto_switch originally took a boolean value, let's keep
|
|
|
|
|
* compatibility with configuration files that still pass a boolean. */
|
|
|
|
|
if (pa_modargs_get_value_boolean(ma, "auto_switch", &auto_switch_bool) >= 0) {
|
|
|
|
|
if (auto_switch_bool)
|
|
|
|
|
u->auto_switch = 1;
|
|
|
|
|
else
|
|
|
|
|
u->auto_switch = 0;
|
|
|
|
|
|
|
|
|
|
} else if (pa_modargs_get_value_u32(ma, "auto_switch", &u->auto_switch) < 0) {
|
|
|
|
|
pa_log("Failed to parse auto_switch argument.");
|
|
|
|
|
goto fail;
|
|
|
|
|
}
|
2016-07-02 22:26:17 +02:00
|
|
|
}
|
|
|
|
|
|
2013-01-11 22:11:04 +02:00
|
|
|
u->enable_a2dp_source = true;
|
2012-08-22 09:04:21 +02:00
|
|
|
if (pa_modargs_get_value_boolean(ma, "a2dp_source", &u->enable_a2dp_source) < 0) {
|
|
|
|
|
pa_log("Failed to parse a2dp_source argument.");
|
|
|
|
|
goto fail;
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-22 11:07:13 +03:00
|
|
|
u->enable_ag = true;
|
|
|
|
|
if (pa_modargs_get_value_boolean(ma, "ag", &u->enable_ag) < 0) {
|
|
|
|
|
pa_log("Failed to parse ag argument.");
|
|
|
|
|
goto fail;
|
|
|
|
|
}
|
2012-08-22 09:04:22 +02:00
|
|
|
|
2016-07-02 22:26:17 +02:00
|
|
|
u->will_need_revert_card_map = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
|
|
|
|
|
|
2014-02-04 19:03:46 -03:00
|
|
|
u->source_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT], PA_HOOK_NORMAL,
|
|
|
|
|
(pa_hook_cb_t) source_put_hook_callback, u);
|
2012-07-04 16:48:27 +02:00
|
|
|
|
2014-02-04 19:03:46 -03:00
|
|
|
u->sink_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT], PA_HOOK_NORMAL,
|
|
|
|
|
(pa_hook_cb_t) sink_put_hook_callback, u);
|
2012-08-22 09:04:22 +02:00
|
|
|
|
2016-09-11 16:41:02 +02:00
|
|
|
if (u->auto_switch) {
|
2016-07-02 22:26:17 +02:00
|
|
|
u->source_output_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_PUT], PA_HOOK_NORMAL,
|
|
|
|
|
(pa_hook_cb_t) source_output_put_hook_callback, u);
|
|
|
|
|
|
bluetooth-policy: do A2DP profile restoring a bit later
This fixes a crash that happens if the bluetooth headset is the only
non-monitor source in the system and the last "phone" stream dies.
When the stream dies, the native protocol calls pa_source_output_unlink()
and would call pa_source_output_unref() next, but without this patch,
things happen during the unlinking, and the unreffing ends up being
performed on a stream that is already freed.
pa_source_output_unlink() fires the "unlink" hook before doing anything
else. module-bluetooth-policy then switches the headset profile from HSP
to A2DP within that hook. The HSP source gets removed, and at this point
the dying stream is still connected to it, and needs to be rescued.
Rescuing fails, because there are no other sources in the system, so the
stream gets killed. The native protocol has a kill callback, which again
calls pa_source_output_unlink() and pa_source_output_unref(). This is
the point where the native protocol drops its own reference to the
stream, but another unref call is waiting to be executed once we return
from the original unlink call.
I first tried to avoid the double unreffing by making it safe to do
unlinking recursively, but I found out that there's code that assumes
that once unlink() returns, unlinking has actually occurred (a
reasonable assumption), and at least with my implementation this was not
guaranteed. I now think that we must avoid situations where unlinking
happens recursively. It's just too hairy to deal with. This patch moves
the bluetooth profile switch to happen at a time when the dead stream
isn't any more connected to the source, so it doesn't have to be
rescued or killed.
BugLink: https://bugs.freedesktop.org/show_bug.cgi?id=97906
2016-10-12 17:20:39 +03:00
|
|
|
u->source_output_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_UNLINK_POST], PA_HOOK_NORMAL,
|
2016-07-02 22:26:17 +02:00
|
|
|
(pa_hook_cb_t) source_output_unlink_hook_callback, u);
|
|
|
|
|
|
|
|
|
|
u->card_init_profile_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_CHOOSE_INITIAL_PROFILE], PA_HOOK_NORMAL,
|
|
|
|
|
(pa_hook_cb_t) card_init_profile_hook_callback, u);
|
|
|
|
|
|
|
|
|
|
u->card_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_UNLINK], PA_HOOK_NORMAL,
|
|
|
|
|
(pa_hook_cb_t) card_unlink_hook_callback, u);
|
|
|
|
|
}
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
u->profile_available_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_PROFILE_AVAILABLE_CHANGED],
|
|
|
|
|
PA_HOOK_NORMAL, (pa_hook_cb_t) profile_available_hook_callback, u);
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
handle_all_profiles(m->core);
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2012-08-22 09:04:21 +02:00
|
|
|
pa_modargs_free(ma);
|
2012-07-04 16:48:27 +02:00
|
|
|
return 0;
|
2012-08-22 09:04:21 +02:00
|
|
|
|
|
|
|
|
fail:
|
2016-01-13 20:27:39 -08:00
|
|
|
if (ma)
|
|
|
|
|
pa_modargs_free(ma);
|
2012-08-22 09:04:21 +02:00
|
|
|
return -1;
|
2012-07-04 16:48:27 +02:00
|
|
|
}
|
|
|
|
|
|
2012-08-22 09:04:20 +02:00
|
|
|
void pa__done(pa_module *m) {
|
2012-07-04 16:48:27 +02:00
|
|
|
struct userdata *u;
|
|
|
|
|
|
|
|
|
|
pa_assert(m);
|
|
|
|
|
|
|
|
|
|
if (!(u = m->userdata))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (u->source_put_slot)
|
|
|
|
|
pa_hook_slot_free(u->source_put_slot);
|
|
|
|
|
|
2012-08-22 09:04:22 +02:00
|
|
|
if (u->sink_put_slot)
|
|
|
|
|
pa_hook_slot_free(u->sink_put_slot);
|
|
|
|
|
|
2016-07-02 22:26:17 +02:00
|
|
|
if (u->source_output_put_slot)
|
|
|
|
|
pa_hook_slot_free(u->source_output_put_slot);
|
|
|
|
|
|
|
|
|
|
if (u->source_output_unlink_slot)
|
|
|
|
|
pa_hook_slot_free(u->source_output_unlink_slot);
|
|
|
|
|
|
|
|
|
|
if (u->card_init_profile_slot)
|
|
|
|
|
pa_hook_slot_free(u->card_init_profile_slot);
|
|
|
|
|
|
|
|
|
|
if (u->card_unlink_slot)
|
|
|
|
|
pa_hook_slot_free(u->card_unlink_slot);
|
|
|
|
|
|
2013-02-18 09:10:34 +01:00
|
|
|
if (u->profile_available_changed_slot)
|
|
|
|
|
pa_hook_slot_free(u->profile_available_changed_slot);
|
2012-08-31 12:51:13 +02:00
|
|
|
|
2016-07-02 22:26:17 +02:00
|
|
|
pa_hashmap_free(u->will_need_revert_card_map);
|
|
|
|
|
|
2012-07-04 16:48:27 +02:00
|
|
|
pa_xfree(u);
|
|
|
|
|
}
|