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

Wahib Ul Haq
ProAndroidDev
Published in
7 min readNov 2, 2018

--

Beautifully decorated truck driving on the mighty Karakoram Highway in Pakistan (Photo by Mehtab Alam)

In our last project, we worked on a smartphone app to combat electric vehicle anxiety by understanding users’ needs, providing the owner with real-time data about their drives and offering a mechanism to compare suitable electric vehicles. In order to make that happen, tracking of driving behaviour through the app was an essential aspect of the project. It means that the app should be able to constantly listen to location coordinates in the background during the user’s journey while driving.

We had to implement checks to ensure GPS is enabled on the device and Location Permission is granted by the user. It is important to highlight that both are critically needed to ensure the app can perform Location Tracking during all drives.

Use Case 🚙

Imagine a scenario when the user installs the app, goes through onboarding and maybe grants Location Permission but ignores the dialog which prompts to enable GPS. Even worse, the user didn’t grant permission access or revoked later for any reason. Now it is safe to assume that user will even forget about the newly installed app and intended goal won’t be achieved.

Our best bet was to inform visually when the app is in the foreground in order to encourage the user to fulfill one or both of these requirements. This was only possible when UI (Activity/Fragment) can listen to changes in GPS and Runtime Permission and react accordingly.

I faced several challenges while doing that and decided to share my learnings and solution. It is not the best solution ever but something which works for now and I would be more than happy and thankful to hear feedback on doing it in a better way. ✌️

Before I move to implementation part, I would like to mention that our team already decided to use Architecture Components so it was a natural choice to opt for LiveData to implement GPS and Permission Listeners so to say.

LiveData is an observable data holder and lifecycle-aware. It lets the components in your app, usually the UI, observe LiveData objects for changes.

Listening to GPS Status 📍

The user can always enable or disable GPS on their device.

Implementing GPS LiveData

We have to extend LiveData to create our own custom LiveData

class GpsStatusListener(private val context: Context) : LiveData<GpsStatus>() {}

GpsStatus is a sealed class and reflects an enabled and disabled state

Sealed classes are suitable when a value can have one of the types from a limited set, but cannot have any other type

I read somewhere that sealed classes are enums with swag and can’t disagree. 😉 I also got some inspiration from Craig’s article which shows that sealed classes are perfect when you want to send commands for your observing entity to obey.

sealed class GpsStatus {  

data class Enabled(val message: Int = R.string.gps_status_enabled) : GpsStatus()
data class Disabled(val message: Int = R.string.gps_status_disabled) : GpsStatus()}

Two very important functions of LiveData are onActive() and onInactive(). These allow observing changes only if Activity (or any other LifecycleOwner) is in started or resumed state and only then LiveData will emit any items . It means no more memory leaks and NullPointerExceptions due to non-existing view.

override fun onInactive() = unregisterReceiver()     override fun onActive() {        
registerReceiver()
checkGpsAndReact()
}

Whenever a change happens on GPS Provider, onReceive() of Broadcast Receiver will be called and relevant GpsStatus state will be emitted to the observer.

private val gpsSwitchStateReceiver = object : BroadcastReceiver() {               

override fun onReceive(context: Context, intent: Intent) = checkGpsAndReact()
}private fun checkGpsAndReact() = if (isLocationEnabled()) {
postValue(GpsStatus.Enabled())
} else {
postValue(GpsStatus.Disabled())
}

If you noticed, postValue is used here because we would need to set a value from a background thread as well and another alternative is to use setValue when method must be called from the main thread.

Observing GPS Status LiveData

For demo purposes, the sample app shows the state of GPS via the title of clickable TextView and also through a custom dialog.

Example UI reacting to the disabled state of GPS

Custom LiveData is exposed via ViewModel

val gpsStatusLiveData = GpsStatusListener(application)

But there’s a small caveat here.

Think carefully before sharing a LiveData instance across consumers

Remember that LiveData dispatches the latest value to a new observer. So better take special care if you are using Dagger for dependency injection because I made a mistake of using @Singleton scope and it took me a while to debug a weird behaviour.

To start listening to changes in for example Activity in my case, you have to observe on it. Also, this passed to observe() specifies that our Activity is the LifecycleOwner and the observer here. We don’t have to care about unsubscribing because it is handled by Android.

