Chapters

Hide chapters

Android Apprentice

Third Edition · Android 10 · Kotlin 1.3 · Android Studio 3.6

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

14. User Location and Permissions
Written by Namrata Bandekar

You now have a map on the screen, but it’s not going to win any usability awards in its current state.

For starters, the map always starts off centered over Sydney, Australia. Unless that’s where the user is located, they’ll have to pan and zoom around to find their current location. The other issue is there’s no way to track the user’s location as they move.

In this chapter, you’ll address some of these problems by adding the following features to the app:

  • Automatically center the map on the user’s location at startup.
  • Allow the user to recenter the map to their current location at any time.

Getting started

If you’re following along with your own app, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the PlaceBook app inside the starter folder. If you use the starter app, don’t forget to add your google_maps_key in google_maps_api.xml. Review Chapter 13 for more details about the Google Maps key.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

The first order of business is to fix the starting location. Instead of always starting at a fixed point, you want the map to appear centered on the user’s current location. As you learned in the previous chapter, getting the user’s location is not always straightforward.

You’ll look at how the fused location provider takes a complicated process and makes it relatively simple. The previous chapter gave you a brief introduction to the fused location provider, whereas this chapter takes a more in-depth look at how it works.

Fused location provider

The job of the fused location provider is to take all of the different inputs provided by the hardware and fuse them into location data that reflects the user’s accuracy requests. OK, that was a mouthful. Let’s break down how it works in practice.

There are two primary ways to interact with the fused location provider:

  1. Directly ask for the last known device location.
  2. Request location updates based on hints about accuracy and power consumption.

Asking for the last known device location is a simple call to FusedLocationProviderClient.getLastLocation(). This returns a Task that you can use to get the last known location of the device. If the device has not yet retrieved a location, this may return null.

In the second scenario, requesting location updates based on hints, you ask for periodic location updates by calling FusedLocationProviderClient.requestLocationUpdates() and indicating your priorities with LocationRequest.

The fused location provider uses the most appropriate sensors on the device to match your priorities while preserving as much battery power as possible.

You can request location updates in two ways:

  1. Using a LocationListener callback method: This method works best when your app is running in the foreground and is actively displaying the user’s location. Whenever there’s relevant location data available, this makes an asynchronous call to a method you’ve defined yourself.

  2. Using a PendingIntent: This is useful when you want to be notified of location events, even if your app is not currently running.

Adding location services

The fused location provider is part of the location services library within Google Play Services. Before using it, you’ll need to add a new dependency.

Open build.gradle (Module:app) and add the following line to the dependencies section, taking care to use the same version as the existing play-services-maps dependency in the prior chapter if possible.

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

Note: The Google Play services API libraries are designed to be independent and the versions may be different. Currently, both play-services-maps and play-services-location are at 17.0.0.

This adds the location APIs to the app.

Note: The Google Play services APIs provide a wealth of useful features. You’ll explore more of them in later sections of the book, but if you want a sense of the depth of capabilities, check out the list of services at https://developers.google.com/android/.

Ad-Hoc Gradle properties

Before moving on, this is a good time to practice the DRY principle in your Gradle dependency management. The app/build.gradle dependencies section now has two entries for play-services that both use the 17.0.0 library version. You’ll fix that by adding some ad-hoc properties using Gradle’s ExtraPropertiesExtension.

As your Gradle files grow with more dependencies, they can be easier to manage if you define the library versions in a single location. The place to define global Gradle ad-hoc properties is in the top level Gradle file.

Open your project build.gradle and remove the following line:

ext.kotlin_version = '1.3.61'

Note: Your version might be different. Regardless, remove whatever ext.kotlin version is in your file.

Update the first part of the buildscript section to match this:

