Getting the user's location

All the methods below need the location permission:

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

Getting the last known location

Note that this version does not start a new GPS pinpointing request, it merely gets the last known location. This has the benefit of being fast (no waiting for several satellites to be detected) but it can return an old location, so make sure to check for the timestamp specified in the Location object.

// 
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)

Getting location updates using Play Services

This is the most battery efficient and accurate method. It shares location requests throughout other apps making GPS pinpointing faster and less battery consuming. However, there are drawbacks. In certain apps we notices the location updates to be ‘averaged’ over the last few minutes, making it effectively cut corners and lag behind for minutes.

// TODO add this snippet

Getting location updates

class LocationMonitor(private val locationProvider: FusedLocationProviderClient) {

    companion object {
        fun getInstance(context: Context): LocationMonitor {
            val fusedLocationProviderClient =
                LocationServices.getFusedLocationProviderClient(context)
            return LocationMonitor(fusedLocationProviderClient)
        }

        private const val TAG = "Location"
        val UPDATE_INTERVAL_IN_MILLISECONDS: Long = TimeUnit.MINUTES.toMillis(4)
        val FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2
    }

    private val locationRequest: LocationRequest =
        LocationRequest().apply {
            this.interval =
                UPDATE_INTERVAL_IN_MILLISECONDS
            this.fastestInterval =
                FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS
            this.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        }

    private var callbackFunction: ((Location) -> Unit) = {}

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult?) {
            super.onLocationResult(locationResult)
            Log.d(TAG, "Location service callback with new location")
            locationResult.let {
                if (it?.lastLocation != null) {
                    callbackFunction(it.lastLocation)
                }
            }
        }
    }

	@SuppressLint("MissingPermission")
    // handle permissions here (or use the PermissionStatusLiveData --see monitor the permission status)
    fun startListening(callback: (Location) -> Unit) {
        Log.d(TAG, "Location service starting...")
        if (locationPermissionStatus.value == LocationPermissionStatus.GRANTED) {
            callbackFunction = callback
            locationProvider.requestLocationUpdates(
                locationRequest,
                locationCallback,
                Looper.myLooper()
            )
        }
    }

    fun stopListening() {
        Log.d(TAG, "Location service stop request received")
        locationProvider.removeLocationUpdates(locationCallback)
        callbackFunction = {}
    }
}

Get location repeatedly using a background service

class BackgroundLocationService : JobService() {

    companion object {
        private const val TAG = "Location"

        const val LOCATION_SERVICE_JOB_ID = 111
        const val ACTION_STOP_JOB = "stop_job"
        const val LOCATION_ACQUIRED = "location_acquired"
        const val JOB_STATE_CHANGED = "job_state_changed"
        const val EXTRA_LOCATION = "location"
    }

    private val locationMonitor: LocationMonitor by inject()

    private var jobParams: JobParameters? = null

    private val stopJobReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action != null && intent.action == ACTION_STOP_JOB) {
                Log.d(TAG, "Stopping Job receiver")
                locationMonitor.stopListening()
                jobFinished(jobParams, false)
            }
        }
    }

    override fun onStartJob(params: JobParameters?): Boolean {
        // Remember for later
        jobParams = params

        // Start location listener
        locationMonitor.startListening(::setUpdatedLocation)

        // Listen for stop actions
        LocalBroadcastManager
            .getInstance(this@BackgroundLocationService)
            .registerReceiver(stopJobReceiver, IntentFilter(ACTION_STOP_JOB))
        return true
    }

    override fun onStopJob(params: JobParameters?): Boolean {
        Log.d(TAG, "Job receiver stopped")
        locationMonitor.stopListening()
        return true
    }

    /**
     * Delivers the result back to the receiver in LocationLiveData
     */
    private fun setUpdatedLocation(locationUpdate: Location) {
        Log.d(TAG, "Acquired new location")
        val i = Intent(LOCATION_ACQUIRED)
        i.putExtra(EXTRA_LOCATION, locationUpdate)
        LocalBroadcastManager.getInstance(baseContext).sendBroadcast(i)
    }

}

And in the AndroidManifest.xml

<service
	android:name=".BackgroundLocationService"
    android:exported="true"
    android:permission="android.permission.BIND_JOB_SERVICE" />

Use LiveData to get updates from the LocationMonitor

class LocationLiveData :
    MutableLiveData<Location>(), KoinComponent {

    private val locationMonitor: LocationMonitor by inject()
    private val repository: Repository by inject()

    fun create() {
        locationMonitor.startListening(::setUpdatedLocation)
    }

    fun destroy() {
        onInactive()
        locationMonitor.stopListening()
    }

    private fun setUpdatedLocation(locationUpdate: Location) {
        postValue(locationUpdate)
        repository.updateLocation(locationUpdate)
    }
}

Use LiveData to get updates from the LocationMonitor and the background service

class LocationLiveData(private val applicationContext: Context) :
    MutableLiveData<Location>(), KoinComponent {

    private var registered = false
    private val repository: Repository by inject()

    private val jobStateReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent) {
            Log.d(TAG, "JobStateReceiver received action ${intent.action}")
            when {
                intent.action == null -> return
                intent.action == JOB_STATE_CHANGED -> return
                intent.action == LOCATION_ACQUIRED -> intent.extras?.getParcelable<Location>(
                    BackgroundLocationService.EXTRA_LOCATION
                )?.let {
                    setUpdatedLocation(it)
                }
            }
        }
    }

    companion object {
        private const val TAG = "Location"
    }

    fun create() {
        startBackgroundListening(applicationContext)
    }

    private fun startBackgroundListening(context: Context) {
        if (!registered) {
            val i = IntentFilter(JOB_STATE_CHANGED)
            i.addAction(LOCATION_ACQUIRED)
            LocalBroadcastManager.getInstance(context).registerReceiver(jobStateReceiver, i)
        }
        val jobScheduler =
            (context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler?)!!
        val builder = JobInfo.Builder(
            BackgroundLocationService.LOCATION_SERVICE_JOB_ID,
            ComponentName(context, BackgroundLocationService::class.java)
        )
            .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
            .setOverrideDeadline(LocationMonitor.UPDATE_INTERVAL_IN_MILLISECONDS)
            .setPersisted(true)
            .setRequiresDeviceIdle(false)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            builder.setEstimatedNetworkBytes(800, 200)
        }
        jobScheduler.schedule(builder.build())
    }

    fun destroy() {
        onInactive()
    }

    private fun setUpdatedLocation(locationUpdate: Location) {
        Log.d(TAG, "LocationLiveData acquired new location")
        postValue(locationUpdate)
        repository.updateLocation(locationUpdate)
    }
}