Geofence failed to be added when location is set to "allow only while in use"

Viewed 130

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>
1 Answers

The Android geofencing documentation clearly says:

To use geofencing, your app must request the following:

  • ACCESS_FINE_LOCATION

  • ACCESS_BACKGROUND_LOCATION if your app targets Android 10 (API level 29) or higher

That second permission is the "Allow all the time" permission.

Related