0

After 2 days of struggle, I have been forced to ask this question. I have been searching StackOverflow but no solution seems to work for me. I don't know why.

I am trying to get the current device location once:

(a) the permissions have been granted

(b) user has enabled location service by tapping 'Okay' in the dialogue box that appears in the fragment (SelectLocationFragment.kt)

The issue: Once the permissions are granted, location is enabled by pressing 'okay' in the dialogue - If I use hard coded values like here:

//currentLatLng = LatLng(51.000, -0.0886)

instead of :

currentLatLng = LatLng(mCurrentLocation.latitude,mCurrentLocation.longitude)

It works fine. But this is not ideal of course. I'd like real time device location using onLocationCallBack's onLocationResult using:

                    fusedLocationClient.requestLocationUpdates(locationRequest,locationCallback, Looper.getMainLooper())

The problem is when I try to use:

currentLatLng = LatLng(mCurrentLocation.latitude,mCurrentLocation.longitude)

a) onLocationCallBack does not seem to be called. And therefore no call to onLocationResult happens, and therefore, mCurrentLocation remains uninitialised. And hence I get the error :

kotlin.UninitializedPropertyAccessException: lateinit property mCurrentLocation has not been initialized

This my onMapReady()

@SuppressLint("MissingPermission")
override fun onMapReady(gMap: GoogleMap) {
    isPoiSelected = false

    map = gMap

    **checkPermissionsAndDeviceLocationSettings()**

    setMapStyle(map)
    setPoiClick(map)
    setMapLongClick(map)
}

This is checkPermissionAndDeviceLocationSettings():

@SuppressLint("MissingPermission")
private fun checkPermissionsAndDeviceLocationSettings() {
    if (isPermissionGranted()) {
        checkDeviceLocationSettings()
    } else {
        requestPermissions(
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
            REQUEST_PERMISSION_LOCATION
        )
    }
}

This is checkDeviceLocationSettings:

private fun checkDeviceLocationSettings(resolve: Boolean = true) {

    val locationRequest = LocationRequest.create().apply {
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        interval = 10000L
    }

    val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)

    val settingsClient = LocationServices.getSettingsClient(requireActivity())

    val locationSettingsResponseTask =
        settingsClient.checkLocationSettings(builder.build())

    locationSettingsResponseTask.addOnFailureListener { exception ->
        if (exception is ResolvableApiException && resolve) {
            try {
                startIntentSenderForResult(
                    exception.resolution.intentSender,
                    REQUEST_TURN_DEVICE_LOCATION_ON, null, 0, 0, 0, null
                )

            } catch (sendEx: IntentSender.SendIntentException) {
                Log.d(
                    SaveReminderFragment.TAG,
                    "Error getting location settings resolution: " + sendEx.message
                )
            }
        } else {
            Snackbar.make(
                activity!!.findViewById<CoordinatorLayout>(R.id.myCoordinatorLayout),
                R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
            ).setAction(android.R.string.ok) {
                checkDeviceLocationSettings()
            }.show()
        }
    }

    locationSettingsResponseTask.addOnSuccessListener {

        Log.e(TAG, "SUCCESSFUL!")
        Toast.makeText(requireContext(), "TOAST", Toast.LENGTH_SHORT).show()

        enableLocation(locationRequest)
        showSnackBar(getString(R.string.select_location))
    }
}

This is enableLocation:

@SuppressLint("MissingPermission")
private fun enableLocation(locationRequest: LocationRequest) {

    Log.e(TAG, "Inside Enable Location Start")

    locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)
            Log.e(TAG, "Inside on Location Result")
            val locationList = locationResult.locations
            Log.e(TAG, "${locationResult.locations}")
            if(locationList.isNotEmpty()){
                val location = locationList.last()
                Log.i("MapsActivity", "Location: " + location.latitude + " " + location.longitude)
                mCurrentLocation = location
            }

            fusedLocationClient.removeLocationUpdates(locationCallback)
        }
    }

    map.isMyLocationEnabled = true

    val locationResult: Task<Location> = fusedLocationClient.lastLocation

    locationResult.addOnCompleteListener(OnCompleteListener<Location?> { task ->
        if (task.isSuccessful) {
            Log.e(TAG, locationResult.result?.latitude.toString())
            // Set the map's camera position to the current location of the device.
            if (task.result != null) {
                mCurrentLocation = task.result!!
                val latLng = LatLng(
                    mCurrentLocation.latitude,
                    mCurrentLocation.longitude
                )
                val update = CameraUpdateFactory.newLatLngZoom(
                    latLng,
                    18f
                )
                Toast.makeText(requireContext(), "CAMERA MOVING", Toast.LENGTH_SHORT).show()
                map.animateCamera(update)
            } else {
                Log.e(TAG, " Task result is null")
                //Need to do something here to get the real time location
                fusedLocationClient.requestLocationUpdates(locationRequest,locationCallback, Looper.getMainLooper())

                currentLatLng = LatLng(mCurrentLocation.latitude,mCurrentLocation.longitude)

                //currentLatLng = LatLng(51.000, -0.0886)

                val update = CameraUpdateFactory.newLatLngZoom(currentLatLng, 18f)
                map.animateCamera(update)

            }
        } else {

            Log.e(TAG, "Unsuccessful Task result")
            Toast.makeText(requireContext(), "ENABLE LOCATION ELSE", Toast.LENGTH_LONG).show()

        }
    })
}

