Friday, May 30, 2014

Introduction to Geofencing with a Service

   ** LocationClient was deprecated in a later release of Play Services from this post. The geofencing code should still apply, just switch to using the new 7.5 version of location **

   Geofencing is a feature in Google Play Services that allows for checking when a user has entered or left a circular area. This feature is implemented using Google's Location Services, so it relies on location data from cellular towers, wireless networks and GPS. While this can be a powerful tool, it should be used conservatively as continuously polling for a users location can be taxing on the device battery. It should also be noted that geofencing can take some time to register if the area has been entered or left, so you should plan accordingly when designing any apps that use this feature. For this post I have decided to put together an app that creates a geofence around the users starting location and post a notification when the user enters or leaves the fence. All code for this demo can be found on GitHub.

    The first thing that should be done is to update the gradle.build file to include Google Play Services.

compile 'com.google.android.gms:play-services:4.+'

    Next the manifest should be edited to allow for the ACCESS_FINE_LOCATION permission in order to use the device's GPS functionality. Meta-data for the Google Play Services version should be included as a tag, and the service that will be used for handling what action to take on a geofence event should be declared. For this demo I also force the app to be in portrait mode in order to avoid the broiler plate code required for reconnecting to Play Services on rotate.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ptrprograms.geofencing" >

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

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Activity.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".Service.GeofencingService" />

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />

    </application>

</manifest>


    Once the manifest file is set, then the main activity for the application should be set to implement GooglePlayServices connection callbacks and LocationClient Geofencing result and removal listeners. These are generally used for doing specific actions when PlayServices has connected or disconnected, as well as handling geofencing events.

public class MainActivity extends Activity implements 
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationClient.OnAddGeofencesResultListener,
        LocationClient.OnRemoveGeofencesResultListener {

     For now we'll leave the required methods as stubs and get back to them once everything else is ready. The first thing we should do in our activity, after calling setContentView, is verify that the device has Google Play Services available. If not, we can take action to alert the user to download it from the play store, or in this case simply end the activity.

private void verifyPlayServices() {
    switch ( GooglePlayServicesUtil.isGooglePlayServicesAvailable( this ) ) {
        case ConnectionResult.SUCCESS: {
            break;
        }
        case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: {
            finish();
        }
        default: {
            finish();
        }
    }
}

    Once Play Services have been confirmed, we can create a reference to the device LocationClient, and create a PendingIntent to start our service (GeofencingService.class) that handles performing an action when the user interacts with a geofence.

mLocationClient = new LocationClient( this, this, this );
mIntent = new Intent( this, GeofencingService.class );
mPendingIntent = PendingIntent.getService( this, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT );

    After this point you can start the geofence operations whenever appropriate for your application. In this case the operations are handled by a simple ToggleButton. In order to start listening for geofence events, you need to use the Geofence.Builder class to create a fence at a given lat/lng pair with a radius (in meters), specify if the fence should listen for entering, exiting or both interactions, and set an expiration time on it. For this example I am using the user's current location, a radius of 100 meters, listening for both entering and exiting events, and never letting the geofence listeners expire since I am controlling them myself. Once the geofence is built, it is placed in an ArrayList of Geofence objects and added to the location client with our pending intent so that when the fence is triggered, the PendingIntent is fired off.

private void startGeofence() {
    Location location = mLocationClient.getLastLocation();

    Geofence.Builder builder = new Geofence.Builder();
    mGeofence = builder.setRequestId( FENCE_ID )
            .setCircularRegion( location.getLatitude(), location.getLongitude(), RADIUS )
            .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT )
            .setExpirationDuration( Geofence.NEVER_EXPIRE )
            .build();

    ArrayList<Geofence> geofences = new ArrayList<Geofence>();
    geofences.add( mGeofence );
    mLocationClient.addGeofences( geofences, mPendingIntent, this );
}

    At this point we need to populate the LocationClient.OnAddGeofencesResultListener onAddGeofencesResult method. This method only has one responsibility: initiate the IntentService that will handle geofence events.

public void onAddGeofencesResult(int status, String[] geofenceIds ) {
    if( status == LocationStatusCodes.SUCCESS ) {
        Intent intent = new Intent( mIntent );
        startService( intent );
    }
}

    Once the service is created and the PendingIntent is associated with LocationClient, our service should receive intents every time the geofence is entered or exited, even if the app itself is in the background. The only thing we're doing with the service in this case is posting a notification for the user to inform them that they have entered or exited our specified area by checking the LocationClient.getGeofenceTransition( intent ) method. An introduction to notifications can be found in one of my earlier posts.

Entering the geofenced area
@Override
protected void onHandleIntent( Intent intent ) {
    NotificationCompat.Builder builder = new NotificationCompat.Builder( this );
    builder.setSmallIcon( R.drawable.ic_launcher );
    builder.setDefaults( Notification.DEFAULT_ALL );
    builder.setOngoing( true );

    int transitionType = LocationClient.getGeofenceTransition( intent );
    if( transitionType == Geofence.GEOFENCE_TRANSITION_ENTER ) {
        builder.setContentTitle( "Geofence Transition" );
        builder.setContentText( "Entering Geofence" );
        mNotificationManager.notify( 1, builder.build() );
    }
    else if( transitionType == Geofence.GEOFENCE_TRANSITION_EXIT ) {
        builder.setContentTitle( "Geofence Transition" );
        builder.setContentText( "Exiting Geofence" );
        mNotificationManager.notify( 1, builder.build() );
    }
}

Exiting the geofenced area 
    In order to remove the fences, we simply call removeGeofences with the PendingIntent and context passed in.

private void stopGeofence() {
    mLocationClient.removeGeofences( mPendingIntent, this );
}

    One important thing to notice here is that while the fence is removed, we also need to kill the background service that is waiting for geofence events. This is where the LocationClient.OnRemoveGeofenceListener interface comes into play. Since we are removing the geofences by passing in our intent, we need to populate the onRemoveGeofencesByPendingIntentResult method.

@Override
public void onRemoveGeofencesByPendingIntentResult(int status, PendingIntent pendingIntent) {
    if( status == LocationStatusCodes.SUCCESS ) {
        stopService( mIntent );
    }
}

    And with that, we have everything we need to create and use geofences within an Android application. There's a lot that can be done with this feature, such as added a "warmer/colder" feature to a scavenger hunt, keeping track of when a user enters a store, and monitoring when a user is near a landmark to name a few. I hope this tutorial helps others create something awesome, and good luck!

4 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
  2. Sorry.

    LocationClient is deprecated in GooglePlayServices 7.5.0

    ReplyDelete
    Replies
    1. Yeah, if I kept up with every deprecation, I'd have no time to add new content.

      Delete
    2. Yeah, if I kept up with every deprecation, I'd have no time to add new content.

      Delete