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 3 of 4 of this article. Click here to view the first page.

Wiring Everything Up

All the hooks are in place and ready to be connected. Open AllGeofencesFragment, and add the following along with the other properties:

private GeofenceController.GeofenceControllerListener geofenceControllerListener = 
    new GeofenceController.GeofenceControllerListener() {
  @Override
  public void onGeofencesUpdated() {
    refresh();
  }

  @Override
  public void onError() {
    showErrorToast();
  }
};

The update callback refreshes the UI while the error callback displays a an error message.

You now need to hook up the adapter in order for the geofence card views to display.

Add the following near the end of onViewCreated(), just above the call to refresh():

allGeofencesAdapter = new AllGeofencesAdapter(GeofenceController.getInstance().getNamedGeofences());
viewHolder.geofenceRecyclerView.setAdapter(allGeofencesAdapter);

Here you instantiate allGeofencesAdapter with the list of named geofences and set the adapter on the recycler view.

Add the following code to refresh():

allGeofencesAdapter.notifyDataSetChanged();

if (allGeofencesAdapter.getItemCount() > 0) {
  getViewHolder().emptyState.setVisibility(View.INVISIBLE);
} else {
  getViewHolder().emptyState.setVisibility(View.VISIBLE);
}

Here you notify the adapter that data has been updated and show or hide the empty state based on whether or not there were any results.

Finally, add the following line to onDialogPositiveClick():

GeofenceController.getInstance().addGeofence(geofence, geofenceControllerListener);

When the user taps the Add button, the controller kicks off the add geofence chain.

It’s time to test this all out!

Build and run. Tap the floating action button, and enter some geofence data. If you need to find a specific location, Google Maps will give you the latitude and longitude you require. It’s best to use six significant figures after the decimal.

You should see your first geofence card:

AWTY-Paris

If you’re developing on the emulator, you may receive an error when you attempt to add a geofence. If you do, follow these steps to add location permissions to your emulator:

  1. Go to Settings\Location in your emulator.
  2. Tap on Mode; on Lollipop this is near the top of the list.
  3. Set the mode to set to Device Only, then set the mode to any other option, such as High accuracy.
  4. Tap Agree on the “Use Google’s location service?” popup.

AWTY-LocationPermission

This should remove the error you received when adding geofences in the emulator.

Add a few of your favorite destinations as new geofences and scroll through the list:

AWTY-Multiple

Your users can add as many geofences as they like, but right now they’ll lose their data when the app restarts. Time to implement a save data function!

Adding Persistence

Open GeofenceController.java and add the following code to the bottom of saveGeofence():

String json = gson.toJson(namedGeofenceToAdd);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(namedGeofenceToAdd.id, json);
editor.apply();

Here you use Gson to convert namedGeofenceToAdd into JSON and store that JSON as a string in the users’ shared preferences.

That will save the newly created geofence, but what about reloading saved geofences when the app launches?

Add the following method to GeofenceController:

private void loadGeofences() {
  // Loop over all geofence keys in prefs and add to namedGeofences
  Map<String, ?> keys = prefs.getAll();
  for (Map.Entry<String, ?> entry : keys.entrySet()) {
    String jsonString = prefs.getString(entry.getKey(), null);
    NamedGeofence namedGeofence = gson.fromJson(jsonString, NamedGeofence.class);
    namedGeofences.add(namedGeofence);
  }

  // Sort namedGeofences by name
  Collections.sort(namedGeofences);
}

First, you create a map for all the geofence keys. You then loop over all the keys and use Gson to convert the saved JSON back into a NamedGeofence. Finally, you sort the geofences by name.

As always, import the missing headers; in this case, Map and Collections.

Add the following code to the end of init() to call your new method:

loadGeofences();

Build and run. Add a geofence or two, then use the app switcher to kill the app and run it again. You should see your geofences loaded back up from disk:

AndroidClosing

Removing Geofences

Adding geofences is handy, but what if you want to remove some – or all – of them?

Add the following callback to GeofenceController, which is similar to the one you wrote for adding fences:

