/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "audio_hw_sndmonitor"
/*#define LOG_NDEBUG 0*/
#define LOG_NDDEBUG 0
/* monitor sound card, cpe state
audio_dev registers for a callback from this module in adev_open
Each stream in audio_hal registers for a callback in
adev_open_*_stream.
A thread is spawned to poll() on sound card state files in /proc.
On observing a sound card state change, this thread invokes the
callbacks registered.
Callbacks are deregistered in adev_close_*_stream and adev_close
*/
#include <stdlib.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/poll.h>
#include <pthread.h>
#include <cutils/list.h>
#include <cutils/hashmap.h>
#include <log/log.h>
#include <cutils/str_parms.h>
#include <ctype.h>
#include "audio_hw.h"
#include "audio_extn.h"
//#define MONITOR_DEVICE_EVENTS
#define CPE_MAGIC_NUM 0x2000
#define MAX_CPE_SLEEP_RETRY 2
#define CPE_SLEEP_WAIT 100
#define MAX_SLEEP_RETRY 100
#define AUDIO_INIT_SLEEP_WAIT 100 /* 100 ms */
#define AUDIO_PARAMETER_KEY_EXT_AUDIO_DEVICE "ext_audio_device"
typedef enum {
audio_event_on,
audio_event_off
} audio_event_status;
typedef struct {
int card;
int fd;
struct listnode node; // membership in sndcards list
card_status_t status;
} sndcard_t;
typedef struct {
char * dev;
int fd;
int status;
struct listnode node; // membership in deviceevents list;
} dev_event_t;
typedef void (* notifyfn)(const void * target, const char * msg);
typedef struct {
const void * target;
notifyfn notify;
struct listnode cards;
unsigned int num_cards;
struct listnode dev_events;
unsigned int num_dev_events;
pthread_t monitor_thread;
int intpipe[2];
Hashmap * listeners; // from stream * -> callback func
bool initcheck;
} sndmonitor_state_t;
static sndmonitor_state_t sndmonitor;
static char * read_state(int fd)
{
struct stat buf;
if (fstat(fd, &buf) < 0)
return NULL;
off_t pos = lseek(fd, 0, SEEK_CUR);
off_t avail = buf.st_size - pos;
if (avail <= 0) {
ALOGD("avail %ld", avail);
return NULL;
}
char * state = (char *)calloc(avail+1, sizeof(char));
if (!state)
return NULL;
ssize_t bytes=read(fd, state, avail);
if (bytes <= 0)
return NULL;
// trim trailing whitespace
while (bytes && isspace(*(state+bytes-1))) {
*(state + bytes - 1) = '\0';
--bytes;
}
lseek(fd, 0, SEEK_SET);
return state;
}
static int add_new_sndcard(int card, int fd)
{
sndcard_t * s = (sndcard_t *)calloc(sizeof(sndcard_t), 1);
if (!s)
return -1;
s->card = card;
s->fd = fd; // dup?
char * state = read_state(fd);
bool online = state && !strcmp(state, "ONLINE");
ALOGV("card %d initial state %s %d", card, state, online);
if (state)
free(state);
s->status = online ? CARD_STATUS_ONLINE : CARD_STATUS_OFFLINE;
list_add_tail(&sndmonitor.cards, &s->node);
return 0;
}
static int enum_sndcards()
{
const char* cards = "/proc/asound/cards";
int tries = 10;
char *line = NULL;
size_t len = 0;
ssize_t bytes_read;
char path[128] = {0};
char *ptr, *saveptr, *card_id;
int line_no=0;
unsigned int num_cards=0, num_cpe=0;
FILE *fp;
int fd, ret;
while (--tries) {
if ((fp = fopen(cards, "r")) == NULL) {
ALOGE("Cannot open %s file to get list of sound cards", cards);
usleep(100000);
continue;
}
break;
}
if (!tries)
return -ENODEV;
while ((bytes_read = getline(&line, &len, fp) != -1)) {
// skip every other line to to match
// the output format of /proc/asound/cards
if (line_no++ % 2)
continue;
ptr = strtok_r(line, " [", &saveptr);
if (!ptr)
continue;
card_id = strtok_r(saveptr+1, "]", &saveptr);
if (!card_id)
continue;
// Limit to sound cards associated with ADSP
if ((strncasecmp(card_id, "msm", 3) != 0) &&
(strncasecmp(card_id, "sdm", 3) != 0) &&
(strncasecmp(card_id, "sdc", 3) != 0) &&
(strncasecmp(card_id, "apq", 3) != 0)) {
ALOGW("Skip over non-ADSP snd card %s", card_id);
continue;
}
snprintf(path, sizeof(path), "/proc/asound/card%s/state", ptr);
ALOGV("Opening sound card state : %s", path);
fd = open(path, O_RDONLY);
if (fd == -1) {
ALOGE("Open %s failed : %s", path, strerror(errno));
continue;
}
ret = add_new_sndcard(atoi(ptr), fd);
if (ret != 0) {
close(fd); // card state fd ownership is taken by sndcard on success
continue;
}
num_cards++;
// query cpe state for this card as well
tries=MAX_CPE_SLEEP_RETRY;
snprintf(path, sizeof(path), "/proc/asound/card%s/cpe0_state", ptr);
if (access(path, R_OK) < 0) {
ALOGW("access %s failed w/ err %s", path, strerror(errno));
continue;
}
ALOGV("Open cpe state card state %s", path);
while (--tries) {
if ((fd = open(path, O_RDONLY)) < 0) {
ALOGW("Open cpe state card state failed, retry : %s", path);
usleep(CPE_SLEEP_WAIT*1000);
continue;
}
break;
}
if (!tries)
continue;
ret = add_new_sndcard(CPE_MAGIC_NUM+num_cpe, fd);
if (ret != 0) {
close(fd); // card state fd ownership is taken by sndcard on success
continue;
}
num_cpe++;
num_cards++;
}
if (line)
free(line);
fclose(fp);
ALOGV("sndmonitor registerer num_cards %d", num_cards);
sndmonitor.num_cards = num_cards;
return num_cards ? 0 : -1;
}
static void free_sndcards()
{
while (!list_empty(&sndmonitor.cards)) {
struct listnode * n = list_head(&sndmonitor.cards);
sndcard_t * s = node_to_item(n, sndcard_t, node);
list_remove(n);
close(s->fd);
free(s);
}
}
static int add_new_dev_event(char * d_name, int fd)
{
dev_event_t * d = (dev_event_t *)calloc(sizeof(dev_event_t), 1);
if (!d)
return -1;
d->dev = strdup(d_name);
d->fd = fd;
list_add_tail(&sndmonitor.dev_events, &d->node);
return 0;
}
static int enum_dev_events()
{
const char* events_dir = "/sys/class/switch/";
DIR *dp;
struct dirent* in_file;
int fd;
char path[128] = {0};
unsigned int num_dev_events = 0;
if ((dp = opendir(events_dir)) == NULL) {
ALOGE("Cannot open switch directory %s err %s",
events_dir, strerror(errno));
return -1;
}
while ((in_file = readdir(dp)) != NULL) {
if (!strstr(in_file->d_name, "qc_"))
continue;
snprintf(path, sizeof(path), "%s/%s/state",
events_dir, in_file->d_name);
ALOGV("Opening audio dev event state : %s ", path);
fd = open(path, O_RDONLY);
if (fd == -1) {
ALOGE("Open %s failed : %s", path, strerror(errno));
} else {
if (!add_new_dev_event(in_file->d_name, fd))
num_dev_events++;
}
}
closedir(dp);
sndmonitor.num_dev_events = num_dev_events;
return num_dev_events ? 0 : -1;
}
static void free_dev_events()
{
while (!list_empty(&sndmonitor.dev_events)) {
struct listnode * n = list_head(&sndmonitor.dev_events);
dev_event_t * d = node_to_item(n, dev_event_t, node);
list_remove(n);
close(d->fd);
free(d->dev);
free(d);
}
}
static int notify(const struct str_parms * params)
{
if (!params)
return -1;
char * str = str_parms_to_str((struct str_parms *)params);
if (!str)
return -1;
if (sndmonitor.notify)
sndmonitor.notify(sndmonitor.target, str);
ALOGV("%s", str);
free(str);
return 0;
}
int on_dev_event(dev_event_t * dev_event)
{
char state_buf[2];
if (read(dev_event->fd, state_buf, 1) <= 0)
return -1;
lseek(dev_event->fd, 0, SEEK_SET);
state_buf[1]='\0';
if (atoi(state_buf) == dev_event->status)
return 0;
dev_event->status = atoi(state_buf);
struct str_parms * params = str_parms_create();
if (!params)
return -1;
char val[32] = {0};
snprintf(val, sizeof(val), "%s,%s", dev_event->dev,
dev_event->status ? "ON" : "OFF");
if (str_parms_add_str(params, AUDIO_PARAMETER_KEY_EXT_AUDIO_DEVICE, val) < 0)
return -1;
int ret = notify(params);
str_parms_destroy(params);
return ret;
}
bool on_sndcard_state_update(sndcard_t * s)
{
char rd_buf[9]={0};
card_status_t status;
if (read(s->fd, rd_buf, 8) <= 0)
return -1;
rd_buf[8] = '\0';
lseek(s->fd, 0, SEEK_SET);
ALOGV("card num %d, new state %s", s->card, rd_buf);
bool is_cpe = (s->card >= CPE_MAGIC_NUM);
if (strstr(rd_buf, "OFFLINE"))
status = CARD_STATUS_OFFLINE;
else if (strstr(rd_buf, "ONLINE"))
status = CARD_STATUS_ONLINE;
else {
ALOGE("unknown state");
return 0;
}
if (status == s->status) // no change
return 0;
s->status = status;
struct str_parms * params = str_parms_create();
if (!params)
return -1;
char val[32] = {0};
// cpe actual card num is (card - MAGIC_NUM). so subtract accordingly
snprintf(val, sizeof(val), "%d,%s", s->card - (is_cpe ? CPE_MAGIC_NUM : 0),
status == CARD_STATUS_ONLINE ? "ONLINE" : "OFFLINE");
if (str_parms_add_str(params, is_cpe ? "CPE_STATUS" : "SND_CARD_STATUS",
val) < 0)
return -1;
int ret = notify(params);
str_parms_destroy(params);
return ret;
}
void * monitor_thread_loop(void * args __unused)
{
ALOGV("Start threadLoop()");
unsigned int num_poll_fds = sndmonitor.num_cards +
sndmonitor.num_dev_events + 1/*pipe*/;
struct pollfd * pfd = (struct pollfd *)calloc(sizeof(struct pollfd),
num_poll_fds);
if (!pfd)
return NULL;
pfd[0].fd = sndmonitor.intpipe[0];
pfd[0].events = POLLPRI|POLLIN;
int i=1;
struct listnode *node;
list_for_each(node, &sndmonitor.cards) {
sndcard_t * s = node_to_item(node, sndcard_t, node);
pfd[i].fd = s->fd;
pfd[i].events = POLLPRI;
++i;
}
list_for_each(node, &sndmonitor.dev_events) {
dev_event_t * d = node_to_item(node, dev_event_t, node);
pfd[i].fd = d->fd;
pfd[i].events = POLLPRI;
++i;
}
while (1) {
if (poll(pfd, num_poll_fds, -1) < 0) {
int errno_ = errno;
ALOGE("poll() failed w/ err %s", strerror(errno));
switch (errno_) {
case EINTR:
case ENOMEM:
sleep(2);
continue;
default:
/* above errors can be caused due to current system
state .. any other error is not expected */
LOG_ALWAYS_FATAL("unxpected poll() system call failure");
break;
}
}
ALOGV("out of poll()");
#define READY_TO_READ(p) ((p)->revents & (POLLIN|POLLPRI))
#define ERROR_IN_FD(p) ((p)->revents & (POLLERR|POLLHUP|POLLNVAL))
// check if requested to exit
if (READY_TO_READ(&pfd[0])) {
char buf[2]={0};
read(pfd[0].fd, buf, 1);
if (!strcmp(buf, "Q"))
break;
} else if (ERROR_IN_FD(&pfd[0])) {
// do not consider for poll again
// POLLERR - can this happen?
// POLLHUP - adev must not close pipe
// POLLNVAL - fd is valid
LOG_ALWAYS_FATAL("unxpected error in pipe poll fd 0x%x",
pfd[0].revents);
pfd[0].fd *= -1;
}
i=1;
list_for_each(node, &sndmonitor.cards) {
sndcard_t * s = node_to_item(node, sndcard_t, node);
if (READY_TO_READ(&pfd[i]))
on_sndcard_state_update(s);
else if (ERROR_IN_FD(&pfd[i])) {
// do not consider for poll again
// POLLERR - can this happen as we are reading from a fs?
// POLLHUP - not valid for cardN/state
// POLLNVAL - fd is valid
LOG_ALWAYS_FATAL("unxpected error in card poll fd 0x%x",
pfd[i].revents);
pfd[i].fd *= -1;
}
++i;
}
list_for_each(node, &sndmonitor.dev_events) {
dev_event_t * d = node_to_item(node, dev_event_t, node);
if (READY_TO_READ(&pfd[i]))
on_dev_event(d);
else if (ERROR_IN_FD(&pfd[i])) {
// do not consider for poll again
// POLLERR - can this happen as we are reading from a fs?
// POLLHUP - not valid for switch/state
// POLLNVAL - fd is valid
LOG_ALWAYS_FATAL("unxpected error in dev poll fd 0x%x",
pfd[i].revents);
pfd[i].fd *= -1;
}
++i;
}
}
return NULL;
}
// ---- listener static APIs ---- //
static int hashfn(void * key)
{
return (int)key;
}
static bool hasheq(void * key1, void *key2)
{
return key1 == key2;
}
static bool snd_cb(void* key, void* value, void* context)
{
snd_mon_cb cb = (snd_mon_cb)value;
cb(key, context);
return true;
}
static void snd_mon_update(const void * target __unused, const char * msg)
{
// target can be used to check if this message is intended for the
// recipient or not. (using some statically saved state)
struct str_parms *parms = str_parms_create_str(msg);
if (!parms)
return;
hashmapLock(sndmonitor.listeners);
hashmapForEach(sndmonitor.listeners, snd_cb, parms);
hashmapUnlock(sndmonitor.listeners);
str_parms_destroy(parms);
}
static int listeners_init()
{
sndmonitor.listeners = hashmapCreate(5, hashfn, hasheq);
if (!sndmonitor.listeners)
return -1;
return 0;
}
static int listeners_deinit()
{
// XXX TBD
return -1;
}
static int add_listener(void *stream, snd_mon_cb cb)
{
Hashmap * map = sndmonitor.listeners;
hashmapLock(map);
hashmapPut(map, stream, cb);
hashmapUnlock(map);
return 0;
}
static int del_listener(void * stream)
{
Hashmap * map = sndmonitor.listeners;
hashmapLock(map);
hashmapRemove(map, stream);
hashmapUnlock(map);
return 0;
}
// --- public APIs --- //
int audio_extn_snd_mon_deinit()
{
if (!sndmonitor.initcheck)
return -1;
write(sndmonitor.intpipe[1], "Q", 1);
pthread_join(sndmonitor.monitor_thread, (void **) NULL);
free_dev_events();
listeners_deinit();
free_sndcards();
close(sndmonitor.intpipe[0]);
close(sndmonitor.intpipe[1]);
sndmonitor.initcheck = 0;
return 0;
}
int audio_extn_snd_mon_init()
{
sndmonitor.notify = snd_mon_update;
sndmonitor.target = NULL; // unused for now
list_init(&sndmonitor.cards);
list_init(&sndmonitor.dev_events);
sndmonitor.initcheck = false;
if (pipe(sndmonitor.intpipe) < 0)
goto pipe_error;
if (enum_sndcards() < 0)
goto enum_sncards_error;
if (listeners_init() < 0)
goto listeners_error;
#ifdef MONITOR_DEVICE_EVENTS
enum_dev_events(); // failure here isn't fatal
#endif
int ret = pthread_create(&sndmonitor.monitor_thread,
(const pthread_attr_t *) NULL,
monitor_thread_loop, NULL);
if (ret) {
goto monitor_thread_create_error;
}
sndmonitor.initcheck = true;
return 0;
monitor_thread_create_error:
listeners_deinit();
listeners_error:
free_sndcards();
enum_sncards_error:
close(sndmonitor.intpipe[0]);
close(sndmonitor.intpipe[1]);
pipe_error:
return -ENODEV;
}
int audio_extn_snd_mon_register_listener(void *stream, snd_mon_cb cb)
{
if (!sndmonitor.initcheck) {
ALOGW("sndmonitor initcheck failed, cannot register");
return -1;
}
return add_listener(stream, cb);
}
int audio_extn_snd_mon_unregister_listener(void * stream)
{
if (!sndmonitor.initcheck) {
ALOGW("sndmonitor initcheck failed, cannot deregister");
return -1;
}
ALOGV("deregister listener for stream %p ", stream);
return del_listener(stream);
}