I'm currently working on a location feature to track device location using FusedLocationProviderClient
. When App is visible the tracking is working as intented on all Android versions. But when switching to foreground (app not visible), location are no more provided on Android 12+, I have the following error from the logcat: LocationUpdateReceiver - LocationEngineResult == null
. And for devices running below Android 12, I'm receiving less location than the maxInterval
set with LocationRequest
I'm not sure what I'm doing wrong as I followed the different Android documentation about Services / Location.
My AndroidManifest is looking like this:
...
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
...
<service
android:name=".app.services.RecordingService"
android:foregroundServiceType="location"/>
My location repository, which handle the tracking location:
class PlayServicesDataStore(private val context: Context, private val logger: Logger) : LocationDataStore {
override val userLocationState: MutableStateFlow<PointZ?> = MutableStateFlow(null)
private val fusedLocationProviderClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val locationCallback = Callback()
private inner class Callback : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { lastLocation ->
logger.logMessage("New GPS location: $lastLocation")
/**
* We want to keep only location that have an accuracy of [MAX_ACCURACY_METER]
*/
if (lastLocation.hasAccuracy() && lastLocation.accuracy <= MAX_ACCURACY_METER) {
userLocationState.update {
PointZ(lastLocation.latitude, lastLocation.longitude, lastLocation.altitude)
}
}
}
}
}
override fun startListeningLocationUpdates() {
val locationRequest = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(1)
fastestInterval = TimeUnit.SECONDS.toMillis(1)
maxWaitTime = TimeUnit.SECONDS.toMillis(2)
priority = Priority.PRIORITY_HIGH_ACCURACY
}
try {
fusedLocationProviderClient
.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
} catch (exception: SecurityException) {
logger.logException(exception, "Missing permission to request Location Updates")
}
}
override fun stopListeningLocationUpdates() {
try {
fusedLocationProviderClient.removeLocationUpdates(locationCallback)
} catch (exception: SecurityException) {
logger.logException(exception, "Missing permission to remove Location Updates")
}
}
private companion object {
const val MAX_ACCURACY_METER = 20
}
}
The Service:
...
class RecordingService : LifecycleService(), HasAndroidInjector {
....
private val notificationManager by lazy {
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
private var started = false
private var configurationChange = false
private var serviceRunningInForeground = false
private val localBinder = LocalBinder()
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
// This action comes from our ongoing notification. The user requested to stop updates.
if (intent?.action == ACTION_STOP_UPDATES) {
stopListeningLocationUpdates()
generateRecordingNotification(...)
}
if (!started) {
started = true
lifecycleScope.launch {
recordingInteractor
.recordingProgressState
.collect {
updateRecordingNotification(...)
}
}
}
// Tells the system not to recreate the service after it's been killed.
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
// AppActivity (client) comes into foreground and binds to service, so the service can
// become a background services.
stopForeground(STOP_FOREGROUND_REMOVE)
serviceRunningInForeground = false
configurationChange = false
return localBinder
}
override fun onRebind(intent: Intent) {
// AppActivity (client) returns to the foreground and rebinds to service, so the service
// can become a background services.
stopForeground(STOP_FOREGROUND_REMOVE)
serviceRunningInForeground = false
configurationChange = false
super.onRebind(intent)
}
override fun onUnbind(intent: Intent): Boolean {
// MainActivity (client) leaves foreground, so service needs to become a foreground service
// to maintain the 'while-in-use' label.
// NOTE: If this method is called due to a configuration change in AppActivity,
// we do nothing.
if (!configurationChange) {
val notification = generateRecordingNotification(
notificationTitle = getString(R.string.trail_recording_live_activity_recording_status_active),
context = applicationContext,
paused = false,
recordingDuration = getDurationText(recordingInteractor.recordingProgressState.value.time),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_LOCATION)
} else {
startForeground(NOTIFICATION_ID, notification)
}
serviceRunningInForeground = true
}
// Ensures onRebind() is called if AppActivity (client) rebinds.
return true
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
configurationChange = true
}
fun startListeningLocationUpdates() {
// Binding to this service doesn't actually trigger onStartCommand(). That is needed to
// ensure this Service can be promoted to a foreground service, i.e., the service needs to
// be officially started (which we do here).
startService(Intent(applicationContext, RecordingService::class.java))
locationRepository.startListeningLocationUpdates()
}
fun stopListeningLocationUpdates() {
stopSelf()
locationRepository.stopListeningLocationUpdates()
}
/**
* Class used for the client Binder. Since this service runs in the same process as its
* clients, we don't need to deal with IPC.
*/
internal inner class LocalBinder : Binder() {
fun getService(): RecordingService = this@RecordingService
}
}
Not sure what I'm missing to make it works properly, any help will be greatly appreciated, thanks!