Location aware resources allow your application to interact with the physical world and they are ideal for increasing user engagement. Although many mobile apps use them, the topic of this tutorial is a feature that is often overlooked, geofencing.
A geofence is a virtual perimeter set on a real geographic area. Combining a user position with a geofence perimeter, it is possible to know if the user is inside or outside the geofence or even if he is exiting or entering the area.
Imagine a university app that can tell you which colleagues and professors are currently on campus. Or an app for a mall that rewards regular customers. There are many other interesting possibilities that you can explore.
In this tutorial, you learn how to use geofences on Android by creating an application that shows the user a notification when they enter or exit a geofence. It helps if you have previous knowledge of Google Play Services, the Google Maps Android API, or IntentService
. If you don't, then you can still follow along, but you may want to do some research about these topics after reading this tutorial.
1. Geofences on Android
On Android, there are several ways to work with geofences. You could even create your own implementation to work with geofences, but it is easier to use Google's GeofencingApi
.
This APIs is part of Google's Location APIs. It includes Geofence
, GeofencingRequest
, GeofenceApi
, GeofencingEvent
, and GeofenceStatusCodes
. In this tutorial, we use these classes to create and work with geofences.
Geofence Interface
Geofence
is an interface that represents a geographical area that should be monitored. It is created by using the Geofence.Builder
. During its creation, you set the monitored region, the geofence's expiration date, responsiveness, an identifier, and the kind of transitions that it should be looking for.
To keep power consumption to a minimum, it is recommended to use a geofence with a radius of at least 100 meters for most situations. If geofences are located in the countryside, you should increase the radius to 500 meters or higher to make sure the geofences are effective.
Geofence geofence = new Geofence.Builder() .setRequestId(GEOFENCE_REQ_ID) // Geofence ID .setCircularRegion( LATITUDE, LONGITUDE, RADIUS) // defining fence region .setExpirationDuration( DURANTION ) // expiring date // Transition types that it should look for .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT ) .build();
Geofence Transitions
GEOFENCE_TRANSITION_DWELL
indicates that the user entered the area and spent some time there. It is useful to avoid multiple alerts when the user is entering and exiting the area too fast. You can configure the dwelling time using thesetLoiteringDelay
parameter.GEOFENCE_TRANSITION_ENTER
indicates when the user enters the monitored region.GEOFENCE_TRANSITION_EXIT
indicates when the user exits the region.
GeofenceRequest
The GeofencingRequest
class receives the geofences that should be monitored. You can create an instance by using a Builder
, passing a Geofence
or a List<Geofence>
, and the kind of notification to trigger when the geofence(s) is created.
GeofencingRequest request = new GeofencingRequest.Builder() // Notification to trigger when the Geofence is created .setInitialTrigger( GeofencingRequest.INITIAL_TRIGGER_ENTER ) .addGeofence( geofence ) // add a Geofence .build();
GeofencingApi
The GeofencingApi
class is the entry point for all interactions with Google's geofencing API. It is part of the Location APIs and it depends on a GoogleApiClient
to work. You will use the GeofencingApi
to add and remove geofences.
To add a geofence, you call the addGeofence()
method. It monitors the given area using the settings passed to the GeofencingRequest
and shoots a PendingIntent
when a geofence transition, entering or exiting the area, takes place.
PendingResult<Status> addGeofences (GoogleApiClient client, GeofencingRequest geofencingRequest, PendingIntent pendingIntent)
To remove the geofence, you call removeGeofences()
. You can either remove the geofence using its request identifier or its pending intent.
PendingResult<Status> removeGeofences(GoogleApiClient client, List<String> geofenceRequestIds)
PendingResult<Status> removeGeofences (GoogleApiClient client, PendingIntent pendingIntent)
2. Creating a Geofencing App
In this tutorial, we create a simple application that monitors the user location and posts a notification when the user enters or exits a geofenced area. The app consists of only one Activity
and an IntentService
. We also take a quick look at GoogleMap
, GoogleApiClient
, and FusedLocationProviderApi
, and we explore some caveats of the geofence API.
Step 1: Project Setup
GeofencingApi
is part of Google Play Services. To access it, you need to correctly set up your development environment and create an instance of the GoogleApiClient
. Create a new project with a blank Activity
, edit the project's build.gradle file as shown below, and synchronize your project.
Step 2: Permissions
We need to set the correct permissions to create and use geofences. Add the following permission to the project's manifest:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Starting with Android 6.0, the app asks for permission at run time and not during the installation. We address this later in the tutorial.
Step 3: Creating the Layout
The project consists of one layout, the MainActity
layout. It contains the device's current latitude and longitude, and a GoogleMap
fragment that displays the geofences and the user's position.
Since activity_main.xml is pretty straightforward, I want to concentrate only on the MapFragment
element. You can take a look at the completed layout in the source files of this tutorial.
<!--GoogleMap fragment--><fragment xmlns:android="http://schemas.android.com/apk/res/android" android:name="com.google.android.gms.maps.MapFragment" android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent"/>
Step 4: Google Maps API Key
Since we are using a MapFragment
, we need to set up and initialize a GoogleMap
instance. First, you need to obtain an API key. Once you have an API key, add it to the project's manifest.
<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_API_KEY"/>
Let's begin with the GoogleMap
instance. Implement GoogleMap.OnMapReadyCallback
, GoogleMap.OnMapClickListener
, and GoogleMap.OnMarkerClickListener
in the Activity
class and initialize the map.
public class MainActivity extends AppCompatActivity implements OnMapReadyCallback, GoogleMap.OnMapClickListener, GoogleMap.OnMarkerClickListener { private static final String TAG = MainActivity.class.getSimpleName(); private TextView textLat, textLong; private MapFragment mapFragment; private GoogleMap map; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textLat = (TextView) findViewById(R.id.lat); textLong = (TextView) findViewById(R.id.lon); // initialize GoogleMaps initGMaps(); } // Initialize GoogleMaps private void initGMaps(){ mapFragment = (MapFragment) getFragmentManager().findFragmentById(R.id.map); mapFragment.getMapAsync(this); } // Callback called when Map is ready @Override public void onMapReady(GoogleMap googleMap) { Log.d(TAG, "onMapReady()"); map = googleMap; map.setOnMapClickListener(this); map.setOnMarkerClickListener(this); } // Callback called when Map is touched @Override public void onMapClick(LatLng latLng) { Log.d(TAG, "onMapClick("+latLng +")"); } // Callback called when Marker is touched @Override public boolean onMarkerClick(Marker marker) { Log.d(TAG, "onMarkerClickListener: " + marker.getPosition() ); return false; } }
Step 5: GoogleApiClient
To use the GeofencingApi
interface, we need a GoogleApiClient
entry point. Let's implement a GoogleApiClient.ConnectionCallbacks
and a GoogleApiClient.OnConnectionFailedListener
in the Activity
as shown below.
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, OnMapReadyCallback, GoogleMap.OnMapClickListener, GoogleMap.OnMarkerClickListener { // ... private GoogleApiClient googleApiClient; @Override protected void onCreate(Bundle savedInstanceState) { // ... // create GoogleApiClient createGoogleApi(); } // Create GoogleApiClient instance private void createGoogleApi() { Log.d(TAG, "createGoogleApi()"); if ( googleApiClient == null ) { googleApiClient = new GoogleApiClient.Builder( this ) .addConnectionCallbacks( this ) .addOnConnectionFailedListener( this ) .addApi( LocationServices.API ) .build(); } } @Override protected void onStart() { super.onStart(); // Call GoogleApiClient connection when starting the Activity googleApiClient.connect(); } @Override protected void onStop() { super.onStop(); // Disconnect GoogleApiClient when stopping Activity googleApiClient.disconnect(); } // GoogleApiClient.ConnectionCallbacks connected @Override public void onConnected(@Nullable Bundle bundle) { Log.i(TAG, "onConnected()"); } // GoogleApiClient.ConnectionCallbacks suspended @Override public void onConnectionSuspended(int i) { Log.w(TAG, "onConnectionSuspended()"); } // GoogleApiClient.OnConnectionFailedListener fail @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { Log.w(TAG, "onConnectionFailed()"); } }
Step 6: FusedLocationProviderApi
We also need to access the user's current location. The FusedLocationProviderApi
interface gives us this information and allows a great level of control of the location request. This is very important, considering that location requests have a direct effect over the device's battery consumption.
Now, let's implement a LocationListener
. Check if the user gave the application the appropriate permissions by creating the Location
request and display their the current location on the screen.
public class MainActivity extends AppCompatActivity implements // .... LocationListener { private Location lastLocation; //... // GoogleApiClient.ConnectionCallbacks connected @Override public void onConnected(@Nullable Bundle bundle) { Log.i(TAG, "onConnected()"); getLastKnownLocation(); } // Get last known location private void getLastKnownLocation() { Log.d(TAG, "getLastKnownLocation()"); if ( checkPermission() ) { lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient); if ( lastLocation != null ) { Log.i(TAG, "LasKnown location. " + "Long: " + lastLocation.getLongitude() + " | Lat: " + lastLocation.getLatitude()); writeLastLocation(); startLocationUpdates(); } else { Log.w(TAG, "No location retrieved yet"); startLocationUpdates(); } } else askPermission(); } private LocationRequest locationRequest; // Defined in mili seconds. // This number in extremely low, and should be used only for debug private final int UPDATE_INTERVAL = 1000; private final int FASTEST_INTERVAL = 900; // Start location Updates private void startLocationUpdates(){ Log.i(TAG, "startLocationUpdates()"); locationRequest = LocationRequest.create() .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) .setInterval(UPDATE_INTERVAL) .setFastestInterval(FASTEST_INTERVAL); if ( checkPermission() ) LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this); } @Override public void onLocationChanged(Location location) { Log.d(TAG, "onLocationChanged ["+location+"]"); lastLocation = location; writeActualLocation(location); } // Write location coordinates on UI private void writeActualLocation(Location location) { textLat.setText( "Lat: " + location.getLatitude() ); textLong.setText( "Long: " + location.getLongitude() ); } private void writeLastLocation() { writeActualLocation(lastLocation); } // Check for permission to access Location private boolean checkPermission() { Log.d(TAG, "checkPermission()"); // Ask for permission if it wasn't granted yet return (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ); } // Asks for permission private void askPermission() { Log.d(TAG, "askPermission()"); ActivityCompat.requestPermissions( this, new String[] { Manifest.permission.ACCESS_FINE_LOCATION }, REQ_PERMISSION ); } // Verify user's response of the permission requested @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Log.d(TAG, "onRequestPermissionsResult()"); super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch ( requestCode ) { case REQ_PERMISSION: { if ( grantResults.length> 0&& grantResults[0] == PackageManager.PERMISSION_GRANTED ){ // Permission granted getLastKnownLocation(); } else { // Permission denied permissionsDenied(); } break; } } } // App cannot work without the permissions private void permissionsDenied() { Log.w(TAG, "permissionsDenied()"); } }
It is important to address that the LocationRequest
created above isn't optimized for a production environment. The UPDATE_INTERVAL
is too short and would consume too much battery power. A more realistic configuration for production could be:
private final int UPDATE_INTERVAL = 3 * 60 * 1000; // 3 minutes private final int FASTEST_INTERVAL = 30 * 1000; // 30 secs private void startLocationUpdates(){ Log.i(TAG, "startLocationUpdates()"); locationRequest = LocationRequest.create() .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) .setInterval(UPDATE_INTERVAL) .setFastestInterval(FASTEST_INTERVAL); if ( checkPermission() ) LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this); }
Step 7: GoogleMap Markers
Our Activity
needs two different markers. A locationMarker
uses the latitude and longitude given by the FusedLocationProviderApi
to inform the device's current location. A geoFenceMarker
is the target for the geofence creation as it uses the last touch given on the map to retrieve its position.
@Override public void onMapClick(LatLng latLng) { Log.d(TAG, "onMapClick("+latLng +")"); markerForGeofence(latLng); } private void writeActualLocation(Location location) { // ... markerLocation(new LatLng(location.getLatitude(), location.getLongitude())); } private Marker locationMarker; // Create a Location Marker private void markerLocation(LatLng latLng) { Log.i(TAG, "markerLocation("+latLng+")"); String title = latLng.latitude + ", " + latLng.longitude; MarkerOptions markerOptions = new MarkerOptions() .position(latLng) .title(title); if ( map!=null ) { // Remove the anterior marker if ( locationMarker != null ) locationMarker.remove(); locationMarker = map.addMarker(markerOptions); float zoom = 14f; CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, zoom); map.animateCamera(cameraUpdate); } } private Marker geoFenceMarker; // Create a marker for the geofence creation private void markerForGeofence(LatLng latLng) { Log.i(TAG, "markerForGeofence("+latLng+")"); String title = latLng.latitude + ", " + latLng.longitude; // Define marker options MarkerOptions markerOptions = new MarkerOptions() .position(latLng) .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE)) .title(title); if ( map!=null ) { // Remove last geoFenceMarker if (geoFenceMarker != null) geoFenceMarker.remove(); geoFenceMarker = map.addMarker(markerOptions); } }
Step 8: Creating a Geofence
At last, it is time to create a geofence. We use the geoFenceMarker
as the center point for the geofence.
private static final long GEO_DURATION = 60 * 60 * 1000; private static final String GEOFENCE_REQ_ID = "My Geofence"; private static final float GEOFENCE_RADIUS = 500.0f; // in meters // Create a Geofence private Geofence createGeofence( LatLng latLng, float radius ) { Log.d(TAG, "createGeofence"); return new Geofence.Builder() .setRequestId(GEOFENCE_REQ_ID) .setCircularRegion( latLng.latitude, latLng.longitude, radius) .setExpirationDuration( GEO_DURATION ) .setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT ) .build(); }
Next, we create the GeofencingRequest
object.
// Create a Geofence Request private GeofencingRequest createGeofenceRequest( Geofence geofence ) { Log.d(TAG, "createGeofenceRequest"); return new GeofencingRequest.Builder() .setInitialTrigger( GeofencingRequest.INITIAL_TRIGGER_ENTER ) .addGeofence( geofence ) .build(); }
We use a PendingIntent
object to call a IntentService
that will handle the GeofenceEvent
. We create the GeofenceTrasitionService.class
later.
private PendingIntent geoFencePendingIntent; private final int GEOFENCE_REQ_CODE = 0; private PendingIntent createGeofencePendingIntent() { Log.d(TAG, "createGeofencePendingIntent"); if ( geoFencePendingIntent != null ) return geoFencePendingIntent; Intent intent = new Intent( this, GeofenceTrasitionService.class); return PendingIntent.getService( this, GEOFENCE_REQ_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT ); } // Add the created GeofenceRequest to the device's monitoring list private void addGeofence(GeofencingRequest request) { Log.d(TAG, "addGeofence"); if (checkPermission()) LocationServices.GeofencingApi.addGeofences( googleApiClient, request, createGeofencePendingIntent() ).setResultCallback(this); }
We also draw the geofence on the map as a visual reference.
@Override public void onResult(@NonNull Status status) { Log.i(TAG, "onResult: " + status); if ( status.isSuccess() ) { drawGeofence(); } else { // inform about fail } } // Draw Geofence circle on GoogleMap private Circle geoFenceLimits; private void drawGeofence() { Log.d(TAG, "drawGeofence()"); if ( geoFenceLimits != null ) geoFenceLimits.remove(); CircleOptions circleOptions = new CircleOptions() .center( geoFenceMarker.getPosition()) .strokeColor(Color.argb(50, 70,70,70)) .fillColor( Color.argb(100, 150,150,150) ) .radius( GEOFENCE_RADIUS ); geoFenceLimits = map.addCircle( circleOptions ); }
The startGeofence()
method is responsible for starting the geofencing process in the MainActivity
class.
@Override public boolean onOptionsItemSelected(MenuItem item) { switch ( item.getItemId() ) { case R.id.geofence: { startGeofence(); return true; } } return super.onOptionsItemSelected(item); } // Start Geofence creation process private void startGeofence() { Log.i(TAG, "startGeofence()"); if( geoFenceMarker != null ) { Geofence geofence = createGeofence( geoFenceMarker.getPosition(), GEOFENCE_RADIUS ); GeofencingRequest geofenceRequest = createGeofenceRequest( geofence ); addGeofence( geofenceRequest ); } else { Log.e(TAG, "Geofence marker is null"); } }
Step 9: Geofence Transition Service
We can now finally create the GeofenceTrasitionService.class
mentioned earlier. This class extends IntentService
and is responsible for handling the GeofencingEvent
. First, we get this event from the received intent.
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
We then check if the kind of geofencing transition that took place is of interest to us. If it is, we retrieve a list of the triggered geofences and create a notification with the appropriate actions.
// Retrieve GeofenceTrasition int geoFenceTransition = geofencingEvent.getGeofenceTransition(); // Check if the transition type if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || geoFenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT ) { // Get the geofence that were triggered List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences(); // Create a detail message with Geofences received String geofenceTransitionDetails = getGeofenceTrasitionDetails(geoFenceTransition, triggeringGeofences ); // Send notification details as a String sendNotification( geofenceTransitionDetails ); }
I have also implemented a few helper methods to make the implementation of the class easier to understand.
public class GeofenceTrasitionService extends IntentService { private static final String TAG = GeofenceTrasitionService.class.getSimpleName(); public static final int GEOFENCE_NOTIFICATION_ID = 0; public GeofenceTrasitionService() { super(TAG); } @Override protected void onHandleIntent(Intent intent) { // Retrieve the Geofencing intent GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); // Handling errors if ( geofencingEvent.hasError() ) { String errorMsg = getErrorString(geofencingEvent.getErrorCode() ); Log.e( TAG, errorMsg ); return; } // Retrieve GeofenceTrasition int geoFenceTransition = geofencingEvent.getGeofenceTransition(); // Check if the transition type if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || geoFenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT ) { // Get the geofence that were triggered List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences(); // Create a detail message with Geofences received String geofenceTransitionDetails = getGeofenceTrasitionDetails(geoFenceTransition, triggeringGeofences ); // Send notification details as a String sendNotification( geofenceTransitionDetails ); } } // Create a detail message with Geofences received private String getGeofenceTrasitionDetails(int geoFenceTransition, List<Geofence> triggeringGeofences) { // get the ID of each geofence triggered ArrayList<String> triggeringGeofencesList = new ArrayList<>(); for ( Geofence geofence : triggeringGeofences ) { triggeringGeofencesList.add( geofence.getRequestId() ); } String status = null; if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ) status = "Entering "; else if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT ) status = "Exiting "; return status + TextUtils.join( ", ", triggeringGeofencesList); } // Send a notification private void sendNotification( String msg ) { Log.i(TAG, "sendNotification: " + msg ); // Intent to start the main Activity Intent notificationIntent = MainActivity.makeNotificationIntent( getApplicationContext(), msg ); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addParentStack(MainActivity.class); stackBuilder.addNextIntent(notificationIntent); PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); // Creating and sending Notification NotificationManager notificatioMng = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE ); notificatioMng.notify( GEOFENCE_NOTIFICATION_ID, createNotification(msg, notificationPendingIntent)); } // Create a notification private Notification createNotification(String msg, PendingIntent notificationPendingIntent) { NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this); notificationBuilder .setSmallIcon(R.drawable.ic_action_location) .setColor(Color.RED) .setContentTitle(msg) .setContentText("Geofence Notification!") .setContentIntent(notificationPendingIntent) .setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_SOUND) .setAutoCancel(true); return notificationBuilder.build(); } // Handle errors private static String getErrorString(int errorCode) { switch (errorCode) { case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: return "GeoFence not available"; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: return "Too many GeoFences"; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS: return "Too many pending intents"; default: return "Unknown error."; } } }
3. Testing
Testing on a Virtual Device
It is much simpler to test geofencing on a virtual device. There are several ways to do this. In Android Studio, open a virtual device and click the more options button in the bottom right.
In the Location tab on the left, enter the coordinates for the location.
I prefer to use telnet commands to control the virtual device. To use this, you need to connect to the device from the command line using the following command:
telnet localhost [DEVICE_PORT]
The device port is shown in the virtual device window. The device port is usually equal to 5554.
It is possible that you need to authorize this connection using your auth_token
, but the command line shows you where it is located. Navigate to that location and copy the token and type, auth [YOUR_AUTH_TOKEN]
.
You can now set the location of the device by running the following command:
geo fix [LATITUDE] [LONGITUDE]
Conclusion
Geofencing may be a great addition to your app as it can considerably increase user engagement. There are lots of possibilities to explore and you could even create a sophisticated experience using indoor beacons, such as the Estimote. With indoor beacons, you know exactly where the user has passed in, for example, a shopping mall.
Adding Geofencing to a project is simple, but we need to keep power consumption in mind at all times. This means that we need to carefully choose the size of the geofence and the update rate because both directly impact power consumption of your application.
Testing is therefore very important to get a realistic idea of the power consumption of your application. Also consider giving users the option to disable geofencing altogether if they don't want or need this feature.