Android — Getting user location updates in a background service and send data to UI using publisher-subscriber pattern

Mohammed S. Hassan
7 min readJun 2, 2020

--

What you will learn here

  • Difference between background location and foreground location
  • Creating Foreground service that’s responsible for getting user’s location updates
  • Publish location updates from the service and subscribe to updates from any other context (UI in our case)

Introduction

Location-based applications are widely used nowadays in mobile apps. You may want to get user location to show services around, get a sense of user city or country or act based on the location updates of the user while moving (like Uber calculating the distance and trip fare). In this article, we are going to explain how you can achieve Uber’s kind of behavior that gets user location updates while moving and acts accordingly.

Location Access Categories

There are 2 modes of user location access. Foreground and background. While the article title.

Foreground Location

The app is in Foreground if :

  • There is an activity/UI showing in front of the user — Basically you need to get the location in the user interface.
  • There is a foreground service. When a foreground service is running, we implement a persistent notification that’s always showing to the user indicating something is happening and done by the app.

And this will be the case we are implementing. Some service in the background that continuously gets user location; yet showing a notification to let users know that the app is running and getting location updates.

Background Location

You many need background location category if your app really needs to get location silently without showing the user anything, This, of course, is a major privacy issue that developers may violate. That’s why it needs special permission ACCESS_BACKGROUND_LOCATIONand special approval from Google while publishing your app.

Asking User For Location Permission

In the manifest:

<manifest ... >
<!-- To request foreground location access, declare one of these permissions. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

Then at runtime, when the user is about to use the feature that requires the location, check and ask for permission like that

boolean permissionAccessCoarseLocationApproved =
ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED;

if (permissionAccessCoarseLocationApproved) {
startLocationUpdatesService();
} else {
// Make a request for foreground-only location access.
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.ACCESS_FINE_LOCATION},
AppConstants.LOCATION_SERVICE_REQUEST_TAG);
}

You might also need to check if the user accepted or denied the permission and act accordingly

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(AppConstants.LOCATION_SERVICE_REQUEST_TAG == requestCode){

//You might check the grant results as ususal. I just chose to check the granted permission again
boolean permissionAccessCoarseLocationApproved =
isPermissionAccessCoarseLocationApproved();

if(permissionAccessCoarseLocationApproved){
startLocationUpdatesService();
}else {
//act accordingly
}
}
}

Location Updates Service

Now we are going to create the service that actually gets the user location updates. Create a class LocationUpdateService extends Service

public class LocationUpdateService extends Service {


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

And since we decided to make the service a foreground service, we must create a notification object and call startForground() from within the onStartCommand of this service like that

public class LocationUpdateService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {

prepareForegroundNotification();

return START_STICKY;
}

private void prepareForegroundNotification() {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel serviceChannel = new NotificationChannel(
AppConstants.CHANNEL_ID,
"Location Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(serviceChannel);
}
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
AppConstants.SERVICE_LOCATION_REQUEST_CODE,
notificationIntent, 0);

Notification notification = new NotificationCompat.Builder(this, AppConstants.CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentTitle(getString(R.string.app_notification_description))
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build();
startForeground(AppConstants.LOCATION_SERVICE_NOTIF_ID, notification);
}

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

Last Known Location

You might get the last known location first for better user experience but in our case, we won’t need any action before getting the actual first location of the user.

Create a Location Request

//....
LocationRequest locationRequest;
//...
protected void createLocationRequest() {
locationRequest = LocationRequest.create();
locationRequest.setInterval(3000);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}

setInterval

sets the rate in milliseconds at which your app wants to receive location updates. If you want to get updates every second, this set it to 1000.

setPrioirty

which of the following is your priority. Each has different effects on the battery and the performance. So be wise about your choice.

Location Callback

You need first to define the location callback in which you will receive the location updates

//Location Callback
private LocationCallback locationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
Location currentLocation = locationResult.getLastLocation();


Log.d("Locations", currentLocation.getLatitude() +"," +currentLocation.getLongitude());
//ToDO Publish Location
}
};

Start Location Updates

create this method and call it inside onStartCommand, since this is the place we want to start getting updates at.

private void startLocationUpdates() {
fusedLocationClient.requestLocationUpdates(locationRequest,
locationCallback,
Looper.getMainLooper());
}

By now we have a full service that gets location updates in the background.

