I have defined a BroacastReceiver according to the Geofencing docs in order to receive ENTRY & EXIT updates when the user interacts with a geofence. My issue is that the app is going to be used on the road so, when a user drives and enters a geofence I'm getting notified about it and the same happens when he exits it. However, when the exit event is received I need to remove the triggered geofence(s) from both the Client & the Google Maps map. Both of those exist in my MapsActivity (which is where I set up the receiver & event notification process based on the documentation) so I'd like to call the removeGeofences(...)
method of my activity from the receiver. I looked at a TON of posts regarding this matter but none seems to cover the Geofencing use case. I've tried declaring the receiver dynamically through code instead of statically through the manifest but in that case, I'd need intent filters which I can't find for Geofencing. Any ideas on how to achieve this?
BroadcastReceiver:
class GeofenceReceiver : BroadcastReceiver() {
private val TAG = GeofenceReceiver::class.java.simpleName
override fun onReceive(context: Context?, intent: Intent?) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes
.getStatusCodeString(geofencingEvent.errorCode)
Log.e(TAG, "error: $errorMessage")
return
}
// Get the transition type.
val geofenceTransition = geofencingEvent.geofenceTransition
// Test that the reported transition was of interest.
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT
) {
// Get the geofences that were triggered. A single event can trigger
// multiple geofences.
val triggeringGeofences = geofencingEvent.triggeringGeofences
val ids = arrayListOf<String>()
for (geofence in triggeringGeofences) {
Log.d(TAG, "Geofence ${geofence.requestId} triggered!")
ids.add(geofence.requestId)
}
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER)
Log.d(TAG, "User entered geofence!")
else {
Log.d(TAG, "User exited geofence!")
//activity.removeGeofences(ids)
}
} else {
// Log the error.
Log.e(TAG, "Invalid transition")
}
}
}
MapsActivity:
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private var mMap: GoogleMap? = null
private var geofenceClient: GeofencingClient? = null
private var geofenceList: ArrayList<Geofence> = arrayListOf()
private var geofenceMapMarks: MutableMap<String, Pair<Marker, Circle>> = mutableMapOf()
private var currLocationMarker: Marker? = null
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceReceiver::class.java)
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
// addGeofences() and removeGeofences()
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
private val mLocationListener: LocationListener = LocationListener {
currLocationMarker?.remove()
currLocationMarker = mMap?.addMarker(
MarkerOptions().position(LatLng(it.latitude, it.longitude)).title("Current Location")
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE))
)
animateCameraToLocation(currLocationMarker?.position!!)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
geofenceClient = LocationServices.getGeofencingClient(this)
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
val mLocationManager = getSystemService(LOCATION_SERVICE) as LocationManager
if (!shouldRequestPermissions()) subscribeToLiveCurrentLocation(mLocationManager)
}
@SuppressLint("MissingPermission")
private fun subscribeToLiveCurrentLocation(mLocationManager: LocationManager) {
mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000,
50F, mLocationListener
)
}
private fun requestPermissionsIfNeeded() {
if (!shouldRequestPermissions()) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
2
)
else ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
1
)
}
@SuppressLint("InlinedApi")
private fun shouldRequestPermissions(): Boolean = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
) != PackageManager.PERMISSION_GRANTED
/**
* Manipulates the map once available.
* This callback is triggered when the map is ready to be used.
* This is where we can add markers or lines, add listeners or move the camera. In this case,
* we just add a marker near Sydney, Australia.
* If Google Play services is not installed on the device, the user will be prompted to install
* it inside the SupportMapFragment. This method will only be triggered once the user has
* installed Google Play services and returned to the app.
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
mMap?.setOnMapClickListener { latlong ->
drawGeofenceOnMap(latlong)
addGeofenceToList(latlong)
}
}
private fun drawGeofenceOnMap(latlong: LatLng) {
val marker = mMap?.addMarker(MarkerOptions().position(latlong).title("Geofence"))
val circle = mMap?.addCircle(
CircleOptions().center(latlong).radius(defaultGeofenceRadius.toDouble()).strokeColor(
resources.getColor(android.R.color.holo_red_light, null)
).fillColor(
resources.getColor(android.R.color.transparent, null)
)
)
mMap?.moveCamera(CameraUpdateFactory.newLatLng(latlong))
geofenceMapMarks["Geofence #" + (geofenceList.size + 1)] = Pair(marker!!, circle!!)
}
private fun animateCameraToLocation(latlong: LatLng) {
val cameraPosition = CameraPosition.Builder()
.target(latlong)
.zoom(17f)
.build()
mMap?.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
}
private fun addGeofenceToList(latlong: LatLng) {
geofenceList.add(
Geofence.Builder()
// Set the request ID of the geofence. This is a string to identify this
// geofence.
.setRequestId("Geofence #" + (geofenceList.size + 1))
// Set the circular region of this geofence.
.setCircularRegion(
latlong.latitude,
latlong.longitude,
defaultGeofenceRadius
)
// Set the expiration duration of the geofence. This geofence gets automatically
// removed after this period of time.
.setExpirationDuration(Geofence.NEVER_EXPIRE)
// Set the transition types of interest. Alerts are only generated for these
// transition. We track entry and exit transitions in this sample.
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
// Create the geofence.
.build()
)
}
private fun getGeofencingRequest(): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofences(geofenceList)
}.build()
}
private fun addGeofences() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Toast.makeText(this, "Need to grant permissions to continue", Toast.LENGTH_LONG).show()
requestPermissionsIfNeeded()
return
}
geofenceClient?.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
addOnSuccessListener {
Toast.makeText(this@MapsActivity, "Geofences added!", Toast.LENGTH_LONG).show()
}
addOnFailureListener {
Toast.makeText(this@MapsActivity, "Failed to add geofences!", Toast.LENGTH_LONG)
.show()
Log.d("MAPSACTIVITY", it.message.toString())
}
}
}
fun removeGeofences(ids: ArrayList<String>) {
removeGeofencesFromClient(ids)
removeGeofencesFromMap(ids)
}
private fun removeGeofencesFromMap(ids: ArrayList<String>) {
for (id in ids) {
if (geofenceMapMarks.keys.contains(id)) geofenceMapMarks.remove(id)
}
}
private fun removeGeofencesFromClient(ids: ArrayList<String>) {
for (fence in geofenceList) {
if (ids.contains(fence.requestId)) geofenceList.remove(fence)
}
geofenceClient?.removeGeofences(ids)
?.addOnCompleteListener { task ->
if (task.isSuccessful) {
Toast.makeText(
this,
"Geofences have been removed!",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(
this,
"Failed to remove geofences",
Toast.LENGTH_LONG
).show()
Log.d("MAPSACTIVITY", "error ==> " + task.exception)
}
}
}
override fun onDestroy() {
super.onDestroy()
mMap = null
geofenceClient = null
}
@SuppressLint("InlinedApi")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
var grantFailed = false
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "GRANTED", Toast.LENGTH_SHORT).show()
} else grantFailed = true
}
2 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "GRANTED first", Toast.LENGTH_SHORT).show()
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
3
)
} else grantFailed = true
}
3 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "GRANTED second", Toast.LENGTH_SHORT).show()
} else grantFailed = true
}
}
if (grantFailed) {
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) ||
shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
) {
Toast.makeText(this, "Show permission rationale", Toast.LENGTH_LONG).show()
} else Toast.makeText(
this,
"You must grant the requested permissions to continue",
Toast.LENGTH_SHORT
).show()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.addGeofence -> {
addGeofences()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.options_menu, menu)
return true
}
}
Manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.spap.geofencepoc">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GeoFencePoC"
tools:ignore="AllowBackup">
<receiver android:name=".receivers.GeofenceReceiver"/>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<activity
android:name=".presentation.MapsActivity"
android:label="@string/title_activity_maps">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
You can use SharesPreference to notify triggered geoFenceId to MainActivity.
class GeofenceReceiver : BroadcastReceiver() {
private val TAG = GeofenceReceiver::class.java.simpleName
private var geoFencePref: SharedPreferences? = null
private val triggeredExitGeofenceIds: HashSet<String> = HashSet()
private var triggedGeofenceIdsList: ArrayList<String> = ArrayList()
override fun onReceive(context: Context?, intent: Intent?) {
geoFencePref = context?.getSharedPreferences("TriggerdExitedId",Context.MODE_PRIVATE)
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes
.getStatusCodeString(geofencingEvent.errorCode)
Log.e(TAG, "error: $errorMessage")
return
}
// Get the transition type.
val geofenceTransition = geofencingEvent.geofenceTransition
// Test that the reported transition was of interest.
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT
) {
// Get the geofences that were triggered. A single event can trigger
// multiple geofences.
val triggeringGeofences = geofencingEvent.triggeringGeofences
storeGeofenceTransitionDetails(geofenceTransition,triggeringGeofences)
for (geofence in triggeringGeofences) Log.d(TAG, "Geofence ${geofence.requestId} triggered!")
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER)
Log.d(TAG, "User entered geofence!")
else {
Log.d(TAG, "User exited geofence!")
}
} else {
// Log the error.
Log.e(TAG, "Invalid transition")
}
}
private fun storeGeofenceTransitionDetails(
geofenceTransition: Int,
triggeredGeofences: List<Geofence>
) {
triggeredExitGeofenceIds.clear()
for (geofence in triggeredGeofences) {
triggedGeofenceIdsList.add(geofence.requestId)
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
triggeredExitGeofenceIds.add(geofence.requestId)
}
}
geoFencePref?.edit()?.putStringSet("geoFenceId", triggeredExitGeofenceIds)?.apply()
}
}
//Then in MainActivity , register sharedpreference to listen for changes.
class MapsActivity : AppCompatActivity(), OnMapReadyCallback,
SharedPreferences.OnSharedPreferenceChangeListener {
....
override fun onStart() {
super.onStart()
requestPermissionsIfNeeded()
geoFencePref = getSharedPreferences("TriggerdExitedId", Context.MODE_PRIVATE)
geoFencePref!!.registerOnSharedPreferenceChangeListener(this)
}
....
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
val triggeredExitFences: HashSet<String>
val triggeredGeofences = ArrayList<String>()
if (key != null) {
Log.d("onSharedChanged: ", key)
}
if (key.equals("geoFenceId")) {
triggeredExitFences = geoFencePref?.getStringSet("geoFenceId", null) as HashSet<String>
if(triggeredExitFences.isEmpty()) Log.d("onSharedChanged: ", "no exit fences triggered")
triggeredGeofences.addAll(triggeredExitFences)
for(fence in triggeredExitFences) Log.d("onSharedChanged: ", "ID: $fence triggered!")
//Here you can call removeGeoFencesFromClient() to unRegister geoFences and removeGeofencesFromMap() to remove marker.
// removeGeofencesFromClient(triggerdIdList);
// removeGeofencesFromMap(triggerdIdList);
}
}
}