Search code examples
androidlocationmanagerfusedlocationproviderclient

Android not updating location in the background


I wanted to create Android app that would update location in background. Went through documentation, articles and tutorials and didn't achieve my goal. When app is minimized it suspends updates and start them again I return to it.

My manifest file:

<service android:name=".LocationService" android:foregroundServiceType="location" />
...
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

My activity code starting location service:

val serviceIntent = Intent(applicationContext, LocationService::class.java).apply {
    action = LocationService.ACTION_START
    startService(this)
}

applicationContext.startService(serviceIntent)

My location service:

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import com.google.android.gms.location.LocationServices
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class LocationService: Service() {

    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private lateinit var locationClient: LocationClient

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        super.onCreate()
        locationClient = LocationClient(
            applicationContext,
            LocationServices.getFusedLocationProviderClient(applicationContext)
        )
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when(intent?.action) {
            ACTION_START -> start()
            ACTION_STOP -> stop()
            ACTION_PAUSE -> pause()
            ACTION_REFRESH -> refresh()
        }
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }

    private fun start() {    
        locationClient
            .getLocationUpdates(1000L)
            .catch { e -> e.printStackTrace() }
            .onEach { location ->
                Log.i("LOCATION", DistanceTracker.getInstance().fullDistance.toString())
                DistanceTracker.getInstance().addDistance(location)
            }
            .launchIn(serviceScope)
    }

    private fun stop() {
        stopSelf()
        DistanceTracker.getInstance().resetDistances()
        refresh()
    }

    private fun pause() {
        stopSelf()
        DistanceTracker.getInstance().clearLocation()
        refresh()
    }

    companion object {
        const val ACTION_START = "ACTION_START"
        const val ACTION_STOP = "ACTION_STOP"
        const val ACTION_PAUSE = "ACTION_PAUSE"
        const val ACTION_REFRESH = "ACTION_REFRESH"
    }
}

And finally my location client:

import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.Looper
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY
import com.google.android.gms.location.LocationResult
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch

class LocationClient(
    private val context: Context,
    private val client: FusedLocationProviderClient
): ILocationClient {
    @SuppressLint("MissingPermission")
    override fun getLocationUpdates(interval: Long): Flow<Location> {
        val locationFlow = callbackFlow {
            if(!context.hasLocationPermission()) {
                throw ILocationClient.LocationException("Missing location permission")
            }

            val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
            val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
            val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
            if(!isGpsEnabled && !isNetworkEnabled) {
                throw ILocationClient.LocationException("GPS is disabled")
            }

            val request = LocationRequest.create()
                .setInterval(interval)
                .setFastestInterval(interval)
                .setPriority(PRIORITY_HIGH_ACCURACY)

            val locationCallback = object : LocationCallback() {
                override fun onLocationResult(result: LocationResult) {
                    val location = result.locations.minWith(Comparator.comparingDouble{it.accuracy as Double})

                    if(location.accuracy < 50) {
                        super.onLocationResult(result)
                        result.locations.lastOrNull()?.let { location ->
                            launch { send(location) }
                        }
                    }
                }
            }

            client.requestLocationUpdates(
                request, locationCallback, Looper.getMainLooper())

            awaitClose {
                client.removeLocationUpdates(locationCallback)
            }
        }

        return locationFlow
    }
}

What am I missing? I would like to track distance I've covered without need to have my app permanently in the foreground. In this state it's really inconvenient.


Solution

  • you should use a foreground service. A foreground service is a special type of service that has a higher priority, which allows it to continue running even when the app is in the background. You have already specified the service as a foreground service in the manifest using android:foregroundServiceType="location"... BUT you need to start the service as a foreground service with a notification when your app is minimized. Like:

    private fun start() {
        val notification = createNotification()
        startForeground(NOTIFICATION_ID, notification)
    
        locationClient
            .getLocationUpdates(1000L)
            .catch { e -> e.printStackTrace() }
            .onEach { location ->
                Log.i("LOCATION", DistanceTracker.getInstance().fullDistance.toString())
                DistanceTracker.getInstance().addDistance(location)
            }
            .launchIn(serviceScope)
    }
    

    You'll need to define the createNotification() function to create a notification for your foreground service, and NOTIFICATION_ID is just an integer constant to identify the notification.

    Also:

    Even if you already have the ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission... in your manifest, you already have android.permission.ACCESS_BACKGROUND_LOCATION, but you also need to request this permission at runtime.

    val backgroundLocationPermission = Manifest.permission.ACCESS_BACKGROUND_LOCATION
    if (ContextCompat.checkSelfPermission(this, backgroundLocationPermission) == PackageManager.PERMISSION_GRANTED) {
    
    } else {
        ActivityCompat.requestPermissions(this, arrayOf(backgroundLocationPermission), REQUEST_CODE_BACKGROUND_LOCATION)
    }
    

    finally:

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode == x) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            } else {
            }
        }
    }
    

    Example createNotification:

    import android.app.NotificationChannel
    import android.app.NotificationManager
    import android.app.PendingIntent
    import android.app.Service
    import android.content.Context
    import android.content.Intent
    import android.os.Build
    import android.os.IBinder
    import androidx.core.app.NotificationCompat
    
    class LocationService : Service() {
    
        private fun createNotification(): Notification {
            val channelId = "location_service_channel"
            val channelName = "Location Service"
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
                notificationManager.createNotificationChannel(channel)
            }
    
            val intent = Intent(this, YourMainActivity::class.java)
            intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
            val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
    
            val notification = NotificationCompat.Builder(this, channelId)
                .setContentTitle("Tracking Location")
                .setContentText("tracking location.")
                .setSmallIcon(R.drawable.ic_notification_icon)
                .setContentIntent(pendingIntent)
                .build()
    
            return notification
        }
    }