Geofences on Android with GoogleApiClient

In this tutorial you’ll learn how to leverage GoogleApiClient to add geofences to an Android app, as well as post notifications when a geofence is crossed. By Joe Howard.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Creating GeofenceController

You’ll use a singleton class named GeofenceController to interact with the GoogleApiClient.

Right-click on the app package name in app/src/main/java, select New\Java Class, name the class GeofenceController and click OK:

AWTY-NewJavaClass

Next, add the following code to the file you just created:

public class GeofenceController {

  private final String TAG = GeofenceController.class.getName();

  private Context context;
  private GoogleApiClient googleApiClient;
  private Gson gson;
  private SharedPreferences prefs;

  private List<NamedGeofence> namedGeofences;
  public List<NamedGeofence> getNamedGeofences() {
    return namedGeofences;
  }

  private List<NamedGeofence> namedGeofencesToRemove;

  private Geofence geofenceToAdd;
  private NamedGeofence namedGeofenceToAdd;
}

Here, you’ve added a String TAG property, a Context property that you’ll need to connect with GoogleApiClient, a GoogleApiClient property, as well as Gson and SharedPreferences properties that you’ll use to serialize geofences to disk.

You’ve also added List properties to store the current geofences in memory and maintain a list of geofences to remove. Finally, there are properties that will store Geofence and NamedGeofence objects to be added to the list.

Be sure to add any required imports by clicking on the red highlighted types and pressing Option-Return for each one.

Next, add the following code just below the property declarations:

private static GeofenceController INSTANCE;

public static GeofenceController getInstance() {
  if (INSTANCE == null) {
    INSTANCE = new GeofenceController();
  }
  return INSTANCE;
}

This adds a private static property to hold the singleton reference to the GeofenceController class, as well as a method to create and access the instance.

Now add the following initializer to your class:

public void init(Context context) {
  this.context = context.getApplicationContext();

  gson = new Gson();
  namedGeofences = new ArrayList<>();
  namedGeofencesToRemove = new ArrayList<>();
  prefs = this.context.getSharedPreferences(Constants.SharedPrefs.Geofences, Context.MODE_PRIVATE);
}

Make sure to import the ArrayList class. This method simply initializes the context and some other properties of the controller.

Open AllGeofencesActivity and add the following call to the bottom of onCreate():

GeofenceController.getInstance().init(this);

This simply initializes GeofenceController when the app starts.

Now that you’ve added the controller, run your app to make sure all is well; your app won’t look any different, but rest assured GeofenceController is there in the background, waiting to do its job.

AWTY-FeedMe

Adding Geofences

When the user taps Add in AddGeofenceFragment to create a new fence, you’ll kick off a chain of calls that result in GeofenceController connecting to GoogleApiClient to add the geofence.

Both the Add and Cancel buttons in AddGeofenceFragment have existing OnClickListeners, so you simply need to add the listener calls.

Open AddGeofenceFragment and add the following to the end of onClick for the Cancel click listener:

if (listener != null) {
  listener.onDialogNegativeClick(AddGeofenceFragment.this);
}

This code calls the negative click callback on the listener if it exists.

Next, replace onClick() for the Add click listener with the following:

public void onClick(View view) {
  // 1. Check for valid data
  if (dataIsValid()) {
    // 2. Create a named geofence
    NamedGeofence geofence = new NamedGeofence();
    geofence.name = getViewHolder().nameEditText.getText().toString();
    geofence.latitude = Double.parseDouble(
            getViewHolder().latitudeEditText.getText().toString());
    geofence.longitude = Double.parseDouble(
            getViewHolder().longitudeEditText.getText().toString());
    geofence.radius = Float.parseFloat(
            getViewHolder().radiusEditText.getText().toString()) * 1000.0f;

    // 3. Call listener and dismiss or show error
    if (listener != null) {
      listener.onDialogPositiveClick(AddGeofenceFragment.this, geofence);
      dialog.dismiss();
    }
  } else {
    // 4. Display an error message
    showValidationErrorToast();
  }
}

You do the following things when the user taps the Add button:

  1. Check if the user entered valid data.
  2. If so, create a new NamedGeofence and set its properties.
  3. Call the listener and dismiss the dialog.
  4. If the user entered invalid data, show a validation error toast.

Connecting to GoogleApiClient

Add the following interface to the bottom of GeofenceController:

