package com.androsz.electricsleepbeta.app;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import org.achartengine.model.PointD;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.Settings;
import com.androsz.electricsleepbeta.R;
import com.androsz.electricsleepbeta.alarmclock.Alarm;
import com.androsz.electricsleepbeta.alarmclock.Alarms;
import com.androsz.electricsleepbeta.app.wizard.CalibrationWizardActivity;
import com.androsz.electricsleepbeta.content.StartSleepReceiver;
import com.androsz.electricsleepbeta.util.WakeLockManager;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
public class SleepMonitoringService extends Service implements SensorEventListener {
private static final String TAG = SleepMonitoringService.class.getSimpleName();
private final class UpdateTimerTask extends TimerTask {
@Override
public void run() {
final long currentTime = System.currentTimeMillis();
final double x = currentTime;
final double y = java.lang.Math
.min(SettingsActivity.MAX_ALARM_SENSITIVITY, maxNetForce);
final PointD sleepPoint = new PointD(x, y);
if (sleepData.size() >= MAX_POINTS_IN_A_GRAPH) {
sleepData.remove(0);
}
sleepData.add(sleepPoint);
// append the two doubles in sleepPoint to file
try {
synchronized (DATA_LOCK) {
final FileOutputStream fos = openFileOutput(SLEEP_DATA, Context.MODE_APPEND);
fos.write(PointD.toByteArray(sleepPoint));
fos.close();
}
} catch (final IOException e) {
GoogleAnalyticsTracker.getInstance().trackEvent(Integer.toString(VERSION.SDK_INT),
Build.MODEL, "sleepMonitorCacheFailWrite : " + e.getMessage(), 0);
}
final Intent i = new Intent(SleepActivity.UPDATE_CHART);
i.putExtra(EXTRA_X, x);
i.putExtra(EXTRA_Y, y);
//TODO: ensure that this isn't needed anymore.
//i.putExtra(StartSleepReceiver.EXTRA_ALARM, alarmTriggerSensitivity);
sendBroadcast(i);
maxNetForce = 0;
triggerAlarmIfNecessary(currentTime, y);
}
}
public static final String EXTRA_ALARM_WINDOW = "alarmWindow";
public static final String EXTRA_ID = "id";
public static final String EXTRA_NAME = "name";
public static final String EXTRA_X = "x";
public static final String EXTRA_Y = "y";
private final static int INTERVAL = 5000;
public static int MAX_POINTS_IN_A_GRAPH = 200;
private static final int NOTIFICATION_ID = 0x1337a;
// Object for intrinsic lock
public static final Object[] DATA_LOCK = new Object[0];
public static final String SERVICE_IS_RUNNING = "serviceIsRunning";
public static final String SLEEP_DATA = "sleepData";
public static final String SLEEP_START = "com.androsz.electricsleepbeta.SLEEP_START";
public static final String SLEEP_STOPPED = "com.androsz.electricsleepbeta.SLEEP_STOPPED";
public static final String STOP_AND_SAVE_SLEEP = "com.androsz.electricsleepbeta.STOP_AND_SAVE_SLEEP";
/** Handle that allows others to access the sleep monitoring service. */
private final IBinder mBinder = new ServiceBinder();
private AtomicBoolean mRunning;
private boolean airplaneMode = false;
private float alarmTriggerSensitivity = SettingsActivity.DEFAULT_ALARM_SENSITIVITY;
private int alarmWindow = 30;
final float alpha = 0.8f;
private boolean alreadyDeletedResidualFile = false;
private Date dateStarted;
private boolean forceScreenOn = false;
private final float[] gravity = { 0, 0, 0 };
private double maxNetForce = SettingsActivity.DEFAULT_MIN_SENSITIVITY;
private int ringerModeBackup = AudioManager.RINGER_MODE_NORMAL;
public int sensorDelay = SensorManager.SENSOR_DELAY_NORMAL;
private final BroadcastReceiver serviceReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
if (action.equals(STOP_AND_SAVE_SLEEP)) {
final Intent saveIntent = addExtrasToSaveSleepIntent(new Intent(
SleepMonitoringService.this, SaveSleepActivity.class));
startActivity(saveIntent);
sendBroadcast(new Intent(Alarms.CANCEL_SNOOZE));
final long now = System.currentTimeMillis();
try {
final Alarm alarm = Alarms.calculateNextAlert(context);
if (now > alarm.time + 60 * alarmWindow * 1000) {
Alarms.setTimeToIgnore(context, alarm, alarm.time);
Alarms.setNextAlert(context);
}
} catch (final NullPointerException npe) {
// there are no enabled alarms
Log.d(TAG, "No enabled alarms.");
}
createSaveSleepNotification();
if (!mRunning.compareAndSet(true, false)) {
Log.d(TAG, "Asked to stop and save sleep when not running.");
}
stopSelf();
} else {
if (action.equals(Alarms.CANCEL_SNOOZE)) {
final long now = System.currentTimeMillis();
try {
final Alarm alarm = Alarms.getAlarm(context.getContentResolver(),
intent.getIntExtra(Alarms.ALARM_ID, -1));
if (now > alarm.time + 60 * alarmWindow * 1000) {
Alarms.setTimeToIgnore(context, alarm, alarm.time);
Alarms.setNextAlert(context);
}
} catch (final NullPointerException npe) {
// there are no enabled alarms
Log.d(TAG, "No enabled alarms.");
}
}
createSaveSleepNotification();
if (!mRunning.compareAndSet(true, false)) {
Log.d(TAG, "Asked to cancel snooze when not running.");
}
stopSelf();
}
}
};
private boolean silentMode = false;
private final ArrayList<PointD> sleepData = new ArrayList<PointD>();
private int testModeRate = Integer.MIN_VALUE;
private int updateInterval = INTERVAL;;
Timer updateTimer;
private boolean useAlarm = false;
int waitForSensorsToWarmUp = 0;
private Intent addExtrasToSaveSleepIntent(final Intent saveIntent) {
saveIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
saveIntent.putExtra(EXTRA_ID, hashCode());
saveIntent.putExtra(StartSleepReceiver.EXTRA_ALARM, alarmTriggerSensitivity);
// send start/end time as well
final DateFormat sdf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT,
Locale.getDefault());
DateFormat sdf2 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT,
Locale.getDefault());
final Date now = new Date();
if (dateStarted.getDate() == now.getDate()) {
sdf2 = DateFormat.getTimeInstance(DateFormat.SHORT);
}
saveIntent.putExtra(EXTRA_NAME, sdf.format(dateStarted) + " " + getText(R.string.to) + " "
+ sdf2.format(now));
return saveIntent;
}
private void createSaveSleepNotification() {
final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
final int icon = R.drawable.ic_stat_notify_save;
final CharSequence tickerText = getText(R.string.notification_save_sleep_ticker);
final long when = System.currentTimeMillis();
final Notification notification = new Notification(icon, tickerText, when);
//notification.flags = Notification.FLAG_AUTO_CANCEL;
final Context context = getApplicationContext();
final CharSequence contentTitle = getText(R.string.notification_save_sleep_title);
final CharSequence contentText = getText(R.string.notification_save_sleep_text);
final Intent notificationIntent = addExtrasToSaveSleepIntent(new Intent(this,
SaveSleepActivity.class));
final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent,
0);
notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);
notificationManager.notify(hashCode(), notification);
startActivity(notificationIntent);
}
private Notification createServiceNotification() {
final int icon = R.drawable.ic_stat_notify_track;
final CharSequence tickerText = getText(R.string.notification_sleep_ticker);
final long when = System.currentTimeMillis();
final Notification notification = new Notification(icon, tickerText, when);
notification.flags = Notification.FLAG_ONGOING_EVENT;
final CharSequence contentTitle = getText(R.string.notification_sleep_title);
final CharSequence contentText = getText(R.string.notification_sleep_text);
Intent notificationIntent = null;
// prevents the user from entering SleepActivity from the notification
// when in test mode
if (this.testModeRate == Integer.MIN_VALUE) {
notificationIntent = new Intent(this, SleepActivity.class);
} else {
notificationIntent = new Intent(this, CalibrationWizardActivity.class);
}
notificationIntent
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent,
0);
notification.setLatestEventInfo(this, contentTitle, contentText, contentIntent);
return notification;
}
private void obtainWakeLock() {
// if forcescreenon is on, hold a dim wakelock, otherwise, partial.
final int wakeLockType = forceScreenOn ? (PowerManager.SCREEN_DIM_WAKE_LOCK
| PowerManager.ON_AFTER_RELEASE | PowerManager.ACQUIRE_CAUSES_WAKEUP)
: PowerManager.PARTIAL_WAKE_LOCK;
WakeLockManager.acquire(this, "sleepMonitoring", wakeLockType);
}
@Override
public void onAccuracyChanged(final Sensor sensor, final int accuracy) {
// not used
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
mRunning = new AtomicBoolean();
final IntentFilter filter = new IntentFilter(Alarms.ALARM_DISMISSED_BY_USER_ACTION);
filter.addAction(Alarms.ALARM_SNOOZE_CANCELED_BY_USER_ACTION);
filter.addAction(STOP_AND_SAVE_SLEEP);
registerReceiver(serviceReceiver, filter);
updateTimer = new Timer();
dateStarted = new Date();
}
@Override
public void onDestroy() {
Log.d(TAG, "Destroying sleep monitoring service.");
unregisterAccelerometerListener();
WakeLockManager.release("sleepMonitoring");
unregisterReceiver(serviceReceiver);
// tell monitoring activities that sleep has ended
sendBroadcast(new Intent(SLEEP_STOPPED));
stopForeground(true);
updateTimer.cancel();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
toggleSilentMode(false);
toggleAirplaneMode(false);
final SharedPreferences.Editor ed = getSharedPreferences(SERVICE_IS_RUNNING,
Context.MODE_PRIVATE).edit();
ed.putBoolean(SERVICE_IS_RUNNING, false);
ed.commit();
return null;
}
}.execute();
super.onDestroy();
}
@Override
public void onSensorChanged(final SensorEvent event) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (gravity) {
if (waitForSensorsToWarmUp < 5) {
if (waitForSensorsToWarmUp == 4) {
waitForSensorsToWarmUp++;
try {
updateTimer.scheduleAtFixedRate(new UpdateTimerTask(),
updateInterval, updateInterval);
} catch (IllegalStateException ise) {
// user stopped monitoring really quickly after
// starting.
Log.d(TAG, "User stopped monitoring quickly after starting.");
}
gravity[0] = event.values[0];
gravity[1] = event.values[1];
gravity[2] = event.values[2];
}
waitForSensorsToWarmUp++;
return;
}
gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0];
gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1];
gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2];
final double curX = event.values[0] - gravity[0];
final double curY = event.values[1] - gravity[1];
final double curZ = event.values[2] - gravity[2];
final double mAccelCurrent = Math.sqrt(curX * curX + curY * curY + curZ * curZ);
final double absAccel = Math.abs(mAccelCurrent);
maxNetForce = absAccel > maxNetForce ? absAccel : maxNetForce;
}
}
}).start();
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent != null && mRunning.compareAndSet(false, true)) {
Log.d(TAG, "Starting sleep monitoring service: " + startId);
testModeRate = intent.getIntExtra("testModeRate", Integer.MIN_VALUE);
updateInterval = testModeRate == Integer.MIN_VALUE ? intent.getIntExtra("interval",
INTERVAL) : testModeRate;
sensorDelay = intent.getIntExtra(StartSleepReceiver.EXTRA_SENSOR_DELAY,
SensorManager.SENSOR_DELAY_FASTEST);
alarmTriggerSensitivity = intent.getFloatExtra(
StartSleepReceiver.EXTRA_ALARM,
SettingsActivity.DEFAULT_ALARM_SENSITIVITY);
useAlarm = intent.getBooleanExtra(StartSleepReceiver.EXTRA_USE_ALARM, false);
alarmWindow = intent.getIntExtra(StartSleepReceiver.EXTRA_ALARM_WINDOW, 0);
airplaneMode = intent.getBooleanExtra(StartSleepReceiver.EXTRA_AIRPLANE_MODE, false);
silentMode = intent.getBooleanExtra(StartSleepReceiver.EXTRA_SILENT_MODE, false);
forceScreenOn = intent.getBooleanExtra(StartSleepReceiver.EXTRA_FORCE_SCREEN_ON, false);
startForeground(NOTIFICATION_ID, createServiceNotification());
obtainWakeLock();
registerAccelerometerListener();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
toggleSilentMode(true);
toggleAirplaneMode(true);
if (!alreadyDeletedResidualFile) {
// TODO: doesn't happen more than once? right?
deleteFile(SleepMonitoringService.SLEEP_DATA);
alreadyDeletedResidualFile = true;
}
final SharedPreferences.Editor ed = getSharedPreferences(SERVICE_IS_RUNNING,
Context.MODE_PRIVATE).edit();
ed.putBoolean(SERVICE_IS_RUNNING, true);
ed.commit();
return null;
}
}.execute();
}
return START_STICKY;
}
public float getAlarmTriggerSensitivity() {
return alarmTriggerSensitivity;
}
public int getAlarmWindow() {
return alarmWindow;
}
public List<PointD> getData() {
return sleepData;
}
public boolean getUseAlarm() {
return useAlarm;
}
public boolean getForceScreenOn() {
return forceScreenOn;
}
private void registerAccelerometerListener() {
final SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
sensorManager.registerListener(this,
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), sensorDelay);
}
private void toggleAirplaneMode(final boolean enabling) {
if (airplaneMode) {
Settings.System.putInt(getContentResolver(), Settings.System.AIRPLANE_MODE_ON,
enabling ? 1 : 0);
final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
intent.putExtra("state", enabling);
sendBroadcast(intent);
}
}
private void toggleSilentMode(final boolean enabling) {
if (silentMode) {
final AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
if (enabling) {
ringerModeBackup = audioManager.getRingerMode();
audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
} else {
audioManager.setRingerMode(ringerModeBackup);
}
}
}
private void triggerAlarmIfNecessary(final long currentTime, final double y) {
if (useAlarm && y >= alarmTriggerSensitivity) {
// TODO: stop calling calculateNextAlert here... battery waster
final Alarm alarm = Alarms.calculateNextAlert(this);
if (alarm != null) {
final Calendar alarmTime = Calendar.getInstance();
alarmTime.setTimeInMillis(alarm.time);
alarmTime.add(Calendar.MINUTE, alarmWindow * -1);
final long alarmMillis = alarmTime.getTimeInMillis();
if (currentTime >= alarmMillis) {
final SharedPreferences alarmPrefs = getSharedPreferences(
SettingsActivity.PREFERENCES, 0);
final int id = alarmPrefs.getInt(Alarms.PREF_SNOOZE_ID, -1);
// if not already snoozing off ANY alarm, trigger the
// alarm
if (id == -1) {
// add 1 second delay to make it less likely that we
// skip the alarm
Alarms.enableAlert(this, alarm, System.currentTimeMillis() + 1000);
}
}
}
}
}
private void unregisterAccelerometerListener() {
final SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(this);
}
/**
* An implementation of a binder that merely hands off the service its running within.
*/
public class ServiceBinder extends Binder {
/** Return the service. */
public SleepMonitoringService getService() {
return SleepMonitoringService.this;
}
}
}