4

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!

Guimareshh
  • 1,214
  • 2
  • 15
  • 26

1 Answers1

2

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.

Stephan
  • 15,704
  • 7
  • 48
  • 63
  • 1
    From the documentation using a Foreground Service with permission `while in use` we should be able to access location without having the need of ACCESS_BACKGROUND_LOCATION permission. For instance app like AllTrails don't have this permission and are just using the `while in use` permission to make it works properly – Guimareshh Feb 28 '23 at 13:43
  • There is a list of situations where this is true. If your app fulfills any of it it should work. However if not the user has to grand background permissions: https://developer.android.com/guide/components/foreground-services#bg-access-restrictions – Stephan Feb 28 '23 at 14:24
  • 1
    Indeed, it's necessary for location if the service is started while in background. Which is not my case (service is started while app is visible in my case). Also this is an issue that begin with Android 11, and it's working fine for Android 11. My issue is for Android 12+ – Guimareshh Feb 28 '23 at 14:42