We have implemented a Geofencing system where our CMS backend operators can send geofence data to the app through FCM. Inside the FirebaseService class that you see below, every time such a notification is sent it triggers the handleSilentNotification method which then attempts to create a Geofence object and add it to the Geofencing client.
We are requesting location access during the onboarding process and if the location permission was not granted, we just ignore the silent notification payload. When the user selects the "Allow all the time" location permission option, everything seems to work fine. Payloads are received, and geofences are created and added to the client for tracking. The issue occurs when the user selects "Allow only while in use".
In this case, when we attempt to add the geofence to the GeofencingClient, it always fails by throwing the following stack trace:
2022-05-05 11:27:48.107 13685-13685/com.threenitas.ewayprototype I/EwayApp: New location received: 37.95690471,23.72790214
2022-05-05 11:27:55.580 13685-14907/com.threenitas.ewayprototype D/MyFirebaseMessagingService: body = "{\"Id\":\"1234654\",\"Latitude\":37.3495,\"Longitude\":23.4646,\"Radius\":100,\"StatusCode\":\"EnableEvent\",\"Title\":\"test\",\"Description\":\"test\"}"
2022-05-05 11:27:55.593 13685-14907/com.threenitas.ewayprototype D/MyFirebaseMessagingService: New Geofence created: Geofence[CIRCLE id:1234654 transitions:3 37.349500, 23.464600 9999999933815813000000000000000000000m, resp=0s, dwell=-1ms, @-1]
2022-05-05 11:27:55.616 13685-13685/com.threenitas.ewayprototype D/GeofencingManager: Failed to add geofence with id 1234654 to client!
2022-05-05 11:27:55.616 13685-13685/com.threenitas.ewayprototype W/System.err: com.google.android.gms.common.api.ApiException: 1004:
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.common.api.internal.TaskUtil.setResultOrApiException(com.google.android.gms:play-services-base@@18.0.1:4)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.location.zzat.setResult(com.google.android.gms:play-services-location@@18.0.0:2)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.internal.location.zzaw.zzb(com.google.android.gms:play-services-location@@18.0.0:3)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.internal.location.zzaj.zza(com.google.android.gms:play-services-location@@18.0.0:9)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at com.google.android.gms.internal.location.zzb.onTransact(com.google.android.gms:play-services-location@@18.0.0:3)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at android.os.Binder.execTransactInternal(Binder.java:1195)
2022-05-05 11:27:55.617 13685-13685/com.threenitas.ewayprototype W/System.err: at android.os.Binder.execTransact(Binder.java:1159)
2022-05-05 11:27:58.087 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 6.101ms
2022-05-05 11:28:06.071 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 5.875ms
2022-05-05 11:28:06.089 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 8.474ms
2022-05-05 11:28:06.106 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 6.250ms
2022-05-05 11:28:06.136 13685-14822/com.threenitas.ewayprototype W/s.ewayprototyp: Suspending all threads took: 6.388ms
Is this a design decision on google's end like this bug tracker post seems to claim or am I missing something in my implementation?
App.kt:
class App : Application() {
lateinit var appContext: Context
lateinit var preferences: SharedPreferences
lateinit var editor: SharedPreferences.Editor
lateinit var geofencingClient: GeofencingClient
val inactiveGeofenceIds = arrayListOf<String>()
val geofenceEventInfo = mutableMapOf<String, GeoEventData>()
val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(appContext, GeofenceBroadcastReceiver::class.java)
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
// addGeofences() and removeGeofences().
PendingIntent.getBroadcast(appContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
val locationListener: LocationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
Log.i("EwayApp", "New location received: ${location.latitude},${location.longitude}")
}
override fun onLocationChanged(locations: MutableList<Location>) {}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
fun isGeofencingClientInitialized() = this::geofencingClient.isInitialized
...........
}
FirebaseMessagingService:
class MyFirebaseMessagingService : FirebaseMessagingService() {
private val TAG = MyFirebaseMessagingService::class.java.simpleName
private var geofencingManager: GeofencingManager = getKoin().get()
override fun onNewToken(token: String) {
super.onNewToken(token)
NotificationHub.getInstance(App.instance)
.registerForPushNotifications(this, fetchIntegrationIdForLetsPlace())
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// Check if message contains a data payload
remoteMessage.data.isNotEmpty().let {
handleNotification(remoteMessage.data)
}
}
private fun handleNotification(jsonData: MutableMap<String, String>) {
val title = jsonData["Title"]
val description = jsonData["Description"]
val body = jsonData["Body"]
if (title!!.isEmpty()) {
if (hasLocationPermission()) handleSilentNotification(body)
else Log.d(TAG, "No location permission has been granted!")
} else {
showNotification(
title,
description!!
)
}
}
private fun hasLocationPermission() = ContextCompat.checkSelfPermission(
App.instance.appContext,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
private fun showNotification(
title: String,
description: String
) {
// val intentForNotification = Intent(application, SplashScreen::class.java)
// intentForNotification.putExtra("notificationInboxBody", inboxBody)
// intentForNotification.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val hub = NotificationHub.getInstance(App.instance)
val id = hub.lastRandomId ?: 0
hub.lastRandomId = id + 1
//val pendingIntent = PendingIntent.getActivity(application, hub.lastRandomId!! /* Request code */, intentForNotification, PendingIntent.FLAG_UPDATE_CURRENT)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(application, "channel1")
.setSmallIcon(R.drawable.splash_screen_logo)
.setContentTitle(title)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setPriority(Notification.PRIORITY_HIGH)
.setCategory(Notification.CATEGORY_MESSAGE)
.setDefaults(Notification.DEFAULT_ALL)
//.setContentIntent(pendingIntent)
.setContentText(description)
with(NotificationManagerCompat.from(this)) {
// notificationId is a unique int for each notification that you must define
notify(id, notificationBuilder.build())
}
}
private fun handleSilentNotification(body: String?) {
Log.d(TAG, "body = $body")
val geoEventData = jsonToGeoEvent(body)
if (geoEventData == null) {
Log.d(TAG, "Geo event data conversion failed!")
return;
}
if (!App.instance.isGeofencingClientInitialized()) { // app is killed , store the payload for later use
Log.d(TAG, "persist called")
persistGeoData(geoEventData)
return
}
if (geoEventData.statusCode == GeoEventStatus.DisableEvent.name) {
geofencingManager.removeGeofence(geoEventData.id)
return
}
val geofence = geofencingManager.createGeofence(geoEventData)
geofencingManager.storeGeofenceInfo(geofence, geoEventData)
Log.d(TAG, "New Geofence created: $geofence")
geofencingManager.addGeofenceToClient(geofence)
}
private fun persistGeoData(geoEventData: GeoEventData) {
val gson = Gson()
val serializedData = gson.toJson(geoEventData, GeoEventData::class.java)
App.instance.inactiveGeofenceIds.add(geoEventData.id)
App.instance.preferences.edit().putString(geoEventData.id, serializedData).apply()
}
private fun jsonToGeoEvent(body: String?): GeoEventData? {
if (body == null) return null
val gson = Gson()
val json = body.replace("\\", "")
val jo = JSONObject(json.substring(1, json.length - 1))
return gson.fromJson(jo.toString(), GeoEventData::class.java)
}
}
GeofencingManagerImpl:
class GeofencingManagerImpl : GeofencingManager {
private val tag: String
get() = GeofencingManager::class.java.simpleName
@SuppressLint("MissingPermission")
override fun addGeofenceToClient(geofence: Geofence) {
App.instance.geofencingClient.addGeofences(
getGeofencingRequest(geofence),
App.instance.geofencePendingIntent
).run {
addOnSuccessListener {
Log.d(tag, "Geofence with id ${geofence.requestId} added to client!")
}
addOnFailureListener {
Log.d(tag, "Failed to add geofence with id ${geofence.requestId} to client!")
it.printStackTrace()
}
}
}
override fun createGeofence(geoEventData: GeoEventData) =
Geofence.Builder()
.setRequestId(geoEventData.id)
.setCircularRegion(
geoEventData.latitude,
geoEventData.longitude,
geoEventData.radius
)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
override fun storeGeofenceInfo(
geofence: Geofence,
geoEventData: GeoEventData
) {
App.instance.geofenceEventInfo[geofence.requestId] = geoEventData
}
override fun getGeofencingRequest(geofence: Geofence): GeofencingRequest {
return GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
}
override fun removeGeofence(id: String) {
App.instance.geofenceEventInfo.remove(id)
App.instance.geofencingClient.removeGeofences(listOf(id)).run {
addOnSuccessListener {
Log.d(tag, "Geofence with id $id removed from client!")
}
addOnFailureListener {
Log.d(tag, "Failed to remove geofence with id $id from client!")
it.printStackTrace()
}
}
}
}
BroadcastReceiver:
class GeofenceBroadcastReceiver : BroadcastReceiver() {
private val TAG = GeofenceBroadcastReceiver::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, errorMessage)
return
}
val geofenceTransition = geofencingEvent.geofenceTransition
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT
) {
val triggeringGeofences = geofencingEvent.triggeringGeofences
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
// user entered or is already inside a geofence
for (geofence in triggeringGeofences) {
val eventData = App.instance.geofenceEventInfo[geofence.requestId] ?: continue
if (eventData.statusCode == GeoEventStatus.EnableEvent.name) showNotification(
eventData.title, eventData.description
)
}
Log.d(TAG, "User entered geofence!")
} else {
Log.d(TAG, "User exited geofence!")
}
} else {
Log.e(
TAG, "Invalid transition type"
)
}
}
private fun showNotification(title: String, description: String) {
// val intentForNotification = Intent(application, SplashScreen::class.java)
// intentForNotification.putExtra("notificationInboxBody", inboxBody)
// intentForNotification.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val hub = NotificationHub.getInstance(App.instance)
val id = hub.lastRandomId ?: 0
hub.lastRandomId = id + 1
//val pendingIntent = PendingIntent.getActivity(application, hub.lastRandomId!! /* Request code */, intentForNotification, PendingIntent.FLAG_UPDATE_CURRENT)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(App.instance.appContext, "channel1")
.setSmallIcon(R.drawable.splash_screen_logo)
.setContentTitle(title)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setPriority(Notification.PRIORITY_HIGH)
.setCategory(Notification.CATEGORY_MESSAGE)
.setDefaults(Notification.DEFAULT_ALL)
//.setContentIntent(pendingIntent)
.setContentText(description)
with(NotificationManagerCompat.from(App.instance.appContext)) {
// notificationId is a unique int for each notification that you must define
notify(id, notificationBuilder.build())
}
}
}
Manifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.threenitas.ewayprototype">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:name=".App"
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/Theme.EWayPrototype"
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
<receiver android:name=".geofencing.GeofenceBroadcastReceiver"/>
<service android:foregroundServiceType="location" android:enabled="true" android:name=".geofencing.GeofenceForegroundService"/>
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
</application>
</manifest>