Android Geofencing with Push Notifications
In this article, you can learn how to use geofencing in android applications. We send push notification whenever user enters or exits our geofencing range.
We can also use custom location service for android geofencing. But in this article, We use google location api for geofencing. Geofencing is very helpful to notify app users with specific location information. In this example, We notify user with push notification whenever he enters our range of predefined locations.
Add below dependencies in your app level build.gradle file
Add location permissions in your manifest for android geofencing
<uses-permission android:name = “android.permission.ACCESS_FINE_LOCATION” />
Ask background location permission for android geofencing
For geofencing, We need to track user location constantly foreground and background to check user enters our defined location range or not. In earlier versions of android, If user granted foreground location access then app got both foreground and background location access. But in android 10 and higher versions, We need to add background location permission in android manifest file.
Check Location Permissions at runtime for android geofencing
ActivityCompat.requestPermissions( this@MainActivity, arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_ LOCATION ), MY_PERMISSIONS_REQUEST_LOCATION )
} else {
addGeofences()
}
when (requestCode) {
MY_PERMISSIONS_REQUEST_LOCATION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
addGeofences()
} else {
}
return
}
}
}
Add Your Geofences in List with Geofence Object
Now we create geofence using specific latitude, longitude, location range and notification duration.
var listPlaces = ArrayList<MyLocation>()
listPlaces.add(MyLocation(“Place1”, 63.153409,-7.294953))
listPlaces.add(MyLocation(“Place2”, 63.213090, -7.340183))
listPlaces.add(MyLocation(“Place3”, 63.294233, -7.381343))
listPlaces.add(MyLocation(“Place4”, 63.336492, -7.438219))
listPlaces.add(MyLocation(“Place5”, 63.383416, -7.491321))
for (location: MyLocation in listPlaces) {
mGeofenceList?.add( Geofence.Builder()
.setRequestId(location.key)
.setCircularRegion( location.latitude, location.longitude, 100f )
.setNotificationResponsiveness(1000)
.setExpirationDuration( Geofence.NEVER_EXPIRE)
.setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER)
.build()
)
}
Create Geofencing Client
Now we create an instance of geofencing client to access google location api.
createGeoFencePendingIntent()?.let { mGeofencePendingIntent ->
mGeofencingClient?.addGeofences( createGeofencingRequest(), mGeofencePendingIntent ) ?.addOnCompleteListener(this)
}
if (task.isSuccessful) {
Toast.makeText(this, “Geofencing Successful”, Toast.LENGTH_SHORT).show()
} else {
val errorMessage = task.exception?.let { MyGeofenceErrorMessages.getErrorString(this, it) }
Log.e(“Geofencing Failed: “, errorMessage)
}
}
Add Geofences in Geofencing Request Builder
We add already created geofences list in geofence request builder class by building geofence request. We can also set initial trigger with geofence enter or exit parameter. Also can use dwell parameter if user stops in geofencing range for specific duration.
builder.setInitialTrigger( GeofencingRequest.INITIAL_TRIGGER_ENTER)
builder.addGeofences(mGeofenceList)
return builder.build()
Create Geofence Pending Intent
Whenever our first geofencing event is triggered, Geofence Intent Service is launched to handle that event.
return it
}
val intent = Intent(this, MyGeofenceTransitionsIntentService::class.java)
mGeofencePendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
return mGeofencePendingIntent
Add Geofence Error Messages for android geofencing
We set some error messages in string.xml file for geofencing errors.
<string name=”geofence_not_available”>Geofence service is not available now. Go to Settings>Location>Mode and choose High accuracy.</string>
<string name=”geofence_too_many_geofences”>Your app has registered too many geofences.</string
<string name = “geofence_too_many_pending_intents”>You have provided too many PendingIntents to the addGeofences() call.</string>
Create Geofencing Transitions Intent Service
Now we create geofence intent service to handle our geofencing events.
override fun onHandleIntent(intent: Intent?) {
try {
val event: GeofencingEvent = GeofencingEvent.fromIntent(intent)
if (event.hasError()) {
val errorMessage = MyGeofenceErrorMessages.getErrorString(this, event.errorCode)
Log.e(“Geofencing Error: “,errorMessage)
return
}
val transition = event.geofenceTransition
if (transition == Geofence.GEOFENCE_TRANSITION_ENTER) {
val geofences: List<Geofence> = event.triggeringGeofences
val transitionDetails: String = geofenceTransitionDetails(transition, geofences)
sendGeofencingNotification( transitionDetails )
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
android:name = “.MyGeofenceTransitionsIntentService”
android:exported = “true” />
Get Geofence Transition Details
We can get the details of triggered geofence using geofence transition type and geofence request id.
val geofenceTransitionString = transitionString(geofenceTransition)
val triggerGeofencesIdsList = ArrayList<String>()
for (geofence: Geofence in triggerGeofences) {
triggerGeofencesIdsList.add( geofence.requestId )
}
val triggerGeofencesIdsString = TextUtils.join(“, “, triggerGeofencesIdsList)
return geofenceTransitionString + “: ” + triggerGeofencesIdsString
}
private fun transitionString(transitionType: Int): String {
return when (transitionType) {
Geofence.GEOFENCE_TRANSITION_ENTER -> getString(R.string.geofence_transition_entered)
Geofence.GEOFENCE_TRANSITION_EXIT -> getString(R.string.geofence_transition_exited)
else -> getString(R.string.unknown_geofence_transition)
}
}
Send Push Notification When User entered selected range
We send push notification to user if he enters in our predefined geofencing range. We can also send notification for exit event if we want.
val geoChannelId = “AndroidGeofencingSample”
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val geoChannelName = getString(R.string.app_name)
val geoChannel = NotificationChannel(geoChannelId, geoChannelName, NotificationManager.IMPORTANCE_HIGH)
geoNotificationManager?. createNotificationChannel( geoChannel )
}
val geoNotificationIntent = Intent(this, MainActivity::class.java)
val geoStackBuilder = TaskStackBuilder.create(this)
geoStackBuilder.addParentStack( MainActivity::class.java )
geoStackBuilder.addNextIntent( geoNotificationIntent )
val geoNotificationPendingIntent = geoStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
val geoDefaultSoundUri: Uri = RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION )
val geoNotificationBuilder = NotificationCompat.Builder(this)
geoNotificationBuilder.setSmallIcon( R.mipmap.ic_launcher )
.setLargeIcon( BitmapFactory.decodeResource( resources, R.mipmap.ic_launcher) )
.setColor(Color.RED)
.setContentTitle(titleText)
.setSound(geoDefaultSoundUri)
.setContentIntent(geoNotificationPendingIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
geoNotificationBuilder.setChannelId( geoChannelId )
} else {
geoNotificationBuilder.priority = Notification.PRIORITY_HIGH
}
geoNotificationBuilder.setAutoCancel(true)
geoNotificationManager?.notify(0, geoNotificationBuilder.build())
Final Code
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.androidgeofencingsample"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".MyGeofenceTransitionsIntentService" android:exported="true" /> </application> </manifest>
strings.xml
<resources> <string name="app_name">AndroidGeofencingSample</string> <string name="geofence_unknown_error">Unknown error: the Geofence service is not available now.</string> <string name="geofence_not_available">Geofence service is not available now. Go to Settings>Location>Mode and choose High accuracy.</string> <string name="geofence_too_many_geofences">Your app has registered too many geofences.</string> <string name="geofence_too_many_pending_intents">You have provided too many PendingIntents to the addGeofences() call.</string> <string name="geofence_transition_invalid_type">Geofence transition error: invalid transition type %1$d</string> <string name="geofence_transition_entered">Entered</string> <string name="geofence_transition_exited">Exited</string> <string name="unknown_geofence_transition">Unknown Transition</string> <string name="geofence_transition_notification_text">Click notification to return to app</string> </resources>
MyLocation.kt
package com.example.androidgeofencingsample data class MyLocation(val key: String, val latitude: Double, val longitude: Double)
MainActivity.kt
package com.example.androidgeofencingsample import android.Manifest import android.app.Activity import android.app.PendingIntent import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.google.android.gms.location.Geofence import com.google.android.gms.location.GeofencingClient import com.google.android.gms.location.GeofencingRequest import com.google.android.gms.location.LocationServices import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task class MainActivity : Activity(), OnCompleteListener<Void> { private var mGeofencingClient: GeofencingClient? = null private val MY_PERMISSIONS_REQUEST_LOCATION = 42 private var mGeofenceList: ArrayList<Geofence>? = null private var mGeofencePendingIntent: PendingIntent? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (ContextCompat.checkSelfPermission( this.applicationContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this@MainActivity, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION), MY_PERMISSIONS_REQUEST_LOCATION) } else { addGeofences() } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { when (requestCode) { MY_PERMISSIONS_REQUEST_LOCATION -> { if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { addGeofences() } else { } return } } } private fun addGeofences() { mGeofenceList = ArrayList<Geofence>() var listPlaces = ArrayList<MyLocation>() listPlaces.add(MyLocation("Place1",63.153409,-7.294953)) listPlaces.add(MyLocation("Place2",63.213090, -7.340183)) listPlaces.add(MyLocation("Place3", 63.294233, -7.381343)) listPlaces.add(MyLocation("Place4", 63.336492, -7.438219)) listPlaces.add(MyLocation("Place5", 63.383416, -7.491321)) for (location: MyLocation in listPlaces) { mGeofenceList?.add( Geofence.Builder() .setRequestId(location.key) .setCircularRegion( location.latitude, location.longitude, 100f ) .setNotificationResponsiveness(1000) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER) .build() ) } createGeofencingClient() } private fun createGeofencingClient() { mGeofencePendingIntent = null mGeofencingClient = LocationServices.getGeofencingClient(this@MainActivity) createGeoFencePendingIntent()?.let { mGeofencePendingIntent -> mGeofencingClient?.addGeofences(createGeofencingRequest(), mGeofencePendingIntent) ?.addOnCompleteListener(this) } } override fun onComplete(task: Task<Void>) { if (task.isSuccessful) { Toast.makeText(this, "Geofencing Successful", Toast.LENGTH_SHORT).show() } else { val errorMessage = task.exception?.let { MyGeofenceErrorMessages.getErrorString(this, it) } Log.e("Geofencing Failed: ", errorMessage) } } private fun createGeofencingRequest(): GeofencingRequest { val builder = GeofencingRequest.Builder() builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) builder.addGeofences(mGeofenceList) return builder.build() } private fun createGeoFencePendingIntent(): PendingIntent? { mGeofencePendingIntent?.let { return it } val intent = Intent(this, MyGeofenceTransitionsIntentService::class.java) mGeofencePendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return mGeofencePendingIntent } }
MyGeofenceTransitionsIntentService.kt
package com.example.androidgeofencingsample import android.app.* import android.content.Context import android.content.Intent import android.graphics.BitmapFactory import android.graphics.Color import android.media.RingtoneManager import android.net.Uri import android.os.Build import android.text.TextUtils import android.util.Log import com.google.android.gms.location.Geofence import com.google.android.gms.location.GeofencingEvent import androidx.core.app.NotificationCompat import androidx.core.app.TaskStackBuilder import kotlin.Exception class MyGeofenceTransitionsIntentService : IntentService("GeofencingService") { override fun onHandleIntent(intent: Intent?) { try { val event: GeofencingEvent = GeofencingEvent.fromIntent(intent) if (event.hasError()) { val errorMessage = MyGeofenceErrorMessages.getErrorString(this, event.errorCode) Log.e("Geofencing Error: ",errorMessage) return } val transition = event.geofenceTransition if (transition == Geofence.GEOFENCE_TRANSITION_ENTER) { val geofences: List<Geofence> = event.triggeringGeofences val transitionDetails: String = geofenceTransitionDetails(transition, geofences) sendGeofencingNotification(transitionDetails) } } catch (e: Exception) { e.printStackTrace() } } private fun geofenceTransitionDetails(geofenceTransition: Int, triggerGeofences: List<Geofence> ): String { val geofenceTransitionString = transitionString(geofenceTransition) val triggerGeofencesIdsList = ArrayList<String>() for (geofence: Geofence in triggerGeofences) { triggerGeofencesIdsList.add(geofence.requestId) } val triggerGeofencesIdsString = TextUtils.join(", ", triggerGeofencesIdsList) return geofenceTransitionString + ": " + triggerGeofencesIdsString } private fun transitionString(transitionType: Int): String { return when (transitionType) { Geofence.GEOFENCE_TRANSITION_ENTER -> getString(R.string.geofence_transition_entered) Geofence.GEOFENCE_TRANSITION_EXIT -> getString(R.string.geofence_transition_exited) else -> getString(R.string.unknown_geofence_transition) } } fun sendGeofencingNotification(titleText: String) { try { val geoNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager val geoChannelId = "AndroidGeofencingSample" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val geoChannelName = getString(R.string.app_name) val geoChannel = NotificationChannel(geoChannelId, geoChannelName, NotificationManager.IMPORTANCE_HIGH) geoNotificationManager?.createNotificationChannel(geoChannel) } val geoNotificationIntent = Intent(this, MainActivity::class.java) val geoStackBuilder = TaskStackBuilder.create(this) geoStackBuilder.addParentStack(MainActivity::class.java) geoStackBuilder.addNextIntent(geoNotificationIntent) val geoNotificationPendingIntent = geoStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) val geoDefaultSoundUri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val geoNotificationBuilder = NotificationCompat.Builder(this) geoNotificationBuilder.setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)) .setColor(Color.RED) .setContentTitle(titleText) .setSound(geoDefaultSoundUri) .setContentIntent(geoNotificationPendingIntent) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { geoNotificationBuilder.setChannelId(geoChannelId) } else { geoNotificationBuilder.priority = Notification.PRIORITY_HIGH } geoNotificationBuilder.setAutoCancel(true) geoNotificationManager?.notify(0, geoNotificationBuilder.build()) } catch (e: Exception) { e.printStackTrace() } } }