1. Welcome
Introduction
Welcome to the Advanced Android in Kotlin lesson on geofences!
The geofencing API allows you to define perimeters, also referred to as geofences, which surround areas of interest. Your app gets a notification when the device crosses a geofence, which allows you to provide a relevant experience when users are inside the "fenced" area.
For example, an airline app can define a geofence around an airport when a flight reservation is near boarding time. When the device crosses the geofence, the app can send a notification that takes users to an activity that allows them to get their boarding pass.
The Geofencing API uses device sensors to accurately detect the location of the device in a battery-efficient way. The device can be in one of three states, or transition types, related to the geofence.
Geofence transition types:
Enter: Indicates that the device has entered the geofence(s).
Dwell: Indicates that the device has entered and is dwelling inside the geofence(s) for a given period of time.
Exit: Indicates that the device has exited the geofence(s).
Geofencing has many applications including:
- Reminder apps, where you can get a reminder when you close in on a destination. For example, you get a reminder to pick up a prescription when you get close to your pharmacy.
- Child location services, where a parent can be notified if a child leaves an area designated by a geofence.
- Attendance recording, where an employer can know when their employees arrive by the time they enter a geofence.
- A treasure hunt app that uses geofences to mark the place where a treasure is hidden. When you enter that perimeter, you will be notified that you have won. - This is the app you will be building in this codelab!
The image below shows geofence locations denoted by markers and the radiuses around them.
What you'll need
- The latest version of Android Studio.
- A minimum of SDK API 29 on your device or emulator. (The app should still work on lower API levels, but may look different.)
What you should already know
- Kotlin, as taught in the Kotlin Bootcamp
- Basic Android development, as taught in Developing Android Apps with Kotlin
What you'll learn
- How to check user permissions.
- How to check device settings.
- How to add Broadcast Receivers.
- How to add geofences.
- How to handle geofence transitions.
- How to mock locations in the emulator.
2. App overview
The app you will create in this codelab is a Treasure Hunt game. This app is a scavenger hunt that gives the user a clue, and when the user enters the correct location, the app will prompt them with the next clue, or a win screen if they have finished the hunt.
The screenshots below show a clue and the win screen.
Note that the current game code has San Francisco locations hardcoded, but you will learn how to customize the game by creating your own geofences to lead people to places in your area.
3. Getting Started
To get started, download the code:
Alternatively, you can clone the GitHub repository for the code and switch to the starter-code
branch:
$ git clone https://github.com/googlecodelabs/android-kotlin-geo-fences
4. Task: Familiarizing yourself with the code
Step 1: Run the starter app
- Run the starter app on an emulator or on your own device. You should see a splash screen with an Android holding a treasure map.
Step 2: Familiarize yourself with the code
The starter app contains code to help you get started and save you some work. It contains assets, layouts, an activity, a view model, and a broadcast receiver that you will complete during this lesson.
Open the following important classes provided for you, and familiarize yourself with the code:
HuntMainActivity.kt
is the primary class you will be working in. This class contains skeleton code for functions that handle permissions, and for adding and removing geofences.GeofenceViewModel.kt
is theViewModel
associated withHuntMainActivity.kt
. This class handles theGeofenceIndex
LiveData and determines which hint should be shown on the screen.NotificationUtils.kt
: When you enter a geofence, a notification pops up. This class creates and styles that notification.activity_main.xml
currently displays an image of an Android, but you will implement it to display a hint to lead your players to the next location.GeofenceBroadcastReceiver.kt
contains skeleton code for theonReceive()
method of theBroadcastReceiver
. You will update theonReceive()
method in this codelab.
5. Task: Requesting permissions
The first thing your app needs to do is get location permissions from the user. This involves the following high-level steps, and will be the same for any app you create that needs permissions.
- Add the permissions to the Android manifest.
- Create a method that checks for permissions.
- Request those permissions by calling that method.
- Handle the result of asking the user for the permissions.
Step 1: Add permissions to the AndroidManifest
The Geofencing API requires that location be shared at all times. If you are on Android version Q or later, you will need to specifically ask the user for this permission.
- Open
AndroidManifest.xml
. - Add permissions for
ACCESS_FINE_LOCATION
andACCESS_BACKGROUND_LOCATION
above theapplication
tag.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Step 2: Check if the device is running Android Q (API 29) or later
Check whether the device is running Android Q or later. For devices running Android Q (API 29) or later, you will have to ask for an additional background location permission.
- Open
HuntMainActivity.kt
. - Above the
onCreate()
method, add a member variable a calledrunningQOrLater
. This will check what API the device is running.
private val runningQOrLater = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q
Step 3: Create a method to check for permissions
In your app, you need to check if permissions have been granted, and if not, ask for them.
- In
HuntMainActivity
, replace the code in theforegroundAndBackgroundLocationPermissionApproved()
method with the code below, which is explained afterwards.
@TargetApi(29)
private fun foregroundAndBackgroundLocationPermissionApproved(): Boolean {
val foregroundLocationApproved = (
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION))
val backgroundPermissionApproved =
if (runningQOrLater) {
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
true
}
return foregroundLocationApproved && backgroundPermissionApproved
}
- First, you must check if the
ACCESS_FINE_LOCATION
permission has been granted.
val foregroundLocationApproved = (
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION))
- If the device is running Android Q (API 29) or higher, check that the
ACCESS_BACKGROUND_LOCATION
permission has been granted. Returntrue
if the device is running a version lower than Q, where you don't need a permission to access location in the background.
val backgroundPermissionApproved =
if (runningQOrLater) {
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
true
}
- Return
true
if the permissions have been granted, andfalse
if not.
return foregroundLocationApproved && backgroundPermissionApproved
Step 4: Request permissions
- Copy the following code into the
requestForegroundAndBackgroundLocationPermissions()
method. This is where you ask the user to grant location permissions. Each step is explained in the bullet points below.
@TargetApi(29 )
private fun requestForegroundAndBackgroundLocationPermissions() {
if (foregroundAndBackgroundLocationPermissionApproved())
return
var permissionsArray = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
val resultCode = when {
runningQOrLater -> {
permissionsArray += Manifest.permission.ACCESS_BACKGROUND_LOCATION
REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
}
else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
}
Log.d(TAG, "Request foreground only location permission")
ActivityCompat.requestPermissions(
this@HuntMainActivity,
permissionsArray,
resultCode
)
}
- If the permissions have already been granted, you don't need to ask again, so you can
return
out of the method.
if (foregroundAndBackgroundLocationPermissionApproved())
return
- The
permissionsArray
contains the permissions to be requested. Initially, addACCESS_FINE_LOCATION
since that is needed by all API levels.
var permissionsArray = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
- Next, you need a
resultCode
. This code is different if the device is running Q (API 29) or later and determines whether you need to check for one permission (fine location) or multiple permissions (fine and background location) when the user returns from the permission request screen. - Add a
when
statement to check the version running, and assignresultCode
toREQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
if the device is running Q (API 29) or later, andREQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
, if not.
val resultCode = when {
runningQOrLater -> {
permissionsArray += Manifest.permission.ACCESS_BACKGROUND_LOCATION
REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
}
else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
}
- Finally, request permissions passing in the current activity, the permissions array, and the result code.
ActivityCompat.requestPermissions(
this@HuntMainActivity,
permissionsArray,
resultCode
)
Step 5: Handle permissions
Once the user responds to the permissions request, you need to handle their response in onRequestPermissionsResult()
.
- Copy this code into the
onRequestPermissionsResult()
method.
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
Log.d(TAG, "onRequestPermissionResult")
if (
grantResults.isEmpty() ||
grantResults[LOCATION_PERMISSION_INDEX] == PackageManager.PERMISSION_DENIED ||
(requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE &&
grantResults[BACKGROUND_LOCATION_PERMISSION_INDEX] ==
PackageManager.PERMISSION_DENIED))
{
Snackbar.make(
binding.activityMapsMain,
R.string.permission_denied_explanation,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.settings) {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.show()
} else {
checkDeviceLocationSettingsAndStartGeofence()
}
}
- Permissions can be denied in a few ways:
- If the
grantResults
array is empty, then the interaction was interrupted and the permission request was cancelled. - If the
grantResults
array's value at theLOCATION_PERMISSION_INDEX
has aPERMISSION_DENIED
, it means that the user denied foreground permissions. - If the request code equals
REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
and theBACKGROUND_LOCATION_PERMISSION_INDEX
is denied, it means that the device is running Q (API 29) or above and that background permissions were denied.
if (grantResults.isEmpty() ||
grantResults[LOCATION_PERMISSION_INDEX] == PackageManager.PERMISSION_DENIED ||
(requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE &&
grantResults[BACKGROUND_LOCATION_PERMISSION_INDEX] ==
PackageManager.PERMISSION_DENIED))
- This app has very little use if permissions are not granted, so present a snackbar explaining to the user that the app needs location permissions in order for them to be able to play.
Snackbar.make(
binding.activityMapsMain,
R.string.permission_denied_explanation,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.settings) {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.show()
- Otherwise, permissions have been granted and you can call the
checkDeviceLocationSettingsAndStartGeofence()
method.
else {
checkDeviceLocationSettingsAndStartGeofence()
}
- Run your app! You should see a pop-up prompting you to grant permissions. Choose Allow all the time, or Allow if you are running an API lower than 29.
6. Task: Checking device location
Your code now asks the user to give permissions.
However, if the user's device location is turned off, then that permission won't mean anything.
The next thing to check is if the device's location is on. In this step, you will add code to check that a user has their device location enabled, and if not, display an activity where they can turn it on using a location request.
- Copy this code into the
checkDeviceLocationSettingsAndStartGeofence()
method inHuntMainActivity.kt
. The steps are explained in the bullet points below.
private fun checkDeviceLocationSettingsAndStartGeofence(resolve:Boolean = true) {
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
val settingsClient = LocationServices.getSettingsClient(this)
val locationSettingsResponseTask =
settingsClient.checkLocationSettings(builder.build())
locationSettingsResponseTask.addOnFailureListener { exception ->
if (exception is ResolvableApiException && resolve){
try {
exception.startResolutionForResult(this@HuntMainActivity,
REQUEST_TURN_DEVICE_LOCATION_ON)
} catch (sendEx: IntentSender.SendIntentException) {
Log.d(TAG, "Error getting location settings resolution: " + sendEx.message)
}
} else {
Snackbar.make(
binding.activityMapsMain,
R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
).setAction(android.R.string.ok) {
checkDeviceLocationSettingsAndStartGeofence()
}.show()
}
}
locationSettingsResponseTask.addOnCompleteListener {
if ( it.isSuccessful ) {
addGeofenceForClue()
}
}
}
- First, create a
LocationRequest
and use it with aLocationSettingsRequest
Builder
.
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
- Next, use
LocationServices
to get theSettingsClient
. Create aval
calledlocationSettingsResponseTask
and use it to check the location settings.
val settingsClient = LocationServices.getSettingsClient(this)
val locationSettingsResponseTask =
settingsClient.checkLocationSettings(builder.build())
- Since the case you are most interested in is finding out if the location settings are not satisfied, add an
onFailureListener()
to thelocationSettingsResponseTask
.
locationSettingsResponseTask.addOnFailureListener { exception ->
}
- Check if the exception is of type
ResolvableApiException
, and if so, try calling thestartResolutionForResult()
method in order to prompt the user to turn on device location.
if (exception is ResolvableApiException && resolve){
try {
exception.startResolutionForResult(this@HuntMainActivity,
REQUEST_TURN_DEVICE_LOCATION_ON)
}
- If calling
startResolutionForResult
enters the catch block, print a log.
catch (sendEx: IntentSender.SendIntentException) {
Log.d(TAG, "Error getting location settings resolution: " + sendEx.message)
}
- If the exception is not of type
ResolvableApiException
, present a snackbar that alerts the user that location needs to be enabled to play the treasure hunt.
else {
Snackbar.make(
binding.activityMapsMain,
R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
).setAction(android.R.string.ok) {
checkDeviceLocationSettingsAndStartGeofence()
}.show()
}
- If the
locationSettingsResponseTask
does complete, check that it is successful, and add a geofence for a clue.
locationSettingsResponseTask.addOnCompleteListener {
if ( it.isSuccessful ) {
addGeofenceForClue()
}
}
- In
onActivityResult()
, replace the existing code with the code below. After the user chooses whether to accept or deny device location permissions, this checks if the user has chosen to accept the permissions. If not, ask again.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_TURN_DEVICE_LOCATION_ON) {
checkDeviceLocationSettingsAndStartGeofence(false)
}
}
- To test this, turn off your device location and run the app. You should see a pop-up as shown below. Press OK.
7. Task: Adding geofences
Now that you are done checking that the appropriate permissions are granted, add some geofences!
Step 1: Create a Pending Intent
You need a way to handle geofence transitions, which is done with a PendingIntent
. A PendingIntent
is a description of an Intent, and a target action to perform with it. You will create a pending intent for a BroadcastReceiver
to handle the geofence transitions.
- In
HuntMainActivity.kt
, aboveonCreate()
, add a private variable calledgeofencePendingIntent
of typePendingIntent
to handle the geofence transitions. ConnectgeofencePendingIntent
to theGeofenceTransitionsBroadcastReceiver
.
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
intent.action = ACTION_GEOFENCE_EVENT
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
Step 2: Add a Geofencing Client
A GeofencingClient
is the main entry point for interacting with the geofencing APIs.
- In the
onCreate()
method, instantiate thegeofencingClient
which is already declared in the starter code.
geofencingClient = LocationServices.getGeofencingClient(this)
Step 3: Add geofences
- Copy this code into the
addGeofenceForClue()
method. Each step is explained in the bullet points below..
private fun addGeofenceForClue() {
if (viewModel.geofenceIsActive()) return
val currentGeofenceIndex = viewModel.nextGeofenceIndex()
if(currentGeofenceIndex >= GeofencingConstants.NUM_LANDMARKS) {
removeGeofences()
viewModel.geofenceActivated()
return
}
val currentGeofenceData = GeofencingConstants.LANDMARK_DATA[currentGeofenceIndex]
val geofence = Geofence.Builder()
.setRequestId(currentGeofenceData.id)
.setCircularRegion(currentGeofenceData.latLong.latitude,
currentGeofenceData.latLong.longitude,
GeofencingConstants.GEOFENCE_RADIUS_IN_METERS
)
.setExpirationDuration(GeofencingConstants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()
val geofencingRequest = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
geofencingClient.removeGeofences(geofencePendingIntent)?.run {
addOnCompleteListener {
geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent)?.run {
addOnSuccessListener {
Toast.makeText(this@HuntMainActivity, R.string.geofences_added,
Toast.LENGTH_SHORT)
.show()
Log.e("Add Geofence", geofence.requestId)
viewModel.geofenceActivated()
}
addOnFailureListener {
Toast.makeText(this@HuntMainActivity, R.string.geofences_not_added,
Toast.LENGTH_SHORT).show()
if ((it.message != null)) {
Log.w(TAG, it.message)
}
}
}
}
}
}
- First, check if you have any active geofences for your treasure hunt. If you already do, you shouldn't add another since you only want them looking for one treasure at a time.
if (viewModel.geofenceIsActive()) return
- Find the
currentGeofenceIndex
from theviewModel
. Remove any existing geofences, callgeofenceActivated
on theviewModel
, and return.
val currentGeofenceIndex = viewModel.nextGeofenceIndex()
if(currentGeofenceIndex >= GeofencingConstants.NUM_LANDMARKS){
removeGeofences()
viewModel.geofenceActivated()
return
}
- Once you have the index of the geofence, and know it is valid, get the data surrounding the geofence, which includes the id, and the latitude and longitude coordinates.
val currentGeofenceData = GeofencingConstants.LANDMARK_DATA[currentGeofenceIndex]
- Build the geofence using the geofence builder and the information in
currentGeofenceData
. Set the expiration duration using the constant set inGeofencingConstants
. Set the transition type toGEOFENCE_TRANSITION_ENTER
. Finally, build the geofence.
val geofence = Geofence.Builder()
.setRequestId(currentGeofenceData.id)
.setCircularRegion(currentGeofenceData.latLong.latitude,
currentGeofenceData.latLong.longitude,
GeofencingConstants.GEOFENCE_RADIUS_IN_METERS
)
.setExpirationDuration(GeofencingConstants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()
- Build the geofence request. Set the initial trigger to
INITIAL_TRIGGER_ENTER
, add the geofence you just built, and then build.
val geofencingRequest = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
- Call
removeGeofences()
on thegeofencingClient
to remove any geofences already associated with thePendingIntent
.
geofencingClient.removeGeofences(geofencePendingIntent)?.run {
}
- When
removeGeofences()
completes, regardless of its success or failure, add the new geofences. You can disregard the success or failure of the removal of geofences because even if the removal fails, it will not affect you adding another geofence.
addOnCompleteListener {
geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent)?.run {
}
}
- If adding the geofences is successful, let the user know with a toast.
addOnSuccessListener {
Toast.makeText(this@HuntMainActivity, R.string.geofences_added,
Toast.LENGTH_SHORT)
.show()
Log.e("Add Geofence", geofence.requestId)
viewModel.geofenceActivated()
}
- If adding the geofences fails, present a different toast, letting the user know that there was an issue with adding the geofences.
addOnFailureListener {
Toast.makeText(this@HuntMainActivity, R.string.geofences_not_added,
Toast.LENGTH_SHORT).show()
if ((it.message != null)) {
Log.w(TAG, it.message)
}
}
- Run your app. Your screen should display a clue, and a toast that tells you that the geofence has been added.
8. Task: Updating the Broadcast Receiver
Your app now adds geofences. However, try to navigate to the Golden Gate Bridge (the correct location for the default first clue). Nothing happens. Why is that?
When the user enters a geofence established by a clue, in this case the Golden Gate Bridge, you want to be notified, so that you can present the next clue. You can do this by using a Broadcast receiver that can receive details about geofence transition events.
Android apps can send or receive broadcast messages from the Android system and other apps using Broadcast Receivers. They use the publish-subscribe design pattern, where broadcasts are sent out, and apps can register to receive specific broadcasts. When a subscribed broadcast is sent out, the app is notified.
Step 1: Override the onReceive() method
- In
GeofenceBroadcastReceiver.kt
, find theonReceive()
function and copy this code into the class. Each step is explained in the bullet points below.
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_GEOFENCE_EVENT) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = errorMessage(context, geofencingEvent.errorCode)
Log.e(TAG, errorMessage)
return
}
if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
Log.v(TAG, context.getString(R.string.geofence_entered))
val fenceId = when {
geofencingEvent.triggeringGeofences.isNotEmpty() ->
geofencingEvent.triggeringGeofences[0].requestId
else -> {
Log.e(TAG, "No Geofence Trigger Found! Abort mission!")
return
}
}
val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst {
it.id == fenceId
}
if ( -1 == foundIndex ) {
Log.e(TAG, "Unknown Geofence: Abort Mission")
return
}
val notificationManager = ContextCompat.getSystemService(
context,
NotificationManager::class.java
) as NotificationManager
notificationManager.sendGeofenceEnteredNotification(
context, foundIndex
)
}
}
}
- A Broadcast Receiver can receive many types of actions. For this app, you only need to know when the geofence has been entered. Check that the intent's action is of type
ACTION_GEOFENCE_EVENT
.
if (intent.action == ACTION_GEOFENCE_EVENT) {
}
- Create a variable called
geofencingEvent
and initialize it toGeofencingEvent
with the intent passed in.
val geofencingEvent = GeofencingEvent.fromIntent(intent)
- If there is an error, you need to understand what went wrong. Save a variable with the error message obtained through the geofences error code. Log that message and return from the method.
if (geofencingEvent.hasError()) {
val errorMessage = errorMessage(context, geofencingEvent.errorCode)
Log.e(TAG, errorMessage)
return
}
- Check if the
geofenceTransition
type isENTER
.
if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {}
- If the
triggeringGeofences
array is not empty, set thefenceID
to the first geofence'srequestId
. You would only have one geofence active at a time, so if the array is non-empty, there would only be one to interact with. If the array is empty, log a message andreturn
.
val fenceId = when {
geofencingEvent.triggeringGeofences.isNotEmpty() ->
geofencingEvent.triggeringGeofences[0].requestId
else -> {
Log.e(TAG, "No Geofence Trigger Found! Abort mission!")
return
}
}
- Check that the geofence is consistent with the constants listed in
GeofenceUtil.kt
. If not, print a log andreturn
.
val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst {
it.id == fenceId
}
if ( -1 == foundIndex ) {
Log.e(TAG, "Unknown Geofence: Abort Mission")
return
}
- If your code execution has gotten this far, the user has entered a valid geofence. Send a notification telling them the good news!
val notificationManager = ContextCompat.getSystemService(
context,
NotificationManager::class.java
) as NotificationManager
notificationManager.sendGeofenceEnteredNotification(
context, foundIndex
)
- Try it yourself by walking into a geofence or emulating your location to be at the geofence (instructions in the next step). When you enter, a notification should pop up.
9. Task: Mocking a location on the emulator
Skip this section if you are not using an emulator.
Since testing this codelab is dependent on walking around, it may be more convenient to use a mocked location on the emulator. In this task, you learn how to mock location on your emulator.
Step 1: Mock your location
- On the menu bar next to your emulator, tap the three dots (...) at the bottom to open the Extended controls plane.
- Select Location.
- In the search bar of the map, enter a location, such as the Golden Gate Bridge. The location marker displays at the location you entered.
- At the bottom right of the pane, press the Set Location button.
- Go to the Google Maps app and the notification should pop up. This may take a few seconds.
10. Task: Removing geofences
When you no longer need geofences, it is a best practice to remove them, which stops monitoring, in order to save battery and CPU cycles.
Step 1: Remove geofences
- In
HuntMainActivity.kt
, copy this code into theremoveGeofences()
method. Each step is explained in the bullet points below.
private fun removeGeofences() {
if (!foregroundAndBackgroundLocationPermissionApproved()) {
return
}
geofencingClient.removeGeofences(geofencePendingIntent)?.run {
addOnSuccessListener {
Log.d(TAG, getString(R.string.geofences_removed))
Toast.makeText(applicationContext, R.string.geofences_removed, Toast.LENGTH_SHORT)
.show()
}
addOnFailureListener {
Log.d(TAG, getString(R.string.geofences_not_removed))
}
}
}
- Initially, check if foreground permissions have been approved. If they have not, return.
if (!foregroundAndBackgroundLocationPermissionApproved()) {
return
}
- Call
removeGeofences()
on thegeofencingClient
and pass in thegeofencePendingIntent
.
geofencingClient.removeGeofences(geofencePendingIntent)?.run {
}
- Add an
onSuccessListener()
, and inform the user with a toast that the geofences were successfully removed.
addOnSuccessListener {
Log.d(TAG, getString(R.string.geofences_removed))
Toast.makeText(applicationContext, R.string.geofences_removed, Toast.LENGTH_SHORT)
.show()
}
- Add an
onFailureListener()
where you log if the geofences weren't removed.
addOnFailureListener {
Log.d(TAG, getString(R.string.geofences_not_removed))
}
- The
removeGeofences()
method is called in theonDestroy()
method included in the starter code.
11. Task: Navigating to the winning location
Now that everything is set up, there is only one thing left to do. Win the game!
Step 1: Win the game!
Navigate to the winning location either by mocking the location on your emulator or physically walking there in person! Congratulations, you won this codelab!
12. Coding challenge
You can add landmarks to customize your treasure hunt and add more geofences to make the treasure hunt last longer.
- In
strings.xml
add your custom hint and location.
<!-- Geofence Hints -->
<string name="lombard_street_hint">Go to the most crooked street in the City</string>
<!-- Geofence Locations -->
<string name="lombard_street_location">at Lombard Street</string>
- In
GeofenceUtils.kt
, customize the landmarks by creating aLandmarkDataObject
with a destination ID, destination hint, destination location, and destination latitude and longitude. Add this to theLANDMARK_DATA
array with your own landmark objects.
val LANDMARK_DATA = arrayOf(
LandmarkDataObject(
"Lombard street",
R.string.lombard_street_hint,
R.string.lombard_street_location,
LatLng(37.801205, -122.426752))
)
13. Summary
In this codelab you learned how to:
- Add permissions, request permissions, check permissions, and handle permissions.
- Check the device location using the settings client.
- Add geofences using a pending intent and a geofencing client.
- Integrate a broadcast receiver to detect when a geofence is entered by overriding the
onReceive()
method. - Remove geofences using the geofencing client.
14. Learn more
Udacity courses:
Android developer documentation:
Other resources:
15. Next codelab
For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.