1

I'm using android device's native Geofence service. Here are implementation details:

Tracked transition type: ENTER

Range: 500 meters (1640 feet)

Notification responsive time: 500ms

Added Geofence count: 15-20

Initial trigger (setInitialTrigger()): Not set

Location accuracy on device: High

Location permissions: FINE LOCATION and COARSE LOCATION

Location service on device: ON

Location permission to app: Yes

Android Oreo support: Yes (Used Broadcast receiver and JobIntentService)

Issues:

  1. On some device, same notification is triggering again and again when user is moving withing same geofence.
  2. On some device, some notifications are triggering some are not.
  3. On some device, no geofence in triggering at all.

Shall I move to third-party geofence services? If yes, could you please suggest any good service at this?

Creating goefence:

private const val NOTIFICATION_RESPONSIVENESS_TIME = 500
private const val GEOFENCE_RADIUS_IN_METERS = 500f
private const val GEOFENCE_PENDING_INTENT_REQUEST_CODE = 1

private fun createGeofences(context: Context, communityList: List<Community>) {
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return
        }

    //Adding geofence for all received communities
    val geofenceList = communityList
            .asSequence()
            .filter { community -> isValidCommunityForGeofence(community) }
            .map { community -> toGeofence(community) }
            .toList()

    val geofencingRequest = GeofencingRequest.Builder()
            .addGeofences(geofenceList)
            .build()

    val pendingIntent = getGeofencePendingIntent(context)
    val geofencingClient: GeofencingClient = LocationServices.getGeofencingClient(context)
    geofencingClient.addGeofences(geofencingRequest, pendingIntent)
            .addOnCompleteListener(GeofenceAddRemoveListener(true))
}


private fun toGeofence(community: Community): Geofence {
    return Geofence.Builder()
            .setRequestId(community.bdxCommunityId.toString())//unique ID for geofence
            .setCircularRegion(community.latitude, community.longitude, GEOFENCE_RADIUS_IN_METERS)
            .setNotificationResponsiveness(NOTIFICATION_RESPONSIVENESS_TIME)
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
            .setLoiteringDelay(0)
            .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
            .build()
}



private fun getGeofencePendingIntent(context: Context): PendingIntent {
    val intent = Intent(context, GeofenceBroadcastReceiver::class.java)
    return PendingIntent.getBroadcast(context, GEOFENCE_PENDING_INTENT_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}

private fun isValidCommunityForGeofence(community: Community): Boolean {
    return community.latitude != null && community.longitude != null && community.latitude != 0.0
            && community.longitude != 0.0 && !TextUtils.isEmpty(community.name)
}

Manifest file:

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-feature android:name="android.hardware.location.network" />
    <uses-feature android:name="android.hardware.location.gps" />

 <receiver
            android:name=".misc.geofence.GeofenceBroadcastReceiver"
            android:enabled="true"
            android:exported="true" />

        <service
            android:name=".misc.geofence.GeofenceTransitionsJobIntentService"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE" />

Broadcast receiver:

class GeofenceBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // Enqueues a JobIntentService passing the context and intent as parameters
        GeofenceTransitionsJobIntentService.enqueueWork(context, intent)
    }
}

JobIntentService:

class GeofenceTransitionsJobIntentService : JobIntentService() {

    companion object {
        fun enqueueWork(context: Context, intent: Intent) {
            JobIntentService.enqueueWork(context, GeofenceTransitionsJobIntentService::class.java, JobServiceID.GEOFENCE_JOB_ID, intent)
        }
    }

