Geofencing in background on Android 8 or 9 does not work
Asked Answered
G

5

13

I try to make appear a push alert to the user when he reach a defined zone.

So I coded my app from : https://developer.android.com/training/location/geofencing

It is working perfectly if my app is running with a service following the location of the user.

It also work if I start google map for example, that will track my location too. Pushes will appear.

But if I close my app the push won't appear, so the geofencing is not detected if no app is tracking my location.

Is it normal ? How to make it work always ? What is the point of geofencing if you need a foreground service following your location ?

 public void createGeofenceAlerts(LatLng latLng, int radius) {
    final Geofence enter = buildGeofence(ID_ENTER, latLng, radius, Geofence.GEOFENCE_TRANSITION_ENTER);
    final Geofence exit = buildGeofence(ID_EXIT, latLng, radius, Geofence.GEOFENCE_TRANSITION_EXIT);
    final Geofence dwell = buildGeofence(ID_DWELL, latLng, radius, Geofence.GEOFENCE_TRANSITION_DWELL);

    GeofencingRequest request = new GeofencingRequest.Builder()
            .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
            .addGeofence(enter)
            .addGeofence(exit)
            .addGeofence(dwell)
            .build();

    fencingClient.addGeofences(request, getGeofencePendingIntent()).addOnSuccessListener(new OnSuccessListener<Void>() {
        @Override
        public void onSuccess(Void aVoid) {
            Timber.i("succes");
            Toast.makeText(mContext, "Geofence added", Toast.LENGTH_LONG).show();
        }
    }).addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
            Timber.e(e,"failure");
            Toast.makeText(mContext, "Geofence ERROR", Toast.LENGTH_LONG).show();
        }
    });
}

private PendingIntent getGeofencePendingIntent() {
    Intent intent = new Intent(mContext, GeofenceTransitionsIntentService.class);
    PendingIntent pending = PendingIntent.getService(
            mContext,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT);
    return pending;
}

private Geofence buildGeofence(String id, LatLng center, int radius, int transitionType) {
    Geofence.Builder builder = new Geofence.Builder()
            // 1
            .setRequestId(id)
            // 2
            .setCircularRegion(
                    center.getLatitude(),
                    center.getLongitude(),
                    radius)
            // 3
            .setTransitionTypes(transitionType)
            // 4
            .setExpirationDuration(Geofence.NEVER_EXPIRE);
    if (transitionType == Geofence.GEOFENCE_TRANSITION_DWELL) {
        builder.setLoiteringDelay(LOITERING_DELAY);
    }

    return builder.build();
}
Galleywest answered 24/1, 2019 at 13:44 Comment(0)
G
8

I think I found a solution, tested on Android 9. I used the Google documentation https://developer.android.com/training/location/geofencing but I replaced the service by a broadcast receiver.

My GeofenceManager :

private val braodcastPendingIntent: PendingIntent
    get() {
        val intent = Intent(mContext, GeofenceTransitionsBroadcastReceiver::class.java)
        val pending = PendingIntent.getBroadcast(
                mContext.applicationContext,
                0,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT)
        return pending
    }

 fun createGeofenceAlerts(latLng: LatLng, radiusMeter: Int, isBroadcast: Boolean) {
    val enter = buildGeofence(ID_ENTER, latLng, radiusMeter, Geofence.GEOFENCE_TRANSITION_ENTER)
    val exit = buildGeofence(ID_EXIT, latLng, radiusMeter, Geofence.GEOFENCE_TRANSITION_EXIT)
    val dwell = buildGeofence(ID_DWELL, latLng, radiusMeter, Geofence.GEOFENCE_TRANSITION_DWELL)

    val request = GeofencingRequest.Builder()
            .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
            .addGeofence(enter)
            .addGeofence(exit)
            .addGeofence(dwell)
            .build()

    val pending = if (isBroadcast) {
        braodcastPendingIntent
    } else {
        servicePendingIntent
    }
    fencingClient.addGeofences(request, pending).addOnSuccessListener {
        Timber.i("succes")
        Toast.makeText(mContext, "Geofence added", Toast.LENGTH_LONG).show()
    }.addOnFailureListener { e ->
        Timber.e(e, "failure")
        Toast.makeText(mContext, "Geofence ERROR", Toast.LENGTH_LONG).show()
    }
}

