Targeting Users In Specific Area Using Geofence API | by Himanshu Verma | Towards Data Science

Google Geofence API is used when you want to notify users if they are near the specific area in the world (such as Shopping Malls, Airport, Restaurants etc.). For example, the customer has installed an online shopping app and if they are near one of the stores that the app has a tie up with, then store could generate a notification in the app to let the customer know that the store near them has a special offer running for them. This way you can engage more users.
To use the Geofence API, you’ll have to obtain the Google Maps API key using the following set of steps.
Click on THIS link to open the Google Cloud Platform, if you’re not signed in, then please sign in first and create the project.
You can give any name to this project or you can just leave it as it is and click on CREATE now.
Now ensure that you’ve selected the newly created project.
Now click on the menu icon on left and select the APIs & Services and then choose Library from the sub-list.
Search Maps SDK for Android and enable this API (don’t forget to enable API because it's important)
Now again click on the menu icon on left and choose Credentials and then + CREATE CREDENTIALS and finally click on API Key
Copy this API Key and paste it inside the string.xml file
We’re going to build an application which will allow you to set your location (or specific area to be precise) on a map. And when you reach the specified location, your app will notify you with a message you defined at the time when you set the location.
In the end, you’ll have an app which will look similar to this.
To set the location on the map, you can drag the marker on your desired location. Once you specified your location, you can proceed by clicking CONTINUE button.
Are you excited now?, I’m sure you’ll be.
Google Map API
Google Geofence API
Service
Broadcast Receiver
Creating Notification
Storing and retrieving custom objects in Shared-preference
Now let’s quickly set up our project and proceed.
Create a New Android Project, select the empty activity and give any name you want to your project.
Add the following dependencies in your app-level build.gradle
implementation 'com.google.android.gms:play-services-location:16.0.0'implementation 'com.google.android.gms:play-services-maps:16.0.0'implementation 'com.google.code.gson:gson:2.8.5'implementation "com.google.android.material:material:1.1.0-alpha02"
If you encounter the following error in your activity.xml file
Then add the following line inside your build.gradle
android{ defaultConfig {vectorDrawables.useSupportLibrary = true}}
Add the following permissions in your AndroidManifest.xml file
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Now add the below meta-data tag in your AndroidManifest.xml file inside the <application> tag, we’ll add the API_KEY here later on.
<meta-data    android:name="com.google.android.geo.API_KEY"    android:value="API_KEY"/>
Finally, sync the project and setup done.
Let’s get quickly familiar with some android components used in this app
Service: A service is an android component which is used to perform background tasks. A task may be of any kind such as uploading photos, syncing user data on server and generating notifications etc.
Broadcast Receiver: A broadcast receiver is also a fundamental component of android, it comes handy when you want to perform some actions in your app which are based on future events. For instance, you only want to upload user data to your server when the phone is connected to the WIFI and another example is, you want to start the background service when the user restarts his phone.
PendingIntent: PendingIntent is similar to normal intent except that it will not execute immediately.
I’ve all the classes, utility functions and constants under the root package.
Let’s understand them one by one.
MainActivity.kt: This is our home screen which will show you Google Map and two action buttons on the bottom right side. The first button will allow you to zoom in on the map and the plus action button will open new activity which is ReminderActivity.kt, on this screen you can choose your location and set the message and save the reminder.
ReminderActivity.kt: As I explained this activity will allow you to set the location and save the reminder in the shared-preference. When you choose the location and save it, you’ll see the circle around the location you specified. This circle is boundary or radius you’ll define in your code. The larger circle means, it will cover the larger area. And as you enter into this area, you’ll be notified by a notification.
MyBroadcast Receiver: This is a broadcast receiver which will be fired when the user reaches to the specified location. When this broadcast receiver fires, it will execute the background service.
GeofenceTransitionService.kt: This is a service which will be executed by our broadcast receiver and when this executes it generates the notification to let user know he has reached his destination.
MyReminder.kt: This is a data class and in its constructor, I’m accepting four parameters and they’re unique id, latitude, longitude, radium and a message. We’ll keep this information in shared-preference and at the same time, we will set the Pending Intent to generate the notification.
MyReminderRepository.kt: As you know repository helps us to get the core methods of the application to perform actions we defined for our app. So only this class is responsible for adding a new reminder, removing reminder and setting Pending Intents. We will always create an object of this class and perform the actions.
Utils.kt: This is our utility class which contains four methods.vectorToBitmap() method converts the vector image to bitmap, showReminderInMap() method displays the reminder on the map, this is the method which draws the circle around the specified location. And sendNotification() method creates the notification, this method is called by our background service. The last method is getUniqueId() which obviously returns the unique id.
AppConstants.kt: This class contains all the constants used for this app. This class also contains a service method and two methods to call the MainActivity.kt and ReminderActivity.kt activities.
Enough talking? let’s get back to business...
When you create a project, you will have a default ActivityMain.kt class. I want you to create one more class and name it as ReminderActivity.kt. In this class extend AppCompatActivity() class and implement OnMapReadyCallback the interface of Google Map and implement its method onMapReady(). For now, leave everything as it is.
Now we’ll create our AppConstants class and define all constants and few methods.
In this class, we defined our constants and three methods, newIntentForMainActivity() method will call the MainActivity.kt class and newIntentForReminderActivity() will call the ReminderActivity.kt class. enqueueWork() method will be called by our broadcast receiver to generate the notification.Tip: All constants and methods are defined inside the companion object, and this is because whenever we need to call any of these methods, we won’t have to create object of this class.
Now we’ll create Utils.kt and add the following methods
When we’ll call this method, we have to pass two parameters i.e context.resources and an id of our image which needs to converted into bitmap. The context.resources returns a resources instance for your application. Inside this method, I got the drawable object, created the Bitmap and Canvas objects. We now have our image in vectorDrawable and we’ll convert this object into bitmap, then draw bitmap on canvas and set its bounds and return.
Tip: If you’re not familiar with Canvas, then you can think of it as a blank piece of paper where you can draw any shape.
showReminderInMap() method takes context, GoogleMap object and MyReminder class object as an argument and checks if myReminder has a value, then it proceeds further otherwise it does nothing. Inside the if statement I’m getting the latitude and longitude from my myReminder object and casting it to LatLng and in the second if statement, I’m drawing the circle around the specified location. In general, this method is used to draw the circle on a specified location.
The sendNotification() method is pretty straightforward, it does nothing except creating the notification. This method is called from the service to show the notification.
Now create this class in your root directory and also add this broadcast receiver in your AndroidManifest.xml file.
<receiver    android:name=".MyBroadcastReceiver"    android:enabled="true"    android:exported="true" />
This class extends the BroadcastReceiver() class which is an abstract class and implements its onReceive() method. As the onReceive() method is executed, it will execute the enqueueWork() method of AppConstants.kt.
This receiver would be registered viaPendingIntent when you will add the reminder.
Create this class and declare it in the AndroidManifest.xml file
<service    android:name=".GeofenceTransitionService"    android:exported="true"    android:permission="android.permission.BIND_JOB_SERVICE" />
Note: If a job service is declared in the manifest but not protected with BIND_JOB_SERVICE permission, that service will be ignored by the system.
This is a service which extends the JobIntentService class, IntentService or JobIntentService is a service which runs in the background on a separate thread and it gets destroyed when the job is done. To use this class we’ll have to implement its required method onHandleWork() .
class GeofenceTransitionService : JobIntentService() {override fun onHandleWork(intent: Intent) {    val geofencingEvent = GeofencingEvent.fromIntent(intent)    repositoryMy = MyReminderRepository(this)    if (geofencingEvent.hasError()) {        Log.e(AppConstants.LOG_TAG, "An error occured")        return    }    handleEvent(geofencingEvent)}}
Inside this method, we can do our task. I’m getting the object of GeofencingEvent class inside this method, if there is an issue, we show the error message otherwise we call the handleEvent() method.
private fun handleEvent(event: GeofencingEvent) {    if (event.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {        val reminder = getFirstReminder(event.triggeringGeofences)        val message = reminder?.message        val latLng = reminder?.latLng        if (message != null && latLng != null) {            sendNotification(this, message, latLng)        }    }}
In this method, we first use the GeofencingEvent object and checks whether the user enters in the specified area or not, if it then gets the reminder object from the repository, retrieves its data and send the notification.
private fun getFirstReminder(triggeringGeofences: List<Geofence>): MyReminder? {    val firstGeofence = triggeringGeofences[0]    return repositoryMy.get(firstGeofence.requestId)}
getFirstReminder() method returns the object of MyReminder class from the MyRepository class.
Below is the full code for this service class
Tip: lateinit is a keyword in Kotlin which frees you from not to initialise it at time of declaration but it should be initialised before its use otherwise, you will run into the NullPointerException.
You can think of this class as a local server which means it serves us everything we needed to perform any action on the map such as adding a reminder or removing reminder and maintaining all reminders.
The following code snippet creates an object of GeofencingClient which helps you to manipulate the geofences.
private val geofencingClient = LocationServices.getGeofencingClient(context)
Here we’re getting the SharedPreference object and Gson object
private val preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)private val gson = Gson()
In the SharedPreference, we’ll store the reminders and using Gson object we’ll convert json string to Java object and vice-versa. Since SharedPreference can hold only primitive values in the form of key-value pairs but we want to store our class object, so how it is possible?. Obviously, we can convert our object to json string and store it in a SharedPreference, whenever we need to use it, we will convert it into a real object.
The combination of latitude, longitude and a radius is known as geofence. It’s a circular area at a specific location on the map. Whenever users enter this area, the app will trigger particular behaviour.
private fun buildGeofence(myReminder: MyReminder): Geofence? {    val latitude = myReminder.latLng?.latitude    val longitude = myReminder.latLng?.longitude    val radius = myReminder.radius    if (latitude != null && longitude != null && radius != null) {        return Geofence.Builder()            .setRequestId(myReminder.id)            .setCircularRegion(                latitude,                longitude,                radius.toFloat()            )            .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)            .setExpirationDuration(Geofence.NEVER_EXPIRE)            .build()    }    return null}
Look at the code, we’re building the geofence using Geofence.Builder().
In the method setRequestId(), I’m passing unique id since every geofence is unique in its way, you will get this id from your model class.
In the setCircularRegion(), I’m setting my latitude, longitude and radius, all these parameters together form a circle on a specific location.
The setTransitionTypes() defines the transition type, you can use GEOFENCE_TRANSITION_ENTER to trigger an event when the user enters into the defined area.
The setExpirationDuration() method sets the expiry of the geofence, we’re using NEVER_EXPIRE which means that it will never expire until the user removes it.
You’ll use this method to build the geofence request.
private fun buildGeofencingRequest(geofence: Geofence): GeofencingRequest {    return GeofencingRequest.Builder()        .setInitialTrigger(0)        .addGeofences(listOf(geofence))        .build()}
Here you’re setting setInitialTrigger() to 0 which means that you don’t want your app to trigger an event when the app is already inside the geofence. And the next addGeofences() method adds the request to geofence.
As you can see in the code snippet, we’re passing MyBroadcastReceiver in the intent object which means that when this pending intent is called, it will call broadcast receiver and this receiver will call GeofenceTransitionService.
private val geofencePendingIntent: PendingIntent by lazy {    val intent = Intent(context, MyBroadcastReceiver::class.java)    PendingIntent.getBroadcast(        context,        0,        intent,        PendingIntent.FLAG_UPDATE_CURRENT    )}
This code will only be executed when GEOFENCE_TRANSITION_ENTER event is triggered.
Tip: by lazy property, the value only gets computed when this is used for the first time.
This add() method adds the reminder in the SharedPreference and also adds the request to the geofence.
fun add(myReminder: MyReminder, success: () -> Unit, failure: (error: String) -> Unit){    val geofence = buildGeofence(myReminder)    if (geofence != null && ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {        geofencingClient.addGeofences(buildGeofencingRequest(geofence), geofencePendingIntent)            .addOnSuccessListener {                saveAll(getAll() + myReminder)                success()            }            .addOnFailureListener {                failure("An error occured")            }    }}
First of all, we’re checking the permission if it is granted, then we proceed, and next, we’re creating the geofence using the geofencingClient if it is successful, then only we’ll save in shared preference, otherwise report an error message.
This method simply removes the reminder from the list.
fun remove(myReminder: MyReminder) {    val list = getAll() - myReminder    saveAll(list)}
This method adds all the reminders in the list in shared-preference.
private fun saveAll(list: List<MyReminder>) {    preferences        .edit()        .putString(REMINDERS, gson.toJson(list))        .apply()}
Notice we’re converting the list to Json string, then we’re storing it.
This method returns the list of all reminders we stored in the shared-preference.
fun getAll(): List<MyReminder> {    if (preferences.contains(REMINDERS)) {        val remindersString = preferences.getString(REMINDERS, null)        val arrayOfReminders = gson.fromJson(            remindersString,            Array<MyReminder>::class.java        )        if (arrayOfReminders != null) {            return arrayOfReminders.toList()        }    }    return listOf()}
This function returns the reminder added in the list.
fun getLast() = getAll().lastOrNull()
Notice this function first calls the getAll() method which returns a list and we call lastOrNull() function which is a method of List class and this method returns the last value.
This method returns the first reminder from the list if the list is not null.
fun get(requestId: String?) = getAll().firstOrNull { it.id == requestId }
{it.id == requestId } is a lambda expression which is performing null checking. When there is only a single parameter inside the lambda expression, then it can be used.
Complete code for MyReminderRepository.kt
This activity allows us to add the new reminder on the specified location.
First of all, create these objects in this activity.
private lateinit var map: GoogleMaplateinit var myRepository: MyReminderRepositoryprivate var reminder = MyReminder(latLng = null, radius = null, message = null)
Now paste the following code inside your onCreate() method
override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  setContentView(R.layout.activity_new_reminder)  myRepository = MyReminderRepository(this)  val mapFragment = supportFragmentManager      .findFragmentById(R.id.map) as SupportMapFragment  mapFragment.getMapAsync(this)  instructionTitle.visibility = View.GONE  radiusDescription.visibility = View.GONE  message.visibility = View.GONE}
In this onCreate() method, I’m initialising my repository instance variable and setting the map. Initially, I hide all the TextView.
In this method, I’m getting LatLng as well as zoom value from the intent and setting the camera at the centre of the location.
private fun centerCamera() {  val latLng = intent.extras?.get(AppConstants.LAT_LNG) as LatLng  val zoom = intent.extras?.get(AppConstants.ZOOM) as Float  map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom))}
This method updates the reminder. On calling this method, it will show you circle around the location.
private fun showReminderUpdate() {  map.clear()  showReminderInMap(this, map, reminder)}
This method simply guides you through how you can navigate on the map.
private fun showConfigureLocationStep() {  marker.visibility = View.VISIBLE  instructionTitle.visibility = View.VISIBLE  radiusDescription.visibility = View.GONE  message.visibility = View.GONE  instructionTitle.text = "Drag map to set location"  next.setOnClickListener {    reminder.latLng = map.cameraPosition.target    showConfigureRadiusStep()  }  showReminderUpdate()}
This method is same as the above method and it guides you through how to add a message. This message will be shown when you will get a notification.
private fun showConfigureMessageStep() {  marker.visibility = View.GONE  instructionTitle.visibility = View.VISIBLE  message.visibility = View.VISIBLE  instructionTitle.text = "Enter a message"  next.setOnClickListener {    reminder.message = message.text.toString()    if (reminder.message.isNullOrEmpty()) {      message.error = "Message Error"    } else {      addReminder(reminder)    }  }  showReminderUpdate()}
This method calls the add method from the MyReminderRepository class and adds the reminder. If the reminder is added successfully, then it will you Snackbar on the bottom with a message “Reminder added” else failure message.
private fun addReminder(myReminder: MyReminder) {  myRepository.add(myReminder,      success = {        setResult(Activity.RESULT_OK)        finish()      },      failure = {        Snackbar.make(main, it, Snackbar.LENGTH_LONG).show()      })}
Finally modify your onMapReady()
override fun onMapReady(googleMap: GoogleMap) {  map = googleMap  map.uiSettings.isMapToolbarEnabled = false  centerCamera()  showConfigureLocationStep()}
Inside this method, we’re assigning the GoogleMap object to our map and then configuring settings.
Complete code for this activity
Layout file for this activity
The only thing you can notice in <fragment> tag is “android:name=”com.google.android.gms.maps.SupportMapFragment” line and this line helps you to add the Google map on the screen. This is the simplest way of adding a map. And rest of the code inside this file must be quite familiar for you.
Finally, this is the last part of our code. This is our home activity which you will see when you first interact with the app.
First of all, add these listeners in your activity and implement all its methods.
class MainActivity : AppCompatActivity(), OnMapReadyCallback, GoogleMap.OnMarkerClickListener
Now create these instance variables.
lateinit var myRepository: MyReminderRepository private var map: GoogleMap? = null private lateinit var locationManager: LocationManager
In this method, we’re initialising our instance variables first, then getting a map from a fragment from the activity_main.xml file and the listeners on the action buttons. Initially, these buttons will not be visible until the user gives ACCESS_FINE_LOCATION permission. As a user grants the permission, we make the buttons visible to the users inside the onMapAndPermissionReady() method.
Look at the below code, here I’m attaching a listener to the button and inside the lambda expression, I’m calling run method of Google map class. This run is also a lambda function. When user presses (+) action button, we take the user to the ReminderActivity.kt class.
actionNewReminder.setOnClickListener {    map?.run {        val intent = AppConstants.newIntentForReminderActivity(            this@MainActivity,            cameraPosition.target,            cameraPosition.zoom        )        startActivityForResult(intent, AppConstants.REMINDER_REQUEST_CODE)    }}
When permission is granted and the map doesn’t equal null, then we’re making visible our buttons and adding a listener to the actionCurrentLocation button. Now if the user presses the actionCurrentLocation button, then we get the location provider and last known location of the user. And if this is not null, then we’ll move the camera to the last know location.
When a user clicks on the marker, then an alert dialog pops up to ask user, if he wants to remove the reminder, if he presses “YES” then that reminder gets removed from the list.
override fun onMarkerClick(marker: Marker): Boolean {    val reminder = myRepository.get(marker.tag as String)    if (reminder != null) {        showReminderRemoveAlert(reminder)    }    return true}
showReminderRemoveAlert():
This method only creates an alert dialog with two options, i.e OK and Cancel and if a user clicks on “Ok”, then we call removeReminder() to remove it.
private fun showReminderRemoveAlert(myReminder: MyReminder) {    val alertDialog = AlertDialog.Builder(this).create()    alertDialog.run {        setMessage("Reminder Removed")        setButton(            AlertDialog.BUTTON_POSITIVE,            "OK"        ) { dialog, _ ->            removeReminder(myReminder)            dialog.dismiss()        }        setButton(            AlertDialog.BUTTON_NEGATIVE,            "Cancel"        ) { dialog, _ ->            dialog.dismiss()        }        show()    }}
Complete code of our ActivityMain.kt file
Now finally we have our activity_main.xml file
Run the app and give permission.
If you’re running app on emulator, then click on three dots you’ll see the follow screen. Now set any location you want (I set Connaught Place, New Delhi). And save this location.
Now click on the first action button on your app and you’ll see your map will navigate to the specified location.
Now add a reminder by click the (+) action button, drag the map to set your location, click on Continue and now type your message and again click Continue. Now your reminder has been saved.
Now again click on three dots, change location to somewhere else on emulator and save it.
Click on the first button your app, you will reach to the location you set on your emulator.
Finally, In the emulator, set your location where you set your reminder while saving it. And wait for a few seconds you’ll a notification.
If you have any difficulty with your project, then you can seek help from my Github project.
Subscribe to my mailing list to get the early access of my articles directly in your inbox and don’t forget to follow my own publication on Medium The Code Monster to polish your technical knowledge.
We’ve learnt how we can use Google map and Geofence APIs in our own project and we also learnt about services and broadcast receivers. Apart from this, we learnt a trick to store custom java object in SharedPreference using Gson and retrieving them using the same.