buildscript {
  ext {
    kotlin_version = '1.3.61'
    play_services_version = '17.0.0'
  }

You now have two properties defined within the build script domain that you can access from any .gradle file within the project.

Open app/build.gradle and update the play services dependencies to take advantage of the new play_services_version extension property.

implementation "com.google.android.gms:play-services-maps:$play_services_version"
implementation "com.google.android.gms:play-services-location:$play_services_version"

Note: The single quotes must be changed to double quotes when using extension properties.

Creating the location services client

To use the fused location API, you must create a Fused Location Provider Client using the FusedLocationProviderClient class.

In MapsActivity.kt, add a new private member below the map member:

private lateinit var fusedLocationClient: FusedLocationProviderClient

Add the following method to MapsActivity under onMapReady():

private fun setupLocationClient() {
  fusedLocationClient = 
      LocationServices.getFusedLocationProviderClient(this)
}

Finally, add a call to setupLocationClient() at the bottom of onCreate().

setupLocationClient()

Querying current location

Next, you’ll start by trying to query the user’s current location, then place a marker and center the map on the location. Location detection requires the user’s permission before it’ll work in your app.

Before moving on to the details of location permissions, a quick overview of how permissions work on Android is in order.

Permissions overview

Each app running on an Android device lives in its own little world. This is known as process sandboxing. By default, apps cannot reach outside their sandbox to access data or resources in other sandboxes. This is done to protect the user’s privacy as well as system stability.

If your app needs to reach outside its sandbox and access protected features, it must add a <uses-permission> tag to the apps manifest file. Android divides permissions into two main categories; Normal and Dangerous.

  • Normal permissions: Permissions in this category are considered less harmful and are granted automatically if they’re listed in the manifest. Examples of normal permissions include BLUETOOTH, ACCESS_NETWORK_STATE, INTERNET and SET_ALARM.

  • Dangerous permissions: Permissions in this category can affect user’s privacy or system stability. For these permissions, the system explicitly asks the user to allow the permissions. Examples of dangerous permissions include READ_CALENDAR, READ_CONTACTS, CALL_PHONE and SEND_SMS.

    Android handles the dangerous requests differently depending on the OS version. If running Android 6.0 or higher and the app’s targetSdkVersion is 23 or higher, you must request the user approval at run-time. On this version, the user can revoke individual permissions at any time, so the app must check for permissions every time it uses a protected feature. Even though you’ll request dangerous permissions at run-time, they still must be specified in the manifest.

    If running Android 5.1.1 or lower, or the app’s targetSdkVersion is 22 or lower, the user is asked to approve the permissions when the app is first installed. If an app update adds new permissions, then the user is asked to approve the new permissions when the app is updated. On this version, the user can only remove permissions by uninstalling the app.

In addition to the primary categories, the dangerous permissions are separated into groups. Android won’t display the specific permission when asking the user for permission; it’ll only show the group that the permissions belong to.

For example, the SEND_SMS and RECEIVE_SMS permissions are part of the SMS group. If your app requests SEND_SMS and RECEIVE_SMS permissions, only a single SMS permission will be requested by the system.

Note: It’s also possible for an app to define its own permissions. This allows an app to share resources or capabilities with other apps.

You can learn more about this feature at https://developer.android.com/guide/topics/permissions/defining.html.

The first run-time permission you’ll use is ACCESS_FINE_LOCATION from the LOCATION group. It’s already specified with the tag in the manifest file. Now, you’ll check for it at run-time before any code uses the location features.

Permission accuracy options

Your app can choose between two levels of location accuracy:

  1. ACCESS_FINE_LOCATION: Used when you want the most accurate location data possible. This uses all location sources, including the GPS chip, and will use more battery.

  2. ACCESS_COARSE_LOCATION: The less “refined” location permission. If you don’t need a location more accurate than a city block, then choose this option. This only uses the Wi-Fi and cell towers to provide location data.

You should only choose one of these options.

In PlaceBook, you want to get the most accurate location readings, so you’ll use ACCESS_FINE_LOCATION.

Adding run-time permissions

Open MapsActivity.kt and add the following method:

private fun requestLocationPermissions() {
  ActivityCompat.requestPermissions(this, 
      arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 
      REQUEST_LOCATION)
}

Ignore the unresolved reference for REQUEST_LOCATION, you’ll define it next.

This method uses requestPermissions() to prompt the user to grant or deny the ACCESS_FINE_LOCATION permission. Notice that this is the same permission as in AndroidManifest.xml.

You pass the current activity as the context; then an array of requested permissions; and finally a requestCode to identify this specific request.

Add the following to MapsActivity:

companion object {
  private const val REQUEST_LOCATION = 1
  private const val TAG = "MapsActivity"
}

REQUEST_LOCATION is a request code passed to requestPermissions(). It’s used to identify the specific permission request when the result is returned by Android.

TAG is passed into the Log.e method in the next code block. Log.e() is used to print information to the Logcat window to help with debugging.

With that in place, you’re ready to create a method to get the user’s current location.

Add the following new method:

private fun getCurrentLocation() {
  // 1
  if (ActivityCompat.checkSelfPermission(this,
          Manifest.permission.ACCESS_FINE_LOCATION) !=
      PackageManager.PERMISSION_GRANTED) {
    // 2
    requestLocationPermissions()
  } else {
    // 3
    fusedLocationClient.lastLocation.addOnCompleteListener {
      val location = it.result
      if (location != null) {
        // 4
        val latLng = LatLng(location.latitude, location.longitude)
        // 5
        map.addMarker(MarkerOptions().position(latLng)
            .title("You are here!"))
        // 6
        val update = CameraUpdateFactory.newLatLngZoom(latLng, 16.0f)
        // 7
        map.moveCamera(update)
      } else {
        // 8
        Log.e(TAG, "No location found")
      }
    }
  }   
}

getCurrentLocation() gets the user’s current location and moves the map so that it centers on the location.

Here’s how it works:

  1. Check if the ACCESS_FINE_LOCATION permission was granted before requesting a location.

  2. If the permission has not been granted, then requestLocationPermissions() is called.

  3. This may look a little odd. Why is addOnCompleteListener called on the lastLocation property? The reason is that the lastLocation property is actually a Task that runs in the background to fetch the location. You request to be notified when the location is ready by adding an OnCompleteListener to the lastLocation Task.

    When the Task completes, it calls the default onComplete() method with a Task<TResult> object. it.result represents a Location object containing the last known location. it.result can be null if there is no location data available. The reason for this will be explained soon.

  4. If location is not null, you create a LatLng object from location. LatLng is just a simple object for storing the latitude and longitude coordinate for a single map location. You’ll see this often when working with location services.

  5. You use addMarker() on map to create a marker at that location. addMarker() tells the map to add and display the marker. There are many options when adding markers to a map. In this case, you’re using the default marker style with a simple title that gets displayed if tapped. You’ll learn more about markers in future chapters.

  6. You use CameraUpdateFactory.newLatLngZoom() to create a CameraUpdate object. CameraUpdate objects are used to specify how the map camera is updated.

When working with Google Maps, you can change the view of the map by adjusting parameters on a virtual map camera. You can think of the map view as a flat plane with the virtual camera looking straight down on it. The main camera properties you can adjust are:

  • Target: This is the location the camera is viewing. The map is always centered on this location.

  • Bearing: This is the direction that a vertical line on the map will point. This starts at 0 degrees north and increases in a clockwise direction. For example, if you wanted the top of the map to be east, you would set the bearing to 90 degrees.

  • Tilt: You can show maps at an angle to give a perspective view. The tilt is the angle in degrees from the camera nadir line (the line pointing directly down from the camera).

  • Zoom: You set the scale of the map using this parameter. Larger values zoom you closer to the map and display more detail. A zoom value of 0 will show the full Earth on a 256dp-widescreen. A zoom level of 15 is typical for a street-level view.

    CameraUpdateFactory provides several convenience methods for creating CameraUpdate objects. You use newLatLngZoom() to specify updates to the camera target and zoom.

    Note: See https://developers.google.com/android/reference/com/google/android/gms/maps/CameraUpdateFactory for additional options for CameraUpdateFactory.

  1. You call moveCamera() on map to update the camera with the CameraUpdate object.

  2. If result is null, you log an error message.

With getCurrentLocation() implemented, you can call it once the map is ready.

Replace onMapReady() with the following code.

override fun onMapReady(googleMap: GoogleMap) {
  map = googleMap
  getCurrentLocation()
}

Here, you initialize map when the map is ready to be displayed and then call getCurrentLocation().

Finally, define the callback method to handle the user’s response to the permission request. When requestLocationPermissions() is called, the system displays a permission dialog to the user. It then calls onRequestPermissionsResult() with the results. Add the following method:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray) {
  if (requestCode == REQUEST_LOCATION) {
    if (grantResults.size == 1 && grantResults[0] == 
        PackageManager.PERMISSION_GRANTED) {
      getCurrentLocation()
    } else {
      Log.e(TAG, "Location permission denied")
    }
  } 
}