private fun subscribeToGpsListener() = viewModel.gpsStatusLiveData            .observe(this, gpsObserver)

gpsObserver is where you will specify how UI will react to changes in GPS Status. I would like to highlight that Sealed classes pair nicely with the when expression.

private val gpsObserver = Observer<GpsStatus> { 
status -> status?.let {
when (status) {
is GpsStatus.Enabled -> {
//Update UI Accordingly
}
is GpsStatus.Disabled -> {
//Update UI Accordingly
}
}
}
}

Listening to Runtime Permission Status ✅

Starting from Android 6.0 and higher, every app is required to ask the user to grant permission access to one or more of the so called dangerous permissions. It is common to prompt the user during the onboarding phase and also repeat it every time an operation is performed that requires that permission.

Beginning with Android 6.0 (API level 23), users can revoke permissions from any app at any time, even if the app targets a lower API level.

Source: Kaspersky

Implementing Runtime Permission LiveData

Similarly, we have to extend LiveData to create our own custom LiveData. My idea was to make it a generic one which could be used with any particular runtime permission.

class PermissionStatusListener(
private val context: Context,
private val permissionToListen: String) : LiveData<PermissionStatus>() {
}

PermissionStatus is a sealed class and reflects granted, denied and blocked state.

sealed class PermissionStatus {
data class Granted(val message: Int = R.string.permission_status_granted) : PermissionStatus()
data class Denied(val message: Int = R.string.permission_status_denied) : PermissionStatus()
data class Blocked(val message: Int = R.string.permission_status_blocked) : PermissionStatus()
}

Whenever PermissionStatusListener is invoked, it checks for the current status of the runtime permission passed to the LiveData

override fun onActive() = handlePermissionCheck()private fun handlePermissionCheck() {
val isPermissionGranted = ActivityCompat.checkSelfPermission(context, permissionToListen) == PackageManager.PERMISSION_GRANTED

if (isPermissionGranted)
postValue(PermissionStatus.Granted())
else
postValue(PermissionStatus.Denied())
}

Observing Runtime Permission LiveData

For our use-case, we are only concerned with Location Permission. We intentionally made it optional for the user during onboarding which means we’ll need to always check for it before triggering Location Tracking. More on this is covered in the next part.

Also, Location Permission is checked first and later GPS and dialog keep appearing whenever the user brings the app to the foreground. This is of course just to show how UI can be made to react to it.

Example UI reacting to denied state of Location Permission

Custom LiveData is exposed via ViewModel

val locationPermissionStatusLiveData = PermissionStatusListener(application,      Manifest.permission.ACCESS_FINE_LOCATION)

Permission passed on to PermissionStatusListener should match with the one defined in AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

To start listening to changes in for example Activity, you have to observe on it. permissionObserver shows how to react to different states.

private val permissionObserver = Observer<PermissionStatus> { 
status ->
status?.let {
updatePermissionCheckUI(status)
when (status) {
is PermissionStatus.Granted -> handleGpsAlertDialog()
is PermissionStatus.Denied -> showLocationPermissionNeededDialog()
}
}
}
private fun subscribeToLocationPermissionListener() = viewModel.locationPermissionStatusLiveData.observe(this, permissionObserver)

I am using Permissions lib to handle app requesting permission, invoking native dialog and reacting to permission callbacks based on user input. I found it lightweight, easier to use and customisable and preferred it over implementing my own business logic.

Permissions.check(this,
Manifest.permission.ACCESS_FINE_LOCATION,
null,
permissionHandler)
//Permission callbacks
private val permissionHandler = object : PermissionHandler() {
override fun onGranted() {
}
override fun onDenied(context: Context?, deniedPermissions: ArrayList<String>?) {
}
override fun onJustBlocked(
context: Context?,
justBlockedList: ArrayList<String>?,
deniedPermissions: ArrayList<String>?
) {
}
}

You can have a look at the code on GitHub

Part 2 will cover the topic of Monitoring GPS and Location Permission Checks while the app is running in the background.

It goes without saying that at KI labs, we have adopted Kotlin as our primary Android development language from day one 🎉

--

--

Senior Program Manager at Diconium • Dev Advocate • Talks about Tech, DevRel, Soft skills, Careerintech & Partnerships • Co-Organizer @DevRelMunich