Geo-fencing is a relatively new feature used to detect geographical boundaries by using GPS (Global Positioning System) and RFID (Radio Frequency Identification).

Geo-fencing is majorly used in mobile monitoring, it is a popular feature in parental controls, parents set so-called geo-fences zones to detect their child’s location.

Once the device enters or leaves the virtual barriers like “school” or “home”, Parental Board receives an alerting message via SMS or email.

In this article we will look through the technical aspects of geo-fencing.

Firstly create a new project and it MapsActivity. In order to work with Google Maps, you need a key. And to get the key, you need to register the application in the Google Developer Console.

Next you need to include the key in the application. Insert it into AndroidManifest.xml tag <meta-data>. Don’t forget to give permission to determine the location of the phone.

 

<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/AppTheme" >

 

            <uses-permissionandroid:name="android.permission.INTERNET"/>

            <uses-permission             android:name="android.permission.ACCESS_COARSE_LOCATION"/>

            <uses-permission             android:name="android.permission.ACCESS_FINE_LOCATION"/>

 

<!--

The API key for Google Maps-based APIs is defined as a string resource.

(See the file "res/values/google_maps_api.xml").

Note that the API key is linked to the encryption key used to sign the APK.

You need a different API key for each encryption key, including the release key that is used to

sign the APK for publishing.

You can define the keys for the debug and release targets in src/debug/ and src/release/.

<meta-data

android:name="com.google.android.geo.API_KEY"

android:value="@string/google_maps_key" />

...

 

</application>

Now the application can work with Google geo-service. Let’s add Maps Activity:

public class MapsActivity extends FragmentActivityimplements OnMapReadyCallback,GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener, LocationListener {

 

privateGoogleMapmMap;

privateLocationManagermLocationManager;

privateMarker mCurrentPositionMarker;

privateList<Circle>mZones;

 

// Constant radius for each geofence zone

private final float mRadius= 500.0f;

 

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_maps);

// Obtain the SupportMapFragment and get notified when the map is ready to be used.

SupportMapFragmentmapFragment = (SupportMapFragment) getSupportFragmentManager()

.findFragmentById(R.id.map);

mapFragment.getMapAsync(this);

// Get LocationManager for determining current user location

mLocationManager= (LocationManager) getSystemService(Context.LOCATION_SERVICE);

mLocationManager.requestLocationUpdates(

LocationManager.GPS_PROVIDER, 0, 0, this);

mZones= new ArrayList<>();

}

@Override

public void onMapReady(GoogleMapgoogleMap) {

mMap= googleMap;

mMap.setOnMapClickListener(this);

mMap.setOnMapLongClickListener(this);

Location location = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);

setCurrentPositionMarker(location);

}

 

@Override

public void onLocationChanged(Location location) {

setCurrentPositionMarker(location);

}

@Override

public void onStatusChanged(String provider, intstatus, Bundle extras) { /* do nothing */ }

@Override

public void onProviderEnabled(String provider) { /* do nothing */ }

@Override

public void onProviderDisabled(String provider) { /* do nothing */ }

private void setCurrentPositionMarker(Location location) {

if(location != null) {

if(mCurrentPositionMarker!= null) {

mMap.clear();

}

LatLng position = new LatLng(location.getLatitude(), location.getLongitude());

MarkerOptions options = new MarkerOptions();

options.anchor(0.5f, 0.5f).position(position);

mCurrentPositionMarker= mMap.addMarker(options);

// Move camera to the current user position if position was changed

mMap.moveCamera(CameraUpdateFactory.newCameraPosition(CameraPosition.fromLatLngZoom(new LatLng(location.getLatitude(), location.getLongitude()), 11.0f)));

}

}

}

Now we need to create a map and define the current location of the user. The position is displayed by a marker on the map. Thanks to  LocationListener we can track the change of the position. At this point, we update the map and reposition the marker to point to the new position.

We need to add functionality according to geo-fence display areas on the map.

Update MapsActivityand add methods.

/**

* Action for adding new zone

* @paramlatLng-- position of zone center

*/

@Override

public void onMapClick(LatLnglatLng) {

mZones.add(mMap.addCircle(new CircleOptions()

.center(latLng)

.radius(mRadius)

.strokeColor(getResources().getColor(R.color.colorMapCirleStroke))

.fillColor(getResources().getColor(R.color.colorMapCirleFill))));

Intent geoIntent = new Intent(this, GeofencingService.class);

geoIntent.setAction(Consts.ADD_GEOFENCE_ACTION);

geoIntent.putExtra(Consts.LATITUDE, latLng.latitude);

geoIntent.putExtra(Consts.LONGITUDE, latLng.longitude);

geoIntent.putExtra(Consts.RADIUS, mRadius);

startService(geoIntent);

}

/**

* Action for removing existing zones

* @paramlatLng-- position on user tap

*/

@Override

public void onMapLongClick(LatLnglatLng) {

Intent geoIntent = new Intent(this, GeofencingService.class);

geoIntent.setAction(Consts.REMOVE_GEOFENCE_ACTION);

geoIntent.putExtra(Consts.LATITUDE, latLng.latitude);

geoIntent.putExtra(Consts.LONGITUDE, latLng.longitude);

startService(geoIntent);

// Removing zones out of map

Iterator geoIterator = mZones.iterator();

while(geoIterator.hasNext()) {

Circle zone = (Circle) geoIterator.next();

float[] results = new float[1];

Location.distanceBetween(zone.getCenter().latitude, zone.getCenter().longitude, latLng.latitude, latLng.longitude, results);

if(zone.getRadius() >= results[0]) {

geoIterator.remove();

}

}

updateMapZones();

}

private void updateMapZones() {

mMap.clear();

// Add geo zones that left

for(Circle zone : mZones) {

mMap.addCircle(new CircleOptions()

.center(zone.getCenter())

.radius(zone.getRadius())

.strokeColor(zone.getStrokeColor())

.fillColor(zone.getFillColor()));

}

// Add current user position marker on map

if(mCurrentPositionMarker!= null) {

MarkerOptions options = new MarkerOptions();

options.anchor(0.5f, 0.5f).position(mCurrentPositionMarker.getPosition());

mCurrentPositionMarker= mMap.addMarker(options);

}

}

A new zone with a center in the click point will be added 500 meters in radius. Long click will delete the zone. If the user clicks on the map in the area of intersection of two or more zones, all zones will be deleted. In any case, we will have an Intent, which will be sent to Service to monitor geo-fence zones. Action can be changed depending on the click. It is done so the service can figure out what to do with a new point. 

Now create a Geofencing Service and add a new code:

public class GeofencingService extends Service implements Connection Callbacks, OnConnectionFailedListener, ResultCallback<Status> {

private final String TAG = "GeofencingService";

protectedGoogleApiClientmGoogleApiClient;

protectedHashMap<Pair<LatLng, Float>, Geofence>mGeofenceMap;

privatePendingIntentmGeofencePendingIntent;

privateintmId;

@Override

public void onCreate() {

super.onCreate();

mGeofenceMap= new HashMap<>();

mGeofencePendingIntent= null;

if(!mGeofenceMap.isEmpty()) {

buildGoogleApiClient();

}

}

protected synchronized void buildGoogleApiClient() {

mGoogleApiClient= new GoogleApiClient.Builder(this)

.addConnectionCallbacks(this)

.addOnConnectionFailedListener(this)

.addApi(LocationServices.API)

.build();

}

 

@Nullable

@Override

publicIBinderonBind(Intent intent) {

return null;

}

@Override

public void onConnected(Bundle connectionHint) {

}

@Override

public void onConnectionFailed(ConnectionResult result) {

}

@Override

public void onConnectionSuspended(intcause) {

mGoogleApiClient.connect();

}

 

}

You need to create GoogleApiClient. We need it for functional interaction with the provided Google service. It is this service that will work with geo-fence objects, that’s why we need GoogleApiClient here.

Now, add the service functionality that will add / remove geofences.

publicintonStartCommand(Intent intent, intflags, intstartId) {

Log.d(TAG, "onStartCommand");

super.onStartCommand(intent, flags, startId);

if(mGoogleApiClient== null) {

buildGoogleApiClient();

}

if(!mGoogleApiClient.isConnected()) {

mGoogleApiClient.connect();

}

doublelat = intent.getDoubleExtra(Consts.LATITUDE, 0);

doublelng = intent.getDoubleExtra(Consts.LONGITUDE, 0);

if(Consts.ADD_GEOFENCE_ACTION.equals(intent.getAction())) {

Log.d(TAG, "Try to add geofence zone");

floatrad = intent.getFloatExtra(Consts.RADIUS, 0);

mGeofenceMap.put(new Pair<LatLng, Float>(new LatLng(lat, lng), rad), new Geofence.Builder()

.setRequestId(String.valueOf(++mId))

.setCircularRegion(lat, lng, rad)

.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER| Geofence.GEOFENCE_TRANSITION_EXIT)

.setExpirationDuration(Geofence.NEVER_EXPIRE)

.build());

Log.e(TAG, String.valueOf(mGeofenceMap.size()));

addGeofences();

}

if(Consts.REMOVE_GEOFENCE_ACTION.equals(intent.getAction())) {

Log.d(TAG, "Try to remove geofence zones");

// Delete selected geofence zones

Iterator geoIterator = mGeofenceMap.entrySet().iterator();

while(geoIterator.hasNext()) {

Pair<LatLng, Float> circle = ((Map.Entry<Pair<LatLng, Float>, Geofence>)geoIterator.next()).getKey();

float[] results = new float[1];

Location.distanceBetween(circle.first.latitude, circle.first.longitude, lat, lng, results);

if(circle.second>= results[0]) {

geoIterator.remove();

}

}

if(!mGeofenceMap.isEmpty()) {

addGeofences();

} else {

// There's no way we can set empty geofences list with addGeofences() method

// thus we should remove any notifications and monitoring

removeGeofences();

}

Log.e(TAG, String.valueOf(mGeofenceMap.size()));

}

returnSTART_STICKY;

}

 

privateGeofencingRequestgetGeofencingRequest() {

GeofencingRequest.Builder builder = new GeofencingRequest.Builder();

// The INITIAL_TRIGGER_ENTER flag indicates that geofencing service should trigger a

// GEOFENCE_TRANSITION_ENTER notification when the geofence is added and if the device

// is already inside that geofence.

builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);

// Add the geofences to be monitored by geofencing service.

builder.addGeofences(new ArrayList<Geofence>(mGeofenceMap.values()));

// Return a GeofencingRequest.

returnbuilder.build();

}

/**

* Adds geofences, which sets alerts to be notified when the device enters or exits one of the

* specifiedgeofences. Handles the success or failure results returned by addGeofences().

*/

public void addGeofences() {

if(!mGoogleApiClient.isConnected()) {

Log.e(TAG, "Google API client is not connected");

return;

}

try{

LocationServices.GeofencingApi.addGeofences(mGoogleApiClient, getGeofencingRequest(), getGeofencePendingIntent()).setResultCallback(this);

} catch (SecurityExceptionsecurityException) {

Log.e(TAG, "Add geofenced error: ", securityException);

}

}

/**

* Removes geofences, which stops further notifications when the device enters or exits

* previously registered geofences.

*/

public void removeGeofences() {

if(!mGoogleApiClient.isConnected()) {

return;

}

try{

// Remove geofences.

LocationServices.GeofencingApi.removeGeofences(

mGoogleApiClient,

// This is the same pending intent that was used in addGeofences().

getGeofencePendingIntent()

).setResultCallback(this); // Result processed in onResult().

} catch (SecurityExceptionsecurityException) {

// Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission.

}

}

/**

* Gets a PendingIntent to send with the request to add or remove Geofences. Location Services

* issues the Intent inside this PendingIntent whenever a geofence transition occurs for the

* current list of geofences.

*

* @return A PendingIntent for the IntentService that handles geofence transitions.

*/