public interface GeofenceControllerListener {
  void onGeofencesUpdated();
  void onError();
}

You’ll call the first interface method when you add or remove geofences, and the second if an error occurs.

Add the following field to the other fields near the top of the class:

private GeofenceControllerListener listener;

In order to add geofences to the device, GeofenceController must connect to GoogleApiClient and implement its defined interfaces ConnectionCallbacks and OnConnectionFailedListener.

Add the following code after the listener you added above:

private GoogleApiClient.ConnectionCallbacks connectionAddListener = 
    new GoogleApiClient.ConnectionCallbacks() {
  @Override
  public void onConnected(Bundle bundle) {

  }

  @Override
  public void onConnectionSuspended(int i) {

  }
};

private GoogleApiClient.OnConnectionFailedListener connectionFailedListener = 
    new GoogleApiClient.OnConnectionFailedListener() {
  @Override
  public void onConnectionFailed(ConnectionResult connectionResult) {

  }
};

This declares the add geofence and connection failed callbacks. Make sure to import the required headers for Bundle and ConnectionResult.

Before implementing the callbacks, add the following two helper methods to the bottom of the class:

private GeofencingRequest getAddGeofencingRequest() {
  List<Geofence> geofencesToAdd = new ArrayList<>();
  geofencesToAdd.add(geofenceToAdd);
  GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
  builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
  builder.addGeofences(geofencesToAdd);
  return builder.build();
}

private void connectWithCallbacks(GoogleApiClient.ConnectionCallbacks callbacks) {
  googleApiClient = new GoogleApiClient.Builder(context)
    .addApi(LocationServices.API)
    .addConnectionCallbacks(callbacks)
    .addOnConnectionFailedListener(connectionFailedListener)
    .build();
  googleApiClient.connect();
}

getAddGeofencingRequest() adds the geofenceToAdd object to an ArrayList, then uses the builder pattern to create a GeofencingRequest object that will be used in the connection callbacks. connectWithCallbacks() populates the googleApiClient property and uses it to connect to the location service.

Make sure to import both GeofencingRequest and LocationServices.

Now add the following two helper methods:

private void sendError() {
  if (listener != null) {
    listener.onError();
  }
}

private void saveGeofence() {
  namedGeofences.add(namedGeofenceToAdd);
  if (listener != null) {
    listener.onGeofencesUpdated();
  }
}

sendError() calls the listener to pass along the error. saveGeofence() adds the new geofence to the controller’s list of geofences and calls the listener’s onGeofencesUpdated method.

With these two helper methods in place, add the following code to onConnected() within connectionAddListener:

// 1. Create an IntentService PendingIntent
Intent intent = new Intent(context, AreWeThereIntentService.class);
PendingIntent pendingIntent =
    PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

// 2. Associate the service PendingIntent with the geofence and call addGeofences
PendingResult<Status> result = LocationServices.GeofencingApi.addGeofences(
    googleApiClient, getAddGeofencingRequest(), pendingIntent);

// 3. Implement PendingResult callback
result.setResultCallback(new ResultCallback<Status>() {

  @Override
  public void onResult(Status status) {
    if (status.isSuccess()) {
      // 4. If successful, save the geofence
      saveGeofence();
    } else {
      // 5. If not successful, log and send an error
      Log.e(TAG, "Registering geofence failed: " + status.getStatusMessage() +
          " : " + status.getStatusCode());
      sendError();
    }
  }
});

Here’s the play-by-play of the code above:

  1. Create a PendingIntent for AreWeThereIntentService.
  2. Associate the pending intent with geofenceToAdd and make the call to addGeofences().
  3. Handle the PendingResult callback.
  4. On success, make a call to save the geofence.
  5. On error, make a call to send an error.

Add the red missing imports just like before. The imports for Status, ResultCallback, and LocationServices are all from subpackages of com.google.android.gms.

Add the following public method to GeofenceController you can use to start the process of adding a geofence:

public void addGeofence(NamedGeofence namedGeofence, GeofenceControllerListener listener) {
  this.namedGeofenceToAdd = namedGeofence;
  this.geofenceToAdd = namedGeofence.geofence();
  this.listener = listener;

  connectWithCallbacks(connectionAddListener);
}

Here you simply hold the references to the geofence object and listener you’re going to create.

Contributors

Over 300 content creators. Join our team.