| /* |
| ** |
| ** Copyright 2012, 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 "AudioHAL:AudioHotplugThread" |
| #include <utils/Log.h> |
| |
| #include <assert.h> |
| #include <dirent.h> |
| #include <poll.h> |
| #include <sys/eventfd.h> |
| #include <sys/inotify.h> |
| #include <sys/ioctl.h> |
| |
| // Bionic's copy of asound.h contains references to these kernel macros. |
| // They need to be removed in order to include the file from userland. |
| #define __force |
| #define __bitwise |
| #define __user |
| #include <sound/asound.h> |
| #undef __force |
| #undef __bitwise |
| #undef __user |
| |
| #include <utils/misc.h> |
| #include <utils/String8.h> |
| |
| #include "AudioHotplugThread.h" |
| |
| // This name is used to recognize the AndroidTV Remote mic so we can |
| // use it for voice recognition. |
| #define ANDROID_TV_REMOTE_AUDIO_DEVICE_NAME "ATVRAudio" |
| |
| namespace android { |
| |
| /* |
| * ALSA parameter manipulation routines |
| * |
| * TODO: replace this when TinyAlsa offers a suitable API |
| */ |
| |
| static inline int param_is_mask(int p) |
| { |
| return (p >= SNDRV_PCM_HW_PARAM_FIRST_MASK) && |
| (p <= SNDRV_PCM_HW_PARAM_LAST_MASK); |
| } |
| |
| static inline int param_is_interval(int p) |
| { |
| return (p >= SNDRV_PCM_HW_PARAM_FIRST_INTERVAL) && |
| (p <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL); |
| } |
| |
| static inline struct snd_interval *param_to_interval( |
| struct snd_pcm_hw_params *p, int n) |
| { |
| assert(p->intervals); |
| assert(param_is_interval(n)); |
| return &(p->intervals[n - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL]); |
| } |
| |
| static inline struct snd_mask *param_to_mask(struct snd_pcm_hw_params *p, int n) |
| { |
| assert(p->masks); |
| assert(param_is_mask(n)); |
| return &(p->masks[n - SNDRV_PCM_HW_PARAM_FIRST_MASK]); |
| } |
| |
| static inline void snd_mask_any(struct snd_mask *mask) |
| { |
| memset(mask, 0xff, sizeof(struct snd_mask)); |
| } |
| |
| static inline void snd_interval_any(struct snd_interval *i) |
| { |
| i->min = 0; |
| i->openmin = 0; |
| i->max = UINT_MAX; |
| i->openmax = 0; |
| i->integer = 0; |
| i->empty = 0; |
| } |
| |
| static void param_init(struct snd_pcm_hw_params *p) |
| { |
| int n, k; |
| |
| memset(p, 0, sizeof(*p)); |
| for (n = SNDRV_PCM_HW_PARAM_FIRST_MASK; |
| n <= SNDRV_PCM_HW_PARAM_LAST_MASK; n++) { |
| struct snd_mask *m = param_to_mask(p, n); |
| snd_mask_any(m); |
| } |
| for (n = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; |
| n <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; n++) { |
| struct snd_interval *i = param_to_interval(p, n); |
| snd_interval_any(i); |
| } |
| p->rmask = 0xFFFFFFFF; |
| } |
| |
| /* |
| * Hotplug thread |
| */ |
| |
| const char* AudioHotplugThread::kThreadName = "ATVRemoteAudioHotplug"; |
| |
| // directory where ALSA device nodes appear |
| const char* AudioHotplugThread::kAlsaDeviceDir = "/dev/snd"; |
| |
| // filename suffix for ALSA nodes representing capture devices |
| const char AudioHotplugThread::kDeviceTypeCapture = 'c'; |
| |
| AudioHotplugThread::AudioHotplugThread(Callback& callback) |
| : mCallback(callback) |
| , mShutdownEventFD(-1) |
| { |
| } |
| |
| AudioHotplugThread::~AudioHotplugThread() |
| { |
| if (mShutdownEventFD != -1) { |
| ::close(mShutdownEventFD); |
| } |
| } |
| |
| bool AudioHotplugThread::start() |
| { |
| mShutdownEventFD = eventfd(0, EFD_NONBLOCK); |
| if (mShutdownEventFD == -1) { |
| return false; |
| } |
| |
| return (run(kThreadName) == NO_ERROR); |
| } |
| |
| void AudioHotplugThread::shutdown() |
| { |
| requestExit(); |
| uint64_t tmp = 1; |
| ::write(mShutdownEventFD, &tmp, sizeof(tmp)); |
| join(); |
| } |
| |
| bool AudioHotplugThread::parseCaptureDeviceName(const char* name, |
| unsigned int* card, |
| unsigned int* device) |
| { |
| char deviceType; |
| int ret = sscanf(name, "pcmC%uD%u%c", card, device, &deviceType); |
| return (ret == 3 && deviceType == kDeviceTypeCapture); |
| } |
| |
| static inline void getAlsaParamInterval(const struct snd_pcm_hw_params& params, |
| int n, unsigned int* min, |
| unsigned int* max) |
| { |
| struct snd_interval* interval = param_to_interval( |
| const_cast<struct snd_pcm_hw_params*>(¶ms), n); |
| *min = interval->min; |
| *max = interval->max; |
| } |
| |
| // This was hacked out of "alsa_utils.cpp". |
| static int s_get_alsa_card_name(char *name, size_t len, int card_id) |
| { |
| int fd; |
| int amt = -1; |
| snprintf(name, len, "/proc/asound/card%d/id", card_id); |
| fd = open(name, O_RDONLY); |
| if (fd >= 0) { |
| amt = read(fd, name, len - 1); |
| if (amt > 0) { |
| // replace the '\n' at the end of the proc file with '\0' |
| name[amt - 1] = 0; |
| } |
| close(fd); |
| } |
| return amt; |
| } |
| |
| bool AudioHotplugThread::getDeviceInfo(unsigned int pcmCard, |
| unsigned int pcmDevice, |
| DeviceInfo* info) |
| { |
| bool result = false; |
| int ret; |
| int len; |
| char cardName[64] = ""; |
| |
| String8 devicePath = String8::format("%s/pcmC%dD%d%c", |
| kAlsaDeviceDir, pcmCard, pcmDevice, kDeviceTypeCapture); |
| |
| ALOGD("AudioHotplugThread::getDeviceInfo opening %s", devicePath.string()); |
| int alsaFD = open(devicePath.string(), O_RDONLY); |
| if (alsaFD == -1) { |
| ALOGE("AudioHotplugThread::getDeviceInfo open failed for %s", devicePath.string()); |
| goto done; |
| } |
| |
| // query the device's ALSA configuration space |
| struct snd_pcm_hw_params params; |
| param_init(¶ms); |
| ret = ioctl(alsaFD, SNDRV_PCM_IOCTL_HW_REFINE, ¶ms); |
| if (ret == -1) { |
| ALOGE("AudioHotplugThread: refine ioctl failed"); |
| goto done; |
| } |
| |
| info->pcmCard = pcmCard; |
| info->pcmDevice = pcmDevice; |
| getAlsaParamInterval(params, SNDRV_PCM_HW_PARAM_SAMPLE_BITS, |
| &info->minSampleBits, &info->maxSampleBits); |
| getAlsaParamInterval(params, SNDRV_PCM_HW_PARAM_CHANNELS, |
| &info->minChannelCount, &info->maxChannelCount); |
| getAlsaParamInterval(params, SNDRV_PCM_HW_PARAM_RATE, |
| &info->minSampleRate, &info->maxSampleRate); |
| |
| // Ugly hack to recognize Remote mic and mark it for voice recognition |
| info->forVoiceRecognition = false; |
| len = s_get_alsa_card_name(cardName, sizeof(cardName), pcmCard); |
| ALOGD("AudioHotplugThread get_alsa_card_name returned %d, %s", len, cardName); |
| if (len > 0) { |
| if (strcmp(ANDROID_TV_REMOTE_AUDIO_DEVICE_NAME, cardName) == 0) { |
| ALOGD("AudioHotplugThread found Android TV remote mic on Card %d, for VOICE_RECOGNITION", pcmCard); |
| info->forVoiceRecognition = true; |
| } |
| } |
| |
| result = true; |
| |
| done: |
| if (alsaFD != -1) { |
| close(alsaFD); |
| } |
| return result; |
| } |
| |
| // scan the ALSA device directory for a usable capture device |
| void AudioHotplugThread::scanForDevice() |
| { |
| DIR* alsaDir; |
| DeviceInfo deviceInfo; |
| |
| alsaDir = opendir(kAlsaDeviceDir); |
| if (alsaDir == NULL) |
| return; |
| |
| while (true) { |
| struct dirent entry, *result; |
| int ret = readdir_r(alsaDir, &entry, &result); |
| if (ret != 0 || result == NULL) |
| break; |
| unsigned int pcmCard, pcmDevice; |
| if (parseCaptureDeviceName(entry.d_name, &pcmCard, &pcmDevice)) { |
| if (getDeviceInfo(pcmCard, pcmDevice, &deviceInfo)) { |
| mCallback.onDeviceFound(deviceInfo); |
| } |
| } |
| } |
| |
| closedir(alsaDir); |
| } |
| |
| bool AudioHotplugThread::threadLoop() |
| { |
| int inotifyFD = -1; |
| int watchFD = -1; |
| int flags; |
| |
| // watch for changes to the ALSA device directory |
| inotifyFD = inotify_init(); |
| if (inotifyFD == -1) { |
| ALOGE("AudioHotplugThread: inotify_init failed"); |
| goto done; |
| } |
| flags = fcntl(inotifyFD, F_GETFL, 0); |
| if (flags == -1) { |
| ALOGE("AudioHotplugThread: F_GETFL failed"); |
| goto done; |
| } |
| if (fcntl(inotifyFD, F_SETFL, flags | O_NONBLOCK) == -1) { |
| ALOGE("AudioHotplugThread: F_SETFL failed"); |
| goto done; |
| } |
| |
| watchFD = inotify_add_watch(inotifyFD, kAlsaDeviceDir, |
| IN_CREATE | IN_DELETE); |
| if (watchFD == -1) { |
| ALOGE("AudioHotplugThread: inotify_add_watch failed"); |
| goto done; |
| } |
| |
| // check for any existing capture devices |
| scanForDevice(); |
| |
| while (!exitPending()) { |
| // wait for a change to the ALSA directory or a shutdown signal |
| struct pollfd fds[2] = { |
| { inotifyFD, POLLIN, 0 }, |
| { mShutdownEventFD, POLLIN, 0 } |
| }; |
| int ret = poll(fds, NELEM(fds), -1); |
| if (ret == -1) { |
| ALOGE("AudioHotplugThread: poll failed"); |
| break; |
| } else if (fds[1].revents & POLLIN) { |
| // shutdown requested |
| break; |
| } |
| |
| if (!(fds[0].revents & POLLIN)) { |
| continue; |
| } |
| |
| // parse the filesystem change events |
| char eventBuf[256]; |
| ret = read(inotifyFD, eventBuf, sizeof(eventBuf)); |
| if (ret == -1) { |
| ALOGE("AudioHotplugThread: read failed"); |
| break; |
| } |
| |
| for (int i = 0; i < ret;) { |
| if ((ret - i) < (int)sizeof(struct inotify_event)) { |
| ALOGE("AudioHotplugThread: read an invalid inotify_event"); |
| break; |
| } |
| |
| struct inotify_event *event = |
| reinterpret_cast<struct inotify_event*>(eventBuf + i); |
| |
| if ((ret - i) < (int)(sizeof(struct inotify_event) + event->len)) { |
| ALOGE("AudioHotplugThread: read a bad inotify_event length"); |
| break; |
| } |
| |
| char *name = ((char *) event) + |
| offsetof(struct inotify_event, name); |
| |
| unsigned int pcmCard, pcmDevice; |
| if (parseCaptureDeviceName(name, &pcmCard, &pcmDevice)) { |
| if (event->mask & IN_CREATE) { |
| // Some devices can not be opened immediately after the |
| // inotify event occurs. Add a delay to avoid these |
| // races. (50ms was chosen arbitrarily) |
| const int kOpenTimeoutMs = 50; |
| struct pollfd pfd = {mShutdownEventFD, POLLIN, 0}; |
| if (poll(&pfd, 1, kOpenTimeoutMs) == -1) { |
| ALOGE("AudioHotplugThread: poll failed"); |
| break; |
| } else if (pfd.revents & POLLIN) { |
| // shutdown requested |
| break; |
| } |
| |
| DeviceInfo deviceInfo; |
| if (getDeviceInfo(pcmCard, pcmDevice, &deviceInfo)) { |
| mCallback.onDeviceFound(deviceInfo); |
| } |
| } else if (event->mask & IN_DELETE) { |
| mCallback.onDeviceRemoved(pcmCard, pcmDevice); |
| } |
| } |
| |
| i += sizeof(struct inotify_event) + event->len; |
| } |
| } |
| |
| done: |
| if (watchFD != -1) { |
| inotify_rm_watch(inotifyFD, watchFD); |
| } |
| if (inotifyFD != -1) { |
| close(inotifyFD); |
| } |
| |
| return false; |
| } |
| |
| }; // namespace android |