So I've been writing an app that determines your running speed, and integrates that into a workout routine. As I was new to Android, I did everything in an Activity but I've reached a point where I want to divert the speed calculation portion, and everything involving the GPS to a service, which will be bound to whatever workout is going to need it.
This is the workout activity:
package com.example.stropheum.speedcalculatortest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.IBinder;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.os.Vibrator;
import com.example.stropheum.speedcalculatortest.SpeedCalculationService.SpeedCalculationBinder;
public class SpeedAlarmActivity extends ActionBarActivity {
public SpeedCalculationService speedCalculator;
boolean isBound = false;
final int MILLI_TO_SEC = 1000;
final int SEC_TO_HOUR = 3600;
double currentPace, goalPace;
String paceText;
Vibrator vibrator;
// Allow 15 seconds of error for time calculations
final double MILE_TIME_ERROR = 0.25;
LocationManager locationManager;
LocationListener locationListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent i = new Intent(this, SpeedCalculationService.class);
startService(i);
bindService(i, speedConnection, Context.BIND_AUTO_CREATE);
// Starts the service for calulating user's speed
//startService(new Intent(getBaseContext(), SpeedCalculationService.class)); // was below bind before
vibrator = (Vibrator) this.getSystemService(Context.VIBRATOR_SERVICE);
setContentView(R.layout.activity_speed_alarm);
double startTime, elapsedTime;
double alertIntervalStart, alertIntervalElapsed;
double speed, goalPace, currentPace;
String paceText = "Waiting for GPS signal";
updatePaceText(paceText);
if (isBound);
// Delays workout until the service finds a signal
while (!speedCalculator.gpsSignalFound());
// Once GPS connection is established, being the workout
paceText = "Begin!";
updatePaceText(paceText);
///////////////////
// Part One Begins
/////////////////
startTime = System.currentTimeMillis();
elapsedTime = 0;
alertIntervalStart = startTime; // Initialize 30 second timer on workout start
goalPace = 10.0;
updateGoalPace(goalPace);
do {
// Update time since last alert
alertIntervalElapsed = System.currentTimeMillis() - alertIntervalStart;
speed = speedCalculator.getCurrentSpeed();
currentPace = 60 / speed;
// Update speed and pace every second
if (elapsedTime >= 1.0 * MILLI_TO_SEC) {
updateSpeed(speed);
updateCurrentPace(currentPace);
}
// Alerts user if 30 seconds have gone by with no change
if (alertIntervalStart >= 30 * MILLI_TO_SEC) {
paceAlert();
alertIntervalStart = System.currentTimeMillis();
}
// If 5 seconds have elapsed and perfect pace, alert user
if (alertIntervalElapsed >= 5 * MILLI_TO_SEC && checkPace(currentPace, goalPace)) {
paceAlert();
alertIntervalStart = System.currentTimeMillis();
}
elapsedTime = System.currentTimeMillis() - startTime;
} while (elapsedTime < 120 * MILLI_TO_SEC);
paceText = "Workout Complete!";
updatePaceText(paceText);
// ///////////////////
// // Part Two Begins
// /////////////////
// startTime = System.currentTimeMillis();
// elapsedTime = 0;
// alertIntervalStart = startTime; // Initialize 30 second timer on workout start
//
// goalPace = 6.0;
//
// do {
//
// elapsedTime = System.currentTimeMillis() - startTime;
// } while (elapsedTime < 60 * MILLI_TO_SEC);
//
//
}
/**
* Checks if the user is running in an acceptable range of the goal pace
* @param currentPace Current speed of the user
* @param goalPace Goal speed of the user
* @return True if the pace is acceptable, false otherwise
*/
private boolean checkPace(double currentPace, double goalPace) {
boolean result = true;
if (currentPace > goalPace + MILE_TIME_ERROR || currentPace < goalPace - MILE_TIME_ERROR) {
result = false;
}
return result;
}
/**
* Updates the display to show the current speed
* @param speed The current speed of the user
*/
private void updateSpeed(double speed) {
final TextView speedVal = (TextView) findViewById(R.id.SpeedVal);
speedVal.setText(String.format("%.2f", speed));
}
/**
* Updates the current estimated mile time
* @param currentPace User's current mile time
*/
private void updateCurrentPace(double currentPace) {
int minutes = (int)currentPace;
int seconds = (int)(((currentPace * 100) % 100) * 0.6);
final TextView emtVal = (TextView) findViewById(R.id.emtVal);
emtVal.setText(String.format("%d:%02d", minutes, seconds));
}
/**
* Updates the current goal mile time
* @param goalPace New goal mile time
*/
private void updateGoalPace(double goalPace) {
int minutes = (int)goalPace;
int seconds = (int)(((goalPace * 100) % 100) * 0.6);
final TextView gmtVal = (TextView) findViewById(R.id.gmtVal);
gmtVal.setText(String.format("%d:%02d", minutes, seconds));
}
/**
* Updates the current pace text
* @param paceText indicator for user;s current speed in relation to goal time
*/
private void updatePaceText(String paceText) {
final TextView pace = (TextView) findViewById(R.id.paceView);
pace.setText(paceText);
}
/**
* Checks current pace and assigns appropriate text
*/
private void paceAlert() {
if (currentPace > goalPace + MILE_TIME_ERROR) {
paceText = "Speed up";
vibrator.vibrate(300);
try {
Thread.sleep(300);
} catch (Exception e) {}
vibrator.vibrate(300);
try {
Thread.sleep(300);
} catch (Exception e) {}
vibrator.vibrate(300);
} else if (currentPace < goalPace - MILE_TIME_ERROR) {
paceText = "Slow Down";
vibrator.vibrate(1000);
} else {
paceText = "Perfect Pace!";
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_speed_alarm, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
locationManager.removeUpdates(locationListener);
// Terminate the speed calculation service
stopService(new Intent(getBaseContext(), SpeedCalculationService.class));
finish();
return;
}
ServiceConnection speedConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
SpeedCalculationBinder binder = (SpeedCalculationBinder) service;
speedCalculator = binder.getService();
isBound = true;
}
@Override
public void onServiceDisconnected(ComponentName name) {
isBound = false;
}
};
}
and this is the service that I've made:
package com.example.stropheum.speedcalculatortest;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
public class SpeedCalculationService extends Service {
final int MILLI_TO_SEC = 1000; // Number of milliseconds in a second
final int SEC_TO_HOUR = 3600; // Number of seconds in an hour
private final IBinder binder = new SpeedCalculationBinder();
LocationManager locationManager;
LocationListener locationListener;
boolean firstLocationCheck = true;
// Tracks distance traveled between location calls
double distanceTraveled = 0;
double speed = 0;
public SpeedCalculationService() {
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// This service runs until it is stopped
Toast.makeText(this, "Service Started", Toast.LENGTH_LONG).show();
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
locationListener = new LocationListener() {
// Tracks the longitude and latitude of the previous and current location calls
double lonNew, lonOld;
double latNew, latOld;
double startTime = System.currentTimeMillis();
double currentTime, timeElapsed;
public void onLocationChanged(Location location) {
if (firstLocationCheck) {
// Prime old locations for first distance calculation
latOld = Math.toRadians(location.getLatitude());
lonOld = Math.toRadians(location.getLongitude());
firstLocationCheck = false;
}
latNew = Math.toRadians(location.getLatitude());
lonNew = Math.toRadians(location.getLongitude());
currentTime = System.currentTimeMillis();
timeElapsed = currentTime - startTime;
distanceTraveled += haversine(latOld, lonOld, latNew, lonNew);
if (distanceTraveled > 1000) { distanceTraveled = 0; } // Handles start errors
speed = distanceTraveled / timeElapsed * MILLI_TO_SEC * SEC_TO_HOUR;
latOld = latNew;
lonOld = lonNew;
}
public void onStatusChanged(String Provider, int status, Bundle extras) {}
public void onProviderEnabled(String provider) {}
public void onProviderDisabled(String provider) {}
};
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);
return START_STICKY;
}
/**
* Computes distance (in miles) between two coordinates using the haversine formula
* @param lat1 latitude of previous location
* @param lon1 longitude of previous location
* @param lat2 latitude of current location
* @param lon2 longitude of current location
* @return distance in miles
*/
private double haversine(double lat1, double lon1, double lat2, double lon2) {
final double EARTH_RADIUS_M = 3959;
double dLon, dLat, a, c, distance;
// Calculate distance traveled using Haversine formula
dLon = lon2 - lon1;
dLat = lat2 - lat1;
a = Math.sin(dLat/2.0) * Math.sin(dLat/2.0) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon/2.0) * Math.sin(dLon/2.0);
System.out.println("a = " + a);
c = 2.0 * Math.atan(Math.sqrt(a));
System.out.println("c = " + c);
distance = EARTH_RADIUS_M * c;
return distance;
}
/**
* Returns the current calculated speed of the user
* @return the current speed in mph format
*/
public double getCurrentSpeed() {
return this.speed;
}
/**
* Method to check if GPS connection is established
* @return true if first location check has been completed
*/
public boolean gpsSignalFound() {
return !this.firstLocationCheck;
}
@Override
public void onDestroy() {
super.onDestroy();
locationManager.removeUpdates(locationListener);
Toast.makeText(this, "Service Stopped", Toast.LENGTH_LONG).show();
}
// Binder class that will bind to the workout activities
public class SpeedCalculationBinder extends Binder {
SpeedCalculationService getService() {
return SpeedCalculationService.this;
}
}
}
I've managed to narrow down that the errors are coming from the methods in the service returning null values when called, and I added a few debugging lines to determine that the isBound variable is never set to true, so the service is not actually binding to the activity, and I'm pretty sure that's why I'm getting the NullPointerException.
Any help would be wonderful, I can't seem to find anything in my searches here or otherwise as to what my issue is. As far as I can tell, I've bound the service properly. Thanks for your time!