DEV Community

Cover image for Dynamic Geofencing 100+ in Kotlin - Android
Ibrahim
Ibrahim

Posted on • Edited on

4 1

Dynamic Geofencing 100+ in Kotlin - Android

Geofencing API by Google helps provide contextual information to users based on their location. However, we have a limit of 100 active geofences at one time.

Lets break the limit with dynamic geofencing to suit our usecase.

What's our use case?

We need to ping our users when they are near any Toll Plaza with an "Upcoming Toll Plaza" notification on their device. How many Tolls do we have in India? 500+. Before we get into registering 500+ geofences dynamically. Lets see what do we already know.

Geo.. What?

Geofence is a geographical region which can be monitored for transition events such as Entry, Exit or Dwell. Have a look at this diagram.

Alt Text

We are interested only in Entry and Exit transition events for now.

We know we have a limit of 100 geofences at a time.

Straight Outta Docs:

You can have multiple active geofences, with a limit of 100 per app, per device user. For each geofence, you can ask Location Services to send you entrance and exit events, or you can specify a duration within the geofence area to wait, or dwell, before triggering an event. You can limit the duration of any geofence by specifying an expiration duration in milliseconds. After the geofence expires, Location Services automatically removes it.

Geo.. How?

Let's Engineer. We have a limit of 100 active geofence. So we need to figure out a radius for a region which can never have more than 100 geofences inside it. According to our use case, we can safely assume 300km radius. We will call this region Parent Region. Inside this Parent Region we will have multiple Child Regions - 5km radius which are our actual geofences for Tolls. When the device exits the Parent Region we will setup a new Parent Region from the device's current location and reset the toll geofences. Using this, at no point device will have 100+ active geofences. As device moves out of the 300km region, new geofences will get registered dynamically.

We now have following tasks to perform:

  1. Parent Region : Setup a Geofence of 300km radius from user's current location with Geofence transition event set to EXIT.

  2. Filter out locations (Tolls) from the list of 500+ locations which fall inside the above region by calculating their distance.

  3. Child Region : Setup a Geofence of 5km radius on all filtered locations with Geofence transition event set to ENTRY.

  4. When transition event ENTRY is triggered on Child Region, display a notification to user.

  5. When transition event EXIT is triggered on Parent Region, unregister all the existing Child Regions and repeat steps 1 to 3.

Note: 300km for Parent Region and 5km for Child Region are assumptions I have made for my use case. Feel free to set the radii as per your requirement.

Geo..Code!

Before we get into dynamic geofences. How do we register a single geofence? Check out this great article on raywenderlich.com on how to setup geofence in Android which I have followed in this article as well.

Lets start by creating models.

Geofence

  • id
  • latitude
  • longitude
  • radius
data class GeofenceModel(var id: String,var lat : Double, var lng: Double, var radius: Double)

Toll

  • id
  • name
  • latitude
  • longitude

We will be fetching our tolls from Room database. Here's an Entity class for Toll table.

@Entity(tableName = "toll")
data class Toll(
@PrimaryKey
var id: Int = 0,
var name: String = "",
var lat: String = "",
var lng: String = ""
)
view raw Toll.kt hosted with ❤ by GitHub

1) The first step is to setup a parent geofence with a region of 300km radius.

Put this code in your Launcher/Main Activity.

