A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2022 Aidan MacDonald
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
19 *
20 ****************************************************************************/
21
22#include "plugin.h"
23
24/* should be more than enough */
25#define MAX_ROOTS 128
26
27static enum plugin_status plugin_status = PLUGIN_OK;
28static char tmpbuf[MAX_PATH+1];
29static char tmpbuf2[MAX_PATH+1];
30static char cur_root[MAX_PATH];
31static char roots[MAX_ROOTS][MAX_PATH];
32static int nroots;
33
34/* Read a redirect file and return the path */
35static char* read_redirect_file(const char* filename)
36{
37 int fd = rb->open(filename, O_RDONLY);
38 if(fd < 0)
39 return NULL;
40
41 ssize_t ret = rb->read(fd, tmpbuf, sizeof(tmpbuf));
42 rb->close(fd);
43 if(ret < 0 || (size_t)ret >= sizeof(tmpbuf))
44 return NULL;
45
46 /* relative paths are ignored */
47 if(tmpbuf[0] != '/')
48 ret = 0;
49
50 /* remove trailing control chars (newlines etc.) */
51 for(ssize_t i = ret - 1; i >= 0 && tmpbuf[i] < 0x20; --i)
52 tmpbuf[i] = '\0';
53
54 return tmpbuf;
55}
56
57/* Search for a redirect file, like get_redirect_dir() */
58static const char* read_redirect(void)
59{
60 for(int vol = NUM_VOLUMES-1; vol >= MULTIBOOT_MIN_VOLUME; --vol) {
61 rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR);
62 const char* path = read_redirect_file(tmpbuf);
63 if(path && path[0] == '/') {
64 /* prepend the volume because that's what we expect */
65 rb->snprintf(tmpbuf2, sizeof(tmpbuf2), "/<%d>%s", vol, path);
66 return tmpbuf2;
67 }
68 }
69
70 tmpbuf[0] = '\0';
71 return tmpbuf;
72}
73
74/* Remove all redirect files except for the one on volume 'exclude_vol' */
75static int clear_redirect(int exclude_vol)
76{
77 int ret = 0;
78 for(int vol = MULTIBOOT_MIN_VOLUME; vol < NUM_VOLUMES; ++vol) {
79 if(vol == exclude_vol)
80 continue;
81
82 rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR);
83 if(rb->file_exists(tmpbuf) && rb->remove(tmpbuf) < 0)
84 ret = -1;
85 }
86
87 return ret;
88}
89
90/* Save a path to the redirect file */
91static int write_redirect(const char* abspath)
92{
93 /* get the volume (required) */
94 const char* path = abspath;
95 int vol = rb->path_strip_volume(abspath, &path, false);
96 if(path == abspath)
97 return -1;
98
99 /* remove all other redirect files */
100 if(clear_redirect(vol))
101 return -1;
102
103 /* open the redirect file on the same volume */
104 rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR);
105 int fd = rb->open(tmpbuf, O_WRONLY|O_CREAT|O_TRUNC, 0644);
106 if(fd < 0)
107 return fd;
108
109 size_t pathlen = rb->strlen(path);
110 ssize_t ret = rb->write(fd, path, pathlen);
111 if(ret < 0 || (size_t)ret != pathlen) {
112 rb->close(fd);
113 return -1;
114 }
115
116 ret = rb->write(fd, "\n", 1);
117 rb->close(fd);
118 if(ret != 1)
119 return -1;
120
121 return 0;
122}
123
124/* Check if the firmware file is valid
125 * TODO: this should at least check model number or something */
126static bool check_firmware(const char* path)
127{
128 return rb->file_exists(path);
129}
130
131static int root_compare(const void* a, const void* b)
132{
133 const char* as = a;
134 const char* bs = b;
135 return rb->strcmp(as, bs);
136}
137
138/* Scan the filesystem for possible redirect targets. To prevent this from
139 * taking too long we only check the directories in the root of each volume
140 * look check for a rockbox firmware binary underneath /dir/.rockbox. If it
141 * exists then we report /<vol#>/dir as a root. */
142static int find_roots(void)
143{
144 const char* bootdir = *BOOTDIR == '/' ? &BOOTDIR[1] : BOOTDIR;
145 nroots = 0;
146
147 for(int vol = MULTIBOOT_MIN_VOLUME; vol < NUM_VOLUMES; ++vol) {
148 rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>", vol);
149
150 /* try to open the volume root; ignore failures since they'll
151 * occur if the volume is unmounted */
152 DIR* dir = rb->opendir(tmpbuf);
153 if(!dir)
154 continue;
155
156 struct dirent* ent;
157 while((ent = rb->readdir(dir))) {
158 /* skip non-directories */
159 if ((rb->dir_get_info(dir, ent).attribute & ATTR_DIRECTORY) == 0) {
160 continue;
161 }
162
163 const char *dname = ent->d_name;
164 /* check for bootdir in the root of the volume */
165 if (rb->strcmp(bootdir, dname) == 0) {
166 dname = "";
167 }
168
169 int r = rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s/%s/%s",
170 vol, dname, bootdir, BOOTFILE);
171
172 if(r < 0 || (size_t)r >= sizeof(tmpbuf))
173 continue;
174
175 if(check_firmware(tmpbuf)) {
176 rb->snprintf(roots[nroots], MAX_PATH, "/<%d>/%s",
177 vol, dname);
178 nroots += 1;
179
180 /* quit if we hit the maximum */
181 if(nroots == MAX_ROOTS) {
182 vol = NUM_VOLUMES;
183 break;
184 }
185 }
186 }
187
188 rb->closedir(dir);
189 }
190
191 rb->qsort(roots, nroots, MAX_PATH, root_compare);
192 return nroots;
193}
194
195static const char* picker_get_name_cb(int selected, void* data,
196 char* buffer, size_t buffer_len)
197{
198 (void)data;
199 (void)buffer;
200 (void)buffer_len;
201 return roots[selected];
202}
203
204static int picker_action_cb(int action, struct gui_synclist* lists)
205{
206 (void)lists;
207 if(action == ACTION_STD_OK)
208 action = ACTION_STD_CANCEL;
209 return action;
210}
211
212static bool show_picker_menu(int* selection)
213{
214 struct simplelist_info info;
215 rb->simplelist_info_init(&info, "Select new root", nroots, NULL);
216 info.selection = *selection;
217 info.get_name = picker_get_name_cb;
218 info.action_callback = picker_action_cb;
219 if(rb->simplelist_show_list(&info))
220 return true;
221
222 if(0 <= info.selection && info.selection < nroots)
223 *selection = info.selection;
224
225 return false;
226}
227
228enum {
229 MB_SELECT_ROOT,
230 MB_CLEAR_REDIRECT,
231 MB_CURRENT_ROOT,
232 MB_SAVE_AND_EXIT,
233 MB_SAVE_AND_REBOOT,
234 MB_EXIT,
235 MB_NUM_ITEMS,
236};
237
238static const char* menu_get_name_cb(int selected, void* data,
239 char* buffer, size_t buffer_len)
240{
241 (void)data;
242
243 switch(selected) {
244 case MB_SELECT_ROOT:
245 return "Select root";
246
247 case MB_CLEAR_REDIRECT:
248 return "Clear redirect";
249
250 case MB_CURRENT_ROOT:
251 if(cur_root[0]) {
252 rb->snprintf(buffer, buffer_len, "Using root: %s", cur_root);
253 return buffer;
254 }
255
256 return "Using default root";
257
258 case MB_SAVE_AND_EXIT:
259 return "Save and Exit";
260
261 case MB_SAVE_AND_REBOOT:
262 return "Save and Reboot";
263
264 case MB_EXIT:
265 default:
266 return "Exit";
267 }
268}
269
270static int menu_action_cb(int action, struct gui_synclist* lists)
271{
272 int selected = rb->gui_synclist_get_sel_pos(lists);
273
274 if(action != ACTION_STD_OK)
275 return action;
276
277 switch(selected) {
278 case MB_SELECT_ROOT: {
279 if(find_roots() <= 0) {
280 rb->splashf(3*HZ, "No roots found");
281 break;
282 }
283
284 int root_sel = nroots-1;
285 for(; root_sel > 0; --root_sel)
286 if(!rb->strcmp(roots[root_sel], cur_root))
287 break;
288
289 if(show_picker_menu(&root_sel))
290 return SYS_USB_CONNECTED;
291
292 rb->strcpy(cur_root, roots[root_sel]);
293 action = ACTION_REDRAW;
294 } break;
295
296 case MB_CLEAR_REDIRECT:
297 if(cur_root[0]) {
298 memset(cur_root, 0, sizeof(cur_root));
299 rb->splashf(HZ, "Cleared redirect");
300 }
301
302 action = ACTION_REDRAW;
303 break;
304
305 case MB_SAVE_AND_REBOOT:
306 case MB_SAVE_AND_EXIT: {
307 int ret;
308 if(cur_root[0])
309 ret = write_redirect(cur_root);
310 else
311 ret = clear_redirect(-1);
312
313 if(ret < 0)
314 rb->splashf(3*HZ, "Couldn't save settings");
315 else
316 rb->splashf(HZ, "Settings saved");
317
318 action = ACTION_STD_CANCEL;
319 plugin_status = (ret < 0) ? PLUGIN_ERROR : PLUGIN_OK;
320
321 if(ret >= 0 && selected == MB_SAVE_AND_REBOOT)
322 rb->sys_reboot();
323 } break;
324
325 case MB_EXIT:
326 return ACTION_STD_CANCEL;
327
328 default:
329 action = ACTION_REDRAW;
330 break;
331 }
332
333 return action;
334}
335
336static bool show_menu(void)
337{
338 struct simplelist_info info;
339 rb->simplelist_info_init(&info, "Multiboot Settings", MB_NUM_ITEMS, NULL);
340 info.get_name = menu_get_name_cb;
341 info.action_callback = menu_action_cb;
342 return rb->simplelist_show_list(&info);
343}
344
345enum plugin_status plugin_start(const void* param)
346{
347 (void)param;
348
349#ifdef HAVE_TOUCHSCREEN
350 rb->touchscreen_set_mode(rb->global_settings->touch_mode);
351#endif
352
353 /* load the current root */
354 const char* myroot = read_redirect();
355 rb->strcpy(cur_root, myroot);
356
357 /* display the menu */
358 if(show_menu())
359 return PLUGIN_USB_CONNECTED;
360 else
361 return plugin_status;
362}