Thursday 2 August 2012

Android getting the best user Location

Getting the most accurate user location on an android device can turn out to be a harder task then you initially wanted. From looking at the developer docs available at http://developer.android.com/reference/android/location/LocationManager.html there are a number of different methods available, so knowing which is best way for your application can be troublesome.

If you just want a rough location for you application you can attempt to get the "last known location". You can query any of the three providers using the following code to get the last fix they had:

Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);

"GPS_PROVIDER" can be replaced with "NETWORK_PROVIDER" or "PASSIVE_PROVIDER" depending on which provider you wish to query.

This can be annoying if you query just one provider and there is no location you then have to query each again. So from this post http://stackoverflow.com/questions/4735942/android-get-location-only-one-time I modified and used the following:

This will loop over the location providers backwards, so that if there is no location from gps it will try network etc.....

    public static Location getLastKnownLocation(Context context) {
        LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        List<String> providers = lm.getProviders(true);


        /* Loop over the array backwards, and if you get an accurate location, then break out the loop*/
        Location l = null;


        for (int i = providers.size() - 1; i >= 0; i--) {
            l = lm.getLastKnownLocation(providers.get(i));
            if (l != null) break;
        }
        return l;
    }

The issue with using getLastKnowLocation is that it might be old or not very accurate. If you want something more accurate for you app you need to write something a bit more complex which involves getting you application to listen to location changes.

To do this I started from this post http://stackoverflow.com/questions/3145089/what-is-the-simplest-and-most-robust-way-to-get-the-users-current-location-in-a/3145655#3145655 and modified slightly for my use. The class created basically follows these steps:

1. Check which providers are enabled. Users may have disabled gps from settings or cell signal / triangulation is not available.
2. Go through the available providers in the following order: gps, cell, passive and start the location listeners for a set time. If a location if not found before the time runs out then move to the next provider.
3. Once a location is found then break out from the listener timers and return this location back to the activity.
4. If no location is found from any provider then go back to using last known location, but what ever is most recent out of the three providers.

Below is the Location class that I use to do all of the above querying, some comments added to explain the process:

public class Location extends Activity {


    private Timer timer1;
    private LocationManager lm;
    private LocationResult locationResult;
    private boolean gps_enabled = false;
    private boolean network_enabled = false;
    private boolean passive_enabled = false;


