mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-22 11:02:53 -05:00
implement reverse geocoding for device locations using Nominatim, add distance-based update threshold, and remove isTabletLike from LocationsScreen
This commit is contained in:
1 parent
74c28e6aa8
commit
52690f4fcf
8 files changed
+114
-42
No files matched your search
+6
@@ -0,0 +1,6 @@
|
||||
package com.pranshulgg.weather_master_app.core.model.domain.location
|
||||
|
||||
data class Address(
|
||||
val city: String?,
|
||||
val country: String
|
||||
)
|
||||
+5
-2
@@ -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
|
||||
}
|
||||
+9
-9
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
+55
-9
@@ -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 = "",
|
||||
|
||||
+19
-2
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
-19
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
Reference in new issue
Block a user