Geofencing API Tutorial for Android

In this geofence tutorial, you’ll learn how to use Android’s Geofencing API to build an app with custom geofences.

Version

  • Kotlin 1.2, Android 4.4, Android Studio 3

Geofences are a powerful tool in a developer’s arsenal of location tricks to use on Android. Geofences give devices the power to monitor a circular area in the world, and let the device inform you whenever it enters or exits that area.

This has enormous benefits for apps that want to leverage location as a trigger. A retail company could send a special notification to a customer near one of their stores with a special discount to tempt them in. A holiday resort could welcome its customers via its app whenever they enter the resort. With a limit of 100 geofences per device, the possibilities are nearly endless!

In this tutorial on geofencing, you’ll learn how to use Android’s geofencing API to build custom geofences in your very own app called Remind Me There. Let’s get to it!

Getting Started

The project you’ll work with, Remind Me There, is an app to create reminders based on geofences. You’ll set up custom geofences and messages; then, as you travel into a geofenced area, you’ll receive your custom message as a notification on your device.

Use the Download materials button at the top or bottom of this tutorial to download the starter project.

Once downloaded, open the starter project in Android Studio 3.2 or later.

Obtaining a Google Maps API Key

Because this app uses Google Maps, you’ll need to obtain an API key.

  1. Open Google Cloud Platform and create a new project.

    Feel free to leave the Project Name as is. You won’t need the name going forward. Leave the default value for Location. Select Create.

  2. Select APIs & Services ▸ Library from the navigation menu.

  3. Select Maps SDK for Android.
  4. Click Enable, or Manage if already enabled.

  5. Click on Credentials, Credentials in the API Manager, Create credentials and then choose API key.

  6. Copy your API key value. In your project, open debug/res/values/google_maps_api.xml and replace YOUR_KEY_HERE with the copied value.
Note: To release an app to the Google Play Store, you should create a separate project and, when you click on Create credentials▸API key, choose Restrict Key. Keys without restrictions are generic and are not limited to certain APIs. Set this key in release/res/values/google_maps_api.xml.

Building and Running the Starter Project

You can run the sample project on an Android device or emulator. For the emulator, you’ll need to make sure you have an emulator setup with Google APIs in order to show map information.

When you build and run the app, you’ll see the following screens:

Currently, tapping on the + button will let you create the reminder. However, the actual geofence is not yet being created. You’ll write code to create the geofence.

Review the project to get familiar with the files:

  • MainActivity.kt: Shows the reminders in a map.
  • NewReminderActivity.kt: This activity contains the code to create a new reminder, providing latitude/longitude, radius and a message to be shown in the notification.
  • Reminder.kt: A model class used to store the reminder. It has an id, a latitude/longitude, a radius and a message. You’ll use this later to build a geofence.
  • ReminderRepository.kt: This file saves the reminders that you create that will be shown in the MainActivity. You’ll also add code to create the geofences, here.
  • BaseActivity.kt: This is the parent activity of MainActivity and NewReminderActivity. It provides common access to the ReminderRepository.
  • ReminderApp.kt: When the app is launched, it creates the ReminderRepository.
  • Utils.kt: Here, you’ll find a few generic functions useful for the app – for example, a function to hide the keyboard, show a notification, etc.

Using geofences requires the play-services-location library added to your project. To do that, open the build.gradle file for the app module and add the following dependency:

implementation 'com.google.android.gms:play-services-location:15.0.1'

Your app also requires the device location to know when to trigger a reminder. You do this by requesting the ACCESS_FINE_LOCATION permission.

This permission is set up in AndroidManifest.xml. It’s classified as a dangerous permission — that is, a permission that could potentially affect the user’s privacy. For Android 6 and later, it’s necessary to check and request this permission during runtime.

Because the starter project shows a map and the user location, the permission and runtime check were both already set up for you in AndroidMainfest.xml and MainActivity.kt.

Creating a Geofence

To manipulate geofences, you need to use the GeofencingClient, so open ReminderRepository.kt and add a new property:

private val geofencingClient = LocationServices.getGeofencingClient(context)

You’ll also need to import com.google.android.gms.location.LocationServices.

Note: Going forward, when you receive an import warning, you can import with your cursor over the warning by pressing option+return on a Mac or Alt+Enter on a PC.

If you look at the add() method, you’ll see that, currently, it just saves the reminder to SharedPreferences. First, you’ll change this behavior to create the geofence, using the GeofencingClient. Then, only if the geofence creation is successful, you’ll save the reminder to SharedPreferences.

