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

implementation ‘com.google.android.gms:play-services-location:17.0.0’
 
 

Add location permissions in your manifest for android geofencing

<uses-permission android:name = “android.permission.ACCESS_COARSE_LOCATION” />
<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.

<uses-permission android:name = “android.permission.ACCESS_BACKGROUND_ LOCATION” />
 
 

Check Location Permissions at runtime for android geofencing

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
        }
    }
}

 
 

Add Your Geofences in List with Geofence Object

Now we create geofence using specific latitude, longitude, location range and notification duration.

var 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()
    )
}

 
 

Create Geofencing Client

Now we create an instance of geofencing client to access google location api.

var 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)
    }
}
 
 

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.

val builder = GeofencingRequest.Builder()
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.

mGeofencePendingIntent?.let {
    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_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>
 
 

Create Geofencing Transitions Intent Service

Now we create geofence intent service to handle our geofencing events.

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()
    }
}

}

 
<service
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.

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)
    }
}

 
 

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 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())

 
 

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() &amp;&amp; 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()
        }
    }

}

Leave a Reply