Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RequestLocationUpdates() with no location updates on Foreground for Android 12 +

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!

like image 227
Guimareshh Avatar asked Sep 15 '25 08:09

Guimareshh


1 Answers

If your app targets a newer Android version, you have to make sure to declare background permissions.

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

And make sure that the user grants background permissions to the app. Depending on the Android version of the user, you have to send them to the settings of the app in order to make this possible. See the official documentation on requesting background location permission for more information.

like image 195
Stephan Avatar answered Sep 17 '25 23:09

Stephan