Android GeofencingAPI
Intro
The Android GeofencingApi makes it easy to define a circular virtual fence around a point on a map and monitor for transitions as the device crosses the virtual fence line.
I recently built an app that used this api to send alerts as the device approaches a location. This was accomplished by defining several concentric geofences around the destination point and responding to the GEOFENCE_TRANSITION_ENTER
trigger as the device crossed each geofence as it moved towards the destination.
This walkthrough will show how the api was used to create and monitor geofences, and talk about some lessons learned.
Setup
To make use of the Android GeofencingAPI, we need an instance of GoogleApiClient from the Google Play Services API. To instantiate the client, our code must have access to the library containing the api classes. This is accomplished by expressing a dependency in the app build.gradle file:
<pre>
dependencies {
…
compile 'com.google.android.gms:play-services-location:10.2.1'
}
</pre>
We also need to get the user’s permission to have access to the device’s location. This is accomplished by declaring the use of the permission in your AndroidManifest.xml
file:
<pre>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</pre>
Code
To start using the GeofencingAPI in our app, we create a GoogleApiClient
field in the MainActivity
class, and make sure the class implements the GoogleApiClient.ConnectionCallbacks
and GoogleApiClient.OnConnectionFailedListener
interfaces.
<pre>
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private GoogleApiClient googleApiClient;
</pre>
We instantiate the GoogleApiClient
in the onCreate
method.
<pre>
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (googleApiClient == null) {
googleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
}
</pre>
This tells the client that we want to use LocationServices.API
and what class will be listening for connection callbacks and failed connections.
Before we can call methods on the client, we need to connect the client to Google Play services – this is done in the onStart()
lifecycle method. It is good practice to disconnect the client after we’re done using it – this is done in the onStop()
lifecycle method.
<pre>
@Override
protected void onStart() {
super.onStart();
googleApiClient.connect();
}
@Override
protected void onStop() {
googleApiClient.disconnect();
super.onStop();
}
</pre>
Now we can use the client to call the GeofencingAPI. First we get our current location so we can calculate our proximity to the destination. We’ll need this to decide how many geofences to create.
<pre>
Location currentLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
</pre>
Next we define a destination.
<pre>
// create a destination
Location destination = new Location("GeofenceAPI demo");
destination.setLatitude(41.246102);
destination.setLongitude(-96.022887);
</pre>
We need to know how far away the destination is.
<pre>
float distanceInMeters = currentLocation.distanceTo(destination);
</pre>
Now we can build a list of concentric geofences – one at 100 meters from the destination, one at 500 meters and one a 1 km. Notice we set the ids of the geofences to something we could display back to the user.
<pre>
List geofenceList = new ArrayList();
// create concentric geofences
if (distanceInMeters > 100f) {
geofenceList.add(
new Geofence.Builder()
.setRequestId("Arrived at Destination!")
.setCircularRegion(
destination.getLatitude(),
destination.getLongitude(),
100f)
.setExpirationDuration(60*60*1000)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.setNotificationResponsiveness(0)
.build());
if (distanceInMeters > 500f) {
geofenceList.add(
new Geofence.Builder()
.setRequestId("500 meters to destination")
.setCircularRegion(
destination.getLatitude(),
destination.getLongitude(),
500f)
.setExpirationDuration(60*60*1000)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.setNotificationResponsiveness(0)
.build());
}
if (distanceInMeters > 1000f) {
geofenceList.add(
new Geofence.Builder()
.setRequestId("1km to destination")
.setCircularRegion(
destination.getLatitude(),
destination.getLongitude(),
1000f)
.setExpirationDuration(60*60*1000)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.setNotificationResponsiveness(0)
.build()
);
}
</pre>
The GeofencingAPI will fire an Intent when the specified transition trigger occurs. We create a PendingIntent that holds the Intent we want to be fired.
<pre>
// build a pending intent
Intent intent = new Intent(this, GeofenceDemoIntentService.class);
PendingIntent pendingIntent = PendingIntent.getService(this, 123, intent, PendingIntent.FLAG_UPDATE_CURRENT);
</pre>
Now we build a GeofencingRequest
, passing it the list of geofences.
<pre>
// build geofencing request
GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
builder.addGeofences(geofenceList);
GeofencingRequest geofencingRequest = builder.build();
</pre>
Finally, we ask the GeofencingAPI to monitor our location for geofence transitions.
<pre>
LocationServices.GeofencingApi.addGeofences(
googleApiClient,
geofencingRequest,
pendingIntent
)
.setResultCallback(this);
</pre>
As our device moves towards the destination, the GeofencingAPI detects that we have crossed a geofence boundary and fires the intent we specified. That intent is received by the onHandleIntent
method of our GeofenceDemoIntentService
class.
<pre>
public class GeofenceDemoIntentService extends IntentService {
public GeofenceDemoIntentService() {
super("GeofenceDemoIntentService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
if (geofencingEvent.hasError()) {
return;
}
int geofenceTransitionType = geofencingEvent.getGeofenceTransition();
if (geofenceTransitionType == Geofence.GEOFENCE_TRANSITION_ENTER) {
List triggeringGeofences = geofencingEvent.getTriggeringGeofences();
// inspect triggeringGeofences and do something interesting
}
}
}
</pre>
Once we have the triggering geofences from the GeofencingEvent
that fired our Intent, we can take whatever action is desired – send an alert, launch another activity, etc.
Lessons Learned
Detection Latency
There is occasional latency in the GeofencingAPI’s detection of transitions, and the latency is unpredictable. It is not unusual to have multiple fences returned by geofencingEvent.getTriggeringGeofences(). My testing was done with my trusty but aging Nexus 5, so perhaps with a newer device, the latency would be less of an issue.
Too Many Fences
Early on, I was creating far too many geofences. Considering the latency mentioned above, my use case was better served by reducing the number of geofences.
Radius Too Small
If the radius of a geofence is small, it may be too small to be dependably detected. If the radius is small enough, and the device is moving fast enough, the transition may not be detected at all.
Remove Unneeded Fences
It is good practice to remove geofences when they are no longer needed. Be sure to pass the same PendingIntent that was used to create the fences.
<pre>
if (googleApiClient.isConnected()) {
LocationServices.GeofencingApi.removeGeofences(
googleApiClient,
pendingIntent
).setResultCallback(this);
}
</pre>
Wrap Up
The Android GeofencingAPI is easy to use. By using the api to create concentric geofences around a single destination point, we were able to be alerted as our device approached a destination. Hopefully this example will help you use the GeofencingAPI for other interesting geolocation use cases.
Read more at Google’s Android API training documentation.