    /**
     * Handles incoming intents.
     *
     * @param intent sent by Location Services. This Intent is provided to Location Services (inside a PendingIntent)
     * when @GeofenceInteractor#refreshGeofences() is called.
     */
    override fun onHandleWork(intent: Intent) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)

        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceErrorMessages.getErrorString(geofencingEvent.errorCode)
            Logger.e(this, errorMessage)
            return
        }

        val geofenceTransition = geofencingEvent.geofenceTransition
        val userCommunityList = GeofenceInteractor.getUserCommunityList(this)

        // Get the geofences that were triggered. A single event can trigger multiple geofences.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
            val triggeringGeofences = geofencingEvent.triggeringGeofences

            //Showing notification for each geofence which triggered ENTER transition.
            for (geofence in triggeringGeofences) {
                val community = userCommunityList.asSequence().filter { community -> community.bdxCommunityId == geofence.requestId.toInt() }.firstOrNull()

                if (community != null) {
                    val transitionMessage = String.format(resources.getString(R.string.community_geofence_transition_entered), community.name)
                    sendGeofenceNotification(transitionMessage, community)
                }
                Logger.d(this, "Geofene triggered. Transition: " + geofenceTransition + " Community:" + community?.name)
            }
        } else {
            Logger.e(this, getString(R.string.geofence_transition_invalid_type, geofenceTransition))
        }
    }


    private fun sendGeofenceNotification(contentText: String, community: Community) {
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
                ?: return

        val notificationBuilder = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            NotificationCompat.Builder(this)
        } else {
            val notificationChannel = NotificationUtil.getOrCreateGeofenceNotificationChannel(this, notificationManager)!!
            NotificationCompat.Builder(this, notificationChannel.id)
        }

        val nextNotificationId = NotificationUtil.getNextNotificationId(this)
        val viewCommunityPendingIntent = getViewCommunityPendingIntent(nextNotificationId, community)
        val mapNavigationPendingIntent = getGeofenceMapNavigationPendingIntent(nextNotificationId, community)

        notificationBuilder.setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
                .setContentTitle(community.name)
                .setContentText(contentText)
                .setContentIntent(viewCommunityPendingIntent)
                .setAutoCancel(true)
                .setGroup(NotificationUtil.GEOFENCE_GROUP)
                .addAction(0, getString(R.string.navigate_to_community), mapNavigationPendingIntent)
                .addAction(0, getString(R.string.view), viewCommunityPendingIntent)

        notificationManager.notify(nextNotificationId, notificationBuilder.build())
    }

    private fun getViewCommunityPendingIntent(notificationId: Int, community: Community): PendingIntent? {
        val notificationBundle = Bundle()
        notificationBundle.putParcelable(Constants.COMMUNITY, community)
        notificationBundle.putInt(Constants.NOTIFICATION_ID, notificationId)

        val notificationIntent = Intent(applicationContext, SplashActivity::class.java)
        notificationIntent.putExtras(notificationBundle)

        val stackBuilder = TaskStackBuilder.create(this)
        stackBuilder.addParentStack(SplashActivity::class.java)
        stackBuilder.addNextIntent(notificationIntent)

        return stackBuilder.getPendingIntent(notificationId, PendingIntent.FLAG_UPDATE_CURRENT)
    }

    private fun getGeofenceMapNavigationPendingIntent(notificationId: Int, community: Community): PendingIntent? {
        val notificationBundle = Bundle()
        notificationBundle.putParcelable(Constants.COMMUNITY, community)
        notificationBundle.putInt(Constants.NOTIFICATION_ID, notificationId)

        val geofenceMapNavigationIntent = Intent(this, GeofenceMapNavigationActivity::class.java)
        geofenceMapNavigationIntent.putExtras(notificationBundle)

        val mapNavigationStackBuilder = TaskStackBuilder.create(this)
        mapNavigationStackBuilder.addParentStack(SplashActivity::class.java)
        mapNavigationStackBuilder.addNextIntent(geofenceMapNavigationIntent)

        return mapNavigationStackBuilder.getPendingIntent(notificationId, PendingIntent.FLAG_UPDATE_CURRENT)
    }

}
Rahul Rastogi
  • 4,486
  • 5
  • 32
  • 51
  • please post the code – Vikash Bijarniya Nov 21 '18 at 05:47
  • @VikashBijarniya Added source code. – Rahul Rastogi Nov 21 '18 at 06:00
  • Have you come to a solution yet? I am facing the same issue. – viper May 13 '19 at 08:14
  • @viper you got the solution ? – k_kumar Jun 26 '19 at 12:46
  • Yes, I got the solution. I used the Geofence sample by Google from GitHub. – Rahul Rastogi Jun 26 '19 at 13:55
  • @RahulRastogi How is the sample from Google different from what you posted above? I see they are the same. Is your solution working even when you kill the app (swipe away your activity)? – fernandospr Jul 09 '19 at 22:43
  • 1
    Yes! It works even when the application process is killed. But geofence isn't accurate on Android devices. I had 5-6 devices but the accuracy was good only on Google nexus 5X, Pixel , Samsung-S7 edge and was bad on devices of brands lenovo, motorola etc. So, it's not guranteed that Geofence notification triggers on exact time and location. I've seen time delay upto 4 minutes and range difference around 1km. But that's how it's on Android. But Apple provides this thing very nice. Though, sometimes Apple also looses to show Geofence notification. – Rahul Rastogi Jul 10 '19 at 12:02

