2

Intention: I am trying to get user's current location after user grants (coarse/fine)location permission. I am using jetpack compose accompanist lib to manager permission.

So when user grants the permission, am using getCurrentLocation of FusedLocationProviderClient to get the location object, and fetching lat lang from it.

Problem: In the below code block, Logcat logs: Coordinates[0.0,0.0]

class LocationManager @Inject constructor(
    private val fusedLocation: FusedLocationProviderClient
) {

    @SuppressLint("MissingPermission")
    @Composable
    @ExperimentalPermissionsApi
    fun getLatLang(context: Context): Coordinates {
        val coordinates = Coordinates(0.0, 0.0)

        /**
         * Checking and requesting permission. If granted it will fetch current lat lang,
         * else it will request for permission.
         * If denied, will show popup to open app settings and grant location permission.
         */
        LocationPermissionManager.RequestPermission(
            actionPermissionGranted = {
                fusedLocation.getCurrentLocation(LocationRequest.PRIORITY_HIGH_ACCURACY, null)
                    .addOnSuccessListener { location ->
                        if (location != null) {
                            coordinates.lat = location.latitude
                            coordinates.long = location.longitude
                        }
                    }
            },
            actionPermissionDenied = { context.openAppSystemSettings() }
        )

        return coordinates
    }
}


data class Coordinates(var lat: Double, var long: Double)

Consuming LocationManager below:

@ExperimentalPermissionsApi
@Composable
fun AirQualityLayout(locationManager: LocationManager) {
    val context: Context = LocalContext.current

    val coordinates: Coordinates = locationManager.getLatLang(context = context)

    if (coordinates.lat != 0.0 && coordinates.long != 0.0) {
        Timber.d("Current location: $coordinates")
        ShowUI()
    }
}

Expecting suggestions/help what I am doing wrong here.

Ankush
  • 175
  • 1
  • 12

3 Answers3

1

This serves me well for foreground location updates

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.location.Location
import android.location.LocationManager
import android.os.Looper
import android.provider.Settings
import android.util.Log
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds

private const val TAG = "ForegroundLocationTracker"

private object SimulatedDisposableEffectResult : DisposableEffectResult {
    override fun dispose() {

    }
}

@OptIn(ExperimentalPermissionsApi::class)
private object SimulatedMultiplePermissionsState : MultiplePermissionsState {
    override val allPermissionsGranted: Boolean
        get() = false
    override val permissions: List<PermissionState>
        get() = emptyList()
    override val revokedPermissions: List<PermissionState>
        get() = emptyList()
    override val shouldShowRationale: Boolean
        get() = false

    override fun launchMultiplePermissionRequest() {

    }
}

@OptIn(ExperimentalPermissionsApi::class)
sealed interface LocationPermissionsState {
    @Composable
    operator fun invoke(): MultiplePermissionsState

    object CoarseAndFine : LocationPermissionsState {
        @Composable
        override fun invoke(): MultiplePermissionsState {
            return rememberMultiplePermissionsState(
                permissions = listOf(
                    Manifest.permission.ACCESS_COARSE_LOCATION,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                )
            )
        }
    }

    /**
     * This could be used in a compose preview
     */
    object Simulated : LocationPermissionsState {
        @Composable
        override fun invoke(): MultiplePermissionsState {
            return remember {
                SimulatedMultiplePermissionsState
            }
        }
    }
}

private suspend fun requestToEnableGPS(context: Context, snackbarHostState: SnackbarHostState) {
    val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
    val canNavigateToGPSSettings =
        intent.resolveActivity(context.packageManager) != null

    val result = snackbarHostState.showSnackbar(
        message = "GPS is disabled",
        actionLabel = if (!canNavigateToGPSSettings) {
            null
        } else {
            "ENABLE"
        },
        withDismissAction = true,
        duration = SnackbarDuration.Indefinite,
    )

    when (result) {
        SnackbarResult.Dismissed -> {

        }

        SnackbarResult.ActionPerformed -> {
            if (canNavigateToGPSSettings) {
                context.startActivity(intent)
            }
        }
    }
}

fun isGPSEnabled(context: Context): Boolean {
    val locationManager = context.getSystemService(
        Context.LOCATION_SERVICE
    ) as LocationManager

    return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
            || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}

