The idea of the feature is that a device vendor may want to initialize device volumes to known good defaults. The factory settings are read from a configuration that contains entries that look like this: [Entry sink:foodevicename:barportname] volume = front-left:70%,front-right:70% The settings are written to the device-restore database during module loading, but only if the database doesn't already contain an entry with matching key, so user choices are not overwritten. --- src/modules/module-device-restore.c | 143 ++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/src/modules/module-device-restore.c b/src/modules/module-device-restore.c index e334ff6..b1b2d38 100644 --- a/src/modules/module-device-restore.c +++ b/src/modules/module-device-restore.c @@ -53,6 +53,7 @@ #include <pulsecore/pstream-util.h> #include <pulsecore/database.h> #include <pulsecore/tagstruct.h> +#include <pulsecore/conf-parser.h> #include "module-device-restore-symdef.h" @@ -64,15 +65,19 @@ PA_MODULE_USAGE( "restore_port=<Save/restore port?> " "restore_volume=<Save/restore volumes?> " "restore_muted=<Save/restore muted states?> " - "restore_formats=<Save/restore saved formats?>"); + "restore_formats=<Save/restore saved formats?> " + "factory_settings=<file>"); #define SAVE_INTERVAL (10 * PA_USEC_PER_SEC) +#define DEFAULT_FACTORY_SETTINGS_FILE PA_DEFAULT_CONFIG_DIR PA_PATH_SEP "device-restore-factory-settings.conf" +#define DEFAULT_FACTORY_SETTINGS_FILE_USER "device-restore-factory-settings.conf" static const char* const valid_modargs[] = { "restore_volume", "restore_muted", "restore_port", "restore_formats", + "factory_settings", NULL }; @@ -131,6 +136,13 @@ struct perportentry { pa_idxset *formats; }; +struct factory_settings_entry { + char *key; + pa_cvolume volume; + pa_channel_map channel_map; + bool volume_valid; +}; + static void save_time_callback(pa_mainloop_api*a, pa_time_event* e, const struct timeval *t, void *userdata) { struct userdata *u = userdata; @@ -1230,6 +1242,130 @@ static pa_hook_result_t connection_unlink_hook_cb(pa_native_protocol *p, pa_nati return PA_HOOK_OK; } +static struct factory_settings_entry *factory_settings_entry_get(pa_hashmap *factory_settings, const char *section) { + struct factory_settings_entry *entry; + + pa_assert(factory_settings); + + if (!section || !pa_startswith(section, "Entry ")) + return NULL; + + section += 6; + + if ((entry = pa_hashmap_get(factory_settings, section))) + return entry; + + entry = pa_xnew0(struct factory_settings_entry, 1); + entry->key = pa_xstrdup(section); + pa_hashmap_put(factory_settings, entry->key, entry); + + return entry; +} + +static int parse_volume_cb(pa_config_parser_state *state) { + pa_hashmap *factory_settings; + struct factory_settings_entry *entry; + + pa_assert(state); + + factory_settings = state->userdata; + + if (!(entry = factory_settings_entry_get(factory_settings, state->section))) { + pa_log("[%s:%u] volume option not expected in section '%s'.", state->filename, state->lineno, pa_strnull(state->section)); + return -1; + } + + if (pa_parse_cvolume(state->rvalue, &entry->volume, &entry->channel_map) < 0) { + pa_log("[%s:%u] failed to parse volume.", state->filename, state->lineno); + return -1; + } + + entry->volume_valid = true; + + return 0; +} + +static int load_factory_settings(struct userdata *u, const char *filename) { + FILE *f = NULL; + char *fn; + int r; + pa_hashmap *factory_settings; + struct factory_settings_entry *entry; + void *state; + + pa_config_item config_items[] = { + { "volume", parse_volume_cb, NULL, NULL }, + { NULL, NULL, NULL, NULL } + }; + + if (!filename && !(f = pa_open_config_file(DEFAULT_FACTORY_SETTINGS_FILE, DEFAULT_FACTORY_SETTINGS_FILE_USER, NULL, &fn))) { + if (errno != ENOENT) + return -1; + + return 0; + } + + /* pa_config_parse() would open the file for us if we didn't open it + * ourselves, but if pa_config_parse() has to open the file, it will not + * return an error if the file is not found. That's supposedly useful in + * other situations, but we want to report failure if filename was + * explicitly configured and the file is not found, so we have to open the + * file ourselves. */ + if (filename && !(f = pa_fopen_cloexec(filename, "r"))) { + pa_log("Failed to open '%s': %s", filename, pa_cstrerror(errno)); + return -1; + } + + if (!filename) + filename = fn; + + factory_settings = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + + if ((r = pa_config_parse(filename, f, config_items, NULL, factory_settings)) < 0) + goto finish; + + PA_HASHMAP_FOREACH(entry, factory_settings, state) { + struct perportentry *ppe; + + if (!entry->volume_valid) + continue; + + ppe = perportentry_read_raw_name(u, entry->key); + + if (!ppe || !ppe->volume_valid) { + if (!ppe) + ppe = perportentry_new(FALSE); + + ppe->volume = entry->volume; + ppe->channel_map = entry->channel_map; + ppe->volume_valid = true; + + perportentry_write_raw_name(u, entry->key, ppe); + pa_log_debug("Added factory settings to the database for entry %s.", entry->key); + + /* Note that the device type doesn't matter when we give PA_INVALID_INDEX. */ + trigger_save(u, PA_DEVICE_TYPE_SINK, PA_INVALID_INDEX); + } + + perportentry_free(ppe); + } + +finish: + + while ((entry = pa_hashmap_steal_first(factory_settings))) { + pa_xfree(entry->key); + pa_xfree(entry); + } + + pa_hashmap_free(factory_settings, NULL, NULL); + pa_xfree(fn); + + if (f) + fclose(f); + + return r; +} + int pa__init(pa_module*m) { pa_modargs *ma = NULL; struct userdata *u; @@ -1302,6 +1438,11 @@ int pa__init(pa_module*m) { pa_log_info("Successfully opened database file '%s'.", fname); pa_xfree(fname); + if (load_factory_settings(u, pa_modargs_get_value(ma, "factory_settings", NULL)) < 0) { + pa_log("Loading factory settings failed."); + goto fail; + } + PA_IDXSET_FOREACH(sink, m->core->sinks, idx) subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_NEW, sink->index, u); -- 1.7.10.4