1 Answers1

0

Let me show you what i have done for a similar task. Below code has been used to achieve geofencing.

class LocationService : Service(), GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks {

var mLocationManager: LocationManager? = null
var googleApiClient: GoogleApiClient? = null
var pendingIntent: PendingIntent? = null
var geofencingRequest: GeofencingRequest? = null
var mGeofenceList: ArrayList<Geofence>? = null

private inner class LocationListener(provider: String) : android.location.LocationListener {
    private var mLastLocation: Location = Location(provider)

    override fun onLocationChanged(location: Location) {
        mLastLocation.set(location)
    }

    override fun onProviderDisabled(provider: String) {}

    override fun onProviderEnabled(provider: String) {}

    override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
}

internal var mLocationListeners = arrayOf<android.location.LocationListener>(LocationListener(LocationManager.GPS_PROVIDER), LocationListener(LocationManager.NETWORK_PROVIDER))


override fun onBind(arg0: Intent): IBinder? {
    return null
}

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
    mGeofenceList = ArrayList()
    populateGeofenceList()
    super.onStartCommand(intent, flags, startId)
    return Service.START_STICKY
}

override fun onCreate() {
    initializeLocationManager()
    try {
        mLocationManager!!.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Constant.LOCATION_INTERVAL.toLong(), Constant.LOCATION_DISTANCE, mLocationListeners[1])
    } catch (ex: java.lang.SecurityException) {
        ex.printStackTrace()
    } catch (ex: IllegalArgumentException) {
        ex.printStackTrace()
    }

    try {
        mLocationManager!!.requestLocationUpdates(
                LocationManager.GPS_PROVIDER, Constant.LOCATION_INTERVAL.toLong(), Constant.LOCATION_DISTANCE,
                mLocationListeners[0])
    } catch (ex: java.lang.SecurityException) {
        ex.printStackTrace()
    } catch (ex: IllegalArgumentException) {
        ex.printStackTrace()
    }

}

override fun onDestroy() {
    super.onDestroy()
    if (mLocationManager != null) {
        for (i in mLocationListeners.indices) {
            try {
                mLocationManager!!.removeUpdates(mLocationListeners[i])
            } catch (ex: Exception) {
                ex.printStackTrace()
            }
        }
    }
}

private fun initializeLocationManager() {

    googleApiClient = GoogleApiClient.Builder(this)
            .addApi(LocationServices.API)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this).build()
    googleApiClient!!.connect()
    if (mLocationManager == null) {
        mLocationManager = applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
    }
}

