/** * @file audiorouting.c * Audio routing module -- this listens to the audio routing *

* Copyright © 2009-2010 Nokia Corporation and/or its subsidiary(-ies). * Copyright (C) 2014-2019 Jolla Ltd. *

* @author David Weinehall * @author Simo Piiroinen * * mce is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * mce 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mce. If not, see . */ #include "../mce.h" #include "../mce-log.h" #include "../mce-dbus.h" #include #include #include /** Module name */ #define MODULE_NAME "audiorouting" /** Functionality provided by this module */ static const gchar *const provides[] = { MODULE_NAME, NULL }; /** Module information */ G_MODULE_EXPORT module_info_struct module_info = { /** Name of the module */ .name = MODULE_NAME, /** Module provides */ .provides = provides, /** Module priority */ .priority = 100 }; /** D-Bus interface for the policy framework */ #define POLICY_DBUS_INTERFACE "com.nokia.policy" /** D-Bus signal for actions from the policy framework */ #define POLICY_AUDIO_ACTIONS "audio_actions" /** Bits for members values available in ohm_decision_t */ enum { DF_NONE = 0, DF_TYPE = 1 << 0, DF_DEVICE = 1 << 1, DF_MUTE = 1 << 2, DF_GROUP = 1 << 3, DF_CORK = 1 << 4, DF_MODE = 1 << 5, DF_HWID = 1 << 6, DF_VARIABLE = 1 << 7, DF_VALUE = 1 << 8, DF_LIMIT = 1 << 9, }; /** Generic struct capable of holding any ohm decision data */ typedef struct { unsigned fields; const char *type; const char *device; const char *mute; const char *group; const char *cork; const char *mode; const char *hwid; const char *variable; const char *value; dbus_int32_t limit; } ohm_decision_t; /** Lookup table for parsing/showing ohm_decision_t content */ static const struct { const char *name; unsigned field; int type; size_t offs; } field_lut[] = { // fields { "type", DF_TYPE, 's', offsetof(ohm_decision_t, type) }, { "device", DF_DEVICE, 's', offsetof(ohm_decision_t, device) }, { "mute", DF_MUTE, 's', offsetof(ohm_decision_t, mute) }, { "group", DF_GROUP, 's', offsetof(ohm_decision_t, group) }, { "cork", DF_CORK, 's', offsetof(ohm_decision_t, cork) }, { "mode", DF_MODE, 's', offsetof(ohm_decision_t, mode) }, { "hwid", DF_HWID, 's', offsetof(ohm_decision_t, hwid) }, { "variable",DF_VARIABLE,'s', offsetof(ohm_decision_t, variable) }, { "value", DF_VALUE, 's', offsetof(ohm_decision_t, value) }, { "limit", DF_LIMIT, 'i', offsetof(ohm_decision_t, limit) }, // sentinel { 0, DF_NONE, '-', 0 } }; /** Lookup table for mce audio route from sink device reported by ohmd */ static const struct { const char *device; audio_route_t route; } route_lut[] = { { "bta2dp", AUDIO_ROUTE_HEADSET, }, { "bthfp", AUDIO_ROUTE_HEADSET, }, { "bthsp", AUDIO_ROUTE_HEADSET, }, { "earpiece", AUDIO_ROUTE_HANDSET, }, { "earpieceandtvout", AUDIO_ROUTE_HANDSET, }, { "fmtx", AUDIO_ROUTE_UNDEF, }, { "headphone", AUDIO_ROUTE_UNDEF, }, { "headset", AUDIO_ROUTE_HEADSET, }, { "ihf", AUDIO_ROUTE_SPEAKER, }, { "ihfandbthsp", AUDIO_ROUTE_SPEAKER, }, { "ihfandfmtx", AUDIO_ROUTE_SPEAKER, }, { "ihfandheadset", AUDIO_ROUTE_HEADSET, }, { "ihfandtvout", AUDIO_ROUTE_SPEAKER, }, { "null", AUDIO_ROUTE_UNDEF, }, { "tvout", AUDIO_ROUTE_UNDEF, }, { "tvoutandbta2dp", AUDIO_ROUTE_HEADSET, }, { "tvoutandbthsp", AUDIO_ROUTE_HEADSET, }, // sentinel { NULL, AUDIO_ROUTE_UNDEF, }, }; /** Audio route; derived from audio sink device name */ static audio_route_t audio_route = AUDIO_ROUTE_UNDEF; /** Audio playback: derived from media_state */ static tristate_t media_playback_state = TRISTATE_UNKNOWN; /* Volume limits used for "music playback" heuristics */ static int volume_limit_player = 100; static int volume_limit_flash = 100; static int volume_limit_inputsound = 100; /* Prototypes for local functions */ static void audio_mute_cb(ohm_decision_t *ohm); static void audio_cork_cb(ohm_decision_t *ohm); static void audio_route_sink(ohm_decision_t *ohm); static void audio_route_cb(ohm_decision_t *ohm); static void volume_limit_cb(ohm_decision_t *ohm); static void context_cb(ohm_decision_t *ohm); static void ohm_decision_reset_fields(ohm_decision_t *self); static void ohm_decision_show_fields(ohm_decision_t *self); static bool ohm_decision_parse_field(ohm_decision_t *self, const char *field, DBusMessageIter *from); static bool ohm_decision_parse(ohm_decision_t *self, DBusMessageIter *arr); static bool handle_policy_decisions(DBusMessageIter *ent, void (*cb)(ohm_decision_t *)); static bool handle_policy(DBusMessageIter *arr); static gboolean actions_dbus_cb(DBusMessage *sig); /** Helper for doing base + offset address calculations */ static inline void *lea(const void *base, off_t offs) { return (char *)(base) + offs; } /** Handle com.nokia.policy.audio_mute decision */ static void audio_mute_cb(ohm_decision_t *ohm) { unsigned want = DF_DEVICE | DF_MUTE; unsigned have = ohm->fields & want; if( have != want ) goto EXIT; // nothing for mce in here EXIT: return; } /** Handle com.nokia.policy.audio_cork decision */ static void audio_cork_cb(ohm_decision_t *ohm) { unsigned want = DF_GROUP | DF_CORK; unsigned have = ohm->fields & want; if( have != want ) goto EXIT; // nothing for mce in here EXIT: return; } /** Handle com.nokia.policy.audio_route decision for sink device */ static void audio_route_sink(ohm_decision_t *ohm) { /* Lookup audio route id from sink device name. * * Note: For the purposes of mce device names * "xxx" and "xxxforcall" are considered equal. */ for( int i = 0; ; ++i ) { if( !route_lut[i].device ) { mce_log(LL_WARN, "unknown audio sink device = '%s'", ohm->device); audio_route = route_lut[i].route; break; } int n = strlen(route_lut[i].device); if( strncmp(route_lut[i].device, ohm->device, n) ) continue; const char *e = &ohm->device[n]; if( *e && strcmp(e, "forcall") && strcmp(e, "foralien") ) continue; audio_route = route_lut[i].route; break; } mce_log(LL_DEBUG, "audio sink '%s' -> audio route %s", ohm->device, audio_route_repr(audio_route)); } /** Handle com.nokia.policy.audio_route decision */ static void audio_route_cb(ohm_decision_t *ohm) { unsigned want = DF_TYPE | DF_DEVICE | DF_MODE | DF_HWID; unsigned have = ohm->fields & want; if( have != want ) goto EXIT; mce_log(LL_DEBUG, "handling: %s - %s - %s - %s", ohm->type, ohm->device, ohm->mode, ohm->hwid); if( !strcmp(ohm->type, "sink") ) { audio_route_sink(ohm); } EXIT: return; } /** Handle com.nokia.policy.volume_limit decision */ static void volume_limit_cb(ohm_decision_t *ohm) { unsigned want = DF_GROUP | DF_LIMIT; unsigned have = ohm->fields & want; if( have != want ) goto EXIT; if( !strcmp(ohm->group, "player") ) { if( volume_limit_player != ohm->limit ) { mce_log(LL_DEBUG, "volume_limit_player: %d -> %d", volume_limit_player, ohm->limit); volume_limit_player = ohm->limit; } } else if( !strcmp(ohm->group, "flash") ) { if( volume_limit_flash != ohm->limit ) { mce_log(LL_DEBUG, "volume_limit_flash: %d -> %d", volume_limit_flash, ohm->limit); volume_limit_flash = ohm->limit; } } else if( !strcmp(ohm->group, "inputsound") ) { if( volume_limit_inputsound != ohm->limit ) { mce_log(LL_DEBUG, "volume_limit_inputsound: %d -> %d", volume_limit_inputsound, ohm->limit); volume_limit_inputsound = ohm->limit; } } EXIT: return; } /** Handle com.nokia.policy.context decision */ static void context_cb(ohm_decision_t *ohm) { unsigned want = DF_VARIABLE | DF_VALUE; unsigned have = ohm->fields & want; if( have != want ) goto EXIT; if( !strcmp(ohm->variable, "media_state") ) { tristate_t state = TRISTATE_UNKNOWN; if( !strcmp(ohm->value, "active") || !strcmp(ohm->value, "background") ) state = TRISTATE_TRUE; else state = TRISTATE_FALSE; if( media_playback_state != state ) { mce_log(LL_DEBUG, "media_playback_state: %s -> %s", tristate_repr(media_playback_state), tristate_repr(state)); media_playback_state = state; } } EXIT: return; } /** Reset ohm_decision_t fields */ static void ohm_decision_reset_fields(ohm_decision_t *self) { self->fields = DF_NONE; self->type = 0; self->device = 0; self->mute = 0; self->group = 0; self->cork = 0; self->mode = 0; self->hwid = 0; self->variable = 0; self->value = 0; self->limit = -1; } /** Show ohm_decision_t fields */ static void ohm_decision_show_fields(ohm_decision_t *self) { char buf[1024]; *buf = 0; for( int i = 0; field_lut[i].name; ++i ) { if( !(self->fields & field_lut[i].field) ) continue; switch( field_lut[i].type ) { case 's': sprintf(strchr(buf,0), " %s='%s'", field_lut[i].name, *(const char **)lea(self, field_lut[i].offs)); break; case 'i': sprintf(strchr(buf,0), " %s=%ld", field_lut[i].name, (long)*(dbus_int32_t *)lea(self, field_lut[i].offs)); break; default: sprintf(strchr(buf,0), " %s=???", field_lut[i].name); break; } } if( *buf ) mce_log(LL_DEBUG, "%s", buf+1); } /** Parse one named field from dbus message iterator */ static bool ohm_decision_parse_field(ohm_decision_t *self, const char *field, DBusMessageIter *from) { bool ack = false; for( int i = 0; ; ++i ) { if( !field_lut[i].name ) { mce_log(LL_WARN, "unhandled ohm field '%s'", field); break; } if( strcmp(field, field_lut[i].name) ) continue; switch( field_lut[i].type ) { case 's': ack = mce_dbus_iter_get_string(from, lea(self, field_lut[i].offs)); break; case 'i': ack = mce_dbus_iter_get_int32(from, lea(self, field_lut[i].offs)); break; default: ack = true; break; } self->fields |= field_lut[i].field; break; } return ack; } /** Parse all decision fields from dbus message iterator */ static bool ohm_decision_parse(ohm_decision_t *self, DBusMessageIter *arr) { bool ack = false; DBusMessageIter str, var; while( !mce_dbus_iter_at_end(arr) ) { const char *key = 0; if( !mce_dbus_iter_get_struct(arr, &str) ) goto EXIT; if( !mce_dbus_iter_get_string(&str, &key) ) goto EXIT; if( !mce_dbus_iter_get_variant(&str, &var) ) goto EXIT; if( !ohm_decision_parse_field(self, key, &var) ) goto EXIT; } ack = true; EXIT: return ack; } /** Handle policy decision blocks within audio_actions signal */ static bool handle_policy_decisions(DBusMessageIter *ent, void (*cb)(ohm_decision_t *)) { bool ack = false; DBusMessageIter arr1, arr2; if( !mce_dbus_iter_get_array(ent, &arr1) ) goto EXIT; while( !mce_dbus_iter_at_end(&arr1) ) { if( !mce_dbus_iter_get_array(&arr1, &arr2) ) goto EXIT; ohm_decision_t ohm; ohm_decision_reset_fields(&ohm); if( !ohm_decision_parse(&ohm, &arr2) ) goto EXIT; if( mce_log_p(LL_DEBUG) ) ohm_decision_show_fields(&ohm); cb(&ohm); } ack = true; EXIT: return ack; } /** Handle policy block within audio_actions signal */ static bool handle_policy(DBusMessageIter *arr) { bool ack = false; const char *name = 0; DBusMessageIter ent; if( !mce_dbus_iter_get_entry(arr, &ent) ) goto EXIT; if( !mce_dbus_iter_get_string(&ent, &name) ) goto EXIT; mce_log(LL_DEBUG, "policy name = %s", name); void (*cb)(ohm_decision_t *) = 0; /* com.nokia.policy.audio_mute * device - mute * * com.nokia.policy.audio_cork * group - cork * * com.nokia.policy.audio_route * type - device - mode - hwid * * com.nokia.policy.volume_limit * group - limit * * com.nokia.policy.context * variable - value */ if( !strcmp(name, "com.nokia.policy.audio_mute") ) cb = audio_mute_cb; else if( !strcmp(name, "com.nokia.policy.audio_cork") ) cb = audio_cork_cb; else if( !strcmp(name, "com.nokia.policy.audio_route") ) cb = audio_route_cb; else if( !strcmp(name, "com.nokia.policy.volume_limit") ) cb = volume_limit_cb; else if( !strcmp(name, "com.nokia.policy.context") ) cb = context_cb; else mce_log(LL_WARN, "unknown policy '%s'", name); if( cb && !handle_policy_decisions(&ent, cb) ) goto EXIT; ack = true; EXIT: return ack; } /** * D-Bus callback for the actions signal * * @param msg The D-Bus message * * @return TRUE */ static gboolean actions_dbus_cb(DBusMessage *sig) { bool playback = false; DBusMessageIter body, arr; mce_log(LL_DEVEL, "Received audio policy actions from %s", mce_dbus_get_message_sender_ident(sig)); dbus_message_iter_init(sig, &body); dbus_uint32_t unused = 0; if( !mce_dbus_iter_get_uint32(&body, &unused) ) goto EXIT; if( !mce_dbus_iter_get_array(&body, &arr) ) goto EXIT; while( !mce_dbus_iter_at_end(&arr) ) { if( !handle_policy(&arr) ) goto EXIT; } if( media_playback_state != TRISTATE_UNKNOWN ) { /* Use media_state from com.nokia.policy.context * when it is included in OHM policy signal. */ playback = (media_playback_state == TRISTATE_TRUE); } else { /* Fallback to volume limit heuristics */ playback = (volume_limit_player > 0 && volume_limit_flash <= 0 && volume_limit_inputsound <= 0); } EXIT: if( datapipe_get_gint(music_playback_ongoing_pipe) != playback ) { mce_log(LL_DEVEL, "music playback: %d", playback); datapipe_exec_full(&music_playback_ongoing_pipe, GINT_TO_POINTER(playback)); } if( datapipe_get_gint(audio_route_pipe) != audio_route ) { mce_log(LL_DEVEL, "audio route: %s", audio_route_repr(audio_route)); datapipe_exec_full(&audio_route_pipe, GINT_TO_POINTER(audio_route)); } return TRUE; } /** Array of dbus message handlers */ static mce_dbus_handler_t handlers[] = { /* signals */ { .interface = POLICY_DBUS_INTERFACE, .name = POLICY_AUDIO_ACTIONS, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = actions_dbus_cb, }, /* sentinel */ { .interface = 0 } }; /** * Init function for the audio routing module * * @param module Unused * * @return NULL on success, a string with an error message on failure */ G_MODULE_EXPORT const gchar *g_module_check_init(GModule *module); const gchar *g_module_check_init(GModule *module) { (void)module; mce_dbus_handler_register_array(handlers); return NULL; } /** * Exit function for the audio routing module * * @param module Unused */ G_MODULE_EXPORT void g_module_unload(GModule *module); void g_module_unload(GModule *module) { (void)module; mce_dbus_handler_unregister_array(handlers); return; }