How to get continuous location updates in Android

In this post, I am going to explain how to update the live location to the server every some interval. For example, sending live location to the server for every 1 minute.

To fetch live location we are using android service to run in the background to fetch the location continuously within the given time interval. Due to the current restriction on android services, we cannot run service in the background or after closing the app. To run the android app in the background we need to start the service in the foreground.

I am using locationListener to fetch the location information on the given interval. Also, using Retrofit to upload location details to API.

Also, checkout the video version of this post.

Step to get continuous location updates on android

  1. Check Location Permissions
  2. Start service for location updates
  3. Start Location update listener
  4. Call the API to update the location to the server
  5. Adding boot complete receiver

Check Location Permissions

To get the location in android we need ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions. So we need to ask this permission in run time.

define the permission in Manifests.xml.

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

checking the permissions at runtime,

if (!checkPermission()) {
        requestPermission()
    }

    private fun checkPermission(): Boolean {
        val result = ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.ACCESS_FINE_LOCATION)
        val result1 = ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.ACCESS_COARSE_LOCATION)
        return result == PackageManager.PERMISSION_GRANTED && result1 == PackageManager.PERMISSION_GRANTED
    }

    private fun requestPermission() {
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), 1)
    }

Start service for location updates

Create LocationService class and extends the service to create a service. Also, override the onstart and onstartcommand methods.

class LocationService : Service() {
    
    override fun onCreate() {
        super.onCreate()
        
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        return START_STICKY
    }

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

    override fun onDestroy() {
        super.onDestroy()

    }
    
}

The next step is to run the service to fetch the location continuously. So as I mentioned before we need to start the service as a foreground service to run on Android O.

ContextCompat.startForegroundService(this, Intent(this, LocationService::class.java))

On the oncreate of the service, we need to start the foreground with notification. This will avoid the termination of android app after closing.

override fun onCreate() {
        super.onCreate()
        isServiceStarted = true
        val builder: NotificationCompat.Builder =
            NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
                .setOngoing(false)
                .setSmallIcon(R.drawable.ic_launcher_background)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager: NotificationManager =
                getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val notificationChannel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW
            )
            notificationChannel.description = NOTIFICATION_CHANNEL_ID
            notificationChannel.setSound(null, null)
            notificationManager.createNotificationChannel(notificationChannel)
            startForeground(1, builder.build())
        }
    }

Once, service class created, we need to define the service in manifest file inside the application tag.

<service
            android:name=".LocationService"
            android:enabled="true"
            android:exported="true" />

Also, we need to add foreground service permission in the user permissions to run the foreground service.

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

Start Location update listener

In the onStartCommand of the service, we need to start the location update listener to fetch the location update on the service. I have created the LocationHelper class to initiate the location listener. also, create an interface to update the live location to service.

Check the this link to learn more about getting current location in detail.

LocationHelper.kt

class LocationHelper {
    var LOCATION_REFRESH_TIME = 3000 // 3 seconds. The Minimum Time to get location update
    var LOCATION_REFRESH_DISTANCE =
        0 // 0 meters. The Minimum Distance to be changed to get location update

    @SuppressLint("MissingPermission")
    fun startListeningUserLocation(context: Context, myListener: MyLocationListener) {
        val mLocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        val locationListener: LocationListener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
                myListener.onLocationChanged(location) // calling listener to inform that updated location is available
            }

            override fun onProviderEnabled(provider: String) {}
            override fun onProviderDisabled(provider: String) {}
            override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
        }
        mLocationManager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,
            LOCATION_REFRESH_TIME.toLong(),
            LOCATION_REFRESH_DISTANCE.toFloat(),
            locationListener
        )
    }
}

interface MyLocationListener {
    fun onLocationChanged(location: Location?)
}

in the requestLocationUpdates(), we need to pass the location provided, the minimum time for the location refresh, and the minimum distance for the location refresh. in our case, we need to get the location every 3 seconds. So I set 3000 ms.

No we need to call the startListeningUserLocation() helper method in our onStartCommand().

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        val timer = Timer()
        LocationHelper().startListeningUserLocation(
            this, object : MyLocationListener {
                override fun onLocationChanged(location: Location?) {
                    mLocation = location
                    mLocation?.let {
                    }
                }
            })
        return START_STICKY
    }

Call the API to update the location to the server

Once you received the location on service, er need to call the API from the service. To call API, I am using a retrofit API client. Please check the below link to learn more about the retrofit setup.

Retrofit android example [step by step]