    public boolean getLocation(Context context, LocationResult result) {
        //use LocationResult callback class to pass location value from Location to the activity.
        locationResult = result;
        if (lm == null)
            lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        // check to see which providers are enabled, and throw exception if not available 
        try {
            gps_enabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
        } catch (Exception ex) {
           // gps not available
        }
        try {
            network_enabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
        } catch (Exception ex) {
           // cell not available
        }


        try {
            passive_enabled = lm.isProviderEnabled(LocationManager.PASSIVE_PROVIDER);
        } catch (Exception ex) {
           // wifi / passive not available
        }


        // if no providers are enabled don't start listeners and return false to the activity
        if (!gps_enabled && !network_enabled && !passive_enabled)
            return false;


        // use gps provider first
        if (gps_enabled) {
            lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListenerGps);
            // start a timer to listen to gps for 25 secs, gps requires longer than the others
            timer1 = new Timer();
            timer1.schedule(new GetLastGPSLocation(), 25000);
            // use network as the second provider
        } else if (network_enabled) {
            lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
           // start a timer to listen to cell triangulation for 15 secs
           timer1 = new Timer();
            timer1.schedule(new GetLastNetworkLocation(), 15000);
            // revert to passive as a last resort
        } else {
            lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, locationListenerPassive);
          // start a timer to listen to wifi passive networks for 15 secs
            timer1 = new Timer();
            timer1.schedule(new GetLastPassiveLocation(), 15000);
        }
        return true;
    }


    private final LocationListener locationListenerGps = new LocationListener() {
        public void onLocationChanged(Location location) {
            // we've got a gps fix, cancel timer, listener and return location
            timer1.cancel();
            locationResult.gotLocation(location);
            lm.removeUpdates(this);
            lm.removeUpdates(locationListenerGps);
        }


        public void onProviderDisabled(String provider) {
        }


        public void onProviderEnabled(String provider) {
        }


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


    private final LocationListener locationListenerNetwork = new LocationListener() {
        public void onLocationChanged(Location location) {
            // we've got a network fix, cancel timer, listener and return location
            timer1.cancel();
            locationResult.gotLocation(location);
            lm.removeUpdates(this);
            lm.removeUpdates(locationListenerNetwork);
        }


        public void onProviderDisabled(String provider) {
        }


        public void onProviderEnabled(String provider) {
        }


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


    private final LocationListener locationListenerPassive = new LocationListener() {
        public void onLocationChanged(Location location) {
            // we've got a passive fix, cancel timer, listener and return location
            timer1.cancel();
            locationResult.gotLocation(location);
            lm.removeUpdates(this);
            lm.removeUpdates(locationListenerPassive);
        }


        public void onProviderDisabled(String provider) {
        }


        public void onProviderEnabled(String provider) {
        }


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


    private class GetLastGPSLocation extends TimerTask {
        @Override
        public void run() {
            runOnUiThread(new Runnable() {
                public void run() {
                    //gps has timed out without a change we should start the next listener
                    lm.removeUpdates(locationListenerGps);
                   if (network_enabled) {
                        lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListenerNetwork);
                        timer1 = new Timer();
                        timer1.schedule(new GetLastNetworkLocation(), 15000);
                    } else if (passive_enabled) {
                        lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, locationListenerPassive);
                        timer1 = new Timer();
                        timer1.schedule(new GetLastPassiveLocation(), 15000);
                    } else {
                        // no more providers enabled, so get most recent
                        returnRecentLocation();
                    }
                }
            });
        }
    }


    private class GetLastNetworkLocation extends TimerTask {
        @Override
        public void run() {
            runOnUiThread(new Runnable() {
                public void run() {
                    //network has timed out without a change we should start next listener
                    lm.removeUpdates(locationListenerNetwork);
                   if (passive_enabled) {
                        lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, locationListenerPassive);
                        timer1 = new Timer();
                        timer1.schedule(new GetLastPassiveLocation(), 15000);
                    } else {
                        // no more providers enabled, so get most recent
                        returnRecentLocation();
                    }
                }
            });
        }
    }


    private class GetLastPassiveLocation extends TimerTask {
        @Override
        public void run() {
            runOnUiThread(new Runnable() {
                public void run() {
                    lm.removeUpdates(locationListenerPassive);
                   //passive has timed out without a change so get most recent
                    returnRecentLocation();
                }
            });
        }
    }


    private void returnRecentLocation() {
        Location net_loc = null, gps_loc = null, passive_loc = null;


        if (gps_enabled)
            gps_loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        if (network_enabled)
            net_loc = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        if (passive_enabled)
            passive_loc = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);


        //if there are both values use the latest one
        if (gps_loc != null && net_loc != null) {
            if (gps_loc.getTime() > net_loc.getTime())
                locationResult.gotLocation(gps_loc);
            else
                locationResult.gotLocation(net_loc);
            return;
        }


        if (gps_loc != null) {
            locationResult.gotLocation(gps_loc);
            return;
        }
        if (net_loc != null) {
            locationResult.gotLocation(net_loc);
            return;
        }
        if (passive_loc != null) {
            locationResult.gotLocation(passive_loc);
            return;
        }


        // we couldn't get anything
        locationResult.gotLocation(null);
    }


    public static abstract class LocationResult {
        public abstract void gotLocation(Location location);
    }
}




Below is how I call the class from my activity:

1. First I check in my local database to ensure my location isn't "old". If my local location is less than 5 minutes I don't query the providers and just use this again.
2. If I need to get a new location I use the following:

Location location = new Location();
providerEnabled = location.getLocation(getApplicationContext(), locationResult);


3. The callback:

   private final Location.LocationResult locationResult = new Location.LocationResult() {
        @Override
        public void gotLocation(final Location location) {
            //Got the location. Save location and date to local db to use in application 
       }
    };

Currently there isn't a much simpler way to query to location providers to get an accurate location. People have written various classes and methods to get the most accurate location without affecting device performance too much. I find that the above class works well and is only called when required from the activity so limits to need to ask for hungry gps resources. 


I'm also happy to take on any tweaks or ideas for the above code.