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