Search code examples
javaandroidandroid-fusedlocationandroid-googleapiclient

How to receive location updates every 5 minutes using the FusedLocation API


I am currently working on an app that has to check the user's location every five minutes and send the coordinates to a server. I decided to go with the FusedLocation API in Google Play Services instead of the plain old LocationManager API, mainly because I noticed the LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY priority level, which claims to offer a 100-meter accuracy level with reasonable battery usage, which is EXACTLY what I need.

In my case, I have an Activity whose inheritance structure is:

public class MainActivity extends AppCompatActivity implements
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener, LocationListener

and which implements the relevant callbacks (onConnected, onConnectionFailed, onConnectionSuspended, onLocationChanged). I also get an instance of the GoogleApiClient, with this method, as suggested by the official documentation:

protected synchronized GoogleApiClient buildGoogleApiClient() {
        return new GoogleApiClient.Builder(this).addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(LocationServices.API).build();

In onConnected, I start the location updates by using

LocationServices.FusedLocationApi.requestLocationUpdates(mApiClient,
                mLocationRequest, this);

... and capture changes in onLocationChanged().

However, I quickly found out that the location updates seem to stop after a while. Perhaps it's because this method is tied to the Activity lifecycle, I'm not sure. Anyway, I tried to get around this by creating an inner class which extends IntentService and starting it by an AlarmManager. So in onConnected, I ended up doing this:

AlarmManager alarmMan = (AlarmManager) this
                .getSystemService(Context.ALARM_SERVICE);

        Intent updateIntent = new Intent(this, LocUpService.class);

        PendingIntent pIntent = PendingIntent.getService(this, 0, updateIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        alarmMan.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 0,
                1000 * 60 * 5, pIntent);

The LocUpService class looks like this:

public static class LocUpService extends IntentService {

        public LocUpService() {
            super("LocUpService");

        }

        @Override
        protected void onHandleIntent(Intent intent) {
            Coords coords = LocationUpdater.getLastKnownLocation(mApiClient);


        }

    }

LocationUpdater is another class, which contains the static method getLastKnownLocation, which is this:

public static Coords getLastKnownLocation(GoogleApiClient apiClient) {

        Coords coords = new Coords();
        Location location = LocationServices.FusedLocationApi
                .getLastLocation(apiClient);

        if (location != null) {

            coords.setLatitude(location.getLatitude());
            coords.setLongitude(location.getLongitude());

            Log.e("lat ", location.getLatitude() + " degrees");
            Log.e("lon ", location.getLongitude() + " degrees");

        }
        return coords;
    }

But surprise!! I get "IllegalArgumentException: GoogleApiClient parameter is required", when I clearly pass the reference to the static method, which again I guess must have something to do with the GoogleApiClient instance being implicated with the Activity's lifecycle and something going wrong with passing the instance into the IntentService.

So I'm thinking: how do I get regular location updates every five minutes without going crazy? Do I extend a Service, implement all interface callbacks on that component, build the GoogleApiClient instance in there and keep it running in the background? Do I have an AlarmManager start a service that extends IntentService every five minutes to do the work, again having all relevant callbacks and GoogleApiClient constructed in the IntentService? Do I keep doing what I'm doing right now but construct the GoogleApiClient as a singleton, expecting that it'll make a difference? How would you do it?

Thanks and sorry for this being so long-winded.


Solution

  • I am currently working on an app that has to check the user's location every five minutes and send the coordinates to a server. I decided to go with the FusedLocation API in Google Play Services instead of the plain old LocationManager API

    Our app has exactly that same requirement, I implemented that a couple of days ago and here is how I did it.

    In the launch activity or wherever you want to start, configure a LocationTracker to run every 5 minutes, using an AlarmManager.

    private void startLocationTracker() {
        // Configure the LocationTracker's broadcast receiver to run every 5 minutes.
        Intent intent = new Intent(this, LocationTracker.class);
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
        alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, Calendar.getInstance().getTimeInMillis(),
                LocationProvider.FIVE_MINUTES, pendingIntent);
    }
    

    LocationTracker.java

    public class LocationTracker extends BroadcastReceiver {
    
        private PowerManager.WakeLock wakeLock;
    
        @Override
        public void onReceive(Context context, Intent intent) {
            PowerManager pow = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            wakeLock = pow.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "");
            wakeLock.acquire();
    
            Location currentLocation = LocationProvider.getInstance().getCurrentLocation();
    
            // Send new location to backend. // this will be different for you
            UserService.registerLocation(context, new Handlers.OnRegisterLocationRequestCompleteHandler() {
                @Override
                public void onSuccess() {
                    Log.d("success", "UserService.RegisterLocation() succeeded");
    
                    wakeLock.release();
                }
    
                @Override
                public void onFailure(int statusCode, String errorMessage) {
                    Log.d("error", "UserService.RegisterLocation() failed");
                    Log.d("error", errorMessage);
    
                    wakeLock.release();
                }
            }, currentLocation);
        }
    }
    

    LocationProvider.java

    public class LocationProvider {
    
        private static LocationProvider instance = null;
        private static Context context;
    
        public static final int ONE_MINUTE = 1000 * 60;
        public static final int FIVE_MINUTES = ONE_MINUTE * 5;
    
        private static Location currentLocation;
    
        private LocationProvider() {
    
        }
    
        public static LocationProvider getInstance() {
            if (instance == null) {
                instance = new LocationProvider();
            }
    
            return instance;
        }
    
        public void configureIfNeeded(Context ctx) {
            if (context == null) {
                context = ctx;
                configureLocationUpdates();
            }
        }
    
        private void configureLocationUpdates() {
            final LocationRequest locationRequest = createLocationRequest();
            final GoogleApiClient googleApiClient = new GoogleApiClient.Builder(context)
                    .addApi(LocationServices.API)
                    .build();
    
            googleApiClient.registerConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                @Override
                public void onConnected(Bundle bundle) {
                    startLocationUpdates(googleApiClient, locationRequest);
                }
    
                @Override
                public void onConnectionSuspended(int i) {
    
                }
            });
            googleApiClient.registerConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
                @Override
                public void onConnectionFailed(ConnectionResult connectionResult) {
    
                }
            });
    
            googleApiClient.connect();
        }
    
        private static LocationRequest createLocationRequest() {
            LocationRequest locationRequest = new LocationRequest();
            locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
            locationRequest.setInterval(FIVE_MINUTES);
            return locationRequest;
        }
    
        private static void startLocationUpdates(GoogleApiClient client, LocationRequest request) {
            LocationServices.FusedLocationApi.requestLocationUpdates(client, request, new com.google.android.gms.location.LocationListener() {
                @Override
                public void onLocationChanged(Location location) {
                    currentLocation = location;
                }
            });
        }
    
        public Location getCurrentLocation() {
            return currentLocation;
        }
    }
    

    I first create an instance of the LocationProvider in a class that extends application, creating the instance when the app is launched:

    MyApp.java

    public class MyApp extends Application {
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            LocationProvider locationProvider = LocationProvider.getInstance();
            locationProvider.configureIfNeeded(this);
        }
    }
    

    The LocationProvider is instantiated and configured for location updates exactly once, because it is a singleton. Every 5 minutes it will update its currentLocation value, which we can retrieve from anywhere we need with

    Location loc = LocationProvider.getInstance().getCurrentLocation();
    

    Running a background service of any kind is not required. The AlarmManager will broadcast to LocationTracker.onReceive() every 5 minutes and the partial wakelock will ensure that the code will finish running even if the device is standby. This is also energy efficient.

    Note that you need the following permissions

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />
    
    <!-- For keeping the LocationTracker alive while it is doing networking -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    

    and don't forget to register the receiver:

    <receiver android:name=".LocationTracker" />