Java tutorial
package com.brucegiese.perfectposture; 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.os.IBinder; import android.os.PowerManager; import android.os.Vibrator; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.widget.Toast; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * This class provides a Service for tracking the user device's orientation periodically. * It notifies the user when they are outside of the range parameters for tilt orientation. * It keeps track of metrics which can be fetched. It allows for changing the various * configuration parameters while the service is running. * * This service also updates the database with posture data and sends out a broadcast each * time new data is added to the database. */ public class OrientationService extends Service { private static final String TAG = "com.brucegiese.service"; public static final String NEW_DATA_POINT_INTENT = "com.brucegiese.perfectposture.sample"; public static final String EXTRA_VALUE = "value"; // Z-axis posture value public static final String CHECK_STATUS_INTENT = "com.brucegiese.perfectposture.check"; /** * Is the service running right now? We need to effectively create a singleton object * with the service. The OS cooperates by only calling the constructor once, even if there * are multiple calls to startService(). */ public static boolean sIsRunning = false; private static OrientationService sInstance = null; private static final int LOW_Z_AXIS_POS_THRESHOLD = 20; // units of degrees private static final int LOW_Z_AXIS_NEG_THRESHOLD = -20; // units of degrees private static final int MEDIUM_Z_AXIS_POS_THRESHOLD = 40; // units of degrees private static final int MEDIUM_Z_AXIS_NEG_THRESHOLD = -40; // units of degrees private static final int HIGH_Z_AXIS_POS_THRESHOLD = 50; // units of degrees private static final int HIGH_Z_AXIS_NEG_THRESHOLD = -50; // units of degrees private int mZAxisPosThreshold = MEDIUM_Z_AXIS_POS_THRESHOLD; // sensitivity feature upgrade private int mZAxisNegThreshold = MEDIUM_Z_AXIS_NEG_THRESHOLD; // sensitivity feature upgrade /* * Number of consecutive good/bad samples before we declare a change in posture. * This hysteresis is only for the user's benefit in giving alerts. It's not saved in data. * */ private static final int LOW_POSITIVE_HYSTERESIS = 2; private static final int LOW_NEGATIVE_HYSTERESIS = 10; private static final int MEDIUM_POSITIVE_HYSTERESIS = 4; private static final int MEDIUM_NEGATIVE_HYSTERESIS = 30; private static final int HIGH_POSITIVE_HYSTERESIS = 8; private static final int HIGH_NEGATIVE_HYSTERESIS = 60; private int mPositiveHysteresis = MEDIUM_POSITIVE_HYSTERESIS; private int mNegativeHysteresis = MEDIUM_NEGATIVE_HYSTERESIS; // number of additional consecutive bad posture samples before we issue a reminder private static final int LOW_BAD_REMINDER_THRESHOLD = 10; private static final int MEDIUM_BAD_REMINDER_THRESHOLD = 30; private static final int HIGH_BAD_REMINDER_THRESHOLD = 120; private int mBadPostureReminderThreshold = MEDIUM_BAD_REMINDER_THRESHOLD; private final int CHIN_TUCK_REMINDER_TIME = 15; // units of minutes private final int CHIN_TUCK_REMINDER_DURATION = 1; // units of minutes private static final int UPDATE_INTERVAL = 1; // units of seconds private Orientation mOrientation = null; private ScheduledFuture mScheduledFuture; private boolean mCurrentPostureGood; private int mHysteresisCounter; private int mBadPostureReminderCounter; private int mChinTuckReminderCounter; private boolean mChinTuckReminderState; // Configuration settings sent from main activity. private final boolean DEFAULT_ALERT_NOTIFICATION = true; private boolean mAlertNotification = DEFAULT_ALERT_NOTIFICATION; private final boolean DEFAULT_ALERT_VIBRATION = true; private boolean mAlertVibration = DEFAULT_ALERT_VIBRATION; private final boolean DEFAULT_ALERT_LED = false; private boolean mAlertLed = DEFAULT_ALERT_LED; private final boolean DEFAULT_ALERT_CHIN_TUCK = true; private boolean mChinTuck = DEFAULT_ALERT_CHIN_TUCK; private Context mContext; private Vibrator mVibrator = null; private static final int SERVICE_NOTIFICATION_ID = 1; private static final int POSTURE_NOTIFICATION_ID = 2; private static final int CHIN_TUCK_NOTIFICATION_ID = 3; private static final String SERVICE_NOTIFICATION_TITLE = "serviceNotification"; private static final String POSTURE_NOTIFICATION_TITLE = "postureNotification"; private static final String CHIN_TUCK_NOTIFICATION_TITLE = "chinTuckNotification"; public static final String TURN_ON_SERVICE_ACTION = "com.brucegiese.perfectposture.serviceon"; public static final String TURN_OFF_SERVICE_ACTION = "com.brucegiese.perfectposture.serviceoff"; private NotificationManager mNotificationManager; // These must match the values in preferences.xml // The reason for not using a @string value is that the user can change languages which would // ...change the key if that language is implemented in this app. private static final String PREF_SENSITIVITY = "PREF_SENSITIVITY"; private static final String PREF_NOTIFICATION = "PREF_NOTIFICATION"; private static final String PREF_VIBRATION = "PREF_VIBRATION"; private static final String PREF_LED = "PREF_LED"; private static final String PREF_CHIN_TUCK = "PREF_CHIN_TUCK"; private enum NotificationType { SERVICE_RUNNING, BAD_POSTURE, CHIN_TUCK_REMINDER } private final CommandReceiver mCommandReceiver; public OrientationService() { if (OrientationService.sInstance != null) { Log.e(TAG, "Our assumption that the OS treats service as a singleton is WRONG!"); } OrientationService.sInstance = this; mCommandReceiver = new CommandReceiver(); } @Override public void onCreate() { super.onCreate(); if (mOrientation == null) { mOrientation = new Orientation(this); } if (mVibrator == null) { mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); } mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); mPrefs.registerOnSharedPreferenceChangeListener(prefListener); // listen for changes mContext = this; // needed by Runnable below } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getAction() != null) { switch (intent.getAction()) { case TURN_OFF_SERVICE_ACTION: Log.d(TAG, "onStartCommand(): turning off the service"); stopChecking(); // Tell the Activity to check its status because this might be sent... // ...by a notification, so the Activity might not know about it. Intent bcastIntent = new Intent(CHECK_STATUS_INTENT); LocalBroadcastManager.getInstance(mContext).sendBroadcast(bcastIntent); stopSelf(); // Completely shut down the service break; case TURN_ON_SERVICE_ACTION: Log.d(TAG, "onStartCommand(): turning on the service"); loadSharedPreferences(); startChecking(); break; default: Log.e(TAG, "onStartCommand(): unexpected action: " + intent.getAction()); break; } } else { Log.e(TAG, "Someone started the service without an action, we ignored it"); } return super.onStartCommand(intent, flags, startId); } @Override public IBinder onBind(Intent intent) { // We don't use binding, just start the service with an action. return null; } @Override public void onDestroy() { super.onDestroy(); if (mScheduledFuture != null) { Log.i(TAG, "onDestroy(): mScheduledFuture was not already null"); mScheduledFuture.cancel(true); mScheduledFuture = null; } if (mOrientation != null) { // Just to be safe mOrientation.stopOrienting(); mOrientation = null; } else { Log.e(TAG, "mOrientation was null in onDestroy(). That should never happen."); } mNotificationManager.cancel(SERVICE_NOTIFICATION_ID); // This object is a de-facto singleton OrientationService.sIsRunning = false; OrientationService.sInstance = null; } /** * Start monitoring the user's posture. */ private void startChecking() { mChinTuckReminderCounter = 0; mCurrentPostureGood = true; // start out assuming good posture mHysteresisCounter = 0; mBadPostureReminderCounter = 0; try { if (mOrientation.startOrienting()) { OrientationService.sIsRunning = true; if (mScheduledFuture == null) { // We use an additional thread for the periodic execution task. ScheduledExecutorService mScheduler = Executors.newScheduledThreadPool(1); mScheduledFuture = mScheduler.scheduleAtFixedRate(mDoPeriodicWork, UPDATE_INTERVAL, UPDATE_INTERVAL, TimeUnit.SECONDS); } else { Log.e(TAG, "startChecking() was called when checking was already running"); } } else { Toast.makeText(this, R.string.no_sensors, Toast.LENGTH_LONG).show(); } } catch (Exception e) { Log.e(TAG, "Exception when starting orientation and scheduler: ", e); } // Register the broadcast receiver IntentFilter iFilter = new IntentFilter(); iFilter.addAction(TURN_OFF_SERVICE_ACTION); LocalBroadcastManager.getInstance(this).registerReceiver(mCommandReceiver, iFilter); } /** * Stop monitoring the user's posture. */ private void stopChecking() { // remove any existing notifications sendNotification(NotificationType.BAD_POSTURE, false); sendNotification(NotificationType.CHIN_TUCK_REMINDER, false); OrientationService.sIsRunning = false; if (mScheduledFuture != null) { mScheduledFuture.cancel(true); mScheduledFuture = null; mOrientation.stopOrienting(); } else { Log.e(TAG, "stopChecking() was called when checking wasn't running."); } // Un-register the broadcast receiver LocalBroadcastManager.getInstance(this).unregisterReceiver(mCommandReceiver); } /** * This runs periodically in the background (yet another thread). */ private final Runnable mDoPeriodicWork = new Runnable() { // must be executed in the UI thread @Override public void run() { int z = mOrientation.getZ(); // Enter the data point into the database. Sample sample = new Sample(z, new Date(), measurePosture(z)); sample.save(); // Broadcast the data point Intent bcastIntent = new Intent(NEW_DATA_POINT_INTENT); bcastIntent.putExtra(EXTRA_VALUE, z); // Don't bother adding the date or goodPosture value LocalBroadcastManager.getInstance(mContext).sendBroadcast(bcastIntent); // Apply hysteresis to determine when to alert the user. if (measurePosture(z)) { // Good posture if (!mCurrentPostureGood) { mHysteresisCounter++; if (mHysteresisCounter >= mPositiveHysteresis) { mCurrentPostureGood = true; // posture has been good for long enough mHysteresisCounter = 0; goodPostureAlerts(); } } else { mHysteresisCounter = 0; } } else { // Bad posture if (mCurrentPostureGood) { mHysteresisCounter++; if (mHysteresisCounter >= mNegativeHysteresis) { mCurrentPostureGood = false; // posture has been bad for too long mHysteresisCounter = 0; badPostureAlerts(); mBadPostureReminderCounter = 0; } } else { mHysteresisCounter = 0; // If posture stays bad for too long, remind the user mBadPostureReminderCounter++; if (mBadPostureReminderCounter >= mBadPostureReminderThreshold) { mBadPostureReminderCounter = 0; badPostureAlerts(); } } } // Chin tuck reminder functionality if (mChinTuck) { // if the functionality is enabled // Note that the various types of notification may still be disabled. mChinTuckReminderCounter++; if (!mChinTuckReminderState) { // We're not currently reminding the user to do a chin tuck exercise if (mChinTuckReminderCounter > CHIN_TUCK_REMINDER_TIME * 60 / UPDATE_INTERVAL) { mChinTuckReminderCounter = 0; mChinTuckReminderState = true; sendNotification(NotificationType.CHIN_TUCK_REMINDER, true); vibrate(true); } } else { if (mChinTuckReminderCounter > CHIN_TUCK_REMINDER_DURATION * 60 / UPDATE_INTERVAL) { mChinTuckReminderCounter = 0; mChinTuckReminderState = false; sendNotification(NotificationType.CHIN_TUCK_REMINDER, false); } } } } }; /** * Determine whether the user currently has good posture or bad posture. * * @param angle angle of device Z-axis from the vertical * @return true if good posture */ boolean measurePosture(int angle) { return !(angle > mZAxisPosThreshold || angle < mZAxisNegThreshold); } /** * Send out all alerts associated with a bad posture event */ private void badPostureAlerts() { PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); // Only send out alerts if the screen is active. if (pm.isScreenOn()) { // This was deprecated in API level 20 Log.d(TAG, "Posture is bad!"); vibrate(true); sendNotification(NotificationType.BAD_POSTURE, true); } } /** * Send out all alerts associated with a good posture event */ private void goodPostureAlerts() { PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); // Only send out alerts if the screen is active. if (pm.isScreenOn()) { // This was deprecated in API level 20 Log.d(TAG, "Posture just got good!"); vibrate(false); sendNotification(NotificationType.BAD_POSTURE, false); } } /** * Send or cancel a notification, subject to user settings * * @param n The type of notification to send or cancel * @param send true if send, false if cancel */ private void sendNotification(NotificationType n, boolean send) { String title; String text; int id; int icon; if (mAlertNotification || !send) { // always attempt to cancel pending notifications switch (n) { // Right now, we're not sending any SERVICE_RUNNING notifications. // If we do, then we need two different icons for service running vs bad posture. case SERVICE_RUNNING: title = SERVICE_NOTIFICATION_TITLE; text = getResources().getString(R.string.service_notification_text); id = SERVICE_NOTIFICATION_ID; icon = R.drawable.ic_posture_notif; break; case BAD_POSTURE: title = POSTURE_NOTIFICATION_TITLE; text = getResources().getString(R.string.posture_notification_text); id = POSTURE_NOTIFICATION_ID; icon = R.drawable.ic_posture_notif; break; case CHIN_TUCK_REMINDER: title = CHIN_TUCK_NOTIFICATION_TITLE; text = getResources().getString(R.string.chin_tuck_notification_text); id = CHIN_TUCK_NOTIFICATION_ID; icon = R.drawable.ic_chin_tuck_notif; break; default: Log.e(TAG, "Unknown type given to sendNotification"); return; } if (send) { Intent resultIntent = new Intent(this, PerfectPostureActivity.class); PendingIntent pIntent = PendingIntent.getActivity(this, 0, resultIntent, 0); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).setSmallIcon(icon) .setContentTitle(title).setContentText(text); // Add an action allowing the user to open the application mBuilder.addAction(R.drawable.ic_posture, getString(R.string.open_application), pIntent); // Add an action to turn off the service (needs to be a broadcast intent) Intent turnOffIntent = new Intent(this, OrientationService.class); turnOffIntent.setAction(TURN_OFF_SERVICE_ACTION); PendingIntent pendingIntentTurnOff = PendingIntent.getService(this, 0, turnOffIntent, 0); // ic_action_halt icon is from Opoloo, covered by Attribution-ShareAlike 4.0 license // http://creativecommons.org/licenses/by-sa/4.0/ // icons are at http://www.opoloo.com/ mBuilder.addAction(R.drawable.ic_action_halt, getString(R.string.turn_off_service), pendingIntentTurnOff); mNotificationManager.notify(id, mBuilder.build()); } else { // remove the notification mNotificationManager.cancel(id); } } } /** * Vibrate the device to signal a good or bad posture, subject to user config settings. * * @param longInterval if true, long vibration, otherwise short vibration time */ private void vibrate(boolean longInterval) { if (mAlertVibration) { int vibrationTime = 30; // short vibration time, units of milliseconds if (longInterval) { vibrationTime = 800; // long vibration time, units of milliseconds } if (mVibrator != null) { mVibrator.vibrate(vibrationTime); } } } /** * Listen for changes in the application's shared preferences. * <p/> * This is the recommended way of implementing the listener for changes in shared preferences * Otherwise, the OS will garbage collect the listener. This creates a strong reference. */ private final SharedPreferences.OnSharedPreferenceChangeListener prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { setupPreference(key); } }; private void setupPreference(String key) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); switch (key) { case PREF_NOTIFICATION: mAlertNotification = sharedPrefs.getBoolean(PREF_NOTIFICATION, DEFAULT_ALERT_NOTIFICATION); if (!mAlertNotification) { // remove all notifications that might be currently displayed sendNotification(NotificationType.CHIN_TUCK_REMINDER, false); sendNotification(NotificationType.BAD_POSTURE, false); // sendNotification(NotificationType.SERVICE_RUNNING, false); } break; case PREF_VIBRATION: mAlertVibration = sharedPrefs.getBoolean(PREF_VIBRATION, DEFAULT_ALERT_VIBRATION); break; case PREF_LED: mAlertLed = sharedPrefs.getBoolean(PREF_LED, DEFAULT_ALERT_LED); break; case PREF_CHIN_TUCK: mChinTuck = sharedPrefs.getBoolean(PREF_CHIN_TUCK, DEFAULT_ALERT_CHIN_TUCK); if (!mChinTuck) { // remove any chin tuck notification that might be currently displayed sendNotification(NotificationType.CHIN_TUCK_REMINDER, false); } break; case PREF_SENSITIVITY: // These are split out because GraphFragment needs them, too. mZAxisPosThreshold = getZAxisPositiveThreshold(this); mZAxisNegThreshold = getZAxisNegativeThreshold(this); switch (Integer.valueOf(sharedPrefs.getString(PREF_SENSITIVITY, "2"))) { case 1: mPositiveHysteresis = LOW_POSITIVE_HYSTERESIS; mNegativeHysteresis = LOW_NEGATIVE_HYSTERESIS; mBadPostureReminderThreshold = LOW_BAD_REMINDER_THRESHOLD; break; case 2: mPositiveHysteresis = MEDIUM_POSITIVE_HYSTERESIS; mNegativeHysteresis = MEDIUM_NEGATIVE_HYSTERESIS; mBadPostureReminderThreshold = MEDIUM_BAD_REMINDER_THRESHOLD; break; case 3: mPositiveHysteresis = HIGH_POSITIVE_HYSTERESIS; mNegativeHysteresis = HIGH_NEGATIVE_HYSTERESIS; mBadPostureReminderThreshold = HIGH_BAD_REMINDER_THRESHOLD; break; default: Log.e(TAG, "invalid sensitivity setting"); break; } break; } } /** * Read in the various configuration settings via the shared preferences. */ private void loadSharedPreferences() { // the getAll() method isn't going to work with the support library ArrayMap. // so just grab each value one-by-one. setupPreference(PREF_NOTIFICATION); setupPreference(PREF_VIBRATION); setupPreference(PREF_LED); setupPreference(PREF_CHIN_TUCK); setupPreference(PREF_SENSITIVITY); } /** * Get the user configured value for the Z-Axis positive threshold. * This is needed by the charting function to draw the dotted red limit lines. * @param c Context * @return Z-Axis positive threshold */ public static int getZAxisPositiveThreshold(Context c) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(c); switch (Integer.valueOf(sharedPrefs.getString(PREF_SENSITIVITY, "2"))) { case 1: return LOW_Z_AXIS_POS_THRESHOLD; case 2: return MEDIUM_Z_AXIS_POS_THRESHOLD; case 3: return HIGH_Z_AXIS_POS_THRESHOLD; default: Log.e(TAG, "invalid sensitivity setting"); return MEDIUM_Z_AXIS_POS_THRESHOLD; // just use the default } } /** * Get the user configured value for the Z-Axis negative threshold. * This is needed by the charting function to draw the dotted red limit lines. * @param c Context * @return Z-Axis negative threshold */ public static int getZAxisNegativeThreshold(Context c) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(c); switch (Integer.valueOf(sharedPrefs.getString(PREF_SENSITIVITY, "2"))) { case 1: return LOW_Z_AXIS_NEG_THRESHOLD; case 2: return MEDIUM_Z_AXIS_NEG_THRESHOLD; case 3: return HIGH_Z_AXIS_NEG_THRESHOLD; default: Log.e(TAG, "invalid sensitivity setting"); return MEDIUM_Z_AXIS_NEG_THRESHOLD; // just use the default } } /** * Receive intents telling us to stop the service (these are actions within notifications) */ class CommandReceiver extends BroadcastReceiver { public CommandReceiver() { } @Override public void onReceive(Context c, Intent i) { if (i.getAction().equals(TURN_OFF_SERVICE_ACTION)) { stopChecking(); } else { Log.e(TAG, "Received an unexpected broadcast intent"); } } } }