private fun startLocationMonitor() {
    val locationRequest = LocationRequest.create()
            .setInterval(Constant.LOCATION_INTERVAL.toLong())
            .setFastestInterval(Constant.LOCATION_INTERVAL.toLong())
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
    try {
        LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, object : com.google.android.gms.location.LocationListener {
            override fun onLocationChanged(location: Location) {
                DashBoardActivity.latitude = location.latitude
                DashBoardActivity.longitude = location.longitude
                if (BuildConfig.DEBUG) {
                    Log.e("LocationChanged:", location.latitude.toString() + " ," + location.longitude)
                }
            }
        })
    } catch (e: SecurityException) {
        e.printStackTrace()
    }
}

private fun startGeofencing() {
    pendingIntent = getGeofencePendingIntent()
    geofencingRequest = GeofencingRequest.Builder()
            .setInitialTrigger(Geofence.GEOFENCE_TRANSITION_ENTER)
            .addGeofences(mGeofenceList)
            .build()

    if (googleApiClient?.isConnected!!) {
        try {
            LocationServices.GeofencingApi.addGeofences(googleApiClient, geofencingRequest, pendingIntent).setResultCallback(object : ResultCallback<Status> {
                override fun onResult(status: Status) {
                }
            })
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }
}

private fun populateGeofenceList() {
    for (entry in Constant.AREA_LANDMARKS.entries) { // Replace with your Location List

        mGeofenceList?.add(Geofence.Builder()
                .setRequestId(entry.key)
                .setCircularRegion(entry.value.latitude, entry.value.longitude, Constant.GEOFENCE_RADIUS_IN_METERS)
                .setExpirationDuration(Geofence.NEVER_EXPIRE)
                .setNotificationResponsiveness(1000)
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
                .build())
    }
}

private fun getGeofencePendingIntent(): PendingIntent? {
    if (pendingIntent != null) {
        return pendingIntent
    }
    val intent = Intent(this, GeofenceRegistrationService::class.java)
    pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
    return pendingIntent
}

override fun onConnected(bundle: Bundle?) {
    startGeofencing()
    startLocationMonitor()
}

override fun onConnectionSuspended(i: Int) {}

override fun onConnectionFailed(connectionResult: ConnectionResult) {}

}

For getting the geofence events, i have used below code:

class GeofenceRegistrationService : IntentService("GeoIntentService") {

val TAG = "GeoIntentService"
var mGeofencList: ArrayList<Geofence>? = null

override fun onHandleIntent(intent: Intent?) {
    mGeofencList = ArrayList()
    val geofencingEvent = GeofencingEvent.fromIntent(intent)
    if (geofencingEvent.hasError()) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Error" + geofencingEvent.errorCode)
        }
    } else {
        try {
            val transaction = geofencingEvent.geofenceTransition
            val geofences = geofencingEvent.triggeringGeofences
            for (i in 0 until geofences.size) {
                mGeofencList?.add(geofences[i])
            }
            if (transaction == Geofence.GEOFENCE_TRANSITION_ENTER) {
                sendBroadCast(true)
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "You are inside Geofenced area")
                }
            }
            if (transaction == Geofence.GEOFENCE_TRANSITION_EXIT) {
                sendBroadCast(false)
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "You are outside Geofenced area")
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
}

private fun sendBroadCast(isInside: Boolean) {
    val broadCastIntent = Intent(Constant.SRI_GEO_FENCE)
    broadCastIntent.putExtra(Constant.KEY_GEOFENCE_STATE, isInside)
    broadCastIntent.putExtra(Constant.KEY_GEOFENCE_LIST, mGeofencList)
    LocalBroadcastManager.getInstance(this).sendBroadcast(broadCastIntent)
}

}

Then you just have to start the LocationService as follows:

 val locationIntent = Intent(activity, LocationService::class.java)
                    activity.startService(locationIntent)

It has been tested and working perfectly. If there is any question, please approach me. Thanks

Vikash Bijarniya
  • 404
  • 4
  • 10