Android App add geofences and receive intents in same service

2.8k views Asked by At

I'm trying to construct a geofencing app, but it seems to only register geofences when the main activity started, and the intent service stops receiving them when the app is closed. So, I moved the add geofence logic into the intent service (along with the intent handling code) and make sure that service gets started, but now the service doesn't get receive any intents at all!

Service definition

public class GeofenceTransitionsIntentService extends IntentService implements ConnectionCallbacks, OnConnectionFailedListener, ResultCallback<Status>

Everything in the service (google api client built and connected) is done in onCreate, with both the intent handlers and the geofence registration stuff onConnected registers geofences, etc. Basically, I've tried to implement heavily borrowed geofencing sample code (from the docs) in the same service intended to handle those intents.

All the main activity does is starts the service and draws things related to the geofence notifications received in the service.

If you need more info, just let me know.

edit

Ok so it seems like we need more information -- an outline of the service:

public class GeofenceTransitionsIntentService extends IntentService implements ConnectionCallbacks, OnConnectionFailedListener, ResultCallback<Status> {

    protected static final String TAG = "GeofenceTransitionsIS";

    protected GoogleApiClient mGoogleApiClient;
    protected ArrayList<Geofence> mGeofenceList;
    private boolean mGeofencesAdded;
    private PendingIntent mGeofencePendingIntent;
    private SharedPreferences mSharedPreferences;

    public GeofenceTransitionsIntentService() {
        super(TAG);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        buildGoogleApiClient();
        populateGeofenceList();
        mGoogleApiClient.connect();

    }

    ...

    @Override
    protected void onHandleIntent(Intent intent) {
        GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
        // handle the intent, send a notification
    }


    private void sendNotification(String notificationDetails) {
        // sends a notification
    }

    @Override
    public void onConnected(Bundle connectionHint)
    {
        LocationServices.GeofencingApi.addGeofences(
                mGoogleApiClient,
                getGeofencingRequest(),
                getGeofencePendingIntent()
        ).setResultCallback(this);
    }

    // straight out of the example
    private GeofencingRequest getGeofencingRequest()
    {
        ...
    }


    // from a branch of the example that reuses the pending intent
    private PendingIntent getGeofencePendingIntent()
    {
        if (mGeofencePendingIntent != null)
        {
            return mGeofencePendingIntent;
        }

        Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
        mGeofencePendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        return mGeofencePendingIntent;
    }

    public void populateGeofenceList() {
        for (thing place : listofplaces) {
            mGeofenceList.add(...)
        }
    }

    protected synchronized void buildGoogleApiClient() {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(LocationServices.API)
                .build();
    }

    public void onResult(Status status)
    {
        // were fences added? usually yes
    }
}

My research has been frustrating -- I see people claiming to be able to do something like this from a broadcast receiver (see first comment) but not from a service?

I have a pretty mangled manifest.xml from all the revisions I've been working through:

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

<application
    android:allowBackup="true"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">

    <activity
        android:name=".MainActivity"
        android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>

    <service android:name=".GeofenceTransitionsIntentService"
             android:exported="true"
             android:enabled="true">
        <intent-filter >
            <action android:name="com.aol.android.geofence.ACTION_RECEIVE_GEOFENCE"/>
        </intent-filter>
    </service>

    ...

</application>

Neither adding the intent-filter nor the android:exported="true" to the service definition helped at all.

1

There are 1 answers

3
Pablo Baxter On BEST ANSWER

First, don't use the IntentService for this. Its sole purpose is to get a single intent, run it in a background thread, then stops itself. What you are looking for is a Service, as this will stick around for a while (until the OS starts running low on resources).

Second, once you move your code to a Service, do the following:

public class GeofenceTransitionsService extends Service implements ConnectionCallbacks, OnConnectionFailedListener, ResultCallback<Status> {
    //Whatever you need to declare
    ....
    GeofencingRequest mRequest;

    //This is only called once per instance of a Service, so use this to instantiate class variables
    @Override
    public void onCreate() {
        super.onCreate();
        buildGoogleApiClient();
        mGoogleApiClient.connect();
    }

    //Every time you call context.startService(Intent intent) after the service is created, 
    //this function gets called with the intent you have given it. You can use this to modify or change the geofence api,
    //passing GeofencingRequests in intents by calling intent.putExtra(...) before sending the intent, and retrieving it here.
    //I just assume you are passing GeofencingRequest objects, since they are pacelable.
    @Override
    public int onStartCommand(Intent intent, int flags, final int startId) {
        mRequest = intent.getParcelableExtra("request"); //Or whatever the key is for your request.
        if(mGoogleApiClient.isConnected()){
            LocationServices.GeofencingApi.addGeofences(
                mGoogleApiClient,
                mRequest,
                getGeofencePendingIntent()
            ).setResultCallback(this);
        }
    }

    @Override
    public void onConnected(Bundle connectionHint)
    {
        LocationServices.GeofencingApi.addGeofences(
            mGoogleApiClient,
            mRequest,
            getGeofencePendingIntent()
        ).setResultCallback(this);
    }

    // from a branch of the example that reuses the pending intent
    private PendingIntent getGeofencePendingIntent()
    {
        if (mGeofencePendingIntent != null)
        {
            return mGeofencePendingIntent;
        }

        mGeofencePendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(this, GoogleGeofenceReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT);
        return mGeofencePendingIntent;
    }

    //The rest of your code
    ....
}

Keep in mind that Android will kill your service when it runs low on resources, without much if any warning. I strongly suggest you look into starting in the foreground if you need this service to run at a higher priority.

Third, now that we have the service setup, you may have noticed the getGeofencePendingIntent() function now uses a BroadcastReceiver instead of the service it is running in. Here is how you set that up:

public class GoogleGeofenceReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(final Context context, final Intent intent) {
        GeofencingEvent event = GeofencingEvent.fromIntent(intent);
        ...
        //Do whatever you did in your Service handleIntent function here.
    }
}

Fourth, you'll need to modify your Manifest to let the app know that this BroadcastReceiver should be used:

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

<application
    android:allowBackup="true"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">

    <activity
        android:name=".MainActivity"
        android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>

    <service android:name=".GeofenceTransitionsService"
             android:exported="true"
             android:enabled="true">
        <intent-filter >
            <action android:name="com.aol.android.geofence.ACTION_RECEIVE_GEOFENCE"/>
        </intent-filter>
    </service>
    <receiver android:name=".GoogleGeofenceReceiver"/>

    ...

</application>

I'm not sure why you are using the export and enabled flags, but they don't need to be declared, because enabled is set by default, and exported is defaulted "true" if you have an intent-filter.

I would suggest you read up on Activity, Service, and BroadcastReceiver life cycles, as understanding that would greatly benefit you with this project and give you a better understanding of the pain of Android in general.