A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 343 lines 13 kB view raw
1/*************************************************************************** 2 * __________ __ ___. 3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 * \/ \/ \/ \/ \/ 8 * $Id$ 9 * 10 * Copyright (C) 2010 Thomas Martitz 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 22package org.rockbox; 23 24import java.io.BufferedInputStream; 25import java.io.BufferedOutputStream; 26import java.io.File; 27import java.io.FileOutputStream; 28import java.io.OutputStreamWriter; 29import java.util.Enumeration; 30import java.util.zip.ZipEntry; 31import java.util.zip.ZipFile; 32import org.rockbox.Helper.Logger; 33import org.rockbox.Helper.MediaButtonReceiver; 34import org.rockbox.Helper.RunForegroundManager; 35import android.app.Activity; 36import android.app.Service; 37import android.content.Intent; 38import android.os.Bundle; 39import android.os.Environment; 40import android.os.IBinder; 41import android.os.ResultReceiver; 42import android.view.KeyEvent; 43 44/* This class is used as the main glue between java and c. 45 * All access should be done through RockboxService.get_instance() for safety. 46 */ 47 48public class RockboxService extends Service 49{ 50 /* this Service is really a singleton class - well almost. */ 51 private static RockboxService instance = null; 52 53 /* locals needed for the c code and Rockbox state */ 54 private static volatile boolean rockbox_running; 55 private Activity mCurrentActivity = null; 56 private RunForegroundManager mFgRunner; 57 private MediaButtonReceiver mMediaButtonReceiver; 58 private ResultReceiver mResultReceiver; 59 60 /* possible result values for intent handling */ 61 public static final int RESULT_INVOKING_MAIN = 0; 62 public static final int RESULT_LIB_LOAD_PROGRESS = 1; 63 public static final int RESULT_SERVICE_RUNNING = 3; 64 public static final int RESULT_ERROR_OCCURED = 4; 65 public static final int RESULT_LIB_LOADED = 5; 66 public static final int RESULT_ROCKBOX_EXIT = 6; 67 68 @Override 69 public void onCreate() 70 { 71 instance = this; 72 mMediaButtonReceiver = new MediaButtonReceiver(this); 73 mFgRunner = new RunForegroundManager(this); 74 } 75 76 public static RockboxService getInstance() 77 { 78 /* don't call the constructor here, the instances are managed by 79 * android, so we can't just create a new one */ 80 return instance; 81 } 82 83 public boolean isRockboxRunning() 84 { 85 return rockbox_running; 86 } 87 public Activity getActivity() 88 { 89 return mCurrentActivity; 90 } 91 92 public void setActivity(Activity a) 93 { 94 mCurrentActivity = a; 95 } 96 97 private void putResult(int resultCode) 98 { 99 putResult(resultCode, null); 100 } 101 102 private void putResult(int resultCode, Bundle resultData) 103 { 104 if (mResultReceiver != null) 105 mResultReceiver.send(resultCode, resultData); 106 } 107 108 private void doStart(Intent intent) 109 { 110 Logger.d("Start RockboxService (Intent: " + intent.getAction() + ")"); 111 112 if (intent.getAction().equals("org.rockbox.ResendTrackUpdateInfo")) 113 { 114 if (rockbox_running) 115 mFgRunner.resendUpdateNotification(); 116 return; 117 } 118 119 if (intent.hasExtra("callback")) 120 mResultReceiver = (ResultReceiver) intent.getParcelableExtra("callback"); 121 122 if (!rockbox_running) 123 startService(); 124 125 if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) 126 { 127 KeyEvent kev = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); 128 RockboxFramebuffer.buttonHandler(kev.getKeyCode(), 129 kev.getAction() == KeyEvent.ACTION_DOWN); 130 } 131 132 /* (Re-)attach the media button receiver, in case it has been lost */ 133 mMediaButtonReceiver.register(); 134 putResult(RESULT_SERVICE_RUNNING); 135 136 rockbox_running = true; 137 } 138 139 public void onStart(Intent intent, int startId) { 140 doStart(intent); 141 } 142 143 public int onStartCommand(Intent intent, int flags, int startId) 144 { 145 /* if null, then the service was most likely restarted by android 146 * after getting killed for memory pressure earlier */ 147 if (intent == null) 148 intent = new Intent("org.rockbox.ServiceRestarted"); 149 doStart(intent); 150 return START_STICKY; 151 } 152 153 private void startService() 154 { 155 final Object lock = new Object(); 156 Thread rb = new Thread(new Runnable() 157 { 158 public void run() 159 { 160 final int BUFFER = 8*1024; 161 String rockboxDirPath = "/data/data/org.rockbox/app_rockbox/rockbox"; 162 String rockboxCreditsPath = "/data/data/org.rockbox/app_rockbox/rockbox/rocks/viewers"; 163 String rockboxSdDirPath = "/sdcard/rockbox"; 164 165 /* the following block unzips libmisc.so, which contains the files 166 * we ship, such as themes. It's needed to put it into a .so file 167 * because there's no other way to ship files and have access 168 * to them from native code 169 */ 170 File libMisc = new File("/data/data/org.rockbox/lib/libmisc.so"); 171 /* use arbitrary file to determine whether extracting is needed */ 172 File arbitraryFile = new File(rockboxCreditsPath, "credits.rock"); 173 File rockboxInfoFile = new File(rockboxSdDirPath, "rockbox-info.txt"); 174 /* unzip newer or doesnt exist */ 175 boolean doExtract = !arbitraryFile.exists() 176 || (libMisc.lastModified() > arbitraryFile.lastModified()); 177 178 /* load library before unzipping which may take a while 179 * but at least tell if unzipping is going to be done before*/ 180 synchronized (lock) { 181 Bundle bdata = new Bundle(); 182 bdata.putBoolean("unzip", doExtract); 183 System.loadLibrary("rockbox"); 184 putResult(RESULT_LIB_LOADED, bdata); 185 lock.notify(); 186 } 187 188 if (doExtract) 189 { 190 boolean extractToSd = false; 191 if(rockboxInfoFile.exists()) { 192 extractToSd = true; 193 Logger.d("extracting resources to SD card"); 194 } 195 else { 196 Logger.d("extracting resources to internal memory"); 197 } 198 try 199 { 200 Bundle progressData = new Bundle(); 201 byte data[] = new byte[BUFFER]; 202 ZipFile zipfile = new ZipFile(libMisc); 203 Enumeration<? extends ZipEntry> e = zipfile.entries(); 204 progressData.putInt("max", zipfile.size()); 205 206 while(e.hasMoreElements()) 207 { 208 ZipEntry entry = (ZipEntry) e.nextElement(); 209 File file; 210 /* strip off /.rockbox when extracting */ 211 String fileName = entry.getName(); 212 int slashIndex = fileName.indexOf('/', 1); 213 /* codecs are now stored as libs, only keep rocks on internal */ 214 if(extractToSd == false 215 || fileName.substring(slashIndex).startsWith("/rocks")) 216 { 217 file = new File(rockboxDirPath + fileName.substring(slashIndex)); 218 } 219 else 220 { 221 file = new File(rockboxSdDirPath + fileName.substring(slashIndex)); 222 } 223 224 if (!entry.isDirectory()) 225 { 226 /* Create the parent folders if necessary */ 227 File folder = new File(file.getParent()); 228 if (!folder.exists()) 229 folder.mkdirs(); 230 231 /* Extract file */ 232 BufferedInputStream is = new BufferedInputStream(zipfile.getInputStream(entry), BUFFER); 233 FileOutputStream fos = new FileOutputStream(file); 234 BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER); 235 236 int count; 237 while ((count = is.read(data, 0, BUFFER)) != -1) 238 dest.write(data, 0, count); 239 240 dest.flush(); 241 dest.close(); 242 is.close(); 243 } 244 245 progressData.putInt("value", progressData.getInt("value", 0) + 1); 246 putResult(RESULT_LIB_LOAD_PROGRESS, progressData); 247 } 248 arbitraryFile.setLastModified(libMisc.lastModified()); 249 } catch(Exception e) { 250 Logger.d("Exception when unzipping", e); 251 Bundle bundle = new Bundle(); 252 e.printStackTrace(); 253 bundle.putString("error", getString(R.string.error_extraction)); 254 putResult(RESULT_ERROR_OCCURED, bundle); 255 } 256 } 257 258 /* Generate default config if none exists yet */ 259 File rockboxConfig = new File(Environment.getExternalStorageDirectory(), "rockbox/config.cfg"); 260 if (!rockboxConfig.exists()) { 261 File rbDir = new File(rockboxConfig.getParent()); 262 if (!rbDir.exists()) 263 rbDir.mkdirs(); 264 265 OutputStreamWriter strm; 266 try { 267 strm = new OutputStreamWriter(new FileOutputStream(rockboxConfig)); 268 strm.write("# config generated by RockboxService\n"); 269 strm.write("start directory: " + Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "\n"); 270 strm.write("lang: /.rockbox/langs/" + getString(R.string.rockbox_language_file) + "\n"); 271 strm.close(); 272 } catch(Exception e) { 273 Logger.d("Exception when writing default config", e); 274 } 275 } 276 277 /* Start native code */ 278 putResult(RESULT_INVOKING_MAIN); 279 280 main(); 281 282 putResult(RESULT_ROCKBOX_EXIT); 283 284 Logger.d("Stop service: main() returned"); 285 stopSelf(); /* service is of no use anymore */ 286 } 287 }, "Rockbox thread"); 288 rb.setDaemon(false); 289 /* wait at least until the library is loaded */ 290 synchronized (lock) 291 { 292 rb.start(); 293 while(true) 294 { 295 try { 296 lock.wait(); 297 } catch (InterruptedException e) { 298 continue; 299 } 300 break; 301 } 302 } 303 } 304 305 private native void main(); 306 307 @Override 308 public IBinder onBind(Intent intent) 309 { 310 return null; 311 } 312 313 void startForeground() 314 { 315 mFgRunner.startForeground(); 316 } 317 318 void stopForeground() 319 { 320 mFgRunner.stopForeground(); 321 } 322 323 @Override 324 public void onDestroy() 325 { 326 super.onDestroy(); 327 /* Don't unregister so we can receive them (and startup the service) 328 * after idle power-off. Hopefully it's OK if mMediaButtonReceiver is 329 * garbage collected. 330 * mMediaButtonReceiver.unregister(); */ 331 mMediaButtonReceiver = null; 332 /* Make sure our notification is gone. */ 333 stopForeground(); 334 instance = null; 335 rockbox_running = false; 336 System.runFinalization(); 337 /* exit() seems unclean but is needed in order to get the .so file garbage 338 * collected, otherwise Android caches this Service and librockbox.so 339 * The library must be reloaded to zero the bss and reset data 340 * segment */ 341 System.exit(0); 342 } 343}