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)
}
}