ApiClient.kt

interface ApiClient {

    @GET("updateLocation.json")
    fun updateLocation() : Call<LocationResponse>

    companion object {

        var retrofit: Retrofit? = null
        val logging = HttpLoggingInterceptor().apply {
            this.level = HttpLoggingInterceptor.Level.BODY
        }
        fun getInstance(context: Context) : Retrofit {
            if (retrofit == null) {
                retrofit = Retrofit.Builder()
                    .baseUrl("https://howtodoandroid.com/apis/")
                    .client(OkHttpClient.Builder().addInterceptor(logging).build())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
            }
            return retrofit!!
        }
    }
}

Once the retrofit setup is completed, we need to call the API call in the background thread. service will run on the main thread. So I have crested AppExecutor to run the API call in the background.

AppExecutors.kt

class AppExecutors private constructor(
    private val diskIO: Executor,
    private val networkIO: Executor,
    private val mainThread: Executor
) {
    fun diskIO(): Executor {
        return diskIO
    }

    fun mainThread(): Executor {
        return mainThread
    }

    fun networkIO(): Executor {
        return networkIO
    }

    private class MainThreadExecutor : Executor {
        private val mainThreadHandler = Handler(Looper.getMainLooper())
        override fun execute(command: Runnable) {
            mainThreadHandler.post(command)
        }
    }

    companion object {
        // For Singleton instantiation
        private val LOCK = Any()
        private var sInstance: AppExecutors? = null
        val instance: AppExecutors?
            get() {
                if (sInstance == null) {
                    synchronized(LOCK) {
                        sInstance = AppExecutors(
                            Executors.newSingleThreadExecutor(),
                            Executors.newFixedThreadPool(3),
                            MainThreadExecutor()
                        )
                    }
                }
                return sInstance
            }
    }
}

If you run this code now, you can able to see the app with update the current location to a server in every given interval.

LocationService.kt

class LocationService : Service() {
    private val NOTIFICATION_CHANNEL_ID = "my_notification_location"
    private val TAG = "LocationService"
    override fun onCreate() {
        super.onCreate()
        isServiceStarted = true
        val builder: NotificationCompat.Builder =
            NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
                .setOngoing(false)
                .setSmallIcon(R.drawable.ic_launcher_background)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager: NotificationManager =
                getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val notificationChannel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW
            )
            notificationChannel.description = NOTIFICATION_CHANNEL_ID
            notificationChannel.setSound(null, null)
            notificationManager.createNotificationChannel(notificationChannel)
            startForeground(1, builder.build())
        }
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        val timer = Timer()
        LocationHelper().startListeningUserLocation(
            this, object : MyLocationListener {
                override fun onLocationChanged(location: Location?) {
                    mLocation = location
                    mLocation?.let {
                        AppExecutors.instance?.networkIO()?.execute {
                            val apiClient = ApiClient.getInstance(this@LocationService)
                                .create(ApiClient::class.java)
                            val response = apiClient.updateLocation()
                            response.enqueue(object : Callback<LocationResponse> {
                                override fun onResponse(
                                    call: Call<LocationResponse>,
                                    response: Response<LocationResponse>
                                ) {
                                    Log.d(TAG, "onLocationChanged: Latitude ${it.latitude} , Longitude ${it.longitude}")
                                    Log.d(TAG, "run: Running = Location Update Successful")
                                }

                                override fun onFailure(call: Call<LocationResponse>, t: Throwable) {
                                    Log.d(TAG, "run: Running = Location Update Failed")

                                }
                            })

                        }
                    }
                }
            })
        return START_STICKY
    }

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

    override fun onDestroy() {
        super.onDestroy()
        isServiceStarted = false

    }

    companion object {
        var mLocation: Location? = null
        var isServiceStarted = false
    }
}

Adding boot complete receiver

If you want to start the service automatically after the restart of the device, You need to add the broadcast receiver for boot completed receiver.

class BootDeviceReceivers : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        
    }
}

on the onReceive of the boot complete, we need to start the service again. We can do this by calling the same service again.

class BootDeviceReceivers : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        context?.let {
            ContextCompat.startForegroundService(it, Intent(it, LocationService::class.java))
        }
    }
}

Also, don’t forget to add the broadcast receiver on manifeats.xml file inside the application tag.

<receiver
            android:name=".BootDeviceReceivers"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

That’s it. Now you can able to fetch the location continuously and send the location to the server. Download this example from GITHUB.

Thanks for reading. Please provide your feedback in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *