Android Location Proximity Alert Using Google Maps & Geofencing Example

This post explains how to create in android apps the feature that allows users of the app to select a location on the map and set location or proximity alert for it so that they will get notified when they enter to, exist from or dwell at the specified locations. The said feature can be developed using Google maps and geofencing API.

To know how to use Google play services APIs in general, Google maps android API and location API in particular, you can see Android Google maps API and Android location updates using location API tutorials.

If you want to learn android and its latest features with examples, please visit android tutorials.

Geofencing or Location Proximity Alert

Here is how geofencing works, first geofencing requests need to be added to the location service using geofencing api. Once a geofence has been added, location service will trigger events when there is a location transition meaning when device enters into, exists from or dwells at the target locations.

Each geofencing identified by request id is configured using Geofence object which tells the location service when to trigger location transition events. Circular area around target location can be defined by setting radius. Expiration duration can be defined, after which the geofence will be removed from the location service. Interested location transitions can be added to Geofence object so that location service triggers only the chosen location transition events.

When geofencing is added to location service, pending intent containing the target service is passed to location service. Location service fires the pending intent when location transition events occur. You need to define a service which handles the pending intent and do further processing with the location transition events.

Add Libraries

After setting up your project to make it ready to use Google play service APIs by following steps listed out in Android Google maps API tutorial, you need to make sure that you add Google maps android api and location api libraries to your project’s gradle.build file as shown below.

dependencies { . . . implementation 'com.google.android.gms:play-services-maps:11.6.0' implementation 'com.google.android.gms:play-services-location:11.6.0' }

Location Proximity Alert or Geofencing Example

Adding Location Proximity Alert or Geofencing Request

Location proximity alert feature example app displays user’s current location on the map. User can scroll the map to pick interested locations. On clicking the map at the target location, location’s latitude and longitude will be passed to GoogleMap.OnMapClickListener. In the listener, geofencing or location proximity alert will be added to location service using Geofence API.

To add Geofencing, first you need to define Geofence object. You can set location radius, interested location transition types such as GEOFENCE_TRANSITION_DWELL, GEOFENCE_TRANSITION_ENTER and GEOFENCE_TRANSITION_EXIT, Geofence life duration and loitering delay for dwell events by calling setCircularRegion, setTransitionTypes, setExpirationDuration and setLoiteringDelay methods respectively on Geofence.Builder object.

Then create GeofencingRequest containing one or more Geofence objects using GeofencingRequest.Builder. Then create pending intent which will be fired by location service when location transition event occurs.

To add GeofencingRequest to location service, you need to call addGeofences method on GeofencingClient object which can be obtained by calling getGeofencingClient method on LocationServices. You need to pass GeofencingRequest and pending intent object as parameters to addGeofences method which returns Task object to which you can add OnCompleteListener to know whether GeofencingRequest is successful or not.

You can see all the steps in the activity code below.

Removing Location Proximity Alert or Geofencing Request

The example uses menu item to trigger remove all geofencing requests. To remove all geofencing requests, you need to just pass pending intent, which is used to add geofences, to the removeGeofences method of GeofencingClient.

You can make location service automatically remove geofencing request after certain time duration expires which you can set on Geofence.Builder object by calling setExpirationDuration method while adding geofencing request to location service.

Processing Location Proximity Alerts or Geofencing Events

As mentioned before, location service fires pending intent which is passed to it when geofencing request is added. In the intent service which handles the pending intent, information about the event can be retrieved from GeofencingEvent object which can be obtained by calling fromIntent method on GeofencingEvent passing the intent to it.

You can get location transition type and list of Geofence objects from GeofencingEvent object and use it for further processing. In the example app, since concatenated latitude and longitude string is used as request id for Geofence, the request id will be used to get location name using Geocoder in the intent service. Thus obtained location name and location transition event type information is used to create a notification which will be notified to user using notification service.

Activity

public class LocationAlertActivity extends AppCompatActivity implements OnMapReadyCallback { private static final int LOC_PERM_REQ_CODE = 1; //meters private static final int GEOFENCE_RADIUS = 500; //in milli seconds private static final int GEOFENCE_EXPIRATION = 6000; private GoogleMap mMap; private GeofencingClient geofencingClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.location_alert_layout); Toolbar tb = findViewById(R.id.toolbar); setSupportActionBar(tb); tb.setSubtitle("Location Alert"); SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.g_map); mapFragment.getMapAsync(this); geofencingClient = LocationServices.getGeofencingClient(this); } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; mMap.getUiSettings().setZoomControlsEnabled(true); mMap.setMinZoomPreference(15); showCurrentLocationOnMap(); mMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() { @Override public void onMapClick(LatLng latLng) { addLocationAlert(latLng.latitude, latLng.longitude); } }); } @SuppressLint("MissingPermission") private void showCurrentLocationOnMap() { if (isLocationAccessPermitted()) { requestLocationAccessPermission(); } else if (mMap != null) { mMap.setMyLocationEnabled(true); } } private boolean isLocationAccessPermitted(){ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return true; }else{ return false; } } private void requestLocationAccessPermission(){ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, LOC_PERM_REQ_CODE); } @SuppressLint("MissingPermission") private void addLocationAlert(double lat, double lng){ if (isLocationAccessPermitted()) { requestLocationAccessPermission(); } else { String key = ""+lat+"-"+lng; Geofence geofence = getGeofence(lat, lng, key); geofencingClient.addGeofences(getGeofencingRequest(geofence), getGeofencePendingIntent()) .addOnCompleteListener(new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if (task.isSuccessful()) { Toast.makeText(LocationAlertActivity.this, "Location alter has been added", Toast.LENGTH_SHORT).show(); }else{ Toast.makeText(LocationAlertActivity.this, "Location alter could not be added", Toast.LENGTH_SHORT).show(); } } }); } } private void removeLocationAlert(){ if (isLocationAccessPermitted()) { requestLocationAccessPermission(); } else { geofencingClient.removeGeofences(getGeofencePendingIntent()) .addOnCompleteListener(new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if (task.isSuccessful()) { Toast.makeText(LocationAlertActivity.this, "Location alters have been removed", Toast.LENGTH_SHORT).show(); }else{ Toast.makeText(LocationAlertActivity.this, "Location alters could not be removed", Toast.LENGTH_SHORT).show(); } } }); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case LOC_PERM_REQ_CODE: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { showCurrentLocationOnMap(); Toast.makeText(LocationAlertActivity.this, "Location access permission granted, you try " + "add or remove location allerts", Toast.LENGTH_SHORT).show(); } return; } } } private PendingIntent getGeofencePendingIntent() { Intent intent = new Intent(this, LocationAlertIntentService.class); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private GeofencingRequest getGeofencingRequest(Geofence geofence) { GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); builder.addGeofence(geofence); return builder.build(); } private Geofence getGeofence(double lat, double lang, String key) { return new Geofence.Builder() .setRequestId(key) .setCircularRegion(lat, lang, GEOFENCE_RADIUS) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_DWELL) .setLoiteringDelay(10000) .build(); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.remove_loc_alert: removeLocationAlert(); return true; default: return super.onOptionsItemSelected(item); } } }

Intent Service

public class LocationAlertIntentService extends IntentService { private static final String IDENTIFIER = "LocationAlertIS"; public LocationAlertIntentService() { super(IDENTIFIER); } @Override protected void onHandleIntent(Intent intent) { GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); if (geofencingEvent.hasError()) { Log.e(IDENTIFIER, "" + getErrorString(geofencingEvent.getErrorCode())); return; } Log.i(IDENTIFIER, geofencingEvent.toString()); int geofenceTransition = geofencingEvent.getGeofenceTransition(); if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) { List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences(); String transitionDetails = getGeofenceTransitionInfo( triggeringGeofences); String transitionType = getTransitionString(geofenceTransition); notifyLocationAlert(transitionType, transitionDetails); } } private String getGeofenceTransitionInfo(List<Geofence> triggeringGeofences) { ArrayList<String> locationNames = new ArrayList<>(); for (Geofence geofence : triggeringGeofences) { locationNames.add(getLocationName(geofence.getRequestId())); } String triggeringLocationsString = TextUtils.join(", ", locationNames); return triggeringLocationsString; } private String getLocationName(String key) { String[] strs = key.split("-"); String locationName = null; if (strs != null && strs.length == 2) { double lat = Double.parseDouble(strs[0]); double lng = Double.parseDouble(strs[1]); locationName = getLocationNameGeocoder(lat, lng); } if (locationName != null) { return locationName; } else { return key; } } private String getLocationNameGeocoder(double lat, double lng) { Geocoder geocoder = new Geocoder(this, Locale.getDefault()); List<Address> addresses = null; try { addresses = geocoder.getFromLocation(lat, lng, 1); } catch (Exception ioException) { Log.e("", "Error in getting location name for the location"); } if (addresses == null || addresses.size() == 0) { Log.d("", "no location name"); return null; } else { Address address = addresses.get(0); ArrayList<String> addressInfo = new ArrayList<>(); for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) { addressInfo.add(address.getAddressLine(i)); } return TextUtils.join(System.getProperty("line.separator"), addressInfo); } } private String getErrorString(int errorCode) { switch (errorCode) { case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: return "Geofence not available"; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: return "geofence too many_geofences"; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS: return "geofence too many pending_intents"; default: return "geofence error"; } } private String getTransitionString(int transitionType) { switch (transitionType) { case Geofence.GEOFENCE_TRANSITION_ENTER: return "location entered"; case Geofence.GEOFENCE_TRANSITION_EXIT: return "location exited"; case Geofence.GEOFENCE_TRANSITION_DWELL: return "dwell at location"; default: return "location transition"; } } private void notifyLocationAlert(String locTransitionType, String locationDetails) { String CHANNEL_ID = "Zoftino"; NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.zoftino) .setContentTitle(locTransitionType) .setContentText(locationDetails); builder.setAutoCancel(true); NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.notify(0, builder.build()); } }

Activity Layout

<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".LocationAlertActivity"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/> <fragment android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/g_map" android:name="com.google.android.gms.maps.SupportMapFragment" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/toolbar"/> </android.support.constraint.ConstraintLayout>

Menu

<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/remove_loc_alert" android:icon="@drawable/alerts_off" android:title="Remove Location Alerts" app:showAsAction="always"></item> </menu>

Android Manifest