Location tracking

A few of you might know that I spend a lot of time helping on stackoverflow.com. One of the most ironic and frustrating things is when stackoverflow itself is the source of bad information. Sometimes bad code that mostly works gets voted up, and its impossible to fix it. So here’s my attempt to take one of those and do it right, in the hope I can help future newbies.

A common question asked is how to track your location via GPS. It’s a moderately complex API with a few corner cases that can trip up a beginner. So of course everyone wants to simplify it. Here’s the most common abstraction I see used- GPSTracker.

 
public class GPSTracker extends Service implements LocationListener {

    private final Context mContext;

    // Flag for GPS status
    boolean isGPSEnabled = false;

    // Flag for network status
    boolean isNetworkEnabled = false;

    // Flag for GPS status
    boolean canGetLocation = false;

    Location location; // Location
    double latitude; // Latitude
    double longitude; // Longitude

    // The minimum distance to change Updates in meters
    private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 10; // 10 meters

    // The minimum time between updates in milliseconds
    private static final long MIN_TIME_BW_UPDATES = 1000 * 60 * 1; // 1 minute

    // Declaring a Location Manager
    protected LocationManager locationManager;

    public GPSTracker(Context context) {
        this.mContext = context;
        getLocation();
    }

    public Location getLocation() {
        try {
            locationManager = (LocationManager) mContext
                    .getSystemService(LOCATION_SERVICE);

            // Getting GPS status
            isGPSEnabled = locationManager
                    .isProviderEnabled(LocationManager.GPS_PROVIDER);

            // Getting network status
            isNetworkEnabled = locationManager
                    .isProviderEnabled(LocationManager.NETWORK_PROVIDER);

            if (!isGPSEnabled && !isNetworkEnabled) {
                // No network provider is enabled
            } else {
                this.canGetLocation = true;
                if (isNetworkEnabled) {
                    locationManager.requestLocationUpdates(
                            LocationManager.NETWORK_PROVIDER,
                            MIN_TIME_BW_UPDATES,
                            MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
                    Log.d("Network", "Network");
                    if (locationManager != null) {
                        location = locationManager
                                .getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
                        if (location != null) {
                            latitude = location.getLatitude();
                            longitude = location.getLongitude();
                        }
                    }
                }
                // If GPS enabled, get latitude/longitude using GPS Services
                if (isGPSEnabled) {
                    if (location == null) {
                        locationManager.requestLocationUpdates(
                                LocationManager.GPS_PROVIDER,
                                MIN_TIME_BW_UPDATES,
                                MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
                        Log.d("GPS Enabled", "GPS Enabled");
                        if (locationManager != null) {
                            location = locationManager
                                    .getLastKnownLocation(LocationManager.GPS_PROVIDER);
                            if (location != null) {
                                latitude = location.getLatitude();
                                longitude = location.getLongitude();
                            }
                        }
                    }
                }
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return location;
    }


    /**
     * Stop using GPS listener
     * Calling this function will stop using GPS in your app.
     * */
    public void stopUsingGPS(){
        if(locationManager != null){
            locationManager.removeUpdates(GPSTracker.this);
        }
    }


    /**
     * Function to get latitude
     * */
    public double getLatitude(){
        if(location != null){
            latitude = location.getLatitude();
        }

        // return latitude
        return latitude;
    }


    /**
     * Function to get longitude
     * */
    public double getLongitude(){
        if(location != null){
            longitude = location.getLongitude();
        }

        // return longitude
        return longitude;
    }

    /**
     * Function to check GPS/Wi-Fi enabled
     * @return boolean
     * */
    public boolean canGetLocation() {
        return this.canGetLocation;
    }


    /**
     * Function to show settings alert dialog.
     * On pressing the Settings button it will launch Settings Options.
     * */
    public void showSettingsAlert(){
        AlertDialog.Builder alertDialog = new AlertDialog.Builder(mContext);

        // Setting Dialog Title
        alertDialog.setTitle("GPS is settings");

        // Setting Dialog Message
        alertDialog.setMessage("GPS is not enabled. Do you want to go to settings menu?");

        // On pressing the Settings button.
        alertDialog.setPositiveButton("Settings", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog,int which) {
                Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                mContext.startActivity(intent);
            }
        });

        // On pressing the cancel button
        alertDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
            dialog.cancel();
            }
        });

        // Showing Alert Message
        alertDialog.show();
    }


    @Override
    public void onLocationChanged(Location location) {
    }


    @Override
    public void onProviderDisabled(String provider) {
    }


    @Override
    public void onProviderEnabled(String provider) {
    }


    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
    }


    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }
}