Change the add() method to the following:

fun add(reminder: Reminder,
        success: () -> Unit,
        failure: (error: String) -> Unit) {
  // 1
  val geofence = buildGeofence(reminder)
  if (geofence != null
      && ContextCompat.checkSelfPermission(
          context,
          Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
    // 2
    geofencingClient
        .addGeofences(buildGeofencingRequest(geofence), geofencePendingIntent)
        // 3
        .addOnSuccessListener {
          saveAll(getAll() + reminder)
          success()
        }
        // 4
        .addOnFailureListener {
          failure("Error")
        }
  }
}

Use the hotkeys you learned above to import ContextCompat and Manifest. Don’t worry about the other errors. Those are methods that you’ll build, soon. Take a minute to have a look at what you have just added:

  1. You create the geofence model using the data from the reminder.
  2. You use the GeofencingClient to add the geofence that you’ve just built with a geofencing request and a pending intent. More on this later.
  3. If the geofence is added successfully, you save the reminder and call the success argument.
  4. If there’s an error, you call the failure argument (without saving the reminder).

Building the Geofence

A geofence is defined by a latitude, a longitude and a radius. It’s a circular area at a specific location that an app can use to trigger particular behaviors when a device enters, exits or stays for a certain amount of time within geofence boundaries.

You’re going to build a geofence that behaves this way right now. To build your geofence, add the following method to ReminderRepository.kt:

private fun buildGeofence(reminder: Reminder): Geofence? {
  val latitude = reminder.latLng?.latitude
  val longitude = reminder.latLng?.longitude
  val radius = reminder.radius

  if (latitude != null && longitude != null && radius != null) {
    return Geofence.Builder()
        // 1
        .setRequestId(reminder.id)
        // 2
        .setCircularRegion(
            latitude,
            longitude,
            radius.toFloat()
        )
        // 3
        .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
        // 4
        .setExpirationDuration(Geofence.NEVER_EXPIRE)
        .build()
  }

  return null
}

Take a look at the Geofence.Builder code in your new method:

  1. RequestId: This id uniquely identifies the geofence within your app. You obtain this from the reminder model.
  2. latitude, longitude and radius: You also get these from the reminder model at the top of the new method.
  3. TransitionType: To trigger an event when the user enters the geofence, use GEOFENCE_TRANSITION_ENTER. Other options are GEOFENCE_TRANSITION_EXIT and GEOFENCE_TRANSITION_DWELL. You can learn more about transition options here.
  4. ExpirationDuration: Use NEVER_EXPIRE so this geofence will exist until the user removes it. The other option is to enter a duration (ms) after which the geofence will expire.

Building the Geofence Request

Add this method in ReminderRepository.kt to build the request:

private fun buildGeofencingRequest(geofence: Geofence): GeofencingRequest {
  return GeofencingRequest.Builder()
      .setInitialTrigger(0)
      .addGeofences(listOf(geofence))
      .build()
}

You use setInitialTrigger() to set the desired behavior at the moment the geofences are added. Setting the value to 0 indicates that you don’t want to trigger a GEOFENCE_TRANSITION_ENTER event if the device is already inside the geofence that you’ve just added.

You also use addGeofences() to add your geofence to the request.

Building the Geofence Pending Intent

Next, add this lazy property:

private val geofencePendingIntent: PendingIntent by lazy {
  val intent = Intent(context, GeofenceTransitionsIntentService::class.java)
  PendingIntent.getService(
      context,
      0,
      intent,
      PendingIntent.FLAG_UPDATE_CURRENT)
}

Here you’re creating a PendingIntent. A PendingIntent is similar to a normal Intent, except rather than happening immediately it will happen sometime in the future. Think of it as a promise to a friend that you’ll do something for them later.

In this case, when the GEOFENCE_TRANSITION_ENTER event is triggered, it’ll launch GeofenceTransitionsIntentService to handle the event. This service will be explained later.

Checking for Errors

There might be times where it’s not possible to add a geofence — when you are trying to add the 101st geofence, for example. Review the error strings below to see some of the more common issues that can arise when working with geofences. It is good practice to check for these errors and let the user know (or at least log the error).

Open strings.xml and add the following strings:

<string name="geofence_unknown_error">
  Unknown error: the Geofence service is not available now.
</string>
<string name="geofence_not_available">
  Geofence service is not available now. Go to Settings>Location>Mode and choose High accuracy.
</string>
<string name="geofence_too_many_geofences">
  Your app has registered too many geofences.
</string>
<string name="geofence_too_many_pending_intents">
  You have provided too many PendingIntents to the addGeofences() call.
</string>

Now, create a file called GeofenceErrorMessages.kt with the following content:

package com.android.raywenderlich.remindmethere

import android.content.Context
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.location.GeofenceStatusCodes

object GeofenceErrorMessages {
  fun getErrorString(context: Context, e: Exception): String {
    return if (e is ApiException) {
      getErrorString(context, e.statusCode)
    } else {
      context.resources.getString(R.string.geofence_unknown_error)
    }
  }

  fun getErrorString(context: Context, errorCode: Int): String {
    val resources = context.resources
    return when (errorCode) {
      GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE ->
        resources.getString(R.string.geofence_not_available)

      GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES ->
        resources.getString(R.string.geofence_too_many_geofences)

      GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS ->
        resources.getString(R.string.geofence_too_many_pending_intents)

      else -> resources.getString(R.string.geofence_unknown_error)
    }
  }
}

Here, you’ve added a Kotlin object with a method that takes in an error code and returns a more user friendly string representation of the error.

Open ReminderRepository.kt again and change the add() method’s failure listener to this:

// 4
.addOnFailureListener {
  failure(GeofenceErrorMessages.getErrorString(context, it))
}

Now, you are returning the error strings you created earlier.

Handling Transitions

Previously, you created a PendingIntent and set a class to handle the GEOFENCE_TRANSITION_ENTER event. Remember that promise you made earlier? Now you’re going to follow up on that promise by writing a class to handle it.

Creating the Transitions Intent Service

Create the file GeofenceTransitionsIntentService.kt and add the following code:

package com.android.raywenderlich.remindmethere

import android.app.IntentService
import android.content.Intent
import android.util.Log
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofencingEvent

class GeofenceTransitionsIntentService : IntentService("GeoTrIntentService") {

  companion object {
    private const val LOG_TAG = "GeoTrIntentService"
  }

  override fun onHandleIntent(intent: Intent?) {
    // 1
    val geofencingEvent = GeofencingEvent.fromIntent(intent)
    // 2
    if (geofencingEvent.hasError()) {
      val errorMessage = GeofenceErrorMessages.getErrorString(this,
          geofencingEvent.errorCode)
      Log.e(LOG_TAG, errorMessage)
      return
    }
    // 3
    handleEvent(geofencingEvent)
  }
}
  1. You obtain the GeofencingEvent object using the intent.
  2. If there’s an error, you log it.
  3. Otherwise, you handle the event.

Add the handleEvent() method to your new class:

private fun handleEvent(event: GeofencingEvent) {
  // 1
  if (event.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
    // 2
    val reminder = getFirstReminder(event.triggeringGeofences)
    val message = reminder?.message
    val latLng = reminder?.latLng
    if (message != null && latLng != null) {
      // 3
      sendNotification(this, message, latLng)
    }
  }
}
  1. You first check if the transition is related to entering a geofence.
  2. If the user creates overlapping geofences, there may be multiple triggering events, so, here, you pick only the first reminder object.
  3. You then show the notification using the message from the reminder.

Now add the getFirstReminder() method code:

private fun getFirstReminder(triggeringGeofences: List<Geofence>): Reminder? {
  val firstGeofence = triggeringGeofences[0]
  return (application as ReminderApp).getRepository().get(firstGeofence.requestId)
}

Here, you get the first triggered geofence and use its requestId to find the associated reminder object from the repository.

Finally, all services must be declared in the AndroidManifest.xml, so add the service just before the application closing tag:

<service android:name=".GeofenceTransitionsIntentService"/>

Testing the Geofences

I bet you are anxious to test what you’ve coded so far!

But, before that, it’s important to mention that, during development, it’s rather difficult to test this. Just imagine creating a few geofences separated by hundreds of feet or miles. You would need to walk or drive a lot! To address this, you can mock your location.

Mocking the Location

For quick and easy testing, you can use an Android emulator and modify its location coordinates. With an emulator running, open the extended controls by clicking the button at the bottom of the menu:

Modify the coordinates to something else, for example, a latitude and longitude near your current actual location, and press SEND:

After a few seconds, you’ll see the user location updated in the map:

Note: You can also mock the location on a real device. There are several apps for this, such as Fake GPS location. To enable mocking, you need to enable Allow mock locations in Settings▸Developer options. For devices with API 23 and greater, the option has changed to Select mock location app.
Mocking locations on older SDKs Mocking locations on newer SDKs

Creating a Reminder and Getting the Notification

Now that you know how to easily mock your location:

  1. Build and run the app.
  2. Create a reminder on your current location.
  3. Modify your coordinates so that your current location is outside of the geofence.
  4. Now, modify your coordinates so that your current location is inside the geofence.
  5. Wait a few seconds and you’ll get a notification with the message you set.
Note: If you’re developing on an emulator, you may get an error message when attempting to add a geofence. If you do, go to Settings▸Security&Location▸Location▸Mode and choose High accuracy.

Congratulations, you’ve added your first geofence reminder!

Removing a Geofence

If you tap on a reminder, you’ll see that you get the option to remove it. Currently, this action just removes the reminder from SharedPreferences. Now, you’ll change the remove() method so it also removes the geofence.

Open ReminderRepository.kt and modify the remove() method:

fun remove(reminder: Reminder,
           success: () -> Unit,
           failure: (error: String) -> Unit) {
  geofencingClient
      .removeGeofences(listOf(reminder.id))
      .addOnSuccessListener {
        saveAll(getAll() - reminder)
        success()
      }
      .addOnFailureListener {
        failure(GeofenceErrorMessages.getErrorString(context, it))
      }
}

If you compare this with the add() method you created earlier, you’ll notice that they’re very similar. Here, you’re just removing the geofence and reminder instead of adding them — easy!

Best Practices

When working in a mobile environment, it is always important to consider issues such as battery drain, bandwidth and connectivity. Here are a few things to keep in mind as you develop your awesome geofence-based apps:

Responsiveness

To save battery, you can use the setNotificationResponsiveness() method to decrease how frequently the app checks for an event. For example, if you set it to 180,000 milliseconds, the callback will be called every three minutes to see if a device has entered/exited a geofence. The default is zero.

Radius

Android recommends a minimum geofence radius of 100–150m. When deciding on a radius, think about the use cases for your app. Will it primarily be used while walking indoors? Driving in rural areas? Wi-Fi availability makes a big difference for device location accuracy; if there is poor or no Wi-Fi in a specific region, try using a larger radius.

Dwell

Another way to reduce your app’s power usage is to use GEOFENCE_TRANSITION_DWELL and setLoiteringDelay() when building the geofence. For example, setLoiteringDelay(360000) means that the user will have to stay within the geofence for six minutes to trigger an event. Doing this could also improve the user experience by reducing unintended notifications if a device’s location reading is not stable or if the user is traveling along the edge of a geofence.

Re-registering Your Geofence

Only re-register your geofences under the following circumstances:

  • Device reboot: You can register a BroadcastReceiver for BOOT_COMPLETED.
  • App is uninstalled and re-installed.
  • App data is cleared.
  • Google Play services data is cleared.
  • GEOFENCE_NOT_AVAILABLE error: You may get this error in the GeofenceTransitionsIntentService. Usually this is because the Network Location Provider is disabled.

Wi-Fi and Location Accuracy

  • Latency for triggering a geofence transition is usually two minutes on average. However, if the device has been stationary, the latency may increase to up to six minutes.
  • On most devices, the geofence service uses the Network Location Provider. This provider determines location based on cell towers and Wi-Fi access points. It works indoors and uses much less power than GPS.
  • If there’s no reliable connection to cell towers or Wi-Fi access points, geofence transitions might not trigger.
  • If Wi-Fi is disabled, the geofence service may not trigger the transitions. For Android 4.3 and later, there’s a “Wi-Fi scan only mode” that disables Wi-Fi but not the network location.

Where to Go From Here?

Congratulations! You’ve just learned the basics of geofences on Android.

You can download the final version of the project using the Download materials button at the top or bottom of this tutorial. Remember to add your Google Maps API key to the final project before you run it.

Here are some great references to learn more about the subject:

  • Official Docs: You can find them here.
  • Fence API: To be aware also of the context (walking, driving, headphones plugged in, etc.), check out more here.
  • The background processing guide will help you choose the best way for your app to perform operations in the background.
  • There’s a Google sample that you might want to check out.

Feel free to share your feedback, findings or ask any questions in the comments below or in the forums. I hope you enjoyed this tutorial on geofences!

Contributors

Comments