Android: Fix some race conditions and crashes on startup.
[kugel-rb.git] / android / src / org / rockbox / RockboxService.java
blob43d122a8cc4a8de5e7ed81d7e879bad81613610d
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
10 * Copyright (C) 2010 Thomas Martitz
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.
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
20 ****************************************************************************/
22 package org.rockbox;
24 import java.io.BufferedInputStream;
25 import java.io.BufferedOutputStream;
26 import java.io.File;
27 import java.io.FileOutputStream;
28 import java.util.Enumeration;
29 import java.util.Timer;
30 import java.util.TimerTask;
31 import java.util.zip.ZipEntry;
32 import java.util.zip.ZipFile;
34 import org.rockbox.Helper.MediaButtonReceiver;
35 import org.rockbox.Helper.RunForegroundManager;
37 import android.app.Activity;
38 import android.app.Service;
39 import android.content.BroadcastReceiver;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.IntentFilter;
43 import android.os.Bundle;
44 import android.os.IBinder;
45 import android.os.ResultReceiver;
46 import android.util.Log;
47 import android.view.KeyEvent;
49 /* This class is used as the main glue between java and c.
50 * All access should be done through RockboxService.get_instance() for safety.
53 public class RockboxService extends Service
55 /* this Service is really a singleton class - well almost.
56 * To do it properly this line should be instance = new RockboxService()
57 * but apparently that doesnt work with the way android Services are created.
59 private static RockboxService instance = null;
61 /* locals needed for the c code and rockbox state */
62 private RockboxFramebuffer fb = null;
63 private volatile boolean rockbox_running;
64 private Activity current_activity = null;
65 private IntentFilter itf;
66 private BroadcastReceiver batt_monitor;
67 private RunForegroundManager fg_runner;
68 private MediaButtonReceiver mMediaButtonReceiver;
69 @SuppressWarnings("unused")
70 private int battery_level;
71 private ResultReceiver resultReceiver;
72 final private Object lock = new Object();
74 public static final int RESULT_INVOKING_MAIN = 0;
75 public static final int RESULT_LIB_LOAD_PROGRESS = 1;
76 public static final int RESULT_FB_INITIALIZED = 2;
77 public static final int RESULT_SERVICE_RUNNING = 3;
78 public static final int RESULT_ERROR_OCCURED = 4;
79 public static final int RESULT_LIB_LOADED = 5;
81 @Override
82 public void onCreate()
84 instance = this;
85 mMediaButtonReceiver = new MediaButtonReceiver(this);
86 fg_runner = new RunForegroundManager(this);
89 public static RockboxService get_instance()
91 return instance;
94 public RockboxFramebuffer get_fb()
96 return fb;
99 public Activity get_activity()
101 return current_activity;
103 public void set_activity(Activity a)
105 current_activity = a;
108 private void do_start(Intent intent)
110 LOG("Start Service");
112 if (intent != null && intent.hasExtra("callback"))
113 resultReceiver = (ResultReceiver) intent.getParcelableExtra("callback");
115 /* Display a notification about us starting.
116 * We put an icon in the status bar. */
117 if (fg_runner == null)
118 { /* needs to be initialized before main() runs */
119 try {
120 } catch (Exception e) {
121 e.printStackTrace();
125 if (!rockbox_running)
127 synchronized(lock)
129 startservice();
130 while(true) {
131 try {
132 lock.wait();
133 } catch (InterruptedException e) {
134 continue;
136 break;
138 fb = new RockboxFramebuffer(this);
139 if (resultReceiver != null)
140 resultReceiver.send(RESULT_FB_INITIALIZED, null);
143 if (resultReceiver != null)
144 resultReceiver.send(RESULT_LIB_LOADED, null);
146 if (intent != null && intent.getAction() != null)
148 Log.d("RockboxService", intent.getAction());
149 if (intent.getAction().equals("org.rockbox.PlayPause"))
151 if (fb != null)
152 fb.onKeyUp(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, null);
154 else if (intent.getAction().equals("org.rockbox.Prev"))
156 if (fb != null)
157 fb.onKeyUp(KeyEvent.KEYCODE_MEDIA_PREVIOUS, null);
159 else if (intent.getAction().equals("org.rockbox.Next"))
161 if (fb != null)
162 fb.onKeyUp(KeyEvent.KEYCODE_MEDIA_NEXT, null);
164 else if (intent.getAction().equals("org.rockbox.Stop"))
166 if (fb != null)
167 fb.onKeyUp(KeyEvent.KEYCODE_MEDIA_STOP, null);
171 /* (Re-)attach the media button receiver, in case it has been lost */
172 mMediaButtonReceiver.register();
174 if (resultReceiver != null)
175 resultReceiver.send(RESULT_SERVICE_RUNNING, null);
178 private void LOG(CharSequence text)
180 Log.d("Rockbox", (String) text);
183 private void LOG(CharSequence text, Throwable tr)
185 Log.d("Rockbox", (String) text, tr);
188 public void onStart(Intent intent, int startId) {
189 do_start(intent);
192 public int onStartCommand(Intent intent, int flags, int startId)
194 do_start(intent);
195 return 1; /* old API compatibility: 1 == START_STICKY */
198 private void startservice()
200 final int BUFFER = 8*1024;
201 Thread rb = new Thread(new Runnable()
203 public void run()
205 String rockboxDirPath = "/data/data/org.rockbox/app_rockbox/rockbox";
206 File rockboxDir = new File(rockboxDirPath);
208 /* the following block unzips libmisc.so, which contains the files
209 * we ship, such as themes. It's needed to put it into a .so file
210 * because there's no other way to ship files and have access
211 * to them from native code
213 File libMisc = new File("/data/data/org.rockbox/lib/libmisc.so");
214 /* use arbitrary file to determine whether extracting is needed */
215 File arbitraryFile = new File(rockboxDir, "viewers.config");
216 if (!arbitraryFile.exists() || (libMisc.lastModified() > arbitraryFile.lastModified()))
220 Bundle progressData = new Bundle();
221 byte data[] = new byte[BUFFER];
222 ZipFile zipfile = new ZipFile(libMisc);
223 Enumeration<? extends ZipEntry> e = zipfile.entries();
224 progressData.putInt("max", zipfile.size());
226 while(e.hasMoreElements())
228 ZipEntry entry = (ZipEntry) e.nextElement();
229 File file;
230 /* strip off /.rockbox when extracting */
231 String fileName = entry.getName();
232 int slashIndex = fileName.indexOf('/', 1);
233 file = new File(rockboxDirPath + fileName.substring(slashIndex));
235 if (!entry.isDirectory())
237 /* Create the parent folders if necessary */
238 File folder = new File(file.getParent());
239 if (!folder.exists())
240 folder.mkdirs();
242 /* Extract file */
243 BufferedInputStream is = new BufferedInputStream(zipfile.getInputStream(entry), BUFFER);
244 FileOutputStream fos = new FileOutputStream(file);
245 BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
247 int count;
248 while ((count = is.read(data, 0, BUFFER)) != -1)
249 dest.write(data, 0, count);
251 dest.flush();
252 dest.close();
253 is.close();
256 if (resultReceiver != null) {
257 progressData.putInt("value", progressData.getInt("value", 0) + 1);
258 resultReceiver.send(RESULT_LIB_LOAD_PROGRESS, progressData);
261 } catch(Exception e) {
262 LOG("Exception when unzipping", e);
263 e.printStackTrace();
264 if (resultReceiver != null) {
265 Bundle bundle = new Bundle();
266 bundle.putString("error", getString(R.string.error_extraction));
267 resultReceiver.send(RESULT_ERROR_OCCURED, bundle);
272 synchronized (lock) {
273 System.loadLibrary("rockbox");
274 lock.notify();
277 rockbox_running = true;
278 if (resultReceiver != null)
279 resultReceiver.send(RESULT_INVOKING_MAIN, null);
281 main();
282 throw new IllegalStateException("native main() returned!");
284 }, "Rockbox thread");
285 rb.setDaemon(false);
286 rb.start();
289 private native void main();
291 @Override
292 public IBinder onBind(Intent intent)
294 // TODO Auto-generated method stub
295 return null;
299 @SuppressWarnings("unused")
301 * Sets up the battery monitor which receives the battery level
302 * about each 30 seconds
304 private void initBatteryMonitor()
306 itf = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
307 batt_monitor = new BroadcastReceiver()
309 @Override
310 public void onReceive(Context context, Intent intent)
312 /* we get literally spammed with battery statuses
313 * if we don't delay the re-attaching
315 TimerTask tk = new TimerTask()
317 public void run()
319 registerReceiver(batt_monitor, itf);
322 Timer t = new Timer();
323 context.unregisterReceiver(this);
324 int rawlevel = intent.getIntExtra("level", -1);
325 int scale = intent.getIntExtra("scale", -1);
326 if (rawlevel >= 0 && scale > 0)
327 battery_level = (rawlevel * 100) / scale;
328 else
329 battery_level = -1;
330 /* query every 30s should be sufficient */
331 t.schedule(tk, 30000);
334 registerReceiver(batt_monitor, itf);
337 public void startForeground()
339 fg_runner.startForeground();
342 public void stopForeground()
344 fg_runner.stopForeground();
347 @Override
348 public void onDestroy()
350 super.onDestroy();
351 mMediaButtonReceiver.unregister();
352 mMediaButtonReceiver = null;
353 /* Make sure our notification is gone. */
354 stopForeground();