/**
*
* For information on usage and redistribution, and for a DISCLAIMER OF ALL
* WARRANTIES, see the file, "LICENSE.txt," in this distribution.
*
*/
package org.puredata.android.service;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import org.puredata.android.io.AudioParameters;
import org.puredata.android.io.PdAudio;
import org.puredata.android.utils.Properties;
import org.puredata.core.PdBase;
import org.puredata.core.utils.IoUtils;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
/**
*
* PdService allows applications to run Pure Data as a (local) service, with foreground priority if desired.
*
* @author Peter Brinkmann (peter.brinkmann@gmail.com)
*
*/
public class PdService extends Service {
public class PdBinder extends Binder {
public PdService getService() {
return PdService.this;
}
}
private final PdBinder binder = new PdBinder();
private static final boolean hasEclair = Properties.version >= 5;
private final ForegroundManager fgManager = hasEclair ? new ForegroundEclair() : new ForegroundCupcake();
private static final String PD_SERVICE = "PD Service";
private volatile int sampleRate = 0;
private volatile int inputChannels = 0;
private volatile int outputChannels = 0;
private volatile float bufferSizeMillis = 0.0f;
/**
* @return the current audio buffer size in milliseconds (approximate value;
* the exact value is a multiple of the Pure Data tick size (64 samples))
*/
public float getBufferSizeMillis() {
return bufferSizeMillis;
}
/**
* @return number of input channels
*/
public int getInputChannels() {
return inputChannels;
}
/**
* @return number of output channels
*/
public int getOutputChannels() {
return outputChannels;
}
/**
* @return current sample rate
*/
public int getSampleRate() {
return sampleRate;
}
/**
* Initialize Pure Data and audio thread
*
* @param srate sample rate
* @param nic number of input channels
* @param noc number of output channels
* @param millis audio buffer size in milliseconds
* @throws IOException if the audio parameters are not supported by the device
*/
public synchronized void initAudio(int srate, int nic, int noc, float millis) throws IOException {
fgManager.stopForeground();
Resources res = getResources();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (srate < 0) {
String s = prefs.getString(res.getString(R.string.pref_key_srate), null);
srate = (s == null) ? AudioParameters.suggestSampleRate() : Integer.parseInt(s);
}
if (nic < 0) {
String s = prefs.getString(res.getString(R.string.pref_key_inchannels), null);
nic = (s == null) ? AudioParameters.suggestInputChannels() : Integer.parseInt(s);
}
if (noc < 0) {
String s = prefs.getString(res.getString(R.string.pref_key_outchannels), null);
noc = (s == null) ? AudioParameters.suggestOutputChannels() : Integer.parseInt(s);
}
if (millis < 0) {
String s = prefs.getString(res.getString(R.string.pref_key_bufsize_millis), null);
millis = (s == null) ? AudioParameters.suggestBufferSizeMillis() : Float.parseFloat(s);
}
int tpb = (int) (0.001f * millis * srate / PdBase.blockSize()) + 1;
PdAudio.initAudio(srate, nic, noc, tpb, true);
sampleRate = srate;
inputChannels = nic;
outputChannels = noc;
bufferSizeMillis = millis;
}
/**
* Start the audio thread without foreground privileges
*/
public synchronized void startAudio() {
PdAudio.startAudio(this);
}
/**
* Start the audio thread with foreground privileges
*
* @param intent intent to be triggered when the user selects the notification of the service
* @param icon icon representing the notification
* @param title title of the notification
* @param description description of the notification
*/
public synchronized void startAudio(Intent intent, int icon, String title, String description) {
fgManager.startForeground(intent, icon, title, description);
PdAudio.startAudio(this);
}
/**
* Stop the audio thread
*/
public synchronized void stopAudio() {
PdAudio.stopAudio();
fgManager.stopForeground();
}
/**
* @return true if and only if the audio thread is running
*/
public synchronized boolean isRunning() {
return PdAudio.isRunning();
}
/**
* Releases all resources
*/
public synchronized void release() {
stopAudio();
PdAudio.release();
PdBase.release();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
release();
return false;
}
@Override
public void onCreate() {
super.onCreate();
Resources res = getResources();
File dir = getFilesDir();
try {
IoUtils.extractZipResource(res.openRawResource(R.raw.extra_abs), dir, false);
IoUtils.extractZipResource(res.openRawResource(Properties.hasArmeabiV7a ? R.raw.extra_ext_v7a : R.raw.extra_ext), dir, false);
} catch (IOException e) {
Log.e(PD_SERVICE, "unable to unpack abstractions/extras:" + e.toString());
}
PdBase.addToSearchPath(dir.getAbsolutePath());
};
@Override
public void onDestroy() {
super.onDestroy();
release();
}
/**
* @return the current audio session ID, for Gingerbread and later; will throw an exception on older versions
*/
public int getAudioSessionId() {
return PdAudio.getAudioSessionId();
}
// Hack to support multiple versions of the Android API, based on an idea
// from http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html
private interface ForegroundManager {
void startForeground(Intent intent, int icon, String title, String description);
void stopForeground();
}
// Another version support hack, this time adapted from
// http://tuntis.net/2011/03/06/setforeground-missing-in-android-3-0-services/.
// This one works around the disappearance of the setForeground method in Honeycomb.
private void invokeSetForeground(boolean foreground) {
try {
Method method = getClass().getMethod("setForeground", boolean.class);
method.invoke(this, foreground);
} catch (Exception e) {
Log.e(PD_SERVICE, e.toString());
}
}
private class ForegroundCupcake implements ForegroundManager {
protected static final int NOTIFICATION_ID = 1;
private boolean hasForeground = false;
protected Notification makeNotification(Intent intent, int icon, String title, String description) {
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
Notification notification = new Notification(icon, title, System.currentTimeMillis());
notification.setLatestEventInfo(PdService.this, title, description, pi);
notification.flags |= Notification.FLAG_ONGOING_EVENT;
return notification;
}
@Override
public void startForeground(Intent intent, int icon, String title, String description) {
stopForeground();
versionedStart(intent, icon, title, description);
hasForeground = true;
}
protected void versionedStart(Intent intent, int icon, String title, String description) {
invokeSetForeground(true);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(NOTIFICATION_ID, makeNotification(intent, icon, title, description));
}
@Override
public void stopForeground() {
if (hasForeground) {
versionedStop();
hasForeground = false;
}
}
protected void versionedStop() {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(NOTIFICATION_ID);
invokeSetForeground(false);
}
}
private class ForegroundEclair extends ForegroundCupcake {
@Override
protected void versionedStart(Intent intent, int icon, String title, String description) {
PdService.this.startForeground(NOTIFICATION_ID, makeNotification(intent, icon, title, description));
}
@Override
protected void versionedStop() {
PdService.this.stopForeground(true);
}
}
}