@OptIn(ExperimentalPermissionsApi::class)
@SuppressLint("MissingPermission")
@Composable
fun ForegroundLocationTracker(
    /**
     * Will be used to show a prompt for enabling GPS if it is disabled
     * or requesting location permissions if non has been granted.
     */
    snackbarHostState: SnackbarHostState,
    permissionsState: LocationPermissionsState = LocationPermissionsState.CoarseAndFine,
    onLocationUpdates: (Location) -> Unit,
) {
    val scope = rememberCoroutineScope()
    val context = LocalContext.current

    val fusedLocationProviderClient = remember {
        LocationServices.getFusedLocationProviderClient(context)
    }

    val permissions = permissionsState()

    var isGPSEnabled by remember {
        mutableStateOf(isGPSEnabled(context))
    }

    LaunchedEffect(true) {
        while (true) {
            isGPSEnabled = isGPSEnabled(context)
            delay(2.seconds)
        }
    }

    DisposableEffect(
        isGPSEnabled,
        permissions.shouldShowRationale,
        permissions.allPermissionsGranted,
    ) {
        if (!permissions.allPermissionsGranted || permissions.shouldShowRationale) {
            scope.launch {
                val result = snackbarHostState.showSnackbar(
                    "Missing required permissions",
                    "Grant",
                    withDismissAction = true,
                    duration = SnackbarDuration.Indefinite,
                )

                when (result) {
                    SnackbarResult.Dismissed -> {

                    }

                    SnackbarResult.ActionPerformed -> {
                        permissions.launchMultiplePermissionRequest()
                    }
                }
            }
            return@DisposableEffect SimulatedDisposableEffectResult
        }

        if (!isGPSEnabled) {
            scope.launch {
                requestToEnableGPS(
                    context = context,
                    snackbarHostState = snackbarHostState,
                )
            }
            return@DisposableEffect SimulatedDisposableEffectResult
        }

        val locationRequest = LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY,
            10000L,
        ).build()

        val locationCallback = object : LocationCallback() {
            override fun onLocationAvailability(p0: LocationAvailability) {
                if (p0.isLocationAvailable) {
                    return super.onLocationAvailability(p0)
                }

                scope.launch {
                    requestToEnableGPS(
                        context = context,
                        snackbarHostState = snackbarHostState,
                    )
                }
            }

            override fun onLocationResult(p0: LocationResult) {
                super.onLocationResult(p0)
                p0.lastLocation?.also(onLocationUpdates)?.also {
                    Log.i(TAG, "Current location: $it")
                }
            }
        }

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

        onDispose {
            fusedLocationProviderClient.removeLocationUpdates(locationCallback)
        }
    }
}

Example usage

@Composable
fun ExampleForegroundLocationTrackerScreen() {
    ForegroundLocationTracker(snackbarHostState = SnackbarHostState()){
        Log.i("LogTag", "Current location is: $it")
    }
}
Mitch
  • 1,556
  • 1
  • 17
  • 17
0

Do you have your manifest right? (Access_fine_location and access_coarse_location

This is a class i made sometime ago:

class LocationLiveData(var context: Context): LiveData<LocationDetails>() {

    //add dependency implementation "com.google.android.gms:play-services-maps:18.0.2"
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)


    override fun onActive() {
        super.onActive()


    
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) { // alse geen permissie hebben just return, anders voer functie location uit

            return
        }
        fusedLocationClient.lastLocation.addOnSuccessListener {
            location -> location.also {
                setLocationData(it)


        }

        }
    }

      internal fun startLocationUpdates() {

        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }
        fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())

    }

    private fun setLocationData(location: Location?) {
      
        location?.let { it ->
            //value is observed in LiveData
            value = LocationDetails(
                longitude = it.longitude.toString(),
                lattitude = it.latitude.toString()
            )

        }
        println("value $value")


    }


    override fun onInactive() {
        super.onInactive()
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)
            println("we have a new location result")
            locationResult ?: return //als er een result is dan prima, zo niet dan just return (elvis operator)
            for (location in locationResult.locations) {
                setLocationData(location = location)

            }
        }
    }

    companion object {

        val ONE_MINUTE: Long = 1000
        @RequiresApi(Build.VERSION_CODES.S)
        val locationRequest : com.google.android.gms.location.LocationRequest = com.google.android.gms.location.LocationRequest.create().apply {
            interval = ONE_MINUTE
            fastestInterval = ONE_MINUTE/4
            priority = LocationRequest.QUALITY_HIGH_ACCURACY
        }


    }

}
saro
  • 705
  • 3
  • 13
  • Yes @saro I have added those two permissions on manifest file. I tried to use your class, but this is using ActivityCompat, however I am using accompanist permission library which is for jetpack compose. Secondly your solution needs min api 31. Whereas I have 26. – Ankush Sep 21 '22 at 05:12
  • I know, but the location logic is the same. Api you are right. – saro Sep 21 '22 at 07:59
0

I've just resolved it with UiState, placing function into ViewModel class

Here my solution:

UiState:

data class MyCityUiState(
    ...
    val currentLocation: Location? = null
    ...
)

update funtion in ViewModel:

    fun updateCurrentLocation(location: Location?) {
        _uiState.update {
            it.copy(
                currentLocation = location
            )
        }
    }

fun that use ".addOnListener":

    @SuppressLint("MissingPermission")
    fun displayDistance(placeLocation: LatLng, context: Context): String? {

        var fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
        fusedLocationClient.lastLocation
            .addOnSuccessListener { location: Location? ->
                if (location != null) {
                    updateCurrentLocation(location)
                }
            }

        var result: Float?
        var formatResult: String? = null

        val placeLocationToLocationType = Location("Place")
        placeLocationToLocationType.latitude = placeLocation.latitude
        placeLocationToLocationType.longitude = placeLocation.longitude

        result = 
        uiState.value.currentLocation?.distanceTo(placeLocationToLocationType)

        if (result != null) {
            formatResult = "%.1f".format(result / 1000) + " km"
        }
        return formatResult
    }
``