6

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.)

davidgyoung
  • 63,876
  • 14
  • 121
  • 204

2 Answers2

5

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

phnmnn
  • 12,813
  • 11
  • 47
  • 64
  • Thanks for the suggestion. I am testing a switch to a BroadcastReceiver like you suggest. I see that the sample you reference does do this. Unfortunately, the Geofence Training page on [developer.google.com still says to use a PendingIntent to start an IntentService](https://developer.android.com/training/location/geofencing). – davidgyoung Mar 07 '19 at 20:07
  • This does work. The bottom line is that Google's training page that tells you to use a PendingIntent to a IntentService linked above is totally wrong. An explicit BroadcastReceiver registration does work to launch the app in the background. – davidgyoung Mar 07 '19 at 21:24
1

If you trust yourself for doing everything right by this article you can use ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS setting. This setting disables the application's battery optimization. So intents are not blocking by system. Because sometimes system block our jobs for better battery life. So if your app running with this setting you can blame the system.

One more thing. How did you test geofencing?

Mustafa Kuloğlu
  • 1,122
  • 1
  • 8
  • 16
  • Thanks for the tip about ignoring battery optimisations. I sure hope that is not necessary! I test by bringing the app to the foreground when over 1km away from my 100m geofence, then putting it to the background by hitting the home button, waiting 1+ hours, then going to the center of the geofence and waiting up to 15 minutes for my local notification which never fires even if I illuminate the screen. When I unlock the phone and bring the app to the foreground, it immediately fires. – davidgyoung Mar 04 '19 at 13:08
  • You can test with fake location app like Lockito. Thats are more stable than real location. You can get wrong results with real location. Because geofence triggering is not stable in real scenario. You can be sure with fake location. – Mustafa Kuloğlu Mar 04 '19 at 13:19
  • I tried Lockito, which reproduced the same problem on a Nokia 6.1+ with Android 9. I would not run at all on a Huawei P9 with Android 7. – davidgyoung Mar 07 '19 at 19:59