private fun setupGeofence() {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(mContext)
//Get user's current location
fusedLocationClient.lastLocation.addOnSuccessListener {
val userLat = it.latitude
val userLong = it.longitude
//Create an instance of our GeofenceHelper Class
val helper = GeofenceHelper(mContext)
val geofencingClient = LocationServices.getGeofencingClient(mContext)
//Step 1
helper.setupParentGeofence(mContext, userLat, userLong, geofencingClient)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

What's happening?

  • We get user's current location.
  • We create an instance of GeofenceHelper class.
  • We setup parent geofence.

GeofenceHelper:

class GeofenceHelper(context: Context) {
fun buildGeofencingRequest(geofences: List<Geofence>): GeofencingRequest {
return GeofencingRequest.Builder()
.setInitialTrigger(0)
.addGeofences(geofences)
.build()
}
val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(context, GeofenceBroadcastReceiver::class.java)
PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
fun setupParentGeofence(
context: Context,
userLat: Double,
userLong: Double,
geofencingClient: GeofencingClient
) {
//Create Parent geofence model with id "p0" and radius 300km
val parentGeoFence = GeofenceModel("p0", userLat, userLong, 300000.0)
//Build parent Geofence
val geofence = buildGeoFence(parentGeoFence, GeoFenceType.PARENT)
//Check Background Location permission is granted
if (geofence != null && ContextCompat.checkSelfPermission(context,Manifest.permission.ACCESS_BACKGROUND_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
geofencingClient
.addGeofences(
buildGeofencingRequest(listOf(geofence)),
geofencePendingIntent)
.addOnSuccessListener {
"Parent Geofence Added Successfully".log("GEO","Parent Geofence Success")
}
.addOnFailureListener {
"Parent Geofence Add Failed".log("GEO", "Parent Geofence Failed")
}
}
}
fun buildGeoFence(geofence: GeofenceModel, type: GeoFenceType): Geofence? {
val latitude = geofence.lat
val longitude = geofence.lng
val radius = geofence.radius
if (latitude != null && longitude != null && radius != null) {
val builder = Geofence.Builder()
.setRequestId(geofence.id)
.setCircularRegion(
latitude,
longitude,
radius.toFloat()
)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
//Set Exit Transition for parent region
if (type == GeoFenceType.PARENT) {
builder.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_EXIT)
//Set Enter Transition for child region
} else {
builder.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
}
return builder.build()
}
return null
}
}
enum class GeoFenceType {
PARENT, CHILD
}

What's happening?

  • Inside setupParentGeofence() function, we create Parent Geofence model with id,lat,long and radius of 300km.
  • We build a Geofence with transition EXIT for parent region. We are passing an enum value GeofenceType.PARENT to buildGeofence() function for distinction.
  • Make sure to ask user for Background Location permission before setting up geofences in MainActivity. We include a location permission check here before registering geofences.
  • Use geofencingClient object to register geofences. It takes a GeofencingRequest and a PendingIntent pointing to GeofenceBroadcastReceiver where we handle our geofencing event triggers. More about this later.

2) Now we setup our Child Regions (Toll locations) which fall in the above Parent Region.

Update setupGeofences() function in MainActivity as below.

private fun setupGeofence() {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(mContext)
//Get user's current location
fusedLocationClient.lastLocation.addOnSuccessListener {
val userLat = it.latitude
val userLong = it.longitude
val helper = GeofenceHelper(mContext)
val geofencingClient = LocationServices.getGeofencingClient(mContext)
//Step 1
helper.setupParentGeofence(mContext, userLat, userLong, geofencingClient)
//Step 2
mViewModel.tollsList.observe(this, Observer {
helper.setupChildGeofences(
mContext,
it,
userLat,
userLong,
geofencingClient
)
})
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

What's happening?

  • We are using MVVM design pattern with LiveData. mViewModel.tollsList.observe will fetch all the tolls from the database which we pass into setupChildGeofences() function inside our helper class.

  • Hold up! as we have seen above we need a Geofence model for registering geofences. However, we get a list of Toll models from the database. Let's transform all Toll models into Geofence models using Transformations provided by androidx.lifecycle.

var tollsList: LiveData<List<GeofenceModel>> =
Transformations.map(DataRepository.getAllTolls(), ::getTolls)
private fun getTolls(tolls: List<Toll>): List<GeofenceModel> {
val list = ArrayList<GeofenceModel>()
tolls.forEach {
list.add(
GeofenceModel(
it.id,
it.lat.toDouble(),
it.lng.toDouble(),
Constants.GEOFENCE_TOLL_RADIUS
)
)
}
return list
}
  • Along with the tolls list, we are also passing user's current lat-long into setupChildGeofences(). Why? Keep reading!

Let's look at setupChildGeofences() function.

fun setupChildGeofences(
context: Context,
geofenceModelList: List<GeofenceModel>,
currentLat: Double,
currentLong: Double,
geofencingClient: GeofencingClient
) {
//Filter list as per user's current location
val filteredGeofences = filterListAsperUserLocation(
geofenceModelList,
currentLat,
currentLong
)
filteredGeofences.forEach { model ->
//Build Geofence for each child Geofence models.
val geofence = buildGeoFence(model, GeoFenceType.CHILD)
if (geofence != null && ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
geofencingClient
.addGeofences(
buildGeofencingRequest(listOf(geofence)),
geofencePendingIntent
)
.addOnSuccessListener {
"Child Geofence Added Successfully".log("GEO","Geofence Success")
}
.addOnFailureListener {
"Child Geofence Add Failed".log("GEO","Geofence Failed")
}
}
}
//Save Registered child geofence ids in SharesPreferences, so we can unregister them later.
val childGeofenceIds = filteredGeofences.map { it.id }
SPManager.getInstance(context).saveChildGeoFences(childGeofenceIds)
}
private fun filterListAsperUserLocation(
allTolls: List<GeofenceModel>,
lat: Double,
long: Double
): List<GeofenceModel> {
return allTolls.filter { model ->
val results = FloatArray(1)
//Check if tolls distance is less than 300km from user's current location
Location.distanceBetween(model.lat, model.lng, lat, long, results)
results[0] < 300000
}
}

What's happening?

  • We use Location.distanceBetween() function to filter the list of locations. We use user's current lat-long and check if the toll's location is less than 300km from user's current location in distance.

  • We run a loop on this filtered list and register geofences similar to parent geofence. Double check you are passing GeofenceType.CHILD to buildGeofence()function.

  • After successful registration, we store ids of these child geofences to SharedPreferences, so that we can unregister them later.

Congratulations! We have successfully registered one parent geofence and multiple child geofences inside it.

Geo..Dynamic!

Let's jump right into code, where our Entry and Exit events are captured.

GeofenceTransitionsJobIntentService

class GeofenceTransitionsJobIntentService : JobIntentService() {
companion object {
private const val JOB_ID = 101
fun enqueueWork(context: Context, intent: Intent) {
enqueueWork(
context,
GeofenceTransitionsJobIntentService::class.java, JOB_ID,
intent
)
}
}
override fun onHandleWork(intent: Intent) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
handleEvent(geofencingEvent)
}
private fun handleEvent(event: GeofencingEvent) {
//Child geofence Enter event
if (event.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
var geofence = event.triggeringGeofences[0]
generateNotification(DataRepository.getTollByName(geofence.requestId), "Upcoming Toll Plaza")
//Parent geofence Exit event
} else if (event.geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
//Get Geofencing Client
val geofencingClient =
LocationServices.getGeofencingClient(this@GeofenceTransitionsJobIntentService)
//Unregister registered child geofences
unregisterExistingGeofences(geofencingClient)
//Get help
val helper = GeofenceHelper(this@GeofenceTransitionsJobIntentService)
//Setup new Parent Geofence
helper.setupParentGeofence(
this@GeofenceTransitionsJobIntentService,
event.triggeringLocation.latitude,
event.triggeringLocation.longitude,
geofencingClient
)
//Setup new Child Geofences
helper.setupChildGeofences(
this@GeofenceTransitionsJobIntentService,
//Get Tolls from Db and map each Toll Object to Geofence Object
DataRepository.getAllTollsBackground().map {
GeofenceModel(it.id, it.lat.toDouble(), it.lng.toDouble(), 5000.0)
},
event.triggeringLocation.latitude,
event.triggeringLocation.longitude,
geofencingClient
)
}
}
private fun unregisterExistingGeofences(geofencingClient: GeofencingClient) {
geofencingClient
//Pass the ids of registered child geofences stored in SharedPreferences
.removeGeofences(SPManager.getInstance(this@GeofenceTransitionsJobIntentService).getChildGeofences())
.addOnSuccessListener {
"Geofences removed Successfully".log("GEO")
}
.addOnFailureListener {
"Geofences remove Failed".log("GEO")
}
}
}

What's happening?

  • GeofenceTransitionsJobIntentService's enqueueWork() is called from the onReceive() of our registered GeofenceBroadcastReceiver below.
class GeofenceBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
GeofenceTransitionsJobIntentService.enqueueWork(context, intent)
}
}
  • In onHandleWork(), we have a geofencingEvent created from the intent. We pass this event to handleEvent() function where all the magic happens.

  • If the transition event is GEOFENCE_TRANSITION_ENTER, we have a child geofence entry event, so we generate a notification for the user with the Toll name and a title - "Upcoming Toll Plaza".

  • If the transition event is GEOFENCE_TRANSITION_EXIT, we have a parent geofence exit event, so we setup a new parent region and child regions same as before.

  • Before registering new geofences, we unregister existing child geofences by Ids stored in SharedPreferences.

  • For device's current location we use, event.triggeringLocation.latitude and event.triggeringLocation.longitude.

  • Since we are already in a background thread now, we can directly get the list of Tolls and map each element to Geofence model.

Awesome! We have successfully implemented dynamic geofencing in Android.

I am open to feedbacks and suggestions. Please reach out if you have any doubts.

Sentry blog image

The Visual Studio App Center’s retiring

But sadly….you’re not. See how to make the switch to Sentry for all your crash reporting needs.

Read more

Top comments (2)

Collapse
 
tomslabon profile image
Tomasz Słaboń

Hi Ibrahim, great article and tips how to omit the limitation on the geofencing Android feature :)

What do you think about delegating this logic and possibility to create as many fences as you want to the online API, like this one: developer.tomtom.com/geofencing-ap...?

Collapse
 
hugoroam profile image
HugoRoam

Hey @ibrahim_broach maybe you wanna check out our easy to use, but very elaborate Geofencing API?

Sentry growth stunted Image

If you are wasting time trying to track down the cause of a crash, it’s time for a better solution. Get your crash rates to zero (or close to zero as possible) with less time and effort.

Try Sentry for more visibility into crashes, better workflow tools, and customizable alerts and reporting.

Switch Tools