68532

Geofence triggering issues in Android

Question:

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)

<strong>Issues:</strong>

<ol><li>On some device, same notification is triggering again and again when user is moving withing same geofence.</li> <li>On some device, some notifications are triggering some are not.</li> <li>On some device, no geofence in triggering at all.</li> </ol>

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

<strong>Creating goefence:</strong>

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

<strong>Manifest file:</strong>

<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" />

<strong>Broadcast receiver:</strong>

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

<strong>JobIntentService:</strong>

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

Answer1:

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

Recommend