private fun buildGeofence(id: String, center: LatLng, radius: Int, transitionType: Int): Geofence {
    val builder = Geofence.Builder()
            // 1
            .setRequestId(id)
            // 2
            .setCircularRegion(
                    center.latitude,
                    center.longitude,
                    radius.toFloat())
            // 3
            .setTransitionTypes(transitionType)
            // 4
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
    if (transitionType == Geofence.GEOFENCE_TRANSITION_DWELL) {
        builder.setLoiteringDelay(LOITERING_DELAY)
    }

    return builder.build()
}

My BroadcastReceiver, obviously you need to declare it in the manfifest :

class GeofenceTransitionsBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
    Timber.i("received")
    val geofencingEvent = GeofencingEvent.fromIntent(intent)
    if (geofencingEvent.hasError()) {
        Timber.e("Geofence error")
        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
            || geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) {

        // Get the geofences that were triggered. A single event can trigger
        // multiple geofences.
        val triggeringGeofences = geofencingEvent.triggeringGeofences

        // Get the transition details as a String.
        val geofenceTransitionDetails = GeofenceManager.getGeofenceTransitionDetails(
                geofenceTransition,
                triggeringGeofences, true
        )

        // Send notification and log the transition details.
        GeofenceManager.sendNotification(context, geofenceTransition, geofenceTransitionDetails)
        Timber.i(geofenceTransitionDetails)
    } else {
        // Log the error.
        Timber.e("Unknown geo event : %d", geofenceTransition)
    }
}

The important part is to know that on Android 8 and 9 the geofencing has a latency of 2 minutes.

Galleywest answered 30/1, 2019 at 8:58 Comment(9)
Is there a means of reducing those 2 minutes?Westberg
Yes it depends on your business, if you are actively tracking the position with a service you might have a faster reaction.Galleywest
@Galleywest Why you replaced service with Broadcast Receiver? Any specific reason behind that?Taiga
@AmbarJain Due to new restrictions on Oreo we can’t start background services, so we’re required to use Broadcast receiverZackaryzacks
@Galleywest You stated that you replaced the service with a broadcast receiver but later in the code you have: ` val pending = if (isBroadcast) { braodcastPendingIntent } else { servicePendingIntent } ` Can you please post the servicePendingIntent code? Also, there's a isBroadcast boolean, so, can you clarify who/how calls createGeofenceAlerts?Yolanda
Sorry my code has evolved and only use 'braodcastPendingIntent' nowGalleywest
@Galleywest Thanks. Does it work reliably for you? I found that sometimes the geofence doesn't trigger or trigger after 2 hours of being inside the geofence. I'm using a device with Android 8.0Yolanda
@Yolanda did you manage to find your problem ? why event will get trigger lateInshrine
Id rather calculate the displacement between two locations this is too tediousNeediness
U
13

I have been working with GeoFence for such a long time, I had the same question and I got the answer by myself after trying different solutions, So basically, GeoFence only get triggers if any app in the phone is fetching the location for some x duration. If you test the GeoFence sample app provided by google then you can see that the app works only when you open the Google maps application, its because Google Maps is the only app in the device that requests locations passively.

For Prove you can clone GeoFence sample and the LocationUpdateForGroundService sample from this below link https://github.com/googlesamples/android-play-location Run both of them GeoFence and LocationUpdateForGroundService at the same time, You will notice by changing the lat and long from the emulator that now you dont need to open Google maps any more because now there is another app which is requesting location.

So do create a foreground service in the GeoFence application and use Fuse Location Client to request location updates for some x duration.

Underneath answered 28/1, 2019 at 19:55 Comment(3)
can you provide some code of what you are trying to prove!Sainted
The second paragraph I wrote for prove. Please follow this.Underneath
Yes, it's exactly working well when google map application is opened. Otherwise, it doesn't work.Purloin
G
8

I think I found a solution, tested on Android 9. I used the Google documentation https://developer.android.com/training/location/geofencing but I replaced the service by a broadcast receiver.

My GeofenceManager :