First, you check to make sure this result matches the REQUEST_LOCATION request code. Next, you check to see if the first item in the grantResults array contains the PERMISSION_GRANTED value. If so, you can use the granted permission and call getCurrentLocation() again. If grantResults doesn’t indicate permission was granted, then you print an error message to the Logcat window using Log.e().

Testing permissions

Run the app on a device or emulator running Android 6.0 or newer, and you’ll see the following prompt:

Click DENY, and the Location permission denied message appears in Logcat.

Run the app again, and the prompt displays again with one small change, offering the user a chance to tell the system “Deny & don’t ask again”.

If you choose “Deny & don’t ask again”, the dialog won’t be displayed again within the app. The only way to then grant permissions is to manually turn them on in device settings by tapping on Apps & notifications>PlaceBook>Permissions.

Note: Google recommends that you display a more detailed reason for asking for permission if the user denies it multiple times. There’s a built-in method, ActivityCompat.shouldShowRequestPermissionRationale, you can use to determine if it’s time to show a detailed reason.

See https://developer.android.com/training/permissions/requesting.html#perm-request for more information.

Now, click Allow on the permission dialog. At this point, you can expect the app to return your current location and then zoom the map to your current location.

If you’re running on a device, that’s most likely true, and you’ll be looking at a screen similar to the following, although centered at your current location.

