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.
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:
Parent Region : Setup a Geofence of 300km radius from user's current location with Geofence transition event set to EXIT.
Filter out locations (Tolls) from the list of 500+ locations which fall inside the above region by calculating their distance.
Child Region : Setup a Geofence of 5km radius on all filtered locations with Geofence transition event set to ENTRY.
When transition event ENTRY is triggered on Child Region, display a notification to user.
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 = "" | |
) |
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) | |
} | |
} |
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
tobuildGeofence()
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 | |
) | |
}) | |
} | |
} |
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 intosetupChildGeofences()
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
tobuildGeofence()
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 theonReceive()
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 tohandleEvent()
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
andevent.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.
Top comments (2)
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...?
Hey @ibrahim_broach maybe you wanna check out our easy to use, but very elaborate Geofencing API?