So lets take a look at the way it tries to simplify the location API and the issues it has. Well, we can start with the name- it calls itself GPSTracker. Not bad, I assume it tracks your GPS location. The problem is- it doesn’t track GPS. It tracks location from the GPS with fallback to network. So right there you have a bit problem- if you need the accuracy of GPS, it isn’t going to necessarily give it to you. You’d have to know that fact and double check on your own (and the fact you can do that is probably an accident, not by design). No beginner is going to get that, most people who haven’t used the LocationManager API would get tripped up. Similarly on names, we have canGetLocation- which sounds like it should be true only if a location exists. It actually returns true if there’s an enabled provider, regardless of if a location has been successfully gotten. So you can call getLocation and still get NULL, even if canGetLocation returns true.

What else? Well, it calls getLastKnownLocation at startup to try and get the location when it starts. The problem is that getLastKnownLocation can be stale- really stale. You shouldn’t assume that value is valid. Yet with this class not only do they assume its valid, there’s no way to tell if you’re getting this stale value or a new value from the onLocationChanged callback. Its simpler, but it isn’t right- you could be using a very old and very invalid value. Sometimes that’s ok, but the client need to decide that. Using a 10 minute old value on a navigation app is probably not ok. Using it in a local weather app may be. But the library can’t know that.

Then there’s the notification method. There is no callback, you have to poll the class to get location changes. That may seem easier, but if you need updates its shifting the responsibility of both checking for changes and for setting up the polling mechanism onto the client. That’s not the right way to do things. Finally there’s the getLatitude and getLongitude functions. The idea isn’t horrible for a polled class, but how often do you need one but not both? In addition, these functions return defaults or stale values if the last Location object was NULL. But there’s no way to know that without reading the entire class- so a lot of questions ask “Why am I getting back 0s” instead of “Why am I not getting a location”, making it harder to debug. At the very least if you want to do it this way your should return an error value, not 0.

So what does it do right? The code will compile and run. It makes it easy to start and stop. You don’t need to implement a bunch of listener functions you don’t need. Polling makes sense for some apps. It removes the need of setting minimum times and distances. Picking good defaults for those is probably the right thing to do for most apps- and those who need more accuracy can write their own. We can keep those. But we have to keep a way of either telling if a value is stale or not. We need to separate out network and GPS tracking- or at least make it explicit that its doing fallback (which can be useful in some cases). We need to provide at least an option for callbacks, although we’d like to keep the ability to poll too.

So here’s my solution for the same problem. First, we have a generic LocationTracker interface. There’s a variety of ways you can track location, it would be nice to smoothly switch between them.

package com.gabesechan.android.reusable.location;

import android.location.Location;

public interface LocationTracker {
	public interface LocationUpdateListener{
		public void onUpdate(Location oldLoc, long oldTime, Location newLoc, long newTime);
	}
	
    public void start();
    public void start(LocationUpdateListener update);
    
    public void stop();

    public boolean hasLocation();
    
    public boolean hasPossiblyStaleLocation();
    
    public Location getLocation();
    
    public Location getPossiblyStaleLocation();
    
}

Notice that we have 2 start methods- one allows you to set a listener to get callbacks on, one doesn’t. We also break getLocation and hasLocation into a version that only gets fresh data, and one that may return stale data.

Next we have out ProviderLocationTracker class. This class will allow you to track via GPS or network, but will only use one at a time.

package com.gabesechan.android.reusable.location;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;

public class ProviderLocationTracker implements LocationListener, LocationTracker {

    // The minimum distance to change Updates in meters
    private static final long MIN_UPDATE_DISTANCE = 10; 

    // The minimum time between updates in milliseconds
    private static final long MIN_UPDATE_TIME = 1000 * 60; 

    private LocationManager lm;

    public enum ProviderType{
    	NETWORK,
    	GPS
    };    
    private String provider;
    
    private Location lastLocation;
    private long lastTime;
    
    private boolean isRunning;
    
    private LocationUpdateListener listener;
    
    public ProviderLocationTracker(Context context, ProviderType type) {
    	lm = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
    	if(type == ProviderType.NETWORK){
    		provider = LocationManager.NETWORK_PROVIDER;
    	}
    	else{
    		provider = LocationManager.GPS_PROVIDER;
    	}
    }
    
    public void start(){
    	if(isRunning){
    		//Already running, do nothing
    		return;
    	}
    	
		//The provider is on, so start getting updates.  Update current location
		isRunning = true;
		lm.requestLocationUpdates(provider, MIN_UPDATE_TIME, MIN_UPDATE_DISTANCE, this);
		lastLocation = null;
		lastTime = 0;
		return;
    }
    
	public void start(LocationUpdateListener update) {
		start();
		listener = update;
		
	}
 
    
    public void stop(){
    	if(isRunning){
    		lm.removeUpdates(this);
    		isRunning = false;
    		listener = null;
    	}
    }

	public boolean hasLocation(){
    	if(lastLocation == null){
    		return false;
    	}
    	if(System.currentTimeMillis() - lastTime > 5 * MIN_UPDATE_TIME){
    		return false; //stale
    	}
    	return true;
    }
    
    public boolean hasPossiblyStaleLocation(){
    	if(lastLocation != null){
    		return true;
    	}
    	return lm.getLastKnownLocation(provider)!= null;
    }
    