If you run it and logged location updates, you should get something like that

Here is the full source code for it:

Test Location Updates

You can test the location updates on your emulator or real device with fake GPS points (mock locations). Check this article to learn how to do so.

Publish Location Updates from Service to Activities or Fragments

Now you probably need to use these location updates and show them on a map or something in the user interface layer (activity or fragment).

To send data from Service to Activity (or Fragment) You can use any of the following options:

  • Send broadcast Intent and use a broadcast receiver in the activity to receive and handle that intent.
  • Use Messenger that’s tied a handler (coming from the activity) to send messages to it.
  • Use EventBus library (my favorite option that we are going to use here).

Publish/Subscribe Pattern

When you have some entity that has some data to share with one or many other entities, the Publish/Subscribe pattern is a very handy way to do so. Whoever wants to send something (Event) it publishes it. Any other part that’s interested in this kind of event subscribes to it. You can have as many subscribers as you want. For example, you might have 1 user interface screen that’s interested in the location updates and show it on a map and another screen that shows them in latitude and longitude with some information about the distance and speed or so. EventbBus library makes this process peace of cake.

Use EventBus to publish location events

Step 0: Configure EventBus

Add this to your app’s Gradle file (Check latest version number and more setup options here)

implementation 'org.greenrobot:eventbus:3.2.0'

Step1: Define the kind of Event (or Events) that you want to publish.

In our case, we need to publish a LocationUpdateEvent that contains information about the latest location reported. Let’s define that:

import android.location.Location;

import com.reviapps.traveldistance.data.LocationDTO;

public class LocationUpdateEvent {
private LocationDTO location;

public LocationUpdateEvent(LocationDTO locationUpdate) {
this.location = locationUpdate;
}

public LocationDTO getLocation() {
return location;
}

public void setLocation(LocationDTO location) {
this.location = location;
}
}

This LocationDTO class is a wrapper I use for location points. Here is the code for it.

public class LocationDTO {
public double latitude;
public double longitude;
public double speed;

@Override
public String toString() {
return "lat:"+latitude +"- lng:"+longitude + "- speed:"+speed;
}
}

Step 2: Publish Location Updates

Open our LocationService class and in the LocationCallback’s onLocationResults method, add the line that posts the location update message.

EventBus.getDefault().post(new LocationUpdateEvent(location));private LocationCallback locationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
Location currentLocation = locationResult.getLastLocation();
Log.d("Locations", currentLocation.getLatitude() + "," + currentLocation.getLongitude());
//Publish Location
LocationDTO location = new LocationDTO();
location.latitude = currentLocation.getLatitude();
location.longitude = currentLocation.getLongitude();
location.speed = currentLocation.getSpeed();

EventBus.getDefault().post(new LocationUpdateEvent(location));

}
};

Step 3: Subscribe and Receive updates

In your activity, Add the following method at:

@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(LocationUpdateEvent event) {
//handle updates here
//event.getLocation().latitude, event.getLocation().longitude
};

Register and unregister your subscriber. For example on Android, activities, and fragments should usually register according to their life cycle:

@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}

@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}

Now you are ready. Once you have any location update sent from the service, you will magically get it in your activity’s onMessageEvent method.

Getting data from the service at any point

If you want to ask the service for some data at any moment, I’d suggest an addition to the previous code that you implement the service as a bounded service so that you can bind to the service from any other content and ask the service for some information by calling a service method you implement (maybe getCurrentLocation() for example).

Here is the documentation for how to do so.

Test Location Updates

You can test the location updates on your emulator or real device with fake GPS points (mock locations). Check this article to learn how to do so.

Stop Location Updates

You can call the below line to stop the location updates.

mFusedLocationClient.removeLocationUpdates(locationCallback);

I tend to stop the service using stopService() method and in the service’s onDestroy() callback I add the above line.

@Override
public void onDestroy() {
super.onDestroy();
mFusedLocationClient.removeLocationUpdates(locationCallback);
}

You can publish that event too so that the API knows we stopped the location service.

EventBus.getDefault().post(new LocationStoppedEvent());

I hope that was useful and clear :).

You can see my other articles on Medium here and You can find me on LinkedIn here.

Thanks for reading…

--

--

Mohammed S. Hassan

Technical Product Manager | Lead Software Engineer |Instructor