/* * Copyright (C) 2015 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 "radio_hw_stub" #define LOG_NDEBUG 0 #include <string.h> #include <stdlib.h> #include <errno.h> #include <pthread.h> #include <sys/prctl.h> #include <sys/time.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <cutils/log.h> #include <cutils/list.h> #include <system/radio.h> #include <system/radio_metadata.h> #include <hardware/hardware.h> #include <hardware/radio.h> static const radio_hal_properties_t hw_properties = { .class_id = RADIO_CLASS_AM_FM, .implementor = "The Android Open Source Project", .product = "Radio stub HAL", .version = "0.1", .serial = "0123456789", .num_tuners = 1, .num_audio_sources = 1, .supports_capture = false, .num_bands = 2, .bands = { { .type = RADIO_BAND_FM, .antenna_connected = false, .lower_limit = 87900, .upper_limit = 107900, .num_spacings = 1, .spacings = { 200 }, .fm = { .deemphasis = RADIO_DEEMPHASIS_75, .stereo = true, .rds = RADIO_RDS_US, .ta = false, .af = false, .ea = true, } }, { .type = RADIO_BAND_AM, .antenna_connected = true, .lower_limit = 540, .upper_limit = 1610, .num_spacings = 1, .spacings = { 10 }, .am = { .stereo = true, } } } }; static const radio_metadata_clock_t hw_clock = { .utc_seconds_since_epoch = 1234567890, .timezone_offset_in_minutes = (-8 * 60), }; struct stub_radio_tuner { struct radio_tuner interface; struct stub_radio_device *dev; radio_callback_t callback; void *cookie; radio_hal_band_config_t config; radio_program_info_t program; bool audio; pthread_t callback_thread; pthread_mutex_t lock; pthread_cond_t cond; struct listnode command_list; }; struct stub_radio_device { struct radio_hw_device device; struct stub_radio_tuner *tuner; pthread_mutex_t lock; }; typedef enum { CMD_EXIT, CMD_CONFIG, CMD_STEP, CMD_SCAN, CMD_TUNE, CMD_CANCEL, CMD_METADATA, CMD_ANNOUNCEMENTS, } thread_cmd_type_t; struct thread_command { struct listnode node; thread_cmd_type_t type; struct timespec ts; union { unsigned int param; radio_hal_band_config_t config; }; }; /* must be called with out->lock locked */ static int send_command_l(struct stub_radio_tuner *tuner, thread_cmd_type_t type, unsigned int delay_ms, void *param) { struct thread_command *cmd = (struct thread_command *)calloc(1, sizeof(struct thread_command)); struct timespec ts; if (cmd == NULL) return -ENOMEM; ALOGV("%s %d delay_ms %d", __func__, type, delay_ms); cmd->type = type; if (param != NULL) { if (cmd->type == CMD_CONFIG) { cmd->config = *(radio_hal_band_config_t *)param; ALOGV("%s CMD_CONFIG type %d", __func__, cmd->config.type); } else cmd->param = *(unsigned int *)param; } clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += delay_ms/1000; ts.tv_nsec += (delay_ms%1000) * 1000000; if (ts.tv_nsec >= 1000000000) { ts.tv_nsec -= 1000000000; ts.tv_sec += 1; } cmd->ts = ts; list_add_tail(&tuner->command_list, &cmd->node); pthread_cond_signal(&tuner->cond); return 0; } #define BITMAP_FILE_PATH "/data/misc/audioserver/android.png" static int add_bitmap_metadata(radio_metadata_t **metadata, radio_metadata_key_t key, const char *source) { int fd; ssize_t ret = 0; struct stat info; void *data = NULL; size_t size; fd = open(source, O_RDONLY); if (fd < 0) return -EPIPE; fstat(fd, &info); size = info.st_size; data = malloc(size); if (data == NULL) { ret = -ENOMEM; goto exit; } ret = read(fd, data, size); if (ret < 0) goto exit; ret = radio_metadata_add_raw(metadata, key, (const unsigned char *)data, size); exit: close(fd); free(data); ALOGE_IF(ret != 0, "%s error %d", __func__, ret); return (int)ret; } static int prepare_metadata(struct stub_radio_tuner *tuner, radio_metadata_t **metadata, bool program) { int ret = 0; char text[RADIO_STRING_LEN_MAX]; struct timespec ts; if (metadata == NULL) return -EINVAL; if (*metadata != NULL) radio_metadata_deallocate(*metadata); *metadata = NULL; ret = radio_metadata_allocate(metadata, tuner->program.channel, 0); if (ret != 0) return ret; if (program) { ret = radio_metadata_add_int(metadata, RADIO_METADATA_KEY_RBDS_PTY, 5); if (ret != 0) goto exit; ret = radio_metadata_add_text(metadata, RADIO_METADATA_KEY_RDS_PS, "RockBand"); if (ret != 0) goto exit; ret = add_bitmap_metadata(metadata, RADIO_METADATA_KEY_ICON, BITMAP_FILE_PATH); if (ret != 0) goto exit; ret = radio_metadata_add_clock(metadata, RADIO_METADATA_KEY_CLOCK, &hw_clock); if (ret != 0) goto exit; } else { ret = add_bitmap_metadata(metadata, RADIO_METADATA_KEY_ART, BITMAP_FILE_PATH); if (ret != 0) goto exit; } clock_gettime(CLOCK_REALTIME, &ts); snprintf(text, RADIO_STRING_LEN_MAX, "Artist %ld", ts.tv_sec % 10); ret = radio_metadata_add_text(metadata, RADIO_METADATA_KEY_ARTIST, text); if (ret != 0) goto exit; snprintf(text, RADIO_STRING_LEN_MAX, "Song %ld", ts.tv_nsec % 10); ret = radio_metadata_add_text(metadata, RADIO_METADATA_KEY_TITLE, text); if (ret != 0) goto exit; return 0; exit: radio_metadata_deallocate(*metadata); *metadata = NULL; return ret; } static void *callback_thread_loop(void *context) { struct stub_radio_tuner *tuner = (struct stub_radio_tuner *)context; struct timespec ts = {0, 0}; ALOGI("%s", __func__); prctl(PR_SET_NAME, (unsigned long)"sound trigger callback", 0, 0, 0); pthread_mutex_lock(&tuner->lock); // Fields which are used to toggle the state of traffic announcements and // ea announcements at random. They are access protected by tuner->lock. bool ea_state = false; while (true) { struct thread_command *cmd = NULL; struct listnode *item; struct listnode *tmp; struct timespec cur_ts; bool got_cancel = false; bool send_meta_data = false; if (list_empty(&tuner->command_list) || ts.tv_sec != 0) { ALOGV("%s SLEEPING", __func__); if (ts.tv_sec != 0) { ALOGV("%s SLEEPING with timeout", __func__); pthread_cond_timedwait(&tuner->cond, &tuner->lock, &ts); } else { ALOGV("%s SLEEPING forever", __func__); pthread_cond_wait(&tuner->cond, &tuner->lock); } ts.tv_sec = 0; ALOGV("%s RUNNING", __func__); } clock_gettime(CLOCK_REALTIME, &cur_ts); list_for_each_safe(item, tmp, &tuner->command_list) { cmd = node_to_item(item, struct thread_command, node); if (got_cancel && (cmd->type == CMD_STEP || cmd->type == CMD_SCAN || cmd->type == CMD_TUNE || cmd->type == CMD_METADATA || cmd->type == CMD_ANNOUNCEMENTS)) { list_remove(item); free(cmd); continue; } if ((cmd->ts.tv_sec < cur_ts.tv_sec) || ((cmd->ts.tv_sec == cur_ts.tv_sec) && (cmd->ts.tv_nsec < cur_ts.tv_nsec))) { radio_hal_event_t event; radio_metadata_t *metadata = NULL; event.type = RADIO_EVENT_HW_FAILURE; list_remove(item); ALOGV("%s processing command %d time %ld.%ld", __func__, cmd->type, cmd->ts.tv_sec, cmd->ts.tv_nsec); switch (cmd->type) { default: case CMD_EXIT: free(cmd); goto exit; case CMD_CONFIG: { tuner->config = cmd->config; event.type = RADIO_EVENT_CONFIG; event.config = tuner->config; ALOGV("%s CMD_CONFIG type %d low %d up %d", __func__, tuner->config.type, tuner->config.lower_limit, tuner->config.upper_limit); if (tuner->config.type == RADIO_BAND_FM) { ALOGV(" - stereo %d\n - rds %d\n - ta %d\n - af %d\n" " - ea %d\n", tuner->config.fm.stereo, tuner->config.fm.rds, tuner->config.fm.ta, tuner->config.fm.af, tuner->config.fm.ea); } else { ALOGV(" - stereo %d", tuner->config.am.stereo); } } break; case CMD_STEP: { int frequency; frequency = tuner->program.channel; if (cmd->param == RADIO_DIRECTION_UP) { frequency += tuner->config.spacings[0]; } else { frequency -= tuner->config.spacings[0]; } if (frequency > (int)tuner->config.upper_limit) { frequency = tuner->config.lower_limit; } if (frequency < (int)tuner->config.lower_limit) { frequency = tuner->config.upper_limit; } tuner->program.channel = frequency; tuner->program.tuned = (frequency / (tuner->config.spacings[0] * 5)) % 2; tuner->program.signal_strength = 20; if (tuner->config.type == RADIO_BAND_FM) tuner->program.stereo = false; else tuner->program.stereo = false; event.type = RADIO_EVENT_TUNED; event.info = tuner->program; } break; case CMD_SCAN: { int frequency; frequency = tuner->program.channel; if (cmd->param == RADIO_DIRECTION_UP) { frequency += tuner->config.spacings[0] * 25; } else { frequency -= tuner->config.spacings[0] * 25; } if (frequency > (int)tuner->config.upper_limit) { frequency = tuner->config.lower_limit; } if (frequency < (int)tuner->config.lower_limit) { frequency = tuner->config.upper_limit; } tuner->program.channel = (unsigned int)frequency; tuner->program.tuned = true; if (tuner->config.type == RADIO_BAND_FM) tuner->program.stereo = tuner->config.fm.stereo; else tuner->program.stereo = tuner->config.am.stereo; tuner->program.signal_strength = 50; event.type = RADIO_EVENT_TUNED; event.info = tuner->program; send_meta_data = true; } break; case CMD_TUNE: { tuner->program.channel = cmd->param; tuner->program.tuned = (tuner->program.channel / (tuner->config.spacings[0] * 5)) % 2; if (tuner->program.tuned) { prepare_metadata(tuner, &tuner->program.metadata, true); send_command_l(tuner, CMD_ANNOUNCEMENTS, 1000, NULL); } else { if (tuner->program.metadata != NULL) radio_metadata_deallocate(tuner->program.metadata); tuner->program.metadata = NULL; } tuner->program.signal_strength = 100; if (tuner->config.type == RADIO_BAND_FM) tuner->program.stereo = tuner->program.tuned ? tuner->config.fm.stereo : false; else tuner->program.stereo = tuner->program.tuned ? tuner->config.am.stereo : false; event.type = RADIO_EVENT_TUNED; event.info = tuner->program; send_meta_data = true; } break; case CMD_METADATA: { int ret = prepare_metadata(tuner, &metadata, false); if (ret == 0) { event.type = RADIO_EVENT_METADATA; event.metadata = metadata; } send_meta_data = true; } break; case CMD_CANCEL: { got_cancel = true; } break; // Fire emergency announcements if they are enabled in the config. Stub // implementation simply fires an announcement for 5 second // duration with a gap of 5 seconds. case CMD_ANNOUNCEMENTS: { ALOGV("In annoucements. %d %d %d\n", ea_state, tuner->config.type, tuner->config.fm.ea); if (tuner->config.type == RADIO_BAND_FM || tuner->config.type == RADIO_BAND_FM_HD) { if (ea_state) { ea_state = false; event.type = RADIO_EVENT_EA; } else if (tuner->config.fm.ea) { ea_state = true; event.type = RADIO_EVENT_EA; } event.on = ea_state; if (tuner->config.fm.ea) { send_command_l(tuner, CMD_ANNOUNCEMENTS, 5000, NULL); } } } break; } if (event.type != RADIO_EVENT_HW_FAILURE && tuner->callback != NULL) { pthread_mutex_unlock(&tuner->lock); tuner->callback(&event, tuner->cookie); pthread_mutex_lock(&tuner->lock); if (event.type == RADIO_EVENT_METADATA && metadata != NULL) { radio_metadata_deallocate(metadata); metadata = NULL; } } ALOGV("%s processed command %d", __func__, cmd->type); free(cmd); } else { if ((ts.tv_sec == 0) || (cmd->ts.tv_sec < ts.tv_sec) || ((cmd->ts.tv_sec == ts.tv_sec) && (cmd->ts.tv_nsec < ts.tv_nsec))) { ts.tv_sec = cmd->ts.tv_sec; ts.tv_nsec = cmd->ts.tv_nsec; } } } if (send_meta_data) { list_for_each_safe(item, tmp, &tuner->command_list) { cmd = node_to_item(item, struct thread_command, node); if (cmd->type == CMD_METADATA) { list_remove(item); free(cmd); } } send_command_l(tuner, CMD_METADATA, 1000, NULL); } } exit: pthread_mutex_unlock(&tuner->lock); ALOGV("%s Exiting", __func__); return NULL; } static int tuner_set_configuration(const struct radio_tuner *tuner, const radio_hal_band_config_t *config) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; int status = 0; ALOGI("%s stub_tuner %p", __func__, stub_tuner); pthread_mutex_lock(&stub_tuner->lock); if (config == NULL) { status = -EINVAL; goto exit; } send_command_l(stub_tuner, CMD_CANCEL, 0, NULL); send_command_l(stub_tuner, CMD_CONFIG, 500, (void *)config); exit: pthread_mutex_unlock(&stub_tuner->lock); return status; } static int tuner_get_configuration(const struct radio_tuner *tuner, radio_hal_band_config_t *config) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; int status = 0; struct listnode *item; radio_hal_band_config_t *src_config; ALOGI("%s stub_tuner %p", __func__, stub_tuner); pthread_mutex_lock(&stub_tuner->lock); src_config = &stub_tuner->config; if (config == NULL) { status = -EINVAL; goto exit; } list_for_each(item, &stub_tuner->command_list) { struct thread_command *cmd = node_to_item(item, struct thread_command, node); if (cmd->type == CMD_CONFIG) { src_config = &cmd->config; } } *config = *src_config; exit: pthread_mutex_unlock(&stub_tuner->lock); return status; } static int tuner_step(const struct radio_tuner *tuner, radio_direction_t direction, bool skip_sub_channel) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; ALOGI("%s stub_tuner %p direction %d, skip_sub_channel %d", __func__, stub_tuner, direction, skip_sub_channel); pthread_mutex_lock(&stub_tuner->lock); send_command_l(stub_tuner, CMD_STEP, 20, &direction); pthread_mutex_unlock(&stub_tuner->lock); return 0; } static int tuner_scan(const struct radio_tuner *tuner, radio_direction_t direction, bool skip_sub_channel) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; ALOGI("%s stub_tuner %p direction %d, skip_sub_channel %d", __func__, stub_tuner, direction, skip_sub_channel); pthread_mutex_lock(&stub_tuner->lock); send_command_l(stub_tuner, CMD_SCAN, 200, &direction); pthread_mutex_unlock(&stub_tuner->lock); return 0; } static int tuner_tune(const struct radio_tuner *tuner, unsigned int channel, unsigned int sub_channel) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; ALOGI("%s stub_tuner %p channel %d, sub_channel %d", __func__, stub_tuner, channel, sub_channel); pthread_mutex_lock(&stub_tuner->lock); if (channel < stub_tuner->config.lower_limit || channel > stub_tuner->config.upper_limit) { pthread_mutex_unlock(&stub_tuner->lock); ALOGI("%s channel out of range", __func__); return -EINVAL; } send_command_l(stub_tuner, CMD_TUNE, 100, &channel); pthread_mutex_unlock(&stub_tuner->lock); return 0; } static int tuner_cancel(const struct radio_tuner *tuner) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; ALOGI("%s stub_tuner %p", __func__, stub_tuner); pthread_mutex_lock(&stub_tuner->lock); send_command_l(stub_tuner, CMD_CANCEL, 0, NULL); pthread_mutex_unlock(&stub_tuner->lock); return 0; } static int tuner_get_program_information(const struct radio_tuner *tuner, radio_program_info_t *info) { struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; int status = 0; radio_metadata_t *metadata; ALOGI("%s stub_tuner %p", __func__, stub_tuner); pthread_mutex_lock(&stub_tuner->lock); if (info == NULL) { status = -EINVAL; goto exit; } metadata = info->metadata; *info = stub_tuner->program; info->metadata = metadata; if (metadata != NULL && stub_tuner->program.metadata != NULL) radio_metadata_add_metadata(&info->metadata, stub_tuner->program.metadata); exit: pthread_mutex_unlock(&stub_tuner->lock); return status; } static int rdev_get_properties(const struct radio_hw_device *dev, radio_hal_properties_t *properties) { struct stub_radio_device *rdev = (struct stub_radio_device *)dev; ALOGI("%s", __func__); if (properties == NULL) return -EINVAL; memcpy(properties, &hw_properties, sizeof(radio_hal_properties_t)); return 0; } static int rdev_open_tuner(const struct radio_hw_device *dev, const radio_hal_band_config_t *config, bool audio, radio_callback_t callback, void *cookie, const struct radio_tuner **tuner) { struct stub_radio_device *rdev = (struct stub_radio_device *)dev; int status = 0; ALOGI("%s rdev %p", __func__, rdev); pthread_mutex_lock(&rdev->lock); if (rdev->tuner != NULL) { status = -ENOSYS; goto exit; } if (config == NULL || callback == NULL || tuner == NULL) { status = -EINVAL; goto exit; } rdev->tuner = (struct stub_radio_tuner *)calloc(1, sizeof(struct stub_radio_tuner)); if (rdev->tuner == NULL) { status = -ENOMEM; goto exit; } rdev->tuner->interface.set_configuration = tuner_set_configuration; rdev->tuner->interface.get_configuration = tuner_get_configuration; rdev->tuner->interface.scan = tuner_scan; rdev->tuner->interface.step = tuner_step; rdev->tuner->interface.tune = tuner_tune; rdev->tuner->interface.cancel = tuner_cancel; rdev->tuner->interface.get_program_information = tuner_get_program_information; rdev->tuner->audio = audio; rdev->tuner->callback = callback; rdev->tuner->cookie = cookie; rdev->tuner->dev = rdev; pthread_mutex_init(&rdev->tuner->lock, (const pthread_mutexattr_t *) NULL); pthread_cond_init(&rdev->tuner->cond, (const pthread_condattr_t *) NULL); pthread_create(&rdev->tuner->callback_thread, (const pthread_attr_t *) NULL, callback_thread_loop, rdev->tuner); list_init(&rdev->tuner->command_list); pthread_mutex_lock(&rdev->tuner->lock); send_command_l(rdev->tuner, CMD_CONFIG, 500, (void *)config); pthread_mutex_unlock(&rdev->tuner->lock); *tuner = &rdev->tuner->interface; exit: pthread_mutex_unlock(&rdev->lock); ALOGI("%s DONE", __func__); return status; } static int rdev_close_tuner(const struct radio_hw_device *dev, const struct radio_tuner *tuner) { struct stub_radio_device *rdev = (struct stub_radio_device *)dev; struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner; int status = 0; ALOGI("%s tuner %p", __func__, tuner); pthread_mutex_lock(&rdev->lock); if (tuner == NULL) { status = -EINVAL; goto exit; } pthread_mutex_lock(&stub_tuner->lock); stub_tuner->callback = NULL; send_command_l(stub_tuner, CMD_EXIT, 0, NULL); pthread_mutex_unlock(&stub_tuner->lock); pthread_join(stub_tuner->callback_thread, (void **) NULL); if (stub_tuner->program.metadata != NULL) radio_metadata_deallocate(stub_tuner->program.metadata); free(stub_tuner); rdev->tuner = NULL; exit: pthread_mutex_unlock(&rdev->lock); return status; } static int rdev_close(hw_device_t *device) { struct stub_radio_device *rdev = (struct stub_radio_device *)device; if (rdev != NULL) { free(rdev->tuner); } free(rdev); return 0; } static int rdev_open(const hw_module_t* module, const char* name, hw_device_t** device) { struct stub_radio_device *rdev; int ret; if (strcmp(name, RADIO_HARDWARE_DEVICE) != 0) return -EINVAL; rdev = calloc(1, sizeof(struct stub_radio_device)); if (!rdev) return -ENOMEM; rdev->device.common.tag = HARDWARE_DEVICE_TAG; rdev->device.common.version = RADIO_DEVICE_API_VERSION_1_0; rdev->device.common.module = (struct hw_module_t *) module; rdev->device.common.close = rdev_close; rdev->device.get_properties = rdev_get_properties; rdev->device.open_tuner = rdev_open_tuner; rdev->device.close_tuner = rdev_close_tuner; pthread_mutex_init(&rdev->lock, (const pthread_mutexattr_t *) NULL); *device = &rdev->device.common; return 0; } static struct hw_module_methods_t hal_module_methods = { .open = rdev_open, }; struct radio_module HAL_MODULE_INFO_SYM = { .common = { .tag = HARDWARE_MODULE_TAG, .module_api_version = RADIO_MODULE_API_VERSION_1_0, .hal_api_version = HARDWARE_HAL_API_VERSION, .id = RADIO_HARDWARE_MODULE_ID, .name = "Stub radio HAL", .author = "The Android Open Source Project", .methods = &hal_module_methods, }, };