
Geofencing is a feature in a software program that uses the global positioning system to define geographical boundaries. 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.
Geofence API is part of Google's Location APIs. It includes Geofence, GeofencingRequest, GeofenceApi, GeofencingEvent, and GeofenceStatusCodes.
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.
Geofence geofence = new Geofence.Builder()
// id = UUID.randomUUID().toString();
.setRequestId(GEOFENCE_REQ_ID) // Geofence ID
// defining fence region
.setCircularRegion(LATITUDE, LONGITUDE, RADIUS)
//.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setExpirationDuration(DURANTION) // expiring date
// transition types that it should look for
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER |
Geofence.GEOFENCE_TRANSITION_EXIT)
.build();
There are several geofence transitions
GEOFENCE_TRANSITION_ENTER indicates when the user enters the monitored region.GEOFENCE_TRANSITION_EXIT indicates when the user exits the region.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 the setLoiteringDelay parameter.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)
// add a Geofence
.addGeofence(geofence)
.build();
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);
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.
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" /> <uses-permission android:name="android.permission. ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" />
Also, geofencing needs Google Services API. Open your build.gradle and add the dependency.
compile 'com.google.android.gms:play-services-location:10.0.1'
First we have to check whether this device has Google Play Services installed.
int resp = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resp == ConnectionResult.SUCCESS) {
Log.e(TAG, "Your device support Google Play Services.");
} else {
Log.e(TAG, "Your device doesn't support Google Play Services.");
}
Our project will consist 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.
Following is activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/colorPrimaryDark"
android:paddingTop="5dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="5dp">
<TextView
android:id="@+id/lat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_weight="0.4"
android:text="Lat: " />
<TextView
android:id="@+id/lon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_weight="0.4"
android:text="Long: " />
<Button
android:id="@+id/btnStart"
android:text="Start"
android:onClick="runGeofence"
android:layout_weight="0.2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<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"
map:uiZoomControls="true"/>
</LinearLayout>
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"/>
Following is steps that describe logic of our application. After that you can see full code.
GoogleMap instance. Implement GoogleMap.OnMapReadyCallback, GoogleMap.OnMapClickListener, and GoogleMap.OnMarkerClickListener in the Activity class and initialize the map.GeofencingApi interface, we need a GoogleApiClient entry point. It will responsible for GoogleApiClient.ConnectionCallbacks and GoogleApiClient.OnConnectionFailedListener callbaks.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. Next step is implementing a LocationListener. Check if the user gave the application the appropriate permissions by creating the Location request and display their current location on the screen. It is important to address that created LocationRequest 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: UPDATE_INTERVAL = 3 * 60 * 1000, FASTEST_INTERVAL = 30 * 1000.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.GeofencingRequest object. We use the geoFenceMarker as the center point for the geofence. We use a PendingIntent object to call a IntentService that will handle the GeofenceEvent.startGeofence() method is responsible for starting the geofencing process in the MainActivity class.
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.api.ResultCallback;
public class LocatonActivity extends AppCompatActivity implements
LocationListener,
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
OnMapReadyCallback,
GoogleMap.OnMapClickListener,
GoogleMap.OnMarkerClickListener,
ResultCallback<Status>
{
private static final String TAG = LocatonActivity.class.getSimpleName();
private TextView textLat, textLong;
private MapFragment mapFragment;
private GoogleMap map;
private Location lastLocation;
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;
private GoogleApiClient googleApiClient;
private Marker locationMarker;
private Marker geoFenceMarker;
private PendingIntent geoFencePendingIntent;
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
private final int GEOFENCE_REQ_CODE = 0;
private Circle geoFenceLimits;
private final int REQ_PERMISSION = 777;
private static final String NOTIFICATION_MSG = "NOTIFICATION MSG";
// create a Intent send by the notification
public static Intent makeNotificationIntent(Context context, String msg) {
Intent intent = new Intent(context, LocatonActivity.class);
intent.putExtra(NOTIFICATION_MSG, msg);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_locaton);
textLat = (TextView) findViewById(R.id.lat);
textLong = (TextView) findViewById(R.id.lon);
checkGooglePlayServicesAvailable();
checkGPS();
// initialize GoogleMaps
initGMaps();
createGoogleApi();
}
// 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 +")");
markerForGeofence(latLng);
}
// callback called when Marker is touched
@Override
public boolean onMarkerClick(Marker marker) {
Log.d(TAG, "onMarkerClickListener: " + marker.getPosition() );
return false;
}
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.d(TAG, "onConnected()");
getLastKnownLocation();
}
// GoogleApiClient.ConnectionCallbacks suspended
@Override
public void onConnectionSuspended(int i) {
Log.d(TAG, "onConnectionSuspended()");
}
// GoogleApiClient.OnConnectionFailedListener fail
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
Log.d(TAG, "onConnectionFailed()");
}
// get last known location
private void getLastKnownLocation() {
Log.d(TAG, "getLastKnownLocation()");
if (checkPermission()) {
lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
if (lastLocation != null) {
Log.d(TAG, "LasKnown location. " +
"Long: " + lastLocation.getLongitude() +
" | Lat: " + lastLocation.getLatitude());
writeLastLocation();
startLocationUpdates();
} else {
Log.d(TAG, "No location retrieved yet");
startLocationUpdates();
}
}
else askPermission();
}
// start location updates
private void startLocationUpdates(){
Log.d(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());
markerLocation(new LatLng(location.getLatitude(), 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.d(TAG, "permissionsDenied()");
}
// Create a Location Marker
private void markerLocation(LatLng latLng) {
Log.d(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);
}
}
// 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);
}
}
// 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();
}
// 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();
}
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);
}
@Override
public void onResult(@NonNull Status status) {
if (status.isSuccess()) {
drawGeofence();
} else {
Log.d(TAG, "Registering geofence failed: " + status.getStatusMessage() +
" : " + status.getStatusCode());
}
}
private void 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);
}
// start Geofence creation process
private void startGeofence() {
Log.d(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");
}
}
public void runGeofence(View v){
startGeofence();
Button btnStart = (Button) findViewById(R.id.btnStart);
btnStart.setEnabled(false);
}
public int checkGooglePlayServicesAvailable() {
int resp = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resp == ConnectionResult.SUCCESS) {
Log.d(TAG, "Your device support Google Play Services!");
} else {
Log.d(TAG, "Your device doesn't support Google Play Services.");
}
return resp;
}
public void checkGPS() {
LocationManager locationManager = (LocationManager) getSystemService(Service.LOCATION_SERVICE);
boolean isGPS = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
boolean isNetwork = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
Log.d(TAG, "checkGPS: GPS - " + isGPS + ". NETWORK - " + isNetwork);
}
}
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 );
}
If you want to use an IntentService to listen for geofence transitions, add an element specifying the service name to AndroidManifest.xml file. This element must be a child of the <application> element:
<application android:allowBackup="true"> ... <service android:name=".GeofenceTrasitionService"/> <application/>
Following is full code of IntentService.
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.";
}
}
}
We can send location using telnet command. To use this, you need to connect to the device from the command line using the following command:
#telnet localhost [DEVICE_PORT] telnet localhost 5554
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] geo fix 49.0933625 33.4391895
Testing geofences on a physical device is best, but if need be, you can run the app on the Android emulator. Doing so requires an emulator setup with Google Play Services installed.
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:
This should remove the error you received when adding geofences in the emulator.
Result
![]() |
![]() |