Implementing Geofencing in Android using Kotlin
Geofence is an imitated variable that describes a real geographical area of interest. Geofencing API allows you to define the outline or limit of a specific area. When users cross the Geofence, they are alerted by a notification. <!--more--> Geofencing API employs the use of device sensors to detect a user's location in a battery-efficient manner.
Geofence comprises of three transition types:
- Enter – This demonstrates that the user has entered the geofence.
- Dwelling – Indicates that the user exists within the geofence for a given period.
- Exit – This shows that the user has moved out of the geofence.
Prerequisites
To follow along this tutorial, you should:
- Have the most recent version of Android Studio installed on your machine.
- Have basic knowledge on Google Maps.
- Have basic knowledge on the Kotlin programming language.
- Be able to use ViewBinding.
Getting started
Step 1 – Creating an Android project
In this step, we'll create an Android Studio project with a Google Map activity.
Make sure you have selected
Google Maps Activity
template.
Step 2 – Including the required dependencies
Include the following dependencies in your app-level build.gradle
file.
implementation 'com.google.android.gms:play-services-maps:17.0.1'
implementation 'com.google.android.gms:play-services-location:18.0.0'
Step 3 – Adding the required permissions
To start using Geofencing API, the user must first allow location permissions.
In the Android manifest
file, add the following permissions:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Check permissions
Before declaring the function, ensure that the app has permission to run in the foreground and background. It's useful to look into the Android API version of the device.
Add the following code in your MainActivity
file:
private val gadgetQ = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q
To determine whether permission has been granted or not, create the following function:
@TargetApi(29)
private fun approveForegroundAndBackgroundLocation(): Boolean {
val foregroundLocationApproved = (
PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION
))
val backgroundPermissionApproved =
if (gadgetQ) {
PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
true
}
return foregroundLocationApproved && backgroundPermissionApproved
}
If the device is running Android Q (API 29), ensure that the permissions ACCESS_BACKGROUND_LOCATION
and ACCESS_FINE_LOCATION
are enabled.
If the device is running an older Android version, you don't need permission to access the user's location in the background.
private fun authorizedLocation(): Boolean {
val formalizeForeground = (
PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION
))
val formalizeBackground =
if (gadgetQ) {
PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
true
}
return formalizeForeground && formalizeBackground
}
Request background and fine location permissions
This is where you request permission from the user to access their location if it was not granted.
Add the following variables in a
global scope
or acompanion object
.
private val REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE = 3 // random unique value
private val REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE = 4
private val REQUEST_TURN_DEVICE_LOCATION_ON = 5
private fun askLocationPermission() {
if (authorizedLocation())
return
var grantingPermission = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
val customResult = when {
gadgetQ -> {
grantingPermission += Manifest.permission.ACCESS_BACKGROUND_LOCATION
REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
}
else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
}
Log.d(TAG, "askLocationPermission")
ActivityCompat.requestPermissions(
this, grantingPermission, customResult
)
}
Once the user responds to the permissions request, you should process their response in the onRequestPermissionsResult()
method, as shown below.
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE ||
requestCode == REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE) {
if (grantResults.size > 0 && (grantResults[0] == PackageManager.PERMISSION_GRANTED)){
validateGadgetAreaInitiateGeofence()
}
}
}
Step 4 - Examine the gadget's location.
Permissions granted will be worthless if the user's device location is deactivated. To verify that the device's location is enabled, add the following code.
Check the device location settings and start the Geofence
private fun validateGadgetAreaInitiateGeofence(resolve: Boolean = true) {
// create a location request that request for the quality of service to update the location
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
// check if the client location settings are satisfied
val client = LocationServices.getSettingsClient(this)
// create a location response that acts as a listener for the device location if enabled
val locationResponses = client.checkLocationSettings(builder.build())
locationResponses.addOnFailureListener { exception ->
if (exception is ResolvableApiException && resolve) {
try {
exception.startResolutionForResult(
this, REQUEST_TURN_DEVICE_LOCATION_ON
)
} catch (sendEx: IntentSender.SendIntentException) {
Log.d(TAG, "Error getting location settings resolution: ${sendEx.message}")
}
} else {
Toast.makeText(this, "Enable your location", Toast.LENGTH_SHORT).show()
}
}
locationResponses.addOnCompleteListener {it ->
if (it.isSuccessful) {
addGeofence()
}
}
}
Check if the user has accepted or rejected the request in the onActivityResult()
method. If they haven't, prompt them again.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
validateGadgetAreaInitiateGeofence(false)
}
Step 5 – Adding and removing a geofence
Adding Geofence
You'll need a method to inherit from the PendingIntent
to manage Geofence transitions.
A PendingIntent
describes both an intent
and the action
that should be done in response to it.
We'll define a pending intent for a BroadcastReceiver
to control the Geofence transitions.
private val geofenceIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
A GeofencingClient
is the most basic way to interact with the Geofencing APIs.
Therefore, create an instance of GeofencingClient
:
private lateinit var geoClient: GeofencingClient
In the onCreate()
method, initialize the geofencingClient
:
geoClient = LocationServices.getGeofencingClient(this)
Still, in the onCreate
method, add a geofenceList
that holds geofences
.
In this step, we will add one geofence but you can have as many as you wish.
val latitude = 0.616016
val longitude = 34.521816
val radius = 100f
geofenceList.add(Geofence.Builder()
.setRequestId("entry.key")
.setCircularRegion(latitude,longitude,radius)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build())
Create a function that specifies the geofence, as highlighted below:
private fun seekGeofencing(): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofences(geofenceList)
}.build()
}
To associate a geofence with a pendingIntent
, create a geofence function and include the following implementation within it:
private fun addGeofence(){
if (ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
geofencingClient?.addGeofences(getGeofencingRequest(), geofenceIntent)?.run {
addOnSuccessListener {
Toast.makeText(this@MapsActivity, "Geofence(s) added", Toast.LENGTH_SHORT).show()
}
addOnFailureListener {
Toast.makeText(this@MapsActivity, "Failed to add geofence(s)", Toast.LENGTH_SHORT).show()
}
}
}
Removing a geofence
It is a good practice to remove any geofence associated with a PendingIntent
when not in use. We do so, using the following method:
private fun removeGeofence(){
geofencingClient?.removeGeofences(geofenceIntent)?.run {
addOnSuccessListener {
Toast.makeText(this@MapsActivity, "Geofences removed", Toast.LENGTH_SHORT).show()
}
addOnFailureListener {
Toast.makeText(this@MapsActivity, "Failed to remove geofences", Toast.LENGTH_SHORT).show()
}
}
}
Within the onDestroy
method, call the removeGeofence()
function:
override fun onDestroy() {
super.onDestroy()
removeGeofence()
}
Step 6 – Creating a BroadcastReceiver class
Android applications can send and receive broadcast messages on devices.
The BroadcastReceiver
listens for Geofence transitions and provides a notification when a device enters a particular geofence area.
The BroadcastReceiver is implemented as follows:
class GeofenceBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
Log.e(TAG, errorMessage)
return
}
val geofenceTransition = geofencingEvent.geofenceTransition
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
val triggeringGeofences = geofencingEvent.triggeringGeofences
// Creating and sending notification
val notificationManager = ContextCompat.getSystemService(
context!!, NotificationManager::class.java
) as NotificationManager
notificationManager.sendGeofenceEnteredNotification(context)
} else {
Log.e(TAG, "Invalid type transition $geofenceTransition")
}
}
}
In your manifest, add the following code to register the BroadCastReceiver:
<application>
...
<receiver android:name=".GeofenceBroadcastReceiver"/>
</application>
Setting up a notification
We set up a notification using the following code:
private const val NOTIFICATION_ID = 33
private const val CHANNEL_ID = "GeofenceChannel"
fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel =
NotificationChannel(CHANNEL_ID, "Channel1", NotificationManager.IMPORTANCE_HIGH)
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(notificationChannel)
}
}
// extension function
fun NotificationManager.sendGeofenceEnteredNotification(context: Context) {
// Opening the notification
val contentIntent = Intent(context, MapsActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity(
context,
NOTIFICATION_ID,
contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
// Building the notification
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(context.getString(R.string.app_name))
.setContentText("You have entered a geofence area")
.setSmallIcon(R.drawable.ic_baseline_notifications_24)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(contentPendingIntent)
.build()
this.notify(NOTIFICATION_ID, builder)
}
Conclusion
In this article, we have learned what geofencing is, how to add and remove a geofence, listening to geofence events using a broadcast receiver, and displaying a notification when someone enters a geofence.
You can check the full implementation on this repository on GitHub.
Happy coding!
Further reading
Peer Review Contributions by: Eric Gacoki