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?
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:
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.
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 OnClickListener
s, 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:
- Check if the user entered valid data.
- If so, create a new
NamedGeofence
and set its properties. - Call the listener and dismiss the dialog.
- 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:
- Create a
PendingIntent
forAreWeThereIntentService
. - Associate the pending intent with
geofenceToAdd
and make the call toaddGeofences()
. - Handle the
PendingResult
callback. - On success, make a call to save the geofence.
- 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.