package com.notificationFramework.stimulusStrategy; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.IBinder; import android.os.SystemClock; import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.notificationFramework.sedentary.frontEnd.R; import com.notificationFramework.sedentary.frontEnd.SaveFile; import java.util.Calendar; /** * Created by Peter De Jonckheere on 09/01/2018. * <p> * The strategy which detects sedentary behaviour using the Accelerometer sensor provided by the * Android hardware. It uses the previous readings to determine if movement has occurred. Upon * determining a significant enough movement which lasts for a period of 15 seconds, an alarm which * is set for one notification period from the current time is cancelled. The Accelerometer sensor * reports in a streaming fashion and so uses significant power on the device and the listener for * the sensor is constantly registered. * </p> */ public class Accelerometer extends Service implements StimulusStrategy, SensorEventListener { /** * The alarm manager instance which is used to manage alarms via the OS */ private AlarmManager am; /** * An instance of Shared Preferences used throughout the class to obtain and save settings */ private SharedPreferences preferences; /** * The previous 3 readings of the accelerometer */ private float[] history = new float[3]; /** * The previous timestamp of the accelerometer reading */ private long historyTime; /** * The last recorded number of minutes moved */ private int prevMinutes = 0; /** * The current number of minutes moved */ private int minutes; /** * The current number of seconds moved up to 60 */ private double seconds = 0; /** * The arbitrary default value of sensitivity */ private double sensitivity = 0.8; /** * An enum which defines values which can be used to alter the sensitivity from within the * application */ private enum sensitivityLevel { ZERO, VERY_HIGH, HIGH, NORMAL, LOW, VERY_LOW; } /** * The method which is called when the Accelerometer service is started. Sets up a number of * fields then delegates for the daily progress and clock to be set up. * * @param intent the intent used to start this service * @param flags additional information about this service * @param startId the unique identifier for this service * @return the conditions under which the OS should treat this service * @see android.app.Service */ @Override public int onStartCommand(Intent intent, int flags, int startId) { historyTime = SystemClock.elapsedRealtimeNanos(); preferences = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE); sensitivity = sensitivity * sensitivityLevel.valueOf( preferences.getString(getString(R.string.accel_sensitivity), "NORMAL")).ordinal(); SensorManager mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE); //1 second used as the sampling period try { mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), 600000000); } catch (NullPointerException e) { Log.e("SENSOR", "FAILED TO FIND ACCELEROMTER"); } am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); setUpDailyProgress(); setUpClock(); return START_STICKY; } /** * Uses the Shared Preferences to set up the daily progress and determine if the daily progress * should be reset for a new day. This is done by storing the current day in the Shared * Preferences and only changing this when the current calendar does not match this day. */ private void setUpDailyProgress() { int currentDay = Calendar.getInstance().get(Calendar.DAY_OF_YEAR); if (currentDay == preferences.getInt(getString(R.string.progress_day), 0)) { minutes = preferences.getInt(getString(R.string.daily_progress), 0); if (minutes >= preferences.getInt(getString(R.string.daily_goal_set), getResources().getInteger(R.integer.daily_goal_minutes)) && !(preferences.getBoolean((getString(R.string.goal_met)), false))) { goalNotify(); preferences.edit().putBoolean(getString(R.string.goal_met), true).commit(); } } else { minutes = 0; SharedPreferences.Editor editor = preferences.edit(); editor.putInt(getString(R.string.progress_day), currentDay); editor.putInt(getString(R.string.daily_progress), minutes); editor.putBoolean(getString(R.string.goal_met), false); editor.commit(); } prevMinutes = minutes; } /** * Sets up the alarm which will trigger the broadcast to the stimulus class, hence triggering a * notification. The true implementation uses the notification period stored in Shared * Preferences and a test implementation is also present which uses an arbitrary short time. */ private void setUpClock() { Intent i = new Intent(getBaseContext(), com.notificationFramework.stimulus.SedentaryStimulus.class); PendingIntent pi = PendingIntent.getBroadcast(getBaseContext(), R.integer.alarm_rc, i, PendingIntent.FLAG_UPDATE_CURRENT); int interval = preferences.getInt(getString(R.string.daily_goal), getResources().getInteger(R.integer.notify_period_minutes)); am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + (1000 * 60 * interval), pi); //test // am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 3000, pi); } /** * The monitor method is called when a movement is identified and uses identical intents and * pending intents to cancel the alarm set previously. This is also recorded as a movement in * the log and a new alarm is set. */ public void monitor() { Intent i = new Intent(getBaseContext(), com.notificationFramework.stimulus.SedentaryStimulus.class); PendingIntent pi = PendingIntent.getBroadcast(getBaseContext(), R.integer.alarm_rc, i, PendingIntent.FLAG_UPDATE_CURRENT); am.cancel(pi); collectData(); SaveFile.recordNotification(0, 0, 1,0, this); setUpClock(); } /** * A method added after the user trial to trigger once daily if a goal has been reached. A local * broadcast is then sent to the relevant stimulus class and on to the relevant notification * class. */ public void goalNotify() { Intent i = new Intent(getBaseContext(), com.notificationFramework.stimulus.GoalStimulus.class); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); lbm.sendBroadcast(i); } /** * Required to be implemented by implementing the SensorEventListener interface. Carries out the * main work of the class in determining if a movement has taken place based on the values * passed in in the event param and the previous values stored. This is done by determining if * multiple values have changed either positively or negatively by the sensitivity level. In * this case the timestamp is then used to determine the length of time for which the movement * lasts and this is recorded as the minute value in Shared Preferences. * * @param event the sensor event which has occurred (here an Accelerometer event) * @see android.hardware.SensorEventListener */ @Override public void onSensorChanged(SensorEvent event) { int moved = 0; //For each x,y and z plane determine if significant movement has occurred. for (int i = 0; i < history.length; i++) { if ((-1 * sensitivity) > (history[i] - event.values[i]) || (history[i] - event.values[i]) > sensitivity) { if (!((history[i] - event.values[i]) > 10 && !(-10 > (history[i]) - event.values[i]))) { moved++; } } } //Update the previous values for (int i = 0; i < history.length; i++) { history[i] = event.values[i]; } //If all 3 planes had movement if (moved >= 3) { //Update seconds and cancel the alarm if the time is greater than the last seconds //value if (((event.timestamp - historyTime) / 1000000000) > (seconds + 15)) { seconds = seconds + 15; monitor(); } //Update minutes if the time is greater than a minute or seconds has reached 60 if ((((event.timestamp - historyTime) / 1000000000) > 60) || (seconds > 60)) { historyTime = event.timestamp; seconds = 0; minutes++; } } //If movement has not occurred update minutes else if (minutes > prevMinutes) { SharedPreferences.Editor editor = preferences.edit(); editor.putInt(getString(R.string.daily_progress), minutes); editor.commit(); prevMinutes = minutes; } } private void collectData(){ } /** * Unused as accuracy is not a major factor when using previous values. Required by * SensorEventListener * * @param sensor as in SensorEventListener * @param i as in SensorEventListener * @see android.hardware.SensorEventListener */ @Override public void onAccuracyChanged(Sensor sensor, int i) { } /** * The method which reponds to a bind request for this service. * * @param intent the intent used to rqeuest binding of this service * @return null as no bind requests are required. * @see android.app.Service **/ @Nullable @Override public IBinder onBind(Intent intent) { return null; } }