private val braodcastPendingIntent: PendingIntent
    get() {
        val intent = Intent(mContext, GeofenceTransitionsBroadcastReceiver::class.java)
        val pending = PendingIntent.getBroadcast(
                mContext.applicationContext,
                0,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT)
        return pending
    }

 fun createGeofenceAlerts(latLng: LatLng, radiusMeter: Int, isBroadcast: Boolean) {
    val enter = buildGeofence(ID_ENTER, latLng, radiusMeter, Geofence.GEOFENCE_TRANSITION_ENTER)
    val exit = buildGeofence(ID_EXIT, latLng, radiusMeter, Geofence.GEOFENCE_TRANSITION_EXIT)
    val dwell = buildGeofence(ID_DWELL, latLng, radiusMeter, Geofence.GEOFENCE_TRANSITION_DWELL)

    val request = GeofencingRequest.Builder()
            .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
            .addGeofence(enter)
            .addGeofence(exit)
            .addGeofence(dwell)
            .build()

    val pending = if (isBroadcast) {
        braodcastPendingIntent
    } else {
        servicePendingIntent
    }
    fencingClient.addGeofences(request, pending).addOnSuccessListener {
        Timber.i("succes")
        Toast.makeText(mContext, "Geofence added", Toast.LENGTH_LONG).show()
    }.addOnFailureListener { e ->
        Timber.e(e, "failure")
        Toast.makeText(mContext, "Geofence ERROR", Toast.LENGTH_LONG).show()
    }
}

private fun buildGeofence(id: String, center: LatLng, radius: Int, transitionType: Int): Geofence {
    val builder = Geofence.Builder()
            // 1
            .setRequestId(id)
            // 2
            .setCircularRegion(
                    center.latitude,
                    center.longitude,
                    radius.toFloat())
            // 3
            .setTransitionTypes(transitionType)
            // 4
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
    if (transitionType == Geofence.GEOFENCE_TRANSITION_DWELL) {
        builder.setLoiteringDelay(LOITERING_DELAY)
    }

    return builder.build()
}

My BroadcastReceiver, obviously you need to declare it in the manfifest :

class GeofenceTransitionsBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
    Timber.i("received")
    val geofencingEvent = GeofencingEvent.fromIntent(intent)
    if (geofencingEvent.hasError()) {
        Timber.e("Geofence error")
        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
            || geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) {

        // Get the geofences that were triggered. A single event can trigger
        // multiple geofences.
        val triggeringGeofences = geofencingEvent.triggeringGeofences

        // Get the transition details as a String.
        val geofenceTransitionDetails = GeofenceManager.getGeofenceTransitionDetails(
                geofenceTransition,
                triggeringGeofences, true
        )

        // Send notification and log the transition details.
        GeofenceManager.sendNotification(context, geofenceTransition, geofenceTransitionDetails)
        Timber.i(geofenceTransitionDetails)
    } else {
        // Log the error.
        Timber.e("Unknown geo event : %d", geofenceTransition)
    }
}

The important part is to know that on Android 8 and 9 the geofencing has a latency of 2 minutes.

Galleywest answered 30/1, 2019 at 8:58 Comment(9)
Is there a means of reducing those 2 minutes?Westberg
Yes it depends on your business, if you are actively tracking the position with a service you might have a faster reaction.Galleywest
@Galleywest Why you replaced service with Broadcast Receiver? Any specific reason behind that?Taiga
@AmbarJain Due to new restrictions on Oreo we can’t start background services, so we’re required to use Broadcast receiverZackaryzacks
@Galleywest You stated that you replaced the service with a broadcast receiver but later in the code you have: ` val pending = if (isBroadcast) { braodcastPendingIntent } else { servicePendingIntent } ` Can you please post the servicePendingIntent code? Also, there's a isBroadcast boolean, so, can you clarify who/how calls createGeofenceAlerts?Yolanda
Sorry my code has evolved and only use 'braodcastPendingIntent' nowGalleywest
@Galleywest Thanks. Does it work reliably for you? I found that sometimes the geofence doesn't trigger or trigger after 2 hours of being inside the geofence. I'm using a device with Android 8.0Yolanda
@Yolanda did you manage to find your problem ? why event will get trigger lateInshrine
Id rather calculate the displacement between two locations this is too tediousNeediness
E
3

I discovered that my geoFenceBroadCastReceiver started to work correctly in the emulator when I had google maps open. I simply could not figure out what the problem was, and as it turns out, I was missing a piece of the puzzle obviously. The behavior is also described in this question (aptly titled): Geofence Broadcast Receiver not triggered but when I open the google map, it works, as well as filed numerous times as an issue in the Android Location Samples project Issue 239, Issue 247 Issue 266.

I don't see the actual answer to this problem posted as an answer here, so for posterity I will provide some suggestions. Issue 264 seems to point to the solution, i.e. using a JobIntentService

