Home Android & Kotlin Books Android Apprentice

14
User Location and Permissions Written by Namrata Bandekar

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

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.

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

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.

ext.kotlin_version = '1.3.61'
buildscript {
  ext {
    kotlin_version = '1.3.61'
    play_services_version = '17.0.0'
  }
implementation "com.google.android.gms:play-services-maps:$play_services_version"
implementation "com.google.android.gms:play-services-location:$play_services_version"

Creating the location services client

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

private lateinit var fusedLocationClient: FusedLocationProviderClient
private fun setupLocationClient() {
  fusedLocationClient = 
      LocationServices.getFusedLocationProviderClient(this)
}
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.

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.

Permission accuracy options

Your app can choose between two levels of location accuracy:

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)
}
companion object {
  private const val REQUEST_LOCATION = 1
  private const val TAG = "MapsActivity"
}
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")
      }
    }
  }   
}
override fun onMapReady(googleMap: GoogleMap) {
  map = googleMap
  getCurrentLocation()
}
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")
    }
  } 
}

Testing permissions

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

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.

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!

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
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)
  }
}
map.clear()

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.

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.

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.

map.isMyLocationEnabled = true
private var locationRequest: LocationRequest? = null
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)
  }
}
map.clear()
map.addMarker(MarkerOptions().position(latLng)
    .title("You are here!"))

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.