diff --git a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt index c51cf23..23355e6 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt @@ -193,65 +193,74 @@ class MainActivity : AppCompatActivity(), SensorEventListener { } binding.cacheRetryButton.setOnClickListener { - currentLocation?.let { loc -> - if (isNetworkAvailable()) { - startCaching(loc.latitude, loc.longitude) - } else { - Toast.makeText(this, R.string.error_download_failed, Toast.LENGTH_SHORT).show() - } + val loc = currentLocation + if (loc == null) { + Toast.makeText(this, R.string.status_no_location, Toast.LENGTH_SHORT).show() + } else if (!isNetworkAvailable()) { + Toast.makeText(this, R.string.error_download_failed, Toast.LENGTH_SHORT).show() + } else { + startCaching(loc.latitude, loc.longitude) } } } private fun loadData() { lifecycleScope.launch { - val hasData = repository.hasCachedData() + try { + val hasData = repository.hasCachedData() - if (!hasData) { - if (!isNetworkAvailable()) { - binding.statusText.text = getString(R.string.error_no_data_offline) - return@launch - } - showLoading(getString(R.string.loading_shelters)) - val success = repository.refreshData() - hideLoading() - - if (!success) { - binding.statusText.text = getString(R.string.error_download_failed) - return@launch - } - } - - // Observe shelter data reactively - launch { - try { - repository.getAllShelters().collectLatest { shelters -> - allShelters = shelters - binding.statusText.text = getString(R.string.status_shelters_loaded, shelters.size) - updateShelterMarkers() - currentLocation?.let { updateNearestShelters(it) } + if (!hasData) { + if (!isNetworkAvailable()) { + binding.statusText.text = getString(R.string.error_no_data_offline) + return@launch } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Log.e(TAG, "Error observing shelter data", e) - binding.statusText.text = getString(R.string.error_download_failed) - } - } - - // Request location and start updates - requestLocationPermission() - - // Check for stale data in background - if (hasData && repository.isDataStale() && isNetworkAvailable()) { - launch { + showLoading(getString(R.string.loading_shelters)) val success = repository.refreshData() - if (success) { - Toast.makeText(this@MainActivity, R.string.update_success, Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this@MainActivity, R.string.update_failed, Toast.LENGTH_SHORT).show() + hideLoading() + + if (!success) { + binding.statusText.text = getString(R.string.error_download_failed) + return@launch } } + + // Observe shelter data reactively + launch { + try { + repository.getAllShelters().collectLatest { shelters -> + allShelters = shelters + binding.statusText.text = getString(R.string.status_shelters_loaded, shelters.size) + updateShelterMarkers() + currentLocation?.let { updateNearestShelters(it) } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.e(TAG, "Error observing shelter data", e) + binding.statusText.text = getString(R.string.error_download_failed) + } + } + + // Request location and start updates + requestLocationPermission() + + // Check for stale data in background + if (hasData && repository.isDataStale() && isNetworkAvailable()) { + launch { + val success = repository.refreshData() + if (success) { + Toast.makeText(this@MainActivity, R.string.update_success, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this@MainActivity, R.string.update_failed, Toast.LENGTH_SHORT).show() + } + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize shelter data", e) + hideLoading() + binding.statusText.text = getString(R.string.error_download_failed) } } } @@ -384,7 +393,12 @@ class MainActivity : AppCompatActivity(), SensorEventListener { ) ) } else { - ShelterWithDistance(shelter = shelter, distanceMeters = 0.0, bearingDegrees = 0.0) + // No GPS yet — use NaN to signal "unknown distance" + ShelterWithDistance( + shelter = shelter, + distanceMeters = Double.NaN, + bearingDegrees = Double.NaN + ) } userSelectedShelter = true diff --git a/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterGeoJsonParser.kt b/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterGeoJsonParser.kt index 92a34f6..1002912 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterGeoJsonParser.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterGeoJsonParser.kt @@ -91,6 +91,14 @@ object ShelterGeoJsonParser { continue } + // Require a valid primary key — without it shelters can collide in the DB + val lokalId = properties.optString("lokalId", null) + if (lokalId.isNullOrBlank()) { + Log.w(TAG, "Skipping shelter at index $i: missing lokalId") + skipped++ + continue + } + val plasser = properties.optInt("plasser", 0) if (plasser < 0) { Log.w(TAG, "Skipping shelter at index $i: negative capacity ($plasser)") @@ -100,7 +108,7 @@ object ShelterGeoJsonParser { shelters.add( Shelter( - lokalId = properties.optString("lokalId", "unknown-$i"), + lokalId = lokalId, romnr = properties.optInt("romnr", 0), plasser = plasser, adresse = properties.optString("adresse", ""), diff --git a/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt b/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt index c4a350a..9a378af 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt @@ -20,6 +20,9 @@ import kotlinx.coroutines.flow.callbackFlow /** * Provides GPS location updates using the Fused Location Provider. * Emits location updates as a Flow for reactive consumption. + * + * Closes the Flow with a SecurityException if location permission is not granted, + * so callers can detect and handle this failure explicitly. */ class LocationProvider(private val context: Context) { @@ -34,22 +37,28 @@ class LocationProvider(private val context: Context) { /** * Stream of location updates. Emits the last known location first (if available), - * then continuous updates. + * then continuous updates. Throws SecurityException if permission is not granted. */ fun locationUpdates(): Flow = callbackFlow { if (!hasLocationPermission()) { - Log.w(TAG, "Location permission not granted") - close() + close(SecurityException("Location permission not granted")) return@callbackFlow } // Try to get last known location for immediate display try { - fusedClient.lastLocation.addOnSuccessListener { location -> - if (location != null) { - trySend(location) + fusedClient.lastLocation + .addOnSuccessListener { location -> + if (location != null) { + val result = trySend(location) + if (result.isFailure) { + Log.w(TAG, "Failed to emit last known location") + } + } + } + .addOnFailureListener { e -> + Log.w(TAG, "Could not get last known location", e) } - } } catch (e: SecurityException) { Log.e(TAG, "SecurityException getting last location", e) } @@ -64,7 +73,12 @@ class LocationProvider(private val context: Context) { val callback = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { - result.lastLocation?.let { trySend(it) } + result.lastLocation?.let { location -> + val sendResult = trySend(location) + if (sendResult.isFailure) { + Log.w(TAG, "Failed to emit location update") + } + } } } @@ -76,7 +90,7 @@ class LocationProvider(private val context: Context) { ) } catch (e: SecurityException) { Log.e(TAG, "SecurityException requesting location updates", e) - close() + close(e) return@callbackFlow } diff --git a/app/src/main/java/no/naiv/tilfluktsrom/util/DistanceUtils.kt b/app/src/main/java/no/naiv/tilfluktsrom/util/DistanceUtils.kt index 3e66239..4fb3343 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/util/DistanceUtils.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/util/DistanceUtils.kt @@ -38,6 +38,7 @@ object DistanceUtils { * Format distance for display: meters if <1km, otherwise km with one decimal. */ fun formatDistance(meters: Double): String { + if (meters.isNaN()) return "—" return if (meters < 1000) { "${meters.toInt()} m" } else {