private GoogleApiClient.ConnectionCallbacks connectionRemoveListener = 
    new GoogleApiClient.ConnectionCallbacks() {
  @Override
  public void onConnected(Bundle bundle) {
    // 1. Create a list of geofences to remove
    List<String> removeIds = new ArrayList<>();
    for (NamedGeofence namedGeofence : namedGeofencesToRemove) {
      removeIds.add(namedGeofence.id);
    }

    if (removeIds.size() > 0) {
      // 2. Use GoogleApiClient and the GeofencingApi to remove the geofences
      PendingResult<Status> result = LocationServices.GeofencingApi.removeGeofences(
        googleApiClient, removeIds);
      result.setResultCallback(new ResultCallback<Status>() {

       // 3. Handle the success or failure of the PendingResult
        @Override
        public void onResult(Status status) {
          if (status.isSuccess()) {
            removeSavedGeofences();
          } else {
            Log.e(TAG, "Removing geofence failed: " + status.getStatusMessage());
            sendError();
          }
        }
      });
    }
  }

  @Override
  public void onConnectionSuspended(int i) {
    Log.e(TAG, "Connecting to GoogleApiClient suspended.");
    sendError();
  }  
};

Here’s what the callback above does:

  1. Builds a list of geofence id values to remove.
  2. Removes the list of geofences you just built from the device.
  3. Handles success or failure of the removal in a result callback.

Now add the following helper methods to the bottom of the same class:

public void removeGeofences(List<NamedGeofence> namedGeofencesToRemove, 
    GeofenceControllerListener listener) {
  this.namedGeofencesToRemove = namedGeofencesToRemove;
  this.listener = listener;

  connectWithCallbacks(connectionRemoveListener);
}

public void removeAllGeofences(GeofenceControllerListener listener) {
  namedGeofencesToRemove = new ArrayList<>();
  for (NamedGeofence namedGeofence : namedGeofences) {
    namedGeofencesToRemove.add(namedGeofence);
  }
  this.listener = listener;

  connectWithCallbacks(connectionRemoveListener);
}

private void removeSavedGeofences() {
  SharedPreferences.Editor editor = prefs.edit();

  for (NamedGeofence namedGeofence : namedGeofencesToRemove) {
    int index = namedGeofences.indexOf(namedGeofence);
    editor.remove(namedGeofence.id);
    namedGeofences.remove(index);
    editor.apply();
  }

  if (listener != null) {
    listener.onGeofencesUpdated();
  }
}

The first two are public methods that remove either a list of geofences, or all geofences. The third method removes geofences from the users’ shared preferences and then alerts the listener that geofences have been updated.

To get this all working, you’ll need to wire up the DELETE button in the geofence card view.

Open AllGeofencesFragment.java and add the following to onViewCreated(), just before the call to refresh():

allGeofencesAdapter.setListener(new AllGeofencesAdapter.AllGeofencesAdapterListener() {
  @Override
  public void onDeleteTapped(NamedGeofence namedGeofence) {
    List<NamedGeofence> namedGeofences = new ArrayList<>();
    namedGeofences.add(namedGeofence);
    GeofenceController.getInstance().removeGeofences(namedGeofences, geofenceControllerListener);
  }
});

Here you add the geofence to be deleted to a list, then you pass that list to removeGeofences().

Build and run. Tap DELETE on any geofence, and you’ll be prompted to confirm the deletion:

AndroidDelete

Click YES and you’ll see the geofence disappear from the list.

Wouldn’t it be great if you could delete them all at once? That would be a perfect job for a menu item.

Open AllGeofencesActivity.java and add the following:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.menu_all_geofences, menu);

  MenuItem item = menu.findItem(R.id.action_delete_all);

  if (GeofenceController.getInstance().getNamedGeofences().size() == 0) {
    item.setVisible(false);
  }

  return true;
}

This simply shows a delete menu item if there are existing geofences. Add the missing imports to remove any build errors.

Now go to AllGeofencesFragment, and add the following:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setHasOptionsMenu(true);
}

The setHasOptionsMenu() call indicates that the fragment will handle the menu.

Next, add the following override to AllGeofencesFragment:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
  int id = item.getItemId();
  if (id == R.id.action_delete_all) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setMessage(R.string.AreYouSure)
      .setPositiveButton(R.string.Yes, new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          GeofenceController.getInstance().removeAllGeofences(geofenceControllerListener);
        }
      })
      .setNegativeButton(R.string.No, new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          // User cancelled the dialog
        }
      })
      .create()
      .show();
    return true;
  }

  return super.onOptionsItemSelected(item);
}

This code builds an AlertDialog to confirm the user wants to delete all geofences, and if so, calls removeAllGeofences on GeofenceController. Import any missing headers as you’ve done previously.

Finally, add the following line to the bottom of refresh():

getActivity().invalidateOptionsMenu();

Invalidating the menu causes the menu to be removed if there are no geofences.

Build and run. Add multiple geofences, then bring up the menu and tap the Delete All Geofences option:

DeleteAllGeofences

Poof! Your geofences are no more. :]

Contributors

Over 300 content creators. Join our team.