Monitoring GPS and Location Permission checks using LiveData (Part 2)

Photo of Attabad Lake (Pakistan) by Sufyan

In the first part of this tutorial, we learned how to implement checks using LiveData and observe on UI level. If you haven’t read the first part, I suggest you read it first because the second part is a continuation of it.


In this article, we’ll try to explore how it can be used when the app is in the background.

Updated Use Case 🚙

Imagine a scenario when the user installs the app, goes through onboarding and grants Location Permission and enables GPS when prompted. There is still a chance that the user can revoke permission at any time in future for any reason. Even worse, the user started driving but don’t have GPS enabled. Those drives will never be tracked and the app will not be able to perform its intended goal.

Listening to GPS and Location Permission Status 🛰

Location Tracking is implemented as a LocationService because this is supposed to run in the background and listen to location updates from FusedLocationProviderClient.

If the service is started and is long-running, the system lowers its position in the list of background tasks over time, and the service becomes highly susceptible to killing.

The only solution is to make the service run in the foreground and it must show an ongoing status bar notification. If your app targets API level 26 or higher, read about updated rules to start a service. Additionally, apps that target Android 9 (API level 28) or higher and use foreground services must request the FOREGROUND_SERVICE permission.

Foreground Notification to inform the user when location tracking is in progress

We saw in first part how we can observe on LiveData in Activity because it implements the LifecycleOwner interface.

In our case, we wanted to listen to GPS and Runtime Permission LiveData changes inside LocationService. If you read the comments in LifecyleOwner class you’ll notice that it doesn’t even mention Service as a candidate. Now what?

We have LifecyleService to our rescue.

Personally, this was troubling to realise that Android doesn’t highlight it prominently enough in code or documentation and quite a low hanging fruit which can save time for developers.

class LocationService : LifecycleService() {
}

In reality, this service should get started whenever a driving activity is detected but that part of implementation is out of scope along with how to listen to location coordinates. We tried to simulate that part using buttons in the sample app for demo purpose.

Showing an example of how notification can look like

If at the time of service start event, one or both of these requirements are not fulfilled, then respective notifications are created and service is stopped. Same happens when service is already running and meanwhile GPS is disabled or Location Permission is revoked.

Observing both LiveData

Initially, we wanted to do something like this so that app can react based on both streams. The intended goal was to create a notification with updated text if both are not provided. For the demo, I am creating two separate notifications. Additionally, unregistering from listening to location updates is also important if even one of them is not provided because it won’t serve the purpose anyway.

private val gpsObserver = Observer<GpsStatus> { 
status -> status?.let { handleGpsStatus(status) }
}
private val permissionObserver = Observer<PermissionStatus> { 
status -> status?.let { handlePermissionStatus(status) }
}

Problem with this approach was that it added unwanted complexity when trying to handle both streams separately which can happen independently to each other. In reality, an ideal situation would be if we could react to both streams at once as if it was a Pair of both statuses.

private val pairObserver = Observer<Pair<PermissionStatus, GpsStatus>> { 
pair -> pair?.let {
handlePermissionStatus(pair.first)
handleGpsStatus(pair.second)
}
}

In order to make this happen, we should do things differently and luckily Architecture Components have something for us.

MediatorLiveData is a LiveData subclass which may observe other LiveData objects and react on OnChanged events from them.

With a MediatorLiveData, we’re able to react to GpsStatus emitted by GpsStatusListener and PermissionStatus emitted by PermissionStatusListener. I found a very suitable approach here and decided to use it. Here’s how the code would look like:

fun <A, B> combineLatest(a: LiveData<A>, b: LiveData<B>): MutableLiveData<Pair<A, B>> {
return MediatorLiveData<Pair<A, B>>().apply {
//Code to update values
var lastA: A? = null
var lastB: B? = null
        fun update() {
val localLastA = lastA
val localLastB = lastB
if (localLastA != null && localLastB != null)
this.value = Pair(localLastA, localLastB)
}
        addSource(a) {
lastA = it
update()
}
addSource(b) {
lastB = it
update()
}
}
}

The resulting LiveData is updated whenever either of the input streams gets updated and MediatorLiveData does this merging part for us. For those who are familiar with RxJava, this behaviour is exactly like CombineLatest function.

In order to observe this new LiveData inside LocationService, this is how code will look like with help of an extension function.

private lateinit var gpsAndPermissionStatusLiveData: LiveData<Pair<PermissionStatus, GpsStatus>>
//onCreate()
gpsAndPermissionStatusLiveData = with(application) {
PermissionStatusListener(this, Manifest.permission.ACCESS_FINE_LOCATION)
.combineLatestWith(GpsStatusListener(this))
}
//onStartCommand()
gpsAndPermissionStatusLiveData.observe(this, pairObserver)

You can have a look at the code on GitHub


We hope this post was helpful to you 🤘 Feel free to check our other interesting articles here.

Follow us on Medium, Twitter, Instagram or Facebook