Is there any God sent man out there to help me resolve this issue. I am beginning to fade away.

If you guys need to see the entire file:

Thank you in advance.

awaqar
  • 149
  • 1
  • 6
  • 1
    Just compare what you're doing differently versus my project: https://github.com/gavingt/SunCalculator/blob/master/app/src/main/java/com/gavinsappcreations/sunrisesunsettimes/ui/MainActivity.kt – Gavin Wright Jun 11 '22 at 23:33
  • @GavinWright - I really appreciate you responding and sharing that. Unfortuntely, I am still unsuccessful. However, I notice this line of code on Line 241 : "// Now that the device has a location, request it from FusedLocationProvider. requestLocationFromFusedLocationProvider()" I am interested why do you need to make this call? Do you not have the location already? – awaqar Jun 12 '22 at 10:27
  • 1
    I do have the location at that point. Doing it that way is just for simplicity. – Gavin Wright Jun 12 '22 at 14:51
  • 1
    @GavinWright - I am ever so thankful to you mate, I was able to get it working looking at your code. Your code definitely gave pointers to me. Added the following: - requestLocationFromFusedLocationProvider(), and - return@addOnSuccessListener – awaqar Jun 12 '22 at 16:01
  • Apparently, accessing mCurrentLocation variable straight after requestLocationUpdates is not wise since the location takes time (that's what I have been told) so adding return@addOnSuccessListener which I am assuming will rerun the locationResult.addonSuccessListener whereby at this point we have the location. Thanks once again. – awaqar Jun 12 '22 at 16:07
  • I have the same problem. It would be amazing, if you could add a code snippet that solves this problem. I'm not clear on what you mean by `return@addOnSuccessListener`. The accepted answer doesn't provide any code this regarding either. It's simply not clear. – Houman Jan 17 '23 at 10:03
  • @Houman - I'll try and get back to you. İ need to revisit my code. – awaqar Jan 18 '23 at 13:33

1 Answers1

1

FusedLocationProvider can return a null Location, especially the first time it fetches a Location. So, as you have found, simply returning when the result is null will force requestLocationUpdates() to fire again and actually fetch a valid Location the second time.

I think you can also force FusedLocationProvider to have a Location the first time if you just open the Google Maps app and wait for it to obtain a GPS lock. Of course, you wouldn't want your users to have to perform this process in order for your app to work, but it's still good to know for debugging.

The cause of this seems to be that FusedLocationProvider tries to return the most recently fetched Location for the device. If this Location is too old, has never been fetched, or the user toggled on/off the Location option on their phone, it just returns null.

Gavin Wright
  • 3,124
  • 3
  • 14
  • 35
  • Hi Gavin, Would you please clarify what you mean by `simply returning when the result is null`, please? It's strange that `addOnCompleteListener` returns `it.isSuccessful` as true, but `it.result` remains null. So should I just return in the `addOnCompleteListener`? – Houman Jan 17 '23 at 10:07
  • @Houman The result can indeed be null there. So if you're using `fusedLocationClient.lastLocation()`, you can try my strategy [here ](https://github.com/gavingt/SunCalculator/blob/8d99560ce6b4888488a1b500f7f3ab3193aaeef8/app/src/main/java/com/gavinsappcreations/sunrisesunsettimes/ui/MainActivity.kt#L177) to force a `Location` fetch. Or if you're using `fusedLocationClient.requestLocationUpdates()`, just return immediately and wait for `OnCompleteListener` to fire a second time, as the second time will contain a non-null `Location`. – Gavin Wright Jan 17 '23 at 15:36
  • You can test the solution by turning your phone's Location setting to off, driving a few blocks in your car, then turning the Location setting back on. The first result you get can be null. At least, that's what I remember from when I last dealt with this a few years ago. – Gavin Wright Jan 17 '23 at 15:38
  • Thank you for your response. I checked your solution. If there is no location then you force fetch the last location in a callback and only stop once you have obtained an actual location. But what is the meaning of `return@addOnSuccessListener`. Is this recursively calling `addOnSuccessListener()`? If so, what if the `locationCallback` still hasn't finished, because it runs asynchronously? Does it keep trying in an endless loop? Thanks – Houman Jan 19 '23 at 11:46
  • `return@addOnSuccessListener` simply exits the `addOnSuccessListener` lambda. See [here](https://stackoverflow.com/questions/40160489/kotlin-whats-does-return-mean). But before exiting, we either call `forceFetchLocation()` or we inform the user that their Location settings are disabled. – Gavin Wright Jan 19 '23 at 15:42
  • 1
    `FusedLocationProvider` tries to obtain a user's location in a battery-friendly way. So we attempt to use it first. If it fails by returning a null location, we fall back to using the more expensive `LocationRequest` by calling `forceFetchLocation()`. – Gavin Wright Jan 19 '23 at 15:45