/**
*
* @author Peter Brinkmann (peter.brinkmann@gmail.com)
*
* 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.scenes;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.puredata.android.scenes.SceneDataBase.SceneColumn;
import org.puredata.android.service.PdService;
import org.puredata.core.PdBase;
import org.puredata.core.PdListener;
import org.puredata.core.utils.IoUtils;
import org.puredata.core.utils.PdDispatcher;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
public class ScenePlayer extends Activity implements SensorEventListener, OnTouchListener, OnClickListener, OnSeekBarChangeListener {
public static final String RECORDING_PATH = "recording_path";
private static final String TAG = "Pd Scene Player";
private static final String RJ_IMAGE_ANDROID = "rj_image_android";
private static final String RJ_TEXT_ANDROID = "rj_text_android";
private static final String TRANSPORT = "#transport";
private static final String ACCELERATE = "#accelerate";
private static final String MICVOLUME = "#micvolume";
private static final int SAMPLE_RATE = 22050;
private final Object lock = new Object();
private SceneDataBase db;
private ProgressDialog progress = null;
private SceneView sceneView;
private ToggleButton play;
private ToggleButton record;
private ImageButton info;
private SeekBar micVolume;
private int micValue;
private File sceneFolder;
private File recDir = null;
private String recFile = null;
private long recStart;
private long sceneId;
private String artist;
private String title;
private String description;
private PdService pdService = null;
private int patch = 0;
private final PdDispatcher dispatcher = new PdDispatcher() {
@Override
public void print(String s) {
post(s);
}
};
private void post(final String msg) {
Log.i(TAG, msg);
}
private Toast toast = null;
private void toast(final String msg) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (toast == null) {
toast = Toast.makeText(getApplicationContext(), "", Toast.LENGTH_SHORT);
}
toast.setText(msg);
toast.show();
}
});
}
private final PdListener overlayListener = new PdListener.Adapter() {
private final Map<String, Overlay> overlays = new HashMap<String, Overlay>();
@Override
public void receiveList(String source, Object... args) {
String key = (String) args[0];
String cmd = (String) args[1];
if (overlays.containsKey(key)) {
Overlay overlay = overlays.get(key);
if (cmd.equals("visible")) {
boolean flag = ((Float) args[2]).floatValue() > 0.5f;
overlay.setVisible(flag);
} else if (cmd.equals("move")) {
float x = ((Float) args[2]).floatValue();
float y = ((Float) args[3]).floatValue();
overlay.setPosition(x, y);
} else {
if (overlay instanceof TextOverlay) {
TextOverlay textOverlay = (TextOverlay) overlay;
if (cmd.equals("text")) {
textOverlay.setText((String) args[2]);
} else if (cmd.equals("size")) {
textOverlay.setSize(((Float) args[2]).floatValue());
}
} else {
ImageOverlay imgOverlay = (ImageOverlay) overlay;
float val = ((Float) args[2]).floatValue();
if (cmd.equals("ref")) {
boolean flag = val > 0.5f;
imgOverlay.setCentered(flag);
} else if (cmd.equals("scale")) {
float sy = ((Float) args[3]).floatValue();
imgOverlay.setScale(val, sy);
} else if (cmd.equals("rotate")) {
imgOverlay.setAngle(val);
} else if (cmd.equals("alpha")) {
imgOverlay.setAlpha(val);
}
}
}
} else {
String arg = (String) args[2];
Overlay overlay;
if (cmd.equals("load")) {
overlay = new ImageOverlay(new File(sceneFolder, arg).getAbsolutePath());
} else if (cmd.equals("text")) {
overlay = new TextOverlay(arg);
} else return;
sceneView.addOverlay(overlay);
overlays.put(key, overlay);
}
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized(lock) {
pdService = ((PdService.PdBinder)service).getService();
initPd();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
// this method will never be called
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
db = new SceneDataBase(this);
Intent intent = getIntent();
sceneId = intent.getLongExtra(SceneColumn.ID.getLabel(), -1);
if (sceneId >= 0) {
Cursor cursor = db.getScene(sceneId);
String scenePath = SceneDataBase.getString(cursor, SceneColumn.SCENE_DIRECTORY);
artist = SceneDataBase.getString(cursor, SceneColumn.SCENE_ARTIST);
title = SceneDataBase.getString(cursor, SceneColumn.SCENE_TITLE);
description = SceneDataBase.getString(cursor, SceneColumn.SCENE_INFO);
cursor.close();
micValue = getPreferences(MODE_PRIVATE).getInt(MICVOLUME, 100);
progress = new ProgressDialog(this);
progress.setCancelable(false);
progress.setIndeterminate(true);
progress.setMessage("Loading scene...");
progress.show();
sceneFolder = new File(scenePath);
String recDirPath = intent.getStringExtra(RECORDING_PATH);
recDir = new File(recDirPath != null ? recDirPath : getResources().getString(R.string.recording_folder));
if (recDir.isFile() || (!recDir.exists() && !recDir.mkdirs())) recDir = null;
initGui();
initSystemServices();
initPdService();
} else {
Log.e(TAG, "launch intent without scene ID");
finish();
}
}
private void initPdService() {
new Thread() {
@Override
public void run() {
fixScene();
bindService(new Intent(ScenePlayer.this, PdService.class), serviceConnection, BIND_AUTO_CREATE);
}
}.start();
}
private void initSystemServices() {
SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_GAME);
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
synchronized (lock) {
if (pdService == null) return;
if (state == TelephonyManager.CALL_STATE_IDLE) {
if (play.isChecked() && !pdService.isRunning()) {
startAudio();
}
} else {
if (pdService.isRunning()) {
stopAudio();
}
}
}
}
}, PhoneStateListener.LISTEN_CALL_STATE);
}
private void fixScene() {
// weird little hack to avoid having our rj_image.pd and such masked by files in the scene
for (String s: new String[] {"rj_image.pd", "rj_text.pd", "soundinput.pd", "soundoutput.pd"}) {
List<File> list = IoUtils.find(sceneFolder, s);
for (File file: list) file.delete();
}
}
@Override
public void onSensorChanged(SensorEvent event) {
final float q = 1.0f / SensorManager.GRAVITY_EARTH; // convert acceleration units from m/s^2 to g
PdBase.sendList(ACCELERATE, event.values[0] * q, -event.values[1] * q, -event.values[2] * q);
/**
* Explanation: Observation of RjDj patches suggests that the z-axis points
* downward on iPhones. Since I'm pretty sure that the coordinate system is
* supposed to be right-handed and that the x-axis points right, I've concluded
* that the way to convert between Android and iPhone accelerometer values is to
* flip the sign of the y and z coordinates.
*/
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// don't care
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return (v == sceneView) && VersionedTouch.evaluateTouch(event, sceneView.getWidth(), sceneView.getHeight());
}
@Override
protected void onPause() {
getPreferences(MODE_PRIVATE).edit().putInt(MICVOLUME, micValue).commit();
dismissProgressDialog();
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
cleanup();
db.close();
}
private void initGui() {
int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
getWindow().setFlags(flags, flags);
setContentView(R.layout.scene_player);
TextView tv = (TextView) findViewById(R.id.sceneplayer_title);
tv.setText(title);
tv = (TextView) findViewById(R.id.sceneplayer_artist);
tv.setText(artist);
sceneView = (SceneView) findViewById(R.id.sceneplayer_pic);
sceneView.setOnTouchListener(this);
sceneView.setImageBitmap(BitmapFactory.decodeFile(new File(sceneFolder, "image.jpg").getAbsolutePath()));
play = (ToggleButton) findViewById(R.id.sceneplayer_pause);
play.setOnClickListener(this);
record = (ToggleButton) findViewById(R.id.sceneplayer_record);
record.setOnClickListener(this);
info = (ImageButton) findViewById(R.id.sceneplayer_info);
info.setOnClickListener(this);
micVolume = (SeekBar) findViewById(R.id.mic_volume);
micVolume.setProgress(micValue);
micVolume.setOnSeekBarChangeListener(this);
}
private void initPd() {
// run in a separate thread in order to keep the progress wheel spinning
new Thread() {
@Override
public void run() {
PdBase.setReceiver(dispatcher);
dispatcher.addListener(RJ_IMAGE_ANDROID, overlayListener);
dispatcher.addListener(RJ_TEXT_ANDROID, overlayListener);
startAudio();
dismissProgressDialog();
}
}.start();
}
private void dismissProgressDialog() {
if (progress!= null) progress.dismiss();
}
private void cleanup() {
synchronized (lock) {
// make sure to release all resources
stopRecording();
stopAudio();
if (patch != 0) {
PdBase.closePatch(patch);
patch = 0;
}
dispatcher.release();
if (pdService != null) {
try {
unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// already unbound
}
pdService = null;
}
}
}
@Override
public void onClick(View v) {
if (v.equals(play)) {
if (play.isChecked()) {
startAudio();
} else {
stopAudio();
}
} else if (v.equals(record)) {
if (record.isChecked()) {
startRecording();
} else {
stopRecording();
}
} else if (v.equals(info)) {
showInfo();
}
}
private void startRecording() {
if (recDir == null) {
record.setChecked(false);
return;
}
recStart = System.currentTimeMillis();
String fileName = "recording_" + recStart + ".wav";
recFile = new File(recDir, fileName).getAbsolutePath();
PdBase.sendMessage(TRANSPORT, "scene", recFile);
PdBase.sendMessage(TRANSPORT, "record", 1);
post("Recording...");
}
private void stopRecording() {
if (recFile == null) return;
PdBase.sendMessage(TRANSPORT, "record", 0);
long duration = System.currentTimeMillis() - recStart;
double longitude = 0.0;
double latitude = 0.0;
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null) { // Paranoid? Maybe...
Location location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if (location != null) {
longitude = location.getLongitude();
latitude = location.getLatitude();
}
}
db.addRecording(recFile, recStart, duration, longitude, latitude, sceneId);
recFile = null;
post("Finished recording");
}
private boolean initAudio(int nIn, int nOut) {
try {
pdService.initAudio(SAMPLE_RATE, nIn, nOut, -1); // negative values default to PdService preferences
} catch (IOException e) {
Log.e(TAG, e.toString());
return false;
}
return true;
}
private void startAudio() {
synchronized (lock) {
if (pdService == null) return;
if (!initAudio(1, 2)) { // only trying one input channel to avoid failures on Samsung Galaxy Tab
if (!initAudio(0, 2)) {
toast("Unable to initialize audio interface");
finish();
return;
} else {
toast("Warning: No audio input available");
}
}
if (patch == 0) {
try {
patch = PdBase.openPatch(new File(sceneFolder, "_main.pd"));
adjustMicVolume(micValue);
} catch (IOException e) {
Log.e(TAG, e.toString());
toast("Unable to open patch; exiting");
finish();
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
}
pdService.startAudio(new Intent(this, ScenePlayer.class), R.drawable.sceneplayer_notify,
title + " by " + artist, "Return to scene.");
PdBase.sendMessage(TRANSPORT, "play", 1);
}
}
private void stopAudio() {
synchronized (lock) {
if (pdService == null) return;
PdBase.sendMessage(TRANSPORT, "play", 0);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// do nothing
}
pdService.stopAudio();
}
}
private void showInfo() {
AlertDialog.Builder ad = new AlertDialog.Builder(this);
View header = View.inflate(this, R.layout.two_line_dialog_title, null);
((TextView) header.findViewById(android.R.id.text1)).setText(title);
((TextView) header.findViewById(android.R.id.text2)).setText(artist);
File pic = new File(sceneFolder, "thumb.jpg");
if (!pic.exists()) pic = new File(sceneFolder, "image.jpg");
if (pic.exists()) {
ImageView sceneThumbnail = (ImageView) header.findViewById(android.R.id.selectedIcon);
sceneThumbnail.setImageDrawable(Drawable.createFromPath(pic.getAbsolutePath()));
}
ad.setCustomTitle(header);
ad.setMessage(description);
ad.setNeutralButton(android.R.string.ok, null);
ad.setCancelable(true);
ad.show();
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
micValue = progress;
adjustMicVolume(progress);
}
public void adjustMicVolume(int vol) {
float q = vol * 0.01f;
float volume = q * q * q * q; // fourth power of mic volume slider value; somewhere between linear and exponential
PdBase.sendFloat(MICVOLUME, volume);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// don't care
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// don't care
}
}