1

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!

Onik
  • 19,396
  • 14
  • 68
  • 91
Dale Diaz
  • 11
  • 3
  • You need to call startService(i); before bindService – l-l May 20 '15 at 20:43
  • 1
    ALso- this code is just bad. You're going to break for a dozen other reasons. You can't just wait in onCreate, you're going to trip the watchdog and be killed. Even if you didn't, its a horrible user experience. – Gabe Sechan May 20 '15 at 20:50
  • I don't actually understand what you're trying to tell me. What do you mean by waiting? What do you mean by trip the watchdog? Why is it a horrible user experience? – Dale Diaz May 20 '15 at 21:02

1 Answers1

0

I don't see you starting the service, you should do something like:

    Intent intent = new Intent(this, SpeedCalculationService.class);
    startService(intent);
    bindService(intent, speedConnection, Context.BIND_AUTO_CREATE);

You need to make sure you are using the same intent when starting and binding the service.

Here is some documentation for "Context.BIND_AUTO_CREATE":

 * Flag for {@link #bindService}: automatically create the service as long
 * as the binding exists.  Note that while this will create the service,
 * its {@link android.app.Service#onStartCommand}
 * method will still only be called due to an
 * explicit call to {@link #startService}.  Even without that, though,
 * this still provides you with access to the service object while the
 * service is created.

Notice that onCommandStart is only called with an explicit call to startService.

l-l
  • 3,804
  • 6
  • 36
  • 42
  • I had that initially, and I read that binding a service starts it anyway, so I figured that was causing some kind of conflict. I just added that line and the error is exactly the same – Dale Diaz May 20 '15 at 20:58
  • you cannot add the same line you had. You need to use the same intent – l-l May 20 '15 at 21:00
  • You should me sure your code reaches the onServiceConnected in your ServiceConnection. And then start anything related to the service from there after you are sure its has been connected – l-l May 20 '15 at 21:09