If running on the emulator, however, the map will likely not show, and you can see the No location found message printed in the Logcat window.

This is because the emulator hasn’t simulated a user location. An emulator doesn’t have access to GPS hardware, so you need another way to supply GPS locations.

Note: If you see the No location found message on a hardware device, then check that location services are turned on in the device settings.

Faking locations in the emulator

The problem is that the fused location provider does not have any location data from which to pull. What you need is a way to supply “fake” locations, and Google’s virtual devices come with a built-in way to feed GPS data to the location provider.

Launch the emulator and click the three dots () at the bottom of the floating toolbar to bring up the extended controls, and then click the Location tab on the left.

On the Single points tab you see a map with a search bar. There is a button to set location.

Click on the map to select a location you would like to set for the emulator. A marker is displayed at the point where you clicked and the selected address is displayed at the bottom of the map.

Now tap Set Location. You see a message, at the bottom of the map, confirming the address the emulator is set to.

Close the app and run again, but the map still won’t display that location. What’s going on?

There’s one final item to address. The fused location provider needs at least one app to actively request a location before it will return valid data from getLastLocation().

On a real device, there are usually plenty of other processes requesting locations and feeding the fusion location provider with data. That’s not the case on the emulator.

One way to wake up the fusion API is to run the Google Maps app. Once you run Google Maps, click the My Location icon (the target) and approve any prompts to turn on location services.

Once you see that Google Maps zooms you to the entered location, close and launch PlaceBook again. This time it should zoom to the location you entered.

If it doesn’t work the first time, try and try again. Sometimes the emulator is a little finicky, but eventually, it’ll zoom to the entered location.

In upcoming chapters, you’ll update the app so it works in the emulator without being triggered by Google Maps.

Tracking the user’s location

It’s great that you have a way to display the user’s location when the app first launches, but what happens when the user moves to a new location? No problem! Simply relaunch the app, and it’ll update to the new location. That’s not the most intuitive way to update the map. You can do better!

You need a way to keep track of the user’s location as they move around. This can be done by directly asking the fused location provider for periodic location updates. This is where the FusedLocationClient.requestLocationUpdates() comes into play. FusedLocationClient.requestLocationUpdates() asks the fused location provider to start sending the app location updates.

Calling requestLocationUpdates()

To request updates from the location client, you need a LocationRequest object to describe the level of accuracy you want to achieve. Add the following new property at the top of MapsActivity:

private var locationRequest: LocationRequest? = null

Now, go to getCurrentLocation() and add the following before the call to fusedLocationClient.lastLocation.addOnCompleteListener:

if (locationRequest == null) {
  locationRequest = LocationRequest.create()
  locationRequest?.let { locationRequest ->
    // 1
    locationRequest.priority = 
        LocationRequest.PRIORITY_HIGH_ACCURACY
    // 2
    locationRequest.interval = 5000
    // 3
    locationRequest.fastestInterval = 1000
    // 4
    val locationCallback = object : LocationCallback() {
      override fun onLocationResult(locationResult: LocationResult?) {
        getCurrentLocation()
      }
    }
    // 5
    fusedLocationClient.requestLocationUpdates(locationRequest, 
        locationCallback, null)
  }
}

You first check to see if locationRequest has already been created. If not, you create a new one, and then if the creation succeeds, you set the following properties:

  1. priority: This provides a general guide to how accurate the locations should be. The following options are allowed:

    PRIORITY_BALANCED_POWER_ACCURACY: Use this setting if you only need accuracy to the city block level, which is around 40-100 meters. This uses very little power and only polls for location updates every 20 seconds or so. The system is likely to only use Wi-Fi or cell tower to determine your location.

    PRIORITY_HIGH_ACCURACY: Use this setting if you need the most accuracy possible, normally within 10 meters. This uses the most battery power and typically polls for locations about every 5 seconds.

    PRIORITY_LOW_POWER: Use this setting if you only need accuracy at the city level within 10 kilometers. This uses a minimal amount of battery power.

    PRIORITY_NO_POWER: You normally only use this setting if your app can live with or without location data. It will not actively request any location from the system but will return a location if another app is requesting location data.

    Here, you set priority to LocationRequest.PRIORITY_HIGH_ACCURACY so it’ll return the most accurate location possible. In the emulator, anything less than PRIORITY_HIGH_ACCURACY may not trigger any updates to occur.

  2. interval: This lets you specify the desired interval in milliseconds to return updates. This is simply a hint to the system, and if other apps have requested faster updates, your app gets the updates at that rate as well.

    Here, you set the requested update interval to 5 seconds by setting interval to 5000.

  3. fastestInterval: This sets the shortest interval in milliseconds that your app is capable of handling. Since other apps can affect the update interval, this sets a hard limit on how often you’ll receive updates. Here, you set the shortest interval to 1 second with locationRequest.fastestInterval = 1000.

