androidlocationandroid-8.0-oreogeofencingandroid-geofence

Do background geofences really work on Android 8+?


I have a geofencing app that I am trying to port to get working on Android 8+. I've read the tutorial on the subject, and switched to using compile 'com.google.android.gms:play-services-location:16.0.0'.

The geofence enter events never trigger when the app is not in the foreground. I wait more than the two minutes the docs say it might take. I've waited up to 15 minutes with no results. As soon as I bring the app to the foreground inside the geofence, it immediately triggers.

I understand that on Android 8+ that there are restrictions on background Services, but Google's tutorial says to use an IntentService which is supposed to be blocked on Android 8+. HINDSIGHT EDIT: The aforementioned tutorial is absolutely wrong. Do not follow it. An IntentService will not work.

I tried following this answer, which said to use the new GeofencingClient to set up your intents to solve this problem. (I don't understand why that would help, because it still uses an IntentService to receive the events, and it does not work in my code.)

This answer to a related question here suggests you must use a BroadcastReceiver instead of an IntentService, but I don't see why that would make a difference either since Android 8 imposes the same restrictions on BroadcastReceivers in the background as IntentServices. HINDSIGHT EDIT: This linked answer is correct as well. While implicit BroadcastReceivers are restricted, explicit ones that are registered at runtime to send requests to a specific package are not restricted and work properly.

Is it really possible to get geofencing callbacks that wake up your app in the background on Android 8+? If so, how can you make this work?

My geofencing setup:

googleApiClient = new GoogleApiClient.Builder(this.context)
        .addApi(LocationServices.API)
        .addConnectionCallbacks(getServiceSetup())
        .addOnConnectionFailedListener(getServiceFailureStrategy())
        .build();
geofencingClient = LocationServices.getGeofencingClient(context);

Intent intent =  new Intent(context, mIntentService);
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
// calling addGeofences() and removeGeofences().
geofencePendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.
            FLAG_UPDATE_CURRENT);

geofencingClient.addGeofences(geofences, geofencePendingIntent)
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                 @Override
                   public void onSuccess(Void aVoid) {
                     getAddGeofencesCallback(runnable, geofencesToAdd).onResult(new Status(0));
                     Log.e(TAG, "Successfully added geofences with GeofencingClient");
                   }
                 })
                .addOnFailureListener(new OnFailureListener() {
                   @Override
                   public void onFailure(Exception e) {
                     getAddGeofencesCallback(runnable, geofencesToAdd).onResult(new Status(1));
                     Log.e(TAG, "Failed to add geofences with GeofencingClient", e);

IntentService:

public class GeofenceService extends IntentService {
    private static final String TAG = "GeofenceService";

    public GeofenceService() {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        GeofenceTransition transition = GeofenceServiceManager.getGeofenceTransition(intent);
        if (transition == null) {
            return;
        }

        GeofenceServiceManager.logd(
                TAG,
                "onHandleIntent. geofence: " +
                        GeofenceMessage.notificationTitleFor(this, transition)
        );
        processErrors(transition);
        sendBroadcast(transition);
    }

    private Intent generateBroadcast(GeofenceTransition transition) {
        Intent broadcastIntent = new Intent();

        broadcastIntent.addCategory(GeofenceUtils.CATEGORY_GEOFENCE_SERVICES)
                       .setAction(GeofenceUtils.actionNameFor(transition))
                       .putStringArrayListExtra(
                               GeofenceUtils.GEOFENCE_IDS,
                               new ArrayList<String>(transition.getIds())
                                               );

        return broadcastIntent;
    }

    private void sendBroadcast(GeofenceTransition transition) {
        if (transition.isError() || transition.unknownType()) {
            GeofenceServiceManager.logd(TAG, "geofence transition is error or unknown type.");
            return;
        }

        broadcastManager().sendBroadcast(generateBroadcast(transition));
    }

    private LocalBroadcastManager broadcastManager() {
        return LocalBroadcastManager.getInstance(this);
    }

    private void processErrors(GeofenceTransition transition) {
        if (!transition.isError()) {
            return;
        }

        Log.e(TAG, "geofence error: "+GeofenceMessage.errorDetailsFor(this, transition));

        Intent broadcastIntent = generateBroadcast(transition);
        broadcastIntent.putExtra(
                GeofenceUtils.GEOFENCE_STATUS,
                GeofenceMessage.errorMessageFor(this, transition)
                                );
        broadcastManager().sendBroadcast(broadcastIntent);
    }
}

AndroidManifest.xml:

...
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

      <service
          android:name=".geofence.GeofenceService"
          android:enabled="true"
          android:exported="false">
      </service>
...

EDIT 1: Based on Google's documented limits on Android 8+ background processing, It sounds to me like it is impossible. Android 8+ kills running apps within 10 minutes of entering the background, and blocks starting services, including intent services, when not in the foreground. Yet Google says:

Note: On Android 8.0 (API level 26) and higher, if an app is running in the background while monitoring a geofence, then the device responds to geofencing events every couple of minutes. To learn how to adapt your app to these response limits, see Background Location Limits.

How is this possible given the documented background execution limits?

EDIT 2: I have seen a BroadcastReceiver declared in the manifest work to launch an app into the background on Android 8+ using an intent-delivered Bluetooth LE detection. That code looks like this:

Intent intent = new Intent();
intent.setComponent(new ComponentName(context.getPackageName(), "com.example.MyBroadcastReceiver"));
<receiver android:name="com.example.MyBroadcastReceiver">
         </receiver>

This does work for BLE detections to launch apps into the background. Are system-originated intents like above somehow immune from Android 8+ background execution limits (in ways that IntentServices are not) if delivered to a BroadcastReceiver? If so, will this work with Geofences? (Spoiler alert: Yes it will! Read the accepted answer below.)


Solution

  • You need to replace:

    geofencePendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.
                FLAG_UPDATE_CURRENT); 
    

    with

    Intent intent = new Intent(this, GeofenceBroadcastReceiver.class); 
    mGeofencePendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    

    For support Android 8, you should to use BroadcastReceiver for receiving geofence intent, due to Background Execution Limits (service can't be run in background)

    see for more details: https://github.com/android/location-samples/commit/5f83047c8a462d7c619f6275b624e219b4622322

    google sample how to use geofence: https://github.com/googlesamples/android-play-location/tree/master/Geofencing