package org.spin.gaitlib.gaitlogger; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioManager; import android.media.SoundPool; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; import android.widget.Toast; import java.io.File; import java.io.PrintWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; /** * GaitLoggerService * * Logs signal data (currently only accelerometer) * to a text file (eventually mySQL or SQLite3 database) * * Services execute in the application process and typically * in the main thread, but will run in the background. * * @author oli * */ public class GaitLoggerService extends Service implements SensorEventListener { private static final String TAG = "GaitLoggerService"; private static final int APP_UID = 1; //an ID unique to this application private PrintWriter accelOut; private PrintWriter gyroOut; WakeLock wakeLock; final int SENSOR_SAMPLING_SPEED = SensorManager.SENSOR_DELAY_FASTEST; //final int SENSOR_SAMPLING_SPEED = 1000; //in ms //SensorManager.SENSOR_DELAY_NORMAL; //final int WAKE_LOCK = PowerManager.PARTIAL_WAKE_LOCK; final int WAKE_LOCK = PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE; final boolean PLAY_ERROR_SOUND = false; final boolean CAPTURE_GYROSCOPE = false; final boolean REREGISTER_ON_SCREEN_OFF = false; final boolean WRITE_TO_FILES = true; final boolean REACQUIRE_WAKELOCK_ON_SCREEN_OFF = true; final boolean HANDLE_SCREEN_OFF_WITH_DELAY = false; final boolean USE_EVENT_TIME = true; private String dir_path = null; private String accel_path_to_use = null; private String gyro_path_to_use = null; //for playing an error SoundPool mSoundPool; int errorSoundID; long lastTimeStamp = -1; final long ERROR_CUTOFF = 500000000; //the minimum number of nanoseconds required to decide that an error has occurred final long SCREEN_OFF_RECEIVER_DELAY = 500; //delay for reregistering listeners in milliseconds //Central Logic /** * setup() * * called when we want to start logging */ private void setup(Intent intent) { //announce to user that we're starting to log //TODO: make this a resource Toast.makeText(this, "log service started", Toast.LENGTH_SHORT).show(); mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); errorSoundID = mSoundPool.load(this, R.raw.error, 1); //this puts the thread to run in the foreground //so it should continue to log even while sleeping //startForeground(APP_UID, new Notification()); PowerManager mgr = (PowerManager)getSystemService(Context.POWER_SERVICE); wakeLock = mgr.newWakeLock(WAKE_LOCK, "MyWakeLock"); wakeLock.acquire(); //PowerManager.ACQUIRE_CAUSES_WAKEUP //set up output file (comma seperated value text file) //date formats for directory and file name //directory will have the current day as the name //file will have the day, hour, minute, and second as part of the file name DateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMMdd"); DateFormat dateFormat = new SimpleDateFormat("yyyyMMMdd-hh-mm-ss"); Date date = new Date(); //get parameters //TODO: Put these keys into strings.xml String offset = intent.getExtras().getString("offset"); String participantID = intent.getExtras().getString("participant"); String phoneID = intent.getExtras().getString("phone"); String location = intent.getExtras().getString("location"); String toRecord = intent.getExtras().getString("toRecord"); //set up directory and file name String dirString = FileManagerUtil.DATA_FOLDER_PREFIX+simpleDateFormat.format(date); String accelFileString = getString(R.string.accel_logfile_name)+dateFormat.format(date) + participantID + ".csv"; String gyroFileString = getString(R.string.gyro_logfile_name)+dateFormat.format(date) + participantID + ".csv"; //create directory //Toast.makeText(this, Environment. getExternalStorageState(), Toast.LENGTH_LONG).show(); File dir = new File(FileManagerUtil.getDataFoldersParentDirectory() + "/" + dirString); if (!dir.exists() && !dir.mkdirs()) { //couldn't create directory Toast.makeText(this, "Error creating directory: " + dir, Toast.LENGTH_LONG).show(); } IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); registerReceiver(mReceiver, filter); dir_path = dir.getAbsolutePath() +"/"; if (toRecord.equals("accel")) { accel_path_to_use=dir_path + accelFileString; } else if (toRecord.equals("gyro")) { gyro_path_to_use=dir_path + gyroFileString; } else { accel_path_to_use=dir_path + accelFileString; gyro_path_to_use=dir_path + gyroFileString; } NativeAccelerometerReader.monitorSensors( participantID, phoneID, location, offset, accel_path_to_use, gyro_path_to_use, 100); //startForeground(0, new Notification()); } /** * teardown() * * called when we want to stop logging */ private void teardown() { NativeAccelerometerReader.stopMonitoringSensors(); if (dir_path != null){ FileManagerUtil.updateIndex(dir_path, this); } if (accel_path_to_use != null){ FileManagerUtil.updateIndex(accel_path_to_use, this); } if (gyro_path_to_use != null){ FileManagerUtil.updateIndex(gyro_path_to_use, this); } //unregister with the sensor manager //SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); //sensorManager.unregisterListener(this); //close output streams //if (accelOut != null) // accelOut.close(); //if (gyroOut != null) // gyroOut.close(); //no longer need to run in background wakeLock.release(); unregisterReceiver(mReceiver); //announce to the user that we're done //TODO: make this a resource Toast.makeText(this, "logging service done", Toast.LENGTH_SHORT).show(); } /** * recordAccelerometerValues * * saves the accelerometer reading to a file * using the established PrintWriter * and current system time in milliseconds or nanoseconds * * @param x * @param y * @param z */ private void recordAccelerometerValues(long t, double x, double y, double z) { //DEBUG Log.v(TAG, "record "+t+","+x+","+y+","+z); if (accelOut != null) { if(!USE_EVENT_TIME) { t = System.currentTimeMillis(); } accelOut.printf("%d,%f,%f,%f\n", t,x,y,z); //accelOut.flush(); if(PLAY_ERROR_SOUND && lastTimeStamp != -1 && (t - lastTimeStamp) > ERROR_CUTOFF) { mSoundPool.play(errorSoundID, 1f, 1f, 1, 0, 1f); } lastTimeStamp = t; } } /** * recordGyroscopeValues * * saves the gyroscope reading to a file * using the established PrintWriter * and current system time in milliseconds or nanoseconds * * @param ax * @param ay * @param az */ private void recordGyroscopeValues(long t, double ax, double ay, double az) { //DEBUG //Log.v(TAG, "record "+x+","+y+","+z); if (gyroOut != null) { if (!USE_EVENT_TIME) { t = System.currentTimeMillis(); } gyroOut.printf("%d,%f,%f,%f\n", t,ax,ay,az); //gyroOut.flush(); } } // Service Methods @Override public int onStartCommand(Intent intent, int flags, int startId) { setup(intent); // If we get killed, after returning from here, restart return START_STICKY; } @Override public void onDestroy() { teardown(); } @Override public IBinder onBind(Intent intent) { // this is not a bound service; return null return null; } /* onLowMemory(): Kept for possible issues in future. //if we are memory intensive, this might help to //keep the service alive. @Override public void onLowMemory () { //STUB } */ // SensorEventListener Methods @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // TODO Auto-generated method stub } @Override public void onSensorChanged(SensorEvent event) { switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: recordAccelerometerValues(event.timestamp, event.values[0], event.values[1], event.values[2]); case Sensor.TYPE_GYROSCOPE: recordGyroscopeValues(event.timestamp, event.values[0], event.values[1], event.values[2]); break; } } public void reregisterListeners() { SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); //unregister from the accelerometer and the gyroscope sensorManager.unregisterListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)); if(CAPTURE_GYROSCOPE) { sensorManager.unregisterListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)); } //register the accelerometer and gyroscope sensors sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SENSOR_SAMPLING_SPEED); // Tells the SensorManager to send accel data to GaitLoggerService if (CAPTURE_GYROSCOPE) { //register with gyroscope //NOTE: originally tried registering with both simultaneously, but then both signals were received at the same time // with only 3 values :S sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SENSOR_SAMPLING_SPEED); // Tells the SensorManager to send gyro data to GaitLoggerService } } //from http://nosemaj.org/android-persistent-sensors public BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.v(TAG, "onReceive("+intent+")"); if (!intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { return; } if(HANDLE_SCREEN_OFF_WITH_DELAY) { Runnable runnable = new Runnable() { @Override public void run() { if(REREGISTER_ON_SCREEN_OFF) { Log.i(TAG, "Re-registering IMU listeners"); reregisterListeners(); } if(REACQUIRE_WAKELOCK_ON_SCREEN_OFF) { wakeLock.release(); wakeLock.acquire(); } } }; new Handler().postDelayed(runnable, SCREEN_OFF_RECEIVER_DELAY); } else { if(REREGISTER_ON_SCREEN_OFF) { Log.i(TAG, "Re-registering IMU listeners"); reregisterListeners(); } if(REACQUIRE_WAKELOCK_ON_SCREEN_OFF) { wakeLock.release(); wakeLock.acquire(); } } } }; }