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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Geofences on Android with GoogleApiClient
30 mins
- Getting Started
- Running the Starter Project
- Working with GoogleApiClient
- Creating Geofence Objects
- Creating GeofenceController
- Adding Geofences
- Connecting to GoogleApiClient
- Wiring Everything Up
- Adding Persistence
- Removing Geofences
- Displaying a Notification
- Testing with Mock Locations
- Where To Go From Here?
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:
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:
- Go to Settings\Location in your emulator.
- Tap on Mode; on Lollipop this is near the top of the list.
- Set the mode to set to Device Only, then set the mode to any other option, such as High accuracy.
- Tap Agree on the “Use Google’s location service?” popup.
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:
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:
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:
- Builds a list of geofence id values to remove.
- Removes the list of geofences you just built from the device.
- 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:
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:
Poof! Your geofences are no more. :]