BUT

There's a code comment in the GeoFence Sample LocationUpdateIntentService

Note: Apps running on "O" devices (regardless of targetSdkVersion) may receive updates less frequently than the interval specified in the {@link com.google.android.gms.location.LocationRequest} when the app is no longer in the foreground.

that seems to confirm what @Vinayak points out in his answer here

O.S will not allow app to get location update from background. Implement geofence location code on foreground service. It will run as expected

The geofence broadcast receiver isn't allowed to get timely location updates (apparently even in an IntentService). You'll have to run location client in a foreground service that's requesting fine location access to get more accurate/timely geoFence triggering. So seemingly the right answer is to run the geoFence in a foreGroundService.

If you go the foreGroundService route, you'll also need to create a Notification Channel, as otherwise, you'll run into problems like this one.

See Also:

https://developer.android.com/guide/components/foreground-services

https://developer.android.com/training/location/change-location-settings#location-request

https://developer.android.com/training/location/request-updates

https://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderClient.html#requestLocationUpdates(com.google.android.gms.location.LocationRequest,%20com.google.android.gms.location.LocationCallback)

Ecospecies answered 23/10, 2021 at 22:55 Comment(2)
Impressive summary on this topic! really had to write this comment to thank you. This is what I needed exactly on this topic.Nicoline
Another thank you, for the summary <3Runaway
H
1

O.S will not allow app to get location update from background. Implement geofence location code on foreground service. It will run as expected

Heshum answered 21/4, 2021 at 11:35 Comment(0)
T
1

I apply @Mudassir Zulfiqar solution for foreground service implementation. Geofence will not working properly on background unless another app does not use your location or you may trigger location by your own.

Here is my foreground service implementation:

class GeofenceForegroundService: Service() {

private lateinit var geofenceHelper: GeofenceHelper
private lateinit var geofencingClient: GeofencingClient
private lateinit var pendingIntent: PendingIntent
private lateinit var locationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback
private val geofenceId = "Some_Geofence_Id"
private val geofenceRadius = 200.0

private val TAG = "GeofenceForegroundServi"

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

@SuppressLint("MissingPermission")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    geofenceHelper = GeofenceHelper(this)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val channel =
            NotificationChannel("Geofence", "Geofence Loc", NotificationManager.IMPORTANCE_NONE)
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(channel)
    }

    val notification: Notification = NotificationCompat.Builder(this, "Geofence")
        .setContentTitle("Geofence Active")
        .setContentText("Location Updating...")
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentIntent(geofenceHelper.pendingIntent)
        .build()

    val latLng = intent?.getParcelableExtra<LatLng>("LATLNG")
    geofencingClient = LocationServices.getGeofencingClient(this)
    locationClient = LocationServices.getFusedLocationProviderClient(this)
    val geofence = geofenceHelper.getGeofence(geofenceId, latLng!!, geofenceRadius.toFloat())
    val geofencingRequest = geofenceHelper.getGeofencingRequest(geofence)
    pendingIntent = geofenceHelper.pendingIntent
    val locationRequest = LocationRequest.create().apply {
        interval = 10000
        fastestInterval = 5000
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY

    }
    locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)
            Log.d(TAG, "${locationResult.lastLocation.latitude} ${locationResult.lastLocation.longitude}")
        }
    }
    Looper.myLooper()?.let {
        locationClient.requestLocationUpdates(locationRequest, locationCallback,
            it
        )
    }

    geofencingClient.addGeofences(geofencingRequest, pendingIntent).run {
        addOnSuccessListener {
            Log.d(TAG, "onSuccess: Geofence Added...")

        }
        addOnFailureListener {
            Log.d(TAG, "onFailure ${geofenceHelper.getErrorString(it)}")
        }
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
    } else {
        startForeground(1, notification)
    }

    return START_STICKY
}

override fun onDestroy() {
    Log.i(TAG, "onDestroy: RUN")
    geofencingClient.removeGeofences(pendingIntent)
    locationClient.removeLocationUpdates(locationCallback)
    super.onDestroy()
}

override fun onTaskRemoved(rootIntent: Intent?) {
    Log.i(TAG, "onTaskRemoved: RUN")
    super.onTaskRemoved(rootIntent)
    stopSelf()
}
}

If you have any questions about above code. Please share on your comments.

Tegument answered 23/8, 2022 at 16:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.