f6c719d7ec
replace applicable calls to strlcpy with calls to strmemccpy which null terminates on truncation in theory the strmemccpy calls should be slightly faster since they don't traverse the rest of the source string on truncation but I seriously doubt there is too much of that going on in the code base Change-Id: Ia0251514e36a6242bbf3f03c5e0df123aba60ed2
634 lines
17 KiB
C
634 lines
17 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
|
|
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
|
|
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
|
|
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|
* \/ \/ \/ \/ \/
|
|
* $Id$
|
|
*
|
|
* Copyright (C) 2003 Linus Nielsen Feltzing
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
* KIND, either express or implied.
|
|
*
|
|
****************************************************************************/
|
|
|
|
#include "config.h"
|
|
#include <stdio.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include "settings.h"
|
|
#include "rbpaths.h"
|
|
#include "general.h"
|
|
#include "radio.h"
|
|
#include "tuner.h"
|
|
#include "file.h"
|
|
#include "string-extra.h"
|
|
#include "misc.h"
|
|
#include "pathfuncs.h"
|
|
#include "lang.h"
|
|
#include "action.h"
|
|
#include "list.h"
|
|
#include "splash.h"
|
|
#include "menu.h"
|
|
#include "yesno.h"
|
|
#include "keyboard.h"
|
|
#include "talk.h"
|
|
#include "filetree.h"
|
|
#include "dir.h"
|
|
#include "presets.h"
|
|
|
|
static int curr_preset = -1;
|
|
|
|
extern int curr_freq; /* from radio.c.. naughty but meh */
|
|
extern int radio_mode;
|
|
int snap_freq_to_grid(int freq);
|
|
void remember_frequency(void);
|
|
|
|
#define MAX_PRESETS 64
|
|
static bool presets_loaded = false;
|
|
static bool presets_changed = false;
|
|
static struct fmstation presets[MAX_PRESETS];
|
|
|
|
static char filepreset[MAX_PATH]; /* preset filename variable */
|
|
|
|
static int num_presets = 0; /* The number of presets in the preset list */
|
|
|
|
int radio_current_preset(void)
|
|
{
|
|
return curr_preset;
|
|
}
|
|
int radio_preset_count(void)
|
|
{
|
|
return num_presets;
|
|
}
|
|
const struct fmstation *radio_get_preset(int preset)
|
|
{
|
|
return &presets[preset];
|
|
}
|
|
|
|
bool presets_have_changed(void)
|
|
{
|
|
return presets_changed;
|
|
}
|
|
|
|
|
|
/* Find a matching preset to freq */
|
|
int preset_find(int freq)
|
|
{
|
|
int i;
|
|
if(num_presets < 1)
|
|
return -1;
|
|
for(i = 0;i < MAX_PRESETS;i++)
|
|
{
|
|
if(freq == presets[i].frequency)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* Return the closest preset encountered in the search direction with
|
|
wraparound. */
|
|
static int find_closest_preset(int freq, int direction)
|
|
{
|
|
int i;
|
|
int lowpreset = 0;
|
|
int highpreset = 0;
|
|
int closest = -1;
|
|
|
|
if (direction == 0) /* direction == 0 isn't really used */
|
|
return 0;
|
|
|
|
for (i = 0; i < num_presets; i++)
|
|
{
|
|
int f = presets[i].frequency;
|
|
if (f == freq)
|
|
return i; /* Exact match = stop */
|
|
|
|
/* remember the highest and lowest presets for wraparound */
|
|
if (f < presets[lowpreset].frequency)
|
|
lowpreset = i;
|
|
if (f > presets[highpreset].frequency)
|
|
highpreset = i;
|
|
|
|
/* find the closest preset in the given direction */
|
|
if (direction > 0 && f > freq)
|
|
{
|
|
if (closest < 0 || f < presets[closest].frequency)
|
|
closest = i;
|
|
}
|
|
else if (direction < 0 && f < freq)
|
|
{
|
|
if (closest < 0 || f > presets[closest].frequency)
|
|
closest = i;
|
|
}
|
|
}
|
|
|
|
if (closest < 0)
|
|
{
|
|
/* no presets in the given direction */
|
|
/* wrap around depending on direction */
|
|
if (direction < 0)
|
|
closest = highpreset;
|
|
else
|
|
closest = lowpreset;
|
|
}
|
|
|
|
return closest;
|
|
}
|
|
|
|
void preset_next(int direction)
|
|
{
|
|
if (num_presets < 1)
|
|
return;
|
|
|
|
if (curr_preset == -1)
|
|
curr_preset = find_closest_preset(curr_freq, direction);
|
|
else
|
|
curr_preset = (curr_preset + direction + num_presets) % num_presets;
|
|
|
|
/* Must stay on the current grid for the region */
|
|
curr_freq = snap_freq_to_grid(presets[curr_preset].frequency);
|
|
|
|
tuner_set(RADIO_FREQUENCY, curr_freq);
|
|
remember_frequency();
|
|
}
|
|
|
|
void preset_set_current(int preset)
|
|
{
|
|
curr_preset = preset;
|
|
}
|
|
|
|
/* Speak a preset by number or by spelling its name, depending on settings. */
|
|
void preset_talk(int preset, bool fallback, bool enqueue)
|
|
{
|
|
if (global_settings.talk_file == 1) /* number */
|
|
talk_number(preset + 1, enqueue);
|
|
else
|
|
{ /* spell */
|
|
if(presets[preset].name[0])
|
|
talk_spell(presets[preset].name, enqueue);
|
|
else if(fallback)
|
|
talk_value_decimal(presets[preset].frequency, UNIT_INT, 6, enqueue);
|
|
}
|
|
}
|
|
|
|
void radio_save_presets(void)
|
|
{
|
|
int fd;
|
|
int i;
|
|
|
|
fd = creat(filepreset, 0666);
|
|
if(fd >= 0)
|
|
{
|
|
for(i = 0;i < num_presets;i++)
|
|
{
|
|
fdprintf(fd, "%d:%s\n", presets[i].frequency, presets[i].name);
|
|
}
|
|
close(fd);
|
|
|
|
if(!strncasecmp(FMPRESET_PATH, filepreset, strlen(FMPRESET_PATH)))
|
|
set_file(filepreset, global_settings.fmr_file, MAX_FILENAME);
|
|
presets_changed = false;
|
|
}
|
|
else
|
|
{
|
|
splash(HZ, ID2P(LANG_FM_PRESET_SAVE_FAILED));
|
|
}
|
|
}
|
|
|
|
void radio_load_presets(char *filename)
|
|
{
|
|
int fd;
|
|
int rc;
|
|
char buf[128];
|
|
char *freq;
|
|
char *name;
|
|
bool done = false;
|
|
int f;
|
|
|
|
memset(presets, 0, sizeof(presets));
|
|
num_presets = 0;
|
|
|
|
/* No Preset in configuration. */
|
|
if(filename[0] == '\0')
|
|
{
|
|
filepreset[0] = '\0';
|
|
return;
|
|
}
|
|
/* Temporary preset, loaded until player shuts down. */
|
|
else if(filename[0] == '/')
|
|
strmemccpy(filepreset, filename, sizeof(filepreset));
|
|
/* Preset from default directory. */
|
|
else
|
|
snprintf(filepreset, sizeof(filepreset), "%s/%s.fmr",
|
|
FMPRESET_PATH, filename);
|
|
|
|
fd = open_utf8(filepreset, O_RDONLY);
|
|
if(fd >= 0)
|
|
{
|
|
while(!done && num_presets < MAX_PRESETS)
|
|
{
|
|
rc = read_line(fd, buf, 128);
|
|
if(rc > 0)
|
|
{
|
|
if(settings_parseline(buf, &freq, &name))
|
|
{
|
|
f = atoi(freq);
|
|
if(f) /* For backwards compatibility */
|
|
{
|
|
struct fmstation * const fms = &presets[num_presets];
|
|
fms->frequency = f;
|
|
strmemccpy(fms->name, name, MAX_FMPRESET_LEN+1);
|
|
num_presets++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
done = true;
|
|
}
|
|
close(fd);
|
|
}
|
|
else /* invalid file name? */
|
|
filepreset[0] = '\0';
|
|
|
|
presets_loaded = num_presets > 0;
|
|
presets_changed = false;
|
|
}
|
|
|
|
const char* radio_get_preset_name(int preset)
|
|
{
|
|
if (preset < num_presets)
|
|
return presets[preset].name;
|
|
return NULL;
|
|
}
|
|
|
|
int handle_radio_add_preset(void)
|
|
{
|
|
char buf[MAX_FMPRESET_LEN + 1];
|
|
|
|
if(num_presets < MAX_PRESETS)
|
|
{
|
|
buf[0] = '\0';
|
|
|
|
if (!kbd_input(buf, MAX_FMPRESET_LEN + 1, NULL))
|
|
{
|
|
struct fmstation * const fms = &presets[num_presets];
|
|
strcpy(fms->name, buf);
|
|
fms->frequency = curr_freq;
|
|
num_presets++;
|
|
presets_changed = true;
|
|
presets_loaded = num_presets > 0;
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
splash(HZ, ID2P(LANG_FM_NO_FREE_PRESETS));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* needed to know which preset we are edit/delete-ing */
|
|
static int selected_preset = -1;
|
|
static int radio_edit_preset(void)
|
|
{
|
|
char buf[MAX_FMPRESET_LEN + 1];
|
|
|
|
if (num_presets > 0)
|
|
{
|
|
struct fmstation * const fms = &presets[selected_preset];
|
|
|
|
strcpy(buf, fms->name);
|
|
|
|
if (!kbd_input(buf, MAX_FMPRESET_LEN + 1, NULL))
|
|
{
|
|
strcpy(fms->name, buf);
|
|
presets_changed = true;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int radio_delete_preset(void)
|
|
{
|
|
if (num_presets > 0)
|
|
{
|
|
struct fmstation * const fms = &presets[selected_preset];
|
|
|
|
if (selected_preset >= --num_presets)
|
|
selected_preset = num_presets - 1;
|
|
|
|
memmove(fms, fms + 1, (uintptr_t)(fms + num_presets) -
|
|
(uintptr_t)fms);
|
|
|
|
if (curr_preset >= num_presets)
|
|
--curr_preset;
|
|
}
|
|
|
|
/* Don't ask to save when all presets are deleted. */
|
|
presets_changed = num_presets > 0;
|
|
|
|
if (!presets_changed)
|
|
{
|
|
/* The preset list will be cleared, switch to Scan Mode. */
|
|
radio_mode = RADIO_SCAN_MODE;
|
|
curr_preset = -1;
|
|
presets_loaded = false;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int preset_list_load(void)
|
|
{
|
|
char selected[MAX_PATH];
|
|
struct browse_context browse;
|
|
snprintf(selected, sizeof(selected), "%s.%s", global_settings.fmr_file, "fmr");
|
|
browse_context_init(&browse, SHOW_FMR, 0,
|
|
str(LANG_FM_PRESET_LOAD), NOICON,
|
|
FMPRESET_PATH, selected);
|
|
return !rockbox_browse(&browse);
|
|
}
|
|
|
|
int preset_list_save(void)
|
|
{
|
|
if(num_presets > 0)
|
|
{
|
|
bool bad_file_name = true;
|
|
|
|
if(!dir_exists(FMPRESET_PATH)) /* Check if there is preset folder */
|
|
mkdir(FMPRESET_PATH);
|
|
|
|
create_numbered_filename(filepreset, FMPRESET_PATH, "preset",
|
|
".fmr", 2 IF_CNFN_NUM_(, NULL));
|
|
|
|
while(bad_file_name)
|
|
{
|
|
if(!kbd_input(filepreset, sizeof(filepreset), NULL))
|
|
{
|
|
/* check the name: max MAX_FILENAME (20) chars */
|
|
char* p2;
|
|
char* p1;
|
|
int len;
|
|
p1 = strrchr(filepreset, '/');
|
|
p2 = p1;
|
|
while((p1) && (*p2) && (*p2 != '.'))
|
|
p2++;
|
|
len = (int)(p2-p1) - 1;
|
|
if((!p1) || (len > MAX_FILENAME) || (len == 0))
|
|
{
|
|
/* no slash, too long or too short */
|
|
splash(HZ, ID2P(LANG_INVALID_FILENAME));
|
|
}
|
|
else
|
|
{
|
|
/* add correct extension (easier to always write)
|
|
at this point, p2 points to 0 or the extension dot */
|
|
*p2 = '\0';
|
|
strcat(filepreset,".fmr");
|
|
bad_file_name = false;
|
|
radio_save_presets();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* user aborted */
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
splash(HZ, ID2P(LANG_FM_NO_PRESETS));
|
|
|
|
return true;
|
|
}
|
|
|
|
int preset_list_clear(void)
|
|
{
|
|
/* Clear all the preset entries */
|
|
memset(presets, 0, sizeof (presets));
|
|
|
|
num_presets = 0;
|
|
presets_loaded = false;
|
|
/* The preset list will be cleared switch to Scan Mode. */
|
|
radio_mode = RADIO_SCAN_MODE;
|
|
curr_preset = -1;
|
|
presets_changed = false; /* Don't ask to save when clearing the list. */
|
|
|
|
return true;
|
|
}
|
|
|
|
MENUITEM_FUNCTION(radio_edit_preset_item, MENU_FUNC_CHECK_RETVAL,
|
|
ID2P(LANG_FM_EDIT_PRESET),
|
|
radio_edit_preset, NULL, NULL, Icon_NOICON);
|
|
MENUITEM_FUNCTION(radio_delete_preset_item, MENU_FUNC_CHECK_RETVAL,
|
|
ID2P(LANG_FM_DELETE_PRESET),
|
|
radio_delete_preset, NULL, NULL, Icon_NOICON);
|
|
static int radio_preset_callback(int action,
|
|
const struct menu_item_ex *this_item,
|
|
struct gui_synclist *this_list)
|
|
{
|
|
if (action == ACTION_STD_OK)
|
|
action = ACTION_EXIT_AFTER_THIS_MENUITEM;
|
|
return action;
|
|
(void)this_item;
|
|
(void)this_list;
|
|
}
|
|
MAKE_MENU(handle_radio_preset_menu, ID2P(LANG_PRESET),
|
|
radio_preset_callback, Icon_NOICON, &radio_edit_preset_item,
|
|
&radio_delete_preset_item);
|
|
/* present a list of preset stations */
|
|
static const char* presets_get_name(int selected_item, void *data,
|
|
char *buffer, size_t buffer_len)
|
|
{
|
|
(void)data;
|
|
struct fmstation *p = &presets[selected_item];
|
|
if(p->name[0])
|
|
return p->name;
|
|
int freq = p->frequency / 10000;
|
|
int frac = freq % 100;
|
|
freq /= 100;
|
|
snprintf(buffer, buffer_len,
|
|
str(LANG_FM_DEFAULT_PRESET_NAME), freq, frac);
|
|
return buffer;
|
|
}
|
|
|
|
static int presets_speak_name(int selected_item, void * data)
|
|
{
|
|
(void)data;
|
|
preset_talk(selected_item, true, false);
|
|
return 0;
|
|
}
|
|
|
|
int handle_radio_presets(void)
|
|
{
|
|
struct gui_synclist lists;
|
|
int result = 0;
|
|
int action = ACTION_NONE;
|
|
|
|
if(presets_loaded == false)
|
|
return result;
|
|
|
|
gui_synclist_init(&lists, presets_get_name, NULL, false, 1, NULL);
|
|
gui_synclist_set_title(&lists, str(LANG_PRESET), NOICON);
|
|
gui_synclist_set_icon_callback(&lists, NULL);
|
|
if(global_settings.talk_file)
|
|
gui_synclist_set_voice_callback(&lists, presets_speak_name);
|
|
gui_synclist_set_nb_items(&lists, num_presets);
|
|
gui_synclist_select_item(&lists, curr_preset<0 ? 0 : curr_preset);
|
|
gui_synclist_speak_item(&lists);
|
|
|
|
while (result == 0)
|
|
{
|
|
gui_synclist_draw(&lists);
|
|
list_do_action(CONTEXT_STD, TIMEOUT_BLOCK, &lists, &action);
|
|
switch (action)
|
|
{
|
|
case ACTION_STD_MENU:
|
|
if (handle_radio_add_preset())
|
|
{
|
|
gui_synclist_set_nb_items(&lists, num_presets);
|
|
gui_synclist_select_item(&lists, num_presets - 1);
|
|
}
|
|
break;
|
|
case ACTION_STD_CANCEL:
|
|
result = 1;
|
|
break;
|
|
case ACTION_STD_OK:
|
|
curr_preset = gui_synclist_get_sel_pos(&lists);
|
|
curr_freq = presets[curr_preset].frequency;
|
|
next_station(0);
|
|
result = 1;
|
|
break;
|
|
case ACTION_STD_CONTEXT:
|
|
selected_preset = gui_synclist_get_sel_pos(&lists);
|
|
do_menu(&handle_radio_preset_menu, NULL, NULL, false);
|
|
gui_synclist_set_nb_items(&lists, num_presets);
|
|
gui_synclist_select_item(&lists, selected_preset);
|
|
gui_synclist_speak_item(&lists);
|
|
break;
|
|
default:
|
|
if(default_event_handler(action) == SYS_USB_CONNECTED)
|
|
result = 2;
|
|
}
|
|
}
|
|
return result - 1;
|
|
}
|
|
|
|
|
|
int presets_scan(void *viewports)
|
|
{
|
|
bool do_scan = true;
|
|
struct viewport *vp = (struct viewport *)viewports;
|
|
|
|
FOR_NB_SCREENS(i)
|
|
screens[i].set_viewport(vp?&vp[i]:NULL);
|
|
if(num_presets > 0) /* Do that to avoid 2 questions. */
|
|
do_scan = yesno_pop(ID2P(LANG_FM_CLEAR_PRESETS));
|
|
|
|
if(do_scan)
|
|
{
|
|
const struct fm_region_data * const fmr =
|
|
&fm_region_data[global_settings.fm_region];
|
|
|
|
curr_freq = fmr->freq_min;
|
|
num_presets = 0;
|
|
memset(presets, 0, sizeof(presets));
|
|
|
|
tuner_set(RADIO_MUTE, 1);
|
|
|
|
while(curr_freq <= fmr->freq_max)
|
|
{
|
|
int freq, frac;
|
|
if(num_presets >= MAX_PRESETS || action_userabort(TIMEOUT_NOBLOCK))
|
|
break;
|
|
|
|
freq = curr_freq / 10000;
|
|
frac = freq % 100;
|
|
freq /= 100;
|
|
|
|
splashf(0, str(LANG_FM_SCANNING), freq, frac);
|
|
|
|
if(tuner_set(RADIO_SCAN_FREQUENCY, curr_freq))
|
|
{
|
|
/* add preset */
|
|
presets[num_presets].name[0] = '\0';
|
|
presets[num_presets].frequency = curr_freq;
|
|
num_presets++;
|
|
}
|
|
|
|
curr_freq += fmr->freq_step;
|
|
}
|
|
|
|
if (get_radio_status() == FMRADIO_PLAYING)
|
|
tuner_set(RADIO_MUTE, 0);
|
|
|
|
presets_changed = true;
|
|
|
|
FOR_NB_SCREENS(i)
|
|
{
|
|
screens[i].clear_viewport();
|
|
screens[i].update_viewport();
|
|
}
|
|
|
|
if(num_presets > 0)
|
|
{
|
|
curr_freq = presets[0].frequency;
|
|
radio_mode = RADIO_PRESET_MODE;
|
|
presets_loaded = true;
|
|
next_station(0);
|
|
}
|
|
else
|
|
{
|
|
/* Wrap it to beginning or we'll be past end of band */
|
|
presets_loaded = false;
|
|
next_station(1);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void presets_save(void)
|
|
{
|
|
if(filepreset[0] == '\0')
|
|
preset_list_save();
|
|
else
|
|
radio_save_presets();
|
|
}
|
|
|
|
#if 0 /* disabled in draw_progressbar() */
|
|
static inline void draw_vertical_line_mark(struct screen * screen,
|
|
int x, int y, int h)
|
|
{
|
|
screen->set_drawmode(DRMODE_COMPLEMENT);
|
|
screen->vline(x, y, y+h-1);
|
|
}
|
|
|
|
/* draw the preset markers for a track of length "tracklen",
|
|
between (x,y) and (x+w,y) */
|
|
void presets_draw_markers(struct screen *screen,
|
|
int x, int y, int w, int h)
|
|
{
|
|
int i,xi;
|
|
const struct fm_region_data *region_data =
|
|
&(fm_region_data[global_settings.fm_region]);
|
|
int len = region_data->freq_max - region_data->freq_min;
|
|
for (i=0; i < radio_preset_count(); i++)
|
|
{
|
|
int freq = radio_get_preset(i)->frequency;
|
|
int diff = freq - region_data->freq_min;
|
|
xi = x + (w * diff)/len;
|
|
draw_vertical_line_mark(screen, xi, y, h);
|
|
}
|
|
}
|
|
#endif
|