privatePendingIntentgetGeofencePendingIntent() {

// Reuse the PendingIntent if we already have it.

if(mGeofencePendingIntent!= null) {

returnmGeofencePendingIntent;

}

Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);

// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling

// addGeofences() and removeGeofences().

returnPendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

}

/**

* Runs when the result of calling addGeofences() and removeGeofences() becomes available.

* Either method can complete successfully or with an error.

*

* Since this activity implements the {@link ResultCallback} interface, we are required to

* define this method.

*

* @paramstatus The Status returned through a PendingIntent when addGeofences() or

*               removeGeofences() get called.

*/

public void onResult(Status status) {

if(status.isSuccess()) {

Log.d(TAG, "Geofence result = success");

} else {

Log.e(TAG, "Geofence result error: code = " + status.getStatusCode() + "; " + status.getStatusMessage());

}

}

It is important to remember that you cannot add an empty list of geo-fences if user has deleted the last zone. Youwillneedtodefollowmonitoringandnotification.

To process the “enter/exit” data, we need to create a service.

 

public class GeofenceTransitionsIntentServiceextends IntentService{

protected static final String TAG = "GeofenceTransitionsIntentService";

/**

* This constructor is required, and calls the super IntentService(String)

* constructor with the name for a worker thread.

*/

publicGeofenceTransitionsIntentService() {

// Use the TAG to name the worker thread.

super(TAG);

}

/**

* Handles incoming intents.

* @paramintent sent by Location Services. This Intent is provided to Location

*               Services (inside a PendingIntent) when addGeofences() is called.

*/

@Override

protected void onHandleIntent(Intent intent) {

Log.d(TAG, "New geo event was fired");

GeofencingEventgeofencingEvent = GeofencingEvent.fromIntent(intent);

if(geofencingEvent.hasError()) {

return;

}

intgeofenceTransition = geofencingEvent.getGeofenceTransition();

if(geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER||

geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

String transitionString = getTransitionString(geofenceTransition);

Location trigerredLocation = geofencingEvent.getTriggeringLocation();

sendNotification(transitionString, trigerredLocation.toString());

} else {

// Log the error.

}

}

private void sendNotification(String title, String message) {

Log.d(TAG, "Send notification: title = " + title + "; message = " + message);

Notification.BuildermBuilder =

newNotification.Builder(this)

.setPriority(Notification.PRIORITY_MAX)

.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))

.setSmallIcon(R.mipmap.ic_launcher)

.setContentTitle(title)

.setContentText(message);

Intent resultIntent = new Intent(this, MapsActivity.class);

PendingIntentresultPendingIntent =

PendingIntent.getActivity(this, 0, resultIntent, 0);

mBuilder.setContentIntent(resultPendingIntent);

NotificationManagermNotificationManager =

(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

mNotificationManager.notify(0, mBuilder.build());

}

/**

* Maps geofence transition types to their human-readable equivalents.

*

* @paramtransitionTypeA transition type constant defined in Geofence

* @return                  A String indicating the type of transition

*/

privateString getTransitionString(inttransitionType) {

switch(transitionType) {

caseGeofence.GEOFENCE_TRANSITION_ENTER:

return"Zone enter";

caseGeofence.GEOFENCE_TRANSITION_EXIT:

return"Zone leave";

default:

return"Unknown transition";

}

}

}

It will consistently accept the fired event and create Notifications, which will appear in the phone’s Notification Bar. When clicking on the notification,you will openMapsActivity. In order to avoid duplication of Activity,return to AndroidManifest.xml and add the description of our MapsActivity. Add one more string - android: launchMode = "singleTask".

 

<application>

 

            ...

<activity

android:name=".MapsActivity"

android:label="@string/title_activity_maps"

android:launchMode="singleTask">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

 

 

</application>

That's it, we have discussed the technological basics of Geo-fencing based on Android parental control app. Of course, you can dig deeper and parameterize these areas bythe type of crossing the borders and the time when they fix the intersection, but we will do it in our next article.