implement reverse geocoding for device locations using Nominatim, add distance-based update threshold, and remove isTabletLike from LocationsScreen

This commit is contained in:
PranshulGG committed 2026-05-28 20:38:27 +05:30
1 parent 74c28e6aa8
commit 52690f4fcf
8 files changed
+114 -42

No files matched your search

@@ -0,0 +1,6 @@
package com.pranshulgg.weather_master_app.core.model.domain.location
data class Address(
val city: String?,
val country: String
)
@@ -36,8 +36,8 @@ interface LocationsDao {
@Query("SELECT * FROM weather_locations WHERE isDefault = 1 LIMIT 1")
fun getDefaultLocation(): Flow<WeatherLocationEntity?>
@Query("UPDATE weather_locations SET lat = :lat, lon = :lon WHERE isDeviceLocation = 1")
suspend fun updateDeviceLocationPosition(lat: Double, lon: Double)
@Query("UPDATE weather_locations SET lat = :lat, lon = :lon, name = :name, country = :country WHERE isDeviceLocation = 1")
suspend fun updateDeviceLocation(lat: Double, lon: Double, name: String, country: String)
@Transaction
@Query("SELECT * FROM weather_locations WHERE id = :locationId LIMIT 1")
@@ -62,4 +62,7 @@ interface LocationsDao {
@Query("SELECT * FROM weather_locations ORDER BY isDefault DESC")
suspend fun getLocationsOnce(): List<WeatherLocationEntity>
@Query("SELECT * FROM weather_locations WHERE isDeviceLocation = 1 LIMIT 1")
suspend fun getDeviceLocation(): WeatherLocationEntity
}
@@ -32,13 +32,9 @@ class GetDeviceLocation {
private var timeoutHandler: Handler? = null
private var timeoutRunnable: Runnable? = null
private val timeoutSecs = 10_000L // 10 seconds
private val timeoutMillis = 20_000L
private fun getProvider(lm: LocationManager): String {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
lm.allProviders.contains(LocationManager.FUSED_PROVIDER) -> LocationManager.FUSED_PROVIDER
lm.allProviders.contains(LocationManager.NETWORK_PROVIDER) -> LocationManager.NETWORK_PROVIDER
lm.allProviders.contains(LocationManager.GPS_PROVIDER) -> LocationManager.GPS_PROVIDER
else -> LocationManager.PASSIVE_PROVIDER
@@ -95,6 +91,9 @@ class GetDeviceLocation {
) {
val lastKnown = lm.getLastKnownLocation(provider)
?: lm.getLastKnownLocation(LocationManager.GPS_PROVIDER)
?: lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
?: lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
if (lastKnown != null) {
timeoutHandler?.removeCallbacks(timeoutRunnable!!)
@@ -127,9 +126,10 @@ class GetDeviceLocation {
lm.requestLocationUpdates(
provider,
5000L,
5f,
locationListener!!
0L,
0f,
locationListener!!,
Looper.getMainLooper()
)
timeoutHandler = Handler(Looper.getMainLooper())
@@ -139,7 +139,7 @@ class GetDeviceLocation {
onTimeout()
}
timeoutHandler?.postDelayed(timeoutRunnable!!, timeoutSecs)
timeoutHandler?.postDelayed(timeoutRunnable!!, timeoutMillis)
}
@@ -1,11 +1,13 @@
package com.pranshulgg.weather_master_app.data.repository
import android.content.Context
import android.util.Log
import androidx.room.Transaction
import com.pranshulgg.weather_master_app.core.model.domain.AppException
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.json.NominatimRepository
import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao
import com.pranshulgg.weather_master_app.data.local.mapper.locations.toDomain
@@ -17,16 +19,20 @@ import com.pranshulgg.weather_master_app.feature.intro.toDomain
import dagger.hilt.android.qualifiers.ApplicationContext
import jakarta.inject.Inject
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resumeWithException
class LocationsRepository @Inject constructor(
private val dao: LocationsDao,
private val airQualityDao: AirQualityDao,
@param:ApplicationContext private val context: Context
@param:ApplicationContext private val context: Context,
private val nominatimRepository: NominatimRepository
) {
private val LOCATION_UPDATE_THRESHOLD_METERS = 1000f // 1000m
class Callbacks {
fun onSuccess(cont: CancellableContinuation<DeviceLocation>, result: DeviceLocation) {
@@ -93,10 +99,12 @@ class LocationsRepository @Inject constructor(
return dao.getWeatherForLocation(locationId).toDomain()
}
val getDeviceLocation = GetDeviceLocation()
suspend fun updateDeviceLocationPosition() {
val location = suspendCancellableCoroutine { cont ->
GetDeviceLocation().getDeviceLocation(
getDeviceLocation.getDeviceLocation(
context,
onTimeout = {
callback.onTimeout(cont)
@@ -105,20 +113,43 @@ class LocationsRepository @Inject constructor(
}
}
if (location.latitude == null || location.longitude == null) {
val newLat = location.latitude ?: return
val newLon = location.longitude ?: return
val currentLocation = dao.getDeviceLocation()
val results = FloatArray(1)
android.location.Location.distanceBetween(
currentLocation.lat,
currentLocation.lon,
newLat,
newLon,
results
)
val distanceInMeters = results[0]
// Only update the location if needed
if (distanceInMeters < LOCATION_UPDATE_THRESHOLD_METERS) {
return
}
val address = nominatimRepository.getAddress(location.latitude, location.longitude)
val formattedLatitude = kotlin.math.round(location.latitude * 100000) / 100000
val formattedLongitude = kotlin.math.round(location.longitude * 100000) / 100000
dao.updateDeviceLocationPosition(formattedLatitude, formattedLongitude)
dao.updateDeviceLocation(
newLat,
newLon,
address?.city ?: "$newLat, $newLon",
address?.country ?: ""
)
}
suspend fun saveDeviceLocation() {
val location = suspendCancellableCoroutine { cont ->
GetDeviceLocation().getDeviceLocation(
getDeviceLocation.getDeviceLocation(
context,
onTimeout = {
callback.onTimeout(cont)
@@ -129,7 +160,22 @@ class LocationsRepository @Inject constructor(
if (location.latitude == null || location.longitude == null) {
throw AppException.CurrentLocationUnavailable()
}
saveLocation(location.toDomain(context))
val address = nominatimRepository.getAddress(location.latitude, location.longitude)
if (address != null && address.city != null) {
saveLocation(
location.toDomain(context).copy(
name = address.city,
country = address.country
)
)
} else {
saveLocation(
location.toDomain(context)
)
}
}
fun getWeatherForAllLocations(): Flow<List<Weather>> {
@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.domain.location.Address
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.ui.components.Gap
@@ -267,7 +268,7 @@ suspend fun DeviceLocation.toDomain(context: Context): Location {
return Location(
id = UuidGenerator.generateId(),
name = "$formattedLatitude, $formattedLongitude", // TODO: Reverse geocoding support
name = "$formattedLatitude, $formattedLongitude",
latitude = formattedLatitude,
longitude = formattedLongitude,
country = "",
@@ -1,9 +1,11 @@
package com.pranshulgg.weather_master_app.feature.intro
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.json.NominatimRepository
import com.pranshulgg.weather_master_app.data.provider.devicelocation.DeviceLocation
import com.pranshulgg.weather_master_app.data.repository.LocationsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -15,13 +17,28 @@ import javax.inject.Inject
class IntroScreenViewModel @Inject constructor(
val locationsRepo: LocationsRepository,
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
private val nominatimRepository: NominatimRepository
) : ViewModel() {
fun saveDeviceLocation(location: DeviceLocation) {
viewModelScope.launch {
locationsRepo.saveLocation(location.toDomain(context))
val address = nominatimRepository.getAddress(location.latitude, location.longitude)
if (address != null && address.city != null) {
locationsRepo.saveLocation(
location.toDomain(context).copy(
name = address.city,
country = address.country
)
)
} else {
locationsRepo.saveLocation(location.toDomain(context))
}
}
}
}
@@ -61,8 +61,7 @@ fun LocationsScreen(
navController: NavController,
locations: List<Location>,
activeLocation: Location?,
onLocationSelect: (Location) -> Unit,
isTabletLike: Boolean = false
onLocationSelect: (Location) -> Unit
) {
val viewModel: LocationsScreenViewModel = hiltViewModel()
@@ -102,10 +101,9 @@ fun LocationsScreen(
Scaffold(
modifier = if (isTabletLike) Modifier.width(360.dp) else Modifier,
containerColor = MaterialTheme.colorScheme.surfaceContainer,
topBar = {
TopBar(onBack, isTabletLike)
TopBar(onBack)
},
floatingActionButton = {
FloatingButton(navController)
@@ -156,7 +154,7 @@ fun LocationsScreen(
}
@Composable
private fun TopBar(onBack: () -> Unit, isTabletLike: Boolean) {
private fun TopBar(onBack: () -> Unit) {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
title = {
@@ -167,21 +165,19 @@ private fun TopBar(onBack: () -> Unit, isTabletLike: Boolean) {
)
},
navigationIcon = {
if (!isTabletLike) {
Tooltip(
"Navigate up",
preferredPosition = TooltipAnchorPosition.Below,
spacing = 10.dp
Tooltip(
"Navigate up",
preferredPosition = TooltipAnchorPosition.Below,
spacing = 10.dp
) {
IconButton(
onClick = { onBack() }, shapes = IconButtonDefaults.shapes()
) {
IconButton(
onClick = { onBack() }, shapes = IconButtonDefaults.shapes()
) {
Symbol(
R.drawable.arrow_back_24px,
desc = "arrow back icon",
color = MaterialTheme.colorScheme.onSurface
)
}
Symbol(
R.drawable.arrow_back_24px,
desc = "arrow back icon",
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
@@ -1,9 +1,11 @@
package com.pranshulgg.weather_master_app.feature.locations
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.util.Logger
import com.pranshulgg.weather_master_app.core.model.domain.AppException
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.toMessageRes
@@ -63,6 +65,7 @@ class LocationsScreenViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isDeviceLocationLoading = true)
viewModelScope.launch {
try {
Log.d("COOL HERE", "CALLED HERE HERE")
locationsRepo.saveDeviceLocation()
} catch (e: Exception) {
if (e is CancellationException) throw e