Introduction
A few months ago, I had written about how to setup and monitor geofences. Since then, Google has made changes to their location APIs by removing some of the classes such as the LocationClient class. This change was announced in 2013, but Google's documentation had made no mention of the changes that were to come. At the time of writing, documentation and examples on how to setup and monitor geofences using the new method are scarce.
Requirements
Make sure that you update your SDK packages so that you have Google Play services revision 22. If you are using eclipse and already have Google Play services as project, you may need to clean and rebuild or reimport the code. If you need help setting up Google Play services, see this.
Application
The AndroidManifest.xml file doesn't require any changes from my previous entry.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.paulusworld.geofence" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- Requests address-level location access, which is usually necessary for Geofencing --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".GeofenceIntentService" /> <!-- Required for Google Maps --> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="YOUR_GOOGLE_MAPS_API_KEY_HERE" /> </application> </manifest>
The android.permission.INTERNET and android.permission.ACCESS_NETWORK_STATE are necessary for downloading Google Map data. The android.permission.ACCESS_FINE_LOCATION and com.google.android.providers.gsf.permission.READ_GSERVICES are needed for use with geofences. The permission android.permission.WAKE_LOCK is used for notifications and is not required for geofencing.
We're specifying a service on line 38. By using a service, our app does not need to be running when we enter, exit, or dwell in a geofence.
I've defined some strings in the strings.xml that will be used in the example:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Geofence</string> <string name="hello_world">Hello world!</string> <string name="action_settings">Settings</string> <string name="geofence_transition_notification_title"> %1$s geofence(s) %2$s </string> <string name="geofence_transition_notification_text"> Click notification to return to app </string> <string name="geofence_transition_unknown">Unknown transition</string> <string name="geofence_transition_entered">Entered</string> <string name="geofence_transition_exited">Exited</string> <string name="geofence_transition_dwell">Stop dwelling!</string> <string name="geofence_intent_service">Geofence Intent Service</string> </resources>
We'll use a simple layout that uses a MapFragment:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:orientation="vertical" tools:context="com.paulusworld.geofence.MainActivity" > <fragment android:id="@+id/map" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="45" class="com.google.android.gms.maps.SupportMapFragment" /> </LinearLayout>
One of the reasons why Google changed the way geofences were handled in code was to make it easier for the developer. If you look at the old way, you will notice that there is a lot less code and logic that is required to make this work. Additionally, I moved the geofence logic into a different class called GeofenceStore. I did this to keep the main activity class as clean as possible.
package com.paulusworld.geofence; import java.util.ArrayList; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.location.Geofence; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.LatLng; import android.graphics.Color; import android.os.Bundle; import android.support.v4.app.FragmentActivity; public class MainActivity extends FragmentActivity implements OnCameraChangeListener { /** * Google Map object */ private GoogleMap mMap; /** * Geofence Data */ /** * Geofences Array */ ArrayList<Geofence> mGeofences; /** * Geofence Coordinates */ ArrayList<LatLng> mGeofenceCoordinates; /** * Geofence Radius' */ ArrayList<Integer> mGeofenceRadius; /** * Geofence Store */ private GeofenceStore mGeofenceStore; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Initializing variables mGeofences = new ArrayList<Geofence>(); mGeofenceCoordinates = new ArrayList<LatLng>(); mGeofenceRadius = new ArrayList<Integer>(); // Adding geofence coordinates to array. mGeofenceCoordinates.add(new LatLng(43.042861, -87.911559)); mGeofenceCoordinates.add(new LatLng(43.042998, -87.909753)); mGeofenceCoordinates.add(new LatLng(43.040732, -87.921364)); mGeofenceCoordinates.add(new LatLng(43.039912, -87.897038)); // Adding associated geofence radius' to array. mGeofenceRadius.add(100); mGeofenceRadius.add(50); mGeofenceRadius.add(160); mGeofenceRadius.add(160); // Bulding the geofences and adding them to the geofence array. // Performing Arts Center mGeofences.add(new Geofence.Builder() .setRequestId("Performing Arts Center") // The coordinates of the center of the geofence and the radius in meters. .setCircularRegion(mGeofenceCoordinates.get(0).latitude, mGeofenceCoordinates.get(0).longitude, mGeofenceRadius.get(0).intValue()) .setExpirationDuration(Geofence.NEVER_EXPIRE) // Required when we use the transition type of GEOFENCE_TRANSITION_DWELL .setLoiteringDelay(30000) .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_DWELL | Geofence.GEOFENCE_TRANSITION_EXIT).build()); // Starbucks mGeofences.add(new Geofence.Builder() .setRequestId("Starbucks") // The coordinates of the center of the geofence and the radius in meters. .setCircularRegion(mGeofenceCoordinates.get(1).latitude, mGeofenceCoordinates.get(1).longitude, mGeofenceRadius.get(1).intValue()) .setExpirationDuration(Geofence.NEVER_EXPIRE) // Required when we use the transition type of GEOFENCE_TRANSITION_DWELL .setLoiteringDelay(30000) .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_DWELL | Geofence.GEOFENCE_TRANSITION_EXIT).build()); // Milwaukee Public Museum mGeofences.add(new Geofence.Builder() .setRequestId("Milwaukee Public Museum") // The coordinates of the center of the geofence and the radius in meters. .setCircularRegion(mGeofenceCoordinates.get(2).latitude, mGeofenceCoordinates.get(2).longitude, mGeofenceRadius.get(2).intValue()) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT).build()); // Milwaukee Art Museum mGeofences.add(new Geofence.Builder() .setRequestId("Milwaukee Art Museum") // The coordinates of the center of the geofence and the radius in meters. .setCircularRegion(mGeofenceCoordinates.get(3).latitude, mGeofenceCoordinates.get(3).longitude, mGeofenceRadius.get(3).intValue()) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT).build()); // Add the geofences to the GeofenceStore object. mGeofenceStore = new GeofenceStore(this, mGeofences); } @Override protected void onStart() { super.onStart(); } @Override protected void onStop() { mGeofenceStore.disconnect(); super.onStop(); } @Override protected void onResume() { super.onResume(); if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) { setUpMapIfNeeded(); } else { GooglePlayServicesUtil.getErrorDialog( GooglePlayServicesUtil.isGooglePlayServicesAvailable(this), this, 0); } } private void setUpMapIfNeeded() { // Do a null check to confirm that we have not already instantiated the // map. if (mMap == null) { // Try to obtain the map from the SupportMapFragment. mMap = ((SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map)).getMap(); // Check if we were successful in obtaining the map. if (mMap != null) { setUpMap(); } } } /** * This is where we can add markers or lines, add listeners or move the * camera. In this case, we just add a marker near Africa. * <p/> * This should only be called once and when we are sure that {@link #mMap} * is not null. */ private void setUpMap() { // Centers the camera over the building and zooms int far enough to // show the floor picker. mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng( 43.039634, -87.908395), 14)); // Hide labels. mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); mMap.setIndoorEnabled(false); mMap.setMyLocationEnabled(true); mMap.setOnCameraChangeListener(this); } @Override public void onCameraChange(CameraPosition position) { // Makes sure the visuals remain when zoom changes. for(int i = 0; i < mGeofenceCoordinates.size(); i++) { mMap.addCircle(new CircleOptions().center(mGeofenceCoordinates.get(i)) .radius(mGeofenceRadius.get(i).intValue()) .fillColor(0x40ff0000) .strokeColor(Color.TRANSPARENT).strokeWidth(2)); } } }
package com.paulusworld.geofence; import java.util.ArrayList; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.location.Location; import android.os.Bundle; import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; public class GeofenceStore implements ConnectionCallbacks, OnConnectionFailedListener, ResultCallback<Status>, LocationListener { private final String TAG = this.getClass().getSimpleName(); /** * Context */ private Context mContext; /** * Google API client object. */ private GoogleApiClient mGoogleApiClient; /** * Geofencing PendingIntent */ private PendingIntent mPendingIntent; /** * List of geofences to monitor. */ private ArrayList<Geofence> mGeofences; /** * Geofence request. */ private GeofencingRequest mGeofencingRequest; /** * Location Request object. */ private LocationRequest mLocationRequest; /** * Constructs a new GeofenceStore. * * @param context The context to use. * @param geofences List of geofences to monitor. */ public GeofenceStore(Context context, ArrayList<Geofence> geofences) { mContext = context; mGeofences = new ArrayList<Geofence>(geofences); mPendingIntent = null; // Build a new GoogleApiClient, specify that we want to use LocationServices // by adding the API to the client, specify the connection callbacks are in // this class as well as the OnConnectionFailed method. mGoogleApiClient = new GoogleApiClient.Builder(context) .addApi(LocationServices.API).addConnectionCallbacks(this) .addOnConnectionFailedListener(this).build(); // This is purely optional and has nothing to do with geofencing. // I added this as a way of debugging. // Define the LocationRequest. mLocationRequest = new LocationRequest(); // We want a location update every 10 seconds. mLocationRequest.setInterval(10000); // We want the location to be as accurate as possible. mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); mGoogleApiClient.connect(); } @Override public void onResult(Status result) { if (result.isSuccess()) { Log.v(TAG, "Success!"); } else if (result.hasResolution()) { // TODO Handle resolution } else if (result.isCanceled()) { Log.v(TAG, "Canceled"); } else if (result.isInterrupted()) { Log.v(TAG, "Interrupted"); } else { } } @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.v(TAG, "Connection failed."); } @Override public void onConnected(Bundle connectionHint) { // We're connected, now we need to create a GeofencingRequest with // the geofences we have stored. mGeofencingRequest = new GeofencingRequest.Builder().addGeofences( mGeofences).build(); mPendingIntent = createRequestPendingIntent(); // This is for debugging only and does not affect // geofencing. LocationServices.FusedLocationApi.requestLocationUpdates( mGoogleApiClient, mLocationRequest, this); // Submitting the request to monitor geofences. PendingResult<Status> pendingResult = LocationServices.GeofencingApi .addGeofences(mGoogleApiClient, mGeofencingRequest, mPendingIntent); // Set the result callbacks listener to this class. pendingResult.setResultCallback(this); } @Override public void onConnectionSuspended(int cause) { Log.v(TAG, "Connection suspended."); } /** * This creates a PendingIntent that is to be fired when geofence transitions * take place. In this instance, we are using an IntentService to handle the * transitions. * * @return A PendingIntent that will handle geofence transitions. */ private PendingIntent createRequestPendingIntent() { if (mPendingIntent == null) { Log.v(TAG, "Creating PendingIntent"); Intent intent = new Intent(mContext, GeofenceIntentService.class); mPendingIntent = PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } return mPendingIntent; } @Override public void onLocationChanged(Location location) { Log.v(TAG, "Location Information\n" + "==========\n" + "Provider:\t" + location.getProvider() + "\n" + "Lat & Long:\t" + location.getLatitude() + ", " + location.getLongitude() + "\n" + "Altitude:\t" + location.getAltitude() + "\n" + "Bearing:\t" + location.getBearing() + "\n" + "Speed:\t\t" + location.getSpeed() + "\n" + "Accuracy:\t" + location.getAccuracy() + "\n"); } }
- On line 74 to 76 we build a new GoogleApiClient, add the LocationServices API, specify where the ConnectionCallbacks and onConnectionFailure callbacks are located, and finally actually building the client.
- On lines 81, 83, and 85 create a LocationRequest, which is used to print information about location updates. This is strictly for debugging purposes and not required for geofencing.
- Line 87 is where the GoogleApiClient is connected. Without calling this method, no operations can be performed.
- The onResult, onConnectionFailed, onConnected, onConnectionSuspended and onLocationChanged are methods that are required by the ResultCallback<Status>, ConnectionCallbacks, onConnectionFailure, and LocationListener.
- The onResult definition, on line 95, is where we would handle various results from the GeofencingApi. This is set on line 135.
- Normally we would handle connection failures, but since this is a sample applications, we are just going to log the failure.
- The onConnected function is where the real work is done. We build a GeofencingRequest and add the geofences that we want to monitor on line 119.
- A PendingIntent is created on line 122 that has an IntentService associated with it. This PendingIntent is used when Geofence transitions happen.
- We make a location update request on line 126, which is strictly for debugging purposes. Again, this can be omitted as this has nothing to do with geofencing.
- The GeofencingRequest is submitted to the LocationServices on line 130.
The GeofenceIntentService is a basic class that handles Intent broadcasts from the LocationServices API. The sendNotification and getTriggeringGeofences are just additional logic code that is executed when a Geofence transition is broadcast.
package com.paulusworld.geofence; import java.util.List; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingEvent; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.os.PowerManager; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; public class GeofenceIntentService extends IntentService { private final String TAG = this.getClass().getCanonicalName(); public GeofenceIntentService() { super("GeofenceIntentService"); Log.v(TAG, "Constructor."); } public void onCreate() { super.onCreate(); Log.v(TAG, "onCreate"); } public void onDestroy() { super.onDestroy(); Log.v(TAG, "onDestroy"); } @Override protected void onHandleIntent(Intent intent) { GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); Log.v(TAG, "onHandleIntent"); if(!geofencingEvent.hasError()) { int transition = geofencingEvent.getGeofenceTransition(); String notificationTitle; switch(transition) { case Geofence.GEOFENCE_TRANSITION_ENTER: notificationTitle = "Geofence Entered"; Log.v(TAG, "Geofence Entered"); break; case Geofence.GEOFENCE_TRANSITION_DWELL: notificationTitle = "Geofence Dwell"; Log.v(TAG, "Dwelling in Geofence"); break; case Geofence.GEOFENCE_TRANSITION_EXIT: notificationTitle = "Geofence Exit"; Log.v(TAG, "Geofence Exited"); break; default: notificationTitle = "Geofence Unknown"; } sendNotification(this, getTriggeringGeofences(intent), notificationTitle); } } private void sendNotification(Context context, String notificationText, String notificationTitle) { PowerManager pm = (PowerManager) context .getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, ""); wakeLock.acquire(); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( context).setSmallIcon(R.drawable.ic_launcher) .setContentTitle(notificationTitle) .setContentText(notificationText) .setDefaults(Notification.DEFAULT_ALL).setAutoCancel(false); NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); wakeLock.release(); } private String getTriggeringGeofences(Intent intent) { GeofencingEvent geofenceEvent = GeofencingEvent.fromIntent(intent); List<Geofence> geofences = geofenceEvent .getTriggeringGeofences(); String[] geofenceIds = new String[geofences.size()]; for (int i = 0; i < geofences.size(); i++) { geofenceIds[i] = geofences.get(i).getRequestId(); } return TextUtils.join(", ", geofenceIds); } }
In order to get Geofence information from the intent received in the IntentService, we create a GeofencingEvent by calling GeofencingEvent.fromIntent(Intent intent) on line 39. On line 41, we make sure there are no errors before getting the transition on line 42 by calling the getGeofenceTransition function. From that point, the rest of the function is self explanatory and the rest of the class is beyond the scope of Geofencing.
There is one important thing to remember when working with geofences, transition broadcasts won't be fired if the user is not completely in the geofence. Accuracy is also taken into account when determining if a user has entered, exitied, or is dwelling. For example, if a geofence has a diameter of 100 meters and your accuracy is 150 meteres even if you are shown as being smack in the center of the geofence, no transitions will be fired because Google isn't sure if you are in the geofence or not.