DEV Community

Bigyan Thapa
Bigyan Thapa

Posted on

WorkManager for Background location updates

If we are working as Android Developers, almost always we will have some requirement to perform some task in the background. One of the common task is to receive location updates in the background.

Previously, we might have used Evernote Android-job, BroadcastReceivers, PendingIntent, Push notification, JobIntentService, Firebase Job dispatcher or some combination along those lines. But with Google's announcement of deprecating those implementations and moving to WorkManager has given us a deadline to migrate our existing implementation towards that direction.

In this article, I am going to put some light on how to receive location updates in background using WorkManager.

Worker class

class UpdateLocationWorker(context: Context, workerParams: WorkerParameters, private val someRepository: SomeRepository) :
    CoroutineWorker(context, workerParams) {
    override suspend fun doWork(): Result {
        return withContext(Dispatchers.IO) {
            try {
                someRepository.getLocation()
                Result.success()
            } catch (e: Exception) {
                Log.d("$TAG", "Exception getting location -->  ${e.message}")
                Result.failure()
            }
        }
    }

    companion object {
        private const val TAG = "UpdateLocationWorker"
        private const val DEFAULT_MIN_INTERVAL = 15L

        @JvmStatic
        fun schedule() {
            val worker = PeriodicWorkRequestBuilder<LocationUpdateWorker>(DEFAULT_MIN_INTERVAL, TimeUnit.MINUTES)
                .addTag(TAG).build()
            WorkManager.getInstance(getApplicationInstance()) // provide application instance here
                .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, worker)
        }
    }
}

Note that the default minimum interval is 15 minutes and that is android default. If we set an interval less than 15 minutes, that will get overridden to 15 minutes in runtime. More on that here.

Repository class

class SomeRepository(someDao: SomeDao, someLocationManager: SomeLocationManager, scope: CoroutineScope) {

  suspend fun getLocation() {
        someLocationManager.getUpdatedLocation { location -> onLocationReceived(location) }
    }

    internal fun onLocationReceived(location: Location?) = coroutineScope.launch {

        location?.let { newLocation ->
            saveLocation(LocationData(newLocation))
        }
    }

   suspend fun saveLocation(location: LocationData) {
        dao.saveLocation(location)
    }
}

Location manager class

class SomeLocationManager(val context: Context) {

    fun getUpdatedLocation(block: (Location?) -> Unit) {

        val permission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)

        if (permission == PackageManager.PERMISSION_GRANTED) {
            LocationServices.getFusedLocationProviderClient(context).lastLocation.addOnSuccessListener { location ->
                block.invoke(location)
            }
        } else {
            block.invoke(null)
        }
    }
}

Dao class

@Dao
interface SomeDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  fun saveLocation(location: LocationData)
}

LocationData and utility

data class LocationData(
    val provider: String,
    val time: Long,
    val latitude: Double,
    val longitude: Double,
    val altitude: Double,
    val accuracy: Float
)

fun Location.toLocationData(location: Location) = LocationData(
   provider = "fused",
   time = this.time,
   latitude = this.latitude,
   longitude = this.longitude,
   altitude = this.altitude,
   accuracy = this.accuracy
)

Usage

We could schedule the worker when MainActivity or the Application is started.

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    UpdateLocationWorker.schedule()
  }
}

Jobs scheduled with WorkManagers are guaranteed to run in the given interval even if the app is closed or force killed.

For further reading, please follow the WorkManger link above.
Also, please feel free to provide feedback in comments.
Thank you!

Top comments (6)

Collapse
 
dave1812 profile image
David Chikli • Edited

Hey ! thanks for the tutorial, however when I run the app I get :

Could not instantiate com.david.dave.backgroundTasks.FirebaseUpdateLocationBackgroundTask

java.lang.NoSuchMethodException: <init> [class android.content.Context, class androidx.work.WorkerParameters]
Enter fullscreen mode Exit fullscreen mode

Any idea why ?

Collapse
 
hiral1987 profile image
Hiral Dharod • Edited

How to create & pass someRepository instance to UpdateLocationWorker class?

Collapse
 
bigyan4424 profile image
Bigyan Thapa

@Hiral For that, you will need to implement your own WorkerFactory class and set the worker configuration in your Application class. Something like below:

class DevApplication : Application, Configuration.Provider {
...

@Override 
fun getWorkManagerConfiguration() : Configuration {
  return Configuration.Builder()
              .setWorkerFactory( DevWorkerFactory(this)
              .build()
 }
class DevWorkerFactory(private val application: DevApplication) : WorkerFactory() {
  ...
    override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters) : ListenableWorker? {
     return  when(workerClassName) {
                     DevWorker::class.java.canonicalName -> DevWorker(
                     appContext, workerParameters, DevRepository()
                     )
                    else -> null
     }
} 
class DevWorker( appContext: Context, workerParameters: WorkerParameters, private val devRepository: DevRepository) : CoroutineWorker(appContext, workerParameters) {

    override suspend fun doWork() : Result {
          TODO("Do work with repository")
    }
   ...
}

This is one way of implementing this. If you are familiar with DI tools like dagger, there are other ways of implementing this.
I hope this helps.

Collapse
 
hiral1987 profile image
Hiral Dharod

thank u. This should work.

Collapse
 
igorganapolsky profile image
Igor Ganapolsky

This is not true. If you force-kill your app from System apps menu, your WorkManager will cease to run.

Collapse
 
bigyan4424 profile image
Bigyan Thapa

Hi @Igor Ganapolsky, please correct me if I am wrong, but based on this documentation, once workmanager tasks are scheduled, those will be executed independent of app's life-cycle or even if the device restarts. The tasks are guaranteed to run.