rockbox/apps/playlist_viewer.c
Frank Gevaerts 92b392e722 Reload the current playlist after reboot even if it has ended. (FS#11644)
This is done to make reboot more transparent. If a playlist has ended, there should be no difference between the player doing nothing for ten minutes and the player shutting down after the idle timeout and being restarted.


git-svn-id: svn://svn.rockbox.org/rockbox/trunk@30226 a1c6a512-1295-4272-9138-f99709370657
2011-07-31 12:56:00 +00:00

915 lines
29 KiB
C

/***************************************************************************
*
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2003 Hardeep Sidhu
*
* 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.
*
****************************************************************************/
/*
* Kevin Ferrare 2005/10/16
* multi-screen support, rewrote a lot of code
*/
#include <string.h>
#include "playlist.h"
#include "audio.h"
#include "screens.h"
#include "settings.h"
#include "icons.h"
#include "menu.h"
#include "plugin.h"
#include "keyboard.h"
#include "filetypes.h"
#include "onplay.h"
#include "talk.h"
#include "misc.h"
#include "action.h"
#include "debug.h"
#include "backlight.h"
#include "lang.h"
#include "playlist_viewer.h"
#include "playlist_catalog.h"
#include "icon.h"
#include "list.h"
#include "splash.h"
#include "playlist_menu.h"
#include "yesno.h"
/* Maximum number of tracks we can have loaded at one time */
#define MAX_PLAYLIST_ENTRIES 200
/* The number of items between the selected one and the end/start of
* the buffer under which the buffer must reload */
#define MIN_BUFFER_MARGIN (screens[0].getnblines()+1)
/* Information about a specific track */
struct playlist_entry {
char *name; /* Formatted track name */
int index; /* Playlist index */
int display_index; /* Display index */
bool queued; /* Is track queued? */
bool skipped; /* Is track marked as bad? */
};
enum direction
{
FORWARD,
BACKWARD
};
struct playlist_buffer
{
char *name_buffer; /* Buffer used to store track names */
int buffer_size; /* Size of name buffer */
int first_index; /* Real index of first track loaded inside
the buffer */
enum direction direction; /* Direction of the buffer (if the buffer
was loaded BACKWARD, the last track in
the buffer has a real index < to the
real index of the the first track)*/
struct playlist_entry tracks[MAX_PLAYLIST_ENTRIES];
int num_loaded; /* Number of track entries loaded in buffer */
};
/* Global playlist viewer settings */
struct playlist_viewer {
struct playlist_info* playlist; /* playlist being viewed */
int num_tracks; /* Number of tracks in playlist */
int current_playing_track; /* Index of current playing track */
int selected_track; /* The selected track, relative (first is 0) */
int moving_track; /* The track to move, relative (first is 0)
or -1 if nothing is currently being moved */
int moving_playlist_index; /* Playlist-relative index (as opposed to
viewer-relative index) of moving track */
struct playlist_buffer buffer;
};
static struct playlist_viewer viewer;
/* Used when viewing playlists on disk */
static struct playlist_info temp_playlist;
static void playlist_buffer_init(struct playlist_buffer *pb, char *names_buffer,
int names_buffer_size);
static void playlist_buffer_load_entries(struct playlist_buffer * pb, int index,
enum direction direction);
static int playlist_entry_load(struct playlist_entry *entry, int index,
char* name_buffer, int remaining_size);
static struct playlist_entry * playlist_buffer_get_track(struct playlist_buffer *pb,
int index);
static bool playlist_viewer_init(struct playlist_viewer * viewer,
const char* filename, bool reload);
static void format_name(char* dest, const char* src);
static void format_line(const struct playlist_entry* track, char* str,
int len);
static bool update_playlist(bool force);
static int onplay_menu(int index);
static int save_playlist_func(void);
static void playlist_buffer_init(struct playlist_buffer *pb, char *names_buffer,
int names_buffer_size)
{
pb->name_buffer = names_buffer;
pb->buffer_size = names_buffer_size;
pb->first_index = 0;
pb->num_loaded = 0;
}
/*
* Loads the entries following 'index' in the playlist buffer
*/
static void playlist_buffer_load_entries(struct playlist_buffer *pb, int index,
enum direction direction)
{
int num_entries = viewer.num_tracks;
char* p = pb->name_buffer;
int remaining = pb->buffer_size;
int i;
pb->first_index = index;
if (num_entries > MAX_PLAYLIST_ENTRIES)
num_entries = MAX_PLAYLIST_ENTRIES;
for (i = 0; i < num_entries; i++)
{
int len = playlist_entry_load(&(pb->tracks[i]), index, p, remaining);
if (len < 0)
{
/* Out of name buffer space */
num_entries = i;
break;
}
p += len;
remaining -= len;
if(direction == FORWARD)
index++;
else
index--;
index += viewer.num_tracks;
index %= viewer.num_tracks;
}
pb->direction = direction;
pb->num_loaded = i;
}
static void playlist_buffer_load_entries_screen(struct playlist_buffer * pb,
enum direction direction)
{
if (direction == FORWARD)
{
int min_start = viewer.selected_track-2*screens[0].getnblines();
while (min_start < 0)
min_start += viewer.num_tracks;
min_start %= viewer.num_tracks;
playlist_buffer_load_entries(pb, min_start, FORWARD);
}
else
{
int max_start = viewer.selected_track+2*screens[0].getnblines();
max_start %= viewer.num_tracks;
playlist_buffer_load_entries(pb, max_start, BACKWARD);
}
}
static int playlist_entry_load(struct playlist_entry *entry, int index,
char* name_buffer, int remaining_size)
{
struct playlist_track_info info;
int len;
/* Playlist viewer orders songs based on display index. We need to
convert to real playlist index to access track */
index = (index + playlist_get_first_index(viewer.playlist)) %
viewer.num_tracks;
if (playlist_get_track_info(viewer.playlist, index, &info) < 0)
return -1;
len = strlen(info.filename) + 1;
if (len <= remaining_size)
{
strcpy(name_buffer, info.filename);
entry->name = name_buffer;
entry->index = info.index;
entry->display_index = info.display_index;
entry->queued = info.attr & PLAYLIST_ATTR_QUEUED;
entry->skipped = info.attr & PLAYLIST_ATTR_SKIPPED;
return len;
}
return -1;
}
static int playlist_buffer_get_index(struct playlist_buffer *pb, int index)
{
int buffer_index;
if (pb->direction == FORWARD)
{
if (index >= pb->first_index)
buffer_index = index-pb->first_index;
else /* rotation : track0 in buffer + requested track */
buffer_index = viewer.num_tracks-pb->first_index+index;
}
else
{
if (index <= pb->first_index)
buffer_index = pb->first_index-index;
else /* rotation : track0 in buffer + dist from the last track
to the requested track (num_tracks-requested track) */
buffer_index = pb->first_index+viewer.num_tracks-index;
}
return buffer_index;
}
#define distance(a, b) \
a>b? (a) - (b) : (b) - (a)
static bool playlist_buffer_needs_reload(struct playlist_buffer* pb,
int track_index)
{
if (pb->num_loaded == viewer.num_tracks)
return false;
int selected_index = playlist_buffer_get_index(pb, track_index);
int first_buffer_index = playlist_buffer_get_index(pb, pb->first_index);
int distance_beginning = distance(selected_index, first_buffer_index);
if (distance_beginning < MIN_BUFFER_MARGIN)
return true;
if (pb->num_loaded - distance_beginning < MIN_BUFFER_MARGIN)
return true;
return false;
}
static struct playlist_entry * playlist_buffer_get_track(struct playlist_buffer *pb,
int index)
{
int buffer_index = playlist_buffer_get_index(pb, index);
return &(pb->tracks[buffer_index]);
}
/* Initialize the playlist viewer. */
static bool playlist_viewer_init(struct playlist_viewer * viewer,
const char* filename, bool reload)
{
char* buffer;
size_t buffer_size;
bool is_playing = audio_status() & (AUDIO_STATUS_PLAY | AUDIO_STATUS_PAUSE);
bool have_list = filename || is_playing;
if (!have_list)
{
/* Try to restore the list from control file */
have_list = (playlist_resume() != -1);
}
if (!have_list && (playlist_amount() > 0))
{
/*If dynamic playlist still exists, view it anyway even
if playback has reached the end of the playlist */
have_list = true;
}
if (!have_list)
{
/* Nothing to view, exit */
splash(HZ, str(LANG_CATALOG_NO_PLAYLISTS));
return false;
}
buffer = plugin_get_buffer(&buffer_size);
if (!buffer)
return false;
if (!filename)
viewer->playlist = NULL;
else
{
/* Viewing playlist on disk */
const char *dir, *file;
char *temp_ptr;
char *index_buffer = NULL;
ssize_t index_buffer_size = 0;
viewer->playlist = &temp_playlist;
/* Separate directory from filename */
temp_ptr = strrchr(filename+1,'/');
if (temp_ptr)
{
*temp_ptr = 0;
dir = filename;
file = temp_ptr + 1;
}
else
{
dir = "/";
file = filename+1;
}
if (is_playing)
{
/* Something is playing, use half the plugin buffer for playlist
indices */
index_buffer_size = buffer_size / 2;
index_buffer = buffer;
}
playlist_create_ex(viewer->playlist, dir, file, index_buffer,
index_buffer_size, buffer+index_buffer_size,
buffer_size-index_buffer_size);
if (temp_ptr)
*temp_ptr = '/';
buffer += index_buffer_size;
buffer_size -= index_buffer_size;
}
playlist_buffer_init(&viewer->buffer, buffer, buffer_size);
viewer->moving_track = -1;
viewer->moving_playlist_index = -1;
if (!reload)
{
if (viewer->playlist)
viewer->selected_track = 0;
else
viewer->selected_track = playlist_get_display_index() - 1;
}
if (!update_playlist(true))
return false;
return true;
}
/* Format trackname for display purposes */
static void format_name(char* dest, const char* src)
{
switch (global_settings.playlist_viewer_track_display)
{
case 0:
default:
{
/* Only display the filename */
char* p = strrchr(src, '/');
strcpy(dest, p+1);
/* Remove the extension */
strrsplt(dest, '.');
break;
}
case 1:
/* Full path */
strcpy(dest, src);
break;
}
}
/* Format display line */
static void format_line(const struct playlist_entry* track, char* str,
int len)
{
char name[MAX_PATH];
char *skipped = "";
format_name(name, track->name);
if (track->skipped)
skipped = "(ERR) ";
if (global_settings.playlist_viewer_indices)
/* Display playlist index */
snprintf(str, len, "%d. %s%s", track->display_index, skipped, name);
else
snprintf(str, len, "%s%s", skipped, name);
}
/* Update playlist in case something has changed or forced */
static bool update_playlist(bool force)
{
if (!viewer.playlist)
playlist_get_resume_info(&viewer.current_playing_track);
else
viewer.current_playing_track = -1;
int nb_tracks = playlist_amount_ex(viewer.playlist);
force = force || nb_tracks != viewer.num_tracks;
if (force)
{
/* Reload tracks */
viewer.num_tracks = nb_tracks;
if (viewer.num_tracks <= 0)
{
global_status.resume_index = -1;
global_status.resume_offset = -1;
return false;
}
playlist_buffer_load_entries_screen(&viewer.buffer, FORWARD);
if (viewer.buffer.num_loaded <= 0)
{
global_status.resume_index = -1;
global_status.resume_offset = -1;
return false;
}
}
return true;
}
MENUITEM_FUNCTION(save_playlist_item, 0, ID2P(LANG_SAVE_DYNAMIC_PLAYLIST),
save_playlist_func, 0, NULL, Icon_NOICON);
/* Menu of playlist commands. Invoked via ON+PLAY on main viewer screen.
Returns -1 if USB attached, 0 if no playlist change, and 1 if playlist
changed. */
static int onplay_menu(int index)
{
int result, ret = 0;
struct playlist_entry * current_track =
playlist_buffer_get_track(&viewer.buffer, index);
MENUITEM_STRINGLIST(menu_items, ID2P(LANG_PLAYLIST), NULL,
ID2P(LANG_CURRENT_PLAYLIST), ID2P(LANG_CATALOG),
ID2P(LANG_REMOVE), ID2P(LANG_MOVE),
ID2P(LANG_SAVE_DYNAMIC_PLAYLIST));
bool current = (current_track->index == viewer.current_playing_track);
result = do_menu(&menu_items, NULL, NULL, false);
if (result == MENU_ATTACHED_USB)
{
ret = -1;
}
else if (result >= 0)
{
/* Abort current move */
viewer.moving_track = -1;
viewer.moving_playlist_index = -1;
switch (result)
{
case 0:
/* playlist */
onplay_show_playlist_menu(current_track->name);
ret = 0;
break;
case 1:
/* add to catalog */
onplay_show_playlist_cat_menu(current_track->name);
ret = 0;
break;
case 2:
/* delete track */
playlist_delete(viewer.playlist, current_track->index);
if (current)
{
if (playlist_amount_ex(viewer.playlist) <= 0)
audio_stop();
else
{
/* Start playing new track except if it's the lasttrack
track in the playlist and repeat mode is disabled */
current_track =
playlist_buffer_get_track(&viewer.buffer, index);
if (current_track->display_index!=viewer.num_tracks ||
global_settings.repeat_mode == REPEAT_ALL)
{
#if CONFIG_CODEC != SWCODEC
talk_buffer_steal(); /* will use the mp3 buffer */
#endif
audio_play(0);
viewer.current_playing_track = -1;
}
}
}
ret = 1;
break;
case 3:
/* move track */
viewer.moving_track = index;
viewer.moving_playlist_index = current_track->index;
ret = 0;
break;
case 4:
/* save playlist */
save_playlist_screen(viewer.playlist);
ret = 0;
break;
}
}
return ret;
}
/* Save playlist to disk */
static int save_playlist_func(void)
{
save_playlist_screen(viewer.playlist);
return 0;
}
/* View current playlist */
enum playlist_viewer_result playlist_viewer(void)
{
return playlist_viewer_ex(NULL);
}
static int get_track_num(struct playlist_viewer *local_viewer,
int selected_item)
{
if (local_viewer->moving_track >= 0)
{
if (local_viewer->selected_track == selected_item)
{
return local_viewer->moving_track;
}
else if (local_viewer->selected_track > selected_item
&& selected_item >= local_viewer->moving_track)
{
return selected_item+1; /* move down */
}
else if (local_viewer->selected_track < selected_item
&& selected_item <= local_viewer->moving_track)
{
return selected_item-1; /* move up */
}
}
return selected_item;
}
static const char* playlist_callback_name(int selected_item,
void *data,
char *buffer,
size_t buffer_len)
{
struct playlist_viewer *local_viewer = (struct playlist_viewer *)data;
int track_num = get_track_num(local_viewer, selected_item);
struct playlist_entry *track =
playlist_buffer_get_track(&(local_viewer->buffer), track_num);
format_line(track, buffer, buffer_len);
return(buffer);
}
static enum themable_icons playlist_callback_icons(int selected_item,
void *data)
{
struct playlist_viewer *local_viewer = (struct playlist_viewer *)data;
int track_num = get_track_num(local_viewer, selected_item);
struct playlist_entry *track =
playlist_buffer_get_track(&(local_viewer->buffer), track_num);
if (track->index == local_viewer->current_playing_track)
{
/* Current playing track */
return Icon_Audio;
}
else if (track->index == local_viewer->moving_playlist_index)
{
/* Track we are moving */
return Icon_Moving;
}
else if (track->queued)
{
/* Queued track */
return Icon_Queued;
}
else
return Icon_NOICON;
}
static int playlist_callback_voice(int selected_item, void *data)
{
struct playlist_viewer *local_viewer = (struct playlist_viewer *)data;
int track_num = get_track_num(local_viewer, selected_item);
struct playlist_entry *track =
playlist_buffer_get_track(&(local_viewer->buffer), track_num);
bool enqueue = false;
if (global_settings.talk_file_clip || global_settings.talk_file == 2)
{
if (global_settings.playlist_viewer_indices)
{
talk_number(track->display_index, false);
enqueue = true;
}
talk_file_or_spell(NULL, track->name, NULL, enqueue);
}
else if (global_settings.talk_file == 1) /* as numbers */
{
talk_id(VOICE_FILE, false);
talk_number(track->display_index, true);
}
return 0;
}
/* Main viewer function. Filename identifies playlist to be viewed. If NULL,
view current playlist. */
enum playlist_viewer_result playlist_viewer_ex(const char* filename)
{
enum playlist_viewer_result ret = PLAYLIST_VIEWER_OK;
bool exit = false; /* exit viewer */
int button;
bool dirty = false;
struct gui_synclist playlist_lists;
if (!playlist_viewer_init(&viewer, filename, false))
goto exit;
gui_synclist_init(&playlist_lists, playlist_callback_name,
&viewer, false, 1, NULL);
gui_synclist_set_voice_callback(&playlist_lists, playlist_callback_voice);
gui_synclist_set_icon_callback(&playlist_lists,
global_settings.playlist_viewer_icons?
&playlist_callback_icons:NULL);
gui_synclist_set_nb_items(&playlist_lists, viewer.num_tracks);
gui_synclist_set_title(&playlist_lists, str(LANG_PLAYLIST), Icon_Playlist);
gui_synclist_select_item(&playlist_lists, viewer.selected_track);
gui_synclist_draw(&playlist_lists);
gui_synclist_speak_item(&playlist_lists);
while (!exit)
{
int track;
if (global_status.resume_index != -1 && !viewer.playlist)
playlist_get_resume_info(&track);
else
track = -1;
if (track != viewer.current_playing_track ||
playlist_amount_ex(viewer.playlist) != viewer.num_tracks)
{
/* Playlist has changed (new track started?) */
if (!update_playlist(false))
goto exit;
/*Needed because update_playlist gives wrong value when
playing is stopped*/
viewer.current_playing_track = track;
gui_synclist_set_nb_items(&playlist_lists, viewer.num_tracks);
gui_synclist_draw(&playlist_lists);
}
/* Timeout so we can determine if play status has changed */
bool res = list_do_action(CONTEXT_TREE, HZ/2,
&playlist_lists, &button, LIST_WRAP_UNLESS_HELD);
/* during moving, another redraw is going to be needed,
* since viewer.selected_track is updated too late (after the first draw)
* drawing the moving item needs it */
viewer.selected_track=gui_synclist_get_sel_pos(&playlist_lists);
if (res)
{
bool reload = playlist_buffer_needs_reload(&viewer.buffer,
viewer.selected_track);
if (reload)
playlist_buffer_load_entries_screen(&viewer.buffer,
button == ACTION_STD_NEXT ? FORWARD : BACKWARD);
if (reload || viewer.moving_track >= 0)
gui_synclist_draw(&playlist_lists);
}
switch (button)
{
case ACTION_TREE_WPS:
case ACTION_STD_CANCEL:
{
if (viewer.moving_track >= 0)
{
viewer.selected_track = viewer.moving_track;
gui_synclist_select_item(&playlist_lists, viewer.moving_track);
viewer.moving_track = -1;
viewer.moving_playlist_index = -1;
gui_synclist_draw(&playlist_lists);
}
else
{
exit = true;
ret = PLAYLIST_VIEWER_CANCEL;
}
break;
}
case ACTION_STD_OK:
{
struct playlist_entry * current_track =
playlist_buffer_get_track(&viewer.buffer,
viewer.selected_track);
if (viewer.moving_track >= 0)
{
/* Move track */
int ret_val;
ret_val = playlist_move(viewer.playlist,
viewer.moving_playlist_index,
current_track->index);
if (ret_val < 0)
splashf(HZ, (unsigned char *)"%s %s", str(LANG_MOVE),
str(LANG_FAILED));
update_playlist(true);
viewer.moving_track = -1;
viewer.moving_playlist_index = -1;
dirty = true;
}
else if (!viewer.playlist)
{
/* play new track */
if (!global_settings.party_mode)
{
playlist_start(current_track->index, 0);
update_playlist(false);
}
}
else if (!global_settings.party_mode)
{
int start_index = current_track->index;
if (!warn_on_pl_erase())
{
gui_synclist_draw(&playlist_lists);
break;
}
/* New playlist */
if (playlist_set_current(viewer.playlist) < 0)
goto exit;
if (global_settings.playlist_shuffle)
start_index = playlist_shuffle(current_tick, start_index);
playlist_start(start_index, 0);
/* Our playlist is now the current list */
if (!playlist_viewer_init(&viewer, NULL, true))
goto exit;
exit = true;
}
gui_synclist_draw(&playlist_lists);
break;
}
case ACTION_STD_CONTEXT:
{
/* ON+PLAY menu */
int ret_val;
ret_val = onplay_menu(viewer.selected_track);
if (ret_val < 0)
{
ret = PLAYLIST_VIEWER_USB;
goto exit;
}
else if (ret_val > 0)
{
/* Playlist changed */
gui_synclist_del_item(&playlist_lists);
update_playlist(true);
if (viewer.num_tracks <= 0)
exit = true;
if (viewer.selected_track >= viewer.num_tracks)
viewer.selected_track = viewer.num_tracks-1;
dirty = true;
}
gui_synclist_draw(&playlist_lists);
break;
}
case ACTION_STD_MENU:
ret = PLAYLIST_VIEWER_MAINMENU;
goto exit;
default:
if(default_event_handler(button) == SYS_USB_CONNECTED)
{
ret = PLAYLIST_VIEWER_USB;
goto exit;
}
break;
}
}
exit:
if (viewer.playlist)
{
if(dirty && yesno_pop(ID2P(LANG_SAVE_CHANGES)))
save_playlist_screen(viewer.playlist);
playlist_close(viewer.playlist);
}
return ret;
}
static const char* playlist_search_callback_name(int selected_item, void * data,
char *buffer, size_t buffer_len)
{
(void)buffer_len; /* this should probably be used */
int *found_indicies = (int*)data;
static struct playlist_track_info track;
playlist_get_track_info(viewer.playlist, found_indicies[selected_item], &track);
format_name(buffer, track.filename);
return buffer;
}
bool search_playlist(void)
{
char search_str[32] = "";
bool ret = false, exit = false;
int i, playlist_count;
int found_indicies[MAX_PLAYLIST_ENTRIES];
int found_indicies_count = 0, last_found_count = -1;
int button;
struct gui_synclist playlist_lists;
struct playlist_track_info track;
if (!playlist_viewer_init(&viewer, 0, false))
return ret;
if (kbd_input(search_str, sizeof(search_str)) < 0)
return ret;
lcd_clear_display();
playlist_count = playlist_amount_ex(viewer.playlist);
cpu_boost(true);
for (i = 0; i < playlist_count &&
found_indicies_count < MAX_PLAYLIST_ENTRIES; i++)
{
if (found_indicies_count != last_found_count)
{
splashf(0, str(LANG_PLAYLIST_SEARCH_MSG), found_indicies_count,
str(LANG_OFF_ABORT));
last_found_count = found_indicies_count;
}
if (action_userabort(TIMEOUT_NOBLOCK))
break;
playlist_get_track_info(viewer.playlist, i, &track);
if (strcasestr(track.filename,search_str))
found_indicies[found_indicies_count++] = track.index;
yield();
}
cpu_boost(false);
if (!found_indicies_count)
{
return ret;
}
backlight_on();
gui_synclist_init(&playlist_lists, playlist_search_callback_name,
found_indicies, false, 1, NULL);
gui_synclist_set_title(&playlist_lists, str(LANG_SEARCH_RESULTS), NOICON);
gui_synclist_set_icon_callback(&playlist_lists, NULL);
gui_synclist_set_nb_items(&playlist_lists, found_indicies_count);
gui_synclist_select_item(&playlist_lists, 0);
gui_synclist_draw(&playlist_lists);
while (!exit)
{
if (list_do_action(CONTEXT_LIST, HZ/4,
&playlist_lists, &button, LIST_WRAP_UNLESS_HELD))
continue;
switch (button)
{
case ACTION_STD_CANCEL:
exit = true;
break;
case ACTION_STD_OK:
{
int sel = gui_synclist_get_sel_pos(&playlist_lists);
playlist_start(found_indicies[sel], 0);
exit = 1;
}
break;
default:
if (default_event_handler(button) == SYS_USB_CONNECTED)
{
ret = true;
exit = true;
}
break;
}
}
return ret;
}