androidkotlinandroid-locationandroid-fusedlocation

How to properly handle location service toggling


In my app, I have a button that gets the user's current location using the FusedLocationProviderClient. This works correctly until location services are turned off and then back on on the device. When this occurs, FusedLocationProviderClient.getLastLocation() always returns null. How should I handle this situation? Do I need to reinitialize the FusedLocationProviderClient in some way?

I understand from the documentation that when location services are turned off, the last cached location is flushed. This tells me that I need to force a location request so the cache gets refreshed. However, I'm unsure of how to do this. I've also followed this Medium article for setting up the FusedLocationProviderClient, but all it does is check if the location result is null, and does not do anything to handle when it is.

I am writing the app in Kotlin. Here is how I initialize the FusedLocationProviderClient and get the user's current location in my fragment:

class LocationFragment : Fragment() {

    private lateinit var mFusedLocationProviderClient: FusedLocationProiderClient

    override fun onCreate(savedInstanceState: Bundle?) {
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(activity)
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        // Initialize button on-click callback.
    }

    private fun getCurrentLocation() {
        // Check that the location service is enabled.
        // Check coarse and fine location permissions.

        mFusedLocationProviderClient.lastLocation.addOnSuccessListener {
            if (it != null) { // Always false if location services are toggled.
                print(it.latitude, it.longitude)
            }
        }
    }
}   

Solution

  • I haven't done a lot with the FusedLocationProviderClient, but as I understand it (and the docs seem to agree), using lastLocation (getLastLocation() for Java folks) doesn't really go out and "fetch" the current device location, but simply returns the last location that was fetched by the device. Typically, any number of services on the device are requesting location, so there is always a reasonably accurate value in the cache for lastLocation to use. But if you toggle Location Services off and back on and then immediately call lastLocation, you very well could get null returned. Note also that the docs for lastLocation state:

    It is particularly well suited for applications that do not require an accurate location

    And from the docs for the getLocationAvailability() call:

    Note it's always possible for getLastLocation() to return null even when this method returns true (e.g. location settings were disabled between calls).

    These also lend credence to the idea that the method doesn't actually do a location lookup and instead just returns the most recent lookup from cache, and when you toggle services off/on, the cache is empty.

    If you need to be sure to get the device's current location and be sure that it's fairly accurate, you probably need to use requestLocationUpdates() to get the location client to actually figure out the device's current location and return it in the callback you provide, and then remove the callback when you are done.

    It's been awhile since I worked with the location provider, but I think you could do something like this, assuming you only need a single location:

        private val locationRequest: LocationRequest by lazy {
            LocationRequest.create().apply {
                interval = (LOCATION_UPDATE_INTERVAL_SECONDS * 1000).toLong()
                fastestInterval = (LOCATION_FAST_INTERVAL_SECONDS * 1000).toLong()
                priority = LocationRequest.PRIORITY_HIGH_ACCURACY
                smallestDisplacement =
                    MINIMUM_DISPLACEMENT_METERS
            }
        }
    
        private fun isValidLocation(location: Location): Boolean {
            /* TODO: validate that the location meets all of your
                     requirements for accuracy, etc, and return the
                     appropriate true/false value
             */
            return true  // or false, for example if the location accuracy is not good enough.    
        }    
    
    
        private val locationCallback = object: LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
    
                Log.d(TAG, "Location Received: $locationResult")
    
                val location = locationResult.lastLocation
    
                if (isValidLocation(location)) {
                    Log.d(TAG, "Valid Location dectected: $location")
    
                    // we have a valid location, so stop receiving further location updates.
                    mFusedLocationClient.removeLocationUpdates(this)
    
                    //TODO: we have a valid location! Use it as you wish to update the
                    //      view, or emit it via a LiveData, etc.
                } else {
                    // nothing to do, wait for more results.
                    Log.d(TAG, "Location received was not valid: $location")
                }
    
            }
        }    
    
        private fun watchLocation() {
    
            // Check Location Permissions
            // Check Google Play Services Version
            // Check Location Settings
            // If all three are good, then:
    
            mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
        } 
    

    The above snippet sets up a LocationCallback that gets called when a location is received. It checks the location (via the isValidLocation method) and if it's valid, it removes itself from the provider so that you don't get additional location updates. Right after the line where it removes itself (mFusedLocationClient.removeLocationUpdates(this)), you can do whatever work you need to do with the location object. If the location received is NOT valid, it'll keep getting additional callbacks as locations are received, until you get one that is valid. To start it all off, just call watchLocation()