    public Location getLocation(){
    	if(lastLocation == null){
    		return null;
    	}
    	if(System.currentTimeMillis() - lastTime > 5 * MIN_UPDATE_TIME){
    		return null; //stale
    	}
    	return lastLocation;
    }
    
    public Location getPossiblyStaleLocation(){
    	if(lastLocation != null){
        	return lastLocation;
    	}
    	return lm.getLastKnownLocation(provider);
    }

	public void onLocationChanged(Location newLoc) {
		long now = System.currentTimeMillis();
		if(listener != null){
			listener.onUpdate(lastLocation, lastTime, newLoc, now);
		}
		lastLocation = newLoc;
		lastTime = now;
	}

	public void onProviderDisabled(String arg0) {
		
	}

	public void onProviderEnabled(String arg0) {
		
	}

	public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
	}
}

Notice a few changes here. We aren’t a service. We can be owned by a Service or Activity, whatever makes sense for the app. We also get rid of the UI elements, because that shouldn’t be at our level of detail- the client should decide how we look. I decided to make the values passed by the provider become stale after 5 minutes, and I avoid using anything from getLastKnownLocation, which returns possibly stale data, unless the user explicitly asks for stale data. We can even call start a second time to change (or get rid of) the callback function. Speaking of callbacks, we provide both the current and last locations and their associated time, which can be useful in processing (side note: I don’t use the embedded times because I want to support versions back to 14, the time was added in 17). And the same exact code will work for GPS or network, its just a parameter to the constructor. Looks like a win to me.

The one thing that we didn’t keep from that is fallback from GPS to network. Sometimes that does help. So lets add a class that does explicitly that:

package com.gabesechan.android.reusable.location;

import android.content.Context;
import android.location.Location;
import android.location.LocationManager;

public class FallbackLocationTracker  implements LocationTracker, LocationTracker.LocationUpdateListener {

    
    private boolean isRunning;
    
    private ProviderLocationTracker gps;
    private ProviderLocationTracker net;
    
    private LocationUpdateListener listener;
    
    Location lastLoc;
    long lastTime;
    
    public FallbackLocationTracker(Context context, ProviderLocationTracker.ProviderType type) {
    	gps = new ProviderLocationTracker(context, ProviderLocationTracker.ProviderType.GPS);
    	net = new ProviderLocationTracker(context, ProviderLocationTracker.ProviderType.NETWORK);
    }
    
    public void start(){
    	if(isRunning){
    		//Already running, do nothing
    		return;
    	}
    	
    	//Start both
    	gps.start(this);
    	net.start(this);
    	isRunning = true;
    }

	public void start(LocationUpdateListener update) {
		start();
		listener = update;
	}

    
    public void stop(){
    	if(isRunning){
        	gps.stop();
        	net.stop();
    		isRunning = false;
    		listener = null;
    	}
    }

	public boolean hasLocation(){
		//If either has a location, use it
		return gps.hasLocation() || net.hasLocation();
    }
    
    public boolean hasPossiblyStaleLocation(){
		//If either has a location, use it
		return gps.hasPossiblyStaleLocation() || net.hasPossiblyStaleLocation();
    }
    
    public Location getLocation(){
    	Location ret = gps.getLocation();
    	if(ret == null){
    		ret = net.getLocation();
    	}
    	return ret;
    }
    
    public Location getPossiblyStaleLocation(){
    	Location ret = gps.getPossiblyStaleLocation();
    	if(ret == null){
    		ret = net.getPossiblyStaleLocation();
    	}
    	return ret;
    }

	public void onUpdate(Location oldLoc, long oldTime, Location newLoc, long newTime) {
		boolean update = false;

		//We should update only if there is no last location, the provider is the same, or the provider is more accurate, or the old location is stale
		if(lastLoc == null){
			update = true;
		}
		else if(lastLoc != null && lastLoc.getProvider().equals(newLoc.getProvider())){
			update = true;
		}
		else if(newLoc.getProvider().equals(LocationManager.GPS_PROVIDER)){
			update = true;
		}
		else if (newTime - lastTime > 5 * 60 * 1000){
			update = true;
		}
		
		if(update){
			lastLoc = newLoc;
			lastTime = newTime;
			if(listener != null){
				listener.onUpdate(lastLoc, lastTime, newLoc, newTime);					
			}
		}
	}
}

Notice the use of layering here- we already have LocationTracker objects for GPS and network, so lets not rewrite all of that- lets use them instead. Using them, all we have to do is pick the value from the correct provider for all of the polling mode functions. We do need to put in some override logic to figure out when to call the updater, but its a simple matter of figuring out whether the new location is more accurate than the old one. And we can trivially switch between the two implementations by changing what class you instantiate.

There you go- a cleaner interface, the ability to use multiple types of tracking, and no more problems with stale data. Let’s try to get this used instead of the others.

This Post Has Been Viewed 219 Times

One thought on “Location tracking

Leave a Reply