Note: Keep in mind that the LocationRequest settings are more like guidelines than they are rules. The fused location provider will try to meet the requested options, but there are no guarantees.

  1. The fused location provider calls LocationCallBack.onLocationResult when it has a new location ready. You define a LocationCallBack object with onLocationResult(). You use this opportunity to update the map to center on the new location. Although onLocationResult() receives a list of locations that you could use to center the map, you just call the existing getCurrentLocation() to grab the latest location and center the map.

  2. Finally, you call fusedLocationClient.requestLocationUpdates(), passing in the LocationRequest object, and the LocationCallback object.

After calling requestLocationUpdates(), your app can go about its business and wait for the onLocationChanged() to be called by the location services.

Add the following line in getCurrentLocation() before the call to map.addMarker:

map.clear()

Since getCurrentLocation() is called each time the location changes, you need to call clear() on the GoogleMap object to remove the previous marker.

Testing location updates

Run the app again on the emulator, and it should center the map over the location you entered before. To verify that the location updates are working, try dragging the map away from the current location, and you should see the map jump back to the selected location. Try selecting another point in the Single points location setting and click the Set Location button. You see the map move to the new selected location.

You should see similar behavior if you run this on a device.

My location

Showing a marker at your current location works for demonstration purposes, but it’s not the typical way to show the user’s location. In addition, you don’t really want the map to continually track the user’s location. The user should be able to freely pan around the map and recenter at will.

You’ll fix these two issues by making the following changes:

  1. Display a blue dot at the user’s location and have it move to keep up with the user.

  2. Add a control that allows the user to recenter the map.

  3. Disable the continuous map centering.

Believe it or not, you can accomplish changes #1 and #2 can with one line of code with the magic of the GoogleMap.isMyLocationEnabled property.

Using GoogleMap.isMyLocationEnabled

The GoogleMap object already has the ability to do exactly what you need without any additional coding. The feature is called MyLocation; you enable it by setting the isMyLocationEnabled to true.

Add the following line to getCurrentLocation() before the call to fusedLocationClient.lastLocation:

map.isMyLocationEnabled = true

Setting isMyLocationEnabled adds a new layer to the map with several useful features:

  1. It displays the trusty blue dot that always keeps up with the user’s current location. Note that it does this without having to request location updates from the location services.

  2. It displays a target icon that will recenter the map on the user’s location if they tap on it.

  3. It will add controls to let the user choose whether the map should rotate with the user’s current bearing.

As a bonus, turning on isMyLocationEnabled handles all of the logic to request location updates, and you can remove the code for location updates that was added earlier.

Remove the following items:

  1. Remove the following line from the top of MapsActivity:
private var locationRequest: LocationRequest? = null
  1. Remove the following block of code from getCurrentLocation():
if (locationRequest == null) {
  locationRequest = LocationRequest.create()
  locationRequest?.let { locationRequest ->
    // 1
    locationRequest.priority = 
        LocationRequest.PRIORITY_HIGH_ACCURACY
    // 2
    locationRequest.interval = 5000
    // 3
    locationRequest.fastestInterval = 1000
    // 4
    val locationCallback = object : LocationCallback() {
      override fun onLocationResult(locationResult: LocationResult?) {
        getCurrentLocation()
      }
    }
    // 5
    fusedLocationClient.requestLocationUpdates(locationRequest, 
        locationCallback, null)
  }
}
  1. Remove the following lines from getCurrentLocation():
map.clear()
map.addMarker(MarkerOptions().position(latLng)
    .title("You are here!"))

Run the app and check out the great new functionality you added with minimal effort.

Click the SET LOCATION button on the GPS Location controls, and you should see the blue dot appear at the selected location. Pan the map around and then click the My Location icon to recenter back to the blue dot.

Where to go from here?

Congratulations, you completed everything needed for the basic map controls! In the next chapter, you’ll start working with Google Places.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.